fix(search): le repli des rails ne masque plus la grille + logos de marque
CI / build-and-test (push) Successful in 14m9s
CI / build-and-test (push) Successful in 14m9s
Bug : le @if (hasQuery()) englobant toute la section basse avait ete passe en @if (hasQuery() && railsOpen()), donc replier le rail masquait aussi la grille, le loader et le sentinel de scroll infini. Seul le panneau ' types + sources ' est desormais imbrique dans @if (railsOpen()). Logos : les formes geometriques approximees sont remplacees par les six marques officielles (traces et couleurs Simple Icons, CC0-1.0), stockees en local pour eviter toute dependance CDN a l'execution. Dailymotion garde le bleu de la marque (#0066DC) plutot que le #0A0A0A de Simple Icons, invisible sur fond sombre. L'etat actif/masque n'est plus porte par la couleur de l'icone (fixe par marque) mais par le bouton : fond plein + logo couleur, ou fond transparent + logo grayscale 40%.
This commit is contained in:
@@ -3,70 +3,77 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
|||||||
/** Clés courtes du registre fournisseurs (provider-registry.ts). */
|
/** Clés courtes du registre fournisseurs (provider-registry.ts). */
|
||||||
export type ProviderIconKey = 'yt' | 'dm' | 'tw' | 'pt' | 'od' | 'ru';
|
export type ProviderIconKey = 'yt' | 'dm' | 'tw' | 'pt' | 'od' | 'ru';
|
||||||
|
|
||||||
|
interface ProviderBrand {
|
||||||
|
/** Nom affiché, utilisé pour les libellés accessibles. */
|
||||||
|
label: string;
|
||||||
|
/** Couleur de marque officielle. */
|
||||||
|
color: string;
|
||||||
|
/** Tracé 24×24. */
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marque d'une source, dessinée au trait et colorée en `currentColor` :
|
* Marques officielles des six sources, aux couleurs de marque.
|
||||||
* - l'icône hérite de la couleur du bouton, donc l'état actif / masqué
|
|
||||||
* reste lisible sans ajouter une pastille de couleur à côté ;
|
|
||||||
* - un seul style graphique pour les six sources, cohérent avec les icônes
|
|
||||||
* d'interface (œil, chevron…) de la même barre.
|
|
||||||
*
|
*
|
||||||
* Les formes sont volontairement géométriques et sobres : l'identité vient du
|
* Tracés issus de Simple Icons (CC0-1.0) et stockés en local : aucune
|
||||||
* geste (bouton lecture, bulle de discussion, initiale), pas d'un logo coloré
|
* requête vers un CDN au runtime, donc rien à afficher en cas de réseau
|
||||||
* qui ne serait plus lisible une fois réduit à 18 px.
|
* lent ou de politique CSP. Une seule passe de rendu, une seule source de
|
||||||
|
* vérité pour les logos de sources.
|
||||||
|
*/
|
||||||
|
const PROVIDER_BRANDS: Record<ProviderIconKey, ProviderBrand> = {
|
||||||
|
yt: {
|
||||||
|
label: 'YouTube',
|
||||||
|
color: '#FF0000',
|
||||||
|
path: 'M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z',
|
||||||
|
},
|
||||||
|
tw: {
|
||||||
|
label: 'Twitch',
|
||||||
|
color: '#9146FF',
|
||||||
|
path: 'M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z',
|
||||||
|
},
|
||||||
|
dm: {
|
||||||
|
label: 'Dailymotion',
|
||||||
|
// Simple Icons ships #0A0A0A (monochrome-safe) ; on garde le bleu de la
|
||||||
|
// marque, illisible sur les fonds sombres de l'application.
|
||||||
|
color: '#0066DC',
|
||||||
|
path: 'M21.823 7.327a11.928 11.928 0 0 0-2.606-3.814 12.126 12.126 0 0 0-3.866-2.57A12.246 12.246 0 0 0 10.617 0H1.831a.602.602 0 0 0-.609.603v3.764c0 .162.064.312.179.426l4.164 4.123a.612.612 0 0 0 .439.177h4.56c.806 0 1.56.313 2.125.88.557.559.856 1.296.843 2.075-.029 1.571-1.343 2.849-2.931 2.849h-6.74a.613.613 0 0 0-.432.176.619.619 0 0 0-.178.427v3.764c0 .162.063.312.178.427l4.139 4.099a.647.647 0 0 0 .476.21h2.572a12.276 12.276 0 0 0 4.733-.945 12.145 12.145 0 0 0 3.866-2.571 11.959 11.959 0 0 0 2.607-3.813c.633-1.479.956-3.051.956-4.67 0-1.619-.321-3.19-.956-4.669l.001-.005ZM2.441 4.118V1.982l2.945 2.755.001 2.297-2.946-2.916Zm4.975 17.813-2.945-2.917v-2.137l2.945 2.755v2.299Zm-2.004-5.832h5.19c2.248 0 4.107-1.807 4.147-4.03a4.027 4.027 0 0 0-1.192-2.937 4.203 4.203 0 0 0-2.996-1.239H6.606V5.216h3.996c1.831 0 3.553.706 4.849 1.986a6.724 6.724 0 0 1-.152 9.736 6.875 6.875 0 0 1-4.697 1.84H8.275L5.412 16.1v-.001Zm15.289.1a10.753 10.753 0 0 1-2.345 3.431 10.91 10.91 0 0 1-3.48 2.314 11.018 11.018 0 0 1-4.26.847H8.633v-2.814h1.916c2.145 0 4.161-.802 5.675-2.254a7.88 7.88 0 0 0 2.451-5.728c0-2.177-.87-4.21-2.451-5.728-1.514-1.454-3.528-2.254-5.675-2.254h-4.16L3.383 1.202h7.234c1.479 0 2.911.285 4.259.847a10.957 10.957 0 0 1 3.48 2.313 10.769 10.769 0 0 1 2.345 3.432c.571 1.33.86 2.743.86 4.202 0 1.46-.289 2.873-.86 4.203Z',
|
||||||
|
},
|
||||||
|
pt: {
|
||||||
|
label: 'PeerTube',
|
||||||
|
color: '#F1680D',
|
||||||
|
path: 'M12 6.545v10.91L20.727 12M3.273 12v12L12 17.455M3.273 0v12L12 6.545',
|
||||||
|
},
|
||||||
|
od: {
|
||||||
|
label: 'Odysee',
|
||||||
|
color: '#EF1970',
|
||||||
|
path: 'M11.965 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12 12 12 0 0 0-3.209-8.167 7.272 7.272 0 0 1-.799 3.218c-.548.957-2.281 2.77-3.264 3.699a.723.723 0 0 0 .056 1.104c.996.74 2.658 2.151 2.788 3.422.176 1.835 1.6 4.02 1.675 4.159a.67.67 0 0 1-.105.327 12.067 12.067 0 0 1-2.03 1.898 2.435 2.435 0 0 1-.807.126c-1.944-.04-1.526-1.866-1.712-2.905s-.78-3.085-2.716-2.788c0 0 .484 4.243-1.489 5.546s-5.843 2.27-6.55-.408 2.46-2.384 2.684-2.384c.223 0 2.233-.632 1.267-2.53-.967-1.898-2.01-3.5-2.01-3.5a11.37 11.37 0 0 0-2.735 1.285 5.42 5.42 0 0 0-1.061.82c-1.065 1.104-2.19 1.713-2.954 1.358a1.368 1.368 0 0 1-.32-.221A11.926 11.926 0 0 1 .1 13.503c.43-.641 2.082-2.038 3.696-2.906 1.304-.702 2.737-.988 3.118-1.355-.671-2.235-1.882-5.703.832-7.33C9.881.634 12.69-.142 13.77 2.958c1.08 3.1.802 3.796 1.267 3.796.465 0 1.608.223 2.09-1.75.356-1.445.574-2.685 1.379-3.087A12 12 0 0 0 12 0a12 12 0 0 0-.035 0zm-.498 2.125c-.353-.019-.78.05-1.303.224 0 0-1.895.52-1.749 2.53.13 1.777 1.08 2.753 3.053 2.01 1.972-.737 2.31-1.264 1.824-2.753-.364-1.117-.765-1.956-1.825-2.011zm.48.726a.249.249 0 0 1 .183.07 1.58 1.58 0 0 1 .232.346.253.253 0 0 1-.197.37.253.253 0 0 1-.233-.131 1.559 1.559 0 0 0-.148-.232.256.256 0 0 1 0-.353.249.249 0 0 1 .163-.07zm3.529.152a.22.22 0 0 1 .192.103.22.22 0 0 1-.07.305.22.22 0 1 1-.122-.408zM12.609 4.27a.253.253 0 0 1 .227.133 1.5 1.5 0 0 1 .102.982.253.253 0 0 1-.246.205h-.049a.253.253 0 0 1-.2-.295 1.013 1.013 0 0 0-.071-.697.253.253 0 0 1 .237-.328zm-8.035.552a.12.12 0 0 1 .029.003.12.12 0 1 1-.147.091.12.12 0 0 1 .118-.094zm15.77 3.419.273.539.593.12-.539.275-.123.592-.272-.539-.592-.123.538-.272zm-17.767.535a.22.22 0 0 1 .193.104.22.22 0 0 1-.07.304.22.22 0 1 1-.123-.408zm16.004 2.79a.258.258 0 0 1 .062.007.258.258 0 1 1-.31.195.258.258 0 0 1 .248-.202zM4.914 16.115a.17.17 0 0 1 .165.14.17.17 0 0 1-.137.197.17.17 0 1 1-.028-.337zm9.769 4.094a.2.2 0 0 1 .036.004.2.2 0 1 1-.233.15.2.2 0 0 1 .197-.154z',
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
label: 'Rumble',
|
||||||
|
color: '#85C742',
|
||||||
|
path: 'M14.4528 13.5458c.8064-.6542.9297-1.8381.2756-2.6445a1.8802 1.8802 0 0 0-.2756-.2756 21.2127 21.2127 0 0 0-4.3121-2.776c-1.066-.51-2.256.2-2.4261 1.414a23.5226 23.5226 0 0 0-.14 5.5021c.116 1.23 1.292 1.964 2.372 1.492a19.6285 19.6285 0 0 0 4.5062-2.704v-.008zm6.9322-5.4002c2.0335 2.228 2.0396 5.637.014 7.8723A26.1487 26.1487 0 0 1 8.2946 23.846c-2.6848.6713-5.4168-.914-6.1662-3.5781-1.524-5.2002-1.3-11.0803.17-16.3045.772-2.744 3.3521-4.4661 6.0102-3.832 4.9242 1.174 9.5443 4.196 13.0764 8.0121v.002z',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const PROVIDER_ICON_KEYS = Object.keys(PROVIDER_BRANDS) as ProviderIconKey[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logo d'une source, aux couleurs de la marque.
|
||||||
|
*
|
||||||
|
* L'état actif / masqué n'est plus porté par la couleur de l'icône (elle est
|
||||||
|
* fixe par marque) mais par l'état du bouton qui la contient : fond plein et
|
||||||
|
* logo en couleur quand la source est affichée, fond transparent et logo
|
||||||
|
* désaturé quand elle est masquée.
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-provider-icon',
|
selector: 'app-provider-icon',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
@switch (normalized) {
|
@if (brand; as b) {
|
||||||
@case ('yt') {
|
<svg [attr.width]="size" [attr.height]="size" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<!-- YouTube : cadre arrondi + triangle de lecture -->
|
<path [attr.d]="b.path" [attr.fill]="b.color" />
|
||||||
<svg [attr.width]="size" [attr.height]="size" viewBox="0 0 24 24" fill="none"
|
</svg>
|
||||||
stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<rect x="2.5" y="5.5" width="19" height="13" rx="4" />
|
|
||||||
<path d="M10.4 9.4v5.2l4.4-2.6-4.4-2.6Z" fill="currentColor" stroke="none" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
@case ('tw') {
|
|
||||||
<!-- Twitch : bulle de discussion + deux yeux -->
|
|
||||||
<svg [attr.width]="size" [attr.height]="size" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<path d="M4 3h16v11.2L14.8 19.4h-4.3L7.2 22.7V19.4H4V3Z" />
|
|
||||||
<path d="M9.2 8.4v2.8M14.8 8.4v2.8" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
@case ('dm') {
|
|
||||||
<!-- Dailymotion : « D » -->
|
|
||||||
<svg [attr.width]="size" [attr.height]="size" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<path d="M5.5 4.5v15" />
|
|
||||||
<path d="M5.5 4.5h4.2a7.5 7.5 0 0 1 0 15H5.5" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
@case ('pt') {
|
|
||||||
<!-- PeerTube : « P » -->
|
|
||||||
<svg [attr.width]="size" [attr.height]="size" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<path d="M7.5 20V4.5h5.6a5.4 5.4 0 0 1 0 10.8H7.5" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
@case ('od') {
|
|
||||||
<!-- Odysee : « O » barré -->
|
|
||||||
<svg [attr.width]="size" [attr.height]="size" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<circle cx="12" cy="12" r="8.2" />
|
|
||||||
<path d="M8.3 15.7 15.7 8.3" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
@case ('ru') {
|
|
||||||
<!-- Rumble : « R » -->
|
|
||||||
<svg [attr.width]="size" [attr.height]="size" viewBox="0 0 24 24" fill="none"
|
|
||||||
stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<path d="M6 20V4.5h6.2a4.6 4.6 0 0 1 0 9.2H6" />
|
|
||||||
<path d="M12.4 13.7 17.6 20" />
|
|
||||||
</svg>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
})
|
})
|
||||||
@@ -74,13 +81,13 @@ export class ProviderIconComponent {
|
|||||||
/** Clé courte du fournisseur. Inconnue => aucun rendu. */
|
/** Clé courte du fournisseur. Inconnue => aucun rendu. */
|
||||||
@Input() key: ProviderIconKey | string | null | undefined;
|
@Input() key: ProviderIconKey | string | null | undefined;
|
||||||
/** Taille en px (carré). */
|
/** Taille en px (carré). */
|
||||||
@Input() size = 18;
|
@Input() size = 20;
|
||||||
|
|
||||||
/** Clé normalisée ; seules les six clés connues dessinent quelque chose. */
|
/** Marque correspondante, ou null si la clé n'est pas connue. */
|
||||||
get normalized(): ProviderIconKey | null {
|
get brand(): ProviderBrand | null {
|
||||||
const k = String(this.key || '').toLowerCase();
|
const k = String(this.key || '').toLowerCase();
|
||||||
return (['yt', 'dm', 'tw', 'pt', 'od', 'ru'] as const).includes(k as ProviderIconKey)
|
return PROVIDER_ICON_KEYS.includes(k as ProviderIconKey)
|
||||||
? (k as ProviderIconKey)
|
? PROVIDER_BRANDS[k as ProviderIconKey]
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,87 +68,94 @@
|
|||||||
<p class="mt-2 text-slate-400">{{ 'search.hint' | t }}</p>
|
<p class="mt-2 text-slate-400">{{ 'search.hint' | t }}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (hasQuery() && railsOpen()) {
|
@if (hasQuery()) {
|
||||||
<!-- ============ Rail de filtres (types + sources) ============ -->
|
<!-- ============ Rail de filtres (types + sources) ============ -->
|
||||||
<div id="search-rails" class="mt-2.5 rounded-xl border border-slate-700 bg-slate-800/50 p-1.5">
|
<!-- Repliable via l'icône œil : seule cette section disparaît, jamais la
|
||||||
<!-- Types de contenu : sélection unique, façon YouTube.
|
grille ni la pagination qui suivent. -->
|
||||||
Le rail défile horizontalement (scrollbar masqué) : le fade de droite
|
@if (railsOpen()) {
|
||||||
signale qu'il reste des pastilles hors champ. -->
|
<div id="search-rails" class="mt-2.5 rounded-xl border border-slate-700 bg-slate-800/50 p-1.5">
|
||||||
<div class="scroll-rail-fade">
|
<!-- Types de contenu : sélection unique, façon YouTube.
|
||||||
<div class="scroll-rail no-scrollbar flex items-center gap-1.5" role="group" aria-label="Filtrer par type de contenu">
|
Le rail défile horizontalement (scrollbar masqué) : le fade de droite
|
||||||
<button type="button" (click)="setTypePill('all')"
|
signale qu'il reste des pastilles hors champ. -->
|
||||||
class="chip inline-flex h-8 shrink-0 items-center whitespace-nowrap rounded-full px-3 text-[13px] font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]"
|
<div class="scroll-rail-fade">
|
||||||
[ngClass]="typePill() === 'all' ? 'border border-transparent bg-slate-200 text-slate-900' : 'border border-slate-700 bg-slate-800 text-slate-300 hover:bg-slate-700'"
|
<div class="scroll-rail no-scrollbar flex items-center gap-1.5" role="group" aria-label="Filtrer par type de contenu">
|
||||||
[attr.aria-pressed]="typePill() === 'all'">
|
<button type="button" (click)="setTypePill('all')"
|
||||||
Tout <span class="ml-1.5 text-[11px] tabular-nums opacity-60">{{ countLabel(typeCounts().all) }}</span>
|
class="chip inline-flex h-8 shrink-0 items-center whitespace-nowrap rounded-full px-3 text-[13px] font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]"
|
||||||
</button>
|
[ngClass]="typePill() === 'all' ? 'border border-transparent bg-slate-200 text-slate-900' : 'border border-slate-700 bg-slate-800 text-slate-300 hover:bg-slate-700'"
|
||||||
<button type="button" *ngIf="typeCounts().live > 0 || typePill() === 'live'" (click)="setTypePill('live')"
|
[attr.aria-pressed]="typePill() === 'all'">
|
||||||
class="chip inline-flex h-8 shrink-0 items-center whitespace-nowrap rounded-full px-3 text-[13px] font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]"
|
Tout <span class="ml-1.5 text-[11px] tabular-nums opacity-60">{{ countLabel(typeCounts().all) }}</span>
|
||||||
[ngClass]="typePill() === 'live' ? 'border border-transparent bg-purple-600 text-white' : 'border border-slate-700 bg-slate-800 text-slate-300 hover:bg-slate-700'"
|
</button>
|
||||||
[attr.aria-pressed]="typePill() === 'live'">
|
<button type="button" *ngIf="typeCounts().live > 0 || typePill() === 'live'" (click)="setTypePill('live')"
|
||||||
Directs <span class="ml-1.5 text-[11px] tabular-nums opacity-60">{{ countLabel(typeCounts().live) }}</span>
|
class="chip inline-flex h-8 shrink-0 items-center whitespace-nowrap rounded-full px-3 text-[13px] font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]"
|
||||||
</button>
|
[ngClass]="typePill() === 'live' ? 'border border-transparent bg-purple-600 text-white' : 'border border-slate-700 bg-slate-800 text-slate-300 hover:bg-slate-700'"
|
||||||
<button type="button" *ngIf="typeCounts().video > 0 || typePill() === 'video'" (click)="setTypePill('video')"
|
[attr.aria-pressed]="typePill() === 'live'">
|
||||||
class="chip inline-flex h-8 shrink-0 items-center whitespace-nowrap rounded-full px-3 text-[13px] font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]"
|
Directs <span class="ml-1.5 text-[11px] tabular-nums opacity-60">{{ countLabel(typeCounts().live) }}</span>
|
||||||
[ngClass]="typePill() === 'video' ? 'border border-transparent bg-slate-200 text-slate-900' : 'border border-slate-700 bg-slate-800 text-slate-300 hover:bg-slate-700'"
|
</button>
|
||||||
[attr.aria-pressed]="typePill() === 'video'">
|
<button type="button" *ngIf="typeCounts().video > 0 || typePill() === 'video'" (click)="setTypePill('video')"
|
||||||
Vidéos <span class="ml-1.5 text-[11px] tabular-nums opacity-60">{{ countLabel(typeCounts().video) }}</span>
|
class="chip inline-flex h-8 shrink-0 items-center whitespace-nowrap rounded-full px-3 text-[13px] font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]"
|
||||||
</button>
|
[ngClass]="typePill() === 'video' ? 'border border-transparent bg-slate-200 text-slate-900' : 'border border-slate-700 bg-slate-800 text-slate-300 hover:bg-slate-700'"
|
||||||
<button type="button" *ngIf="typeCounts().clip > 0 || typePill() === 'shorts'" (click)="setTypePill('shorts')"
|
[attr.aria-pressed]="typePill() === 'video'">
|
||||||
class="chip inline-flex h-8 shrink-0 items-center whitespace-nowrap rounded-full px-3 text-[13px] font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]"
|
Vidéos <span class="ml-1.5 text-[11px] tabular-nums opacity-60">{{ countLabel(typeCounts().video) }}</span>
|
||||||
[ngClass]="typePill() === 'shorts' ? 'border border-transparent bg-purple-600 text-white' : 'border border-slate-700 bg-slate-800 text-slate-300 hover:bg-slate-700'"
|
</button>
|
||||||
[attr.aria-pressed]="typePill() === 'shorts'">
|
<button type="button" *ngIf="typeCounts().clip > 0 || typePill() === 'shorts'" (click)="setTypePill('shorts')"
|
||||||
Clips <span class="ml-1.5 text-[11px] tabular-nums opacity-60">{{ countLabel(typeCounts().clip) }}</span>
|
class="chip inline-flex h-8 shrink-0 items-center whitespace-nowrap rounded-full px-3 text-[13px] font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]"
|
||||||
</button>
|
[ngClass]="typePill() === 'shorts' ? 'border border-transparent bg-purple-600 text-white' : 'border border-slate-700 bg-slate-800 text-slate-300 hover:bg-slate-700'"
|
||||||
<button type="button" *ngIf="typeCounts().channel > 0 || typePill() === 'channel'" (click)="setTypePill('channel')"
|
[attr.aria-pressed]="typePill() === 'shorts'">
|
||||||
class="chip inline-flex h-8 shrink-0 items-center whitespace-nowrap rounded-full px-3 text-[13px] font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]"
|
Clips <span class="ml-1.5 text-[11px] tabular-nums opacity-60">{{ countLabel(typeCounts().clip) }}</span>
|
||||||
[ngClass]="typePill() === 'channel' ? 'border border-transparent bg-slate-200 text-slate-900' : 'border border-slate-700 bg-slate-800 text-slate-300 hover:bg-slate-700'"
|
</button>
|
||||||
[attr.aria-pressed]="typePill() === 'channel'">
|
<button type="button" *ngIf="typeCounts().channel > 0 || typePill() === 'channel'" (click)="setTypePill('channel')"
|
||||||
Chaînes <span class="ml-1.5 text-[11px] tabular-nums opacity-60">{{ countLabel(typeCounts().channel) }}</span>
|
class="chip inline-flex h-8 shrink-0 items-center whitespace-nowrap rounded-full px-3 text-[13px] font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]"
|
||||||
</button>
|
[ngClass]="typePill() === 'channel' ? 'border border-transparent bg-slate-200 text-slate-900' : 'border border-slate-700 bg-slate-800 text-slate-300 hover:bg-slate-700'"
|
||||||
|
[attr.aria-pressed]="typePill() === 'channel'">
|
||||||
|
Chaînes <span class="ml-1.5 text-[11px] tabular-nums opacity-60">{{ countLabel(typeCounts().channel) }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="my-1.5 h-px bg-slate-700"></div>
|
<div class="my-1.5 h-px bg-slate-700"></div>
|
||||||
|
|
||||||
<!-- Sources : bascules d'affichage. Boutons icône seule — la marque
|
<!-- Sources : bascules d'affichage. Boutons icône seule — la marque
|
||||||
hérite de la couleur du bouton, donc l'état actif / masqué se lit
|
hérite de la couleur du bouton, donc l'état actif / masqué se lit
|
||||||
sans pastille supplémentaire. Le libellé et le compte passent par
|
sans pastille supplémentaire. Le libellé et le compte passent par
|
||||||
`title` / `aria-label`. -->
|
`title` / `aria-label`. -->
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<span class="shrink-0 pl-2 text-[11px] font-medium uppercase tracking-wide text-slate-400">Sources</span>
|
<span class="shrink-0 pl-2 text-[11px] font-medium uppercase tracking-wide text-slate-400">Sources</span>
|
||||||
<div class="scroll-rail-fade min-w-0 flex-1">
|
<div class="scroll-rail-fade min-w-0 flex-1">
|
||||||
<div class="scroll-rail no-scrollbar flex items-center gap-1.5" role="group" aria-label="Afficher ou masquer une source">
|
<div class="scroll-rail no-scrollbar flex items-center gap-1.5" role="group" aria-label="Afficher ou masquer une source">
|
||||||
@for (chip of providerChips(); track chip.id) {
|
@for (chip of providerChips(); track chip.id) {
|
||||||
<button type="button" (click)="toggleProvider(chip.id)"
|
<button type="button" (click)="toggleProvider(chip.id)"
|
||||||
class="chip inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]"
|
class="chip inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]"
|
||||||
[ngClass]="chip.visible ? 'border-slate-700 bg-slate-800 text-slate-200 hover:bg-slate-700' : 'border-slate-700 text-slate-400 hover:bg-slate-800'"
|
[ngClass]="chip.visible ? 'border-slate-700 bg-slate-800 hover:bg-slate-700' : 'border-slate-700 hover:bg-slate-800'"
|
||||||
[attr.aria-pressed]="chip.visible"
|
[attr.aria-pressed]="chip.visible"
|
||||||
[attr.aria-label]="(chip.visible ? 'Masquer ' : 'Afficher ') + chip.displayName + ' (' + chip.countLabel + ')'"
|
[attr.aria-label]="(chip.visible ? 'Masquer ' : 'Afficher ') + chip.displayName + ' (' + chip.countLabel + ')'"
|
||||||
[attr.title]="(chip.visible ? 'Masquer ' : 'Afficher ') + chip.displayName + ' — ' + chip.countLabel + ' résultat(s)'">
|
[attr.title]="(chip.visible ? 'Masquer ' : 'Afficher ') + chip.displayName + ' — ' + chip.countLabel + ' résultat(s)'">
|
||||||
<app-provider-icon [key]="chip.id" [size]="18"></app-provider-icon>
|
<!-- Source masquée : logo désaturé, la marque reste identifiable. -->
|
||||||
</button>
|
<app-provider-icon [key]="chip.id" [size]="20"
|
||||||
}
|
[class.opacity-40]="!chip.visible"
|
||||||
@if (!allProvidersVisible()) {
|
[class.grayscale]="!chip.visible"></app-provider-icon>
|
||||||
<button type="button" (click)="showAllProviders()"
|
|
||||||
class="chip inline-flex h-8 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-full px-3 text-[13px] text-slate-300 underline decoration-dotted underline-offset-2 hover:text-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]">
|
|
||||||
Tout afficher
|
|
||||||
<span class="rounded-full bg-slate-700 px-1.5 text-[11px] tabular-nums text-slate-400 no-underline">{{ hiddenProviderCount() }}</span>
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
@if (!allSourcesActive()) {
|
|
||||||
<button type="button" (click)="enableAllSources()"
|
|
||||||
class="chip inline-flex h-8 shrink-0 items-center gap-1 whitespace-nowrap rounded-full border border-slate-700 px-3 text-[13px] text-slate-300 hover:bg-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]">
|
|
||||||
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14M5 12h14" />
|
|
||||||
</svg>
|
|
||||||
Toutes les sources
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
@if (!allProvidersVisible()) {
|
||||||
|
<button type="button" (click)="showAllProviders()"
|
||||||
|
class="chip inline-flex h-8 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-full px-3 text-[13px] text-slate-300 underline decoration-dotted underline-offset-2 hover:text-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]">
|
||||||
|
Tout afficher
|
||||||
|
<span class="rounded-full bg-slate-700 px-1.5 text-[11px] tabular-nums text-slate-400 no-underline">{{ hiddenProviderCount() }}</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (!allSourcesActive()) {
|
||||||
|
<button type="button" (click)="enableAllSources()"
|
||||||
|
class="chip inline-flex h-8 shrink-0 items-center gap-1 whitespace-nowrap rounded-full border border-slate-700 px-3 text-[13px] text-slate-300 hover:bg-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent)]">
|
||||||
|
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14M5 12h14" />
|
||||||
|
</svg>
|
||||||
|
Toutes les sources
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
|
|
||||||
<!-- ============ Panneau de filtres (repliable) ============ -->
|
<!-- ============ Panneau de filtres (repliable) ============ -->
|
||||||
@if (filtersOpen()) {
|
@if (filtersOpen()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user