diff --git a/frontend/src/lib/components/ContextHeader.svelte b/frontend/src/lib/components/ContextHeader.svelte
index 8e1470b..05b2e81 100644
--- a/frontend/src/lib/components/ContextHeader.svelte
+++ b/frontend/src/lib/components/ContextHeader.svelte
@@ -499,42 +499,28 @@
-
-
Bakgrunn
-
-
setThemeColor('bg', 'hue', +e.currentTarget.value)} />
-
setThemeColor('bg', 'saturation', +e.currentTarget.value)} />
-
+ {#each [
+ { key: 'bg' as const, label: 'Bakgrunn' },
+ { key: 'surface' as const, label: 'Overflate' },
+ { key: 'accent' as const, label: 'Aksent' },
+ ] as { key, label } (key)}
+
-
-
-
+ {/each}
{#if $page.data.session?.user}
@@ -818,7 +804,7 @@
.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-hue-slider, .settings-sat-slider, .settings-light-slider {
flex: 1;
height: 6px;
-webkit-appearance: none;
@@ -837,11 +823,17 @@
}
.settings-sat-slider {
background: linear-gradient(to right,
- hsl(0,0%,40%), hsl(0,50%,50%)
+ hsl(0,0%,40%), hsl(0,100%,50%)
+ ) !important;
+ }
+ .settings-light-slider {
+ background: linear-gradient(to right,
+ hsl(0,0%,0%), hsl(0,0%,50%), hsl(0,0%,100%)
) !important;
}
.settings-hue-slider::-webkit-slider-thumb,
- .settings-sat-slider::-webkit-slider-thumb {
+ .settings-sat-slider::-webkit-slider-thumb,
+ .settings-light-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px; height: 14px; border-radius: 50%;
background: white; border: 2px solid rgba(0,0,0,0.3); cursor: pointer;
diff --git a/frontend/src/lib/workspace/theme.ts b/frontend/src/lib/workspace/theme.ts
index 3689537..ecb66f0 100644
--- a/frontend/src/lib/workspace/theme.ts
+++ b/frontend/src/lib/workspace/theme.ts
@@ -11,10 +11,11 @@
* 3. Plattform-default (DEFAULT_THEME)
*/
-/** Color definition: hue + saturation. Lightness is derived per use. */
+/** Color definition: full HSL control. */
export interface ThemeColor {
hue: number; // 0-360
saturation: number; // 0-100
+ lightness: number; // 0-100
}
/** Full theme configuration */
@@ -26,9 +27,9 @@ export interface ThemeConfig {
/** Platform default — neutral dark with indigo accent */
export const DEFAULT_THEME: ThemeConfig = {
- bg: { hue: 0, saturation: 0 },
- surface: { hue: 0, saturation: 0 },
- accent: { hue: 239, saturation: 70 },
+ bg: { hue: 0, saturation: 0, lightness: 4 },
+ surface: { hue: 0, saturation: 0, lightness: 12 },
+ accent: { hue: 239, saturation: 70, lightness: 60 },
};
/** Named theme presets */
@@ -45,41 +46,49 @@ export const THEME_PRESETS: ThemePreset[] = [
{
name: 'Hav',
theme: {
- bg: { hue: 210, saturation: 15 },
- surface: { hue: 210, saturation: 12 },
- accent: { hue: 200, saturation: 75 },
+ bg: { hue: 210, saturation: 15, lightness: 4 },
+ surface: { hue: 210, saturation: 12, lightness: 12 },
+ accent: { hue: 200, saturation: 75, lightness: 60 },
},
},
{
name: 'Skog',
theme: {
- bg: { hue: 150, saturation: 12 },
- surface: { hue: 150, saturation: 10 },
- accent: { hue: 142, saturation: 65 },
+ 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 },
- surface: { hue: 15, saturation: 10 },
- accent: { hue: 25, saturation: 80 },
+ 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 },
- surface: { hue: 270, saturation: 8 },
- accent: { hue: 280, saturation: 65 },
+ 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 },
- surface: { hue: 0, saturation: 0 },
- accent: { hue: 0, saturation: 0 },
+ bg: { hue: 0, saturation: 0, lightness: 4 },
+ surface: { hue: 0, saturation: 0, lightness: 12 },
+ accent: { hue: 0, saturation: 0, lightness: 60 },
},
},
];
@@ -98,22 +107,40 @@ export function applyTheme(config: ThemeConfig): void {
const root = document.documentElement;
const { bg, surface, accent } = config;
+ const isDark = bg.lightness < 50;
- // Background: very dark, slight tint
- root.style.setProperty('--color-bg', hsl(bg.hue, bg.saturation, 4));
+ // Background
+ root.style.setProperty('--color-bg', hsl(bg.hue, bg.saturation, bg.lightness));
- // Surface: slightly lighter
- root.style.setProperty('--color-surface', hsl(surface.hue, surface.saturation, 12));
- root.style.setProperty('--color-surface-hover', hsl(surface.hue, Math.max(0, surface.saturation - 2), 15));
+ // 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
- root.style.setProperty('--color-border', hsl(surface.hue, Math.max(0, surface.saturation - 3), 18));
- root.style.setProperty('--color-border-hover', hsl(surface.hue, Math.max(0, surface.saturation - 3), 24));
+ 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, 60));
- root.style.setProperty('--color-accent-hover', hsl(accent.hue, accent.saturation, 65));
- root.style.setProperty('--color-accent-glow', hsla(accent.hue, accent.saturation, 60, 0.15));
+ 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 {
+ return Math.min(max, Math.max(min, v));
}
/** Reset theme to platform defaults (remove inline styles) */
@@ -139,17 +166,23 @@ export function loadThemeFromMetadata(meta: Record
): ThemeConfi
const theme = prefs?.theme as Record | undefined;
if (!theme) return null;
- // New format
+ // New format (with lightness)
if (theme.bg && typeof theme.bg === 'object') {
- return theme as unknown as ThemeConfig;
+ const t = theme as unknown as ThemeConfig;
+ // Ensure lightness exists (backcompat with intermediate format)
+ return {
+ bg: { hue: t.bg.hue ?? 0, saturation: t.bg.saturation ?? 0, lightness: t.bg.lightness ?? 4 },
+ surface: { hue: t.surface.hue ?? 0, saturation: t.surface.saturation ?? 0, lightness: t.surface.lightness ?? 12 },
+ accent: { hue: t.accent.hue ?? 239, saturation: t.accent.saturation ?? 70, lightness: t.accent.lightness ?? 60 },
+ };
}
// Old format: { hueBg, hueSurface, hueAccent }
if (typeof theme.hueBg === 'number' || typeof theme.hueAccent === 'number') {
return {
- bg: { hue: (theme.hueBg as number) ?? 0, saturation: (theme.hueBg as number) ? 10 : 0 },
- surface: { hue: (theme.hueSurface as number) ?? 0, saturation: (theme.hueSurface as number) ? 8 : 0 },
- accent: { hue: (theme.hueAccent as number) ?? 239, saturation: 70 },
+ bg: { hue: (theme.hueBg as number) ?? 0, saturation: (theme.hueBg as number) ? 10 : 0, lightness: 4 },
+ surface: { hue: (theme.hueSurface as number) ?? 0, saturation: (theme.hueSurface as number) ? 8 : 0, lightness: 12 },
+ accent: { hue: (theme.hueAccent as number) ?? 239, saturation: 70, lightness: 60 },
};
}