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 @@
+
+
+ Pick a fandom
+
+
+
+ Or create a new fandom:
+
+
+ Create
+
+
+
+
+ Cancel
+ Use this fandom
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+ {{ iconFor(h.kind) }}
+
+
+
+ {{ h.name }}
+ — {{ h.fandom_name }}
+
+
+ {{ h.image_count }}
+
+
+
+
+ {{ iconFor(kind) }}
+
+ Create "{{ query }}" as {{ kind }}
+
+
+
+
+
+
+
+
+
+
+
+
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 }
+})