Skip to content

Commit 146bb2e

Browse files
OpenSource03claude
andcommitted
feat: space theme glass tinting for non-native platforms
On platforms without native glass APIs (Linux, older macOS), darken space tint colors and sidebar tokens so colored themes look intentional rather than washed out against the opaque background. Extract sidebar token application into a shared applySidebarTokens() helper that handles glass vs opaque and dark vs light modes. Add a subtle darkening overlay for neutral (no-space-color) glass sessions. Update island border gradients from pure black/white to a soft blue-tinted oklch(0.82 0.03 285) for a more refined glass appearance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4b1e825 commit 146bb2e

2 files changed

Lines changed: 269 additions & 54 deletions

File tree

src/hooks/useSpaceTheme.ts

Lines changed: 248 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useEffect, useState } from "react";
22
import type { MacBackgroundEffect, Space } from "@/types";
33
import { computeGlassTintColor } from "@/lib/color-utils";
4-
import { isMac } from "@/lib/utils";
4+
import { isMac, isWindows } from "@/lib/utils";
55

66
const TINT_VARS = [
77
"--space-hue", "--space-chroma",
@@ -13,6 +13,9 @@ const TINT_VARS = [
1313

1414
const DARK_SURFACE_BRIGHTNESS_MULTIPLIER = 1.3;
1515
const LIGHT_SURFACE_WHITE_MIX = 0.3;
16+
const NON_NATIVE_TINT_DARKEN_FACTOR = 0.25;
17+
const BASE_TINT_OVERLAY_LIGHTNESS = 0.5;
18+
const NEUTRAL_HUE = 0;
1619

1720
function clamp01(value: number): number {
1821
return Math.max(0, Math.min(1, value));
@@ -31,6 +34,82 @@ function brightenLightLightness(value: number): number {
3134
return clamp01(value + (1 - value) * LIGHT_SURFACE_WHITE_MIX);
3235
}
3336

37+
function darkenTintLightness(value: number, isDark: boolean, enabled: boolean): number {
38+
if (!enabled) return clamp01(value);
39+
if (isDark) return clamp01(value * (1 - NON_NATIVE_TINT_DARKEN_FACTOR));
40+
return clamp01(1 - (1 - value) * (1 + NON_NATIVE_TINT_DARKEN_FACTOR));
41+
}
42+
43+
function darkenShellLightness(value: number, enabled: boolean): number {
44+
if (!enabled) return clamp01(value);
45+
return clamp01(value * (1 - NON_NATIVE_TINT_DARKEN_FACTOR));
46+
}
47+
48+
function applySidebarTokens(
49+
root: HTMLElement,
50+
options: {
51+
isDark: boolean;
52+
isGlass: boolean;
53+
hue: number;
54+
tintStrength: number;
55+
sidebarChroma: number;
56+
surfaceChroma: number;
57+
borderChroma: number;
58+
sidebarLightness: number;
59+
sidebarAccentLightness: number;
60+
sidebarBorderLightness: number;
61+
},
62+
): void {
63+
const {
64+
isDark,
65+
isGlass,
66+
hue,
67+
tintStrength,
68+
sidebarChroma,
69+
surfaceChroma,
70+
borderChroma,
71+
sidebarLightness,
72+
sidebarAccentLightness,
73+
sidebarBorderLightness,
74+
} = options;
75+
76+
if (!isGlass) {
77+
root.style.setProperty("--sidebar", `oklch(${sidebarLightness} ${sidebarChroma} ${hue})`);
78+
root.style.setProperty("--sidebar-accent", `oklch(${sidebarAccentLightness} ${surfaceChroma} ${hue})`);
79+
root.style.setProperty("--sidebar-border", `oklch(${sidebarBorderLightness} ${borderChroma} ${hue})`);
80+
return;
81+
}
82+
83+
if (isDark) {
84+
root.style.setProperty(
85+
"--sidebar",
86+
`oklch(${sidebarLightness} ${sidebarChroma} ${hue} / ${0.34 + 0.08 * tintStrength})`,
87+
);
88+
root.style.setProperty(
89+
"--sidebar-accent",
90+
`oklch(${sidebarAccentLightness} ${surfaceChroma} ${hue} / ${0.46 + 0.08 * tintStrength})`,
91+
);
92+
root.style.setProperty(
93+
"--sidebar-border",
94+
`oklch(${sidebarBorderLightness} ${borderChroma} ${hue} / ${0.32 + 0.06 * tintStrength})`,
95+
);
96+
return;
97+
}
98+
99+
root.style.setProperty(
100+
"--sidebar",
101+
`oklch(${sidebarLightness} ${sidebarChroma} ${hue} / ${0.22 + 0.12 * tintStrength})`,
102+
);
103+
root.style.setProperty(
104+
"--sidebar-accent",
105+
`oklch(${sidebarAccentLightness} ${surfaceChroma} ${hue} / ${0.22 + 0.14 * tintStrength})`,
106+
);
107+
root.style.setProperty(
108+
"--sidebar-border",
109+
`oklch(${sidebarBorderLightness} ${borderChroma} ${hue} / ${0.08 + 0.08 * tintStrength})`,
110+
);
111+
}
112+
34113
function bindNativeGlassTintOnFocus(tintColor: string | null): () => void {
35114
const applyTint = () => window.claude.glass?.setTintColor(tintColor);
36115
applyTint();
@@ -63,11 +142,59 @@ export function useSpaceTheme(
63142
const isDark = resolvedTheme === "dark";
64143
// Native macOS glass supports tintColor via addView()
65144
const isNativeGlass = isGlass && isMac && macBackgroundEffect === "liquid-glass";
145+
const isWindowsMica = isGlass && isWindows;
146+
const shouldDarkenTint = !isNativeGlass && !isWindowsMica;
147+
const neutralTintStrength = 0;
148+
const neutralSidebarChroma = 0;
149+
const neutralSurfaceChroma = 0;
150+
const neutralBorderChroma = 0;
151+
const neutralLightSidebarLightness = darkenShellLightness(
152+
isGlass ? 1 : 0.99,
153+
shouldDarkenTint,
154+
);
155+
const neutralLightSidebarAccentLightness = darkenShellLightness(
156+
0.976,
157+
shouldDarkenTint,
158+
);
159+
const neutralLightSidebarBorderLightness = darkenShellLightness(
160+
isGlass ? 0 : 0.945,
161+
shouldDarkenTint,
162+
);
163+
const neutralDarkSidebarLightness = darkenShellLightness(
164+
isGlass ? 0.139 : 0.254,
165+
shouldDarkenTint,
166+
);
167+
const neutralDarkSidebarAccentLightness = darkenShellLightness(
168+
isGlass ? 0.334 : 0.411,
169+
shouldDarkenTint,
170+
);
171+
const neutralDarkSidebarBorderLightness = darkenShellLightness(
172+
isGlass ? 0.468 : 0.494,
173+
shouldDarkenTint,
174+
);
66175

67176
if (!space || space.color.chroma === 0) {
68177
// Clear all tinted vars so the CSS base values take over
69178
for (const v of TINT_VARS) root.style.removeProperty(v);
70-
setGlassOverlayStyle(null);
179+
if (shouldDarkenTint) {
180+
applySidebarTokens(root, {
181+
isDark,
182+
isGlass,
183+
hue: NEUTRAL_HUE,
184+
tintStrength: neutralTintStrength,
185+
sidebarChroma: neutralSidebarChroma,
186+
surfaceChroma: neutralSurfaceChroma,
187+
borderChroma: neutralBorderChroma,
188+
sidebarLightness: isDark ? neutralDarkSidebarLightness : neutralLightSidebarLightness,
189+
sidebarAccentLightness: isDark ? neutralDarkSidebarAccentLightness : neutralLightSidebarAccentLightness,
190+
sidebarBorderLightness: isDark ? neutralDarkSidebarBorderLightness : neutralLightSidebarBorderLightness,
191+
});
192+
}
193+
setGlassOverlayStyle(
194+
isGlass && shouldDarkenTint
195+
? { background: `oklch(0 0 0 / ${NON_NATIVE_TINT_DARKEN_FACTOR})` }
196+
: null,
197+
);
71198
const releaseFocusTint = isNativeGlass
72199
? bindNativeGlassTintOnFocus(null)
73200
: null;
@@ -95,19 +222,95 @@ export function useSpaceTheme(
95222
const surfaceChroma = (isDark ? 0.04 : 0.055) * tintStrength;
96223
const borderChroma = (isDark ? 0.026 : 0.034) * tintStrength;
97224
const sidebarChroma = (isDark ? 0.024 : 0.03) * tintStrength;
98-
const lightBgLightness = brightenLightLightness(0.985 - 0.012 * tintStrength);
99-
const lightSurfaceLightness = brightenLightLightness(0.955 - 0.02 * tintStrength);
100-
const lightBorderLightness = brightenLightLightness(0.91);
101-
const lightCardLightness = brightenLightLightness(0.98);
102-
const lightSidebarLightness = brightenLightLightness(0.968);
103-
const lightSidebarAccentLightness = brightenLightLightness(0.947);
104-
const darkBgLightness = brightenDarkLightness(0.12 - 0.013 * tintStrength);
105-
const darkSurfaceLightness = brightenDarkLightness(0.355 - 0.04 * tintStrength);
106-
const darkBorderLightness = brightenDarkLightness(0.39);
107-
const darkCardLightness = brightenDarkLightness(0.25);
108-
const darkSidebarLightness = brightenDarkLightness(0.2);
109-
const darkSidebarAccentLightness = brightenDarkLightness(0.31);
110-
const darkSidebarBorderLightness = brightenDarkLightness(0.4);
225+
const lightBgLightness = darkenTintLightness(
226+
brightenLightLightness(0.985 - 0.012 * tintStrength),
227+
false,
228+
shouldDarkenTint,
229+
);
230+
const lightSurfaceLightness = darkenTintLightness(
231+
brightenLightLightness(0.955 - 0.02 * tintStrength),
232+
false,
233+
shouldDarkenTint,
234+
);
235+
const lightBorderLightness = darkenTintLightness(
236+
brightenLightLightness(0.91),
237+
false,
238+
shouldDarkenTint,
239+
);
240+
const lightCardLightness = darkenTintLightness(
241+
brightenLightLightness(0.98),
242+
false,
243+
shouldDarkenTint,
244+
);
245+
const lightSidebarLightness = darkenTintLightness(
246+
brightenLightLightness(0.968),
247+
false,
248+
shouldDarkenTint,
249+
);
250+
const lightSidebarAccentLightness = darkenTintLightness(
251+
brightenLightLightness(0.947),
252+
false,
253+
shouldDarkenTint,
254+
);
255+
const darkBgLightness = darkenTintLightness(
256+
brightenDarkLightness(0.12 - 0.013 * tintStrength),
257+
true,
258+
shouldDarkenTint,
259+
);
260+
const darkSurfaceLightness = darkenTintLightness(
261+
brightenDarkLightness(0.355 - 0.04 * tintStrength),
262+
true,
263+
shouldDarkenTint,
264+
);
265+
const darkBorderLightness = darkenTintLightness(
266+
brightenDarkLightness(0.39),
267+
true,
268+
shouldDarkenTint,
269+
);
270+
const darkCardLightness = darkenTintLightness(
271+
brightenDarkLightness(0.25),
272+
true,
273+
shouldDarkenTint,
274+
);
275+
const darkSidebarLightness = darkenTintLightness(
276+
brightenDarkLightness(0.2),
277+
true,
278+
shouldDarkenTint,
279+
);
280+
const darkSidebarAccentLightness = darkenTintLightness(
281+
brightenDarkLightness(0.31),
282+
true,
283+
shouldDarkenTint,
284+
);
285+
const darkSidebarBorderLightness = darkenTintLightness(
286+
brightenDarkLightness(0.4),
287+
true,
288+
shouldDarkenTint,
289+
);
290+
const shellLightSidebarLightness = darkenShellLightness(
291+
brightenLightLightness(0.968),
292+
shouldDarkenTint,
293+
);
294+
const shellLightSidebarAccentLightness = darkenShellLightness(
295+
brightenLightLightness(0.947),
296+
shouldDarkenTint,
297+
);
298+
const shellLightSidebarBorderLightness = darkenShellLightness(
299+
brightenLightLightness(0.91),
300+
shouldDarkenTint,
301+
);
302+
const shellDarkSidebarLightness = darkenShellLightness(
303+
brightenDarkLightness(0.2),
304+
shouldDarkenTint,
305+
);
306+
const shellDarkSidebarAccentLightness = darkenShellLightness(
307+
brightenDarkLightness(0.31),
308+
shouldDarkenTint,
309+
);
310+
const shellDarkSidebarBorderLightness = darkenShellLightness(
311+
brightenDarkLightness(0.4),
312+
shouldDarkenTint,
313+
);
111314

112315
root.style.setProperty("--space-hue", String(hue));
113316
root.style.setProperty("--space-chroma", String(chroma));
@@ -126,11 +329,18 @@ export function useSpaceTheme(
126329
} else {
127330
root.style.removeProperty("--island-fill");
128331
}
129-
if (!isGlass) {
130-
root.style.setProperty("--sidebar", `oklch(${darkSidebarLightness} ${sidebarChroma} ${hue})`);
131-
root.style.setProperty("--sidebar-accent", `oklch(${darkSidebarAccentLightness} ${surfaceChroma} ${hue})`);
132-
root.style.setProperty("--sidebar-border", `oklch(${darkSidebarBorderLightness} ${borderChroma} ${hue})`);
133-
}
332+
applySidebarTokens(root, {
333+
isDark,
334+
isGlass,
335+
hue,
336+
tintStrength,
337+
sidebarChroma,
338+
surfaceChroma,
339+
borderChroma,
340+
sidebarLightness: isGlass ? shellDarkSidebarLightness : darkSidebarLightness,
341+
sidebarAccentLightness: isGlass ? shellDarkSidebarAccentLightness : darkSidebarAccentLightness,
342+
sidebarBorderLightness: isGlass ? shellDarkSidebarBorderLightness : darkSidebarBorderLightness,
343+
});
134344
} else {
135345
root.style.setProperty("--background", `oklch(${lightBgLightness} ${bgChroma} ${hue})`);
136346
root.style.setProperty("--accent", `oklch(${lightSurfaceLightness} ${surfaceChroma} ${hue})`);
@@ -145,20 +355,25 @@ export function useSpaceTheme(
145355
} else {
146356
root.style.removeProperty("--island-fill");
147357
}
148-
if (!isGlass) {
149-
root.style.setProperty("--sidebar", `oklch(${lightSidebarLightness} ${sidebarChroma} ${hue})`);
150-
root.style.setProperty("--sidebar-accent", `oklch(${lightSidebarAccentLightness} ${surfaceChroma} ${hue})`);
151-
root.style.setProperty("--sidebar-border", `oklch(${lightBorderLightness} ${borderChroma} ${hue})`);
152-
} else {
153-
// Glass + light: show more native glass while keeping a subtle space tint.
154-
root.style.setProperty("--sidebar", `oklch(1 ${sidebarChroma} ${hue} / ${0.22 + 0.12 * tintStrength})`);
155-
root.style.setProperty("--sidebar-accent", `oklch(${brightenLightLightness(0.965)} ${surfaceChroma} ${hue} / ${0.22 + 0.14 * tintStrength})`);
156-
root.style.setProperty("--sidebar-border", `oklch(0 ${borderChroma} ${hue} / ${0.08 + 0.08 * tintStrength})`);
157-
}
358+
applySidebarTokens(root, {
359+
isDark,
360+
isGlass,
361+
hue,
362+
tintStrength,
363+
sidebarChroma,
364+
surfaceChroma,
365+
borderChroma,
366+
sidebarLightness: isGlass ? shellLightSidebarLightness : lightSidebarLightness,
367+
sidebarAccentLightness: isGlass ? shellLightSidebarAccentLightness : lightSidebarAccentLightness,
368+
sidebarBorderLightness: isGlass ? shellLightSidebarBorderLightness : lightBorderLightness,
369+
});
158370
}
159371

160372
const gradientHue = space.color.gradientHue;
161373
const overlayChroma = Math.min(0.18, 0.04 + 0.12 * tintStrength);
374+
const overlayLightness = shouldDarkenTint
375+
? BASE_TINT_OVERLAY_LIGHTNESS * (1 - NON_NATIVE_TINT_DARKEN_FACTOR)
376+
: BASE_TINT_OVERLAY_LIGHTNESS;
162377

163378
// ── Glass tinting ──
164379
let releaseFocusTint: (() => void) | null = null;
@@ -172,8 +387,8 @@ export function useSpaceTheme(
172387
// Non-macOS glass (Windows Mica, etc.) — CSS overlay for tinting
173388
const a = 0.04 + 0.12 * tintStrength;
174389
const bg = gradientHue !== undefined
175-
? `linear-gradient(135deg, oklch(0.5 ${overlayChroma} ${hue} / ${a}), oklch(0.5 ${overlayChroma} ${gradientHue} / ${a}))`
176-
: `oklch(0.5 ${overlayChroma} ${hue} / ${a})`;
390+
? `linear-gradient(135deg, oklch(${overlayLightness} ${overlayChroma} ${hue} / ${a}), oklch(${overlayLightness} ${overlayChroma} ${gradientHue} / ${a}))`
391+
: `oklch(${overlayLightness} ${overlayChroma} ${hue} / ${a})`;
177392
setGlassOverlayStyle({ background: bg });
178393
} else {
179394
setGlassOverlayStyle(null);
@@ -184,7 +399,7 @@ export function useSpaceTheme(
184399
// Set CSS custom prop so .island::before picks up the gradient on ALL islands
185400
root.style.setProperty(
186401
"--island-overlay-bg",
187-
`linear-gradient(135deg, oklch(0.5 ${overlayChroma} ${hue} / ${a}), oklch(0.5 ${overlayChroma} ${gradientHue} / ${a}))`,
402+
`linear-gradient(135deg, oklch(${overlayLightness} ${overlayChroma} ${hue} / ${a}), oklch(${overlayLightness} ${overlayChroma} ${gradientHue} / ${a}))`,
188403
);
189404
} else {
190405
root.style.removeProperty("--island-overlay-bg");

0 commit comments

Comments
 (0)