Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@

---

## 📱 1er octobre 2026 — Le haut de l'app installée sur iPhone, deuxième essai

*App installée sur l'écran d'accueil · iOS 26 · retour sur un iPhone 14*

- Le correctif du 29 septembre (une bande opaque derrière la barre d'état) n'a pas suffi : retesté sur l'iPhone, le flou d'iOS 26 recouvrait toujours le haut de l'app.
- Ce flou est dessiné par iOS lui-même, au-dessus de la page, et aucun réglage ne l'enlève. Dans l'app installée sur iPhone, le contenu du haut (barre de navigation, menu, fenêtre de connexion, tuteur IA) démarre maintenant 2,5rem plus bas, sous la zone floue, et la bande opaque couvre ce même espace.
- Rien ne change dans Safari ni sur ordinateur. L'app installée tenue à l'horizontale garde l'ancien espacement.

---

## 🤝 1er octobre 2026 — Le module GitHub devient une suite de gestes

*6 leçons · exercices en étapes · sorties vérifiées sur Git 2.56 et PowerShell 7*
Expand Down
10 changes: 10 additions & 0 deletions STORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,16 @@ Six validateurs de plus ont disparu. Les écarts entre la théorie et le termina

---

### Le flou qu'aucun test ne voit (1er octobre 2026)

Le 29 septembre, j'avais corrigé le flou en haut de l'app installée sur iPhone avec une bande opaque derrière la barre d'état. Plusieurs projets décrivaient cette astuce, les tests passaient, la page s'affichait bien dans tous mes navigateurs. Thierry a rouvert l'app sur son iPhone : le flou était toujours là.

Ce défaut n'existe que dans une app installée, sur un vrai iPhone. Aucun navigateur de test ne le reproduit, donc aucun test automatique ne pouvait me prévenir. En relisant les correctifs des autres projets, j'ai vu ce qui m'avait échappé : la plupart précisaient eux-mêmes qu'ils n'avaient pas été vérifiés sur un appareil. Un seul l'avait été, et il ne combat pas le flou : il en éloigne le contenu.

C'est la version appliquée. Dans l'app installée sur iPhone, le haut de l'app descend de 2,5rem, et le flou ne recouvre plus qu'une bande de couleur unie. Le verdict reste celui de l'iPhone de Thierry, pas celui des tests.

---

## Épilogue ouvert

Il y a des questions auxquelles on n'a pas encore de réponse.
Expand Down
11 changes: 6 additions & 5 deletions src/app/components/Landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,17 @@ export function Landing() {
)}

{/* ── NAV ─────────────────────────────────────────────────── */}
{/* THI-152 brick 7bis: pt-[max(1rem,env(safe-area-inset-top))]
{/* THI-152 brick 7bis: pt-[max(1rem,var(--inset-top))]
shifts the nav (Terminal logo + GitHub + Login + "Commencer →")
below the iOS status bar in PWA standalone mode (Add to Home
Screen). On Safari classic mobile and on desktop, env() resolves
to 0 → max(1rem, 0) = 1rem (= py-4 baseline, no regression).
below the iOS status bar and its edge blur in PWA standalone mode
(Add to Home Screen; --inset-top in theme.css). On Safari classic
mobile and on desktop, it resolves to 0 → max(1rem, 0) = 1rem
(= py-4 baseline, no regression).
Pattern matches the footer (line 593) `pb-[max(2rem,env(...))]`.
Layout.tsx already handles its own mobile top bar via the flex-1
wrapper safe-area paddings (mini-PR 7/9), but Landing does NOT
use Layout — it has its own nav, hence this dedicated fix. */}
<nav className="border-b border-[var(--github-border-primary)]/50 px-4 sm:px-6 pb-4 pt-[max(1rem,env(safe-area-inset-top))] flex items-center justify-between max-w-6xl mx-auto">
<nav className="border-b border-[var(--github-border-primary)]/50 px-4 sm:px-6 pb-4 pt-[max(1rem,var(--inset-top))] flex items-center justify-between max-w-6xl mx-auto">
<div className="flex items-center gap-2 shrink-0">
<div className="p-1.5 rounded-lg bg-emerald-500/10 border border-emerald-500/20">
<Terminal size={18} className="text-emerald-400" aria-hidden="true" />
Expand Down
8 changes: 5 additions & 3 deletions src/app/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ export function Layout() {
<div className="h-dvh flex overflow-hidden bg-[var(--github-bg)]" style={{ fontFamily: 'Inter, sans-serif' }}>
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />

{/* THI-152 brick 7/9: pt/pl/pr env(safe-area-inset-*) on the flex-1
wrapper shifts both the mobile top bar AND the main area below
{/* THI-152 brick 7/9: the top padding (--inset-top, theme.css: the
safe-area inset, plus a clearance below iOS 26's edge blur in the
installed app) and pl/pr env(safe-area-inset-*) on the flex-1
wrapper shift both the mobile top bar AND the main area below
the iOS status bar in PWA standalone mode (Add-to-Home-Screen).
Without this, `apple-mobile-web-app-status-bar-style:
black-translucent` (set in index.html) lets wifi/battery/signal
icons overlay the top-bar content. On non-standalone Safari and
on desktop, env(safe-area-inset-*) resolves to 0 → no shift.
Landscape iPhone: pl/pr handle the notch on the rotated side. */}
<div className="flex-1 flex flex-col min-w-0 overflow-hidden pt-[env(safe-area-inset-top)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]">
<div className="flex-1 flex flex-col min-w-0 overflow-hidden pt-[var(--inset-top)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]">
{/* Mobile top bar */}
<div className="lg:hidden shrink-0 h-14 border-b border-[var(--github-border-primary)] bg-[var(--github-border-secondary)] flex items-center gap-3 px-3">
<MenuButton onClick={() => setSidebarOpen(true)} />
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/MarkdownPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ export function MarkdownPage({ content, title, subtitle, seo }: MarkdownPageProp
<meta name="twitter:creator" content="@thierryvm" />
</Helmet>
{/* Nav */}
<nav className="border-b border-[var(--github-border-primary)]/50 px-6 py-4 flex items-center justify-between max-w-4xl mx-auto">
<nav className="border-b border-[var(--github-border-primary)]/50 px-6 pb-4 pt-[max(1rem,var(--inset-top))] flex items-center justify-between max-w-4xl mx-auto">
<div className="flex items-center gap-2.5">
<Terminal size={18} className="text-emerald-400" />
<span className="font-mono text-sm text-[var(--github-text-primary)]">Terminal Learning</span>
Expand Down
8 changes: 4 additions & 4 deletions src/app/components/PWAInstallModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ export function PWAInstallModal({ onClose }: PWAInstallModalProps) {

if (isInstalled) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" onClick={onClose}>
<div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto p-4 pt-[max(1rem,var(--inset-top))] bg-black/60 backdrop-blur-sm" onClick={onClose}>
<div
className="bg-[var(--github-border-secondary)] border border-[var(--github-border-primary)] rounded-xl p-8 max-w-sm w-full text-center shadow-2xl"
className="bg-[var(--github-border-secondary)] border border-[var(--github-border-primary)] rounded-xl p-8 max-w-sm w-full my-auto text-center shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<CheckCircle2 size={48} className="text-emerald-400 mx-auto mb-4" />
Expand All @@ -70,9 +70,9 @@ export function PWAInstallModal({ onClose }: PWAInstallModalProps) {
}

return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" onClick={onClose}>
<div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto p-4 pt-[max(1rem,var(--inset-top))] bg-black/60 backdrop-blur-sm" onClick={onClose}>
<div
className="bg-[var(--github-border-secondary)] border border-[var(--github-border-primary)] rounded-xl w-full max-w-md shadow-2xl"
className="bg-[var(--github-border-secondary)] border border-[var(--github-border-primary)] rounded-xl w-full max-w-md my-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/PrivacyPolicy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function PrivacyPolicy() {
return (
<div className="min-h-dvh bg-[var(--github-bg)] text-[var(--github-text-primary)]" style={{ fontFamily: 'Inter, sans-serif' }}>
{/* Nav */}
<nav className="border-b border-[var(--github-border-primary)]/50 px-6 py-4 flex items-center justify-between max-w-4xl mx-auto">
<nav className="border-b border-[var(--github-border-primary)]/50 px-6 pb-4 pt-[max(1rem,var(--inset-top))] flex items-center justify-between max-w-4xl mx-auto">
<div className="flex items-center gap-2.5">
<Terminal size={18} className="text-emerald-400" />
<span className="font-mono text-sm text-[var(--github-text-primary)]">Terminal Learning</span>
Expand Down
5 changes: 3 additions & 2 deletions src/app/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,9 @@ export function Sidebar({ isOpen, onClose }: SidebarProps) {
// iPhone with the notch on the rotated side, the inset is ~44 px
// and the sidebar content shifts inward so it does not get
// clipped by the notch. Pattern matches the brick 7bis Landing
// nav `max(...,env())` baseline-preservation idiom.
className={`fixed lg:static inset-y-0 left-0 z-40 w-72 bg-[var(--github-bg)] border-r border-[var(--github-border-primary)] flex flex-col transition-transform duration-300 pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)] pl-[max(0px,env(safe-area-inset-left))] focus:outline-none ${
// nav `max(...,env())` baseline-preservation idiom. The top
// padding is --inset-top (theme.css), below iOS 26's edge blur.
className={`fixed lg:static inset-y-0 left-0 z-40 w-72 bg-[var(--github-bg)] border-r border-[var(--github-border-primary)] flex flex-col transition-transform duration-300 pt-[var(--inset-top)] pb-[env(safe-area-inset-bottom)] pl-[max(0px,env(safe-area-inset-left))] focus:outline-none ${
isOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
}`}
>
Expand Down
18 changes: 12 additions & 6 deletions src/app/components/StatusBarBackdrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@
* iOS 26 lays its Liquid Glass edge blur over the top of the app, about 40pt
* past the status bar: the top bar and the landing nav (GitHub, login) sat
* inside that blur and looked like they slid under the clock (reported by
* @thierry on an iPhone 14, 29 September 2026). iOS leaves the edge alone
* when a fixed box with a background covers the top edge, and shows that
* box's color instead. This strip is that box, in the app's background color.
* @thierry on an iPhone 14, 29 September 2026). Several apps report that iOS
* leaves the edge alone under a fixed box with a background; on that iPhone it
* did not. So the content starts below the blur (--inset-top), and this strip,
* in the app's background color, fills the band above it.
*
* Its height is env(safe-area-inset-top): 0 in a Safari tab and on desktop,
* so it only exists where the status bar overlaps the page.
* Its height is --inset-top (theme.css): 0 in a Safari tab and on desktop, so
* it only exists where the status bar overlaps the page. In the installed app
* on iOS it also covers the 2.5rem clearance kept below the status bar, since
* the strip alone, as tall as the status bar, did not stop the blur on the
* device (retested 1 October 2026): what scrolls up under the blur is then
* this plain color, never text. z-[25]: above the page, under the dimmed
* overlay (z-30) and the drawer (z-40) of the mobile sidebar.
*/
export function StatusBarBackdrop() {
return (
<div
aria-hidden="true"
data-testid="status-bar-backdrop"
className="pointer-events-none fixed inset-x-0 top-0 z-[45] h-[env(safe-area-inset-top)] bg-[var(--github-bg)]"
className="pointer-events-none fixed inset-x-0 top-0 z-[25] h-[var(--inset-top)] bg-[var(--github-bg)]"
/>
);
}
2 changes: 1 addition & 1 deletion src/app/components/ai/AiTutorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export function AiTutorPanel({ lang = 'fr', lessonContext, role, liftAboveMobile
// Mobile: full-screen drawer with safe-area padding (iOS notch + home bar).
// Desktop (md+): floating bottom-right card.
style={{
paddingTop: 'env(safe-area-inset-top, 0px)',
paddingTop: 'var(--inset-top)',
paddingBottom: 'env(safe-area-inset-bottom, 0px)',
}}
// THI-152 brick 6/9: `overflow-x-hidden max-w-full` is a
Expand Down
6 changes: 4 additions & 2 deletions src/app/components/auth/LoginModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,15 +189,17 @@ export function LoginModal({ open, onClose }: LoginModalProps) {
// backdrop ensures the centered modal does not get clipped by iOS
// status bar / home indicator / notch in PWA standalone mode. The
// flex centering remains, but happens inside the safe-area inset.
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]"
// The card centers with my-auto, so a card taller than the screen
// (keyboard open, age gate) scrolls from its top instead of being cut.
className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/60 backdrop-blur-sm pt-[var(--inset-top)] pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]"
onClick={(e) => { if (e.target === e.currentTarget) handleClose(); }}
>
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="login-modal-title"
className="bg-[var(--github-border-secondary)] border border-[var(--github-border-primary)] rounded-xl p-6 w-full max-w-sm mx-4 shadow-2xl"
className="bg-[var(--github-border-secondary)] border border-[var(--github-border-primary)] rounded-xl p-6 w-full max-w-sm mx-4 my-auto shadow-2xl"
>
<div className="flex justify-between items-center mb-6">
<h2 id="login-modal-title" className="text-lg font-semibold text-[var(--github-text-primary)] font-mono">
Expand Down
4 changes: 2 additions & 2 deletions src/app/components/support/SupportTicketModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export function SupportTicketModal({ userId, onClose }: SupportTicketModalProps)

return (
<div
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto p-4 pt-[max(1rem,var(--inset-top))] bg-black/60 backdrop-blur-sm"
onClick={handleBackdrop}
>
<div
Expand All @@ -134,7 +134,7 @@ export function SupportTicketModal({ userId, onClose }: SupportTicketModalProps)
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
className="bg-[var(--github-border-secondary)] border border-[var(--github-border-primary)] rounded-xl w-full max-w-md shadow-2xl focus:outline-none"
className="bg-[var(--github-border-secondary)] border border-[var(--github-border-primary)] rounded-xl w-full max-w-md my-auto shadow-2xl focus:outline-none"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
Expand Down
24 changes: 24 additions & 0 deletions src/styles/theme.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
@custom-variant dark (&:is(.dark *));

/*
* How far the app's top content starts from the top of the screen.
*
* Installed on an iPhone home screen (black-translucent + viewport-fit=cover),
* iOS 26 draws its Liquid Glass edge blur as system chrome over the top of the
* app, about 40pt past the status bar; no CSS or meta tag turns it off. An
* opaque strip as tall as the status bar alone did not stop it on @thierry's
* iPhone 14 (#398, retested 1 October 2026). The fix confirmed on devices by
* other apps is to keep content below the blur: 2.5rem of clearance, only in
* the installed app on iOS (-webkit-touch-callout exists only in WebKit on
* iOS), and none on a landscape phone, where the status bar is hidden.
* Everywhere else it is the plain safe-area inset (0 in a browser tab).
*/
:root {
--inset-top: env(safe-area-inset-top, 0px);
}
@supports (-webkit-touch-callout: none) {
@media (display-mode: standalone) and (orientation: portrait) {
:root {
--inset-top: calc(env(safe-area-inset-top, 0px) + 2.5rem);
}
}
}

:root {
--font-size: 16px;
--background: #ffffff;
Expand Down
32 changes: 28 additions & 4 deletions src/test/statusBarBackdrop.test.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { StatusBarBackdrop } from '../app/components/StatusBarBackdrop';
import { readFileSync } from 'node:fs';

const theme = readFileSync('src/styles/theme.css', 'utf8');

const components = import.meta.glob('../app/**/*.tsx', { query: '?raw', import: 'default', eager: true }) as Record<string, string>;

describe('StatusBarBackdrop (iOS 26 edge blur)', () => {
it('is a fixed, opaque, decorative strip as tall as the status bar', () => {
it('is a fixed, opaque, decorative strip as tall as the top inset', () => {
render(<StatusBarBackdrop />);
const strip = screen.getByTestId('status-bar-backdrop');
expect(strip).toHaveAttribute('aria-hidden', 'true');
// iOS keeps its blur off the top edge only under a fixed box with a background.
// A fixed box in the page color: what scrolls up under the blur is a plain band.
expect(strip).toHaveClass('fixed', 'top-0', 'inset-x-0', 'bg-[var(--github-bg)]');
// No height outside an installed app: env() is 0 in a Safari tab and on desktop.
expect(strip).toHaveClass('h-[env(safe-area-inset-top)]', 'pointer-events-none');
// No height outside an installed app: --inset-top is 0 in a Safari tab and on desktop.
expect(strip).toHaveClass('h-[var(--inset-top)]', 'pointer-events-none');
});
});

describe('--inset-top: content kept below the iOS edge blur', () => {
it('is the safe-area inset, plus 2.5rem in the app installed on iOS, in portrait', () => {
expect(theme).toMatch(/:root \{\s*--inset-top: env\(safe-area-inset-top, 0px\);\s*\}/);
expect(theme).toMatch(
/@supports \(-webkit-touch-callout: none\) \{\s*@media \(display-mode: standalone\) and \(orientation: portrait\) \{\s*:root \{\s*--inset-top: calc\(env\(safe-area-inset-top, 0px\) \+ 2\.5rem\);/,
);
});

it('is what every component uses for the top edge (a bare safe-area inset would sit inside the blur)', () => {
const bare = Object.entries(components).filter(([, src]) => src.includes('safe-area-inset-top')).map(([path]) => path);
expect(bare).toEqual([]);
const users = Object.entries(components).filter(([, src]) => src.includes('var(--inset-top)')).map(([path]) => path.split('/').pop());
expect(users.sort()).toEqual([
'AiTutorPanel.tsx', 'Landing.tsx', 'Layout.tsx', 'LoginModal.tsx', 'MarkdownPage.tsx', 'PWAInstallModal.tsx',
'PrivacyPolicy.tsx', 'Sidebar.tsx', 'StatusBarBackdrop.tsx', 'SupportTicketModal.tsx',
]);
});
});
Loading