feat(mobile): refonte responsive complète (header, drawer, dvh, cibles tactiles)
Le header était en grid-cols-[auto_1fr_auto] avec une barre de recherche non-wrappable (7 chips + input + 2 boutons, aucun min-w-0) : ~1000px de largeur intrinsèque dans un viewport de 375px. Toutes les routes débordaient horizontalement et les boutons Connexion/Inscription partaient hors écran. Socle - --header-h / --header-offset / --app-vh centralisent la métrique du header : supprime les top-16 codés en dur (channel-tabs était en top-0 et glissait sous le header au scroll). - Safe-area iOS, viewport-fit=cover, color-scheme/theme-color. - dvh partout (100vh > hauteur visible sur iOS), overflow-x: clip au lieu de hidden (hidden cassait silencieusement tous les sticky). - Cible tactile 44px sur pointeur grossier, skip link, focus-ring clavier, scroll-rail pour les tab strips. Header - Barre mobile (burger, logo, recherche, menu ⋮) + barre desktop inchangée. - Le menu ⋮ expose ce qui disparaissait : bande des thèmes, préférences, historique, favoris, téléchargements, thème, connexion/inscription. - Recherche en feuille sous le header avec focus automatique. - Échap / clic extérieur / navigation ferment les panneaux transitoires. Drawer - Partait à inset-y-0 : les 56 premiers px du menu étaient derrière le header. Descend sous le header, bouton fermer intégré, verrouille le scroll, se ferme à Échap. Barre de recherche - Chips masquées sous lg, remplacées par un bouton résumé qui ouvre le sélecteur de fournisseurs. Suggestions en dvh, menu Ctrl+K plein écran sur mobile. Pages - Shorts : cadre 9:16 contraint par dvh ET vw (il débordait en paysage), Préc./Suiv. qui étaient hors du conteneur height-capped. - Watch : barre d'actions qui wrappe, mode cinéma en opacity-0 group-hover donc invisible au doigt, thumbnails fluides, playsinline sur les iframes. - Historique, thèmes, chaîne, playlists, préférences, sessions : rails swipables, min-w-0, truncate, en-têtes qui wrappent. Vérifié : audit navigateur (CDP) sur 17 routes x 7 largeurs (320→1440), zéro débordement horizontal ; 19 tests d'interaction mobile ; suites de tests du dépôt vertes (api 40/40, transcript 27/27, search, suggest, playlists, preferences, subscriptions, highlight, telemetry).
This commit is contained in:
@@ -1,23 +1,260 @@
|
||||
/* Minimal stylesheet to accompany Tailwind CDN styles */
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
/* --- Layout metrics -----------------------------------------------------
|
||||
Single source of truth for the app chrome height so templates never have
|
||||
to hardcode `top-16` / `h-[calc(100vh-4rem)]` (which silently break when the
|
||||
header wraps on narrow viewports). */
|
||||
--header-h: 3.5rem; /* 56px — mobile */
|
||||
--safe-top: 0px;
|
||||
--safe-bottom: 0px;
|
||||
--safe-left: 0px;
|
||||
--safe-right: 0px;
|
||||
/* Height actually consumed by the fixed header, safe-area included. */
|
||||
--header-offset: calc(var(--header-h) + var(--safe-top));
|
||||
/* Usable viewport height that follows the mobile browser chrome. */
|
||||
--app-vh: 100vh;
|
||||
}
|
||||
|
||||
@supports (height: 100dvh) {
|
||||
:root { --app-vh: 100dvh; }
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
:root { --header-h: 4rem; } /* 64px — desktop */
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
html {
|
||||
/* `clip` (not `hidden`) so the element never becomes a scroll container —
|
||||
`overflow: hidden` on <html> silently breaks every `position: sticky`
|
||||
child, including the header rails. */
|
||||
overflow-x: clip;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
@supports not (overflow: clip) {
|
||||
html { overflow-x: hidden; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
*, *::before, *::after {
|
||||
animation-duration: .01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: .01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: var(--app-vh);
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans,
|
||||
"Helvetica Neue", Arial, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
background-color: #0f172a; /* slate-900 */
|
||||
color: #e2e8f0; /* slate-200 */
|
||||
/* No grey flash / no accidental text selection while tapping chrome. */
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
overscroll-behavior-y: none;
|
||||
}
|
||||
|
||||
/* Container helper to match Tailwind's container defaults a bit closer */
|
||||
/* Inputs on iOS render at 16px by default which triggers focus zoom; make sure
|
||||
no field ever drops below that on small viewports. Buttons are excluded so
|
||||
compact `text-xs` toolbars keep their compact look. */
|
||||
@media (max-width: 767px) {
|
||||
input:not([type="checkbox"]):not([type="radio"]),
|
||||
select,
|
||||
textarea {
|
||||
font-size: max(16px, 1em);
|
||||
}
|
||||
/* Never let a flex/grid child blow out the viewport. */
|
||||
img, svg, video, iframe {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Touch ergonomics ----------------------------------------------------- */
|
||||
/* WCAG 2.5.5 / Apple HIG: 44px is the comfortable minimum on coarse pointers.
|
||||
Desktop keeps the compact visual sizing. */
|
||||
@media (pointer: coarse) {
|
||||
button,
|
||||
[role="button"],
|
||||
a.chip {
|
||||
min-height: 44px;
|
||||
}
|
||||
/* Icon-only controls (no text child) get a square minimum too. */
|
||||
button.icon-only,
|
||||
[role="button"].icon-only {
|
||||
min-width: 44px;
|
||||
}
|
||||
}
|
||||
.chip {
|
||||
transition: background-color .15s ease, color .15s ease, opacity .15s ease;
|
||||
}
|
||||
|
||||
/* Expand tiny hit areas without changing visual size. */
|
||||
.hit-expand { position: relative; }
|
||||
.hit-expand::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
/* --- Mobile primitives ---------------------------------------------------- */
|
||||
/* Horizontal swipe rail: the standard mobile pattern for tab strips that would
|
||||
otherwise wrap or get clipped. */
|
||||
.scroll-rail {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: thin;
|
||||
overscroll-behavior-x: contain;
|
||||
}
|
||||
|
||||
/* Scroll affordance for swipe rails that hide their scrollbar. */
|
||||
.scroll-rail-fade {
|
||||
position: relative;
|
||||
}
|
||||
.scroll-rail-fade::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 2rem;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(to right, transparent, var(--bg));
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Vertical stacks inside dialogs must be able to scroll within dvh. */
|
||||
.dialog-scroll {
|
||||
max-height: min(85dvh, 100%);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
/* Prevent long unbroken strings (UUIDs, URLs, tokens) from breaking layout. */
|
||||
.break-anywhere { overflow-wrap: anywhere; word-break: break-word; }
|
||||
|
||||
/* Focus ring for keyboard users only (touch devices get :focus instead). */
|
||||
.focus-ring:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* --- App chrome primitives ------------------------------------------------ */
|
||||
/* Icon-only header/button. 40px on pointer devices, 44px on touch. */
|
||||
.app-icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2.5rem;
|
||||
min-height: 2.5rem;
|
||||
padding: 0.375rem;
|
||||
border-radius: 9999px;
|
||||
color: var(--text);
|
||||
transition: background-color .15s ease, opacity .15s ease;
|
||||
}
|
||||
.app-icon-btn:hover { background-color: var(--panel-2); }
|
||||
.app-icon-btn:active { transform: scale(.94); }
|
||||
|
||||
/* Avatar trigger (always >= 44px of tappable area). */
|
||||
.app-avatar-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.25rem;
|
||||
border-radius: 9999px;
|
||||
transition: background-color .15s ease;
|
||||
}
|
||||
.app-avatar-btn:hover { background-color: var(--panel-2); }
|
||||
|
||||
.avatar-circle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 9999px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* The fixed app header: honours the safe area and never overflows. */
|
||||
.app-header {
|
||||
/* Nothing inside may widen the bar. */
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
/* --- Quick menu / command palette (Ctrl+K) -------------------------------- */
|
||||
.quick-menu-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
background-color: rgba(0, 0, 0, .6);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: max(1rem, env(safe-area-inset-top)) 1rem;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.quick-menu-dialog {
|
||||
margin-top: 5vh;
|
||||
width: 100%;
|
||||
max-width: 56rem;
|
||||
max-height: min(85dvh, 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-radius: .75rem;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, .25);
|
||||
background-color: var(--bg, #0f172a);
|
||||
border: 1px solid var(--border, #334155);
|
||||
}
|
||||
@supports not (height: 100dvh) {
|
||||
.quick-menu-dialog { max-height: 85vh; }
|
||||
}
|
||||
@media (max-width: 639px) {
|
||||
/* On phones the dialog owns the viewport: no floating "modal" look. */
|
||||
.quick-menu-backdrop { padding: 0; }
|
||||
.quick-menu-dialog {
|
||||
margin-top: 0;
|
||||
max-height: 100dvh;
|
||||
height: 100dvh;
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Container helper: responsive gutters + fluid cap (Tailwind's CDN `container`
|
||||
is pinned to a single width, which breaks small viewports). */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
margin-inline: auto;
|
||||
padding-inline: 1rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.container { padding-inline: 1.5rem; }
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.container { max-width: 1280px; padding-inline: 2rem; }
|
||||
}
|
||||
|
||||
/* --- Theme System ------------------------------------------------------- */
|
||||
@@ -232,3 +469,51 @@ body {
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sidebar-scroll { scroll-behavior: auto; }
|
||||
}
|
||||
|
||||
/* --- Shorts player frame --------------------------------------------------- */
|
||||
/* 9:16 frame constrained by BOTH the dynamic viewport height and the viewport
|
||||
width, so it never overflows a portrait phone nor a landscape one.
|
||||
`--shorts-chrome` reserves room for the provider rail + bottom navigation. */
|
||||
.shorts-frame {
|
||||
--shorts-chrome: 11rem;
|
||||
aspect-ratio: 9 / 16;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
height: min(
|
||||
calc(var(--app-vh) - var(--header-offset) - var(--shorts-chrome)),
|
||||
calc((100vw - 2rem) * 16 / 9)
|
||||
);
|
||||
min-height: 0;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.shorts-frame {
|
||||
--shorts-chrome: 9rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Drawer / overlay plumbing -------------------------------------------- */
|
||||
/* Applied to <body> while a full-screen drawer or modal owns the viewport.
|
||||
Note: deliberately NOT `touch-action: none` — touch-action is intersected up
|
||||
the ancestor chain, so it would also freeze scrolling *inside* the drawer. */
|
||||
body.scroll-locked {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Skip link: visible only on keyboard focus. */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
left: 0.5rem;
|
||||
top: -4rem;
|
||||
z-index: 100;
|
||||
padding: 0.5rem 0.875rem;
|
||||
border-radius: 0.5rem;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
transition: top .15s ease;
|
||||
}
|
||||
.skip-link:focus {
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user