From 28f3b17261df1ddb83cb4372823134e8534a441f Mon Sep 17 00:00:00 2001 From: vegard Date: Thu, 19 Mar 2026 06:41:09 +0000 Subject: [PATCH] Fargevelger: full HSL-kontroll (hue + lightness + saturation) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ThemeColor har nå lightness-felt. Alle tre slidere (hue, lightness, saturation) dekker hele spekteret 0-100/360. Brukeren kan gå fra hvit til svart og alt imellom. Tekst auto-tilpasses lys/mørk bakgrunn. Nytt preset «Lys» for de som foretrekker lys skjerm. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/lib/components/ContextHeader.svelte | 68 ++++++------ frontend/src/lib/workspace/theme.ts | 101 ++++++++++++------ 2 files changed, 97 insertions(+), 72 deletions(-) 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)} +
+
{label}
+
+ setThemeColor(key, 'hue', +e.currentTarget.value)} /> + setThemeColor(key, 'lightness', +e.currentTarget.value)} /> + setThemeColor(key, 'saturation', +e.currentTarget.value)} /> + +
-
-
-
Overflate
-
- setThemeColor('surface', 'hue', +e.currentTarget.value)} /> - setThemeColor('surface', 'saturation', +e.currentTarget.value)} /> - -
-
-
-
Aksent
-
- setThemeColor('accent', 'hue', +e.currentTarget.value)} /> - setThemeColor('accent', 'saturation', +e.currentTarget.value)} /> - -
-
+ {/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 }, }; }