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:
@@ -3,7 +3,7 @@
|
||||
<div *ngIf="headerError() && !headerLoading()" class="rounded-xl border border-red-500/40 bg-red-500/10 p-8 text-center">
|
||||
<h2 class="mb-2 text-xl font-semibold text-red-100">Impossible de charger la chaîne</h2>
|
||||
<p class="text-sm text-red-200/80">{{ headerError() }}</p>
|
||||
<button type="button" (click)="refreshAll(true)" class="mt-4 rounded-md border border-red-500/60 px-4 py-1.5 text-sm text-red-100 hover:bg-red-500/20">
|
||||
<button type="button" (click)="refreshAll(true)" class="chip mt-4 rounded-md border border-red-500/60 px-4 py-3 text-sm text-red-100 hover:bg-red-500/20 sm:py-2">
|
||||
Réessayer
|
||||
</button>
|
||||
</div>
|
||||
@@ -22,7 +22,7 @@
|
||||
id="channel-sort"
|
||||
[value]="sort()"
|
||||
(change)="onSortChange($any($event.target).value)"
|
||||
class="rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1.5 text-sm text-zinc-100"
|
||||
class="rounded-md border border-zinc-700 bg-zinc-900 px-2 py-2.5 text-sm text-zinc-100 sm:py-1.5"
|
||||
>
|
||||
<option value="recent">Plus récentes</option>
|
||||
<option value="popular">Plus populaires</option>
|
||||
@@ -34,10 +34,10 @@
|
||||
placeholder="Rechercher dans cette chaîne…"
|
||||
[value]="searchQuery()"
|
||||
(input)="onSearchInput($any($event.target).value)"
|
||||
class="w-full rounded-lg border border-zinc-700 bg-zinc-900 px-3 py-2 pr-9 text-sm text-zinc-100 placeholder:text-zinc-500 focus:border-red-500 focus:outline-none"
|
||||
class="w-full min-w-0 rounded-lg border border-zinc-700 bg-zinc-900 px-3 py-3 pr-10 text-sm text-zinc-100 placeholder:text-zinc-500 focus:border-red-500 focus:outline-none sm:py-2 sm:pr-9"
|
||||
/>
|
||||
<button *ngIf="searchQuery()" type="button" (click)="clearSearch()" aria-label="Effacer"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 rounded px-1 text-zinc-400 hover:text-white">✕</button>
|
||||
class="absolute right-1 top-1/2 flex h-9 w-9 -translate-y-1/2 items-center justify-center rounded text-zinc-400 hover:text-white">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { VideoCardSkeletonComponent } from '../../../app/shared/components/video
|
||||
</div>
|
||||
<div *ngIf="!loading && error" class="flex flex-col items-center gap-3 rounded-xl border border-red-500/40 bg-red-500/10 p-8 text-center">
|
||||
<h3 class="text-lg font-semibold text-red-100">Impossible de charger les playlists</h3>
|
||||
<button type="button" (click)="retry.emit()" class="rounded-md border border-red-500/60 px-4 py-1.5 text-sm text-red-100 hover:bg-red-500/20">Réessayer</button>
|
||||
<button type="button" (click)="retry.emit()" class="chip rounded-md border border-red-500/60 px-4 py-3 text-sm text-red-100 hover:bg-red-500/20 sm:py-2">Réessayer</button>
|
||||
</div>
|
||||
<div *ngIf="!loading && !error && playlists.length === 0" class="flex flex-col items-center gap-2 rounded-xl border border-zinc-800 bg-zinc-900/50 p-10 text-center">
|
||||
<h3 class="text-lg font-semibold text-white">Aucune playlist trouvée</h3>
|
||||
@@ -24,7 +24,7 @@ import { VideoCardSkeletonComponent } from '../../../app/shared/components/video
|
||||
<div class="relative aspect-video w-full overflow-hidden bg-zinc-800">
|
||||
<img *ngIf="pl.thumbnailUrl; else noThumb" [src]="pl.thumbnailUrl!" [alt]="pl.title" loading="lazy" class="h-full w-full object-cover" />
|
||||
<ng-template #noThumb><div class="flex h-full w-full items-center justify-center text-4xl text-zinc-600">▤</div></ng-template>
|
||||
<div class="absolute inset-y-0 right-0 flex w-24 flex-col items-center justify-center gap-1 bg-black/70 text-white">
|
||||
<div class="absolute inset-y-0 right-0 flex w-16 flex-col items-center justify-center gap-1 bg-black/70 text-white sm:w-24">
|
||||
<span class="text-xl font-bold">{{ pl.videoCount ?? '–' }}</span>
|
||||
<span class="text-xs">vidéos</span>
|
||||
</div>
|
||||
|
||||
@@ -20,15 +20,19 @@ const ALL_TABS: ChannelTabDef[] = [
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
<nav class="sticky top-0 z-10 -mx-4 border-b border-zinc-800 bg-zinc-950/95 px-4 backdrop-blur" aria-label="Onglets chaîne">
|
||||
<div class="flex gap-1 overflow-x-auto py-2" role="tablist">
|
||||
<!-- Sticky offset must clear the fixed app header, otherwise the tab rail
|
||||
slides underneath it as soon as the page scrolls. -->
|
||||
<nav class="sticky z-20 -mx-4 border-b border-zinc-800 bg-zinc-950/95 px-4 backdrop-blur"
|
||||
[style.top]="'var(--header-offset)'"
|
||||
aria-label="Onglets chaîne">
|
||||
<div class="scroll-rail no-scrollbar flex gap-1 py-2" role="tablist">
|
||||
<button
|
||||
*ngFor="let tab of visibleTabs"
|
||||
type="button"
|
||||
role="tab"
|
||||
[attr.aria-selected]="tab.id === active"
|
||||
(click)="select.emit(tab.id)"
|
||||
class="whitespace-nowrap rounded-lg px-4 py-2 text-sm font-medium transition"
|
||||
class="chip shrink-0 whitespace-nowrap rounded-lg px-4 py-2 text-sm font-medium transition"
|
||||
[class.bg-red-500/20]="tab.id === active"
|
||||
[class.text-red-100]="tab.id === active"
|
||||
[class.text-zinc-400]="tab.id !== active"
|
||||
|
||||
Reference in New Issue
Block a user