feat(menu-lecteur): refonte UI/UX professionnelle du menu FAB
CI / build-and-test (push) Successful in 14m57s

- FAB : pastille verre depoli 40px + icone sliders (standard options)
- Panneau : en-tete (titre + fermer), verre depoli, ombre profonde, coins arrondis
- Animations ouverture/fermeture (fondu + slide + scale), fermeture Echap + clic exterieur
- Items : hover/active/focus, icones teintees, sections + separateurs raffinés
- Playlist : modale rendue HORS du panneau (backdrop-filter piegeait position:fixed)
This commit is contained in:
2026-10-01 07:20:26 -04:00
parent 01e0671de9
commit 7ceec59841
5 changed files with 182 additions and 29 deletions
@@ -15,6 +15,7 @@ import { forkJoin } from 'rxjs';
<div [class]="menuItem ? 'relative w-full text-left' : 'relative inline-block text-left'" (click)="$event.stopPropagation();">
<button (click)="toggleOpen($event)"
[disabled]="!isAuthenticated()"
[style.display]="triggerHidden ? 'none' : ''"
[class]="menuItem ? 'pm-item' : 'flex items-center space-x-2 bg-slate-700 hover:bg-slate-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500/60 disabled:opacity-60 disabled:cursor-not-allowed px-4 py-2 rounded-full transition text-slate-200 font-semibold'"
[attr.aria-expanded]="open()"
[attr.aria-haspopup]="true"
@@ -121,6 +122,8 @@ export class AddToPlaylistComponent implements OnInit, OnDestroy {
@Input() thumbnail?: string;
/** Rendu « élément de menu » (menu FAB du lecteur) : plein largeur, icon + label. */
@Input() menuItem = false;
/** Masque le bouton déclencheur (déclenchement externe via `open()`). */
@Input() triggerHidden = false;
open = signal<boolean>(false);
loading = signal<boolean>(false);
@@ -155,6 +158,15 @@ export class AddToPlaylistComponent implements OnInit, OnDestroy {
this.selected.set(new Set());
}
/** Ouvre la boîte de dialogue (déclenchement externe, ex. menu FAB). */
openDialog(): void {
if (!this.isAuthenticated()) return;
this.open.set(true);
if (this.playlists().length === 0 && !this.loading()) {
this.reloadPlaylists();
}
}
reloadPlaylists(): void {
this.loading.set(true);
this.error.set(null);
+12 -1
View File
@@ -47,7 +47,13 @@
@if (likeError(); as lerr) {
<div class="px-3 py-1.5 text-[11px] text-red-300">{{ lerr }}</div>
}
<app-add-to-playlist [provider]="provider()" [videoId]="v.videoId" [title]="v.title" [thumbnail]="v.thumbnail" [menuItem]="true"></app-add-to-playlist>
<button type="button" (click)="openPlaylist()" class="pm-item">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M4 6a2 2 0 012-2h12a1 1 0 010 2H6v10a1 1 0 11-2 0V6z" />
<path d="M14 11a1 1 0 011-1h5a1 1 0 110 2h-5a1 1 0 01-1-1zM14 15a1 1 0 011-1h5a1 1 0 110 2h-5a1 1 0 01-1-1zM10 11a1 1 0 112 0v1h1a1 1 0 110 2h-1v1a1 1 0 11-2 0v-1H9a1 1 0 110-2h1v-1z" />
</svg>
<span class="pm-item-label">Ajouter à une playlist</span>
</button>
}
<button type="button" (click)="toggleTranscript(); closeMenu()" class="pm-item" [attr.aria-expanded]="transcriptOpen()">
@@ -104,6 +110,11 @@
</div>
</section>
<!-- Boîte de dialogue playlist : rendue HORS du menu FAB (le panneau a un
`backdrop-filter` qui créerait un bloc de positionnement pour la modale
`position: fixed`). Déclencheur masqué, ouvert via openPlaylist(). -->
<app-add-to-playlist #addToPlaylist [provider]="provider()" [videoId]="v.videoId" [title]="v.title" [thumbnail]="v.thumbnail" [triggerHidden]="true"></app-add-to-playlist>
@if (loading()) {
<!-- Squelette des métadonnées pendant /details (le player joue déjà) -->
<div class="mt-4 animate-pulse space-y-3" aria-hidden="true">
+7
View File
@@ -54,6 +54,7 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
private subs = inject(SubscriptionsService);
private routeSubscription!: Subscription;
@ViewChild('embedFrame', { static: false }) embedFrame?: ElementRef<HTMLIFrameElement>;
@ViewChild('addToPlaylist', { static: false }) addToPlaylist?: AddToPlaylistComponent;
private attachTryCount = 0;
// --- Chaîne / abonnement ---
@@ -167,6 +168,12 @@ export class WatchComponent implements OnDestroy, AfterViewInit {
closeMenu(): void {
this.menuOpen.set(false);
}
/** Ouvre la boîte de dialogue playlist (composant rendu hors du menu FAB). */
openPlaylist(): void {
this.closeMenu();
this.addToPlaylist?.openDialog();
}
// Prefer full description if available; otherwise fallback to shortDescription
descriptionText = computed(() => {
const v = this.video();