feat(fc2a): add tag store, TagAutocomplete, and FandomPicker

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 12:34:17 -04:00
parent 0faeebf3aa
commit 89fee260a6
3 changed files with 241 additions and 0 deletions
+58
View File
@@ -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 }
})