diff --git a/README.md b/README.md index 946ce60..2dfeb53 100644 --- a/README.md +++ b/README.md @@ -367,6 +367,230 @@ frontend/src/services/dynamicComponents.ts (~300 lines) --- +--- + +# Theme System + +Agent UI includes a powerful theming engine with visual editor, presets, and design tokens for consistent styling across dynamic components. + +## Overview + +The theme system provides: + +- **Visual Editor**: Edit CSS variables with live preview +- **Presets**: System themes (Dark/Light) + custom user themes +- **Persistence**: Themes saved to SQLite database +- **Export/Import**: Share themes as JSON files +- **Design Tokens**: Structured guide for LLM agents to follow + +## Accessing the Theme Editor + +Navigate to `/themes` in the UI or click the palette icon in the sidebar. + +## Theme Structure + +Themes are organized by categories: + +```javascript +{ + "colors": { + "bg-primary": "#0f0f14", + "bg-secondary": "#16161d", + "bg-hover": "#1e1e28", + "bg-tertiary": "#252530", + "border-color": "#2a2a3a" + }, + "text": { + "text-primary": "#e4e4e7", + "text-secondary": "#a1a1aa", + "text-muted": "#52525b" + }, + "accent": { + "accent": "#6366f1", + "accent-hover": "#818cf8", + "accent-muted": "rgba(99, 102, 241, 0.2)", + "accent-text": "#ffffff" + }, + "semantic": { + "success": "#22c55e", + "success-bg": "rgba(34, 197, 94, 0.15)", + "warning": "#eab308", + "warning-bg": "rgba(234, 179, 8, 0.15)", + "error": "#ef4444", + "error-bg": "rgba(239, 68, 68, 0.15)", + "info": "#3b82f6", + "info-bg": "rgba(59, 130, 246, 0.15)" + }, + "spacing": { + "radius-sm": "4px", + "radius-md": "8px", + "radius-lg": "12px", + "radius-full": "9999px" + }, + "typography": { + "font-sans": "Inter, system-ui, sans-serif", + "font-mono": "JetBrains Mono, monospace" + }, + "effects": { + "shadow-sm": "0 1px 2px rgba(0,0,0,0.3)", + "shadow-md": "0 4px 12px rgba(0,0,0,0.4)", + "transition-fast": "0.15s ease" + } +} +``` + +## MCP Tools + +### `get_design_tokens` + +Returns design tokens for the active theme. Use this to create components with consistent styling. + +```javascript +{ + category: "all" | "colors" | "text" | "accent" | "semantic" | "spacing" | "typography" | "effects" +} +``` + +**Response example:** + +``` +Design Tokens del tema "Dark": + +[COLORS] + --bg-primary: #0f0f14 + --bg-secondary: #16161d + ... + +GUÍA DE USO: +- Usa var(--nombre-variable) en CSS +- Los componentes dinámicos tienen acceso a $theme.getVariable('nombre') +- Colores semánticos: success, warning, error, info (con -bg para fondos) +- Radius: radius-sm (4px), radius-md (8px), radius-lg (12px), radius-full (9999px) +``` + +## Theme API in Dynamic Components + +Components have access to `$theme` helper: + +```javascript +// Get CSS variable value +$theme.getVariable('accent') // "#6366f1" + +// Set variable temporarily (runtime only) +$theme.setVariable('accent', '#ff0000') + +// Get all design tokens +$theme.getTokens() + +// Get active theme object +$theme.getActiveTheme() + +// Get current variables +$theme.getVariables() +``` + +### Example: Theme-Aware Component + +```javascript +{ + id: "themed-card", + name: "ThemedCard", + template: ` +
+

{{ title }}

+

Current accent: {{ accentColor }}

+ +
+ `, + setup: ` + const accentColor = ref($theme.getVariable('accent')); + + const randomizeAccent = () => { + const hue = Math.floor(Math.random() * 360); + const newColor = \`hsl(\${hue}, 70%, 60%)\`; + $theme.setVariable('accent', newColor); + accentColor.value = newColor; + }; + + return { title: props.title, accentColor, randomizeAccent }; + `, + style: ` + .card { + padding: var(--radius-lg); + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + } + .card h3 { color: var(--text-primary); } + .card p { color: var(--text-secondary); } + .card button { + background: var(--accent); + color: var(--accent-text); + border: none; + padding: 0.5rem 1rem; + border-radius: var(--radius-sm); + cursor: pointer; + } + .card button:hover { background: var(--accent-hover); } + `, + props: ["title"], + imports: ["ref"], + componentProps: { title: "Themed Card" } +} +``` + +## API Endpoints (Themes) + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/themes` | GET | List all themes | +| `/api/themes` | POST | Create/update theme | +| `/api/themes/active` | GET | Get active (default) theme | +| `/api/themes/:id` | GET | Get theme by ID | +| `/api/themes/:id` | DELETE | Delete theme | +| `/api/themes/:id/default` | POST | Set as default theme | +| `/api/themes/export/:id` | GET | Export theme as JSON | +| `/api/design-tokens` | GET | Get design tokens guide | + +## Theme Editor Features + +### Desktop View + +- **Sidebar**: List of system and custom themes +- **Editor**: Category tabs (Colors, Text, Accent, etc.) +- **Variables Grid**: Color pickers and input fields +- **Live Preview**: Buttons, cards, badges, inputs + +### Mobile View + +- **Compact Header**: Theme dropdown + action buttons +- **Collapsible Variables**: Toggle to show/hide editors +- **Responsive Preview**: Adapts to screen size + +### Actions + +- **Save**: Saves current changes (creates copy if editing system theme) +- **Reset**: Reverts unsaved changes +- **Export**: Downloads theme as JSON file +- **Clone**: Creates a copy of any theme +- **Set Default**: Makes theme load on startup + +## File Structure + +``` +frontend/src/ +├── stores/theme.ts # Pinia store +├── services/themeService.ts # API client + utilities +├── pages/ThemesPage.vue # Main editor page +└── components/themes/ + ├── ColorPicker.vue # HSL color picker + ├── VariableEditor.vue # Variable input (color/size/text) + ├── ThemePreview.vue # Live preview component + └── ThemeListItem.vue # Theme list item with actions +``` + +--- + ## Tech Stack - **Frontend**: Vue 3, Vite, Pinia, TypeScript diff --git a/frontend/src/components/themes/ColorPicker.vue b/frontend/src/components/themes/ColorPicker.vue index faf08f2..2cfa7ce 100644 --- a/frontend/src/components/themes/ColorPicker.vue +++ b/frontend/src/components/themes/ColorPicker.vue @@ -277,4 +277,41 @@ input[type="range"]::-webkit-slider-thumb { border-radius: 6px; border: 1px solid rgba(255, 255, 255, 0.1); } + +/* Mobile responsive */ +@media (max-width: 600px) { + .color-preview { + padding: 0.25rem 0.375rem; + } + + .color-swatch { + width: 20px; + height: 20px; + } + + .color-value { + font-size: 0.7rem; + } + + .color-dropdown { + width: 220px; + padding: 0.75rem; + left: 50%; + transform: translateX(-50%); + } + + .slider-group label { + width: 55px; + font-size: 0.7rem; + } + + .slider-group span { + width: 35px; + font-size: 0.65rem; + } + + .large-preview { + height: 32px; + } +} diff --git a/frontend/src/components/themes/ThemePreview.vue b/frontend/src/components/themes/ThemePreview.vue index 0988aa6..f487c6b 100644 --- a/frontend/src/components/themes/ThemePreview.vue +++ b/frontend/src/components/themes/ThemePreview.vue @@ -249,4 +249,69 @@ defineProps<{ background: var(--info-bg); color: var(--info); } + +/* Mobile responsive */ +@media (max-width: 768px) { + .theme-preview { + padding: 0.75rem; + } + + .preview-section { + margin-bottom: 1rem; + } + + .color-swatches { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.5rem; + } + + .swatch { + height: 40px; + } + + .swatch span { + font-size: 0.6rem; + } + + .component-samples { + flex-direction: column; + } + + .sample-btn { + width: 100%; + text-align: center; + } + + .sample-card { + padding: 0.75rem; + } + + .sample-card h5 { + font-size: 0.875rem; + } + + .sample-card p { + font-size: 0.8rem; + } + + .status-badges { + gap: 0.375rem; + } + + .badge { + padding: 0.2rem 0.5rem; + font-size: 0.7rem; + } +} + +@media (max-width: 480px) { + .color-swatches { + grid-template-columns: 1fr; + } + + .swatch { + height: 36px; + } +} diff --git a/frontend/src/components/themes/VariableEditor.vue b/frontend/src/components/themes/VariableEditor.vue index 90fd940..6c88a58 100644 --- a/frontend/src/components/themes/VariableEditor.vue +++ b/frontend/src/components/themes/VariableEditor.vue @@ -169,4 +169,35 @@ function handleInputChange(e: Event) { outline: none; border-color: var(--accent); } + +/* Mobile responsive */ +@media (max-width: 600px) { + .variable-editor { + padding: 0.75rem; + } + + .variable-header { + margin-bottom: 0.5rem; + } + + .variable-name { + font-size: 0.8rem; + } + + .variable-key { + font-size: 0.65rem; + } + + .size-input input, + .text-input { + padding: 0.4rem; + font-size: 0.8rem; + } + + .size-preview, + .radius-preview { + width: 24px; + height: 24px; + } +} diff --git a/frontend/src/pages/ThemesPage.vue b/frontend/src/pages/ThemesPage.vue index 5454421..857430a 100644 --- a/frontend/src/pages/ThemesPage.vue +++ b/frontend/src/pages/ThemesPage.vue @@ -10,6 +10,7 @@ const store = useThemeStore() const activeCategory = ref('colors') const variablesCollapsed = ref(false) +const mobileDropdownOpen = ref(false) const newThemeName = ref('') const showNewThemeModal = ref(false) const showCloneModal = ref(false) @@ -141,12 +142,58 @@ onMounted(() => {