Files
NewTube/index.css
T
bruno 66f1da1d72 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).
2026-09-28 14:38:31 -04:00

520 lines
15 KiB
CSS

/* 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;
}
/* 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 {
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 ------------------------------------------------------- */
/* Define CSS variables per theme and override a few common Tailwind utils */
/* Defaults (used as fallback) */
:root {
--bg: #0f172a; /* dark slate */
--text: #e2e8f0; /* slate-200 */
--muted: #94a3b8; /* slate-400 */
--panel: #1f2937; /* slate-800 */
--panel-2: #334155; /* slate-700 */
--overlay: rgba(31,41,55,.5);
--accent: #ef4444; /* red-500/600 */
--accent-hover: #dc2626; /* red-600/700 */
--border: #334155;
--selection: rgba(255,255,255,.06); /* default subtle selection for dark */
}
/* Dark */
[data-theme="dark"] {
color-scheme: dark;
/* Dark gray palette (softer than deep slate) */
--bg: #14171b; /* base background */
--text: #e5e7eb; /* slightly brighter text */
--muted: #a1a7b3; /* softer secondary text */
--panel: #1d2126; /* cards/headers */
--panel-2: #262b31; /* inputs/secondary panels */
--overlay: rgba(29,33,38,.6);
--accent: #ef4444; /* unchanged accent */
--accent-hover: #dc2626;
--border: #2f3540; /* subtle border */
--selection: #2b3139; /* hover/selection on dark */
}
/* Pure Black */
[data-theme="black"] {
color-scheme: dark;
--bg: #000000;
--text: #e5e7eb;
--muted: #9ca3af;
--panel: #0a0a0a;
--panel-2: #111111;
--overlay: rgba(0,0,0,.6);
--accent: #ef4444;
--accent-hover: #dc2626;
--border: #1f2937;
--selection: #121416;
}
/* Light */
[data-theme="light"] {
color-scheme: light;
--bg: #f8fafc; /* slate-50 */
--text: #0f172a; /* slate-900 */
--muted: #64748b;/* slate-500 for secondary text */
--panel: #ffffff;
--panel-2: #f1f5f9; /* slate-100 */
--overlay: #ffffff; /* solid white for cards/headers using bg-slate-800/50 */
--accent: #ef4444;
--accent-hover: #dc2626;
--border: #e2e8f0;
--selection: #eef2f7; /* light mode selection (ref. YouTube style) */
}
/* Blue (navy) */
[data-theme="blue"] {
color-scheme: dark;
--bg: #0b1220;
--text: #dbeafe; /* blue-100 */
--muted: #93c5fd; /* blue-300 */
--panel: #111a2b;
--panel-2: #17223a;
--overlay: rgba(17,26,43,.6);
--accent: #3b82f6; /* blue-500 */
--accent-hover: #2563eb; /* blue-600 */
--border: #1e3a8a; /* blue-900 */
--selection: #1a2440;
}
/* System: default to light, override when OS is dark */
[data-theme="system"] {
color-scheme: light;
--bg: #f8fafc;
--text: #0f172a;
--muted: #334155;
--panel: #ffffff;
--panel-2: #f1f5f9;
--overlay: rgba(255,255,255,.8);
--accent: #ef4444;
--accent-hover: #dc2626;
--border: #e2e8f0;
--selection: #eef2f7;
}
@media (prefers-color-scheme: dark) {
[data-theme="system"] {
color-scheme: dark;
--bg: #0f172a;
--text: #e2e8f0;
--muted: #94a3b8;
--panel: #1f2937;
--panel-2: #334155;
--overlay: rgba(31,41,55,.5);
--accent: #ef4444;
--accent-hover: #dc2626;
--border: #334155;
--selection: #2b3139;
}
}
/* Apply variables */
body {
background-color: var(--bg);
color: var(--text);
}
/* Mild overrides so Tailwind utilities adapt to themes without rebuild */
[data-theme] .bg-slate-900 { background-color: var(--bg) !important; }
[data-theme] .bg-gray-800,
[data-theme] .bg-slate-800 { background-color: var(--panel) !important; }
[data-theme] .bg-slate-700 { background-color: var(--panel-2) !important; }
[data-theme] .bg-slate-800\/50 { background-color: var(--overlay) !important; }
[data-theme] .text-slate-200 { color: var(--text) !important; }
[data-theme] .text-slate-100 { color: var(--text) !important; }
[data-theme] .text-slate-300 { color: color-mix(in srgb, var(--text) 80%, transparent) !important; }
[data-theme] .text-slate-400 { color: var(--muted) !important; }
[data-theme] .text-white { color: var(--text) !important; }
[data-theme] .border-slate-700 { border-color: var(--border) !important; }
/* Accent buttons */
[data-theme] .bg-red-600 { background-color: var(--accent) !important; }
[data-theme] .hover\:bg-red-500:hover { background-color: var(--accent-hover) !important; }
[data-theme] .bg-gray-600 { background-color: var(--panel-2) !important; }
[data-theme] .hover\:bg-gray-500:hover { background-color: var(--panel) !important; }
/* Light-specific refinements */
[data-theme="light"] header {
box-shadow: 0 2px 12px rgba(0,0,0,.06);
}
[data-theme="light"] .shadow-2xl {
box-shadow: 0 25px 50px -12px rgba(0,0,0,.15) !important;
}
[data-theme="light"] .bg-black\/75 {
/* make duration pill less heavy on light */
background-color: rgba(0,0,0,.5) !important;
}
/* Cards and panels separation in Light */
[data-theme="light"] .bg-slate-800,
[data-theme="light"] .bg-gray-800,
[data-theme="light"] .bg-slate-800\/50 {
border: 1px solid var(--border) !important;
box-shadow: 0 1px 2px rgba(0,0,0,.05);
}
/* Hover states for list/grid cards */
[data-theme="light"] .hover\:bg-slate-700\/50:hover,
[data-theme="light"] .hover\:bg-slate-800:hover,
[data-theme="light"] .hover\:bg-slate-800\/50:hover,
[data-theme="light"] .hover\:bg-slate-700:hover,
[data-theme="light"] .hover\:bg-gray-800:hover {
background-color: var(--selection) !important; /* subtle hover */
}
/* In light mode, also treat any active dark-slate background as selection */
[data-theme="light"] .bg-slate-800,
[data-theme="light"] .bg-slate-800\/50,
[data-theme="light"] .bg-gray-800 {
background-color: var(--panel) !important;
border: 1px solid var(--border) !important;
}
/* Inputs in light mode */
[data-theme="light"] input.bg-slate-700,
[data-theme="light"] input.bg-gray-800,
[data-theme="light"] select.bg-gray-800,
[data-theme="light"] select.bg-slate-800,
[data-theme="light"] textarea.bg-gray-800,
[data-theme="light"] textarea.bg-slate-800 {
background-color: var(--panel-2) !important;
color: var(--text) !important;
border: 1px solid var(--border) !important;
}
[data-theme="light"] input::placeholder,
[data-theme="light"] textarea::placeholder {
color: #94a3b8; /* slate-400 */
}
/* Hide scrollbars on horizontal lists where we still want scrolling */
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* --- Sidebar: scrollbar visible au survol / focus uniquement --------------- */
.sidebar-scroll {
overflow-y: hidden;
scrollbar-width: thin;
scrollbar-color: #334155 transparent;
scrollbar-gutter: stable;
}
.sidebar-scroll:hover,
.sidebar-scroll:focus,
.sidebar-scroll:focus-within {
overflow-y: auto;
}
.sidebar-scroll::-webkit-scrollbar { width: 6px; }
.sidebar-scroll::-webkit-scrollbar-track { background: transparent; }
.sidebar-scroll::-webkit-scrollbar-thumb { background: #334155; border-radius: 9999px; }
@media (hover: none) {
/* tactile / mobile : toujours scrollable */
.sidebar-scroll { overflow-y: auto; }
}
@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;
}