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:
@@ -31,21 +31,21 @@ import { forkJoin } from 'rxjs';
|
||||
<div class="fixed inset-0 z-40 bg-black/60 backdrop-blur-[2px]" (click)="close()"></div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="fixed inset-0 z-50 grid place-items-center" aria-modal="true" role="dialog" [attr.aria-labelledby]="'addToPlaylistTitle'">
|
||||
<div class="w-[min(92vw,720px)] max-h-[min(80vh,700px)] rounded-xl bg-slate-900 border border-slate-700 shadow-2xl focus:outline-none overflow-hidden" (click)="$event.stopPropagation()">
|
||||
<div class="fixed inset-0 z-50 grid place-items-center p-4" [style.paddingBottom]="'max(1rem, var(--safe-bottom))'" aria-modal="true" role="dialog" [attr.aria-labelledby]="'addToPlaylistTitle'">
|
||||
<div class="flex w-[min(92vw,720px)] max-h-[min(80dvh,700px)] flex-col overflow-hidden rounded-xl border border-slate-700 bg-slate-900 shadow-2xl focus:outline-none" (click)="$event.stopPropagation()">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-5 py-4 border-b border-slate-700">
|
||||
<div class="flex shrink-0 items-center justify-between gap-3 border-b border-slate-700 px-4 py-4 sm:px-5">
|
||||
<div id="addToPlaylistTitle" class="text-base font-semibold text-slate-100">Vos playlists</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="text-xs text-slate-400 hover:text-slate-200" (click)="reloadPlaylists()">Rafraîchir</button>
|
||||
<button class="h-8 w-8 grid place-items-center rounded-md text-slate-300 hover:text-white hover:bg-slate-700/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60" (click)="close()" aria-label="Fermer">
|
||||
<button type="button" class="chip shrink-0 rounded px-3 py-2 text-xs text-slate-400 hover:text-slate-200" (click)="reloadPlaylists()">Rafraîchir</button>
|
||||
<button type="button" class="app-icon-btn shrink-0 text-slate-300 hover:text-white hover:bg-slate-700/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60" (click)="close()" aria-label="Fermer">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-5 w-5"><path d="M6.225 4.811a1 1 0 011.414 0L12 9.172l4.361-4.361a1 1 0 011.415 1.415L13.415 10.586l4.361 4.361a1 1 0 01-1.415 1.415L12 12l-4.361 4.361a1 1 0 01-1.414-1.415l4.36-4.36-4.36-4.36a1 1 0 010-1.415z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="px-5 py-4 space-y-4">
|
||||
<div class="min-h-0 flex-1 space-y-4 overflow-y-auto px-4 py-4 sm:px-5">
|
||||
@if (loading()) {
|
||||
<div class="text-sm text-slate-400">Chargement...</div>
|
||||
} @else if (error()) {
|
||||
@@ -53,10 +53,10 @@ import { forkJoin } from 'rxjs';
|
||||
} @else if (playlists().length === 0) {
|
||||
<div class="text-sm text-slate-400">Aucune playlist. Créez-en une ci-dessous.</div>
|
||||
} @else {
|
||||
<div class="max-h-[42vh] overflow-auto divide-y divide-slate-800 rounded-md border border-slate-800">
|
||||
<div class="max-h-[min(42dvh,320px)] overflow-auto divide-y divide-slate-800 rounded-md border border-slate-800">
|
||||
@for (pl of playlists(); track pl.id) {
|
||||
<label class="flex items-center gap-4 px-4 py-3 hover:bg-slate-800/60 cursor-pointer">
|
||||
<input type="checkbox" class="accent-red-500 h-4 w-4" [checked]="isSelected(pl.id)" (change)="toggleSelect(pl.id, $event)" />
|
||||
<input type="checkbox" class="h-5 w-5 shrink-0 accent-red-500" [checked]="isSelected(pl.id)" (change)="toggleSelect(pl.id, $event)" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm text-slate-200">{{ pl.title }}</div>
|
||||
<div class="text-xs text-slate-400">{{ pl.itemsCount || 0 }} vidéo(s)</div>
|
||||
@@ -69,16 +69,16 @@ import { forkJoin } from 'rxjs';
|
||||
<!-- Create new playlist -->
|
||||
<div class="border-t border-slate-700 pt-4">
|
||||
<label class="block text-xs text-slate-400 mb-2">Nouvelle playlist</label>
|
||||
<div class="flex flex-col sm:flex-row gap-3 items-stretch sm:items-end">
|
||||
<div class="flex-1">
|
||||
<input type="text" class="w-full bg-slate-950 text-slate-100 border border-slate-700 rounded px-3 py-2 text-sm placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-red-500/60"
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
||||
<div class="min-w-0 flex-1">
|
||||
<input type="text" class="w-full min-w-0 rounded border border-slate-700 bg-slate-950 px-3 py-3 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-red-500/60 sm:py-2"
|
||||
[value]="newTitle()" (input)="onTitleChange($event)" placeholder="Titre de la playlist" />
|
||||
</div>
|
||||
<label class="flex items-center gap-2 text-xs text-slate-300">
|
||||
<input type="checkbox" class="accent-red-500" [checked]="newPrivate()" (change)="onPrivateChange($event)" />
|
||||
<label class="flex items-center gap-2 py-1 text-xs text-slate-300">
|
||||
<input type="checkbox" class="h-5 w-5 accent-red-500" [checked]="newPrivate()" (change)="onPrivateChange($event)" />
|
||||
Privée
|
||||
</label>
|
||||
<button class="px-4 py-2 rounded-md bg-red-600 hover:bg-red-500 text-white text-sm font-semibold disabled:opacity-60 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60"
|
||||
<button type="button" class="chip rounded-md bg-red-600 px-4 py-3 text-sm font-semibold text-white hover:bg-red-500 disabled:cursor-not-allowed disabled:opacity-60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60 sm:py-2"
|
||||
[disabled]="!newTitle().trim() || busyAdd()"
|
||||
(click)="onQuickCreate()">Créer + Ajouter</button>
|
||||
</div>
|
||||
@@ -86,11 +86,11 @@ import { forkJoin } from 'rxjs';
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-5 py-4 border-t border-slate-700 bg-slate-900/60 flex items-center justify-between">
|
||||
<div class="flex shrink-0 flex-col gap-3 border-t border-slate-700 bg-slate-900/60 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-5">
|
||||
<div class="text-xs text-slate-400">{{ selectedCount() }} sélectionnée(s)</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="px-4 py-2 rounded-md bg-slate-700 hover:bg-slate-600 text-slate-100 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400/60" (click)="close()">Annuler</button>
|
||||
<button class="px-4 py-2 rounded-md bg-red-600 hover:bg-red-500 text-white text-sm font-semibold disabled:opacity-60 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60"
|
||||
<div class="flex w-full items-center gap-3 sm:w-auto">
|
||||
<button type="button" class="chip flex-1 rounded-md bg-slate-700 px-4 py-3 text-sm text-slate-100 hover:bg-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400/60 sm:flex-none sm:py-2" (click)="close()">Annuler</button>
|
||||
<button type="button" class="chip flex-1 rounded-md bg-red-600 px-4 py-3 text-sm font-semibold text-white hover:bg-red-500 disabled:cursor-not-allowed disabled:opacity-60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60 sm:flex-none sm:py-2"
|
||||
[disabled]="busyAdd() || selectedCount() === 0"
|
||||
(click)="onAddSelected()">Ajouter</button>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@ import { AuthService } from '../../../../services/auth.service';
|
||||
[class.text-slate-400]="!isLiked()"
|
||||
[class.hover\:text-red-500]="!isLiked()"
|
||||
[disabled]="isLoading()"
|
||||
class="transition-colors duration-200 p-1 rounded-full hover:bg-slate-700/50 flex items-center justify-center"
|
||||
class="flex h-11 w-11 items-center justify-center rounded-full transition-colors duration-200 hover:bg-slate-700/50 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60"
|
||||
[attr.aria-label]="isLiked() ? 'Retirer des vidéos aimées' : 'Ajouter aux vidéos aimées'"
|
||||
[attr.aria-pressed]="isLiked()"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user