feat: Add mobile responsive theme editor and documentation
- ThemesPage: Custom dropdown with theme cards for mobile - ThemesPage: Collapsible variables section - ThemePreview: Responsive grid and compact layout - VariableEditor: Separate radius preview with border-radius - ColorPicker: Mobile-friendly dropdown positioning - README: Complete theme system documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
224
README.md
224
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: `
|
||||||
|
<div class="card">
|
||||||
|
<h3>{{ title }}</h3>
|
||||||
|
<p>Current accent: {{ accentColor }}</p>
|
||||||
|
<button @click="randomizeAccent">Randomize Accent</button>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
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
|
## Tech Stack
|
||||||
|
|
||||||
- **Frontend**: Vue 3, Vite, Pinia, TypeScript
|
- **Frontend**: Vue 3, Vite, Pinia, TypeScript
|
||||||
|
|||||||
@@ -277,4 +277,41 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -249,4 +249,69 @@ defineProps<{
|
|||||||
background: var(--info-bg);
|
background: var(--info-bg);
|
||||||
color: var(--info);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -169,4 +169,35 @@ function handleInputChange(e: Event) {
|
|||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const store = useThemeStore()
|
|||||||
|
|
||||||
const activeCategory = ref<ThemeCategory>('colors')
|
const activeCategory = ref<ThemeCategory>('colors')
|
||||||
const variablesCollapsed = ref(false)
|
const variablesCollapsed = ref(false)
|
||||||
|
const mobileDropdownOpen = ref(false)
|
||||||
const newThemeName = ref('')
|
const newThemeName = ref('')
|
||||||
const showNewThemeModal = ref(false)
|
const showNewThemeModal = ref(false)
|
||||||
const showCloneModal = ref(false)
|
const showCloneModal = ref(false)
|
||||||
@@ -141,12 +142,58 @@ onMounted(() => {
|
|||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<h2>Themes</h2>
|
<div class="sidebar-title">
|
||||||
<button class="btn-icon" @click="createNewTheme" title="New theme">
|
<h2>Themes</h2>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<button class="btn-icon desktop-only" @click="createNewTheme" title="New theme">
|
||||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
</svg>
|
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||||
</button>
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Mobile: theme dropdown + actions -->
|
||||||
|
<div class="mobile-editor-controls">
|
||||||
|
<div class="mobile-theme-dropdown">
|
||||||
|
<button class="mobile-dropdown-trigger" @click="mobileDropdownOpen = !mobileDropdownOpen">
|
||||||
|
<div class="dropdown-color" :style="{ background: store.activeTheme?.variables?.accent?.accent || '#6366f1' }"></div>
|
||||||
|
<span>{{ editingThemeName }}</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" :class="{ rotated: mobileDropdownOpen }">
|
||||||
|
<polyline points="6 9 12 15 18 9"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div v-if="mobileDropdownOpen" class="mobile-dropdown-menu" @click.stop>
|
||||||
|
<div class="dropdown-section">
|
||||||
|
<span class="dropdown-label">System</span>
|
||||||
|
<ThemeListItem
|
||||||
|
v-for="theme in store.systemThemes"
|
||||||
|
:key="theme.id"
|
||||||
|
:theme="theme"
|
||||||
|
:active="store.activeTheme?.id === theme.id"
|
||||||
|
@select="(t) => { handleSelectTheme(t); mobileDropdownOpen = false }"
|
||||||
|
@clone="handleCloneTheme"
|
||||||
|
@setDefault="handleSetDefault"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="store.userThemes.length > 0" class="dropdown-section">
|
||||||
|
<span class="dropdown-label">Custom</span>
|
||||||
|
<ThemeListItem
|
||||||
|
v-for="theme in store.userThemes"
|
||||||
|
:key="theme.id"
|
||||||
|
:theme="theme"
|
||||||
|
:active="store.activeTheme?.id === theme.id"
|
||||||
|
@select="(t) => { handleSelectTheme(t); mobileDropdownOpen = false }"
|
||||||
|
@delete="handleDeleteTheme"
|
||||||
|
@clone="handleCloneTheme"
|
||||||
|
@setDefault="handleSetDefault"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mobile-actions">
|
||||||
|
<button class="btn-sm" @click="handleReset" :disabled="!store.hasUnsavedChanges">Reset</button>
|
||||||
|
<button class="btn-sm primary" @click="handleSave" :disabled="!store.hasUnsavedChanges">Save</button>
|
||||||
|
<button class="btn-sm" @click="handleExport">Export</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="theme-groups">
|
<div class="theme-groups">
|
||||||
@@ -345,6 +392,13 @@ onMounted(() => {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 1rem 1.25rem;
|
padding: 1rem 1.25rem;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-header h2 {
|
.sidebar-header h2 {
|
||||||
@@ -353,6 +407,118 @@ onMounted(() => {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-editor-controls {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-theme-dropdown {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-dropdown-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-dropdown-trigger:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-dropdown-trigger .dropdown-color {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-dropdown-trigger svg {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-dropdown-trigger svg.rotated {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
left: 0;
|
||||||
|
min-width: 240px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 100;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-section {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-section :deep(.theme-item) {
|
||||||
|
margin-bottom: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-only {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm.primary {
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-icon {
|
.btn-icon {
|
||||||
padding: 0.375rem;
|
padding: 0.375rem;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -659,4 +825,119 @@ onMounted(() => {
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.themes-page {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 100%;
|
||||||
|
max-height: none;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-editor-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-groups {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tabs {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
gap: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tabs button {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variables-content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variables-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-section {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.sidebar-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title h2 {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-editor-controls {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-theme-select {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variables-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
margin: 1rem;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user