Fargevelger: velg grensesnittelement fra dropdown, full HSL-kontroll
Ny modell: dropdown velger hva du farger (Canvas, Menylinje, Bokser, Rammer, Knapper/aksent, Tekst). Tre slidere per element (farge, metning, lyshet) gir full kontroll over hele spekteret. Presets setter alle elementer på én gang. Fargeprøve viser valgt farge. BlockShell bruker ny --color-panel variabel. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bf744639c1
commit
798a11f93f
3 changed files with 174 additions and 132 deletions
|
|
@ -9,8 +9,10 @@
|
||||||
import { updateNode, createNode, createEdge, deleteNode } from '$lib/api';
|
import { updateNode, createNode, createEdge, deleteNode } from '$lib/api';
|
||||||
import {
|
import {
|
||||||
type ThemeConfig,
|
type ThemeConfig,
|
||||||
|
type ThemeSurface,
|
||||||
DEFAULT_THEME,
|
DEFAULT_THEME,
|
||||||
THEME_PRESETS,
|
THEME_PRESETS,
|
||||||
|
THEME_SURFACES,
|
||||||
applyTheme,
|
applyTheme,
|
||||||
presetAccentCSS,
|
presetAccentCSS,
|
||||||
} from '$lib/workspace/theme.js';
|
} from '$lib/workspace/theme.js';
|
||||||
|
|
@ -242,6 +244,7 @@
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
let settingsOpen = $state(false);
|
let settingsOpen = $state(false);
|
||||||
|
let editingSurface = $state<ThemeSurface>('accent');
|
||||||
|
|
||||||
function toggleSettings() {
|
function toggleSettings() {
|
||||||
settingsOpen = !settingsOpen;
|
settingsOpen = !settingsOpen;
|
||||||
|
|
@ -249,8 +252,11 @@
|
||||||
toolMenuOpen = false;
|
toolMenuOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTheme(partial: Partial<ThemeConfig>) {
|
function updateSurface(prop: 'hue' | 'saturation' | 'lightness', value: number) {
|
||||||
const newTheme = { ...theme, ...partial };
|
const newTheme = {
|
||||||
|
...theme,
|
||||||
|
[editingSurface]: { ...theme[editingSurface], [prop]: value },
|
||||||
|
};
|
||||||
applyTheme(newTheme);
|
applyTheme(newTheme);
|
||||||
onThemeChange?.(newTheme);
|
onThemeChange?.(newTheme);
|
||||||
}
|
}
|
||||||
|
|
@ -480,37 +486,56 @@
|
||||||
<!-- Presets -->
|
<!-- Presets -->
|
||||||
<div class="settings-title">Tema</div>
|
<div class="settings-title">Tema</div>
|
||||||
<div class="settings-presets">
|
<div class="settings-presets">
|
||||||
{#each THEME_PRESETS as preset (preset.name)}
|
{#each THEME_PRESETS as p (p.name)}
|
||||||
<button
|
<button
|
||||||
class="settings-preset"
|
class="settings-preset"
|
||||||
title={preset.name}
|
title={p.name}
|
||||||
onclick={() => applyPreset(preset.theme)}
|
onclick={() => applyPreset(p.theme)}
|
||||||
>
|
>
|
||||||
<span class="settings-preset-swatch"
|
<span class="settings-preset-swatch"
|
||||||
style:background={presetAccentCSS(preset.theme)}
|
style:background={presetAccentCSS(p.theme)}
|
||||||
>{preset.emoji}</span>
|
>{p.emoji}</span>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Accent color -->
|
<!-- Surface selector + sliders -->
|
||||||
|
<div class="settings-color-group">
|
||||||
|
<select
|
||||||
|
class="settings-surface-select"
|
||||||
|
value={editingSurface}
|
||||||
|
onchange={(e) => { editingSurface = e.currentTarget.value as ThemeSurface; }}
|
||||||
|
>
|
||||||
|
{#each Object.entries(THEME_SURFACES) as [key, label] (key)}
|
||||||
|
<option value={key}>{label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="settings-color-group">
|
<div class="settings-color-group">
|
||||||
<div class="settings-color-label">Farge</div>
|
<div class="settings-color-label">Farge</div>
|
||||||
<input type="range" class="settings-hue-slider" min="0" max="360"
|
<input type="range" class="settings-hue-slider" min="0" max="360"
|
||||||
value={theme.accentHue}
|
value={theme[editingSurface].hue}
|
||||||
oninput={(e) => updateTheme({ accentHue: +e.currentTarget.value })} />
|
oninput={(e) => updateSurface('hue', +e.currentTarget.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-color-group">
|
<div class="settings-color-group">
|
||||||
<div class="settings-color-label">Intensitet</div>
|
<div class="settings-color-label">Metning</div>
|
||||||
<input type="range" class="settings-sat-slider" min="0" max="100"
|
<input type="range" class="settings-sat-slider" min="0" max="100"
|
||||||
value={theme.accentSat}
|
value={theme[editingSurface].saturation}
|
||||||
oninput={(e) => updateTheme({ accentSat: +e.currentTarget.value })} />
|
oninput={(e) => updateSurface('saturation', +e.currentTarget.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-color-group">
|
<div class="settings-color-group">
|
||||||
<div class="settings-color-label">Lys / mørk</div>
|
<div class="settings-color-label">Lyshet</div>
|
||||||
<input type="range" class="settings-light-slider" min="0" max="100"
|
<input type="range" class="settings-light-slider" min="0" max="100"
|
||||||
value={theme.brightness}
|
value={theme[editingSurface].lightness}
|
||||||
oninput={(e) => updateTheme({ brightness: +e.currentTarget.value })} />
|
oninput={(e) => updateSurface('lightness', +e.currentTarget.value)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-color-preview">
|
||||||
|
<span class="settings-preview-swatch"
|
||||||
|
style:background="hsl({theme[editingSurface].hue}, {theme[editingSurface].saturation}%, {theme[editingSurface].lightness}%)"
|
||||||
|
></span>
|
||||||
|
<span class="settings-preview-label">{THEME_SURFACES[editingSurface]}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-divider"></div>
|
<div class="settings-divider"></div>
|
||||||
|
|
@ -790,6 +815,14 @@
|
||||||
/* 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-surface-select {
|
||||||
|
width: 100%; padding: 5px 8px; border-radius: 5px; font-size: 12px;
|
||||||
|
background: var(--color-bg, #0a0a0b) !important; color: var(--color-text, #e8e8ec) !important;
|
||||||
|
border: 1px solid var(--color-border, #2a2a2e) !important; cursor: pointer;
|
||||||
|
}
|
||||||
|
.settings-color-preview { display: flex; align-items: center; gap: 8px; margin-top: 4px; }
|
||||||
|
.settings-preview-swatch { width: 24px; height: 24px; border-radius: 4px; border: 1px solid var(--color-border, #2a2a2e); flex-shrink: 0; }
|
||||||
|
.settings-preview-label { font-size: 11px; color: var(--color-text-muted, #8a8a96); }
|
||||||
|
|
||||||
.settings-hue-slider, .settings-sat-slider, .settings-light-slider {
|
.settings-hue-slider, .settings-sat-slider, .settings-light-slider {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
|
|
@ -462,7 +462,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--color-surface, #1c1c20);
|
background: var(--color-panel, var(--color-surface, #1c1c20));
|
||||||
border: 1px solid var(--color-border, #2a2a2e);
|
border: 1px solid var(--color-border, #2a2a2e);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
|
|
||||||
|
|
@ -1,173 +1,182 @@
|
||||||
/**
|
/**
|
||||||
* Shared theme logic for Synops workspace.
|
* Shared theme logic for Synops workspace.
|
||||||
*
|
*
|
||||||
* Simplified model: user picks an accent color and a brightness level.
|
* Model: each UI surface has its own HSL color. A dropdown selects
|
||||||
* The system derives all other colors (bg, surface, border, text)
|
* which surface to edit, and three sliders (hue, saturation, lightness)
|
||||||
* automatically. This makes theming intuitive — one color choice
|
* control it. Presets set all surfaces at once.
|
||||||
* plus a light/dark slider.
|
|
||||||
*
|
|
||||||
* Three-layer inheritance:
|
|
||||||
* 1. Flate-spesifikt (lagret i nodens metadata)
|
|
||||||
* 2. Personlig default (fra "Hjem")
|
|
||||||
* 3. Plattform-default (DEFAULT_THEME)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Simplified theme: accent color + brightness */
|
/** The surfaces a user can theme */
|
||||||
export interface ThemeConfig {
|
export const THEME_SURFACES = {
|
||||||
/** Accent hue (0-360) */
|
canvas: 'Canvas',
|
||||||
accentHue: number;
|
header: 'Menylinje',
|
||||||
/** Accent saturation (0-100) */
|
panel: 'Bokser',
|
||||||
accentSat: number;
|
border: 'Rammer',
|
||||||
/** Overall brightness (0=pitch black, 100=white) */
|
accent: 'Knapper/aksent',
|
||||||
brightness: number;
|
text: 'Tekst',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ThemeSurface = keyof typeof THEME_SURFACES;
|
||||||
|
|
||||||
|
/** HSL color for one surface */
|
||||||
|
export interface SurfaceColor {
|
||||||
|
hue: number; // 0-360
|
||||||
|
saturation: number; // 0-100
|
||||||
|
lightness: number; // 0-100
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Platform default — neutral dark with indigo accent */
|
/** Full theme: one color per surface */
|
||||||
|
export type ThemeConfig = Record<ThemeSurface, SurfaceColor>;
|
||||||
|
|
||||||
|
/** Platform default */
|
||||||
export const DEFAULT_THEME: ThemeConfig = {
|
export const DEFAULT_THEME: ThemeConfig = {
|
||||||
accentHue: 239,
|
canvas: { hue: 0, saturation: 0, lightness: 4 },
|
||||||
accentSat: 70,
|
header: { hue: 0, saturation: 0, lightness: 12 },
|
||||||
brightness: 5,
|
panel: { hue: 0, saturation: 0, lightness: 12 },
|
||||||
|
border: { hue: 0, saturation: 0, lightness: 18 },
|
||||||
|
accent: { hue: 239, saturation: 70, lightness: 60 },
|
||||||
|
text: { hue: 0, saturation: 0, lightness: 92 },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Named theme presets */
|
|
||||||
export interface ThemePreset {
|
export interface ThemePreset {
|
||||||
name: string;
|
name: string;
|
||||||
emoji: string;
|
emoji: string;
|
||||||
theme: ThemeConfig;
|
theme: ThemeConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function preset(
|
||||||
|
accent: [number, number, number],
|
||||||
|
brightness: number,
|
||||||
|
): ThemeConfig {
|
||||||
|
const isDark = brightness < 50;
|
||||||
|
const [ah, as, al] = accent;
|
||||||
|
const tint = Math.min(12, as * 0.15);
|
||||||
|
return {
|
||||||
|
canvas: { hue: ah, saturation: tint, lightness: brightness },
|
||||||
|
header: { hue: ah, saturation: tint, lightness: isDark ? brightness + 8 : brightness - 5 },
|
||||||
|
panel: { hue: ah, saturation: tint, lightness: isDark ? brightness + 8 : brightness - 5 },
|
||||||
|
border: { hue: ah, saturation: Math.max(0, tint - 3), lightness: isDark ? brightness + 14 : brightness - 10 },
|
||||||
|
accent: { hue: ah, saturation: as, lightness: al },
|
||||||
|
text: { hue: ah, saturation: Math.min(8, tint), lightness: isDark ? 92 : 10 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const THEME_PRESETS: ThemePreset[] = [
|
export const THEME_PRESETS: ThemePreset[] = [
|
||||||
{ name: 'Standard', emoji: '🌑', theme: { accentHue: 239, accentSat: 70, brightness: 5 } },
|
{ name: 'Standard', emoji: '🌑', theme: DEFAULT_THEME },
|
||||||
{ name: 'Hav', emoji: '🌊', theme: { accentHue: 200, accentSat: 75, brightness: 5 } },
|
{ name: 'Hav', emoji: '🌊', theme: preset([200, 75, 60], 5) },
|
||||||
{ name: 'Skog', emoji: '🌲', theme: { accentHue: 142, accentSat: 65, brightness: 5 } },
|
{ name: 'Skog', emoji: '🌲', theme: preset([142, 65, 55], 5) },
|
||||||
{ name: 'Solnedgang', emoji: '🌅', theme: { accentHue: 25, accentSat: 80, brightness: 6 } },
|
{ name: 'Solnedgang', emoji: '🌅', theme: preset([25, 80, 60], 6) },
|
||||||
{ name: 'Lavendel', emoji: '💜', theme: { accentHue: 280, accentSat: 65, brightness: 5 } },
|
{ name: 'Lavendel', emoji: '💜', theme: preset([280, 65, 60], 5) },
|
||||||
{ name: 'Rosa', emoji: '🌸', theme: { accentHue: 330, accentSat: 70, brightness: 5 } },
|
{ name: 'Rosa', emoji: '🌸', theme: preset([330, 70, 60], 5) },
|
||||||
{ name: 'Lys', emoji: '☀️', theme: { accentHue: 220, accentSat: 50, brightness: 95 } },
|
{ name: 'Lys', emoji: '☀️', theme: preset([220, 50, 50], 95) },
|
||||||
{ name: 'Monokrom', emoji: '⚫', theme: { accentHue: 0, accentSat: 0, brightness: 5 } },
|
{ name: 'Monokrom', emoji: '⚫', theme: preset([0, 0, 60], 5) },
|
||||||
];
|
];
|
||||||
|
|
||||||
function hsl(hue: number, sat: number, light: number): string {
|
function hsl(c: SurfaceColor): string {
|
||||||
return `hsl(${hue}, ${sat}%, ${light}%)`;
|
return `hsl(${c.hue}, ${c.saturation}%, ${c.lightness}%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hsla(hue: number, sat: number, light: number, alpha: number): string {
|
function hsla(c: SurfaceColor, alpha: number): string {
|
||||||
return `hsla(${hue}, ${sat}%, ${light}%, ${alpha})`;
|
return `hsla(${c.hue}, ${c.saturation}%, ${c.lightness}%, ${alpha})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function clamp(v: number, min = 0, max = 100): number {
|
function clamp(v: number): number {
|
||||||
return Math.min(max, Math.max(min, v));
|
return Math.min(100, Math.max(0, v));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Apply theme to CSS custom properties */
|
||||||
* Apply theme to :root CSS custom properties.
|
export function applyTheme(t: ThemeConfig): void {
|
||||||
* Derives all colors from accent + brightness.
|
|
||||||
*/
|
|
||||||
export function applyTheme(config: ThemeConfig): void {
|
|
||||||
if (typeof document === 'undefined') return;
|
if (typeof document === 'undefined') return;
|
||||||
const root = document.documentElement;
|
const s = document.documentElement.style;
|
||||||
|
|
||||||
const { accentHue, accentSat, brightness } = config;
|
s.setProperty('--color-bg', hsl(t.canvas));
|
||||||
const isDark = brightness < 50;
|
s.setProperty('--color-surface', hsl(t.header));
|
||||||
|
s.setProperty('--color-surface-hover', hsl({
|
||||||
// Tint: surfaces get a subtle hint of the accent hue
|
...t.header,
|
||||||
const tintSat = Math.min(15, accentSat * 0.2);
|
lightness: clamp(t.header.lightness + (t.canvas.lightness < 50 ? 3 : -3)),
|
||||||
|
}));
|
||||||
// Background and surfaces derived from brightness
|
s.setProperty('--color-panel', hsl(t.panel));
|
||||||
const bgLight = brightness;
|
s.setProperty('--color-border', hsl(t.border));
|
||||||
const surfaceLight = isDark
|
s.setProperty('--color-border-hover', hsl({
|
||||||
? clamp(brightness + 8)
|
...t.border,
|
||||||
: clamp(brightness - 5);
|
lightness: clamp(t.border.lightness + (t.canvas.lightness < 50 ? 6 : -6)),
|
||||||
const surfaceHoverLight = isDark
|
}));
|
||||||
? clamp(brightness + 12)
|
s.setProperty('--color-text', hsl(t.text));
|
||||||
: clamp(brightness - 8);
|
s.setProperty('--color-text-muted', hsl({
|
||||||
const borderLight = isDark
|
...t.text,
|
||||||
? clamp(brightness + 16)
|
lightness: clamp(t.canvas.lightness < 50 ? 55 : 40),
|
||||||
: clamp(brightness - 12);
|
saturation: Math.min(8, t.text.saturation),
|
||||||
|
}));
|
||||||
root.style.setProperty('--color-bg', hsl(accentHue, tintSat, bgLight));
|
s.setProperty('--color-text-dim', hsl({
|
||||||
root.style.setProperty('--color-surface', hsl(accentHue, tintSat, surfaceLight));
|
...t.text,
|
||||||
root.style.setProperty('--color-surface-hover', hsl(accentHue, tintSat, surfaceHoverLight));
|
lightness: clamp(t.canvas.lightness < 50 ? 38 : 55),
|
||||||
root.style.setProperty('--color-border', hsl(accentHue, Math.max(0, tintSat - 3), borderLight));
|
saturation: Math.min(6, t.text.saturation),
|
||||||
root.style.setProperty('--color-border-hover', hsl(accentHue, Math.max(0, tintSat - 3), clamp(borderLight + (isDark ? 6 : -6))));
|
}));
|
||||||
|
s.setProperty('--color-accent', hsl(t.accent));
|
||||||
// Text adapts to background
|
s.setProperty('--color-accent-hover', hsl({
|
||||||
if (isDark) {
|
...t.accent,
|
||||||
root.style.setProperty('--color-text', hsl(accentHue, Math.min(8, tintSat), 92));
|
lightness: clamp(t.accent.lightness + (t.canvas.lightness < 50 ? 5 : -5)),
|
||||||
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));
|
s.setProperty('--color-accent-glow', hsla(t.accent, 0.15));
|
||||||
} 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 */
|
/** Reset theme */
|
||||||
export function resetTheme(): void {
|
export function resetTheme(): void {
|
||||||
if (typeof document === 'undefined') return;
|
if (typeof document === 'undefined') return;
|
||||||
const root = document.documentElement;
|
const s = document.documentElement.style;
|
||||||
const props = [
|
for (const p of [
|
||||||
'--color-bg', '--color-surface', '--color-surface-hover',
|
'--color-bg', '--color-surface', '--color-surface-hover', '--color-panel',
|
||||||
'--color-border', '--color-border-hover',
|
'--color-border', '--color-border-hover',
|
||||||
'--color-text', '--color-text-muted', '--color-text-dim',
|
'--color-text', '--color-text-muted', '--color-text-dim',
|
||||||
'--color-accent', '--color-accent-hover', '--color-accent-glow',
|
'--color-accent', '--color-accent-hover', '--color-accent-glow',
|
||||||
];
|
]) s.removeProperty(p);
|
||||||
for (const prop of props) root.style.removeProperty(prop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Load from metadata (backward compatible) */
|
||||||
* Load theme from node metadata. Backward compatible with all previous formats.
|
|
||||||
*/
|
|
||||||
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;
|
||||||
|
|
||||||
// Current format: { accentHue, accentSat, brightness }
|
// Current format: has 'canvas' key
|
||||||
if (typeof theme.accentHue === 'number') {
|
if (theme.canvas && typeof theme.canvas === 'object') {
|
||||||
return {
|
const t = theme as unknown as ThemeConfig;
|
||||||
accentHue: theme.accentHue as number,
|
// Fill missing surfaces with defaults
|
||||||
accentSat: (theme.accentSat as number) ?? 70,
|
return { ...DEFAULT_THEME, ...t };
|
||||||
brightness: (theme.brightness as number) ?? 5,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Previous format: { bg: { hue, saturation, lightness }, ... }
|
// Previous format: { accentHue, accentSat, brightness }
|
||||||
|
if (typeof theme.accentHue === 'number') {
|
||||||
|
return preset(
|
||||||
|
[theme.accentHue as number, (theme.accentSat as number) ?? 70, 60],
|
||||||
|
(theme.brightness as number) ?? 5,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Older: { bg: { hue, ... }, accent: { hue, ... } }
|
||||||
if (theme.bg && typeof theme.bg === 'object') {
|
if (theme.bg && typeof theme.bg === 'object') {
|
||||||
const accent = theme.accent as Record<string, number> | undefined;
|
const accent = theme.accent as Record<string, number> | undefined;
|
||||||
const bg = theme.bg as Record<string, number> | undefined;
|
const bg = theme.bg as Record<string, number> | undefined;
|
||||||
return {
|
return preset(
|
||||||
accentHue: accent?.hue ?? 239,
|
[accent?.hue ?? 239, accent?.saturation ?? 70, accent?.lightness ?? 60],
|
||||||
accentSat: accent?.saturation ?? 70,
|
bg?.lightness ?? 4,
|
||||||
brightness: bg?.lightness ?? 4,
|
);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Oldest format: { hueBg, hueSurface, hueAccent }
|
// Oldest: { hueBg, hueAccent }
|
||||||
if (typeof theme.hueAccent === 'number' || typeof theme.hueBg === 'number') {
|
if (typeof theme.hueAccent === 'number') {
|
||||||
return {
|
return preset([(theme.hueAccent as number), 70, 60], 5);
|
||||||
accentHue: (theme.hueAccent as number) ?? 239,
|
|
||||||
accentSat: 70,
|
|
||||||
brightness: 5,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Serialize theme for storage */
|
/** Serialize */
|
||||||
export function themeToMetadata(config: ThemeConfig): Record<string, unknown> {
|
export function themeToMetadata(config: ThemeConfig): Record<string, unknown> {
|
||||||
return { accentHue: config.accentHue, accentSat: config.accentSat, brightness: config.brightness };
|
return { ...config };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get CSS color for a preset swatch */
|
/** CSS string for preset swatch */
|
||||||
export function presetAccentCSS(theme: ThemeConfig): string {
|
export function presetAccentCSS(theme: ThemeConfig): string {
|
||||||
return hsl(theme.accentHue, theme.accentSat, 55);
|
return hsl(theme.accent);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue