diff --git a/frontend/src/lib/components/ContextHeader.svelte b/frontend/src/lib/components/ContextHeader.svelte
index 0d30183..bde1b65 100644
--- a/frontend/src/lib/components/ContextHeader.svelte
+++ b/frontend/src/lib/components/ContextHeader.svelte
@@ -42,6 +42,10 @@
theme?: ThemeConfig;
/** Callback when theme changes */
onThemeChange?: (theme: ThemeConfig) => void;
+ /** User's saved theme slots (up to 6) */
+ savedThemes?: ThemeConfig[];
+ /** Callback when saved themes change */
+ onSavedThemesChange?: (themes: ThemeConfig[]) => void;
}
let {
@@ -57,6 +61,8 @@
activeTraits,
theme = DEFAULT_THEME,
onThemeChange,
+ savedThemes = [],
+ onSavedThemesChange,
}: Props = $props();
const isPersonalWorkspace = $derived(!collectionId);
@@ -266,6 +272,19 @@
onThemeChange?.(preset);
}
+ const MAX_SAVED = 6;
+
+ function saveCurrentTheme() {
+ if (savedThemes.length >= MAX_SAVED) return;
+ const newSaved = [...savedThemes, { ...theme }];
+ onSavedThemesChange?.(newSaved);
+ }
+
+ function deleteSavedTheme(index: number) {
+ const newSaved = savedThemes.filter((_, i) => i !== index);
+ onSavedThemesChange?.(newSaved);
+ }
+
// =========================================================================
// Click outside
// =========================================================================
@@ -498,6 +517,29 @@
{/each}
+
+ {#each savedThemes as saved, i (i)}
+
+ {/each}
+ {#if savedThemes.length < MAX_SAVED}
+
+ {/if}
+
@@ -811,6 +853,18 @@
}
.settings-preset:hover { border-color: var(--color-accent, #6366f1); }
.settings-preset-swatch { width: 28px; height: 28px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 14px; }
+ .settings-user-themes { margin-top: 4px; }
+ .settings-saved-preset .settings-preset-swatch { font-size: 11px; font-weight: 600; color: white; text-shadow: 0 1px 2px rgba(0,0,0,0.5); }
+ .settings-save-swatch {
+ background: transparent !important;
+ border: 2px dashed var(--color-border, #2a2a2e);
+ color: var(--color-text-dim, #5a5a66);
+ font-size: 16px;
+ }
+ .settings-save-btn:hover .settings-save-swatch {
+ border-color: var(--color-accent, #6366f1);
+ color: var(--color-text-muted, #8a8a96);
+ }
/* Per-color controls */
.settings-color-group { margin-bottom: 8px; }
diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte
index b243ced..3b5d447 100644
--- a/frontend/src/routes/+page.svelte
+++ b/frontend/src/routes/+page.svelte
@@ -87,6 +87,9 @@
if (typeof meta.gridEnabled === 'boolean') {
gridEnabled = meta.gridEnabled;
}
+ if (Array.isArray(meta.savedThemes)) {
+ savedThemes = meta.savedThemes as ThemeConfig[];
+ }
}
})
.catch((err) => {
@@ -110,6 +113,7 @@
let layoutInitialized = $state(false);
let savedCamera = $state({ x: 0, y: 0, zoom: 1.0 });
let gridEnabled = $state(false);
+ let savedThemes = $state([]);
// When workspace node appears in store (after creation), load its layout
$effect(() => {
@@ -156,6 +160,7 @@
workspace_layout: layout,
camera: savedCamera,
gridEnabled,
+ savedThemes,
preferences: {
...(currentMeta.preferences ?? {}),
theme: themeToMetadata(currentTheme),
@@ -246,6 +251,11 @@
persistMetadata();
}
+ function handleSavedThemesChange(themes: ThemeConfig[]) {
+ savedThemes = themes;
+ persistMetadata();
+ }
+
function loadThemeFromMeta(meta: Record) {
const loaded = loadThemeFromMetadata(meta);
if (loaded) {
@@ -347,6 +357,8 @@
activeTraits={activeLayoutTraits}
theme={currentTheme}
onThemeChange={handleThemeChange}
+ {savedThemes}
+ onSavedThemesChange={handleSavedThemesChange}
/>