531 lines
24 KiB
HTML
531 lines
24 KiB
HTML
<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-3xl font-bold text-slate-100">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">
|
|
<button
|
|
class="rounded-md bg-red-600 px-3 py-1.5 text-sm font-semibold text-white transition hover:bg-red-500"
|
|
(click)="openNewGroup()"
|
|
type="button"
|
|
>
|
|
+ Nouveau groupe
|
|
</button>
|
|
<button
|
|
class="rounded-md border border-slate-700 bg-slate-800 px-3 py-1.5 text-sm text-slate-200 transition hover:border-slate-500 hover:text-white"
|
|
(click)="refresh()"
|
|
type="button"
|
|
>
|
|
Rafraîchir
|
|
</button>
|
|
<a
|
|
[routerLink]="['/library/import']"
|
|
class="rounded-md border border-slate-700 bg-slate-800 px-3 py-1.5 text-sm text-slate-200 transition hover:border-slate-500 hover:text-white"
|
|
>
|
|
⬇ Importer (Google / Twitch)
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Onglets façon PocketTube -->
|
|
<div class="mb-4 flex gap-2 border-b border-slate-800">
|
|
<button
|
|
type="button"
|
|
(click)="setTab('groups')"
|
|
class="px-4 py-2 text-sm font-semibold transition"
|
|
[class.text-red-300]="activeTab() === 'groups'"
|
|
[class.border-b-2]="activeTab() === 'groups'"
|
|
[class.border-red-500]="activeTab() === 'groups'"
|
|
[class.text-slate-400]="activeTab() !== 'groups'"
|
|
>
|
|
Groupes ({{ groups().length }})
|
|
</button>
|
|
<button
|
|
type="button"
|
|
(click)="setTab('channels')"
|
|
class="px-4 py-2 text-sm font-semibold transition"
|
|
[class.text-red-300]="activeTab() === 'channels'"
|
|
[class.border-b-2]="activeTab() === 'channels'"
|
|
[class.border-red-500]="activeTab() === 'channels'"
|
|
[class.text-slate-400]="activeTab() !== 'channels'"
|
|
>
|
|
Chaînes ({{ items().length }})
|
|
</button>
|
|
</div>
|
|
|
|
<!-- ==================== ONGLET GROUPES ==================== -->
|
|
<div *ngIf="activeTab() === 'groups'">
|
|
<div *ngIf="groups().length > 0; else noGroups" class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
|
<div
|
|
*ngFor="let group of groups()"
|
|
(click)="openGroupChannel(group)"
|
|
class="cursor-pointer rounded-xl border border-slate-800 bg-slate-900/60 p-4 shadow-lg shadow-black/30 transition hover:border-slate-600"
|
|
>
|
|
<div class="flex items-start justify-between gap-2">
|
|
<div class="flex items-center gap-3">
|
|
<span
|
|
class="flex h-11 w-11 items-center justify-center rounded-lg text-xl"
|
|
[style.background-color]="group.color + '26'"
|
|
[style.border]="'1px solid ' + group.color + '66'"
|
|
>{{ group.icon }}</span>
|
|
<div>
|
|
<h3 class="font-semibold text-slate-100">{{ group.name }}</h3>
|
|
<p class="text-xs text-slate-400">{{ group.channelCount }} chaîne(s)</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-1" (click)="$event.stopPropagation()">
|
|
<button
|
|
type="button"
|
|
(click)="openEditGroup(group, $event)"
|
|
class="rounded px-2 py-1 text-xs text-slate-400 hover:bg-slate-800 hover:text-white"
|
|
title="Modifier le groupe"
|
|
>✏️</button>
|
|
<button
|
|
type="button"
|
|
(click)="deleteGroup(group, $event)"
|
|
class="rounded px-2 py-1 text-xs text-slate-400 hover:bg-slate-800 hover:text-red-300"
|
|
title="Supprimer le groupe"
|
|
>🗑️</button>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3 flex items-center justify-between">
|
|
<span
|
|
class="inline-block h-1.5 w-24 rounded-full"
|
|
[style.background-color]="group.color"
|
|
></span>
|
|
<span class="text-xs font-medium text-slate-400 hover:text-red-300">Voir les chaînes →</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ng-template #noGroups>
|
|
<div class="flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-slate-700 bg-slate-900/60 p-10 text-center">
|
|
<div class="text-4xl">📁</div>
|
|
<h3 class="text-lg font-semibold text-slate-100">Aucun groupe pour le moment</h3>
|
|
<p class="text-sm text-slate-400">Créez des groupes thématiques (Tech, Musique, Sport…) pour organiser vos abonnements.</p>
|
|
<button
|
|
type="button"
|
|
(click)="openNewGroup()"
|
|
class="mt-2 rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white hover:bg-red-500"
|
|
>
|
|
+ Créer mon premier groupe
|
|
</button>
|
|
</div>
|
|
</ng-template>
|
|
</div>
|
|
|
|
<!-- ==================== ONGLET CHAÎNES ==================== -->
|
|
<div *ngIf="activeTab() === 'channels'" class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_280px]">
|
|
<div class="bg-slate-900/60 rounded-xl border border-slate-800 p-4 shadow-lg shadow-black/30">
|
|
<!-- Filtre par groupe -->
|
|
<div class="flex flex-wrap items-center gap-2 border-b border-slate-800 pb-4">
|
|
<span class="text-xs uppercase tracking-wide text-slate-400">Groupe</span>
|
|
<div class="flex flex-wrap gap-2">
|
|
<button
|
|
type="button"
|
|
(click)="setGroupFilter('all')"
|
|
class="rounded-full border px-3 py-1 text-xs font-semibold transition"
|
|
[class.bg-red-500/20]="activeGroupId() === 'all'"
|
|
[class.text-red-200]="activeGroupId() === 'all'"
|
|
[class.border-red-500/40]="activeGroupId() === 'all'"
|
|
[class.border-slate-700]="activeGroupId() !== 'all'"
|
|
[class.text-slate-300]="activeGroupId() !== 'all'"
|
|
[class.bg-slate-800/70]="activeGroupId() !== 'all'"
|
|
>
|
|
Tous
|
|
</button>
|
|
<button
|
|
*ngFor="let group of groups()"
|
|
type="button"
|
|
(click)="setGroupFilter(group.id)"
|
|
class="rounded-full border px-3 py-1 text-xs font-semibold transition"
|
|
[class.bg-red-500/20]="activeGroupId() === group.id"
|
|
[class.text-red-200]="activeGroupId() === group.id"
|
|
[class.border-red-500/40]="activeGroupId() === group.id"
|
|
[class.border-slate-700]="activeGroupId() !== group.id"
|
|
[class.text-slate-300]="activeGroupId() !== group.id"
|
|
[class.bg-slate-800/70]="activeGroupId() !== group.id"
|
|
>
|
|
{{ group.icon }} {{ group.name }} ({{ group.channelCount }})
|
|
</button>
|
|
<button
|
|
*ngIf="ungroupedCount() > 0"
|
|
type="button"
|
|
(click)="setGroupFilter('ungrouped')"
|
|
class="rounded-full border px-3 py-1 text-xs font-semibold transition"
|
|
[class.bg-amber-500/20]="activeGroupId() === 'ungrouped'"
|
|
[class.text-amber-200]="activeGroupId() === 'ungrouped'"
|
|
[class.border-amber-500/40]="activeGroupId() === 'ungrouped'"
|
|
[class.border-slate-700]="activeGroupId() !== 'ungrouped'"
|
|
[class.text-slate-300]="activeGroupId() !== 'ungrouped'"
|
|
[class.bg-slate-800/70]="activeGroupId() !== 'ungrouped'"
|
|
>
|
|
Sans groupe ({{ ungroupedCount() }})
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recherche + filtres provider -->
|
|
<div class="flex flex-wrap items-center gap-2 border-b border-slate-800 py-4">
|
|
<input
|
|
type="search"
|
|
placeholder="Rechercher une chaîne…"
|
|
[ngModel]="searchQuery()"
|
|
(ngModelChange)="searchQuery.set($event)"
|
|
class="w-full sm:w-56 rounded-md border border-slate-700 bg-slate-800 px-3 py-1.5 text-sm text-slate-100 placeholder:text-slate-500 focus:border-red-500 focus:outline-none"
|
|
/>
|
|
<div class="flex flex-wrap gap-2">
|
|
<button
|
|
*ngFor="let filter of filters"
|
|
type="button"
|
|
(click)="setFilter(filter.id)"
|
|
class="rounded-full border px-3 py-1 text-xs font-semibold transition"
|
|
[class.bg-red-500/20]="activeFilter() === filter.id"
|
|
[class.text-red-200]="activeFilter() === filter.id"
|
|
[class.border-red-500/40]="activeFilter() === filter.id"
|
|
[class.border-slate-700]="activeFilter() !== filter.id"
|
|
[class.text-slate-300]="activeFilter() !== filter.id"
|
|
[class.bg-slate-800/70]="activeFilter() !== filter.id"
|
|
>
|
|
{{ filter.label }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-2 py-4">
|
|
<div class="flex items-center gap-2 text-sm text-slate-400">
|
|
<span>Trier par :</span>
|
|
<button
|
|
*ngFor="let sort of sorts"
|
|
type="button"
|
|
(click)="setSort(sort.key)"
|
|
class="rounded-md px-3 py-1 text-xs transition"
|
|
[class.bg-emerald-500/20]="activeSort() === sort.key"
|
|
[class.text-emerald-200]="activeSort() === sort.key"
|
|
[class.bg-slate-800/60]="activeSort() !== sort.key"
|
|
[class.text-slate-300]="activeSort() !== sort.key"
|
|
>
|
|
{{ sort.label }}
|
|
</button>
|
|
</div>
|
|
<button
|
|
*ngIf="filteredItems().length > 0"
|
|
type="button"
|
|
(click)="toggleSelectAll()"
|
|
class="text-xs text-slate-400 hover:text-white"
|
|
>
|
|
{{ selectedCount() > 0 ? 'Tout désélectionner' : 'Tout sélectionner' }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Barre d'actions en lot -->
|
|
<div *ngIf="selectedCount() > 0" class="mb-4 flex flex-wrap items-center gap-2 rounded-lg border border-sky-500/30 bg-sky-500/10 p-3">
|
|
<span class="text-xs font-semibold text-sky-200">{{ selectedCount() }} sélectionnée(s)</span>
|
|
<select
|
|
[ngModel]="bulkGroupId()"
|
|
(ngModelChange)="bulkGroupId.set($event)"
|
|
class="rounded-md border border-slate-700 bg-slate-800 px-2 py-1 text-xs text-slate-100"
|
|
>
|
|
<option value="">— Choisir un groupe —</option>
|
|
<option *ngFor="let group of groups()" [value]="group.id">{{ group.icon }} {{ group.name }}</option>
|
|
</select>
|
|
<button
|
|
type="button"
|
|
(click)="bulkAddToGroup()"
|
|
[disabled]="!bulkGroupId()"
|
|
class="rounded-md bg-sky-600 px-3 py-1 text-xs font-semibold text-white disabled:opacity-40 hover:bg-sky-500"
|
|
>
|
|
Ajouter au groupe
|
|
</button>
|
|
<button
|
|
*ngIf="activeGroupId() !== 'all' && activeGroupId() !== 'ungrouped'"
|
|
type="button"
|
|
(click)="bulkRemoveFromActiveGroup()"
|
|
class="rounded-md border border-slate-600 px-3 py-1 text-xs text-slate-200 hover:border-red-400 hover:text-red-200"
|
|
>
|
|
Retirer du groupe actif
|
|
</button>
|
|
</div>
|
|
|
|
<ng-container *ngIf="filteredItems().length > 0; else emptyState">
|
|
<div class="divide-y divide-slate-800">
|
|
<div
|
|
*ngFor="let item of filteredItems()"
|
|
class="flex flex-col gap-4 py-4 md:flex-row md:items-center md:justify-between"
|
|
>
|
|
<ng-container *ngIf="item.channel as channel; else subscriptionFallback">
|
|
<div class="flex items-center gap-3">
|
|
<input
|
|
type="checkbox"
|
|
[checked]="isSelected(item.subscriptionId)"
|
|
(change)="toggleSelect(item.subscriptionId)"
|
|
class="h-4 w-4 shrink-0 accent-red-500"
|
|
title="Sélectionner pour action en lot"
|
|
/>
|
|
<a [routerLink]="['/channel', channel.provider, channel.externalId]" title="Ouvrir la page de la chaîne">
|
|
<img
|
|
[src]="channel.avatarUrl || 'https://placehold.co/80x80/png'"
|
|
[alt]="channel.title || channel.handle || 'Avatar'"
|
|
class="h-16 w-16 rounded-full border border-slate-800 object-cover hover:ring-2 hover:ring-red-500/60"
|
|
/>
|
|
</a>
|
|
<div class="space-y-1">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<a [routerLink]="['/channel', channel.provider, channel.externalId]"
|
|
class="text-lg font-semibold text-slate-100 hover:text-red-300">
|
|
{{ channel.title || channel.handle || 'Chaîne inconnue' }}
|
|
</a>
|
|
<app-provider-badge [provider]="channel.provider"></app-provider-badge>
|
|
<span *ngIf="channel.verified" class="text-xs text-emerald-300">✓ Vérifiée</span>
|
|
</div>
|
|
<!-- Badges groupes -->
|
|
<div class="flex flex-wrap gap-1">
|
|
<span
|
|
*ngFor="let gid of itemGroupIds(item)"
|
|
class="rounded-full px-2 py-0.5 text-[11px] font-semibold"
|
|
[style.background-color]="'#1e293b'"
|
|
[style.color]="'#e2e8f0'"
|
|
[style.border]="'1px solid #475569'"
|
|
>
|
|
{{ groupNameById(gid) }}
|
|
</span>
|
|
<span *ngIf="itemGroupIds(item).length === 0" class="text-[11px] italic text-slate-500">
|
|
Sans groupe
|
|
</span>
|
|
</div>
|
|
<div class="flex flex-wrap gap-3 text-sm text-slate-400">
|
|
<span *ngIf="channel.handle">{{ channel.handle }}</span>
|
|
<span *ngIf="channel.subsCount != null">
|
|
{{ channel.subsCount | number }} abonnés
|
|
</span>
|
|
<span class="text-slate-500">
|
|
Abonné depuis {{ item.createdAt | date:'mediumDate' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col items-stretch gap-2 text-sm md:text-right">
|
|
<!-- Assigneur de groupes -->
|
|
<div class="relative">
|
|
<button
|
|
class="w-full rounded-md border border-slate-700 bg-slate-800/60 px-3 py-1 text-sm text-slate-300 transition hover:border-slate-500 hover:text-white"
|
|
type="button"
|
|
(click)="togglePicker(item.subscriptionId, $event)"
|
|
>
|
|
📁 Groupes ({{ itemGroupIds(item).length }})
|
|
</button>
|
|
<div
|
|
*ngIf="pickerFor() === item.subscriptionId"
|
|
class="absolute right-0 z-20 mt-1 max-h-64 w-64 overflow-y-auto rounded-lg border border-slate-700 bg-slate-900 p-2 text-left shadow-xl"
|
|
>
|
|
<div *ngIf="groups().length === 0" class="p-2 text-xs text-slate-400">
|
|
Aucun groupe.
|
|
<button type="button" (click)="openNewGroup()" class="text-red-300 hover:underline">Créer un groupe</button>
|
|
</div>
|
|
<label
|
|
*ngFor="let group of groups()"
|
|
class="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-sm text-slate-200 hover:bg-slate-800"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
[checked]="itemGroupIds(item).includes(group.id)"
|
|
(change)="toggleMembership(item, group.id)"
|
|
class="h-4 w-4 accent-red-500"
|
|
/>
|
|
<span>{{ group.icon }}</span>
|
|
<span class="flex-1 truncate">{{ group.name }}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<app-subscribe-button
|
|
[provider]="channel.provider"
|
|
[externalId]="channel.externalId"
|
|
[channelMeta]="channel"
|
|
(unsubscribed)="refresh()"
|
|
(subscribed)="refresh()"
|
|
></app-subscribe-button>
|
|
<button
|
|
class="rounded-md border border-slate-700 bg-slate-800/60 px-3 py-1 text-sm text-slate-300 transition hover:border-slate-500 hover:text-white"
|
|
type="button"
|
|
(click)="openChannel(item)"
|
|
>
|
|
Voir la chaîne
|
|
</button>
|
|
</div>
|
|
</ng-container>
|
|
<ng-template #subscriptionFallback>
|
|
<div class="flex-1 text-sm text-slate-400">
|
|
<p class="font-medium text-slate-200">Chaîne non disponible</p>
|
|
<p class="text-xs">Essayez de rafraîchir l'abonnement plus tard.</p>
|
|
</div>
|
|
</ng-template>
|
|
</div>
|
|
</div>
|
|
</ng-container>
|
|
|
|
<ng-template #emptyState>
|
|
<div class="flex flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-slate-700 bg-slate-900/60 p-10 text-center">
|
|
<h3 class="text-lg font-semibold text-slate-100">Aucune chaîne dans ce filtre</h3>
|
|
<p class="text-sm text-slate-400">Changez de groupe, de filtre ou abonnez-vous à de nouvelles chaînes.</p>
|
|
<a
|
|
[routerLink]="['/library/import']"
|
|
class="mt-2 rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-red-500"
|
|
>
|
|
⬇ Importer mes abonnements (Google / Twitch)
|
|
</a>
|
|
</div>
|
|
</ng-template>
|
|
</div>
|
|
|
|
<aside class="rounded-xl border border-slate-800 bg-slate-900/50 p-4 shadow-lg shadow-black/30">
|
|
<h3 class="text-lg font-semibold text-slate-100">Groupes</h3>
|
|
<p class="mt-1 text-xs text-slate-500">Cliquez pour filtrer les chaînes.</p>
|
|
<ul class="mt-3 space-y-1">
|
|
<li>
|
|
<button
|
|
type="button"
|
|
(click)="setGroupFilter('all'); setTab('channels')"
|
|
class="flex w-full items-center justify-between rounded px-2 py-1.5 text-sm hover:bg-slate-800"
|
|
[class.text-red-200]="activeGroupId() === 'all'"
|
|
[class.text-slate-300]="activeGroupId() !== 'all'"
|
|
>
|
|
<span>📚 Toutes</span>
|
|
<span class="text-xs text-slate-500">{{ items().length }}</span>
|
|
</button>
|
|
</li>
|
|
<li *ngFor="let group of groups()">
|
|
<button
|
|
type="button"
|
|
(click)="setGroupFilter(group.id); setTab('channels')"
|
|
class="flex w-full items-center justify-between rounded px-2 py-1.5 text-sm hover:bg-slate-800"
|
|
[class.text-red-200]="activeGroupId() === group.id"
|
|
[class.text-slate-300]="activeGroupId() !== group.id"
|
|
>
|
|
<span class="truncate">{{ group.icon }} {{ group.name }}</span>
|
|
<span class="text-xs text-slate-500">{{ group.channelCount }}</span>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
<button
|
|
type="button"
|
|
(click)="openNewGroup()"
|
|
class="mt-3 w-full rounded-md border border-dashed border-slate-600 px-3 py-1.5 text-xs text-slate-300 hover:border-red-400 hover:text-white"
|
|
>
|
|
+ Nouveau groupe
|
|
</button>
|
|
<p class="mt-4 text-sm text-slate-500">
|
|
Cache TTL: {{ hasCache() ? '6h' : '—' }}.
|
|
</p>
|
|
</aside>
|
|
</div>
|
|
|
|
<!-- ==================== MODALE GROUPE ==================== -->
|
|
<div *ngIf="showGroupModal()" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" (click)="closeGroupModal()">
|
|
<div class="max-h-[90vh] w-full max-w-lg overflow-y-auto rounded-xl border border-slate-700 bg-slate-900 p-5 shadow-2xl" (click)="$event.stopPropagation()">
|
|
<div class="flex items-center gap-3">
|
|
<span
|
|
class="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl text-2xl transition-colors"
|
|
[style.background-color]="groupColor() + '26'"
|
|
[style.border]="'1px solid ' + groupColor() + '66'"
|
|
>{{ groupIcon() }}</span>
|
|
<div class="min-w-0">
|
|
<h3 class="truncate text-lg font-semibold text-slate-100">
|
|
{{ editingGroup() ? 'Modifier le groupe' : 'Nouveau groupe' }}
|
|
</h3>
|
|
<p class="truncate text-xs text-slate-400">{{ groupName() || 'Donnez un nom à votre groupe…' }}</p>
|
|
</div>
|
|
</div>
|
|
<label class="mt-4 block text-xs font-semibold uppercase tracking-wide text-slate-400">Nom</label>
|
|
<input
|
|
type="text"
|
|
[ngModel]="groupName()"
|
|
(ngModelChange)="groupName.set($event)"
|
|
maxlength="80"
|
|
placeholder="Ex : Tech, Musique, Sport…"
|
|
class="mt-1 w-full rounded-md border border-slate-700 bg-slate-800 px-3 py-2 text-sm text-slate-100 placeholder:text-slate-500 focus:border-red-500 focus:outline-none"
|
|
/>
|
|
<div class="mt-4 flex items-center justify-between gap-2">
|
|
<label class="block text-xs font-semibold uppercase tracking-wide text-slate-400" for="group-icon-search">
|
|
Icône <span class="text-slate-500">({{ filteredIcons().length }}/{{ icons.length }})</span>
|
|
</label>
|
|
<button
|
|
*ngIf="iconSearch()"
|
|
type="button"
|
|
(click)="iconSearch.set('')"
|
|
class="text-xs text-slate-400 underline hover:text-slate-200"
|
|
>
|
|
Effacer
|
|
</button>
|
|
</div>
|
|
<div class="relative mt-1">
|
|
<svg class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
</svg>
|
|
<input
|
|
id="group-icon-search"
|
|
type="text"
|
|
[ngModel]="iconSearch()"
|
|
(ngModelChange)="iconSearch.set($event)"
|
|
placeholder="Rechercher : musique, sport, cuisine…"
|
|
class="w-full rounded-md border border-slate-700 bg-slate-800 py-2 pl-9 pr-3 text-sm text-slate-100 placeholder:text-slate-500 focus:border-red-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
<div *ngIf="filteredIcons().length > 0; else noGroupIcon" class="mt-2 grid max-h-48 grid-cols-8 gap-1 overflow-y-auto rounded-lg border border-slate-800 bg-slate-900/60 p-2 sm:grid-cols-10">
|
|
<button
|
|
*ngFor="let icon of filteredIcons()"
|
|
type="button"
|
|
(click)="groupIcon.set(icon)"
|
|
[title]="icon"
|
|
[attr.aria-label]="'Icône ' + icon"
|
|
[attr.aria-pressed]="groupIcon() === icon"
|
|
class="rounded-md px-1 py-1.5 text-xl transition"
|
|
[class.bg-red-500/20]="groupIcon() === icon"
|
|
[class.ring-1]="groupIcon() === icon"
|
|
[class.ring-red-500]="groupIcon() === icon"
|
|
[class.hover:bg-slate-800]="groupIcon() !== icon"
|
|
>{{ icon }}</button>
|
|
</div>
|
|
<ng-template #noGroupIcon>
|
|
<p class="mt-2 rounded-lg border border-dashed border-slate-700 p-4 text-center text-sm text-slate-400">
|
|
Aucune icône pour « {{ iconSearch() }} ».
|
|
</p>
|
|
</ng-template>
|
|
<label class="mt-4 block text-xs font-semibold uppercase tracking-wide text-slate-400">Couleur</label>
|
|
<div class="mt-1 flex flex-wrap gap-1.5">
|
|
<button
|
|
*ngFor="let color of colors"
|
|
type="button"
|
|
(click)="groupColor.set(color)"
|
|
class="h-7 w-7 rounded-full transition"
|
|
[style.background-color]="color"
|
|
[class.ring-2]="groupColor() === color"
|
|
[class.ring-white]="groupColor() === color"
|
|
[class.ring-offset-2]="groupColor() === color"
|
|
[class.ring-offset-slate-900]="groupColor() === color"
|
|
[title]="color"
|
|
[attr.aria-label]="'Couleur ' + color"
|
|
[attr.aria-pressed]="groupColor() === color"
|
|
></button>
|
|
</div>
|
|
<div class="mt-5 flex justify-end gap-2 border-t border-slate-800 pt-4">
|
|
<button
|
|
type="button"
|
|
(click)="closeGroupModal()"
|
|
class="rounded-md border border-slate-700 px-4 py-2 text-sm text-slate-300 hover:border-slate-500 hover:text-white"
|
|
>
|
|
Annuler
|
|
</button>
|
|
<button
|
|
type="button"
|
|
(click)="saveGroup()"
|
|
[disabled]="!groupName().trim() || groupSaving()"
|
|
class="rounded-md bg-red-600 px-4 py-2 text-sm font-semibold text-white disabled:opacity-40 hover:bg-red-500"
|
|
>
|
|
{{ groupSaving() ? '…' : (editingGroup() ? 'Enregistrer' : 'Créer') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|