feat: Add theme system with visual editor

- Backend: themes table and API endpoints (CRUD, export, design-tokens)
- Theme store with preview, apply, and persistence
- ThemesPage with collapsible variables editor and live preview
- Components: ColorPicker (HSL), VariableEditor, ThemePreview, ThemeListItem
- Integration: $theme helper for dynamic components, get_design_tokens MCP tool
- Navigation: /themes route with palette icon in toolbar

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 05:10:18 -06:00
parent d1c0f62fc3
commit b880038b07
19 changed files with 3358 additions and 11 deletions

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import { RouterLink, useRoute } from 'vue-router'
import { useCanvasStore } from '../stores/canvas'
const route = useRoute()
const canvasStore = useCanvasStore()
function clearCanvas() {
@@ -8,13 +10,13 @@ function clearCanvas() {
if (container) {
container.innerHTML = `
<div class="canvas-placeholder">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<path d="M3 9h18"/>
<path d="M9 21V9"/>
</svg>
<p>Canvas listo</p>
<span>Claude Code puede renderizar contenido aquí usando las herramientas MCP</span>
<span>Claude Code puede renderizar contenido aquí</span>
</div>
`
}
@@ -27,9 +29,42 @@ function toggleHistory() {
<template>
<aside class="toolbar">
<!-- Navegación -->
<div class="toolbar-section nav-section">
<RouterLink to="/" class="toolbar-btn" :class="{ active: route.path === '/' }" title="Canvas">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<path d="M3 9h18"/>
<path d="M9 21V9"/>
</svg>
</RouterLink>
<RouterLink to="/components" class="toolbar-btn" :class="{ active: route.path === '/components' }" title="Componentes">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.29 7 12 12 20.71 7"/>
<line x1="12" y1="22" x2="12" y2="12"/>
</svg>
</RouterLink>
<RouterLink to="/themes" class="toolbar-btn" :class="{ active: route.path === '/themes' }" title="Temas">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="13.5" cy="6.5" r="2.5"/>
<circle cx="19" cy="11.5" r="2.5"/>
<circle cx="17" cy="18.5" r="2.5"/>
<circle cx="8.5" cy="17.5" r="2.5"/>
<circle cx="5" cy="10.5" r="2.5"/>
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.93 0 1.82-.13 2.67-.36"/>
</svg>
</RouterLink>
</div>
<div class="toolbar-divider"></div>
<!-- Acciones -->
<div class="toolbar-section">
<button class="toolbar-btn" @click="clearCanvas" title="Limpiar canvas">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18"/>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
@@ -37,7 +72,7 @@ function toggleHistory() {
</button>
<button class="toolbar-btn" @click="toggleHistory" title="Historial">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 8v4l3 3"/>
<circle cx="12" cy="12" r="10"/>
</svg>
@@ -54,6 +89,7 @@ function toggleHistory() {
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.toolbar-section {
@@ -62,6 +98,12 @@ function toggleHistory() {
gap: 0.5rem;
}
.toolbar-divider {
height: 1px;
background: var(--border-color);
margin: 0.25rem 0;
}
.toolbar-btn {
width: 40px;
height: 40px;
@@ -74,6 +116,7 @@ function toggleHistory() {
color: var(--text-secondary);
cursor: pointer;
transition: all 0.15s ease;
text-decoration: none;
}
.toolbar-btn:hover {
@@ -84,4 +127,9 @@ function toggleHistory() {
.toolbar-btn:active {
transform: scale(0.95);
}
.toolbar-btn.active {
background: rgba(99, 102, 241, 0.15);
color: #6366f1;
}
</style>

View File

@@ -0,0 +1,280 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { hexToHSL, hslToHex } from '../../services/themeService'
const props = defineProps<{
modelValue: string
label?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const isOpen = ref(false)
const hexInput = ref(props.modelValue)
const hsl = computed(() => {
try {
return hexToHSL(props.modelValue)
} catch {
return { h: 0, s: 50, l: 50 }
}
})
const hue = ref(hsl.value.h)
const saturation = ref(hsl.value.s)
const lightness = ref(hsl.value.l)
watch(() => props.modelValue, (newVal) => {
hexInput.value = newVal
const newHsl = hexToHSL(newVal)
hue.value = newHsl.h
saturation.value = newHsl.s
lightness.value = newHsl.l
})
function updateFromHSL() {
const hex = hslToHex(hue.value, saturation.value, lightness.value)
hexInput.value = hex
emit('update:modelValue', hex)
}
function updateFromHex() {
if (/^#[0-9A-Fa-f]{6}$/.test(hexInput.value)) {
emit('update:modelValue', hexInput.value)
const newHsl = hexToHSL(hexInput.value)
hue.value = newHsl.h
saturation.value = newHsl.s
lightness.value = newHsl.l
}
}
// Close picker on outside click
function handleClickOutside(e: MouseEvent) {
const target = e.target as HTMLElement
if (!target.closest('.color-picker')) {
isOpen.value = false
}
}
// Register click outside listener
watch(isOpen, (open) => {
if (open) {
setTimeout(() => document.addEventListener('click', handleClickOutside), 0)
} else {
document.removeEventListener('click', handleClickOutside)
}
})
</script>
<template>
<div class="color-picker" @click.stop>
<div class="color-preview" @click="isOpen = !isOpen">
<div class="color-swatch" :style="{ background: modelValue }"></div>
<span class="color-value">{{ modelValue }}</span>
</div>
<div v-if="isOpen" class="color-dropdown" @click.stop>
<div class="color-sliders">
<div class="slider-group">
<label>Hue</label>
<input
type="range"
v-model.number="hue"
min="0"
max="360"
class="hue-slider"
@input="updateFromHSL"
/>
<span>{{ hue }}°</span>
</div>
<div class="slider-group">
<label>Saturation</label>
<input
type="range"
v-model.number="saturation"
min="0"
max="100"
class="saturation-slider"
:style="{
'--sat-left': hslToHex(hue, 0, lightness),
'--sat-right': hslToHex(hue, 100, lightness)
}"
@input="updateFromHSL"
/>
<span>{{ saturation }}%</span>
</div>
<div class="slider-group">
<label>Lightness</label>
<input
type="range"
v-model.number="lightness"
min="0"
max="100"
class="lightness-slider"
:style="{
'--light-mid': hslToHex(hue, saturation, 50)
}"
@input="updateFromHSL"
/>
<span>{{ lightness }}%</span>
</div>
</div>
<div class="hex-input-group">
<label>Hex</label>
<input
type="text"
v-model="hexInput"
@blur="updateFromHex"
@keyup.enter="updateFromHex"
maxlength="7"
/>
</div>
<div class="large-preview" :style="{ background: modelValue }"></div>
</div>
</div>
</template>
<style scoped>
.color-picker {
position: relative;
}
.color-preview {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.5rem;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
cursor: pointer;
transition: border-color 0.15s;
}
.color-preview:hover {
border-color: var(--accent);
}
.color-swatch {
width: 24px;
height: 24px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.color-value {
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--text-secondary);
text-transform: uppercase;
}
.color-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
width: 240px;
padding: 1rem;
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;
}
.color-sliders {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1rem;
}
.slider-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.slider-group label {
width: 70px;
font-size: 0.75rem;
color: var(--text-secondary);
}
.slider-group input[type="range"] {
flex: 1;
height: 8px;
border-radius: 4px;
appearance: none;
cursor: pointer;
}
.slider-group span {
width: 40px;
font-size: 0.7rem;
color: var(--text-muted);
text-align: right;
}
.hue-slider {
background: linear-gradient(
to right,
#ff0000, #ffff00, #00ff00, #00ffff, #0000ff, #ff00ff, #ff0000
);
}
.saturation-slider {
background: linear-gradient(to right, var(--sat-left), var(--sat-right));
}
.lightness-slider {
background: linear-gradient(to right, #000000, var(--light-mid), #ffffff);
}
input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 14px;
height: 14px;
background: white;
border-radius: 50%;
border: 2px solid var(--bg-primary);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
cursor: pointer;
}
.hex-input-group {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.hex-input-group label {
font-size: 0.75rem;
color: var(--text-secondary);
}
.hex-input-group input {
flex: 1;
padding: 0.375rem 0.5rem;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 0.8rem;
text-transform: uppercase;
}
.large-preview {
height: 40px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
</style>

View File

@@ -0,0 +1,168 @@
<script setup lang="ts">
import type { Theme } from '../../stores/theme'
const props = defineProps<{
theme: Theme
active: boolean
}>()
const emit = defineEmits<{
select: [theme: Theme]
delete: [id: string]
clone: [id: string]
setDefault: [id: string]
}>()
function getAccentColor(): string {
return props.theme.variables?.accent?.accent || '#6366f1'
}
</script>
<template>
<div
class="theme-item"
:class="{ active, 'is-default': theme.is_default }"
@click="emit('select', theme)"
>
<div class="theme-color" :style="{ background: getAccentColor() }"></div>
<div class="theme-info">
<span class="theme-name">
{{ theme.name }}
<span v-if="theme.is_default" class="default-badge">Default</span>
</span>
<span class="theme-meta">
{{ theme.is_system ? 'System' : 'Custom' }}
</span>
</div>
<div class="theme-actions" @click.stop>
<button
v-if="!theme.is_default"
class="action-btn"
@click="emit('setDefault', theme.id)"
title="Set as default"
>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
</button>
<button
class="action-btn"
@click="emit('clone', theme.id)"
title="Clone theme"
>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
</button>
<button
v-if="!theme.is_system"
class="action-btn delete"
@click="emit('delete', theme.id)"
title="Delete theme"
>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/>
</svg>
</button>
</div>
</div>
</template>
<style scoped>
.theme-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.625rem 0.75rem;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
cursor: pointer;
transition: all 0.15s;
}
.theme-item:hover {
border-color: var(--border-hover);
background: var(--bg-hover);
}
.theme-item.active {
border-color: var(--accent);
background: var(--accent-muted);
}
.theme-color {
width: 32px;
height: 32px;
border-radius: 6px;
flex-shrink: 0;
}
.theme-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.theme-name {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-primary);
}
.default-badge {
font-size: 0.65rem;
padding: 0.125rem 0.375rem;
background: var(--accent);
color: white;
border-radius: 9999px;
font-weight: 600;
}
.theme-meta {
font-size: 0.7rem;
color: var(--text-muted);
}
.theme-actions {
display: flex;
gap: 0.25rem;
opacity: 0;
transition: opacity 0.15s;
}
.theme-item:hover .theme-actions {
opacity: 1;
}
.action-btn {
padding: 0.375rem;
background: transparent;
border: none;
border-radius: 4px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.15s;
}
.action-btn:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.action-btn.delete:hover {
background: var(--error-bg);
color: var(--error);
}
</style>

View File

@@ -0,0 +1,252 @@
<script setup lang="ts">
import type { ThemeVariables } from '../../stores/theme'
defineProps<{
variables: ThemeVariables | null
}>()
</script>
<template>
<div class="theme-preview">
<div class="preview-section">
<h4>Colors</h4>
<div class="color-swatches">
<div class="swatch" style="background: var(--bg-primary)">
<span>Primary</span>
</div>
<div class="swatch" style="background: var(--bg-secondary)">
<span>Secondary</span>
</div>
<div class="swatch" style="background: var(--bg-hover)">
<span>Hover</span>
</div>
<div class="swatch" style="background: var(--bg-tertiary)">
<span>Tertiary</span>
</div>
</div>
</div>
<div class="preview-section">
<h4>Text</h4>
<div class="text-samples">
<p style="color: var(--text-primary)">Primary Text</p>
<p style="color: var(--text-secondary)">Secondary Text</p>
<p style="color: var(--text-muted)">Muted Text</p>
</div>
</div>
<div class="preview-section">
<h4>Components</h4>
<div class="component-samples">
<button class="sample-btn primary">Primary Button</button>
<button class="sample-btn secondary">Secondary</button>
<button class="sample-btn danger">Danger</button>
</div>
<div class="sample-card">
<h5>Sample Card</h5>
<p>This is a preview of how cards will look with the current theme.</p>
<div class="card-actions">
<button class="sample-btn primary small">Action</button>
</div>
</div>
<div class="sample-input-group">
<input type="text" placeholder="Sample input..." class="sample-input" />
</div>
</div>
<div class="preview-section">
<h4>Status</h4>
<div class="status-badges">
<span class="badge success">Success</span>
<span class="badge warning">Warning</span>
<span class="badge error">Error</span>
<span class="badge info">Info</span>
</div>
</div>
</div>
</template>
<style scoped>
.theme-preview {
padding: 1rem;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 12px;
}
.preview-section {
margin-bottom: 1.5rem;
}
.preview-section:last-child {
margin-bottom: 0;
}
.preview-section h4 {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
margin: 0 0 0.75rem 0;
}
.color-swatches {
display: flex;
gap: 0.5rem;
}
.swatch {
flex: 1;
height: 48px;
border-radius: 8px;
border: 1px solid var(--border-color);
display: flex;
align-items: flex-end;
padding: 0.25rem 0.5rem;
}
.swatch span {
font-size: 0.65rem;
color: var(--text-secondary);
mix-blend-mode: difference;
}
.text-samples {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.text-samples p {
margin: 0;
font-size: 0.875rem;
}
.component-samples {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.sample-btn {
padding: 0.5rem 1rem;
border: none;
border-radius: var(--radius-md);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: var(--transition-fast);
}
.sample-btn.primary {
background: var(--accent);
color: var(--accent-text);
}
.sample-btn.primary:hover {
background: var(--accent-hover);
}
.sample-btn.secondary {
background: var(--bg-hover);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.sample-btn.danger {
background: var(--error-bg);
color: var(--error);
border: 1px solid var(--error);
}
.sample-btn.small {
padding: 0.375rem 0.75rem;
font-size: 0.8rem;
}
.sample-card {
padding: 1rem;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
margin-bottom: 1rem;
}
.sample-card h5 {
margin: 0 0 0.5rem 0;
font-size: 0.95rem;
color: var(--text-primary);
}
.sample-card p {
margin: 0 0 0.75rem 0;
font-size: 0.85rem;
color: var(--text-secondary);
}
.card-actions {
display: flex;
justify-content: flex-end;
}
.sample-input-group {
margin-top: 0.75rem;
}
.sample-input {
width: 100%;
padding: 0.625rem 0.875rem;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: 0.875rem;
transition: var(--transition-fast);
}
.sample-input:focus {
outline: none;
border-color: var(--accent);
}
.sample-input::placeholder {
color: var(--text-muted);
}
.status-badges {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.badge {
padding: 0.25rem 0.625rem;
border-radius: var(--radius-full);
font-size: 0.75rem;
font-weight: 500;
}
.badge.success {
background: var(--success-bg);
color: var(--success);
}
.badge.warning {
background: var(--warning-bg);
color: var(--warning);
}
.badge.error {
background: var(--error-bg);
color: var(--error);
}
.badge.info {
background: var(--info-bg);
color: var(--info);
}
</style>

View File

@@ -0,0 +1,172 @@
<script setup lang="ts">
import { computed } from 'vue'
import ColorPicker from './ColorPicker.vue'
import { isColorVariable, isSizeVariable, isShadowVariable, isTransitionVariable, isFontVariable } from '../../services/themeService'
const props = defineProps<{
name: string
value: string
}>()
const emit = defineEmits<{
update: [value: string]
}>()
const variableType = computed(() => {
if (isColorVariable(props.name)) return 'color'
if (isSizeVariable(props.name)) return 'size'
if (isShadowVariable(props.name)) return 'shadow'
if (isTransitionVariable(props.name)) return 'transition'
if (isFontVariable(props.name)) return 'font'
return 'text'
})
const isRadiusVariable = computed(() => props.name.toLowerCase().includes('radius'))
const displayName = computed(() => {
return props.name
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
})
function handleColorChange(newValue: string) {
emit('update', newValue)
}
function handleInputChange(e: Event) {
emit('update', (e.target as HTMLInputElement).value)
}
</script>
<template>
<div class="variable-editor" :class="variableType">
<div class="variable-header">
<span class="variable-name">{{ displayName }}</span>
<code class="variable-key">--{{ name }}</code>
</div>
<div class="variable-input">
<!-- Color input -->
<ColorPicker
v-if="variableType === 'color'"
:modelValue="value"
@update:modelValue="handleColorChange"
/>
<!-- Size input -->
<div v-else-if="variableType === 'size'" class="size-input">
<input
type="text"
:value="value"
@input="handleInputChange"
placeholder="8px"
/>
<div
v-if="isRadiusVariable"
class="radius-preview"
:style="{ borderRadius: value }"
></div>
<div
v-else
class="size-preview"
:style="{ width: value, height: value }"
></div>
</div>
<!-- Text/Other input -->
<input
v-else
type="text"
:value="value"
@input="handleInputChange"
class="text-input"
/>
</div>
</div>
</template>
<style scoped>
.variable-editor {
padding: 0.875rem;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
transition: border-color 0.15s;
}
.variable-editor:hover {
border-color: var(--border-hover);
}
.variable-header {
display: flex;
flex-direction: column;
gap: 0.25rem;
margin-bottom: 0.75rem;
}
.variable-name {
font-size: 0.875rem;
font-weight: 500;
color: var(--text-primary);
}
.variable-key {
font-size: 0.7rem;
color: var(--text-muted);
font-family: var(--font-mono);
}
.variable-input {
width: 100%;
}
.size-input {
display: flex;
align-items: center;
gap: 0.5rem;
}
.size-input input {
flex: 1;
min-width: 0;
padding: 0.5rem;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 0.875rem;
}
.size-preview {
width: 28px;
height: 28px;
background: var(--accent);
border-radius: 4px;
flex-shrink: 0;
}
.radius-preview {
width: 28px;
height: 28px;
background: var(--accent);
flex-shrink: 0;
}
.text-input {
width: 100%;
padding: 0.5rem;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--text-primary);
font-size: 0.875rem;
}
.text-input:focus {
outline: none;
border-color: var(--accent);
}
</style>