import { defineStore } from 'pinia' import { ref, computed } from 'vue' // ===================== // Types // ===================== export interface ThemeVariables { colors: Record text: Record accent: Record semantic: Record spacing: Record typography: Record effects: Record } export interface ThemeMetadata { author?: string version?: string tags?: string[] base?: string | null exported_at?: string | null } export interface Theme { id: string name: string description?: string is_default: boolean is_system: boolean variables: ThemeVariables metadata?: ThemeMetadata created_at?: string updated_at?: string } export interface DesignTokens { version: string description: string usage: string tokens: ThemeVariables guidelines: Record examples: Record } // Uses relative URLs - works with Vite proxy in dev and Traefik in production const API_URL = '' // ===================== // Store // ===================== export const useThemeStore = defineStore('theme', () => { // State const themes = ref([]) const activeTheme = ref(null) const previewTheme = ref(null) const designTokens = ref(null) const loading = ref(false) const saving = ref(false) const error = ref(null) // Getters const currentVariables = computed(() => { if (previewTheme.value) return previewTheme.value return activeTheme.value?.variables || null }) const systemThemes = computed(() => themes.value.filter(t => t.is_system) ) const userThemes = computed(() => themes.value.filter(t => !t.is_system) ) const hasUnsavedChanges = computed(() => previewTheme.value !== null) const flattenedVariables = computed(() => { const vars = currentVariables.value if (!vars) return {} const flat: Record = {} for (const category of Object.keys(vars)) { for (const [key, value] of Object.entries(vars[category as keyof ThemeVariables])) { flat[`--${key}`] = value } } return flat }) // Actions async function fetchThemes() { loading.value = true error.value = null try { const res = await fetch(`${API_URL}/api/themes`) themes.value = await res.json() const defaultTheme = themes.value.find(t => t.is_default) if (defaultTheme) { activeTheme.value = defaultTheme applyTheme(defaultTheme.variables) } } catch (e) { error.value = 'Error loading themes' console.error(e) } finally { loading.value = false } } async function fetchDesignTokens() { try { const res = await fetch(`${API_URL}/api/design-tokens`) designTokens.value = await res.json() } catch (e) { console.error('Error fetching design tokens:', e) } } async function saveTheme(theme: Partial & { name: string; variables: ThemeVariables }) { saving.value = true try { const res = await fetch(`${API_URL}/api/themes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(theme) }) const result = await res.json() await fetchThemes() return result } catch (e) { error.value = 'Error saving theme' throw e } finally { saving.value = false } } async function updateTheme(id: string, data: { name?: string; description?: string; variables?: ThemeVariables; metadata?: ThemeMetadata }) { saving.value = true try { const res = await fetch(`${API_URL}/api/themes/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) const result = await res.json() if (result.error) { error.value = result.error return null } await fetchThemes() return result } catch (e) { error.value = 'Error updating theme' throw e } finally { saving.value = false } } async function deleteTheme(id: string) { try { const res = await fetch(`${API_URL}/api/themes/${id}`, { method: 'DELETE' }) const result = await res.json() if (result.error) { error.value = result.error return false } await fetchThemes() return true } catch (e) { error.value = 'Error deleting theme' return false } } async function setDefaultTheme(id: string) { try { await fetch(`${API_URL}/api/themes/${id}/default`, { method: 'POST' }) await fetchThemes() } catch (e) { error.value = 'Error setting default theme' } } async function cloneTheme(id: string, newName: string) { const original = themes.value.find(t => t.id === id) if (!original) return null return saveTheme({ name: newName, description: `Cloned from ${original.name}`, variables: JSON.parse(JSON.stringify(original.variables)), metadata: { base: id } }) } function applyTheme(variables: ThemeVariables) { const root = document.documentElement for (const category of Object.keys(variables)) { for (const [key, value] of Object.entries(variables[category as keyof ThemeVariables])) { root.style.setProperty(`--${key}`, value) } } } function selectTheme(theme: Theme) { activeTheme.value = theme previewTheme.value = null applyTheme(theme.variables) } function setPreview(variables: ThemeVariables | null) { previewTheme.value = variables if (variables) { applyTheme(variables) } else if (activeTheme.value) { applyTheme(activeTheme.value.variables) } } function updateVariable(category: keyof ThemeVariables, key: string, value: string) { if (!previewTheme.value && activeTheme.value) { previewTheme.value = JSON.parse(JSON.stringify(activeTheme.value.variables)) } if (previewTheme.value && previewTheme.value[category]) { previewTheme.value[category][key] = value document.documentElement.style.setProperty(`--${key}`, value) } } function exportTheme(theme: Theme): string { return JSON.stringify({ name: theme.name, description: theme.description, variables: theme.variables, metadata: { ...theme.metadata, exported_at: new Date().toISOString() } }, null, 2) } async function importTheme(jsonString: string) { try { const data = JSON.parse(jsonString) if (!data.name || !data.variables) { throw new Error('Invalid theme format') } return saveTheme({ name: data.name, description: data.description, variables: data.variables, metadata: data.metadata }) } catch (e) { error.value = 'Invalid theme file' throw e } } function resetPreview() { previewTheme.value = null if (activeTheme.value) { applyTheme(activeTheme.value.variables) } } async function commitPreview(name?: string) { if (!previewTheme.value) return const themeName = name || activeTheme.value?.name || 'Custom Theme' const themeId = activeTheme.value?.is_system ? undefined : activeTheme.value?.id await saveTheme({ id: themeId, name: themeName, variables: previewTheme.value }) previewTheme.value = null } return { // State themes, activeTheme, previewTheme, designTokens, loading, saving, error, // Getters currentVariables, systemThemes, userThemes, hasUnsavedChanges, flattenedVariables, // Actions fetchThemes, fetchDesignTokens, saveTheme, updateTheme, deleteTheme, setDefaultTheme, cloneTheme, applyTheme, selectTheme, setPreview, updateVariable, exportTheme, importTheme, resetPreview, commitPreview } })