style(ui): harmonisation des couleurs par thème + contour fournisseur 1 px
CI / build-and-test (push) Successful in 15m1s

Balayage de la couche d'overrides de index.css : les 75 jetons neutres
slate/zinc/gray qui n'étaient pas couverts (36 fichiers) sont mappés sur les
variables du thème — fonds, textes, bordures, anneaux, divide, placeholders,
dégradés + variants hover/focus/focus-within/disabled/group-hover, alpha via
color-mix. Barre de recherche, menus, panneaux, cartes et résultats de
recherche suivent donc le thème appliqué (dark/light/blue/black) au lieu de la
palette slate figée. Volontairement exclus : bg-white/text-black (boutons
inversés), pastilles « inversées » conditionnelles au thème clair, chrome
au-dessus des vidéos, rouges sémantiques.

Panneau d'informations ⓘ des Shorts re-colorié dans son propre scope
(fond/bordures/textes/pastilles d'action → panel/text/muted), les mêmes tokens
restant blancs ailleurs (rail, méta). Marqueur border-l-4 des titres de page
sur --accent.

Cartes vidéo harmonisées : titre text-sm font-semibold leading-snug
(résultats de recherche, chaîne, thèmes), méta text-xs, rayon rounded-lg sur les
cadres, pastille de durée sans font-mono, titre « Abonnements » avec le
marqueur d'accent.

Contour fournisseur 1 px autour de chaque cadre vidéo, à la couleur du premier
stop du dégradé de la pastille (table unique en CSS, ids longs + courts) :
résultats de recherche, grille chaîne, pages thèmes, watch (lecteur + « à
suivre »), shorts, historique, aimés/à regarder plus tard, détail de playlist.

Tests verts (search, filters, kind, section, highlight, shorts-catalog,
api 41/41) ; vérif UI Playwright desktop 1280x900 + mobile 390x844 sur 4
thèmes : 250 assertions + 13 couleurs de contour au vert.
This commit is contained in:
2026-10-02 16:30:28 -04:00
parent fe4a0f7f31
commit d1acd1b5b5
14 changed files with 311 additions and 23 deletions
+1
View File
@@ -245,6 +245,7 @@ Ajoutez au besoin `log-driver`, `log-opts`, `default-address-pools`, etc.
* ✅ Liste **« À regarder plus tard »** (watch later) — même stockage « tags » que les likes (aucune migration), bouton horloge à côté du cœur sur toutes les cartes, entrée « Vous » de la barre latérale, page `/#/library/watch-later` avec recherche serveur
* ✅ PeerTube **multi-instances** (activer/désactiver, set active)
* ✅ Thème (système / dark / light / blue / black)
* ✅ **Harmonisation visuelle** — tous les utilitaires neutres (`slate`/`zinc`/`gray`, fonds, textes, bordures, anneaux, placeholders, états `hover:`/`disabled:`) sont mappés sur les variables du thème (**75 jetons, 36 fichiers**) : barre de recherche, menus, panneaux et cartes suivent enfin le thème appliqué ; panneau **ⓘ des Shorts** re-colorié dans son périmètre (le chrome au-dessus de la vidéo reste blanc) ; marqueur `border-l-4` des titres de page sur l'accent du thème ; cartes vidéo harmonisées (`text-sm` / `font-semibold` / méta `text-xs` / rayon `rounded-lg`, pastille de durée sans `font-mono`) ; **contour fournisseur 1 px** à la couleur de la pastille autour de chaque cadre vidéo (grilles, recherche, channel, watch — lecteur et « à suivre » —, shorts, historique, aimés, playlist)
* ✅ Préférences (langue, thème, région, qualité par défaut)
* ✅ Auth légère (JWT), rate-limit API
* ✅ **Recherche unifiée multi-providers** — panneau de filtres (sources / type / période / durée / tri, Ctrl/⌘+Maj+F), autocomplete `@` + opérateurs `live:`/`today:`, deep-links `?providers=…&type=…&period=…`, préférence `defaultProviders` persistée, filtres serveur, télémétrie minimale, a11y (focus trap, Esc, clavier complet sur le panneau de suggestions)
+285
View File
@@ -459,6 +459,291 @@ body {
color: #94a3b8; /* slate-400 */
}
/* --- Harmonisation : TOUS les utilitaires neutres suivent le thème -------- */
/* Le bloc ci-dessus ne couvrait que 15 classes ; le reste de l'app parlait
« slate/zinc/gray » quand le thème parlait --panel/--text (36 fichiers, 75
jetons). Ici : familles slate/zinc/gray -> variables du thème, alpha ->
color-mix, variants hover/focus/disabled/placeholders/ring/divide inclus.
Règle : !important gagne sur l'utilitaire Tailwind (injecté après ou avant
selon le CDN). VOLONTAIREMENT exclus — ils fonctionnent déjà dans tous les
thèmes et/ou sont conditionnels au thème clair : bg-white/text-black (boutons
inversés), bg-slate-50/100/200 + text-slate-700/800/900 (pastilles
« inversées » de la barre de filtres, bouton Google, avatar clair, barre de
thèmes en thème clair), bg-black/* + text-white au-dessus des vidéos. */
.bg-gray-900,
.bg-slate-950,
.bg-zinc-900,
.bg-zinc-950 {
background-color: var(--bg) !important;
}
.bg-zinc-800 {
background-color: var(--panel) !important;
}
.bg-slate-600 {
background-color: var(--panel-2) !important;
}
.disabled\:bg-slate-800:disabled {
background-color: var(--panel) !important;
}
.bg-slate-900\/40 {
background-color: color-mix(in srgb, var(--bg) 40%, transparent) !important;
}
.bg-slate-900\/50,
.bg-zinc-900\/50 {
background-color: color-mix(in srgb, var(--bg) 50%, transparent) !important;
}
.bg-slate-900\/60 {
background-color: color-mix(in srgb, var(--bg) 60%, transparent) !important;
}
.bg-slate-900\/95,
.bg-slate-950\/95,
.bg-zinc-950\/95 {
background-color: color-mix(in srgb, var(--bg) 95%, transparent) !important;
}
.bg-slate-800\/40 {
background-color: color-mix(in srgb, var(--panel) 40%, transparent) !important;
}
.bg-slate-800\/60 {
background-color: color-mix(in srgb, var(--panel) 60%, transparent) !important;
}
.bg-slate-800\/70 {
background-color: color-mix(in srgb, var(--panel) 70%, transparent) !important;
}
.bg-slate-800\/80,
.bg-zinc-800\/80 {
background-color: color-mix(in srgb, var(--panel) 80%, transparent) !important;
}
.bg-slate-800\/90 {
background-color: color-mix(in srgb, var(--panel) 90%, transparent) !important;
}
.bg-slate-100\/10 {
background-color: color-mix(in srgb, var(--text) 10%, transparent) !important;
}
.bg-slate-700\/50 {
background-color: color-mix(in srgb, var(--panel-2) 50%, transparent) !important;
}
.bg-slate-700\/60 {
background-color: color-mix(in srgb, var(--panel-2) 60%, transparent) !important;
}
.bg-slate-700\/70 {
background-color: color-mix(in srgb, var(--panel-2) 70%, transparent) !important;
}
.bg-zinc-700\/80 {
background-color: color-mix(in srgb, var(--panel-2) 80%, transparent) !important;
}
.bg-slate-700\/90 {
background-color: color-mix(in srgb, var(--panel-2) 90%, transparent) !important;
}
.focus-within\:bg-slate-800\/60:focus-within {
background-color: color-mix(in srgb, var(--panel) 60%, transparent) !important;
}
.hover\:bg-slate-800\/60:hover {
background-color: color-mix(in srgb, var(--panel) 60%, transparent) !important;
}
.hover\:bg-slate-700\/40:hover {
background-color: color-mix(in srgb, var(--panel-2) 40%, transparent) !important;
}
.hover\:bg-slate-700\/50:hover {
background-color: color-mix(in srgb, var(--panel-2) 50%, transparent) !important;
}
.hover\:bg-slate-700\/60:hover {
background-color: color-mix(in srgb, var(--panel-2) 60%, transparent) !important;
}
.hover\:bg-slate-700\/80:hover {
background-color: color-mix(in srgb, var(--panel-2) 80%, transparent) !important;
}
.hover\:bg-slate-500:hover,
.hover\:bg-slate-600:hover,
.hover\:bg-slate-700:hover,
.hover\:bg-slate-800:hover,
.hover\:bg-zinc-600:hover {
background-color: color-mix(in srgb, var(--panel-2) 90%, var(--text)) !important;
}
.text-gray-400,
.text-slate-500,
.text-slate-600,
.text-zinc-400,
.text-zinc-500,
.text-zinc-600 {
color: var(--muted) !important;
}
.placeholder-slate-400::placeholder,
.placeholder-slate-500::placeholder,
.placeholder\:text-slate-500::placeholder,
.placeholder\:text-zinc-500::placeholder {
color: var(--muted) !important;
}
.disabled\:text-slate-400:disabled {
color: var(--muted) !important;
}
.group:hover .group-hover\:text-slate-200,
.text-zinc-100,
.text-zinc-200,
.text-zinc-300 {
color: var(--text) !important;
}
.hover\:text-slate-100:hover,
.hover\:text-slate-200:hover,
.hover\:text-zinc-400:hover {
color: var(--text) !important;
}
.border-slate-700\/60 {
border-color: color-mix(in srgb, var(--border) 60%, transparent) !important;
}
.border-slate-500,
.border-slate-600,
.border-slate-800,
.border-zinc-700,
.border-zinc-800,
.border-zinc-950,
.divide-slate-700,
.divide-slate-800 {
border-color: var(--border) !important;
}
.hover\:border-slate-400:hover,
.hover\:border-slate-500:hover,
.hover\:border-slate-600:hover,
.hover\:border-zinc-500:hover,
.hover\:border-zinc-600:hover {
border-color: var(--muted) !important;
}
.ring-slate-500\/50,
.ring-slate-600\/50 {
--tw-ring-color: color-mix(in srgb, var(--border) 50%, transparent) !important;
}
.focus-visible\:ring-slate-400\/60:focus-visible {
--tw-ring-color: var(--accent) !important;
}
.from-slate-800 {
--tw-gradient-from: var(--panel) !important; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to) !important;
}
.from-slate-700 {
--tw-gradient-from: var(--panel-2) !important; --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to) !important;
}
.to-slate-800\/50 {
--tw-gradient-to: color-mix(in srgb, var(--panel) 50%, transparent) !important;
}
.to-slate-800 {
--tw-gradient-to: var(--panel) !important;
}
@media (min-width: 768px) {
.md\:bg-slate-900\/70 { background-color: color-mix(in srgb, var(--bg) 70%, transparent) !important; }
}
/* Marqueur des titres de page (barre `border-l-4`) : suit l'accent du thème
au lieu d'être figé en rouge (et rattrape les titres qui n'avaient pas de
couleur explicite, ex. Bibliothèque → Aimés). */
.border-l-4 { border-left-color: var(--accent) !important; }
/* --- Shorts : le panneau d'informations (ⓘ) suit le thème ----------------- */
/* Ses couleurs sont du « verre blanc » (bg-white/*, text-white/*) : les mêmes
jetons servent AU-DESSUS de la vidéo (rail, méta) et doivent y rester
blancs — donc pas de reprise globale, uniquement un scope sur l'aside. */
[data-theme] aside.shorts-info {
background-color: color-mix(in srgb, var(--panel) 94%, transparent) !important;
border-color: var(--border) !important;
color: var(--text) !important;
}
[data-theme] aside.shorts-info [class*="border-white/"] { border-color: var(--border) !important; }
[data-theme] aside.shorts-info [class*="bg-white/"] {
background-color: color-mix(in srgb, var(--panel-2) 75%, transparent) !important;
}
[data-theme] aside.shorts-info .hover\:bg-white\/20:hover,
[data-theme] aside.shorts-info .hover\:bg-white\/25:hover {
background-color: color-mix(in srgb, var(--panel-2) 90%, var(--text)) !important;
}
[data-theme] aside.shorts-info .bg-white { background-color: var(--text) !important; }
[data-theme] aside.shorts-info .text-black { color: var(--bg) !important; }
[data-theme] aside.shorts-info .hover\:bg-slate-200:hover {
background-color: color-mix(in srgb, var(--text) 85%, var(--bg)) !important;
}
[data-theme] aside.shorts-info [class*="ring-white/"] { --tw-ring-color: var(--border) !important; }
[data-theme] aside.shorts-info .text-white { color: var(--text) !important; }
[data-theme] aside.shorts-info .text-white\/40,
[data-theme] aside.shorts-info .text-white\/45,
[data-theme] aside.shorts-info .text-white\/50,
[data-theme] aside.shorts-info .text-white\/60 { color: var(--muted) !important; }
[data-theme] aside.shorts-info .text-white\/75,
[data-theme] aside.shorts-info .text-white\/80 {
color: color-mix(in srgb, var(--text) 85%, transparent) !important;
}
/* Pastilles d'action du panneau (définies en dur en rgba blanc plus haut). */
[data-theme] aside.shorts-info .shorts-info-action {
color: var(--muted) !important;
background-color: color-mix(in srgb, var(--panel-2) 70%, transparent) !important;
}
[data-theme] aside.shorts-info .shorts-info-action:hover:not(:disabled) {
color: var(--text) !important;
background-color: color-mix(in srgb, var(--panel-2) 95%, var(--text)) !important;
}
[data-theme] aside.shorts-info .shorts-info-action--on {
color: var(--text) !important;
background-color: color-mix(in srgb, var(--panel-2) 90%, var(--text)) !important;
}
/* --- Contour fournisseur autour de chaque cadre vidéo --------------------- */
/* Contour fin (1 px) à la couleur de la pastille du fournisseur, sur toutes les
présentations de vidéo. La couleur = le PREMIER stop du dégradé de
app-provider-badge : c'est la seule table à tenir si une marque change.
`data-video-provider` accepte l'id long ET l'id court (les deux circulent :
VideoItem.provider est long, ChannelMeta.provider est court).
`outline` plutôt que `border` : zéro effet sur la boîte (les cadres sont
dimensionnés en aspect-ratio) et le rayon suit automatiquement. */
[data-video-provider] { outline: 1px solid var(--prov, transparent); outline-offset: -1px; }
[data-video-provider="youtube"],
[data-video-provider="yt"] { --prov: #dc2626; }
[data-video-provider="dailymotion"],
[data-video-provider="dm"] { --prov: #0284c7; }
[data-video-provider="twitch"],
[data-video-provider="tw"] { --prov: #7c3aed; }
[data-video-provider="peertube"],
[data-video-provider="pt"] { --prov: #f59e0b; }
[data-video-provider="rumble"],
[data-video-provider="ru"] { --prov: #059669; }
[data-video-provider="odysee"],
[data-video-provider="od"] { --prov: #ec4899; }
/* 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; }
@@ -3,7 +3,7 @@
<!-- Phase 7.2 : la frame prend le ratio reel quand width/height sont connus
([style] porte aspect-ratio + largeur bornee), 16:9 sinon. Le
`object-cover` reste car une verticale peut deja etre rognee en amont. -->
<div class="relative w-full overflow-hidden rounded-lg bg-zinc-800" [style]="frameStyle">
<div class="relative w-full overflow-hidden rounded-lg bg-zinc-800" [style]="frameStyle" [attr.data-video-provider]="video.provider">
<!-- Frame en cours de chargement : shimmer + spinner (plus de frame vide) -->
<div *ngIf="!thumbLoaded() && !thumbFailed()" class="absolute inset-0 animate-pulse bg-zinc-800/80" aria-hidden="true">
<div class="flex h-full w-full items-center justify-center">
@@ -58,7 +58,7 @@
</div>
<div class="flex flex-col gap-1">
<h3 class="text-base font-semibold text-zinc-100 line-clamp-2" [title]="video.title">
<h3 class="text-sm font-semibold leading-snug text-zinc-100 line-clamp-2" [title]="video.title">
{{ video.title }}
</h3>
<div class="text-xs text-zinc-400 flex flex-wrap gap-x-2 gap-y-0.5">
@@ -245,7 +245,7 @@
<li *ngFor="let t of filteredTranscripts()" class="bg-slate-700/50 rounded-lg overflow-hidden">
<div class="p-4">
<div class="flex items-start gap-3">
<div class="flex-shrink-0 w-20 h-12 bg-slate-700 rounded overflow-hidden flex items-center justify-center">
<div class="flex-shrink-0 w-20 h-12 bg-slate-700 rounded overflow-hidden flex items-center justify-center" [attr.data-video-provider]="t.provider">
@if (t.thumbnail) { <img [src]="t.thumbnail" alt="" class="w-full h-full object-cover" /> }
@else { <span class="text-slate-500 text-xs">TXT</span> }
</div>
@@ -31,7 +31,7 @@
<img [src]="video.thumbnailUrl" [alt]="video.title" loading="lazy" class="h-full w-full object-cover transition-transform group-hover:scale-105" />
</div>
<div class="p-2">
<h3 class="line-clamp-2 text-sm font-medium text-zinc-100">{{ video.title }}</h3>
<h3 class="line-clamp-2 text-sm font-semibold text-zinc-100">{{ video.title }}</h3>
<p *ngIf="video.viewCount" class="mt-1 text-xs text-zinc-400">{{ video.viewCount | number }} vues</p>
</div>
</a>
+3 -3
View File
@@ -22,7 +22,7 @@
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-x-6 gap-y-8">
@for (video of trendingVideos(); track video.videoId) {
<a [routerLink]="routeForVideo(video)" [queryParams]="watchQueryParams(video)" [state]="{ video }" class="group flex flex-col bg-slate-800/50 rounded-lg overflow-hidden hover:bg-slate-700/50 transition-all duration-300 transform hover:-translate-y-1">
<div class="relative">
<div class="relative rounded-lg overflow-hidden" [attr.data-video-provider]="video.provider || instances.selectedProvider()">
<img [src]="video.thumbnail" [alt]="video.title" class="w-full h-48 object-cover transition-transform duration-300 group-hover:scale-105">
<div class="absolute top-2 right-2">
<app-provider-badge [provider]="video.provider || instances.selectedProvider()"></app-provider-badge>
@@ -37,13 +37,13 @@
</div>
<div class="p-4 flex-grow flex flex-col">
<div class="flex justify-between items-start">
<h3 class="font-semibold text-slate-100 group-hover:text-red-400 transition-colors duration-200 line-clamp-2 pr-2">{{ video.title }}</h3>
<h3 class="text-sm font-semibold leading-snug text-slate-100 group-hover:text-red-400 transition-colors duration-200 line-clamp-2 pr-2">{{ video.title }}</h3>
</div>
<div class="mt-2 flex items-center space-x-3 text-sm text-slate-400">
<img [src]="video.uploaderAvatar" [alt]="video.uploaderName" class="w-8 h-8 rounded-full">
<span>{{ video.uploaderName }}</span>
</div>
<div class="mt-auto pt-2 text-sm text-slate-400">
<div class="mt-auto pt-2 text-xs text-slate-400">
<span>{{ formatViews(video.views) }} visionnements</span> &bull; <span>{{ formatRelative(video.uploadedDate) }}</span>
</div>
</div>
@@ -55,7 +55,7 @@
<a [routerLink]="['/watch', it.video_id]" [queryParams]="{ p: it.provider }" class="block p-4 hover:no-underline">
<div class="flex items-center">
<!-- Miniature de la vidéo -->
<div class="relative flex-shrink-0 w-24 h-16 bg-slate-800 rounded overflow-hidden mr-4">
<div class="relative flex-shrink-0 w-24 h-16 bg-slate-800 rounded overflow-hidden mr-4" [attr.data-video-provider]="it.provider">
@if (it.thumbnail) {
<img [src]="it.thumbnail" alt="" class="w-full h-full object-cover" />
} @else {
@@ -59,7 +59,7 @@ import { AuthService } from '../../../services/auth.service';
<div class="space-y-3">
<div *ngFor="let it of items(); let i = index" class="flex flex-wrap items-center gap-3 p-2 rounded border border-slate-700 bg-slate-800/60 sm:flex-nowrap">
<div class="w-28 flex-shrink-0">
<img [src]="it.thumbnail || DEFAULT_PLAYLIST_IMAGE" [alt]="it.title || 'Video thumbnail'" class="w-28 h-16 object-cover rounded">
<img [src]="it.thumbnail || DEFAULT_PLAYLIST_IMAGE" [alt]="it.title || 'Video thumbnail'" class="w-28 h-16 object-cover rounded" [attr.data-video-provider]="it.provider">
</div>
<div class="min-w-0 flex-1 basis-[calc(100%_-_7rem)]">
<a [routerLink]="['/watch', it.videoId]" [queryParams]="{ p: it.provider }" class="break-anywhere font-semibold text-slate-100 hover:text-red-400 line-clamp-2">{{ it.title || it.videoId }}</a>
@@ -1,7 +1,7 @@
<div class="container mx-auto p-4 sm:p-6">
<div class="mb-6 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h2 class="text-2xl font-bold text-slate-100 sm:text-3xl">Abonnements</h2>
<h2 class="border-l-4 border-red-500 pl-4 text-2xl font-bold text-slate-100 sm:text-3xl">Abonnements</h2>
<p class="text-sm text-slate-400">Organisez vos chaînes suivies en groupes thématiques</p>
</div>
<div class="flex flex-wrap gap-2">
@@ -24,6 +24,7 @@
<!-- Video frame (9:16). `translateY` pendant un glissement tactile : tout
le chrome (rail, méta, FAB) reste solidaire de l'image. -->
<div #shortsFrame class="shorts-frame relative shrink-0 overflow-hidden bg-black rounded-none ring-0 shadow-none sm:rounded-2xl sm:ring-1 sm:ring-white/10 sm:shadow-2xl"
[attr.data-video-provider]="providerOf(current())"
[style.transform]="dragOffset() ? 'translateY(' + dragOffset() + 'px)' : null">
@if (embedUrl(); as url) {
<iframe [src]="url" [title]="current()?.title || 'Short video'" class="absolute inset-0 w-full h-full" [class.embed-wide]="wideEmbed()" frameborder="0" playsinline
@@ -396,8 +396,9 @@ export class WatchShortComponent implements OnDestroy {
this.index() < this.items().length - 1 || (!this.forYou() && (!!this.nextCursor() || this.hasMoreQueries()))
);
/** Provider effectif d'une vidéo : en mode "Pour toi", chaque item garde son fournisseur. */
private providerOf(v: Video | null): Provider {
/** Provider effectif d'une vidéo : en mode "Pour toi", chaque item garde son fournisseur.
* Exposé au template (contour de couleur du cadre sur /shorts). */
providerOf(v: Video | null): Provider {
if (this.forYou() && v) {
const p = String((v as any).provider || '').toLowerCase();
if (p === 'youtube' || p === 'dailymotion' || p === 'twitch' || p === 'peertube' || p === 'rumble' || p === 'odysee') {
@@ -105,7 +105,7 @@
<div *ngIf="!showTwitchSectionsAll()" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-x-6 gap-y-8">
<div *ngFor="let v of videos(); trackBy: trackByVideo" class="group bg-slate-800/50 rounded-lg overflow-hidden hover:bg-slate-700/50 transition-all duration-300 transform hover:-translate-y-1 shadow-lg hover:shadow-xl">
<a [routerLink]="['/watch', v.videoId]" [queryParams]="watchQueryParams(v)" [state]="{ video: v }" class="block">
<div class="relative">
<div class="relative rounded-lg overflow-hidden" [attr.data-video-provider]="provider()">
<img [src]="v.thumbnail" [alt]="v.title" loading="lazy" decoding="async" class="w-full aspect-video object-cover transition-transform duration-300 group-hover:scale-105">
<div class="absolute top-2 right-2">
<app-provider-badge [provider]="provider()"></app-provider-badge>
@@ -116,7 +116,7 @@
<span *ngIf="!isLiveCard(v) && v._kind === 'short' && !isClipCard(v)" class="bg-red-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">SHORT</span>
<span *ngIf="isChannelCard(v)" class="bg-slate-700/90 text-slate-200 text-[11px] font-bold px-1.5 py-0.5 rounded ring-1 ring-slate-500/50">CHAÎNE</span>
</div>
<div *ngIf="v.duration && v.duration > 0" class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded-md font-mono">
<div *ngIf="v.duration && v.duration > 0" class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded-md">
{{ v.duration | duration }}
</div>
<div *ngIf="isLiveCard(v) && v.views > 0" class="absolute bottom-2 left-2 bg-black/75 text-white text-xs px-2 py-1 rounded-md">
@@ -159,7 +159,7 @@
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-x-6 gap-y-8">
<div *ngFor="let v of twitchLives(); trackBy: trackByVideo" class="group bg-slate-800/50 rounded-lg overflow-hidden hover:bg-slate-700/50 transition-all duration-300 transform hover:-translate-y-1 shadow-lg hover:shadow-xl">
<a [routerLink]="['/watch', v.videoId]" [queryParams]="watchQueryParams(v)" [state]="{ video: v }" class="block">
<div class="relative">
<div class="relative rounded-lg overflow-hidden" [attr.data-video-provider]="provider()">
<img [src]="v.thumbnail" [alt]="v.title" loading="lazy" decoding="async" class="w-full aspect-video object-cover transition-transform duration-300 group-hover:scale-105">
<div class="absolute top-2 right-2"><app-provider-badge [provider]="provider()"></app-provider-badge></div>
<div class="absolute top-2 left-2"><span class="bg-purple-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">LIVE</span></div>
@@ -181,10 +181,10 @@
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-x-6 gap-y-8">
<div *ngFor="let v of twitchVods(); trackBy: trackByVideo" class="group bg-slate-800/50 rounded-lg overflow-hidden hover:bg-slate-700/50 transition-all duration-300 transform hover:-translate-y-1 shadow-lg hover:shadow-xl">
<a [routerLink]="['/watch', v.videoId]" [queryParams]="watchQueryParams(v)" [state]="{ video: v }" class="block">
<div class="relative">
<div class="relative rounded-lg overflow-hidden" [attr.data-video-provider]="provider()">
<img [src]="v.thumbnail" [alt]="v.title" loading="lazy" decoding="async" class="w-full aspect-video object-cover transition-transform duration-300 group-hover:scale-105">
<div class="absolute top-2 right-2"><app-provider-badge [provider]="provider()"></app-provider-badge></div>
<div *ngIf="v.duration && v.duration > 0" class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded-md font-mono">{{ v.duration | duration }}</div>
<div *ngIf="v.duration && v.duration > 0" class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded-md">{{ v.duration | duration }}</div>
<div class="absolute bottom-2 left-2 flex items-center gap-1"><app-like-button [videoId]="v.videoId" [provider]="provider()" [title]="v.title" [thumbnail]="v.thumbnail"></app-like-button><app-like-button [list]="'watch-later'" [videoId]="v.videoId" [provider]="provider()" [title]="v.title" [thumbnail]="v.thumbnail"></app-like-button></div>
</div>
<div class="p-3 pb-1"><h3 class="font-semibold text-slate-100 group-hover:text-red-400 transition-colors duration-200 text-sm line-clamp-2 pr-2">{{ v.title }}</h3></div>
@@ -205,11 +205,11 @@
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-x-6 gap-y-8">
<div *ngFor="let v of twitchClips(); trackBy: trackByVideo" class="group bg-slate-800/50 rounded-lg overflow-hidden hover:bg-slate-700/50 transition-all duration-300 transform hover:-translate-y-1 shadow-lg hover:shadow-xl">
<a [routerLink]="['/watch', v.videoId]" [queryParams]="watchQueryParams(v)" [state]="{ video: v }" class="block">
<div class="relative">
<div class="relative rounded-lg overflow-hidden" [attr.data-video-provider]="provider()">
<img [src]="v.thumbnail" [alt]="v.title" loading="lazy" decoding="async" class="w-full aspect-video object-cover transition-transform duration-300 group-hover:scale-105">
<div class="absolute top-2 right-2"><app-provider-badge [provider]="provider()"></app-provider-badge></div>
<div class="absolute top-2 left-2"><span class="bg-red-600 text-white text-[11px] font-bold px-1.5 py-0.5 rounded">CLIP</span></div>
<div *ngIf="v.duration && v.duration > 0" class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded-md font-mono">{{ v.duration | duration }}</div>
<div *ngIf="v.duration && v.duration > 0" class="absolute bottom-2 right-2 bg-black/75 text-white text-xs px-2 py-1 rounded-md">{{ v.duration | duration }}</div>
<div class="absolute bottom-2 left-2 flex items-center gap-1"><app-like-button [videoId]="v.videoId" [provider]="provider()" [title]="v.title" [thumbnail]="v.thumbnail"></app-like-button><app-like-button [list]="'watch-later'" [videoId]="v.videoId" [provider]="provider()" [title]="v.title" [thumbnail]="v.thumbnail"></app-like-button></div>
</div>
<div class="p-3 pb-1"><h3 class="font-semibold text-slate-100 group-hover:text-red-400 transition-colors duration-200 text-sm line-clamp-2 pr-2">{{ v.title }}</h3></div>
@@ -46,7 +46,7 @@
[queryParams]="watchQueryParams(b.provider, v)"
[state]="{ video: v }"
class="block">
<div class="relative">
<div class="relative rounded-lg overflow-hidden" [attr.data-video-provider]="b.provider">
<img [src]="v.thumbnail" [alt]="v.title" loading="lazy" decoding="async" class="w-full h-40 object-cover">
<div class="absolute top-2 right-2">
<app-provider-badge [provider]="b.provider"></app-provider-badge>
@@ -69,7 +69,7 @@
</div>
</div>
<div class="p-3 pb-1">
<h4 class="font-semibold line-clamp-2">{{ v.title }}</h4>
<h4 class="text-sm font-semibold leading-snug line-clamp-2">{{ v.title }}</h4>
</div>
</a>
<div class="px-3 pb-3">
+2 -2
View File
@@ -86,7 +86,7 @@
<span class="pm-item-label">Ouvrir chez le fournisseur</span>
</a>
</app-player-menu>
<div class="aspect-video overflow-hidden rounded-2xl">
<div class="aspect-video overflow-hidden rounded-2xl" [attr.data-video-provider]="provider()">
@if (embedUrl(); as src) {
<iframe #embedFrame class="h-full w-full" [src]="src" frameborder="0" playsinline (load)="onEmbedFrameLoad()"
referrerpolicy="origin"
@@ -507,7 +507,7 @@
@for (related of v.relatedStreams; track related.videoId) {
<div class="group rounded-xl p-2 transition hover:bg-slate-800/60 focus-within:bg-slate-800/60">
<a [routerLink]="['/watch', related.videoId]" [queryParams]="watchQueryParams(related)" [state]="{ video: related }" class="flex items-start gap-3 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-red-400">
<div class="relative w-32 shrink-0 sm:w-40">
<div class="relative w-32 shrink-0 sm:w-40 rounded-xl" [attr.data-video-provider]="related.provider">
<img [src]="related.thumbnail" [alt]="related.title" class="h-20 w-full rounded-xl object-cover sm:h-24" loading="lazy">
<div class="absolute right-1.5 top-1.5">
<app-provider-badge [provider]="related.provider"></app-provider-badge>