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:
@@ -1,41 +1,44 @@
|
||||
<div class="container mx-auto p-4 sm:p-6 max-w-6xl">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6 gap-4">
|
||||
<h2 class="text-3xl font-bold text-slate-100 border-l-4 border-red-500 pl-4 flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div class="mb-6 flex flex-col flex-wrap gap-4 xl:flex-row xl:items-center xl:justify-between">
|
||||
<h2 class="flex items-center border-l-4 border-red-500 pl-4 text-2xl font-bold text-slate-100 sm:text-3xl">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mr-2 h-7 w-7 shrink-0 sm:h-8 sm:w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Historique
|
||||
</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<button (click)="resetFilters()" [disabled]="!hasActiveFilters()"
|
||||
class="px-3 py-2 text-xs rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700 disabled:opacity-40 transition-colors">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button type="button" (click)="resetFilters()" [disabled]="!hasActiveFilters()"
|
||||
class="chip px-3 py-2 text-xs rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700 disabled:opacity-40 transition-colors">
|
||||
Réinitialiser les filtres
|
||||
</button>
|
||||
<button (click)="clearActiveTab()" [disabled]="!!clearingTab()"
|
||||
class="px-3 py-2 text-xs rounded-lg bg-red-600/90 hover:bg-red-500 text-white disabled:opacity-60 transition-colors">
|
||||
<button type="button" (click)="clearActiveTab()" [disabled]="!!clearingTab()"
|
||||
class="chip px-3 py-2 text-xs rounded-lg bg-red-600/90 hover:bg-red-500 text-white disabled:opacity-60 transition-colors">
|
||||
<span *ngIf="!clearingTab()">Tout effacer (onglet)</span>
|
||||
<span *ngIf="clearingTab()">Suppression…</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex gap-2 mb-6 border-b border-slate-700" role="tablist">
|
||||
<button role="tab" (click)="setTab('search')"
|
||||
<!-- Tabs: swipeable rail on narrow screens so no tab is ever unreachable -->
|
||||
<div class="scroll-rail no-scrollbar mb-6 flex gap-2 border-b border-slate-700" role="tablist">
|
||||
<button type="button" role="tab" (click)="setTab('search')"
|
||||
class="chip shrink-0 whitespace-nowrap"
|
||||
[class]="activeTab() === 'search'
|
||||
? 'px-4 py-2 text-sm font-semibold text-white border-b-2 border-blue-500 -mb-px'
|
||||
: 'px-4 py-2 text-sm text-slate-400 hover:text-slate-200 border-b-2 border-transparent -mb-px'">
|
||||
Recherches
|
||||
<span class="ml-1 text-xs bg-slate-700 px-2 py-0.5 rounded-full">{{ filteredSearch().length }}</span>
|
||||
</button>
|
||||
<button role="tab" (click)="setTab('watch')"
|
||||
<button type="button" role="tab" (click)="setTab('watch')"
|
||||
class="chip shrink-0 whitespace-nowrap"
|
||||
[class]="activeTab() === 'watch'
|
||||
? 'px-4 py-2 text-sm font-semibold text-white border-b-2 border-red-500 -mb-px'
|
||||
: 'px-4 py-2 text-sm text-slate-400 hover:text-slate-200 border-b-2 border-transparent -mb-px'">
|
||||
Visionnage
|
||||
<span class="ml-1 text-xs bg-slate-700 px-2 py-0.5 rounded-full">{{ filteredWatch().length }}</span>
|
||||
</button>
|
||||
<button role="tab" (click)="setTab('transcripts')"
|
||||
<button type="button" role="tab" (click)="setTab('transcripts')"
|
||||
class="chip shrink-0 whitespace-nowrap"
|
||||
[class]="activeTab() === 'transcripts'
|
||||
? 'px-4 py-2 text-sm font-semibold text-white border-b-2 border-emerald-500 -mb-px'
|
||||
: 'px-4 py-2 text-sm text-slate-400 hover:text-slate-200 border-b-2 border-transparent -mb-px'">
|
||||
@@ -144,7 +147,7 @@
|
||||
</div>
|
||||
|
||||
<!-- ===== Contenu : RECHERCHES ===== -->
|
||||
<section *ngIf="activeTab() === 'search'" class="bg-slate-800/50 rounded-lg p-6 shadow-lg">
|
||||
<section *ngIf="activeTab() === 'search'" class="bg-slate-800/50 rounded-lg p-4 shadow-lg sm:p-6">
|
||||
<div *ngIf="searchLoading() && filteredSearch().length === 0" class="text-center py-6">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent"></div>
|
||||
<p class="mt-2 text-slate-400">Chargement…</p>
|
||||
@@ -155,7 +158,7 @@
|
||||
<ul *ngIf="filteredSearch().length > 0" class="space-y-3">
|
||||
<li *ngFor="let s of filteredSearch()"
|
||||
class="group relative bg-slate-700/50 hover:bg-slate-700/80 rounded-lg overflow-hidden transition-all duration-200">
|
||||
<a [routerLink]="['/search']" [queryParams]="getSearchQueryParams(s)" class="block p-4 pr-24 hover:no-underline">
|
||||
<a [routerLink]="['/search']" [queryParams]="getSearchQueryParams(s)" class="block p-4 pr-4 pb-14 hover:no-underline sm:pr-24 sm:pb-4">
|
||||
<div class="text-slate-100 font-medium group-hover:text-blue-400 transition-colors" [innerHTML]="s.query | histHighlight: searchQuery()"></div>
|
||||
<div class="flex items-center mt-1 text-xs text-slate-400 gap-2">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded border" [ngStyle]="getProviderColors(getSearchProvider(s).id)">
|
||||
@@ -166,21 +169,21 @@
|
||||
</div>
|
||||
</a>
|
||||
<button (click)="onDeleteSearch(s)" [disabled]="deletingSearchId() === s.id"
|
||||
class="absolute bottom-2 right-2 px-2 py-1 rounded-md bg-red-600/90 hover:bg-red-500 text-white text-xs disabled:opacity-60">
|
||||
class="chip absolute bottom-2 right-2 rounded-md bg-red-600/90 hover:bg-red-500 px-3 py-2 text-white text-xs disabled:opacity-60">
|
||||
Supprimer
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mt-4 text-center" *ngIf="searchHasMore() && filteredSearch().length > 0">
|
||||
<button (click)="loadMore()" [disabled]="searchLoading()"
|
||||
class="px-4 py-2 text-sm rounded-lg border border-slate-600 text-slate-200 hover:bg-slate-700 disabled:opacity-50">
|
||||
class="chip px-4 py-3 text-sm rounded-lg border border-slate-600 text-slate-200 hover:bg-slate-700 disabled:opacity-50">
|
||||
{{ searchLoading() ? 'Chargement…' : 'Charger plus' }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Contenu : VISIONNAGE ===== -->
|
||||
<section *ngIf="activeTab() === 'watch'" class="bg-slate-800/50 rounded-lg p-6 shadow-lg">
|
||||
<section *ngIf="activeTab() === 'watch'" class="bg-slate-800/50 rounded-lg p-4 shadow-lg sm:p-6">
|
||||
<div *ngIf="watchLoading() && filteredWatch().length === 0" class="text-center py-6">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-red-500 border-t-transparent"></div>
|
||||
<p class="mt-2 text-slate-400">Chargement…</p>
|
||||
@@ -191,7 +194,7 @@
|
||||
<ul *ngIf="filteredWatch().length > 0" class="space-y-3">
|
||||
<li *ngFor="let w of filteredWatch()"
|
||||
class="group relative bg-slate-700/50 hover:bg-slate-700/80 rounded-lg overflow-hidden transition-all duration-200">
|
||||
<a [routerLink]="['/watch', w.video_id]" [queryParams]="{ p: w.provider }" class="block p-4 pr-24 hover:no-underline">
|
||||
<a [routerLink]="['/watch', w.video_id]" [queryParams]="{ p: w.provider }" class="block p-4 pr-4 pb-14 hover:no-underline sm:pr-24 sm:pb-4">
|
||||
<div class="flex">
|
||||
<div class="relative flex-shrink-0 w-24 h-16 bg-slate-700 rounded overflow-hidden flex items-center justify-center">
|
||||
@if (thumbOf(w)) { <img [src]="thumbOf(w)" alt="" loading="lazy" class="w-full h-full object-cover" /> }
|
||||
@@ -215,21 +218,21 @@
|
||||
</div>
|
||||
</a>
|
||||
<button (click)="onDeleteWatch(w)" [disabled]="deletingWatchId() === w.id"
|
||||
class="absolute bottom-2 right-2 px-2 py-1 rounded-md bg-red-600/90 hover:bg-red-500 text-white text-xs disabled:opacity-60">
|
||||
class="chip absolute bottom-2 right-2 rounded-md bg-red-600/90 hover:bg-red-500 px-3 py-2 text-white text-xs disabled:opacity-60">
|
||||
Supprimer
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mt-4 text-center" *ngIf="watchHasMore() && filteredWatch().length > 0">
|
||||
<button (click)="loadMore()" [disabled]="watchLoading()"
|
||||
class="px-4 py-2 text-sm rounded-lg border border-slate-600 text-slate-200 hover:bg-slate-700 disabled:opacity-50">
|
||||
class="chip px-4 py-3 text-sm rounded-lg border border-slate-600 text-slate-200 hover:bg-slate-700 disabled:opacity-50">
|
||||
{{ watchLoading() ? 'Chargement…' : 'Charger plus' }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Contenu : TRANSCRIPTS ===== -->
|
||||
<section *ngIf="activeTab() === 'transcripts'" class="bg-slate-800/50 rounded-lg p-6 shadow-lg">
|
||||
<section *ngIf="activeTab() === 'transcripts'" class="bg-slate-800/50 rounded-lg p-4 shadow-lg sm:p-6">
|
||||
<div *ngIf="transcriptLoading() && filteredTranscripts().length === 0" class="text-center py-6">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-4 border-emerald-500 border-t-transparent"></div>
|
||||
<p class="mt-2 text-slate-400">Chargement…</p>
|
||||
@@ -283,7 +286,7 @@
|
||||
</ul>
|
||||
<div class="mt-4 text-center" *ngIf="transcriptHasMore() && filteredTranscripts().length > 0">
|
||||
<button (click)="loadMore()" [disabled]="transcriptLoading()"
|
||||
class="px-4 py-2 text-sm rounded-lg border border-slate-600 text-slate-200 hover:bg-slate-700 disabled:opacity-50">
|
||||
class="chip px-4 py-3 text-sm rounded-lg border border-slate-600 text-slate-200 hover:bg-slate-700 disabled:opacity-50">
|
||||
{{ transcriptLoading() ? 'Chargement…' : 'Charger plus' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="container mx-auto p-4 sm:p-6">
|
||||
<h2 class="text-3xl font-bold mb-6 text-slate-100 border-l-4 border-red-500 pl-4">{{ 'preferences.title' | t }}</h2>
|
||||
<h2 class="mb-6 border-l-4 border-red-500 pl-4 text-2xl font-bold text-slate-100 sm:text-3xl">{{ 'preferences.title' | t }}</h2>
|
||||
|
||||
<!-- Notices -->
|
||||
<div *ngIf="notice()" class="mb-4 bg-emerald-900/30 border border-emerald-600 text-emerald-200 px-4 py-3 rounded">{{ notice() }}</div>
|
||||
@@ -9,14 +9,14 @@
|
||||
<!-- General -->
|
||||
<form (submit)="onSave($event)" class="space-y-6">
|
||||
<div class="bg-slate-800/50 rounded-xl border border-slate-700 overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<div class="flex flex-col gap-3 border-b border-slate-700 px-4 py-4 sm:px-5 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold text-slate-100">{{ 'preferences.general' | t : 'Préférences générales' }}</h3>
|
||||
<p class="text-sm text-slate-400">{{ 'preferences.general_hint' | t : 'Personnalisez votre expérience' }}</p>
|
||||
</div>
|
||||
<button [disabled]="saving()" class="px-4 py-2 rounded bg-red-600 hover:bg-red-500 disabled:opacity-50">{{ 'btn.save' | t : 'Enregistrer' }}</button>
|
||||
<button type="submit" [disabled]="saving()" class="chip w-full rounded bg-red-600 px-4 py-3 hover:bg-red-500 disabled:opacity-50 sm:w-auto sm:py-2">{{ 'btn.save' | t : 'Enregistrer' }}</button>
|
||||
</div>
|
||||
<div class="p-5 grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div class="grid grid-cols-1 gap-5 p-4 sm:p-5 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm text-slate-400 mb-1">{{ 'label.language' | t : 'Langue' }}</label>
|
||||
<select class="w-full rounded bg-slate-900 text-slate-100 border border-slate-700 px-3 py-2" [ngModel]="language()" (ngModelChange)="language.set($event)" name="language">
|
||||
@@ -53,16 +53,16 @@
|
||||
|
||||
<!-- Langues à télécharger (sous-titres/transcripts) -->
|
||||
<div class="bg-slate-800/50 rounded-xl border border-slate-700 overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-slate-700">
|
||||
<div class="border-b border-slate-700 px-4 py-4 sm:px-5">
|
||||
<h3 class="text-lg font-semibold text-slate-100">{{ 'preferences.download_languages' | t : 'Langues à télécharger' }}</h3>
|
||||
<p class="text-sm text-slate-400">{{ 'preferences.download_languages_hint' | t : 'Seules les langues activées sont affichées et téléchargées (sous-titres/transcriptions). Français et anglais par défaut.' }}</p>
|
||||
</div>
|
||||
<div class="p-5">
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
<div class="p-4 sm:p-5">
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2 sm:gap-3 md:grid-cols-3 lg:grid-cols-4">
|
||||
<label *ngFor="let opt of downloadLanguageOptions"
|
||||
class="flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-900 px-3 py-2 cursor-pointer hover:border-slate-500">
|
||||
class="flex cursor-pointer items-center gap-2 rounded-lg border border-slate-700 bg-slate-900 px-3 py-3 hover:border-slate-500 sm:py-2">
|
||||
<input type="checkbox"
|
||||
class="h-4 w-4 accent-red-600"
|
||||
class="h-5 w-5 shrink-0 accent-red-600"
|
||||
[checked]="isDownloadLanguageEnabled(opt.code)"
|
||||
(change)="toggleDownloadLanguage(opt.code)"
|
||||
[name]="'dl-' + opt.code" />
|
||||
@@ -78,35 +78,35 @@
|
||||
<!-- PeerTube Instances (below general) -->
|
||||
<section class="space-y-4">
|
||||
<div class="bg-slate-800/50 rounded-xl border border-slate-700 overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-slate-700">
|
||||
<div class="border-b border-slate-700 px-4 py-4 sm:px-5">
|
||||
<h3 class="text-lg font-semibold text-slate-100">PeerTube</h3>
|
||||
<p class="text-sm text-slate-400">{{ 'preferences.peertube_hint' | t : 'Gérez vos instances PeerTube' }}</p>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<form (submit)="addPeerTubeInstance($event)" class="flex items-center gap-3">
|
||||
<div class="space-y-4 p-4 sm:p-5">
|
||||
<form (submit)="addPeerTubeInstance($event)" class="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<input type="text" name="peertube"
|
||||
class="flex-1 rounded bg-slate-900 text-slate-100 border border-slate-700 px-3 py-2"
|
||||
class="min-w-0 flex-1 rounded bg-slate-900 text-slate-100 border border-slate-700 px-3 py-3 sm:py-2"
|
||||
[placeholder]="'preferences.add_instance_placeholder' | t : 'ex: example.org'"
|
||||
[ngModel]="newPeerTubeInstance()" (ngModelChange)="newPeerTubeInstance.set($event)" />
|
||||
<button type="submit" class="px-3 py-2 rounded bg-slate-700 hover:bg-slate-600 text-white">{{ 'btn.add' | t : 'Ajouter' }}</button>
|
||||
<button type="submit" class="chip rounded bg-slate-700 px-4 py-3 text-white hover:bg-slate-600 sm:py-2">{{ 'btn.add' | t : 'Ajouter' }}</button>
|
||||
</form>
|
||||
|
||||
<div *ngIf="(peerTubeInstances() || []).length === 0" class="text-sm text-slate-400">{{ 'preferences.no_instances' | t : 'Aucune instance pour le moment.' }}</div>
|
||||
|
||||
<ul class="divide-y divide-slate-700 rounded border border-slate-700 overflow-hidden">
|
||||
<li *ngFor="let host of peerTubeInstances()" class="flex items-center justify-between px-4 py-2 bg-slate-900">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-block w-2 h-2 rounded-full"
|
||||
<li *ngFor="let host of peerTubeInstances()" class="flex flex-col gap-2 bg-slate-900 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:py-2">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class="inline-block h-2 w-2 shrink-0 rounded-full"
|
||||
[ngClass]="{ 'bg-emerald-400': activePeerTubeInstance() === host, 'bg-slate-600': activePeerTubeInstance() !== host }"></span>
|
||||
<span class="text-slate-200">{{ host }}</span>
|
||||
<span class="min-w-0 truncate text-slate-200" [title]="host">{{ host }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<button type="button" (click)="setActivePeerTube(host)"
|
||||
class="px-2 py-1 rounded border"
|
||||
class="chip px-3 py-2 rounded border"
|
||||
[ngClass]="{ 'border-emerald-500 text-emerald-300': activePeerTubeInstance() === host, 'border-slate-600 text-slate-300 hover:bg-slate-800': activePeerTubeInstance() !== host }">
|
||||
{{ activePeerTubeInstance() === host ? ('preferences.active' | t : 'Actif') : ('preferences.set_active' | t : 'Définir actif') }}
|
||||
</button>
|
||||
<button type="button" (click)="removePeerTubeInstance(host)" class="px-2 py-1 rounded bg-red-600 hover:bg-red-500 text-white">{{ 'btn.remove' | t : 'Supprimer' }}</button>
|
||||
<button type="button" (click)="removePeerTubeInstance(host)" class="chip px-3 py-2 rounded bg-red-600 hover:bg-red-500 text-white">{{ 'btn.remove' | t : 'Supprimer' }}</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<div class="container mx-auto p-4 sm:p-6">
|
||||
<h2 class="text-3xl font-bold mb-6 text-slate-100 border-l-4 border-red-500 pl-4">Sessions</h2>
|
||||
<h2 class="mb-6 border-l-4 border-red-500 pl-4 text-2xl font-bold text-slate-100 sm:text-3xl">Sessions</h2>
|
||||
|
||||
<div *ngIf="error()" class="mb-4 bg-amber-900/40 border border-amber-600 text-amber-200 p-3 rounded">{{ error() }}</div>
|
||||
<div *ngIf="error()" class="mb-4 rounded border border-amber-600 bg-amber-900/40 p-3 text-amber-200">{{ error() }}</div>
|
||||
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<button class="px-4 py-2 rounded bg-slate-700 hover:bg-slate-600 text-white" (click)="reload()" [disabled]="loading()">Reload</button>
|
||||
<button class="px-4 py-2 rounded bg-red-600 hover:bg-red-500 text-white" (click)="logoutAll()" [disabled]="loading()">Logout all devices</button>
|
||||
<div class="mb-4 flex flex-col gap-3 sm:flex-row">
|
||||
<button type="button" class="chip w-full rounded bg-slate-700 px-4 py-3 text-white hover:bg-slate-600 sm:w-auto sm:py-2" (click)="reload()" [disabled]="loading()">Reload</button>
|
||||
<button type="button" class="chip w-full rounded border border-red-500/40 bg-red-600/20 px-4 py-3 text-red-200 hover:bg-red-600/40 sm:w-auto sm:py-2" (click)="logoutAll()" [disabled]="loading()">Logout all devices</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="loading()" class="text-slate-400">Loading...</div>
|
||||
@@ -13,9 +13,10 @@
|
||||
<div *ngIf="!loading() && items().length === 0" class="text-slate-400">No active sessions.</div>
|
||||
|
||||
<ul class="space-y-3">
|
||||
<li *ngFor="let s of items()" class="p-4 bg-slate-800 rounded flex items-start justify-between gap-4">
|
||||
<div class="text-sm text-slate-200">
|
||||
<div><span class="text-slate-400">Session ID:</span> <code>{{ s.id }}</code></div>
|
||||
<li *ngFor="let s of items()" class="flex flex-col gap-3 rounded bg-slate-800 p-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<!-- UUIDs and User-Agent strings cannot wrap on their own: force it. -->
|
||||
<div class="min-w-0 flex-1 break-anywhere text-sm text-slate-200">
|
||||
<div><span class="text-slate-400">Session ID:</span> <code class="break-all">{{ s.id }}</code></div>
|
||||
<div><span class="text-slate-400">User Agent:</span> {{ s.userAgent || '—' }}</div>
|
||||
<div><span class="text-slate-400">Device:</span> {{ s.deviceInfo || '—' }}</div>
|
||||
<div><span class="text-slate-400">IP:</span> {{ s.ip || '—' }}</div>
|
||||
@@ -25,8 +26,8 @@
|
||||
<div><span class="text-slate-400">Expires:</span> {{ s.expiresAt || '—' }}</div>
|
||||
<div *ngIf="s.revokedAt"><span class="text-slate-400">Revoked:</span> {{ s.revokedAt }}</div>
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
<button class="px-3 py-1 rounded bg-slate-700 hover:bg-slate-600 text-white" (click)="revoke(s.id)">Revoke</button>
|
||||
<div class="shrink-0 sm:pl-4">
|
||||
<button type="button" class="chip w-full rounded bg-slate-700 px-4 py-3 text-white hover:bg-slate-600 sm:py-2" (click)="revoke(s.id)">Revoke</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user