feat(mobile): iOS polish — theme-color, safe-area, dvh, tap-highlight
Build and Push Docker Images / Build Backend (FastAPI) (push) Successful in 12s
Build and Push Docker Images / Build Frontend (Next.js) (push) Successful in 52s
Build and Push Docker Images / Build Pipeline (Meltano + dbt + Airflow) (push) Successful in 12s
Build and Push Docker Images / Trigger Portainer Update (push) Successful in 1s

MOB-19: Add a viewport Viewport export with viewportFit: 'cover' and
themeColor entries for light (#faf7f2) / dark (#1a1612), plus the
appleWebApp metadata for the home-screen status bar style and title.
Manifest's stale #3b82f6 theme_color updated to match brand cream.

MOB-20: Apply env(safe-area-inset-*) to the sticky chrome — the top
header gets max(padding, inset-left/right) so the logo and tab links
clear the notch in landscape; the bottom tab bar already had
inset-bottom and now also gets inset-left/right.

MOB-21: Replace 100vh with 100dvh in body min-height, modal max-heights,
the map view container, and the fullscreen map. Older engines fall
back via the duplicated vh declaration.

MOB-22: Set -webkit-tap-highlight-color: transparent on body to
suppress the iOS Safari grey flash; add a generic touch-pointer
:active rule (opacity 0.7) so taps still register visually on plain
anchors and bare buttons. Components with their own :active styling
are unaffected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Tudor Sitaru
2026-05-19 09:47:13 +01:00
parent 56ab1368b1
commit 9133ecdcd4
6 changed files with 49 additions and 10 deletions
+19 -2
View File
@@ -92,7 +92,24 @@ body {
background: var(--bg-primary); background: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
line-height: 1.6; line-height: 1.6;
/* dvh (dynamic viewport) accounts for iOS Safari's collapsing toolbar;
fall back to vh on older engines that don't recognise dvh. */
min-height: 100vh; min-height: 100vh;
min-height: 100dvh;
/* Suppress the iOS Safari grey tap flash — explicit :active states
below carry the press feedback instead. */
-webkit-tap-highlight-color: transparent;
}
/* Provide a baseline press feedback for the most common interactive
elements — replaces the suppressed default tap highlight. Buttons and
.btn-* classes carry their own :active styles already; this handles
plain anchors used as inline links and bare button elements. */
@media (hover: none) and (pointer: coarse) {
a:active,
button:active {
opacity: 0.7;
}
} }
/* Reserve space for the fixed mobile bottom tab bar (56px + safe-area inset). */ /* Reserve space for the fixed mobile bottom tab bar (56px + safe-area inset). */
@@ -1766,7 +1783,7 @@ body {
.modal-content { .modal-content {
margin: 1rem; margin: 1rem;
max-height: calc(100vh - 2rem); max-height: calc(100dvh - 2rem);
} }
.modal-header { .modal-header {
@@ -1836,7 +1853,7 @@ body {
@media (max-width: 480px) { @media (max-width: 480px) {
.modal-content { .modal-content {
margin: 0.5rem; margin: 0.5rem;
max-height: calc(100vh - 1rem); max-height: calc(100dvh - 1rem);
} }
.modal-header { .modal-header {
+18 -1
View File
@@ -1,4 +1,4 @@
import type { Metadata } from 'next'; import type { Metadata, Viewport } from 'next';
import { DM_Sans, Playfair_Display } from 'next/font/google'; import { DM_Sans, Playfair_Display } from 'next/font/google';
import Script from 'next/script'; import Script from 'next/script';
import { Navigation } from '@/components/Navigation'; import { Navigation } from '@/components/Navigation';
@@ -21,7 +21,24 @@ const playfairDisplay = Playfair_Display({
display: 'swap', display: 'swap',
}); });
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
// viewport-fit=cover lets us paint behind the notch / Dynamic Island so
// env(safe-area-inset-*) values resolve to real numbers on iPhone.
viewportFit: 'cover',
themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#faf7f2' },
{ media: '(prefers-color-scheme: dark)', color: '#1a1612' },
],
};
export const metadata: Metadata = { export const metadata: Metadata = {
appleWebApp: {
capable: true,
title: 'SchoolCompare',
statusBarStyle: 'default',
},
title: { title: {
default: 'SchoolCompare | Compare School Performance', default: 'SchoolCompare | Compare School Performance',
template: '%s | SchoolCompare', template: '%s | SchoolCompare',
+2 -2
View File
@@ -142,7 +142,7 @@
display: grid; display: grid;
grid-template-columns: 1fr 340px; grid-template-columns: 1fr 340px;
gap: 1rem; gap: 1rem;
height: calc(100vh - 280px); height: calc(100dvh - 280px);
min-height: 520px; min-height: 520px;
max-height: 800px; max-height: 800px;
} }
@@ -467,7 +467,7 @@
.mapViewContainer { .mapViewContainer {
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: 1fr; grid-template-rows: 1fr;
height: calc(100vh - 280px); height: calc(100dvh - 280px);
min-height: 400px; min-height: 400px;
} }
+6 -2
View File
@@ -10,7 +10,9 @@
.container { .container {
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
padding: 0 1.5rem; /* Add the safe-area inset to horizontal padding so the header content
clears the notch in landscape on iPhones. */
padding-inline: max(1.5rem, env(safe-area-inset-left)) max(1.5rem, env(safe-area-inset-right));
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -231,8 +233,10 @@
background: var(--bg-card, white); background: var(--bg-card, white);
border-top: 1px solid var(--border-color, #e5dfd5); border-top: 1px solid var(--border-color, #e5dfd5);
box-shadow: 0 -2px 12px rgba(26, 22, 18, 0.06); box-shadow: 0 -2px 12px rgba(26, 22, 18, 0.06);
/* Respect iPhone home-indicator inset */ /* Respect iPhone home-indicator (bottom) and notch (left/right in
landscape) insets so the tab content never sits under system UI. */
padding-bottom: env(safe-area-inset-bottom, 0); padding-bottom: env(safe-area-inset-bottom, 0);
padding-inline: env(safe-area-inset-left, 0) env(safe-area-inset-right, 0);
/* Compensate for iOS Chrome's auto-hiding URL bar — Navigation.tsx /* Compensate for iOS Chrome's auto-hiding URL bar — Navigation.tsx
writes the offset based on the Visual Viewport API. translate3d writes the offset based on the Visual Viewport API. translate3d
(instead of translateY) forces hardware compositing so the bar (instead of translateY) forces hardware compositing so the bar
@@ -7,6 +7,7 @@
.mapWrapper.fullscreen { .mapWrapper.fullscreen {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
height: 100dvh;
} }
.fullscreenBtn { .fullscreenBtn {
+3 -3
View File
@@ -1,11 +1,11 @@
{ {
"name": "SchoolCompare", "name": "SchoolCompare",
"short_name": "SchoolCompare", "short_name": "SchoolCompare",
"description": "Compare primary school KS2 performance across England", "description": "Compare primary and secondary school performance across England",
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"background_color": "#ffffff", "background_color": "#faf7f2",
"theme_color": "#3b82f6", "theme_color": "#faf7f2",
"icons": [ "icons": [
{ {
"src": "/favicon.svg", "src": "/favicon.svg",