Files
NewTube/src/components/header/header.component.html
T
bruno 66f1da1d72 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).
2026-09-28 14:38:31 -04:00

264 lines
18 KiB
HTML

<header class="app-header bg-slate-800/50 backdrop-blur-sm fixed inset-x-0 top-0 z-50 shadow-lg border-b border-slate-700/60"
[style.paddingTop]="'var(--safe-top)'"
[style.paddingLeft]="'var(--safe-left)'"
[style.paddingRight]="'var(--safe-right)'">
<!-- Top row: burger + logo (always) / search (desktop) / actions -->
<div class="flex items-center gap-1 h-14 md:h-16 px-2 md:px-4">
<!-- Left: burger + logo -->
<div class="flex items-center gap-1 md:gap-2 shrink-0">
<button type="button" (click)="menuToggle.emit()"
data-drawer-toggle
class="app-icon-btn focus-ring" aria-label="Toggle menu" [attr.aria-expanded]="drawerOpen">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-slate-200" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M3 6h18v2H3V6zm0 5h18v2H3v-2zm0 5h18v2H3v-2z"/>
</svg>
</button>
<a routerLink="/" class="flex items-center gap-2 shrink-0 focus-ring rounded-lg px-1" aria-label="NewTube">
<img src="images/NewTube.png" alt="" class="h-7 md:h-8 w-auto" />
<!-- Wordmark only when there is room for it (>= 400px). -->
<span class="hidden min-[400px]:inline text-xl md:text-2xl font-bold tracking-tight text-white">NewTube</span>
</a>
</div>
<!-- Center: search (desktop + tablet inline). On phones it is moved into
the slide-down search sheet below via the same DOM node. -->
<div class="hidden md:flex flex-1 min-w-0 justify-center px-2 lg:px-4">
<div class="relative w-full max-w-2xl lg:max-w-4xl">
<app-search-box [placeholder]="('search.placeholder' | t)"
(submitted)="onSearchBoxSubmit($event)"></app-search-box>
</div>
</div>
<!-- Right: actions -->
<div class="ml-auto flex items-center gap-0.5 md:gap-2 shrink-0">
<!-- Phone: open the search sheet -->
<button type="button" (click)="toggleMobileSearch()" class="app-icon-btn md:hidden focus-ring"
[attr.aria-expanded]="mobileSearchOpen()" aria-controls="mobile-search-sheet"
[attr.aria-label]="mobileSearchOpen() ? ('menu.close' | t) : ('menu.search' | t)">
@if (mobileSearchOpen()) {
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M6.4 5 5 6.4 10.6 12 5 17.6 6.4 19 12 13.4 17.6 19 19 17.6 13.4 12 19 6.4 17.6 5 12 10.6 6.4 5Z"/>
</svg>
} @else {
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" 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>
}
</button>
<!-- Phone/tablet: overflow menu — nothing may disappear on small screens -->
<button type="button" (click)="toggleMoreMenu()" class="app-icon-btn md:hidden focus-ring"
[attr.aria-expanded]="moreMenuOpen()" aria-haspopup="true" aria-controls="mobile-more-menu"
[attr.aria-label]="('menu.more' | t)">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 8a2 2 0 1 0-2-2 2 2 0 0 0 2 2Zm0 2a2 2 0 1 0 2 2 2 2 0 0 0-2-2Zm0 6a2 2 0 1 0 2 2 2 2 0 0 0-2-2Z"/>
</svg>
</button>
<!-- Desktop: themes bar toggle -->
<button type="button" (click)="toggleThemesBar()"
class="app-icon-btn hidden md:inline-flex focus-ring"
[attr.aria-pressed]="themesBarVisible()"
[attr.title]="themesBarVisible() ? ('themes.hideBar' | t) : ('themes.showBar' | t)"
[attr.aria-label]="themesBarVisible() ? ('themes.hideBar' | t) : ('themes.showBar' | t)"
[class.opacity-40]="!themesBarVisible()">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm0-8a3 3 0 1 0 0 6 3 3 0 0 0 0-6z"/>
</svg>
</button>
<!-- Desktop: provider context badge -->
<div *ngIf="providerContextLabel() as providerLabel"
class="hidden lg:flex items-center gap-1 text-xs min-w-0">
<span class="inline-flex items-center px-2 py-1 rounded border shadow-sm gap-1 max-w-[12rem] truncate"
[ngStyle]="getProviderColors(providerContext())">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<span class="truncate">{{ providerLabel }}</span>
</span>
</div>
<!-- Desktop: auth / user menu -->
<div class="hidden md:flex items-center gap-2">
<div class="h-6 w-px bg-slate-700/60 mx-1"></div>
@if (!user()) {
<a routerLink="/auth/login" class="px-3 py-2 rounded text-slate-100 hover:bg-slate-700 focus-ring">{{ 'btn.login' | t }}</a>
<a routerLink="/auth/register" class="px-3 py-2 rounded bg-red-600 hover:bg-red-500 text-white focus-ring">{{ 'btn.register' | t }}</a>
} @else {
<div class="relative" #userMenuContainer>
<button type="button" (click)="toggleUserMenu()" class="app-avatar-btn focus-ring" aria-haspopup="true"
[attr.aria-expanded]="userMenuOpen()" [attr.aria-label]="user()?.username || user()?.email || ('menu.account' | t)">
<span class="avatar-circle" [ngClass]="avatarClasses()">{{ avatarLetter() }}</span>
</button>
@if (userMenuOpen()) {
<div class="absolute right-0 mt-2 w-[min(20rem,calc(100vw-1rem))] overflow-hidden rounded-xl border border-slate-700 bg-slate-900 shadow-xl z-50" role="menu">
<div class="border-b border-slate-700 px-4 py-3">
<div class="truncate font-semibold text-slate-100">{{ user()?.username || user()?.email }}</div>
<div class="truncate text-xs text-slate-400">{{ user()?.email }}</div>
</div>
<div class="py-1">
<a routerLink="/account/preferences" (click)="closeUserMenu()" role="menuitem" class="block px-4 py-3 text-sm text-slate-200 hover:bg-slate-800">{{ 'menu.preferences' | t }}</a>
<div class="px-4 py-2 text-xs uppercase tracking-wide text-slate-500">{{ 'menu.appearance' | t }}</div>
<div class="px-3 pb-3">
<ng-container *ngTemplateOutlet="themeSwatches"></ng-container>
</div>
<button type="button" (click)="onLogout(); closeUserMenu()" role="menuitem" class="w-full px-4 py-3 text-left text-sm text-red-300 hover:bg-red-900/30">{{ 'menu.logout' | t }}</button>
</div>
</div>
}
</div>
}
</div>
</div>
</div>
<!-- Mobile search sheet: the same <app-search-box> instance is rendered here
through CSS only when the user asks for it. Kept out of the flow so the
page never reflows while typing. -->
<div id="mobile-search-sheet" class="md:hidden border-t border-slate-700/60 bg-slate-900/95 backdrop-blur-sm px-2 pb-2"
[class.hidden]="!mobileSearchOpen()">
<div class="relative">
<app-search-box [placeholder]="('search.placeholder' | t)"
(submitted)="onSearchBoxSubmit($event)"></app-search-box>
</div>
</div>
<!-- Mobile overflow menu -->
<div *ngIf="moreMenuOpen()" id="mobile-more-menu"
class="md:hidden absolute inset-x-0 top-full z-50 max-h-[calc(var(--app-vh)-var(--header-offset)-1rem)] overflow-y-auto overscroll-contain border-b border-slate-700 bg-slate-900 shadow-2xl"
[style.paddingLeft]="'var(--safe-left)'" [style.paddingRight]="'var(--safe-right)'">
@if (providerContextLabel(); as providerLabel) {
<div class="flex items-center justify-between gap-3 border-b border-slate-800 px-4 py-3">
<span class="text-xs uppercase tracking-wide text-slate-400">{{ 'label.provider' | t }}</span>
<span class="inline-flex items-center gap-1 rounded border px-2 py-1 text-xs max-w-[60%] truncate"
[ngStyle]="getProviderColors(providerContext())">
<span class="truncate">{{ providerLabel }}</span>
</span>
</div>
}
<nav class="py-1" [attr.aria-label]="('menu.navigation' | t)">
<button type="button" (click)="toggleThemesBar(); closeMoreMenu()"
class="flex w-full items-center gap-3 px-4 py-3 text-left text-sm text-slate-200 hover:bg-slate-800">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0 opacity-80" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm0-8a3 3 0 1 0 0 6 3 3 0 0 0 0-6z"/>
</svg>
<span class="flex-1">{{ themesBarVisible() ? ('themes.hideBar' | t) : ('themes.showBar' | t) }}</span>
</button>
<a routerLink="/account/preferences" (click)="closeMoreMenu()" class="flex items-center gap-3 px-4 py-3 text-sm text-slate-200 hover:bg-slate-800">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0 opacity-80" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 15.5A3.5 3.5 0 1 1 15.5 12 3.5 3.5 0 0 1 12 15.5Zm7.43-2.53a7.6 7.6 0 0 0 0-1.94l2.05-1.58a.5.5 0 0 0 .12-.64l-1.94-3.36a.5.5 0 0 0-.6-.22l-2.43.98a7.3 7.3 0 0 0-1.67-.97l-.37-2.58A.49.49 0 0 0 14 2h-4a.49.49 0 0 0-.49.42l-.37 2.58a7.7 7.7 0 0 0-1.67.97l-2.43-.98a.5.5 0 0 0-.6.22L2.07 8.58a.5.5 0 0 0 .12.64l2.05 1.58a7.9 7.9 0 0 0 0 1.94l-2.05 1.58a.5.5 0 0 0-.12.64l1.94 3.36a.5.5 0 0 0 .6.22l2.43-.98c.52.4 1.08.73 1.67.97l.37 2.58A.49.49 0 0 0 10 22h4a.49.49 0 0 0 .49-.42l.37-2.58c.59-.24 1.15-.57 1.67-.97l2.43.98a.5.5 0 0 0 .6-.22l1.94-3.36a.5.5 0 0 0-.12-.64Z"/></svg>
<span class="flex-1">{{ 'menu.preferences' | t }}</span>
</a>
@if (user()) {
<a routerLink="/account/history" (click)="closeMoreMenu()" class="flex items-center gap-3 px-4 py-3 text-sm text-slate-200 hover:bg-slate-800">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0 opacity-80" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 8a1 1 0 0 1 1 1v3.38l2.32 1.34a1 1 0 1 1-1 1.74l-2.82-1.63A1 1 0 0 1 11 13V9a1 1 0 0 1 1-1Zm0-6a10 10 0 1 0 10 10 10.01 10.01 0 0 0-10-10Z"/></svg>
<span class="flex-1">{{ 'nav.history' | t }}</span>
</a>
<a routerLink="/library/liked" (click)="closeMoreMenu()" class="flex items-center gap-3 px-4 py-3 text-sm text-slate-200 hover:bg-slate-800">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0 opacity-80" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 21s-6.716-4.248-9.193-6.725A6 6 0 0 1 12 5.414 6 6 0 0 1 21.193 14.275C18.716 16.752 12 21 12 21Z"/></svg>
<span class="flex-1">{{ 'nav.liked' | t }}</span>
</a>
<a routerLink="/library/downloads" (click)="closeMoreMenu()" class="flex items-center gap-3 px-4 py-3 text-sm text-slate-200 hover:bg-slate-800">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0 opacity-80" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 3v10.17l3.59-3.58L17 11l-5 5-5-5 1.41-1.41L12 13.17V3Zm-7 15h14v2H5v-2Z"/></svg>
<span class="flex-1">{{ 'nav.downloads' | t }}</span>
</a>
}
</nav>
<!-- Appearance is offered to everyone, signed in or not: on phones this is
the only place a theme can be changed. -->
<div class="border-t border-slate-800 px-4 py-3">
<div class="mb-2 text-xs uppercase tracking-wide text-slate-500">{{ 'menu.appearance' | t }}</div>
<ng-container *ngTemplateOutlet="themeSwatches"></ng-container>
</div>
@if (user()) {
<div class="border-t border-slate-800 px-4 py-3">
<div class="mb-2 flex items-center gap-3">
<span class="avatar-circle" [ngClass]="avatarClasses()">{{ avatarLetter() }}</span>
<span class="min-w-0 flex-1">
<span class="block truncate text-sm font-semibold text-slate-100">{{ user()?.username || user()?.email }}</span>
<span class="block truncate text-xs text-slate-400">{{ user()?.email }}</span>
</span>
</div>
<button type="button" (click)="onLogout(); closeMoreMenu()" class="w-full rounded px-3 py-3 text-left text-sm text-red-300 hover:bg-red-900/30">
{{ 'menu.logout' | t }}
</button>
</div>
} @else {
<div class="grid grid-cols-2 gap-2 border-t border-slate-800 p-3">
<a routerLink="/auth/login" (click)="closeMoreMenu()" class="rounded bg-slate-800 px-3 py-3 text-center text-sm text-white hover:bg-slate-700">{{ 'btn.login' | t }}</a>
<a routerLink="/auth/register" (click)="closeMoreMenu()" class="rounded bg-red-600 px-3 py-3 text-center text-sm text-white hover:bg-red-500">{{ 'btn.register' | t }}</a>
</div>
}
</div>
</header>
<!-- Backdrop closing the overflow menu / search sheet on outside tap -->
@if (moreMenuOpen() || mobileSearchOpen()) {
<div class="fixed inset-0 z-40 md:hidden bg-black/50" (click)="closeOverlays()" aria-hidden="true"></div>
}
<!-- ======================= Shared templates ======================= -->
<ng-template #themeSwatches>
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-4">
<button type="button" (click)="setTheme('black')"
[attr.aria-pressed]="currentTheme() === 'black'"
[ngClass]="{ 'ring-2 ring-red-500': currentTheme() === 'black' }"
class="flex items-center justify-center gap-1 rounded bg-slate-800 px-2 py-3 text-xs hover:bg-slate-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><circle cx="12" cy="12" r="9"/></svg>
<span class="truncate">{{ 'theme.black' | t }}</span>
</button>
<button type="button" (click)="setTheme('dark')"
[attr.aria-pressed]="currentTheme() === 'dark'"
[ngClass]="{ 'ring-2 ring-red-500': currentTheme() === 'dark' }"
class="flex items-center justify-center gap-1 rounded bg-slate-800 px-2 py-3 text-xs hover:bg-slate-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 1 0 9.79 9.79Z"/></svg>
<span class="truncate">{{ 'theme.dark' | t }}</span>
</button>
<button type="button" (click)="setTheme('blue')"
[attr.aria-pressed]="currentTheme() === 'blue'"
[ngClass]="{ 'ring-2 ring-red-500': currentTheme() === 'blue' }"
class="flex items-center justify-center gap-1 rounded bg-slate-800 px-2 py-3 text-xs hover:bg-slate-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2s7 7.58 7 12a7 7 0 1 1-14 0C5 9.58 12 2 12 2Z"/></svg>
<span class="truncate">{{ 'theme.blue' | t }}</span>
</button>
<button type="button" (click)="setTheme('light')"
[attr.aria-pressed]="currentTheme() === 'light'"
[ngClass]="{ 'ring-2 ring-red-500': currentTheme() === 'light' }"
class="flex items-center justify-center gap-1 rounded bg-slate-800 px-2 py-3 text-xs hover:bg-slate-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 18a6 6 0 1 0 0-12 6 6 0 0 0 0 12Zm0-16a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V3a1 1 0 0 1 1-1Zm0 18a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0v-2a1 1 0 0 1 1-1Zm9-9a1 1 0 0 1 1 1h-2a1 1 0 1 1 0-2h2a1 1 0 0 1-1 1ZM4 12a1 1 0 0 1 1-1H3a1 1 0 1 1 0 2h2a1 1 0 0 1-1-1Zm12.95 6.364a1 1 0 0 1 1.414 1.414l-1.415 1.415a1 1 0 1 1-1.414-1.415l1.415-1.414ZM7.05 4.222a1 1 0 1 1 1.414-1.415L9.88 4.222A1 1 0 0 1 8.465 5.636L7.05 4.222Zm9.9-1.415a1 1 0 1 1 1.414 1.415L17.95 5.636A1 1 0 0 1 16.536 4.22l1.414-1.414ZM7.05 19.778 5.636 21.19a1 1 0 0 1-1.414-1.414L5.636 18.364A1 1 0 0 1 7.05 19.78Z"/></svg>
<span class="truncate">{{ 'theme.light' | t }}</span>
</button>
<button type="button" (click)="setTheme('system')"
[attr.aria-pressed]="currentTheme() === 'system'"
[ngClass]="{ 'ring-2 ring-red-500': currentTheme() === 'system' }"
class="flex items-center justify-center gap-1 rounded bg-slate-800 px-2 py-3 text-xs hover:bg-slate-700">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 shrink-0" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2a10 10 0 1 0 10 10h-2a8 8 0 1 1-8-8V2Z"/></svg>
<span class="truncate">{{ 'theme.system' | t }}</span>
</button>
</div>
</ng-template>
<!-- PeerTube Instance Editor Modal -->
<div *ngIf="editingPeerTubeInstances()" class="fixed inset-0 z-50 flex items-center justify-center bg-black/75 p-4" (click)="editingPeerTubeInstances.set(false)">
<div class="dialog-scroll w-full max-w-md rounded-lg bg-gray-800 p-5 sm:p-6" (click)="$event.stopPropagation()">
<h2 class="mb-3 text-lg font-bold sm:text-xl">Edit PeerTube Instances</h2>
<p class="mb-4 text-sm text-gray-400">Enter one instance domain per line.</p>
<textarea [(ngModel)]="peerTubeInstancesInput" class="h-48 w-full rounded bg-gray-900 p-2 text-white focus:outline-none focus:ring-2 focus:ring-red-500"></textarea>
<div class="mt-4 flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
<button type="button" (click)="editingPeerTubeInstances.set(false)" class="px-4 py-2 rounded bg-gray-600 hover:bg-gray-500">Cancel</button>
<button type="button" (click)="savePeerTubeInstances()" class="px-4 py-2 rounded bg-red-600 hover:bg-red-500">Save</button>
</div>
</div>
</div>