feat(fase-11): settings tag management subsection (11.3.6)
Adds a "Tags" section to /settings between Collectives and Account. Lists every tag in the active collective with: * inline rename — tap the chip, edit, Enter / blur to save * recolor swatch row (8 preset dots, current colour outlined) * delete button per tag (cascades item attachments via FK) Uses the tagsStore which is already subscribed to item_tags realtime, so edits from another tab / device update this view live without a reload. No new screens; everything is inline in the existing settings layout. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,15 @@
|
||||
import ImageCropper from '$lib/components/ImageCropper.svelte';
|
||||
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
|
||||
import SyncConflictsPanel from '$lib/components/SyncConflictsPanel.svelte';
|
||||
import TagChip from '$lib/components/TagChip.svelte';
|
||||
import {
|
||||
tags as tagsStore,
|
||||
loadTags,
|
||||
renameTag,
|
||||
recolorTag,
|
||||
deleteTag
|
||||
} from '$lib/stores/tags';
|
||||
import type { ItemTag, ItemTagColor } from '@colectivo/types';
|
||||
import { languageTag, setLanguageTag } from '$lib/paraglide/runtime';
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
import type { ThemePreference } from '$lib/theme';
|
||||
@@ -43,8 +52,54 @@
|
||||
|
||||
onMount(async () => {
|
||||
await loadProfile();
|
||||
if ($currentCollective) {
|
||||
await loadTags($currentCollective.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Inline edit state for the tag list (one row at a time).
|
||||
let editingTagId = $state<string | null>(null);
|
||||
let editingTagName = $state('');
|
||||
const TAG_COLORS: ItemTagColor[] = [
|
||||
'slate',
|
||||
'red',
|
||||
'amber',
|
||||
'green',
|
||||
'sky',
|
||||
'indigo',
|
||||
'pink',
|
||||
'stone'
|
||||
];
|
||||
|
||||
function startEditTag(tag: ItemTag) {
|
||||
editingTagId = tag.id;
|
||||
editingTagName = tag.name;
|
||||
}
|
||||
|
||||
async function commitEditTag() {
|
||||
const id = editingTagId;
|
||||
const next = editingTagName.trim();
|
||||
editingTagId = null;
|
||||
editingTagName = '';
|
||||
if (id && next) {
|
||||
await renameTag(id, next);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelEditTag() {
|
||||
editingTagId = null;
|
||||
editingTagName = '';
|
||||
}
|
||||
|
||||
async function handleRecolor(tag: ItemTag, color: ItemTagColor) {
|
||||
if (tag.color === color) return;
|
||||
await recolorTag(tag.id, color);
|
||||
}
|
||||
|
||||
async function handleDeleteTag(id: string) {
|
||||
await deleteTag(id);
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
if (!$currentUser) return;
|
||||
|
||||
@@ -397,6 +452,68 @@
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- Tags (Fase 11.3.6) — collective-scoped tag management. Lives in
|
||||
settings because the tag flow on the list view is per-item; the
|
||||
full list of tags + rename + recolor + delete is admin-style. -->
|
||||
<section data-testid="settings-tags-section" class="pt-8">
|
||||
<p class="mb-3 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||
{m.settings_tags_section()}
|
||||
</p>
|
||||
{#if $tagsStore.length === 0}
|
||||
<p class="text-sm text-text-secondary">{m.settings_tags_empty()}</p>
|
||||
{:else}
|
||||
<ul class="space-y-2">
|
||||
{#each $tagsStore as tag (tag.id)}
|
||||
<li
|
||||
data-testid="settings-tag-row"
|
||||
data-tag={tag.name}
|
||||
class="flex items-center gap-3 rounded-md bg-surface-raised px-3 py-2"
|
||||
>
|
||||
{#if editingTagId === tag.id}
|
||||
<input
|
||||
bind:value={editingTagName}
|
||||
onblur={commitEditTag}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
void commitEditTag();
|
||||
} else if (e.key === 'Escape') cancelEditTag();
|
||||
}}
|
||||
class="min-w-0 flex-1 rounded bg-background px-2 py-1 text-sm text-text-primary outline-none ring-1 ring-black/10 focus:ring-slate-900 dark:ring-white/10 dark:focus:ring-slate-100"
|
||||
/>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => startEditTag(tag)}
|
||||
class="min-w-0 flex-1 text-left"
|
||||
>
|
||||
<TagChip name={tag.name} color={tag.color} />
|
||||
</button>
|
||||
{/if}
|
||||
<div class="flex shrink-0 items-center gap-1" aria-label={m.settings_tags_color()}>
|
||||
{#each TAG_COLORS as color}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => handleRecolor(tag, color)}
|
||||
aria-label={color}
|
||||
class="h-4 w-4 rounded-full bg-{color}-500 {tag.color === color ? 'ring-2 ring-offset-1 ring-slate-700 dark:ring-slate-300' : 'opacity-70 hover:opacity-100'}"
|
||||
></button>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="settings-tag-delete"
|
||||
onclick={() => handleDeleteTag(tag.id)}
|
||||
class="rounded-md border border-slate-300 px-2 py-1 text-xs font-medium text-slate-700 hover:bg-slate-50 dark:border-slate-600 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
>
|
||||
{m.settings_tags_delete()}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- Account — spacing creates the separation, not a border line -->
|
||||
<section class="pt-8">
|
||||
<p class="mb-3 text-[13px] font-semibold uppercase tracking-[0.05em] text-text-secondary">
|
||||
|
||||
Reference in New Issue
Block a user