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>
|
</div>
|
||||||
|
|
||||||
<!-- Per-color sliders -->
|
<!-- 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-group">
|
||||||
<div class="settings-color-label">Bakgrunn</div>
|
<div class="settings-color-label">{label}</div>
|
||||||
<div class="settings-color-row">
|
<div class="settings-color-row">
|
||||||
<input type="range" class="settings-hue-slider" min="0" max="360"
|
<input type="range" class="settings-hue-slider" min="0" max="360"
|
||||||
value={theme.bg.hue}
|
value={theme[key].hue}
|
||||||
oninput={(e) => setThemeColor('bg', 'hue', +e.currentTarget.value)} />
|
oninput={(e) => setThemeColor(key, 'hue', +e.currentTarget.value)} />
|
||||||
<input type="range" class="settings-sat-slider" min="0" max="30"
|
<input type="range" class="settings-light-slider" min="0" max="100"
|
||||||
value={theme.bg.saturation}
|
value={theme[key].lightness}
|
||||||
oninput={(e) => setThemeColor('bg', 'saturation', +e.currentTarget.value)} />
|
oninput={(e) => setThemeColor(key, 'lightness', +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)} />
|
|
||||||
<input type="range" class="settings-sat-slider" min="0" max="100"
|
<input type="range" class="settings-sat-slider" min="0" max="100"
|
||||||
value={theme.accent.saturation}
|
value={theme[key].saturation}
|
||||||
oninput={(e) => setThemeColor('accent', 'saturation', +e.currentTarget.value)} />
|
oninput={(e) => setThemeColor(key, 'saturation', +e.currentTarget.value)} />
|
||||||
<span class="settings-color-swatch" style:background={themeColorCSS(theme.accent, 60)}></span>
|
<span class="settings-color-swatch"
|
||||||
|
style:background={themeColorCSS(theme[key], theme[key].lightness)}></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
<div class="settings-divider"></div>
|
<div class="settings-divider"></div>
|
||||||
{#if $page.data.session?.user}
|
{#if $page.data.session?.user}
|
||||||
|
|
@ -818,7 +804,7 @@
|
||||||
.settings-color-row { display: flex; align-items: center; gap: 6px; }
|
.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-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;
|
flex: 1;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
|
|
@ -837,11 +823,17 @@
|
||||||
}
|
}
|
||||||
.settings-sat-slider {
|
.settings-sat-slider {
|
||||||
background: linear-gradient(to right,
|
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;
|
) !important;
|
||||||
}
|
}
|
||||||
.settings-hue-slider::-webkit-slider-thumb,
|
.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;
|
-webkit-appearance: none;
|
||||||
width: 14px; height: 14px; border-radius: 50%;
|
width: 14px; height: 14px; border-radius: 50%;
|
||||||
background: white; border: 2px solid rgba(0,0,0,0.3); cursor: pointer;
|
background: white; border: 2px solid rgba(0,0,0,0.3); cursor: pointer;
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,11 @@
|
||||||
* 3. Plattform-default (DEFAULT_THEME)
|
* 3. Plattform-default (DEFAULT_THEME)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** Color definition: hue + saturation. Lightness is derived per use. */
|
/** Color definition: full HSL control. */
|
||||||
export interface ThemeColor {
|
export interface ThemeColor {
|
||||||
hue: number; // 0-360
|
hue: number; // 0-360
|
||||||
saturation: number; // 0-100
|
saturation: number; // 0-100
|
||||||
|
lightness: number; // 0-100
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Full theme configuration */
|
/** Full theme configuration */
|
||||||
|
|
@ -26,9 +27,9 @@ export interface ThemeConfig {
|
||||||
|
|
||||||
/** 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 },
|
bg: { hue: 0, saturation: 0, lightness: 4 },
|
||||||
surface: { hue: 0, saturation: 0 },
|
surface: { hue: 0, saturation: 0, lightness: 12 },
|
||||||
accent: { hue: 239, saturation: 70 },
|
accent: { hue: 239, saturation: 70, lightness: 60 },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Named theme presets */
|
/** Named theme presets */
|
||||||
|
|
@ -45,41 +46,49 @@ export const THEME_PRESETS: ThemePreset[] = [
|
||||||
{
|
{
|
||||||
name: 'Hav',
|
name: 'Hav',
|
||||||
theme: {
|
theme: {
|
||||||
bg: { hue: 210, saturation: 15 },
|
bg: { hue: 210, saturation: 15, lightness: 4 },
|
||||||
surface: { hue: 210, saturation: 12 },
|
surface: { hue: 210, saturation: 12, lightness: 12 },
|
||||||
accent: { hue: 200, saturation: 75 },
|
accent: { hue: 200, saturation: 75, lightness: 60 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Skog',
|
name: 'Skog',
|
||||||
theme: {
|
theme: {
|
||||||
bg: { hue: 150, saturation: 12 },
|
bg: { hue: 150, saturation: 12, lightness: 4 },
|
||||||
surface: { hue: 150, saturation: 10 },
|
surface: { hue: 150, saturation: 10, lightness: 12 },
|
||||||
accent: { hue: 142, saturation: 65 },
|
accent: { hue: 142, saturation: 65, lightness: 55 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Solnedgang',
|
name: 'Solnedgang',
|
||||||
theme: {
|
theme: {
|
||||||
bg: { hue: 15, saturation: 12 },
|
bg: { hue: 15, saturation: 12, lightness: 5 },
|
||||||
surface: { hue: 15, saturation: 10 },
|
surface: { hue: 15, saturation: 10, lightness: 13 },
|
||||||
accent: { hue: 25, saturation: 80 },
|
accent: { hue: 25, saturation: 80, lightness: 60 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Lavendel',
|
name: 'Lavendel',
|
||||||
theme: {
|
theme: {
|
||||||
bg: { hue: 270, saturation: 10 },
|
bg: { hue: 270, saturation: 10, lightness: 5 },
|
||||||
surface: { hue: 270, saturation: 8 },
|
surface: { hue: 270, saturation: 8, lightness: 13 },
|
||||||
accent: { hue: 280, saturation: 65 },
|
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',
|
name: 'Monokrom',
|
||||||
theme: {
|
theme: {
|
||||||
bg: { hue: 0, saturation: 0 },
|
bg: { hue: 0, saturation: 0, lightness: 4 },
|
||||||
surface: { hue: 0, saturation: 0 },
|
surface: { hue: 0, saturation: 0, lightness: 12 },
|
||||||
accent: { hue: 0, saturation: 0 },
|
accent: { hue: 0, saturation: 0, lightness: 60 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -98,22 +107,40 @@ export function applyTheme(config: ThemeConfig): void {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
|
|
||||||
const { bg, surface, accent } = config;
|
const { bg, surface, accent } = config;
|
||||||
|
const isDark = bg.lightness < 50;
|
||||||
|
|
||||||
// Background: very dark, slight tint
|
// Background
|
||||||
root.style.setProperty('--color-bg', hsl(bg.hue, bg.saturation, 4));
|
root.style.setProperty('--color-bg', hsl(bg.hue, bg.saturation, bg.lightness));
|
||||||
|
|
||||||
// Surface: slightly lighter
|
// Surface: offset from background
|
||||||
root.style.setProperty('--color-surface', hsl(surface.hue, surface.saturation, 12));
|
root.style.setProperty('--color-surface', hsl(surface.hue, surface.saturation, surface.lightness));
|
||||||
root.style.setProperty('--color-surface-hover', hsl(surface.hue, Math.max(0, surface.saturation - 2), 15));
|
const hoverOffset = isDark ? 3 : -3;
|
||||||
|
root.style.setProperty('--color-surface-hover', hsl(surface.hue, surface.saturation, clamp(surface.lightness + hoverOffset)));
|
||||||
|
|
||||||
// Border: derived from surface
|
// Border: derived from surface
|
||||||
root.style.setProperty('--color-border', hsl(surface.hue, Math.max(0, surface.saturation - 3), 18));
|
const borderOffset = isDark ? 6 : -8;
|
||||||
root.style.setProperty('--color-border-hover', hsl(surface.hue, Math.max(0, surface.saturation - 3), 24));
|
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
|
// Accent
|
||||||
root.style.setProperty('--color-accent', hsl(accent.hue, accent.saturation, 60));
|
root.style.setProperty('--color-accent', hsl(accent.hue, accent.saturation, accent.lightness));
|
||||||
root.style.setProperty('--color-accent-hover', hsl(accent.hue, accent.saturation, 65));
|
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, 60, 0.15));
|
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) */
|
/** 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;
|
const theme = prefs?.theme as Record<string, unknown> | undefined;
|
||||||
if (!theme) return null;
|
if (!theme) return null;
|
||||||
|
|
||||||
// New format
|
// New format (with lightness)
|
||||||
if (theme.bg && typeof theme.bg === 'object') {
|
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 }
|
// Old format: { hueBg, hueSurface, hueAccent }
|
||||||
if (typeof theme.hueBg === 'number' || typeof theme.hueAccent === 'number') {
|
if (typeof theme.hueBg === 'number' || typeof theme.hueAccent === 'number') {
|
||||||
return {
|
return {
|
||||||
bg: { hue: (theme.hueBg as number) ?? 0, saturation: (theme.hueBg as number) ? 10 : 0 },
|
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 },
|
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 },
|
accent: { hue: (theme.hueAccent as number) ?? 239, saturation: 70, lightness: 60 },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue