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
1028 lines
24 KiB
Vue
1028 lines
24 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useThemeStore, type ThemeVariables } from '../stores/theme'
|
|
import { THEME_CATEGORIES, downloadTheme, readThemeFile, type ThemeCategory } from '../services/themeService'
|
|
import ThemeListItem from '../components/themes/ThemeListItem.vue'
|
|
import VariableEditor from '../components/themes/VariableEditor.vue'
|
|
import ThemePreview from '../components/themes/ThemePreview.vue'
|
|
|
|
const store = useThemeStore()
|
|
|
|
const activeCategory = ref<ThemeCategory>('colors')
|
|
const variablesCollapsed = ref(false)
|
|
const mobileDropdownOpen = ref(false)
|
|
const newThemeName = ref('')
|
|
const showNewThemeModal = ref(false)
|
|
const showCloneModal = ref(false)
|
|
const cloneSourceId = ref<string | null>(null)
|
|
const cloneName = ref('')
|
|
const showEditModal = ref(false)
|
|
const editingTheme = ref<any>(null)
|
|
const editName = ref('')
|
|
const editDescription = ref('')
|
|
const fileInput = ref<HTMLInputElement | null>(null)
|
|
|
|
const currentCategoryVariables = computed(() => {
|
|
const vars = store.currentVariables
|
|
if (!vars) return []
|
|
const categoryVars = vars[activeCategory.value as keyof ThemeVariables]
|
|
if (!categoryVars) return []
|
|
return Object.entries(categoryVars)
|
|
})
|
|
|
|
const editingThemeName = computed(() => {
|
|
return store.activeTheme?.name || 'No theme selected'
|
|
})
|
|
|
|
function handleSelectTheme(theme: any) {
|
|
store.selectTheme(theme)
|
|
}
|
|
|
|
function handleDeleteTheme(id: string) {
|
|
if (confirm('Delete this theme?')) {
|
|
store.deleteTheme(id)
|
|
}
|
|
}
|
|
|
|
function handleCloneTheme(id: string) {
|
|
const theme = store.themes.find(t => t.id === id)
|
|
if (theme) {
|
|
cloneSourceId.value = id
|
|
cloneName.value = `${theme.name} Copy`
|
|
showCloneModal.value = true
|
|
}
|
|
}
|
|
|
|
async function confirmClone() {
|
|
if (cloneSourceId.value && cloneName.value.trim()) {
|
|
await store.cloneTheme(cloneSourceId.value, cloneName.value.trim())
|
|
showCloneModal.value = false
|
|
cloneSourceId.value = null
|
|
cloneName.value = ''
|
|
}
|
|
}
|
|
|
|
function handleEditTheme(theme: any) {
|
|
editingTheme.value = theme
|
|
editName.value = theme.name
|
|
editDescription.value = theme.description || ''
|
|
showEditModal.value = true
|
|
}
|
|
|
|
async function confirmEdit() {
|
|
if (editingTheme.value && editName.value.trim()) {
|
|
await store.updateTheme(editingTheme.value.id, {
|
|
name: editName.value.trim(),
|
|
description: editDescription.value.trim() || undefined
|
|
})
|
|
showEditModal.value = false
|
|
editingTheme.value = null
|
|
editName.value = ''
|
|
editDescription.value = ''
|
|
}
|
|
}
|
|
|
|
function handleSetDefault(id: string) {
|
|
store.setDefaultTheme(id)
|
|
}
|
|
|
|
function handleUpdateVariable(key: string, value: string) {
|
|
store.updateVariable(activeCategory.value as keyof ThemeVariables, key, value)
|
|
}
|
|
|
|
async function handleSave() {
|
|
if (store.activeTheme?.is_system) {
|
|
newThemeName.value = `${store.activeTheme.name} Custom`
|
|
showNewThemeModal.value = true
|
|
} else {
|
|
await store.commitPreview()
|
|
}
|
|
}
|
|
|
|
async function handleSaveAsNew() {
|
|
if (newThemeName.value.trim()) {
|
|
await store.commitPreview(newThemeName.value.trim())
|
|
showNewThemeModal.value = false
|
|
newThemeName.value = ''
|
|
}
|
|
}
|
|
|
|
function handleReset() {
|
|
store.resetPreview()
|
|
}
|
|
|
|
function handleExport() {
|
|
if (store.activeTheme) {
|
|
downloadTheme(store.activeTheme)
|
|
}
|
|
}
|
|
|
|
function handleImportClick() {
|
|
fileInput.value?.click()
|
|
}
|
|
|
|
async function handleFileChange(e: Event) {
|
|
const input = e.target as HTMLInputElement
|
|
const file = input.files?.[0]
|
|
if (file) {
|
|
try {
|
|
const themeData = await readThemeFile(file)
|
|
await store.saveTheme({
|
|
name: themeData.name || 'Imported Theme',
|
|
description: themeData.description,
|
|
variables: themeData.variables!,
|
|
metadata: themeData.metadata
|
|
})
|
|
} catch (err) {
|
|
alert('Failed to import theme: ' + (err as Error).message)
|
|
}
|
|
}
|
|
input.value = ''
|
|
}
|
|
|
|
function createNewTheme() {
|
|
newThemeName.value = 'New Theme'
|
|
showNewThemeModal.value = true
|
|
}
|
|
|
|
async function confirmCreateTheme() {
|
|
if (newThemeName.value.trim() && store.activeTheme) {
|
|
await store.saveTheme({
|
|
name: newThemeName.value.trim(),
|
|
variables: JSON.parse(JSON.stringify(store.activeTheme.variables))
|
|
})
|
|
showNewThemeModal.value = false
|
|
newThemeName.value = ''
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
store.fetchThemes()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="themes-page">
|
|
<!-- Sidebar -->
|
|
<aside class="sidebar">
|
|
<div class="sidebar-header">
|
|
<div class="sidebar-title">
|
|
<h2>Themes</h2>
|
|
<button class="btn-icon desktop-only" @click="createNewTheme" title="New theme">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
|
</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"
|
|
@edit="handleEditTheme"
|
|
/>
|
|
</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"
|
|
@edit="handleEditTheme"
|
|
/>
|
|
</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 class="theme-groups">
|
|
<section class="theme-group">
|
|
<h3>System</h3>
|
|
<ThemeListItem
|
|
v-for="theme in store.systemThemes"
|
|
:key="theme.id"
|
|
:theme="theme"
|
|
:active="store.activeTheme?.id === theme.id"
|
|
@select="handleSelectTheme"
|
|
@delete="handleDeleteTheme"
|
|
@clone="handleCloneTheme"
|
|
@setDefault="handleSetDefault"
|
|
@edit="handleEditTheme"
|
|
/>
|
|
</section>
|
|
|
|
<section v-if="store.userThemes.length > 0" class="theme-group">
|
|
<h3>Custom</h3>
|
|
<ThemeListItem
|
|
v-for="theme in store.userThemes"
|
|
:key="theme.id"
|
|
:theme="theme"
|
|
:active="store.activeTheme?.id === theme.id"
|
|
@select="handleSelectTheme"
|
|
@delete="handleDeleteTheme"
|
|
@clone="handleCloneTheme"
|
|
@setDefault="handleSetDefault"
|
|
@edit="handleEditTheme"
|
|
/>
|
|
</section>
|
|
</div>
|
|
|
|
<div class="sidebar-footer">
|
|
<button class="btn-secondary" @click="handleImportClick">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
<polyline points="17 8 12 3 7 8"/>
|
|
<line x1="12" y1="3" x2="12" y2="15"/>
|
|
</svg>
|
|
Import
|
|
</button>
|
|
<input
|
|
ref="fileInput"
|
|
type="file"
|
|
accept=".json"
|
|
@change="handleFileChange"
|
|
hidden
|
|
/>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Editor -->
|
|
<main class="editor">
|
|
<header class="editor-header">
|
|
<div class="editor-title">
|
|
<h2>{{ editingThemeName }}</h2>
|
|
<span v-if="store.hasUnsavedChanges" class="unsaved-badge">Unsaved changes</span>
|
|
</div>
|
|
<div class="editor-actions">
|
|
<button
|
|
class="btn-secondary"
|
|
@click="handleReset"
|
|
:disabled="!store.hasUnsavedChanges"
|
|
>
|
|
Reset
|
|
</button>
|
|
<button
|
|
class="btn-primary"
|
|
@click="handleSave"
|
|
:disabled="!store.hasUnsavedChanges"
|
|
>
|
|
Save
|
|
</button>
|
|
<button class="btn-secondary" @click="handleExport">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
<polyline points="7 10 12 15 17 10"/>
|
|
<line x1="12" y1="15" x2="12" y2="3"/>
|
|
</svg>
|
|
Export
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Category Tabs -->
|
|
<nav class="category-tabs">
|
|
<button
|
|
v-for="cat in THEME_CATEGORIES"
|
|
:key="cat.key"
|
|
:class="{ active: activeCategory === cat.key }"
|
|
@click="activeCategory = cat.key"
|
|
>
|
|
{{ cat.label }}
|
|
</button>
|
|
</nav>
|
|
|
|
<!-- Variables Grid -->
|
|
<section class="variables-section" :class="{ collapsed: variablesCollapsed }">
|
|
<div class="variables-header" @click="variablesCollapsed = !variablesCollapsed">
|
|
<span>Variables</span>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
class="collapse-icon"
|
|
:class="{ rotated: variablesCollapsed }"
|
|
>
|
|
<polyline points="6 9 12 15 18 9"/>
|
|
</svg>
|
|
</div>
|
|
<div class="variables-content">
|
|
<div class="variables-grid">
|
|
<VariableEditor
|
|
v-for="[key, value] in currentCategoryVariables"
|
|
:key="key"
|
|
:name="key"
|
|
:value="value"
|
|
@update="handleUpdateVariable(key, $event)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Preview -->
|
|
<section class="preview-section">
|
|
<h3>Preview</h3>
|
|
<ThemePreview :variables="store.currentVariables" />
|
|
</section>
|
|
</main>
|
|
|
|
<!-- New Theme Modal -->
|
|
<div v-if="showNewThemeModal" class="modal-overlay" @click="showNewThemeModal = false">
|
|
<div class="modal" @click.stop>
|
|
<h3>{{ store.activeTheme?.is_system ? 'Save as New Theme' : 'Create Theme' }}</h3>
|
|
<input
|
|
v-model="newThemeName"
|
|
type="text"
|
|
placeholder="Theme name"
|
|
@keyup.enter="store.activeTheme?.is_system ? handleSaveAsNew() : confirmCreateTheme()"
|
|
/>
|
|
<div class="modal-actions">
|
|
<button class="btn-secondary" @click="showNewThemeModal = false">Cancel</button>
|
|
<button
|
|
class="btn-primary"
|
|
@click="store.activeTheme?.is_system ? handleSaveAsNew() : confirmCreateTheme()"
|
|
>
|
|
Create
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Clone Modal -->
|
|
<div v-if="showCloneModal" class="modal-overlay" @click="showCloneModal = false">
|
|
<div class="modal" @click.stop>
|
|
<h3>Clone Theme</h3>
|
|
<input
|
|
v-model="cloneName"
|
|
type="text"
|
|
placeholder="New theme name"
|
|
@keyup.enter="confirmClone"
|
|
/>
|
|
<div class="modal-actions">
|
|
<button class="btn-secondary" @click="showCloneModal = false">Cancel</button>
|
|
<button class="btn-primary" @click="confirmClone">Clone</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Modal -->
|
|
<div v-if="showEditModal" class="modal-overlay" @click="showEditModal = false">
|
|
<div class="modal" @click.stop>
|
|
<h3>Edit Theme</h3>
|
|
<div class="form-group">
|
|
<label>Name</label>
|
|
<input
|
|
v-model="editName"
|
|
type="text"
|
|
placeholder="Theme name"
|
|
/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Description</label>
|
|
<textarea
|
|
v-model="editDescription"
|
|
placeholder="Theme description (optional)"
|
|
rows="3"
|
|
></textarea>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="btn-secondary" @click="showEditModal = false">Cancel</button>
|
|
<button class="btn-primary" @click="confirmEdit">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.themes-page {
|
|
display: flex;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
background: var(--bg-primary);
|
|
}
|
|
|
|
/* Sidebar */
|
|
.sidebar {
|
|
width: 280px;
|
|
background: var(--bg-secondary);
|
|
border-right: 1px solid var(--border-color);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.sidebar-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 1rem 1.25rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
gap: 1rem;
|
|
}
|
|
|
|
.sidebar-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.sidebar-header h2 {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
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 {
|
|
padding: 0.375rem;
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.btn-icon:hover {
|
|
background: var(--bg-hover);
|
|
color: var(--accent);
|
|
}
|
|
|
|
.theme-groups {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.theme-group {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.theme-group h3 {
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--text-muted);
|
|
margin: 0 0 0.75rem 0;
|
|
}
|
|
|
|
.theme-group > :deep(.theme-item) {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.sidebar-footer {
|
|
padding: 1rem;
|
|
border-top: 1px solid var(--border-color);
|
|
}
|
|
|
|
/* Editor */
|
|
.editor {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.editor-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 1rem 1.5rem;
|
|
border-bottom: 1px solid var(--border-color);
|
|
background: var(--bg-secondary);
|
|
}
|
|
|
|
.editor-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.editor-title h2 {
|
|
margin: 0;
|
|
font-size: 1.125rem;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.unsaved-badge {
|
|
font-size: 0.7rem;
|
|
padding: 0.25rem 0.5rem;
|
|
background: var(--warning-bg);
|
|
color: var(--warning);
|
|
border-radius: 9999px;
|
|
}
|
|
|
|
.editor-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
/* Category Tabs */
|
|
.category-tabs {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
padding: 0.75rem 1.5rem;
|
|
background: var(--bg-secondary);
|
|
border-bottom: 1px solid var(--border-color);
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.category-tabs button {
|
|
padding: 0.5rem 1rem;
|
|
background: transparent;
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: var(--text-secondary);
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.category-tabs button:hover {
|
|
background: var(--bg-hover);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.category-tabs button.active {
|
|
background: var(--accent-muted);
|
|
color: var(--accent);
|
|
}
|
|
|
|
/* Variables */
|
|
.variables-section {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
transition: flex 0.3s ease;
|
|
}
|
|
|
|
.variables-section.collapsed {
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.variables-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0.75rem 1.5rem;
|
|
background: var(--bg-tertiary);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
.variables-header:hover {
|
|
background: var(--bg-hover);
|
|
}
|
|
|
|
.variables-header span {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.collapse-icon {
|
|
color: var(--text-muted);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.collapse-icon.rotated {
|
|
transform: rotate(-180deg);
|
|
}
|
|
|
|
.variables-content {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 1.5rem;
|
|
transition: max-height 0.3s ease, opacity 0.3s ease;
|
|
}
|
|
|
|
.variables-section.collapsed .variables-content {
|
|
max-height: 0;
|
|
padding: 0 1.5rem;
|
|
opacity: 0;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.variables-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
gap: 1rem;
|
|
padding-bottom: 1rem;
|
|
}
|
|
|
|
/* Preview */
|
|
.preview-section {
|
|
flex-shrink: 0;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
padding: 2rem;
|
|
border-top: 1px solid var(--border-color);
|
|
background: var(--bg-secondary);
|
|
}
|
|
|
|
.variables-section.collapsed ~ .preview-section {
|
|
max-height: none;
|
|
flex: 1;
|
|
}
|
|
|
|
.preview-section h3 {
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--text-muted);
|
|
margin: 0 0 1rem 0;
|
|
}
|
|
|
|
/* Buttons */
|
|
.btn-primary {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem 1rem;
|
|
background: var(--accent);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
background: var(--accent-hover);
|
|
}
|
|
|
|
.btn-primary:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-secondary {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem 1rem;
|
|
background: var(--bg-hover);
|
|
color: var(--text-primary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 6px;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.btn-secondary:hover:not(:disabled) {
|
|
background: var(--bg-tertiary);
|
|
}
|
|
|
|
.btn-secondary:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* Modal */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.modal {
|
|
width: 100%;
|
|
max-width: 360px;
|
|
padding: 1.5rem;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 12px;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
}
|
|
|
|
.modal h3 {
|
|
margin: 0 0 1rem 0;
|
|
font-size: 1.125rem;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.modal input {
|
|
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;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.modal input:focus {
|
|
outline: none;
|
|
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;
|
|
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>
|