From 66f1da1d7217d23bb8dc129a34b78017a09f5d69 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Mon, 28 Sep 2026 14:38:31 -0400 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20refonte=20responsive=20compl?= =?UTF-8?q?=C3=A8te=20(header,=20drawer,=20dvh,=20cibles=20tactiles)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- index.css | 289 ++++++++++++++- index.html | 15 +- src/app.component.html | 32 +- src/app.component.ts | 76 +++- .../account/history/history.component.html | 49 +-- .../preferences/preferences.component.html | 42 +-- .../account/sessions/sessions.component.html | 21 +- .../google-callback.component.html | 4 +- .../auth/login/login.component.html | 18 +- .../auth/register/register.component.html | 16 +- .../channel/channel-page.component.html | 8 +- .../channel-playlists.component.ts | 4 +- .../channel-tabs/channel-tabs.component.ts | 10 +- src/components/header/header.component.html | 328 ++++++++++++------ src/components/header/header.component.ts | 151 +++++++- src/components/home/home.component.html | 2 +- .../utilisation/utilisation.component.html | 2 +- .../downloads/download-play.component.html | 8 +- .../library/import/import.component.html | 6 +- .../library/liked/liked.component.html | 2 +- .../playlists/playlist-detail.component.ts | 20 +- .../playlists/playlists.component.html | 26 +- .../subscriptions.component.html | 36 +- .../search/provider-picker.component.html | 54 +-- .../search/search-box.component.html | 127 +++---- src/components/search/search-box.component.ts | 30 ++ src/components/search/search.component.html | 18 +- .../add-to-playlist.component.ts | 36 +- .../like-button/like-button.component.ts | 2 +- .../shorts/watch-short.component.html | 77 ++-- src/components/sidebar/sidebar.component.html | 43 ++- src/components/sidebar/sidebar.component.ts | 14 +- .../themes/provider-theme-page.component.html | 47 +-- .../themes/theme-page.component.html | 2 +- .../themes/themes-nav.component.html | 54 ++- src/components/watch/watch.component.html | 55 +-- src/services/i18n.service.ts | 16 + 37 files changed, 1231 insertions(+), 509 deletions(-) diff --git a/index.css b/index.css index e8c7259..a96a84e 100644 --- a/index.css +++ b/index.css @@ -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 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 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; +} + diff --git a/index.html b/index.html index 6ea9c3f..c4cbc02 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,10 @@ - + + + + NewTube @@ -33,6 +36,16 @@ ::-webkit-scrollbar-thumb:hover { background: #6c7086; } + + /* Shift the fixed header below the iOS status bar when in standalone mode. */ + @supports (padding: env(safe-area-inset-top)) { + html { + --safe-top: env(safe-area-inset-top); + --safe-bottom: env(safe-area-inset-bottom); + --safe-left: env(safe-area-inset-left); + --safe-right: env(safe-area-inset-right); + } + }