From 2b04edc51179562bf19bd13081e0ed6ef02d88f9 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Mon, 18 May 2026 03:27:36 +0200 Subject: [PATCH] feat(fase-11): settings tag management subsection (11.3.6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/routes/(app)/settings/+page.svelte | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/apps/web/src/routes/(app)/settings/+page.svelte b/apps/web/src/routes/(app)/settings/+page.svelte index 37bc2e0..e0dc28f 100644 --- a/apps/web/src/routes/(app)/settings/+page.svelte +++ b/apps/web/src/routes/(app)/settings/+page.svelte @@ -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(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} + +
+

+ {m.settings_tags_section()} +

+ {#if $tagsStore.length === 0} +

{m.settings_tags_empty()}

+ {:else} +
    + {#each $tagsStore as tag (tag.id)} +
  • + {#if editingTagId === tag.id} + { + 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} + + {/if} +
    + {#each TAG_COLORS as color} + + {/each} +
    + +
  • + {/each} +
+ {/if} +
+