Mobile : menu compte via createPortal au-dessus de la tabbar, verrou scroll de la feuille filtres, logo tactile
- frontend/src/App.tsx : backdrop + menu compte rendus à la racine de <body> via createPortal (le backdrop-filter du header re-parentait le position:fixed → voile de 66 px, et le stacking context z-50 du header plaquait le menu SOUS la tabbar z-85 et le fab z-80) ; fermeture à Escape. - frontend/src/styles.css : .account-backdrop → var(--z-overlay) (800), .account-menu → position:fixed ancré sous le header (--header-h 66/58 px) à var(--z-modal) (900) ; zone tactile ≥44 px du logo .brand (::after inset -8px, rendu inchangé) ; les 7 selects de filtres étaient déjà couverts par la règle 16 px ≤900 px + filet anti-zoom global de tokens.css. - frontend/src/pages/Home.tsx : feuille de filtres mobile (sheetOpen) — gel du scroll d arrière-plan (pattern App.tsx) + fermeture à Escape. - frontend/src/ka/tokens.css (commité avec Stats v2) : socle mobile commun. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 changed files +50 −7
modified
frontend/src/App.tsx
+17 −2
@@ -5,6 +5,7 @@ | ||
| 5 | 5 | // même patron que le App.tsx de Lou·Ka. |
| 6 | 6 | // ============================================================================== |
| 7 | 7 | import { useEffect, useState } from "react"; |
| 8 | +import { createPortal } from "react-dom"; | |
| 8 | 9 | import { NavLink, Route, Routes, useLocation } from "react-router-dom"; |
| 9 | 10 | import { AccountProvider, useAccount } from "./account"; |
| 10 | 11 | import { |
@@ -34,6 +35,14 @@ function AccountMenu() { | ||
| 34 | 35 | fetchAuthConfig().then((c) => setEnabled(c.ka)).catch(() => {}); |
| 35 | 36 | }, []); |
| 36 | 37 | |
| 38 | + // menu ouvert : fermeture à Escape (le tap extérieur passe par le backdrop) | |
| 39 | + useEffect(() => { | |
| 40 | + if (!menuOpen) return; | |
| 41 | + const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setMenuOpen(false); }; | |
| 42 | + window.addEventListener("keydown", onKey); | |
| 43 | + return () => window.removeEventListener("keydown", onKey); | |
| 44 | + }, [menuOpen]); | |
| 45 | + | |
| 37 | 46 | if (!enabled) return null; |
| 38 | 47 | if (!me) { |
| 39 | 48 | return ( |
@@ -55,7 +64,12 @@ function AccountMenu() { | ||
| 55 | 64 | ? <img src={me.picture} alt="" referrerPolicy="no-referrer" /> |
| 56 | 65 | : <span className="account-initial">{(me.name || me.email).charAt(0).toUpperCase()}</span>} |
| 57 | 66 | </button> |
| 58 | − {menuOpen && ( | |
| 67 | + {/* Backdrop + menu rendus à la racine via createPortal : le | |
| 68 | + backdrop-filter du header re-parentait le position:fixed (voile de | |
| 69 | + 66 px) et le stacking context du header (z-50) plaquait le menu SOUS | |
| 70 | + la tabbar (85) et le fab (80). Le menu devient fixed, ancré sous le | |
| 71 | + header (voir .account-menu dans styles.css). */} | |
| 72 | + {menuOpen && createPortal( | |
| 59 | 73 | <> |
| 60 | 74 | <div className="account-backdrop" onClick={() => setMenuOpen(false)} aria-hidden="true" /> |
| 61 | 75 | <div className="account-menu" role="menu"> |
@@ -79,7 +93,8 @@ function AccountMenu() { | ||
| 79 | 93 | Se déconnecter |
| 80 | 94 | </button> |
| 81 | 95 | </div> |
| 82 | − </> | |
| 96 | + </>, | |
| 97 | + document.body, | |
| 83 | 98 | )} |
| 84 | 99 | </div> |
| 85 | 100 | ); |
modified
frontend/src/pages/Home.tsx
+13 −0
@@ -68,6 +68,19 @@ export default function Home() { | ||
| 68 | 68 | const [statChips, setStatChips] = useState<{ label: string; value: number }[]>([]); |
| 69 | 69 | const [sheetOpen, setSheetOpen] = useState(false); |
| 70 | 70 | |
| 71 | + // feuille de filtres ouverte : gel du scroll d'arrière-plan + fermeture à | |
| 72 | + // Escape (même pattern que le menu hamburger d'App.tsx) | |
| 73 | + useEffect(() => { | |
| 74 | + document.body.style.overflow = sheetOpen ? "hidden" : ""; | |
| 75 | + if (!sheetOpen) return () => { document.body.style.overflow = ""; }; | |
| 76 | + const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setSheetOpen(false); }; | |
| 77 | + window.addEventListener("keydown", onKey); | |
| 78 | + return () => { | |
| 79 | + document.body.style.overflow = ""; | |
| 80 | + window.removeEventListener("keydown", onKey); | |
| 81 | + }; | |
| 82 | + }, [sheetOpen]); | |
| 83 | + | |
| 71 | 84 | const filters = useMemo(() => { |
| 72 | 85 | const f: Partial<Record<FilterKey, string>> = {}; |
| 73 | 86 | for (const k of FILTER_KEYS) { |
modified
frontend/src/styles.css
+20 −5
@@ -36,7 +36,8 @@ img { display: block; } | ||
| 36 | 36 | backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); |
| 37 | 37 | border-bottom: 1.5px solid var(--ink); |
| 38 | 38 | } |
| 39 | −.header-inner { display: flex; align-items: center; gap: 20px; height: 66px; } | |
| 39 | +:root { --header-h: 66px; } /* hauteur du header (ancre du menu compte fixed) */ | |
| 40 | +.header-inner { display: flex; align-items: center; gap: 20px; height: var(--header-h); } | |
| 40 | 41 | .brand { |
| 41 | 42 | font-family: var(--font-display); font-weight: 700; font-size: 24px; |
| 42 | 43 | letter-spacing: -0.04em; display: flex; align-items: center; line-height: 1; |
@@ -583,7 +584,7 @@ img { display: block; } | ||
| 583 | 584 | .fab { display: none; } |
| 584 | 585 | |
| 585 | 586 | @media (max-width: 640px) { |
| 586 | − .header-inner { height: 58px; } | |
| 587 | + :root { --header-h: 58px; } | |
| 587 | 588 | .brand { font-size: 20px; } |
| 588 | 589 | .nav a { padding: 8px 13px; font-size: 13.5px; } |
| 589 | 590 | .ticker { font-size: 10px; padding: 6px 0; } |
@@ -671,11 +672,18 @@ img { display: block; } | ||
| 671 | 672 | .ka-footer { padding-bottom: calc(72px + env(safe-area-inset-bottom)); } |
| 672 | 673 | } |
| 673 | 674 | |
| 674 | −/* Anti-zoom iOS : les champs doivent faire >= 16px */ | |
| 675 | +/* Anti-zoom iOS : les champs doivent faire >= 16px | |
| 676 | + (filet global : tokens.css force aussi 16 px au doigt) */ | |
| 675 | 677 | @media (max-width: 900px) { |
| 676 | 678 | .f-search input, .f-ctl select { font-size: 16px; } |
| 677 | 679 | } |
| 678 | 680 | |
| 681 | +/* Cible tactile ≥44 px du logo (138×30 rendus) : zone étendue invisible */ | |
| 682 | +@media (pointer: coarse) { | |
| 683 | + .brand { position: relative; } | |
| 684 | + .brand::after { content: ""; position: absolute; inset: -8px; } | |
| 685 | +} | |
| 686 | + | |
| 679 | 687 | /* ================= Footer (KaFooter — ka-ui) ================= */ |
| 680 | 688 | /* Le footer commun vient de tokens.css (.ka-footer) + KaFooter.tsx. On garde |
| 681 | 689 | ici seulement la rangée de nav locale posée au-dessus. */ |
@@ -728,9 +736,16 @@ img { display: block; } | ||
| 728 | 736 | width: 100%; height: 100%; |
| 729 | 737 | font-family: var(--font-display); font-weight: 700; font-size: 17px; color: var(--ink); |
| 730 | 738 | } |
| 731 | −.account-backdrop { position: fixed; inset: 0; z-index: 55; } | |
| 739 | +/* Backdrop + menu rendus à la racine de <body> via createPortal (un fixed | |
| 740 | + descendant du header blurré se re-parentait, et le stacking context z-50 du | |
| 741 | + header plaquait le menu sous la tabbar z-85 / le fab z-80). Échelle commune | |
| 742 | + tokens.css : voile = --z-overlay (800), menu = --z-modal (900). */ | |
| 743 | +.account-backdrop { position: fixed; inset: 0; z-index: var(--z-overlay); } | |
| 732 | 744 | .account-menu { |
| 733 | − position: absolute; right: 0; top: calc(100% + 10px); z-index: 60; | |
| 745 | + position: fixed; | |
| 746 | + right: max(12px, env(safe-area-inset-right)); | |
| 747 | + top: calc(var(--header-h, 66px) + 10px); | |
| 748 | + z-index: var(--z-modal); | |
| 734 | 749 | min-width: 230px; background: var(--surface); |
| 735 | 750 | border: 1.5px solid var(--ink); border-radius: var(--r-card); |
| 736 | 751 | box-shadow: var(--shadow-off); padding: 8px; display: flex; flex-direction: column; |
| 737 | 752 | |