From 2e64dceb1e626d8fefbf9f7e227771a3ae76feda Mon Sep 17 00:00:00 2001 From: josedario87 Date: Fri, 13 Feb 2026 05:50:13 -0600 Subject: [PATCH] feat: Add update_theme functionality with UI support Backend: - Add PUT /api/themes/:id endpoint for updating existing themes Store: - Add updateTheme method to theme store MCP: - Add update_theme tool to modify name, description, or save current variables UI: - Add edit button to ThemeListItem (custom themes only) - Add edit modal with name and description fields - Support editing from both desktop sidebar and mobile dropdown --- CLAUDE.md | 21 +++++ .../src/components/themes/ThemeListItem.vue | 13 +++ frontend/src/pages/ThemesPage.vue | 84 +++++++++++++++++++ frontend/src/services/tools/themeTools.ts | 80 ++++++++++++++++++ frontend/src/stores/theme.ts | 24 ++++++ server/index.ts | 41 +++++++++ 6 files changed, 263 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d03095c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,21 @@ +# Agent UI - Claude Code Guidelines + +## Commits +- NO incluir "Co-Authored-By: Claude" en los commits +- Commits concisos y descriptivos en inglés + +## Proyecto +- Frontend: Vue 3 + TypeScript + Pinia +- Backend: Bun + SQLite +- MCP: @nucleoriofrio/webmcp + +## Herramientas MCP dinámicas +Las herramientas MCP cambian según la página activa: +- `/canvas` - render_html, render_vue_component, save/load components +- `/themes` - get_design_tokens, set_theme_variable, save_theme, update_theme, etc. +- `/components` - gestión de componentes Vue + +## Archivos clave +- `frontend/src/services/tools/` - Herramientas MCP por página +- `frontend/src/stores/theme.ts` - Store de temas +- `server/index.ts` - API HTTP y SQLite diff --git a/frontend/src/components/themes/ThemeListItem.vue b/frontend/src/components/themes/ThemeListItem.vue index 3b8a3df..1d95131 100644 --- a/frontend/src/components/themes/ThemeListItem.vue +++ b/frontend/src/components/themes/ThemeListItem.vue @@ -11,6 +11,7 @@ const emit = defineEmits<{ delete: [id: string] clone: [id: string] setDefault: [id: string] + edit: [theme: Theme] }>() function getAccentColor(): string { @@ -48,6 +49,18 @@ function getAccentColor(): string { + + + + + + @@ -820,6 +875,35 @@ onMounted(() => { border-color: var(--accent); } +.form-group { + margin-bottom: 1rem; +} + +.form-group label { + display: block; + font-size: 0.8rem; + font-weight: 500; + color: var(--text-secondary); + margin-bottom: 0.375rem; +} + +.modal textarea { + width: 100%; + padding: 0.75rem 1rem; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-size: 0.95rem; + font-family: inherit; + resize: vertical; +} + +.modal textarea:focus { + outline: none; + border-color: var(--accent); +} + .modal-actions { display: flex; gap: 0.75rem; diff --git a/frontend/src/services/tools/themeTools.ts b/frontend/src/services/tools/themeTools.ts index 4f2b96a..8a76743 100644 --- a/frontend/src/services/tools/themeTools.ts +++ b/frontend/src/services/tools/themeTools.ts @@ -6,6 +6,7 @@ export const THEME_TOOLS = [ 'get_active_theme', 'set_theme_variable', 'save_theme', + 'update_theme', 'list_themes', 'switch_theme', 'set_default_theme', @@ -212,6 +213,85 @@ export function registerThemeTools() { } ) + // update_theme + registerTool( + 'update_theme', + 'Actualiza un tema existente (nombre, descripción o variables)', + { + type: 'object', + properties: { + theme: { + type: 'string', + description: 'Nombre o ID del tema a actualizar' + }, + name: { + type: 'string', + description: 'Nuevo nombre para el tema (opcional)' + }, + description: { + type: 'string', + description: 'Nueva descripción para el tema (opcional)' + }, + saveCurrentVariables: { + type: 'boolean', + description: 'Si es true, guarda las variables actuales (con los cambios de set_theme_variable) en este tema' + } + }, + required: ['theme'] + }, + async (args: { theme: string; name?: string; description?: string; saveCurrentVariables?: boolean }) => { + try { + const themeStore = useThemeStore() + await themeStore.fetchThemes() + + // Find theme by ID or name + const theme = themeStore.themes.find(t => + t.id === args.theme || t.name.toLowerCase() === args.theme.toLowerCase() + ) + + if (!theme) { + const available = themeStore.themes.map(t => t.name).join(', ') + return `Tema "${args.theme}" no encontrado.\nDisponibles: ${available}` + } + + if (theme.is_system) { + return `No se puede modificar "${theme.name}" porque es un tema del sistema. Usa save_theme para crear una copia.` + } + + // Build update data + const updateData: { name?: string; description?: string; variables?: any } = {} + + if (args.name) { + updateData.name = args.name + } + if (args.description !== undefined) { + updateData.description = args.description + } + if (args.saveCurrentVariables) { + const variablesToSave = themeStore.previewTheme || themeStore.activeTheme?.variables + if (variablesToSave) { + updateData.variables = variablesToSave + } + } + + if (Object.keys(updateData).length === 0) { + return 'No se especificaron cambios. Usa name, description o saveCurrentVariables.' + } + + await themeStore.updateTheme(theme.id, updateData) + + const changes = [] + if (args.name) changes.push(`nombre: "${args.name}"`) + if (args.description !== undefined) changes.push('descripción actualizada') + if (args.saveCurrentVariables) changes.push('variables guardadas') + + return `Tema "${theme.name}" actualizado:\n ${changes.join('\n ')}` + } catch (e: any) { + return `Error al actualizar tema: ${e.message}` + } + } + ) + // list_themes registerTool( 'list_themes', diff --git a/frontend/src/stores/theme.ts b/frontend/src/stores/theme.ts index 9e462a9..e2d6edc 100644 --- a/frontend/src/stores/theme.ts +++ b/frontend/src/stores/theme.ts @@ -138,6 +138,29 @@ export const useThemeStore = defineStore('theme', () => { } } + 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' }) @@ -281,6 +304,7 @@ export const useThemeStore = defineStore('theme', () => { fetchThemes, fetchDesignTokens, saveTheme, + updateTheme, deleteTheme, setDefaultTheme, cloneTheme, diff --git a/server/index.ts b/server/index.ts index ef34f99..180d132 100644 --- a/server/index.ts +++ b/server/index.ts @@ -393,6 +393,47 @@ Bun.serve({ return Response.json({ success: true }, { headers: corsHeaders }) } + // PUT /api/themes/:id - Actualizar un tema existente + if (req.method === 'PUT' && !action) { + const theme = db.query('SELECT * FROM themes WHERE id = ?').get(id) as any + if (!theme) { + return Response.json({ error: 'Theme not found' }, { status: 404, headers: corsHeaders }) + } + + const body = await req.json() + + // Build update query dynamically based on provided fields + const updates: string[] = [] + const values: any[] = [] + + if (body.name !== undefined) { + updates.push('name = ?') + values.push(body.name) + } + if (body.description !== undefined) { + updates.push('description = ?') + values.push(body.description) + } + if (body.variables !== undefined) { + updates.push('variables = ?') + values.push(JSON.stringify(body.variables)) + } + if (body.metadata !== undefined) { + updates.push('metadata = ?') + values.push(JSON.stringify(body.metadata)) + } + + if (updates.length > 0) { + updates.push('updated_at = CURRENT_TIMESTAMP') + values.push(id) + + const sql = `UPDATE themes SET ${updates.join(', ')} WHERE id = ?` + db.run(sql, values) + } + + return Response.json({ success: true, id }, { headers: corsHeaders }) + } + // GET /api/themes/:id - Obtener un tema if (req.method === 'GET' && !action) { const row = db.query('SELECT * FROM themes WHERE id = ?').get(id) as any