refactor: update tag-related styles and UI components with new color system
This commit is contained in:
@@ -45,5 +45,6 @@
|
||||
[noteId]="noteId"
|
||||
[tags]="tags"
|
||||
(saved)="tagsChange.emit($event)"
|
||||
(tagSelected)="tagSelected.emit($event)"
|
||||
></app-tag-manager>
|
||||
</header>
|
||||
|
||||
@@ -24,6 +24,7 @@ export class NoteHeaderComponent implements AfterViewInit, OnDestroy {
|
||||
@Output() openDirectory = new EventEmitter<void>();
|
||||
@Output() copyRequested = new EventEmitter<void>();
|
||||
@Output() tagsChange = new EventEmitter<string[]>();
|
||||
@Output() tagSelected = new EventEmitter<string>();
|
||||
|
||||
pathParts: { prefix: string; filename: string } = { prefix: '', filename: '' };
|
||||
|
||||
|
||||
@@ -92,30 +92,21 @@
|
||||
.mode-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 1rem;
|
||||
background: var(--bg-muted);
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--text-main);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
min-width: 6.5rem;
|
||||
gap: 0.45rem;
|
||||
justify-content: center;
|
||||
min-width: 6.5rem;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mode-button:hover {
|
||||
background: var(--bg-main);
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.mode-button.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: white;
|
||||
.mode-button:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
@@ -126,6 +117,66 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Colored shadows small buttons (shared look with tag manager) */
|
||||
.btn-colored-xs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: #0f172a;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--brand) 18%, transparent) 0%,
|
||||
color-mix(in oklab, var(--brand) 8%, transparent) 100%);
|
||||
box-shadow: 0 6px 14px -10px color-mix(in oklab, var(--brand) 65%, transparent);
|
||||
border: 1px solid color-mix(in oklab, var(--brand) 22%, transparent);
|
||||
transition: transform 0.15s ease, box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-colored-xs:hover,
|
||||
.btn-colored-xs:focus-visible {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 18px -10px color-mix(in oklab, var(--brand) 75%, transparent);
|
||||
border-color: color-mix(in oklab, var(--brand) 35%, transparent);
|
||||
}
|
||||
|
||||
:host-context(.dark) .btn-colored-xs {
|
||||
color: #e2e8f0;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--brand-700) 22%, transparent) 0%,
|
||||
color-mix(in oklab, var(--brand-700) 12%, transparent) 100%);
|
||||
box-shadow: 0 10px 22px -14px color-mix(in oklab, var(--brand-700) 85%, transparent);
|
||||
border-color: color-mix(in oklab, var(--brand-700) 30%, transparent);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: white;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--brand) 60%, transparent) 0%,
|
||||
color-mix(in oklab, var(--brand-700) 60%, transparent) 100%);
|
||||
border-color: color-mix(in oklab, var(--brand-700) 50%, transparent);
|
||||
}
|
||||
|
||||
.btn-neutral {
|
||||
color: inherit;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--surface-1, #e5e7eb) 60%, transparent) 0%,
|
||||
color-mix(in oklab, var(--surface-2, #d1d5db) 60%, transparent) 100%);
|
||||
border-color: color-mix(in oklab, var(--border, #cbd5e1) 60%, transparent);
|
||||
}
|
||||
|
||||
:host-context(.dark) .btn-neutral {
|
||||
color: #e2e8f0;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--card, #111827) 60%, transparent) 0%,
|
||||
color-mix(in oklab, var(--surface2, #0b1220) 60%, transparent) 100%);
|
||||
border-color: color-mix(in oklab, var(--border, #334155) 60%, transparent);
|
||||
}
|
||||
|
||||
/* Theme Grid */
|
||||
.theme-grid {
|
||||
display: grid;
|
||||
|
||||
@@ -31,8 +31,9 @@
|
||||
<button
|
||||
*ngFor="let m of modes"
|
||||
(click)="setMode(m)"
|
||||
class="mode-button"
|
||||
[class.active]="prefs().mode === m"
|
||||
class="mode-button btn-colored-xs"
|
||||
[class.btn-primary]="prefs().mode === m"
|
||||
[class.btn-neutral]="prefs().mode !== m"
|
||||
[attr.aria-label]="'Set mode to ' + m">
|
||||
<svg *ngIf="m === 'system'" class="button-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
|
||||
@@ -100,8 +101,9 @@
|
||||
<button
|
||||
*ngFor="let l of languages"
|
||||
(click)="setLanguage(l)"
|
||||
class="mode-button"
|
||||
[class.active]="prefs().language === l"
|
||||
class="mode-button btn-colored-xs"
|
||||
[class.btn-primary]="prefs().language === l"
|
||||
[class.btn-neutral]="prefs().language !== l"
|
||||
[attr.aria-label]="'Set language to ' + l">
|
||||
<span class="button-text">{{ l === 'fr' ? 'Français' : 'English' }}</span>
|
||||
</button>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { GraphSettingsAccordionComponent } from '../../../components/graph-setti
|
||||
<!-- Expand all -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn-standard-icon"
|
||||
class="btn-colored-xs btn-neutral"
|
||||
title="Expand all"
|
||||
(click)="accordion?.expandAll()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -35,7 +35,7 @@ import { GraphSettingsAccordionComponent } from '../../../components/graph-setti
|
||||
<!-- Collapse all -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn-standard-icon"
|
||||
class="btn-colored-xs btn-neutral"
|
||||
title="Collapse all"
|
||||
(click)="accordion?.collapseAll()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -46,7 +46,7 @@ import { GraphSettingsAccordionComponent } from '../../../components/graph-setti
|
||||
<button
|
||||
type="button"
|
||||
(click)="onResetAll()"
|
||||
class="btn-standard-icon"
|
||||
class="btn-colored-xs btn-primary"
|
||||
title="Reset all settings">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
@@ -57,7 +57,7 @@ import { GraphSettingsAccordionComponent } from '../../../components/graph-setti
|
||||
type="button"
|
||||
(click)="close.emit()"
|
||||
(keydown.escape)="close.emit()"
|
||||
class="btn-standard-icon"
|
||||
class="btn-colored-xs btn-neutral"
|
||||
aria-label="Close settings"
|
||||
title="Close">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -133,6 +133,61 @@ import { GraphSettingsAccordionComponent } from '../../../components/graph-setti
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Colored shadows small buttons (shared look with tag manager) */
|
||||
.btn-colored-xs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: #0f172a;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--brand) 18%, transparent) 0%,
|
||||
color-mix(in oklab, var(--brand) 8%, transparent) 100%);
|
||||
box-shadow: 0 6px 14px -10px color-mix(in oklab, var(--brand) 65%, transparent);
|
||||
border: 1px solid color-mix(in oklab, var(--brand) 22%, transparent);
|
||||
transition: transform 0.15s ease, box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
.btn-colored-xs:hover,
|
||||
.btn-colored-xs:focus-visible {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 18px -10px color-mix(in oklab, var(--brand) 75%, transparent);
|
||||
border-color: color-mix(in oklab, var(--brand) 35%, transparent);
|
||||
}
|
||||
:host-context(.dark) .btn-colored-xs {
|
||||
color: #e2e8f0;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--brand-700) 22%, transparent) 0%,
|
||||
color-mix(in oklab, var(--brand-700) 12%, transparent) 100%);
|
||||
box-shadow: 0 10px 22px -14px color-mix(in oklab, var(--brand-700) 85%, transparent);
|
||||
border-color: color-mix(in oklab, var(--brand-700) 30%, transparent);
|
||||
}
|
||||
.btn-primary {
|
||||
color: white;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--brand) 60%, transparent) 0%,
|
||||
color-mix(in oklab, var(--brand-700) 60%, transparent) 100%);
|
||||
border-color: color-mix(in oklab, var(--brand-700) 50%, transparent);
|
||||
}
|
||||
.btn-neutral {
|
||||
color: inherit;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--surface-1, #e5e7eb) 60%, transparent) 0%,
|
||||
color-mix(in oklab, var(--surface-2, #d1d5db) 60%, transparent) 100%);
|
||||
border-color: color-mix(in oklab, var(--border, #cbd5e1) 60%, transparent);
|
||||
}
|
||||
:host-context(.dark) .btn-neutral {
|
||||
color: #e2e8f0;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--card, #111827) 60%, transparent) 0%,
|
||||
color-mix(in oklab, var(--surface2, #0b1220) 60%, transparent) 100%);
|
||||
border-color: color-mix(in oklab, var(--border, #334155) 60%, transparent);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.panel-content {
|
||||
max-width: 100%;
|
||||
|
||||
@@ -1,2 +1,66 @@
|
||||
/* Overlay styling relies on Tailwind utilities in the template. Keep minimal spacing fixes here if needed. */
|
||||
:host { display: contents; }
|
||||
|
||||
/* Small colored buttons with soft gradient and shadow ("Colored shadows") */
|
||||
.btn-colored-xs {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.35rem 0.6rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: #0f172a;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--brand) 18%, transparent) 0%,
|
||||
color-mix(in oklab, var(--brand) 8%, transparent) 100%);
|
||||
box-shadow: 0 6px 14px -10px color-mix(in oklab, var(--brand) 65%, transparent);
|
||||
border: 1px solid color-mix(in oklab, var(--brand) 22%, transparent);
|
||||
transition: transform 0.15s ease, box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-colored-xs:hover,
|
||||
.btn-colored-xs:focus-visible {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 18px -10px color-mix(in oklab, var(--brand) 75%, transparent);
|
||||
border-color: color-mix(in oklab, var(--brand) 35%, transparent);
|
||||
}
|
||||
|
||||
:host-context(.dark) .btn-colored-xs {
|
||||
color: #e2e8f0;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--brand-700) 22%, transparent) 0%,
|
||||
color-mix(in oklab, var(--brand-700) 12%, transparent) 100%);
|
||||
box-shadow: 0 10px 22px -14px color-mix(in oklab, var(--brand-700) 85%, transparent);
|
||||
border-color: color-mix(in oklab, var(--brand-700) 30%, transparent);
|
||||
}
|
||||
|
||||
.btn-colored-active {
|
||||
outline: 2px solid color-mix(in oklab, var(--brand-700) 45%, transparent);
|
||||
}
|
||||
|
||||
/* Semantic variants for footer actions */
|
||||
.btn-primary {
|
||||
color: white;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--brand) 60%, transparent) 0%,
|
||||
color-mix(in oklab, var(--brand-700) 60%, transparent) 100%);
|
||||
border-color: color-mix(in oklab, var(--brand-700) 50%, transparent);
|
||||
}
|
||||
|
||||
.btn-neutral {
|
||||
color: inherit;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--surface-1, #e5e7eb) 60%, transparent) 0%,
|
||||
color-mix(in oklab, var(--surface-2, #d1d5db) 60%, transparent) 100%);
|
||||
border-color: color-mix(in oklab, var(--border, #cbd5e1) 60%, transparent);
|
||||
}
|
||||
|
||||
:host-context(.dark) .btn-neutral {
|
||||
color: #e2e8f0;
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in oklab, var(--card, #111827) 60%, transparent) 0%,
|
||||
color-mix(in oklab, var(--surface2, #0b1220) 60%, transparent) 100%);
|
||||
border-color: color-mix(in oklab, var(--border, #334155) 60%, transparent);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div class="fixed inset-0 z-50 flex items-end md:items-center justify-center">
|
||||
<div class="absolute inset-0 bg-black/40"></div>
|
||||
<div class="relative rounded-2xl shadow-xl border border-border dark:border-border bg-card dark:bg-main p-4 md:p-5 w-[min(780px,92vw)]">
|
||||
<div class="relative rounded-2xl shadow-xl border border-border dark:border-border bg-card dark:bg-main p-4 md:p-5 w-[min(880px,96vw)] md:max-h-[85vh] overflow-auto">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="h-5 w-5 text-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h.01"/><path d="M3 7h5a2 2 0 0 1 1.414.586l7 7a2 2 0 0 1 0 2.828l-3.172 3.172a2 2 0 0 1-2.828 0l-7-7A2 2 0 0 1 3 12V7Z"/></svg>
|
||||
@@ -12,38 +12,55 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-wrap gap-2 rounded-xl border border-border dark:border-border p-3 min-h-[44px]">
|
||||
@for (t of working(); track t) {
|
||||
<span class="inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-sm font-medium bg-surface1 dark:bg-card">
|
||||
{{ t }}
|
||||
<button type="button" class="w-6 h-6 inline-flex items-center justify-center rounded-full hover:bg-surface1 dark:hover:bg-surface2" (click)="removeTag(t)" aria-label="Retirer">
|
||||
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
<input data-tag-input type="text" [value]="inputValue()" (input)="inputValue.set($any($event.target).value)" (keydown)="onKeydown($event)" placeholder="Ajouter un tag..." class="min-w-[200px] flex-1 bg-transparent outline-none px-2 py-1.5" />
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-border dark:border-border overflow-hidden">
|
||||
<div class="max-h-64 overflow-y-auto">
|
||||
@if (suggestions().length === 0) {
|
||||
<div class="px-3 py-2 text-sm text-muted">Aucune suggestion</div>
|
||||
} @else {
|
||||
<ul>
|
||||
@for (s of suggestions(); track s) {
|
||||
<li>
|
||||
<button type="button" class="w-full text-left px-3 py-2 hover:bg-surface1 dark:hover:bg-card" (click)="pickSuggestion(s)">{{ s }}</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Alphabet filter -->
|
||||
<div class="not-prose">
|
||||
<div class="flex flex-wrap items-center gap-1 md:gap-1.5">
|
||||
<button type="button" class="btn-colored-xs" [class.btn-colored-active]="letter()==='all'" (click)="letter.set('all')">Tous</button>
|
||||
@for (L of letters; track L) {
|
||||
<button type="button" class="btn-colored-xs" [class.btn-colored-active]="letter()===L" (click)="letter.set(L)">{{ L }}</button>
|
||||
}
|
||||
<button type="button" class="btn-colored-xs" [class.btn-colored-active]="letter()==='#'" (click)="letter.set('#')">#</button>
|
||||
<button type="button" class="btn-colored-xs" [class.btn-colored-active]="letter()==='other'" (click)="letter.set('other')">Autre</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected tags + input -->
|
||||
<div class="flex flex-wrap items-center gap-2 rounded-xl border border-border dark:border-border p-3 min-h-[48px]">
|
||||
@for (t of working(); track t) {
|
||||
<span class="md-tag-badge" [ngClass]="tagColorClass(t)" [attr.data-tag]="t">
|
||||
{{ t }}
|
||||
<button type="button" class="ml-1 inline-flex items-center justify-center w-5 h-5 rounded-full border border-transparent hover:border-border/60" (click)="removeTag(t)" aria-label="Retirer">
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
<input data-tag-input
|
||||
type="text"
|
||||
[value]="inputValue()"
|
||||
(input)="inputValue.set($any($event.target).value)"
|
||||
(keydown)="onKeydown($event)"
|
||||
placeholder="Ajouter un tag..."
|
||||
class="min-w-[200px] flex-1 w-full md:w-auto rounded-full border border-border bg-bg-muted/70 py-2.5 px-4 text-sm text-text-main placeholder:text-text-muted shadow-subtle focus:outline-none focus:ring-2 focus:ring-ring transition-all dark:border-border dark:bg-card/80 dark:text-gray-100 dark:placeholder-gray-400 dark:focus:ring-ring" />
|
||||
</div>
|
||||
|
||||
<!-- Suggestions -->
|
||||
<div class="rounded-xl border border-border dark:border-border p-3">
|
||||
@if (suggestions().length === 0) {
|
||||
<div class="px-1 py-0.5 text-sm text-muted">Aucune suggestion</div>
|
||||
} @else {
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@for (s of suggestions(); track s) {
|
||||
<button type="button" class="md-tag-badge btn-colored-xs" [ngClass]="tagColorClass(s)" (click)="pickSuggestion(s)" [attr.data-tag]="s" [title]="'Ajouter #'+s">{{ s }}</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center justify-end gap-2 pt-1">
|
||||
<button type="button" class="px-3 py-1.5 rounded-lg border border-border dark:border-border hover:bg-surface1 dark:hover:bg-card" (click)="close.emit()">Annuler</button>
|
||||
<button type="button" class="px-3 py-1.5 rounded-lg bg-primary hover:bg-brand-700 text-white disabled:opacity-60" [disabled]="saving()" (click)="save()">Enregistrer</button>
|
||||
<button type="button" class="btn-colored-xs btn-neutral" (click)="close.emit()">Annuler</button>
|
||||
<button type="button" class="btn-colored-xs btn-primary" [disabled]="saving()" (click)="save()">Enregistrer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,10 +28,28 @@ export class TagEditorOverlayComponent {
|
||||
working = signal<string[]>([]);
|
||||
count = computed(() => this.working().length);
|
||||
|
||||
private readonly tagPaletteSize = 12;
|
||||
private readonly tagColorCache = new Map<string, number>();
|
||||
|
||||
// Alphabet filter state: 'all' | 'other' | '#', or 'a'..'z'
|
||||
letter = signal<string>('all');
|
||||
readonly letters = Array.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
||||
|
||||
ngOnInit() {
|
||||
this.working.set(uniqueTags(this.value || []));
|
||||
}
|
||||
|
||||
tagColorClass(tag: string): string {
|
||||
if (!tag) return '';
|
||||
const normalized = `${tag}`.toLowerCase();
|
||||
if (this.tagColorCache.has(normalized)) return `md-tag-color-${this.tagColorCache.get(normalized)}`;
|
||||
let hash = 0;
|
||||
for (let i = 0; i < normalized.length; i++) hash = (hash * 31 + normalized.charCodeAt(i)) >>> 0;
|
||||
const colorIndex = hash % this.tagPaletteSize;
|
||||
this.tagColorCache.set(normalized, colorIndex);
|
||||
return `md-tag-color-${colorIndex}`;
|
||||
}
|
||||
|
||||
private allKnownTags = computed(() => {
|
||||
const provided = this.allTags || [];
|
||||
if (provided.length > 0) return provided;
|
||||
@@ -44,10 +62,21 @@ export class TagEditorOverlayComponent {
|
||||
|
||||
suggestions = computed(() => {
|
||||
const q = this.inputValue().trim().toLowerCase();
|
||||
const selected = this.letter();
|
||||
const source = this.allKnownTags();
|
||||
const pool = q ? source.filter(t => t.toLowerCase().includes(q)) : source;
|
||||
|
||||
const matchesLetter = (t: string) => {
|
||||
if (selected === 'all') return true;
|
||||
const first = (t?.[0] || '').toUpperCase();
|
||||
if (selected === '#') return /[0-9]/.test(first);
|
||||
if (selected === 'other') return !/[A-Z0-9]/.test(first);
|
||||
return first === selected.toUpperCase();
|
||||
};
|
||||
|
||||
const byLetter = source.filter(matchesLetter);
|
||||
const byQuery = q ? byLetter.filter(t => t.toLowerCase().includes(q)) : byLetter;
|
||||
const exist = new Set(this.working().map(t => t.toLowerCase()));
|
||||
return pool.filter(t => !exist.has(t.toLowerCase())).slice(0, 50);
|
||||
return byQuery.filter(t => !exist.has(t.toLowerCase())).slice(0, 100);
|
||||
});
|
||||
|
||||
onKeydown(ev: KeyboardEvent) {
|
||||
|
||||
@@ -12,9 +12,11 @@
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-1.5 rounded-full bg-slate-700/10 dark:bg-muted/10 text-sm font-medium hover:bg-slate-500/10 dark:hover:bg-surface2/10"
|
||||
class="md-tag-badge"
|
||||
*ngFor="let tag of normalizedTags()"
|
||||
[ngClass]="tagColorClass(tag)"
|
||||
(click)="onChipClick(tag)"
|
||||
[attr.data-tag]="tag"
|
||||
[title]="'Voir les notes #'+tag">
|
||||
{{ tag }}
|
||||
</button>
|
||||
|
||||
@@ -32,6 +32,20 @@ export class TagManagerComponent {
|
||||
isEditing = signal(false);
|
||||
readonly normalizedTags = computed(() => uniqueTags(this.tagsSignal()));
|
||||
|
||||
private readonly tagPaletteSize = 12;
|
||||
private readonly tagColorCache = new Map<string, number>();
|
||||
|
||||
tagColorClass(tag: string): string {
|
||||
if (!tag) return '';
|
||||
const normalized = `${tag}`.toLowerCase();
|
||||
if (this.tagColorCache.has(normalized)) return `md-tag-color-${this.tagColorCache.get(normalized)}`;
|
||||
let hash = 0;
|
||||
for (let i = 0; i < normalized.length; i++) hash = (hash * 31 + normalized.charCodeAt(i)) >>> 0;
|
||||
const colorIndex = hash % this.tagPaletteSize;
|
||||
this.tagColorCache.set(normalized, colorIndex);
|
||||
return `md-tag-color-${colorIndex}`;
|
||||
}
|
||||
|
||||
toggleEditor(): void {
|
||||
const next = !this.isEditing();
|
||||
this.isEditing.set(next);
|
||||
|
||||
Reference in New Issue
Block a user