Forenklet fargevelger: én aksentfarge + lys/mørk
Erstattet 9 slidere (3 per farge × 3 farger) med 3 intuitive kontroller: - Farge: hue-stripe for aksentfarge - Intensitet: saturation - Lys/mørk: brightness slider (0=svart, 100=hvit) Systemet utleder bg, surface, border, text automatisk fra disse. Canvas-bakgrunn styres nå av --color-bg (var ikke det før). Presets med emoji-ikoner. Rosa og lys preset lagt til. Bakoverkompatibel med alle tidligere tema-formater. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
28f3b17261
commit
bf744639c1
3 changed files with 135 additions and 179 deletions
|
|
@ -12,7 +12,7 @@
|
||||||
DEFAULT_THEME,
|
DEFAULT_THEME,
|
||||||
THEME_PRESETS,
|
THEME_PRESETS,
|
||||||
applyTheme,
|
applyTheme,
|
||||||
themeColorCSS,
|
presetAccentCSS,
|
||||||
} from '$lib/workspace/theme.js';
|
} from '$lib/workspace/theme.js';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -249,11 +249,8 @@
|
||||||
toolMenuOpen = false;
|
toolMenuOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setThemeColor(channel: 'bg' | 'surface' | 'accent', prop: 'hue' | 'saturation', value: number) {
|
function updateTheme(partial: Partial<ThemeConfig>) {
|
||||||
const newTheme = {
|
const newTheme = { ...theme, ...partial };
|
||||||
...theme,
|
|
||||||
[channel]: { ...theme[channel], [prop]: value },
|
|
||||||
};
|
|
||||||
applyTheme(newTheme);
|
applyTheme(newTheme);
|
||||||
onThemeChange?.(newTheme);
|
onThemeChange?.(newTheme);
|
||||||
}
|
}
|
||||||
|
|
@ -489,38 +486,32 @@
|
||||||
title={preset.name}
|
title={preset.name}
|
||||||
onclick={() => applyPreset(preset.theme)}
|
onclick={() => applyPreset(preset.theme)}
|
||||||
>
|
>
|
||||||
<span
|
<span class="settings-preset-swatch"
|
||||||
class="settings-preset-swatch"
|
style:background={presetAccentCSS(preset.theme)}
|
||||||
style:background={themeColorCSS(preset.theme.accent, 60)}
|
>{preset.emoji}</span>
|
||||||
></span>
|
|
||||||
<span class="settings-preset-label">{preset.name}</span>
|
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Per-color sliders -->
|
<!-- Accent color -->
|
||||||
{#each [
|
<div class="settings-color-group">
|
||||||
{ key: 'bg' as const, label: 'Bakgrunn' },
|
<div class="settings-color-label">Farge</div>
|
||||||
{ key: 'surface' as const, label: 'Overflate' },
|
<input type="range" class="settings-hue-slider" min="0" max="360"
|
||||||
{ key: 'accent' as const, label: 'Aksent' },
|
value={theme.accentHue}
|
||||||
] as { key, label } (key)}
|
oninput={(e) => updateTheme({ accentHue: +e.currentTarget.value })} />
|
||||||
<div class="settings-color-group">
|
</div>
|
||||||
<div class="settings-color-label">{label}</div>
|
<div class="settings-color-group">
|
||||||
<div class="settings-color-row">
|
<div class="settings-color-label">Intensitet</div>
|
||||||
<input type="range" class="settings-hue-slider" min="0" max="360"
|
<input type="range" class="settings-sat-slider" min="0" max="100"
|
||||||
value={theme[key].hue}
|
value={theme.accentSat}
|
||||||
oninput={(e) => setThemeColor(key, 'hue', +e.currentTarget.value)} />
|
oninput={(e) => updateTheme({ accentSat: +e.currentTarget.value })} />
|
||||||
<input type="range" class="settings-light-slider" min="0" max="100"
|
</div>
|
||||||
value={theme[key].lightness}
|
<div class="settings-color-group">
|
||||||
oninput={(e) => setThemeColor(key, 'lightness', +e.currentTarget.value)} />
|
<div class="settings-color-label">Lys / mørk</div>
|
||||||
<input type="range" class="settings-sat-slider" min="0" max="100"
|
<input type="range" class="settings-light-slider" min="0" max="100"
|
||||||
value={theme[key].saturation}
|
value={theme.brightness}
|
||||||
oninput={(e) => setThemeColor(key, 'saturation', +e.currentTarget.value)} />
|
oninput={(e) => updateTheme({ brightness: +e.currentTarget.value })} />
|
||||||
<span class="settings-color-swatch"
|
</div>
|
||||||
style:background={themeColorCSS(theme[key], theme[key].lightness)}></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<div class="settings-divider"></div>
|
<div class="settings-divider"></div>
|
||||||
{#if $page.data.session?.user}
|
{#if $page.data.session?.user}
|
||||||
|
|
@ -790,22 +781,18 @@
|
||||||
/* Presets */
|
/* Presets */
|
||||||
.settings-presets { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 10px; }
|
.settings-presets { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 10px; }
|
||||||
.settings-preset {
|
.settings-preset {
|
||||||
display: flex; align-items: center; gap: 4px; padding: 3px 8px;
|
padding: 2px; border: 2px solid transparent; border-radius: 8px; background: transparent;
|
||||||
border: 1px solid var(--color-border, #2a2a2e); border-radius: 4px; background: transparent;
|
cursor: pointer; transition: border-color 0.15s;
|
||||||
cursor: pointer; font-size: 11px; color: var(--color-text-muted, #8a8a96); transition: border-color 0.1s;
|
|
||||||
}
|
}
|
||||||
.settings-preset:hover { border-color: var(--color-accent, #6366f1); }
|
.settings-preset:hover { border-color: var(--color-accent, #6366f1); }
|
||||||
.settings-preset-swatch { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
.settings-preset-swatch { width: 28px; height: 28px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 14px; }
|
||||||
.settings-preset-label { white-space: nowrap; }
|
|
||||||
|
|
||||||
/* Per-color controls */
|
/* Per-color controls */
|
||||||
.settings-color-group { margin-bottom: 8px; }
|
.settings-color-group { margin-bottom: 8px; }
|
||||||
.settings-color-label { font-size: 11px; color: var(--color-text-dim, #5a5a66); margin-bottom: 3px; }
|
.settings-color-label { font-size: 11px; color: var(--color-text-dim, #5a5a66); margin-bottom: 3px; }
|
||||||
.settings-color-row { display: flex; align-items: center; gap: 6px; }
|
|
||||||
.settings-color-swatch { width: 16px; height: 16px; border-radius: 3px; flex-shrink: 0; border: 1px solid var(--color-border, #2a2a2e); }
|
|
||||||
|
|
||||||
.settings-hue-slider, .settings-sat-slider, .settings-light-slider {
|
.settings-hue-slider, .settings-sat-slider, .settings-light-slider {
|
||||||
flex: 1;
|
width: 100%;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
|
|
||||||
|
|
@ -537,7 +537,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #0a0a0b;
|
background: var(--color-bg, #0a0a0b);
|
||||||
touch-action: none; /* We handle all touch ourselves */
|
touch-action: none; /* We handle all touch ourselves */
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
@ -551,7 +551,7 @@
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
background: #0a0a0b;
|
background: var(--color-bg, #0a0a0b);
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-world {
|
.canvas-world {
|
||||||
|
|
|
||||||
|
|
@ -1,96 +1,50 @@
|
||||||
/**
|
/**
|
||||||
* Shared theme logic for Synops workspace.
|
* Shared theme logic for Synops workspace.
|
||||||
*
|
*
|
||||||
* Each workspace/collection can have its own theme stored in
|
* Simplified model: user picks an accent color and a brightness level.
|
||||||
* metadata.preferences.theme. Themes use HSL color model for
|
* The system derives all other colors (bg, surface, border, text)
|
||||||
* background, surface, and accent colors.
|
* automatically. This makes theming intuitive — one color choice
|
||||||
|
* plus a light/dark slider.
|
||||||
*
|
*
|
||||||
* Three-layer inheritance:
|
* Three-layer inheritance:
|
||||||
* 1. Flate-spesifikt (lagret i nodens metadata)
|
* 1. Flate-spesifikt (lagret i nodens metadata)
|
||||||
* 2. Personlig default (fra "Min arbeidsflate")
|
* 2. Personlig default (fra "Hjem")
|
||||||
* 3. Plattform-default (DEFAULT_THEME)
|
* 3. Plattform-default (DEFAULT_THEME)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Color definition: full HSL control. */
|
/** Simplified theme: accent color + brightness */
|
||||||
export interface ThemeColor {
|
|
||||||
hue: number; // 0-360
|
|
||||||
saturation: number; // 0-100
|
|
||||||
lightness: number; // 0-100
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Full theme configuration */
|
|
||||||
export interface ThemeConfig {
|
export interface ThemeConfig {
|
||||||
bg: ThemeColor;
|
/** Accent hue (0-360) */
|
||||||
surface: ThemeColor;
|
accentHue: number;
|
||||||
accent: ThemeColor;
|
/** Accent saturation (0-100) */
|
||||||
|
accentSat: number;
|
||||||
|
/** Overall brightness (0=pitch black, 100=white) */
|
||||||
|
brightness: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Platform default — neutral dark with indigo accent */
|
/** Platform default — neutral dark with indigo accent */
|
||||||
export const DEFAULT_THEME: ThemeConfig = {
|
export const DEFAULT_THEME: ThemeConfig = {
|
||||||
bg: { hue: 0, saturation: 0, lightness: 4 },
|
accentHue: 239,
|
||||||
surface: { hue: 0, saturation: 0, lightness: 12 },
|
accentSat: 70,
|
||||||
accent: { hue: 239, saturation: 70, lightness: 60 },
|
brightness: 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Named theme presets */
|
/** Named theme presets */
|
||||||
export interface ThemePreset {
|
export interface ThemePreset {
|
||||||
name: string;
|
name: string;
|
||||||
|
emoji: string;
|
||||||
theme: ThemeConfig;
|
theme: ThemeConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const THEME_PRESETS: ThemePreset[] = [
|
export const THEME_PRESETS: ThemePreset[] = [
|
||||||
{
|
{ name: 'Standard', emoji: '🌑', theme: { accentHue: 239, accentSat: 70, brightness: 5 } },
|
||||||
name: 'Standard',
|
{ name: 'Hav', emoji: '🌊', theme: { accentHue: 200, accentSat: 75, brightness: 5 } },
|
||||||
theme: DEFAULT_THEME,
|
{ name: 'Skog', emoji: '🌲', theme: { accentHue: 142, accentSat: 65, brightness: 5 } },
|
||||||
},
|
{ name: 'Solnedgang', emoji: '🌅', theme: { accentHue: 25, accentSat: 80, brightness: 6 } },
|
||||||
{
|
{ name: 'Lavendel', emoji: '💜', theme: { accentHue: 280, accentSat: 65, brightness: 5 } },
|
||||||
name: 'Hav',
|
{ name: 'Rosa', emoji: '🌸', theme: { accentHue: 330, accentSat: 70, brightness: 5 } },
|
||||||
theme: {
|
{ name: 'Lys', emoji: '☀️', theme: { accentHue: 220, accentSat: 50, brightness: 95 } },
|
||||||
bg: { hue: 210, saturation: 15, lightness: 4 },
|
{ name: 'Monokrom', emoji: '⚫', theme: { accentHue: 0, accentSat: 0, brightness: 5 } },
|
||||||
surface: { hue: 210, saturation: 12, lightness: 12 },
|
|
||||||
accent: { hue: 200, saturation: 75, lightness: 60 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Skog',
|
|
||||||
theme: {
|
|
||||||
bg: { hue: 150, saturation: 12, lightness: 4 },
|
|
||||||
surface: { hue: 150, saturation: 10, lightness: 12 },
|
|
||||||
accent: { hue: 142, saturation: 65, lightness: 55 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Solnedgang',
|
|
||||||
theme: {
|
|
||||||
bg: { hue: 15, saturation: 12, lightness: 5 },
|
|
||||||
surface: { hue: 15, saturation: 10, lightness: 13 },
|
|
||||||
accent: { hue: 25, saturation: 80, lightness: 60 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Lavendel',
|
|
||||||
theme: {
|
|
||||||
bg: { hue: 270, saturation: 10, lightness: 5 },
|
|
||||||
surface: { hue: 270, saturation: 8, lightness: 13 },
|
|
||||||
accent: { hue: 280, saturation: 65, lightness: 60 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Lys',
|
|
||||||
theme: {
|
|
||||||
bg: { hue: 220, saturation: 15, lightness: 95 },
|
|
||||||
surface: { hue: 220, saturation: 12, lightness: 100 },
|
|
||||||
accent: { hue: 239, saturation: 70, lightness: 55 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Monokrom',
|
|
||||||
theme: {
|
|
||||||
bg: { hue: 0, saturation: 0, lightness: 4 },
|
|
||||||
surface: { hue: 0, saturation: 0, lightness: 12 },
|
|
||||||
accent: { hue: 0, saturation: 0, lightness: 60 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function hsl(hue: number, sat: number, light: number): string {
|
function hsl(hue: number, sat: number, light: number): string {
|
||||||
|
|
@ -101,104 +55,119 @@ function hsla(hue: number, sat: number, light: number, alpha: number): string {
|
||||||
return `hsla(${hue}, ${sat}%, ${light}%, ${alpha})`;
|
return `hsla(${hue}, ${sat}%, ${light}%, ${alpha})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Apply theme to :root CSS custom properties */
|
|
||||||
export function applyTheme(config: ThemeConfig): void {
|
|
||||||
if (typeof document === 'undefined') return;
|
|
||||||
const root = document.documentElement;
|
|
||||||
|
|
||||||
const { bg, surface, accent } = config;
|
|
||||||
const isDark = bg.lightness < 50;
|
|
||||||
|
|
||||||
// Background
|
|
||||||
root.style.setProperty('--color-bg', hsl(bg.hue, bg.saturation, bg.lightness));
|
|
||||||
|
|
||||||
// Surface: offset from background
|
|
||||||
root.style.setProperty('--color-surface', hsl(surface.hue, surface.saturation, surface.lightness));
|
|
||||||
const hoverOffset = isDark ? 3 : -3;
|
|
||||||
root.style.setProperty('--color-surface-hover', hsl(surface.hue, surface.saturation, clamp(surface.lightness + hoverOffset)));
|
|
||||||
|
|
||||||
// Border: derived from surface
|
|
||||||
const borderOffset = isDark ? 6 : -8;
|
|
||||||
root.style.setProperty('--color-border', hsl(surface.hue, Math.max(0, surface.saturation - 3), clamp(surface.lightness + borderOffset)));
|
|
||||||
root.style.setProperty('--color-border-hover', hsl(surface.hue, Math.max(0, surface.saturation - 3), clamp(surface.lightness + borderOffset * 2)));
|
|
||||||
|
|
||||||
// Text: auto-adapt to background lightness
|
|
||||||
if (isDark) {
|
|
||||||
root.style.setProperty('--color-text', hsl(bg.hue, Math.min(10, bg.saturation), 92));
|
|
||||||
root.style.setProperty('--color-text-muted', hsl(bg.hue, Math.min(8, bg.saturation), 55));
|
|
||||||
root.style.setProperty('--color-text-dim', hsl(bg.hue, Math.min(6, bg.saturation), 38));
|
|
||||||
} else {
|
|
||||||
root.style.setProperty('--color-text', hsl(bg.hue, Math.min(10, bg.saturation), 10));
|
|
||||||
root.style.setProperty('--color-text-muted', hsl(bg.hue, Math.min(8, bg.saturation), 40));
|
|
||||||
root.style.setProperty('--color-text-dim', hsl(bg.hue, Math.min(6, bg.saturation), 55));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accent
|
|
||||||
root.style.setProperty('--color-accent', hsl(accent.hue, accent.saturation, accent.lightness));
|
|
||||||
root.style.setProperty('--color-accent-hover', hsl(accent.hue, accent.saturation, clamp(accent.lightness + (isDark ? 5 : -5))));
|
|
||||||
root.style.setProperty('--color-accent-glow', hsla(accent.hue, accent.saturation, accent.lightness, 0.15));
|
|
||||||
}
|
|
||||||
|
|
||||||
function clamp(v: number, min = 0, max = 100): number {
|
function clamp(v: number, min = 0, max = 100): number {
|
||||||
return Math.min(max, Math.max(min, v));
|
return Math.min(max, Math.max(min, v));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reset theme to platform defaults (remove inline styles) */
|
/**
|
||||||
|
* Apply theme to :root CSS custom properties.
|
||||||
|
* Derives all colors from accent + brightness.
|
||||||
|
*/
|
||||||
|
export function applyTheme(config: ThemeConfig): void {
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
const root = document.documentElement;
|
||||||
|
|
||||||
|
const { accentHue, accentSat, brightness } = config;
|
||||||
|
const isDark = brightness < 50;
|
||||||
|
|
||||||
|
// Tint: surfaces get a subtle hint of the accent hue
|
||||||
|
const tintSat = Math.min(15, accentSat * 0.2);
|
||||||
|
|
||||||
|
// Background and surfaces derived from brightness
|
||||||
|
const bgLight = brightness;
|
||||||
|
const surfaceLight = isDark
|
||||||
|
? clamp(brightness + 8)
|
||||||
|
: clamp(brightness - 5);
|
||||||
|
const surfaceHoverLight = isDark
|
||||||
|
? clamp(brightness + 12)
|
||||||
|
: clamp(brightness - 8);
|
||||||
|
const borderLight = isDark
|
||||||
|
? clamp(brightness + 16)
|
||||||
|
: clamp(brightness - 12);
|
||||||
|
|
||||||
|
root.style.setProperty('--color-bg', hsl(accentHue, tintSat, bgLight));
|
||||||
|
root.style.setProperty('--color-surface', hsl(accentHue, tintSat, surfaceLight));
|
||||||
|
root.style.setProperty('--color-surface-hover', hsl(accentHue, tintSat, surfaceHoverLight));
|
||||||
|
root.style.setProperty('--color-border', hsl(accentHue, Math.max(0, tintSat - 3), borderLight));
|
||||||
|
root.style.setProperty('--color-border-hover', hsl(accentHue, Math.max(0, tintSat - 3), clamp(borderLight + (isDark ? 6 : -6))));
|
||||||
|
|
||||||
|
// Text adapts to background
|
||||||
|
if (isDark) {
|
||||||
|
root.style.setProperty('--color-text', hsl(accentHue, Math.min(8, tintSat), 92));
|
||||||
|
root.style.setProperty('--color-text-muted', hsl(accentHue, Math.min(6, tintSat), 55));
|
||||||
|
root.style.setProperty('--color-text-dim', hsl(accentHue, Math.min(4, tintSat), 38));
|
||||||
|
} else {
|
||||||
|
root.style.setProperty('--color-text', hsl(accentHue, Math.min(8, tintSat), 10));
|
||||||
|
root.style.setProperty('--color-text-muted', hsl(accentHue, Math.min(6, tintSat), 40));
|
||||||
|
root.style.setProperty('--color-text-dim', hsl(accentHue, Math.min(4, tintSat), 55));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accent
|
||||||
|
const accentLight = isDark ? 60 : 45;
|
||||||
|
root.style.setProperty('--color-accent', hsl(accentHue, accentSat, accentLight));
|
||||||
|
root.style.setProperty('--color-accent-hover', hsl(accentHue, accentSat, clamp(accentLight + (isDark ? 5 : -5))));
|
||||||
|
root.style.setProperty('--color-accent-glow', hsla(accentHue, accentSat, accentLight, 0.15));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset theme to platform defaults */
|
||||||
export function resetTheme(): void {
|
export function resetTheme(): void {
|
||||||
if (typeof document === 'undefined') return;
|
if (typeof document === 'undefined') return;
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
const props = [
|
const props = [
|
||||||
'--color-bg', '--color-surface', '--color-surface-hover',
|
'--color-bg', '--color-surface', '--color-surface-hover',
|
||||||
'--color-border', '--color-border-hover',
|
'--color-border', '--color-border-hover',
|
||||||
|
'--color-text', '--color-text-muted', '--color-text-dim',
|
||||||
'--color-accent', '--color-accent-hover', '--color-accent-glow',
|
'--color-accent', '--color-accent-hover', '--color-accent-glow',
|
||||||
];
|
];
|
||||||
for (const prop of props) {
|
for (const prop of props) root.style.removeProperty(prop);
|
||||||
root.style.removeProperty(prop);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load theme from node metadata. Backward compatible with old format
|
* Load theme from node metadata. Backward compatible with all previous formats.
|
||||||
* ({ hueBg, hueSurface, hueAccent }) and new format ({ bg, surface, accent }).
|
|
||||||
*/
|
*/
|
||||||
export function loadThemeFromMetadata(meta: Record<string, unknown>): ThemeConfig | null {
|
export function loadThemeFromMetadata(meta: Record<string, unknown>): ThemeConfig | null {
|
||||||
const prefs = meta.preferences as Record<string, unknown> | undefined;
|
const prefs = meta.preferences as Record<string, unknown> | undefined;
|
||||||
const theme = prefs?.theme as Record<string, unknown> | undefined;
|
const theme = prefs?.theme as Record<string, unknown> | undefined;
|
||||||
if (!theme) return null;
|
if (!theme) return null;
|
||||||
|
|
||||||
// New format (with lightness)
|
// Current format: { accentHue, accentSat, brightness }
|
||||||
if (theme.bg && typeof theme.bg === 'object') {
|
if (typeof theme.accentHue === 'number') {
|
||||||
const t = theme as unknown as ThemeConfig;
|
|
||||||
// Ensure lightness exists (backcompat with intermediate format)
|
|
||||||
return {
|
return {
|
||||||
bg: { hue: t.bg.hue ?? 0, saturation: t.bg.saturation ?? 0, lightness: t.bg.lightness ?? 4 },
|
accentHue: theme.accentHue as number,
|
||||||
surface: { hue: t.surface.hue ?? 0, saturation: t.surface.saturation ?? 0, lightness: t.surface.lightness ?? 12 },
|
accentSat: (theme.accentSat as number) ?? 70,
|
||||||
accent: { hue: t.accent.hue ?? 239, saturation: t.accent.saturation ?? 70, lightness: t.accent.lightness ?? 60 },
|
brightness: (theme.brightness as number) ?? 5,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Old format: { hueBg, hueSurface, hueAccent }
|
// Previous format: { bg: { hue, saturation, lightness }, ... }
|
||||||
if (typeof theme.hueBg === 'number' || typeof theme.hueAccent === 'number') {
|
if (theme.bg && typeof theme.bg === 'object') {
|
||||||
|
const accent = theme.accent as Record<string, number> | undefined;
|
||||||
|
const bg = theme.bg as Record<string, number> | undefined;
|
||||||
return {
|
return {
|
||||||
bg: { hue: (theme.hueBg as number) ?? 0, saturation: (theme.hueBg as number) ? 10 : 0, lightness: 4 },
|
accentHue: accent?.hue ?? 239,
|
||||||
surface: { hue: (theme.hueSurface as number) ?? 0, saturation: (theme.hueSurface as number) ? 8 : 0, lightness: 12 },
|
accentSat: accent?.saturation ?? 70,
|
||||||
accent: { hue: (theme.hueAccent as number) ?? 239, saturation: 70, lightness: 60 },
|
brightness: bg?.lightness ?? 4,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Oldest format: { hueBg, hueSurface, hueAccent }
|
||||||
|
if (typeof theme.hueAccent === 'number' || typeof theme.hueBg === 'number') {
|
||||||
|
return {
|
||||||
|
accentHue: (theme.hueAccent as number) ?? 239,
|
||||||
|
accentSat: 70,
|
||||||
|
brightness: 5,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Serialize theme for storage in node metadata */
|
/** Serialize theme for storage */
|
||||||
export function themeToMetadata(config: ThemeConfig): Record<string, unknown> {
|
export function themeToMetadata(config: ThemeConfig): Record<string, unknown> {
|
||||||
return {
|
return { accentHue: config.accentHue, accentSat: config.accentSat, brightness: config.brightness };
|
||||||
bg: config.bg,
|
|
||||||
surface: config.surface,
|
|
||||||
accent: config.accent,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get a CSS color string for a theme color at a given lightness */
|
/** Get CSS color for a preset swatch */
|
||||||
export function themeColorCSS(color: ThemeColor, lightness: number): string {
|
export function presetAccentCSS(theme: ThemeConfig): string {
|
||||||
return hsl(color.hue, color.saturation, lightness);
|
return hsl(theme.accentHue, theme.accentSat, 55);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue