Fargevelger: full HSL-kontroll (hue + lightness + saturation)
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) <noreply@anthropic.com>
This commit is contained in:
parent
149046572f
commit
28f3b17261
2 changed files with 97 additions and 72 deletions
|
|
@ -499,42 +499,28 @@
|
|||
</div>
|
||||
|
||||
<!-- Per-color sliders -->
|
||||
{#each [
|
||||
{ key: 'bg' as const, label: 'Bakgrunn' },
|
||||
{ key: 'surface' as const, label: 'Overflate' },
|
||||
{ key: 'accent' as const, label: 'Aksent' },
|
||||
] as { key, label } (key)}
|
||||
<div class="settings-color-group">
|
||||
<div class="settings-color-label">Bakgrunn</div>
|
||||
<div class="settings-color-label">{label}</div>
|
||||
<div class="settings-color-row">
|
||||
<input type="range" class="settings-hue-slider" min="0" max="360"
|
||||
value={theme.bg.hue}
|
||||
oninput={(e) => setThemeColor('bg', 'hue', +e.currentTarget.value)} />
|
||||
<input type="range" class="settings-sat-slider" min="0" max="30"
|
||||
value={theme.bg.saturation}
|
||||
oninput={(e) => setThemeColor('bg', 'saturation', +e.currentTarget.value)} />
|
||||
<span class="settings-color-swatch" style:background={themeColorCSS(theme.bg, 12)}></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-color-group">
|
||||
<div class="settings-color-label">Overflate</div>
|
||||
<div class="settings-color-row">
|
||||
<input type="range" class="settings-hue-slider" min="0" max="360"
|
||||
value={theme.surface.hue}
|
||||
oninput={(e) => setThemeColor('surface', 'hue', +e.currentTarget.value)} />
|
||||
<input type="range" class="settings-sat-slider" min="0" max="30"
|
||||
value={theme.surface.saturation}
|
||||
oninput={(e) => setThemeColor('surface', 'saturation', +e.currentTarget.value)} />
|
||||
<span class="settings-color-swatch" style:background={themeColorCSS(theme.surface, 20)}></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-color-group">
|
||||
<div class="settings-color-label">Aksent</div>
|
||||
<div class="settings-color-row">
|
||||
<input type="range" class="settings-hue-slider" min="0" max="360"
|
||||
value={theme.accent.hue}
|
||||
oninput={(e) => setThemeColor('accent', 'hue', +e.currentTarget.value)} />
|
||||
value={theme[key].hue}
|
||||
oninput={(e) => setThemeColor(key, 'hue', +e.currentTarget.value)} />
|
||||
<input type="range" class="settings-light-slider" min="0" max="100"
|
||||
value={theme[key].lightness}
|
||||
oninput={(e) => setThemeColor(key, 'lightness', +e.currentTarget.value)} />
|
||||
<input type="range" class="settings-sat-slider" min="0" max="100"
|
||||
value={theme.accent.saturation}
|
||||
oninput={(e) => setThemeColor('accent', 'saturation', +e.currentTarget.value)} />
|
||||
<span class="settings-color-swatch" style:background={themeColorCSS(theme.accent, 60)}></span>
|
||||
value={theme[key].saturation}
|
||||
oninput={(e) => setThemeColor(key, 'saturation', +e.currentTarget.value)} />
|
||||
<span class="settings-color-swatch"
|
||||
style:background={themeColorCSS(theme[key], theme[key].lightness)}></span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="settings-divider"></div>
|
||||
{#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;
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>): ThemeConfi
|
|||
const theme = prefs?.theme as Record<string, unknown> | 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 },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue