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:
2026-09-28 14:38:31 -04:00
parent 69c722fdf9
commit 66f1da1d72
37 changed files with 1231 additions and 509 deletions
@@ -1,41 +1,41 @@
<div class="fixed inset-0 z-[9999] bg-black/60" (click)="close.emit()">
<div class="fixed inset-0 grid place-items-center">
<div class="fixed inset-0 grid place-items-center p-4" [style.paddingBottom]="'max(1rem, var(--safe-bottom))'">
<div #dialog
class="w-[90%] max-w-lg bg-slate-900 rounded-xl shadow-2xl border border-slate-700"
class="dialog-scroll flex w-full max-w-lg flex-col overflow-hidden rounded-xl border border-slate-700 bg-slate-900 shadow-2xl"
role="dialog"
aria-modal="true"
aria-labelledby="providersTitle"
(click)="$event.stopPropagation()"
tabindex="0"
(keydown)="onDialogKeydown($event)">
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
<h2 id="providersTitle" class="text-slate-100 font-semibold">Providers</h2>
<button type="button" (click)="close.emit()" class="px-2 py-1 rounded bg-slate-700 hover:bg-slate-600 text-slate-100" aria-label="Fermer (Esc)">Esc</button>
</div>
<div class="p-4 space-y-3">
<input type="text" [ngModel]="filter()" (ngModelChange)="filter.set($event)" placeholder="filter providers…" aria-label="Filtrer les fournisseurs" class="w-full px-3 py-2 rounded bg-slate-800 text-slate-100 placeholder-slate-400 border border-slate-700"/>
<div class="flex items-center gap-2">
<label class="inline-flex items-center gap-2 text-slate-200">
<input type="checkbox" [checked]="allMode()" (change)="toggleAllMode()"/>
<span>All</span>
<div class="sticky top-0 z-10 shrink-0 border-b border-slate-700 bg-slate-900 p-4 flex items-center justify-between gap-3">
<h2 id="providersTitle" class="text-slate-100 font-semibold">Providers</h2>
<button type="button" (click)="close.emit()" class="chip px-3 rounded bg-slate-700 hover:bg-slate-600 text-slate-100" aria-label="Fermer (Esc)">Esc</button>
</div>
<div class="p-4 space-y-3">
<input type="text" [ngModel]="filter()" (ngModelChange)="filter.set($event)" placeholder="filter providers…" aria-label="Filtrer les fournisseurs" class="w-full min-w-0 px-3 py-2 rounded bg-slate-800 text-slate-100 placeholder-slate-400 border border-slate-700"/>
<div class="flex items-center gap-2">
<label class="inline-flex items-center gap-2 text-slate-200 py-1">
<input type="checkbox" [checked]="allMode()" (change)="toggleAllMode()"/>
<span>All</span>
</label>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<label *ngFor="let p of providers()" class="flex items-center gap-2 px-3 py-3 rounded border border-slate-700 bg-slate-800 text-slate-100">
<input type="checkbox" class="shrink-0" [checked]="isChecked(p.id)" (change)="toggle(p.id)"/>
<span class="text-xs font-semibold shrink-0" [class]="p.colorClass">{{ p.shortLabel }}</span>
<span class="min-w-0 truncate text-sm">{{ p.displayName }}</span>
</label>
</div>
<label class="mt-2 inline-flex items-center gap-2 text-slate-300 py-1">
<input type="checkbox" [ngModel]="rememberDefault()" (ngModelChange)="rememberDefault.set($event)" />
<span>Remember as default</span>
</label>
</div>
<div class="grid grid-cols-2 gap-2">
<label *ngFor="let p of providers()" class="flex items-center gap-2 px-2 py-2 rounded border border-slate-700 bg-slate-800 text-slate-100">
<input type="checkbox" [checked]="isChecked(p.id)" (change)="toggle(p.id)"/>
<span class="text-xs font-semibold" [class]="p.colorClass">{{ p.shortLabel }}</span>
<span class="text-sm">{{ p.displayName }}</span>
</label>
<div class="sticky bottom-0 z-10 shrink-0 border-t border-slate-700 bg-slate-900 p-4 flex flex-col-reverse gap-2 sm:flex-row sm:items-center sm:justify-end">
<button type="button" (click)="close.emit()" class="px-4 py-3 rounded bg-slate-700 hover:bg-slate-600 text-slate-100">Cancel</button>
<button type="button" (click)="onApply()" class="px-4 py-3 rounded bg-red-600 hover:bg-red-500 text-white">Apply filters</button>
</div>
<label class="mt-2 inline-flex items-center gap-2 text-slate-300">
<input type="checkbox" [ngModel]="rememberDefault()" (ngModelChange)="rememberDefault.set($event)" />
<span>Remember as default</span>
</label>
</div>
<div class="p-4 border-t border-slate-700 flex items-center justify-end gap-2">
<button type="button" (click)="close.emit()" class="px-3 py-2 rounded bg-slate-700 hover:bg-slate-600 text-slate-100">Cancel</button>
<button type="button" (click)="onApply()" class="px-3 py-2 rounded bg-red-600 hover:bg-red-500 text-white">Apply filters</button>
</div>
</div>
</div>
</div>
+68 -59
View File
@@ -1,48 +1,62 @@
<form class="w-full relative" novalidate (submit)="onSubmit($event)" role="search" aria-label="Recherche">
<!-- Input -->
<div class="flex items-center gap-2 rounded-lg ring-1 ring-slate-600/50 bg-slate-800/70 px-3 py-2">
<!-- Provider chips -->
<button type="button"
(click)="toggleAll()"
class="text-xs px-2 py-0.5 rounded border"
[ngClass]="{
'bg-slate-200 text-slate-800 border-slate-300': providerMode()==='all',
'bg-slate-700 text-slate-100 border-slate-600': providerMode()!=='all'
}"
[attr.aria-pressed]="providerMode()==='all'"
title="Tous les fournisseurs">
All
</button>
<ng-container *ngFor="let c of chips; let i = index">
<button type="button" (click)="toggleChip(c.id)" (keydown.space)="$event.preventDefault(); toggleChip(c.id)" (keydown.enter)="$event.preventDefault(); toggleChip(c.id)" tabindex="0" role="button"
class="text-xs px-2 py-0.5 rounded border"
<form class="w-full min-w-0 relative" novalidate (submit)="onSubmit($event)" role="search" aria-label="Recherche">
<!-- Input row -->
<div class="flex items-center gap-2 rounded-lg ring-1 ring-slate-600/50 bg-slate-800/70 px-2 py-1.5 sm:px-3 sm:py-2">
<!-- Provider chips: only when there is genuinely room for them (>= lg).
On smaller screens the "@" picker below is the equivalent entry point. -->
<div class="hidden lg:flex items-center gap-1.5 shrink-0">
<button type="button"
(click)="toggleAll()"
class="chip text-xs px-2.5 rounded border transition-colors"
[ngClass]="{
'bg-slate-700 text-slate-100 border-slate-600': !(selected().includes(c.id)),
'bg-slate-200 text-slate-800 border-slate-300': selected().includes(c.id)
'bg-slate-200 text-slate-800 border-slate-300': providerMode()==='all',
'bg-slate-700 text-slate-100 border-slate-600': providerMode()!=='all'
}"
[attr.aria-pressed]="selected().includes(c.id)"
[title]="'Inclure ' + c.label + ' (Alt+' + (i + 1) + ')'">
<span [class]="c.colorClass">{{ c.label }}</span>
[attr.aria-pressed]="providerMode()==='all'"
title="Tous les fournisseurs">
All
</button>
</ng-container>
<ng-container *ngFor="let c of chips; let i = index">
<button type="button" (click)="toggleChip(c.id)" (keydown.space)="$event.preventDefault(); toggleChip(c.id)" (keydown.enter)="$event.preventDefault(); toggleChip(c.id)" tabindex="0" role="button"
class="chip text-xs px-2.5 rounded border transition-colors"
[ngClass]="{
'bg-slate-700 text-slate-100 border-slate-600': !(selected().includes(c.id)),
'bg-slate-200 text-slate-800 border-slate-300': selected().includes(c.id)
}"
[attr.aria-pressed]="selected().includes(c.id)"
[title]="'Inclure ' + c.label + ' (Alt+' + (i + 1) + ')'">
<span [class]="c.colorClass">{{ c.label }}</span>
</button>
</ng-container>
</div>
<!-- Text input: combobox pattern for the inline '@' autocomplete -->
<input #qInput type="search"
class="flex-1 bg-transparent outline-none text-slate-100 placeholder-slate-400 px-2"
class="min-w-0 flex-1 bg-transparent outline-none text-slate-100 placeholder-slate-400 px-1 sm:px-2 py-2"
[placeholder]="placeholder"
[ngModel]="query()" (ngModelChange)="queryUpdate($event)" (keydown)="handleKeydown($event)" (keydown.enter)="onSubmit($event)" (focus)="onSuggestInputFocus()" name="q"
autocomplete="off" aria-label="Rechercher des vidéos"
autocomplete="off" aria-label="Rechercher des vidéos" enterkeyhint="search"
role="combobox" aria-expanded="false" aria-autocomplete="list"
[attr.aria-expanded]="atOpen() || suggestionsOpen()"
[attr.aria-controls]="atOpen() ? 'atOptionsList' : (suggestionsOpen() ? 'sgOptionsList' : null)"
[attr.aria-activedescendant]="atOpen() && atOptions().length > 0 ? 'atOption-' + atIndex() : (suggestionsOpen() && suggestIndex() >= 0 ? 'sgOption-' + suggestIndex() : null)"/>
<!-- Actions -->
<button type="button" (click)="openPicker()" class="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600" aria-label="Choisir des fournisseurs (@)">@</button>
<button type="submit" class="ml-1 px-3 py-1 rounded bg-red-600 hover:bg-red-500 text-white"
<!-- Provider picker trigger: shows the active selection as a summary so the
user always knows where they are searching. -->
<button type="button" (click)="openPicker()"
class="chip shrink-0 text-xs font-medium px-2.5 rounded bg-slate-700 text-slate-100 hover:bg-slate-600"
aria-haspopup="dialog"
[attr.aria-label]="'Fournisseurs : ' + providerSummary()">
<span class="max-w-[5.5rem] truncate lg:hidden">{{ providerSummary() }}</span>
<span class="hidden lg:inline" aria-hidden="true">@</span>
</button>
<button type="submit" class="chip ml-auto shrink-0 inline-flex items-center gap-1.5 rounded bg-red-600 px-3 text-sm text-white hover:bg-red-500"
(click)="onSubmit($event)"
[attr.title]="(query() || '').trim().length < 2 ? 'Tapez au moins 2 caractères' : null">
Search
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 lg:hidden" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5Zm-6 0A4.5 4.5 0 1 1 14 9.5 4.49 4.49 0 0 1 9.5 14Z"/>
</svg>
<span class="hidden lg:inline">Search</span>
</button>
</div>
@@ -55,16 +69,16 @@
<!-- Inline @autocomplete popover -->
<div *ngIf="atOpen() && atOptions().length > 0"
class="absolute z-50 mt-1 left-3 right-3 rounded-lg shadow-xl overflow-hidden"
class="absolute z-50 mt-1 left-0 right-0 max-h-[60dvh] overflow-y-auto rounded-lg shadow-xl"
style="top: 100%">
<ul id="atOptionsList" role="listbox" aria-label="Fournisseurs"
class="max-h-64 overflow-y-auto"
class="max-h-[60dvh] overflow-y-auto"
[ngClass]="{ 'bg-slate-900 border border-slate-700 text-slate-200': true }">
<li *ngFor="let opt of atOptions(); let i = index" role="option"
[attr.aria-selected]="i === atIndex()"
[id]="'atOption-' + i">
<button type="button" (click)="applyAtOption(opt.id)"
class="w-full text-left px-3 py-2 text-sm"
class="w-full text-left px-3 py-3 text-sm"
[ngClass]="{
'bg-slate-800': i === atIndex(),
'hover:bg-slate-800': i !== atIndex()
@@ -77,43 +91,39 @@
<!-- Step 15: query typeahead panel (hidden while the @ popover is open) -->
<div *ngIf="suggestionsOpen() && !atOpen() && suggestFlat().length > 0"
class="absolute z-40 mt-1 left-3 right-3 rounded-lg shadow-xl overflow-hidden bg-slate-900 border border-slate-700 text-slate-200"
class="absolute z-40 mt-1 left-0 right-0 max-h-[60dvh] overflow-y-auto rounded-lg shadow-xl bg-slate-900 border border-slate-700 text-slate-200"
style="top: 100%">
<ul id="sgOptionsList" role="listbox" aria-label="Suggestions de recherche"
class="max-h-72 overflow-y-auto py-1">
class="max-h-[60dvh] overflow-y-auto py-1">
<li *ngFor="let item of suggestFlat(); let i = index" role="option"
[attr.aria-selected]="i === suggestIndex()"
[id]="'sgOption-' + i">
<button type="button" (click)="applySuggestion(item.text, true)"
(mouseenter)="suggestIndex.set(i)"
class="w-full flex items-center gap-2 text-left px-3 py-2 text-sm"
[ngClass]="{
'bg-slate-800': i === suggestIndex(),
'hover:bg-slate-800': i !== suggestIndex()
}">
<span *ngIf="!item.provider" class="text-slate-500" title="Recherche récente">🕘</span>
<span *ngIf="item.provider" class="text-[10px] font-bold px-1.5 py-0.5 rounded bg-slate-700 text-slate-300">{{ providerBadge(item.provider) }}</span>
<span class="flex-1 truncate" [title]="item.text"><span>{{ suggestHighlight(item.text).pre }}</span><mark *ngIf="suggestHighlight(item.text).match" class="bg-transparent text-inherit font-semibold underline decoration-red-500">{{ suggestHighlight(item.text).match }}</mark><span>{{ suggestHighlight(item.text).post }}</span></span>
class="w-full flex items-center gap-2 text-left px-3 py-3 text-sm">
<span *ngIf="!item.provider" class="text-slate-500 shrink-0" title="Recherche récente">🕘</span>
<span *ngIf="item.provider" class="shrink-0 text-[10px] font-bold px-1.5 py-0.5 rounded bg-slate-700 text-slate-300">{{ providerBadge(item.provider) }}</span>
<span class="min-w-0 flex-1 truncate" [title]="item.text"><span>{{ suggestHighlight(item.text).pre }}</span><mark *ngIf="suggestHighlight(item.text).match" class="bg-transparent text-inherit font-semibold underline decoration-red-500">{{ suggestHighlight(item.text).match }}</mark><span>{{ suggestHighlight(item.text).post }}</span></span>
</button>
</li>
</ul>
<div *ngIf="suggestLoading()" class="px-3 py-1 text-xs text-slate-500 border-t border-slate-800">Chargement…</div>
<div *ngIf="suggestLoading()" class="px-3 py-2 text-xs text-slate-500 border-t border-slate-800">Chargement…</div>
</div>
<!-- Quick Menu (Ctrl/Cmd+K) -->
<div *ngIf="quickOpen()" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999; background-color: rgba(0, 0, 0, 0.6);" role="presentation" (click)="closeQuickMenu()">
<div #quickDialog style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -0%); width: 90%; max-width: 56rem; max-height: 85vh; border-radius: 0.75rem; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); overflow: hidden; background-color: #0f172a; border: 1px solid #334155; display: flex; flex-direction: column;" role="dialog" aria-modal="true" aria-labelledby="quickMenuTitle" (click)="$event.stopPropagation()" (keydown)="onQuickMenuKeydown($event)">
<div class="p-3 border-b border-slate-700 flex items-center justify-between">
<div *ngIf="quickOpen()" class="quick-menu-backdrop" role="presentation" (click)="closeQuickMenu()">
<div #quickDialog class="quick-menu-dialog" role="dialog" aria-modal="true" aria-labelledby="quickMenuTitle" (click)="$event.stopPropagation()" (keydown)="onQuickMenuKeydown($event)">
<div class="p-3 border-b border-slate-700 flex items-center justify-between gap-3">
<h2 id="quickMenuTitle" class="text-slate-100 font-semibold">Quick Menu</h2>
<button type="button" (click)="closeQuickMenu()" class="px-2 py-1 rounded bg-slate-700 hover:bg-slate-600 text-slate-100" aria-label="Fermer le menu (Esc)">Esc</button>
<button type="button" (click)="closeQuickMenu()" class="chip px-3 rounded bg-slate-700 hover:bg-slate-600 text-slate-100" aria-label="Fermer le menu (Esc)">Esc</button>
</div>
<div class="grid grid-cols-2 gap-0 overflow-y-auto">
<div class="grid grid-cols-1 md:grid-cols-2 overflow-y-auto min-h-0 flex-1">
<!-- History column -->
<div class="p-4 border-r border-slate-700">
<div class="p-4 md:border-r md:border-slate-700">
<div class="text-slate-300 text-sm mb-2">History</div>
<ul class="space-y-1 max-h-80 overflow-y-auto">
<ul class="space-y-1 max-h-64 overflow-y-auto">
<li *ngFor="let it of recentSearches()">
<button type="button" class="w-full text-left px-2 py-1 rounded hover:bg-slate-800 text-slate-200"
<button type="button" class="w-full text-left px-3 py-3 rounded hover:bg-slate-800 text-slate-200 break-anywhere"
(click)="queryUpdate(it.query || ''); closeQuickMenu()">{{ it.query }}</button>
</li>
<li *ngIf="(recentSearches()?.length || 0) === 0" class="text-slate-500 text-sm">No recent searches.</li>
@@ -121,20 +131,19 @@
</div>
<!-- Providers column -->
<div class="p-4">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center justify-between gap-3 mb-2">
<div class="text-slate-300 text-sm">Providers</div>
<button type="button" class="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600 text-slate-100"
<button type="button" class="chip text-xs px-3 rounded bg-slate-700 hover:bg-slate-600 text-slate-100"
(click)="quickToggleAll()">All</button>
</div>
<div class="grid grid-cols-3 gap-2">
<label *ngFor="let c of chips" class="flex items-center gap-2 px-2 py-2 rounded border border-slate-700 bg-slate-800 text-slate-100">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<label *ngFor="let c of chips" class="flex items-center gap-2 px-3 py-3 rounded border border-slate-700 bg-slate-800 text-slate-100">
<input type="checkbox" [checked]="selected().includes(c.id)" (change)="quickToggle(c.id)"/>
<span class="text-xs font-semibold" [class]="c.colorClass">{{ c.label }}</span>
<span class="text-sm">{{ c.label }}</span>
<span class="text-sm truncate" [class]="c.colorClass">{{ c.label }}</span>
</label>
</div>
<div class="mt-4 text-right">
<button type="button" class="px-3 py-2 rounded bg-red-600 hover:bg-red-500 text-white"
<button type="button" class="w-full sm:w-auto px-4 py-3 rounded bg-red-600 hover:bg-red-500 text-white"
(click)="applyQuickMenu({ selection: selected().length ? selected() : 'all', rememberDefault: false })">Apply filters</button>
</div>
</div>
@@ -40,6 +40,12 @@ export interface SuggestFlatItem {
templateUrl: './search-box.component.html'
})
export class SearchBoxComponent {
/**
* Host element, so a parent can tell whether this instance is the visible one.
* Optional: unit tests instantiate the component without a host element.
*/
readonly el = inject(ElementRef, { optional: true });
private search = inject(SearchService);
private users = inject(UserService);
private history = inject(HistoryService);
@@ -130,11 +136,35 @@ export class SearchBoxComponent {
readonly chips = PROVIDERS.map(p => ({ id: p.id, label: p.shortLabel, colorClass: p.colorClass }));
/**
* Focus the query input programmatically (Ctrl/Cmd+K, `/`, mobile search
* sheet). Exposed so the parent header does not have to reach into the DOM.
*/
focusInput(): HTMLInputElement | undefined {
try { return this.qInputRef?.nativeElement; } catch { return undefined; }
}
// Derived state
readonly providersForRequest = computed<ProviderId[] | 'all'>(() => {
return this.providerMode() === 'all' ? 'all' : (this.selected().length ? this.selected() : 'all');
});
/**
* Short human label for the current provider selection. On phones the chip
* row is hidden, so this label is the only feedback the user gets about
* where the search will be routed.
*/
readonly providerSummary = computed<string>(() => {
if (this.providerMode() === 'all') return 'All';
const sel = this.selected();
if (!sel.length) return 'All';
if (sel.length === 1) {
const found = PROVIDERS.find((p) => p.id === sel[0]);
return found ? found.shortLabel : String(sel[0]).toUpperCase();
}
return `${sel.length} src`;
});
constructor() {
// Flag to avoid clobbering user input after initial hydration from SearchService
let hydratedFromService = false;
+9 -9
View File
@@ -26,10 +26,10 @@
}
@if (hasQuery() && availableTags().length > 0) {
<div class="mb-4 flex items-center gap-2 overflow-x-auto pb-1">
<div class="scroll-rail no-scrollbar mb-4 flex items-center gap-2 pb-1">
@for (tag of availableTags(); track tag.key) {
<button
class="px-3 py-1 rounded-full text-sm whitespace-nowrap border transition-colors"
class="chip shrink-0 px-3 py-2 rounded-full text-sm whitespace-nowrap border transition-colors"
[ngClass]="{
'bg-slate-200 text-slate-900 border-slate-200': filterTag() === tag.key,
'bg-slate-800 text-slate-200 border-slate-600 hover:bg-slate-700': filterTag() !== tag.key
@@ -43,35 +43,35 @@
@if (hasQuery()) {
<!-- Pills de type façon directory Twitch : Directs / Vidéos / Clips / Chaînes -->
<div class="flex items-center gap-2 overflow-x-auto pb-1" role="tablist" aria-label="Filtrer par type de contenu">
<button class="px-3 py-1 rounded-full text-sm whitespace-nowrap border transition-colors"
<div class="scroll-rail no-scrollbar flex items-center gap-2 pb-1" role="tablist" aria-label="Filtrer par type de contenu">
<button class="chip shrink-0 px-3 py-2 rounded-full text-sm whitespace-nowrap border transition-colors"
[ngClass]="typePill() === 'all' ? 'bg-slate-200 text-slate-900 border-slate-200' : 'bg-slate-800 text-slate-200 border-slate-600 hover:bg-slate-700'"
[attr.aria-pressed]="typePill() === 'all'" (click)="setTypePill('all')">
Tout ({{ typeCounts().all }})
</button>
<button *ngIf="typeCounts().live > 0 || typePill() === 'live'" class="px-3 py-1 rounded-full text-sm whitespace-nowrap border transition-colors"
<button *ngIf="typeCounts().live > 0 || typePill() === 'live'" class="chip shrink-0 px-3 py-2 rounded-full text-sm whitespace-nowrap border transition-colors"
[ngClass]="typePill() === 'live' ? 'bg-purple-600 text-white border-purple-500' : 'bg-slate-800 text-slate-200 border-slate-600 hover:bg-slate-700'"
[attr.aria-pressed]="typePill() === 'live'" (click)="setTypePill('live')">
Directs ({{ typeCounts().live }})
</button>
<button *ngIf="typeCounts().video > 0 || typePill() === 'video'" class="px-3 py-1 rounded-full text-sm whitespace-nowrap border transition-colors"
<button *ngIf="typeCounts().video > 0 || typePill() === 'video'" class="chip shrink-0 px-3 py-2 rounded-full text-sm whitespace-nowrap border transition-colors"
[ngClass]="typePill() === 'video' ? 'bg-slate-200 text-slate-900 border-slate-200' : 'bg-slate-800 text-slate-200 border-slate-600 hover:bg-slate-700'"
[attr.aria-pressed]="typePill() === 'video'" (click)="setTypePill('video')">
Vidéos ({{ typeCounts().video }})
</button>
<button *ngIf="typeCounts().clip > 0 || typePill() === 'short'" class="px-3 py-1 rounded-full text-sm whitespace-nowrap border transition-colors"
<button *ngIf="typeCounts().clip > 0 || typePill() === 'short'" class="chip shrink-0 px-3 py-2 rounded-full text-sm whitespace-nowrap border transition-colors"
[ngClass]="typePill() === 'short' ? 'bg-red-600 text-white border-red-500' : 'bg-slate-800 text-slate-200 border-slate-600 hover:bg-slate-700'"
[attr.aria-pressed]="typePill() === 'short'" (click)="setTypePill('short')">
Clips ({{ typeCounts().clip }})
</button>
<button *ngIf="typeCounts().channel > 0 || typePill() === 'channel'" class="px-3 py-1 rounded-full text-sm whitespace-nowrap border transition-colors"
<button *ngIf="typeCounts().channel > 0 || typePill() === 'channel'" class="chip shrink-0 px-3 py-2 rounded-full text-sm whitespace-nowrap border transition-colors"
[ngClass]="typePill() === 'channel' ? 'bg-slate-200 text-slate-900 border-slate-200' : 'bg-slate-800 text-slate-200 border-slate-600 hover:bg-slate-700'"
[attr.aria-pressed]="typePill() === 'channel'" (click)="setTypePill('channel')">
Chaînes ({{ typeCounts().channel }})
</button>
</div>
<!-- Barre de filtrage (même UX que la page thème / fournisseur) -->
<div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700 my-4">
<div class="rounded-xl border border-slate-700 bg-slate-800/50 p-4 my-4">
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-5 gap-4">
<div>
<label for="sortSelect" class="block text-sm font-medium text-slate-300 mb-1">{{ 'filter.sort' | t }}</label>