- Create traefik/agent-ui.yml with full routing config for domain z590.nucleoriofrio.com - Add frontend/src/config/endpoints.ts for automatic HTTP/HTTPS detection - Update all hardcoded localhost URLs to use relative paths - WebSocket connections auto-detect wss:// vs ws:// based on page protocol - Configure path-based WebSocket routing (/ws/terminal, /ws/mcp, /ws/status, /ws/whisper) - Add commented IP whitelist middleware for future security
322 lines
7.9 KiB
TypeScript
322 lines
7.9 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref, computed } from 'vue'
|
|
|
|
// =====================
|
|
// Types
|
|
// =====================
|
|
|
|
export interface ThemeVariables {
|
|
colors: Record<string, string>
|
|
text: Record<string, string>
|
|
accent: Record<string, string>
|
|
semantic: Record<string, string>
|
|
spacing: Record<string, string>
|
|
typography: Record<string, string>
|
|
effects: Record<string, string>
|
|
}
|
|
|
|
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<string, string>
|
|
examples: Record<string, string>
|
|
}
|
|
|
|
// 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<Theme[]>([])
|
|
const activeTheme = ref<Theme | null>(null)
|
|
const previewTheme = ref<ThemeVariables | null>(null)
|
|
const designTokens = ref<DesignTokens | null>(null)
|
|
const loading = ref(false)
|
|
const saving = ref(false)
|
|
const error = ref<string | null>(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<string, string> = {}
|
|
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<Theme> & { 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
|
|
}
|
|
})
|