From 89fee260a6c1637993716d47b924711705bb0dd6 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 14 May 2026 12:34:17 -0400 Subject: [PATCH] feat(fc2a): add tag store, TagAutocomplete, and FandomPicker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TagAutocomplete: kind selector + debounced (200ms) search + keyboard nav (up/down/enter/esc). When the operator types a character that doesn't exist, the create flow first opens FandomPicker so the new character tag lands with a valid fandom_id (otherwise the API rejects it). FandomPicker lists existing fandoms (cached after first load) with an inline create-new affordance, so the operator never has to leave the modal to manage fandom hierarchies. Tag store also exposes a kind→icon→color mapping that the TagPanel reuses in Task 22 — keeping chip colors visually distinct per kind without per-app accent collisions. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/modal/FandomPicker.vue | 48 +++++++ .../src/components/modal/TagAutocomplete.vue | 135 ++++++++++++++++++ frontend/src/stores/tags.js | 58 ++++++++ 3 files changed, 241 insertions(+) create mode 100644 frontend/src/components/modal/FandomPicker.vue create mode 100644 frontend/src/components/modal/TagAutocomplete.vue create mode 100644 frontend/src/stores/tags.js diff --git a/frontend/src/components/modal/FandomPicker.vue b/frontend/src/components/modal/FandomPicker.vue new file mode 100644 index 0000000..31189ab --- /dev/null +++ b/frontend/src/components/modal/FandomPicker.vue @@ -0,0 +1,48 @@ + + + diff --git a/frontend/src/components/modal/TagAutocomplete.vue b/frontend/src/components/modal/TagAutocomplete.vue new file mode 100644 index 0000000..cdc81ce --- /dev/null +++ b/frontend/src/components/modal/TagAutocomplete.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/frontend/src/stores/tags.js b/frontend/src/stores/tags.js new file mode 100644 index 0000000..fbb871a --- /dev/null +++ b/frontend/src/stores/tags.js @@ -0,0 +1,58 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { useApi } from '../composables/useApi.js' + +const KIND_OPTIONS = [ + { value: 'general', label: 'General', icon: 'mdi-tag' }, + { value: 'artist', label: 'Artist', icon: 'mdi-palette' }, + { value: 'character', label: 'Character', icon: 'mdi-account-circle' }, + { value: 'fandom', label: 'Fandom', icon: 'mdi-book-open-page-variant' }, + { value: 'series', label: 'Series', icon: 'mdi-bookshelf' }, + { value: 'meta', label: 'Meta', icon: 'mdi-cog-outline' }, + { value: 'rating', label: 'Rating', icon: 'mdi-shield-check-outline' } +] + +const KIND_COLOR = { + artist: 'accent', + character: 'info', + fandom: 'secondary', + series: 'warning', + general: 'on-surface', + meta: 'on-surface', + rating: 'on-surface', + archive: 'on-surface', + post: 'on-surface' +} + +export const useTagStore = defineStore('tags', () => { + const api = useApi() + const fandomCache = ref([]) + + async function autocomplete(q, kind = null, limit = 20) { + if (!q || !q.trim()) return [] + const params = { q, limit } + if (kind) params.kind = kind + return await api.get('/api/tags/autocomplete', { params }) + } + + async function loadFandoms() { + fandomCache.value = await api.get('/api/tags/autocomplete', { + params: { q: ' ', kind: 'fandom', limit: 200 } + }) + return fandomCache.value + } + + async function createFandom(name) { + const fandom = await api.post('/api/tags', { body: { name, kind: 'fandom' } }) + fandomCache.value = [...fandomCache.value, { + id: fandom.id, name: fandom.name, kind: 'fandom', + fandom_id: null, fandom_name: null, image_count: 0 + }] + return fandom + } + + function kindOptions() { return KIND_OPTIONS } + function colorFor(kind) { return KIND_COLOR[kind] || 'on-surface' } + + return { fandomCache, autocomplete, loadFandoms, createFandom, kindOptions, colorFor } +})