fix: make goose2 respect accent color (#8952)

This commit is contained in:
Kalvin C
2026-05-01 13:35:06 -07:00
committed by GitHub
parent 936e019f0e
commit 9b1780bde5
13 changed files with 377 additions and 65 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ Guidelines for AI agents (and developers) working on this codebase.
## Project Overview
Goose2 is a Tauri 2 + React 19 desktop app. It uses TypeScript strict mode, Vite, and Tailwind CSS 3. The codebase follows a feature-sliced architecture organized under `src/app/`, `src/features/`, and `src/shared/`.
Goose2 is a Tauri 2 + React 19 desktop app. It uses TypeScript strict mode, Vite, and Tailwind CSS 4. The codebase follows a feature-sliced architecture organized under `src/app/`, `src/features/`, and `src/shared/`.
## First Steps
@@ -99,7 +99,7 @@ ThemeProvider manages three axes:
| Axis | Values | Persistence | Mechanism |
|--------------|---------------------------------|-----------------|----------------------------------------------|
| Theme mode | `light`, `dark`, `system` | localStorage | `.dark` class on `<html>` |
| Accent color | Any hex value | localStorage | `--color-accent` CSS variable |
| Accent color | Hex color | localStorage | `--brand` / `--color-brand` CSS variables |
| Density | `compact`, `comfortable`, `spacious` | localStorage | `--density-spacing` CSS variable (0.75/1/1.25) |
- CSS variables are defined in `globals.css` with light/dark variants.
+80
View File
@@ -4,6 +4,86 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script>
(() => {
const getDefaultAccent = (theme) =>
theme === "dark" ? "#ffffff" : "#1a1a1a";
const normalizeHexColor = (color) => {
const value = color?.trim();
if (!value || value === "default") return null;
const hex = value.startsWith("#") ? value.slice(1) : value;
if (/^[0-9a-fA-F]{3}$/.test(hex)) {
return `#${hex
.split("")
.map((char) => char + char)
.join("")
.toLowerCase()}`;
}
if (/^[0-9a-fA-F]{6}$/.test(hex)) {
return `#${hex.toLowerCase()}`;
}
return null;
};
const getRelativeLuminance = (hexColor) => {
const hex = hexColor.slice(1);
const channels = [hex.slice(0, 2), hex.slice(2, 4), hex.slice(4, 6)]
.map((channel) => {
const value = Number.parseInt(channel, 16) / 255;
return value <= 0.04045
? value / 12.92
: ((value + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
};
const getContrastColor = (hexColor) => {
const luminance = getRelativeLuminance(hexColor);
const blackContrast = (luminance + 0.05) / 0.05;
const whiteContrast = 1.05 / (luminance + 0.05);
return blackContrast >= whiteContrast ? "#000000" : "#ffffff";
};
try {
const root = document.documentElement;
const storedTheme = localStorage.getItem("goose-theme") || "system";
const resolvedTheme =
storedTheme === "system"
? window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
: storedTheme;
const accent =
normalizeHexColor(localStorage.getItem("goose-accent-color")) ||
getDefaultAccent(resolvedTheme);
const foreground = getContrastColor(accent);
const density = localStorage.getItem("goose-density") || "comfortable";
const spacingScale = {
compact: "0.75",
comfortable: "1",
spacious: "1.25",
};
root.classList.add(resolvedTheme === "dark" ? "dark" : "light");
root.style.colorScheme = resolvedTheme === "dark" ? "dark" : "light";
root.style.setProperty("--brand", accent);
root.style.setProperty("--brand-foreground", foreground);
root.style.setProperty("--color-brand", accent);
root.style.setProperty("--color-brand-foreground", foreground);
root.style.accentColor = accent;
root.style.setProperty(
"--density-spacing",
spacingScale[density] || spacingScale.comfortable,
);
} catch {
// ThemeProvider applies the canonical theme state after React mounts.
}
})();
</script>
<title>Goose</title>
</head>
<body>
@@ -192,8 +192,8 @@ export function AvatarDropZone({
className={cn(
"size-16 overflow-hidden border-2 bg-muted shadow-sm",
isDragOver
? "scale-105 border-accent bg-accent/15 shadow-md ring-4 ring-accent/20"
: "border-border hover:border-border hover:bg-accent",
? "scale-105 border-brand bg-brand/10 shadow-md ring-4 ring-brand/20"
: "border-border hover:border-brand/50 hover:bg-brand/10",
disabled && "opacity-70 cursor-not-allowed",
isUploading && "animate-pulse",
)}
@@ -397,7 +397,7 @@ export function AgentProviderCard({ provider }: AgentProviderCardProps) {
return (
<div className="mt-3 space-y-2 border-t pt-3">
<div className="flex items-center gap-2">
<Spinner className="size-3.5 text-accent" />
<Spinner className="size-3.5 text-brand" />
<div className="min-w-0 flex-1">
<span className="text-xs font-medium">{phaseLabel}</span>
{stepInfo && (
@@ -420,7 +420,7 @@ export function AgentProviderCard({ provider }: AgentProviderCardProps) {
<div
className={cn(
"flex flex-col rounded-lg border bg-background p-3 transition-colors",
isActive && "border-accent/50",
isActive && "border-brand/50 bg-brand/10",
)}
>
<div className="flex items-start justify-between">
@@ -451,7 +451,7 @@ export function AgentProviderCard({ provider }: AgentProviderCardProps) {
setupError
? "bg-danger"
: isActive
? "bg-accent animate-pulse"
? "bg-brand animate-pulse"
: "bg-muted-foreground/40",
)}
/>
@@ -20,7 +20,6 @@ const ACCENT_COLORS = [
{ name: "red", value: "#ef4444" },
{ name: "pink", value: "#ec4899" },
{ name: "purple", value: "#a855f7" },
{ name: "indigo", value: "#6366f1" },
];
const DENSITY_OPTIONS = [
@@ -53,8 +52,15 @@ function SettingRow({
export function AppearanceSettings() {
const { t } = useTranslation("settings");
const { theme, setTheme, accentColor, setAccentColor, density, setDensity } =
useTheme();
const {
theme,
setTheme,
accentColorPreference,
resetAccentColor,
setAccentColor,
density,
setDensity,
} = useTheme();
return (
<SettingsPage title={t("appearance.title")}>
@@ -87,21 +93,38 @@ export function AppearanceSettings() {
label={t("appearance.accent.label")}
description={t("appearance.accent.description")}
>
<div className="grid grid-cols-4 gap-2">
<div className="flex max-w-36 flex-wrap justify-end gap-2">
<button
type="button"
title={t("appearance.accent.colors.default")}
aria-label={t("appearance.accent.colors.default")}
onClick={resetAccentColor}
className={cn(
"relative flex h-7 w-7 items-center justify-center overflow-hidden rounded-full border border-border transition-transform hover:scale-110",
accentColorPreference === "default" &&
"ring-2 ring-ring ring-offset-2 ring-offset-background",
)}
>
<span className="absolute inset-0 bg-[linear-gradient(135deg,#1a1a1a_0_50%,#ffffff_50%_100%)]" />
{accentColorPreference === "default" && (
<Check className="relative h-4 w-4 rounded-full bg-background p-0.5 text-foreground shadow-sm" />
)}
</button>
{ACCENT_COLORS.map((color) => (
<button
type="button"
key={color.value}
title={t(`appearance.accent.colors.${color.name}`)}
aria-label={t(`appearance.accent.colors.${color.name}`)}
onClick={() => setAccentColor(color.value)}
className={cn(
"flex h-7 w-7 items-center justify-center rounded-full transition-transform hover:scale-110",
accentColor === color.value &&
accentColorPreference === color.value &&
"ring-2 ring-ring ring-offset-2 ring-offset-background",
)}
style={{ backgroundColor: color.value }}
>
{accentColor === color.value && (
{accentColorPreference === color.value && (
<Check className="h-3.5 w-3.5 text-white" />
)}
</button>
@@ -33,7 +33,7 @@ export function InventorySyncMessage({
role="status"
className="flex items-center gap-2 text-xs text-muted-foreground"
>
<IconLoader2 className="size-3 animate-spin text-accent" />
<IconLoader2 className="size-3 animate-spin text-brand" />
<span>{t("providers.loadingModels")}</span>
</p>
);
@@ -377,7 +377,7 @@ export function ModelProviderRow({
)}
{authenticating ? (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Spinner className="size-3.5 text-accent" />
<Spinner className="size-3.5 text-brand" />
<span>{t("providers.waitingForSignIn")}</span>
</div>
) : null}
@@ -486,10 +486,10 @@ export function ModelProviderRow({
<IconCheck className="size-4 flex-shrink-0 text-success" />
) : null}
{inventorySyncing ? (
<Spinner className="size-3.5 flex-shrink-0 text-accent" />
<Spinner className="size-3.5 flex-shrink-0 text-brand" />
) : null}
{!isConnected && authenticating ? (
<Spinner className="size-3.5 flex-shrink-0 text-accent" />
<Spinner className="size-3.5 flex-shrink-0 text-brand" />
) : null}
</button>
@@ -349,7 +349,7 @@ export function ProvidersSettings() {
</h4>
{loading ? (
<span className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
<Spinner className="size-3 text-accent" />
<Spinner className="size-3 text-brand" />
{t("providers.models.checkingStatus")}
</span>
) : null}
@@ -20,8 +20,8 @@
"colors": {
"blue": "Blue",
"cyan": "Cyan",
"default": "Default",
"green": "Green",
"indigo": "Indigo",
"orange": "Orange",
"pink": "Pink",
"purple": "Purple",
@@ -20,8 +20,8 @@
"colors": {
"blue": "Azul",
"cyan": "Cian",
"default": "Predeterminado",
"green": "Verde",
"indigo": "Índigo",
"orange": "Naranja",
"pink": "Rosa",
"purple": "Morado",
+30 -26
View File
@@ -85,9 +85,11 @@
--radius: 20px;
/* theming accents */
--background-accent: var(--color-gray-900);
--border-accent: var(--color-gray-900);
--text-accent: var(--color-gray-900);
--brand: var(--color-gray-900);
--brand-foreground: #ffffff;
--background-accent: var(--brand);
--border-accent: var(--brand);
--text-accent: var(--brand);
/* Semantic backgrounds */
--background-default: var(--color-white);
@@ -130,7 +132,7 @@
--text-info: var(--color-blue-200);
--text-placeholder: var(--color-gray-400);
--ring: color-mix(in srgb, var(--border-strong) 20%, transparent);
--ring: color-mix(in oklab, var(--brand) 42%, transparent);
/* Alpha variants */
--dark-10: rgba(26, 26, 26, 0.1);
@@ -168,13 +170,13 @@
--card-foreground: var(--text-default);
--popover: var(--background-default);
--popover-foreground: var(--text-default);
--primary: var(--background-accent);
--primary-foreground: var(--text-inverse);
--primary: var(--brand);
--primary-foreground: var(--brand-foreground);
--secondary: var(--background-muted);
--secondary-foreground: var(--text-default);
--muted: var(--background-muted);
--muted-foreground: var(--text-muted);
--accent: var(--background-muted);
--accent: color-mix(in oklab, var(--brand) 12%, var(--background-muted));
--accent-foreground: var(--text-default);
--destructive: var(--background-danger);
--destructive-foreground: #ffffff;
@@ -184,9 +186,9 @@
/* Sidebar */
--sidebar: var(--background-default);
--sidebar-foreground: var(--text-default);
--sidebar-primary: var(--background-accent);
--sidebar-primary-foreground: var(--text-inverse);
--sidebar-accent: var(--background-muted);
--sidebar-primary: var(--brand);
--sidebar-primary-foreground: var(--brand-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--text-default);
--sidebar-border: var(--border-default);
--sidebar-ring: var(--border-default);
@@ -247,16 +249,18 @@
--duration-slow: 0.4s;
/* goose2 custom — brand accent + density */
--color-brand: #3b82f6;
--color-brand-foreground: #ffffff;
--color-brand: var(--brand);
--color-brand-foreground: var(--brand-foreground);
--density-spacing: 1;
}
.dark {
/* theming accents */
--background-accent: var(--color-white);
--border-accent: var(--color-white);
--text-accent: var(--color-white);
--brand: var(--color-white);
--brand-foreground: #000000;
--background-accent: var(--brand);
--border-accent: var(--brand);
--text-accent: var(--brand);
/* shadcn/ui standard tokens — same aliases, dark values cascade via custom vars */
--background: var(--background-default);
@@ -265,13 +269,13 @@
--card-foreground: var(--text-default);
--popover: var(--background-default);
--popover-foreground: var(--text-default);
--primary: var(--background-accent);
--primary-foreground: var(--text-inverse);
--primary: var(--brand);
--primary-foreground: var(--brand-foreground);
--secondary: var(--background-muted);
--secondary-foreground: var(--text-default);
--muted: var(--background-muted);
--muted-foreground: var(--text-muted);
--accent: var(--background-muted);
--accent: color-mix(in oklab, var(--brand) 18%, var(--background-muted));
--accent-foreground: var(--text-default);
--destructive: var(--background-danger);
--destructive-foreground: #ffffff;
@@ -319,7 +323,7 @@
--text-info: var(--color-blue-100);
--text-placeholder: var(--color-gray-600);
--ring: color-mix(in srgb, var(--border-strong) 20%, transparent);
--ring: color-mix(in oklab, var(--brand) 52%, transparent);
/* Alpha variants (dark mode) */
--dark-10: rgba(242, 242, 242, 0.1);
@@ -347,9 +351,9 @@
/* Sidebar */
--sidebar: var(--background-default);
--sidebar-foreground: var(--text-default);
--sidebar-primary: var(--background-accent);
--sidebar-primary-foreground: var(--text-inverse);
--sidebar-accent: var(--background-muted);
--sidebar-primary: var(--brand);
--sidebar-primary-foreground: var(--brand-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--text-default);
--sidebar-border: var(--border-default);
--sidebar-ring: var(--border-default);
@@ -379,8 +383,8 @@
--color-input: var(--input);
/* brand accent */
--color-brand: var(--color-brand);
--color-brand-foreground: var(--color-brand-foreground);
--color-brand: var(--brand);
--color-brand-foreground: var(--brand-foreground);
/* danger (alias for destructive) */
--color-danger: var(--destructive);
@@ -731,8 +735,8 @@ body,
}
::selection {
background: color-mix(in srgb, var(--color-brand) 60%, transparent);
color: #ffffff;
background: color-mix(in oklab, var(--brand) 60%, transparent);
color: var(--brand-foreground);
}
::-webkit-scrollbar {
@@ -4,11 +4,20 @@ import { describe, it, expect, beforeEach } from "vitest";
import { ThemeProvider, useTheme } from "./ThemeProvider";
function ThemeConsumer() {
const { theme, setTheme, accentColor, density } = useTheme();
const {
theme,
setTheme,
accentColor,
accentColorPreference,
setAccentColor,
resetAccentColor,
density,
} = useTheme();
return (
<div>
<span data-testid="theme">{theme}</span>
<span data-testid="accent">{accentColor}</span>
<span data-testid="accent-preference">{accentColorPreference}</span>
<span data-testid="density">{density}</span>
<button type="button" onClick={() => setTheme("dark")}>
Set Dark
@@ -16,6 +25,18 @@ function ThemeConsumer() {
<button type="button" onClick={() => setTheme("light")}>
Set Light
</button>
<button type="button" onClick={() => setAccentColor("#f97316")}>
Set Orange
</button>
<button type="button" onClick={() => setAccentColor("#fff")}>
Set White
</button>
<button type="button" onClick={() => setAccentColor("red")}>
Set Invalid
</button>
<button type="button" onClick={resetAccentColor}>
Reset Accent
</button>
</div>
);
}
@@ -24,6 +45,7 @@ describe("ThemeProvider", () => {
beforeEach(() => {
localStorage.clear();
document.documentElement.classList.remove("light", "dark");
document.documentElement.removeAttribute("style");
});
it("provides default theme as system", () => {
@@ -64,7 +86,113 @@ describe("ThemeProvider", () => {
<ThemeConsumer />
</ThemeProvider>,
);
expect(screen.getByTestId("accent")).toHaveTextContent("#3b82f6");
expect(screen.getByTestId("accent")).toHaveTextContent("#1a1a1a");
expect(screen.getByTestId("accent-preference")).toHaveTextContent(
"default",
);
});
it("applies accent color tokens to the document", async () => {
const user = userEvent.setup();
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>,
);
await user.click(screen.getByText("Set Orange"));
expect(localStorage.getItem("goose-accent-color")).toBe("#f97316");
expect(document.documentElement.style.getPropertyValue("--brand")).toBe(
"#f97316",
);
expect(
document.documentElement.style.getPropertyValue("--brand-foreground"),
).toBe("#000000");
expect(
document.documentElement.style.getPropertyValue("--color-brand"),
).toBe("#f97316");
expect(
document.documentElement.style.getPropertyValue(
"--color-brand-foreground",
),
).toBe("#000000");
expect(document.documentElement.style.accentColor).toBe(
"rgb(249, 115, 22)",
);
});
it("normalizes and validates accent colors", async () => {
const user = userEvent.setup();
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>,
);
await user.click(screen.getByText("Set White"));
expect(localStorage.getItem("goose-accent-color")).toBe("#ffffff");
expect(
document.documentElement.style.getPropertyValue("--color-brand"),
).toBe("#ffffff");
expect(
document.documentElement.style.getPropertyValue(
"--color-brand-foreground",
),
).toBe("#000000");
await user.click(screen.getByText("Set Invalid"));
expect(localStorage.getItem("goose-accent-color")).toBeNull();
expect(screen.getByTestId("accent-preference")).toHaveTextContent(
"default",
);
expect(
document.documentElement.style.getPropertyValue("--color-brand"),
).toBe("#1a1a1a");
});
it("resets custom accent colors to the theme default", async () => {
const user = userEvent.setup();
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>,
);
await user.click(screen.getByText("Set Orange"));
await user.click(screen.getByText("Reset Accent"));
expect(localStorage.getItem("goose-accent-color")).toBeNull();
expect(screen.getByTestId("accent")).toHaveTextContent("#1a1a1a");
expect(screen.getByTestId("accent-preference")).toHaveTextContent(
"default",
);
expect(
document.documentElement.style.getPropertyValue("--color-brand"),
).toBe("#1a1a1a");
});
it("updates the default accent color with the theme", async () => {
const user = userEvent.setup();
render(
<ThemeProvider>
<ThemeConsumer />
</ThemeProvider>,
);
await user.click(screen.getByText("Set Dark"));
expect(screen.getByTestId("accent")).toHaveTextContent("#ffffff");
expect(
document.documentElement.style.getPropertyValue("--color-brand"),
).toBe("#ffffff");
expect(
document.documentElement.style.getPropertyValue(
"--color-brand-foreground",
),
).toBe("#000000");
});
it("provides default density", () => {
+94 -17
View File
@@ -14,6 +14,8 @@ type ThemeProviderState = {
resolvedTheme: ResolvedTheme;
setTheme: (theme: ThemePreference) => void;
accentColor: string;
accentColorPreference: string;
resetAccentColor: () => void;
setAccentColor: (color: string) => void;
density: Density;
setDensity: (d: Density) => void;
@@ -23,6 +25,10 @@ const ThemeProviderContext = React.createContext<
ThemeProviderState | undefined
>(undefined);
const DEFAULT_ACCENT_COLOR_PREFERENCE = "default";
const DEFAULT_LIGHT_ACCENT_COLOR = "#1a1a1a";
const DEFAULT_DARK_ACCENT_COLOR = "#ffffff";
function resolveTheme(preference: ThemePreference): ResolvedTheme {
if (preference === "system") {
return window.matchMedia("(prefers-color-scheme: dark)").matches
@@ -32,13 +38,59 @@ function resolveTheme(preference: ThemePreference): ResolvedTheme {
return preference;
}
function getDefaultAccentColor(theme: ResolvedTheme): string {
return theme === "dark"
? DEFAULT_DARK_ACCENT_COLOR
: DEFAULT_LIGHT_ACCENT_COLOR;
}
function normalizeHexColor(color: string | null): string | null {
const value = color?.trim();
if (!value || value === DEFAULT_ACCENT_COLOR_PREFERENCE) return null;
const hex = value.startsWith("#") ? value.slice(1) : value;
if (/^[0-9a-fA-F]{3}$/.test(hex)) {
return `#${hex
.split("")
.map((char) => char + char)
.join("")
.toLowerCase()}`;
}
if (/^[0-9a-fA-F]{6}$/.test(hex)) {
return `#${hex.toLowerCase()}`;
}
return null;
}
function getRelativeLuminance(hexColor: string): number {
const hex = hexColor.slice(1);
const channels = [hex.slice(0, 2), hex.slice(2, 4), hex.slice(4, 6)].map(
(channel) => {
const value = Number.parseInt(channel, 16) / 255;
return value <= 0.04045
? value / 12.92
: ((value + 0.055) / 1.055) ** 2.4;
},
);
return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
}
function getContrastColor(hexColor: string): string {
const hex = hexColor.replace("#", "");
const r = Number.parseInt(hex.slice(0, 2), 16);
const g = Number.parseInt(hex.slice(2, 4), 16);
const b = Number.parseInt(hex.slice(4, 6), 16);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.5 ? "#000000" : "#ffffff";
const luminance = getRelativeLuminance(hexColor);
const blackContrast = (luminance + 0.05) / 0.05;
const whiteContrast = 1.05 / (luminance + 0.05);
return blackContrast >= whiteContrast ? "#000000" : "#ffffff";
}
function applyAccentColor(root: HTMLElement, color: string) {
const foreground = getContrastColor(color);
root.style.setProperty("--brand", color);
root.style.setProperty("--brand-foreground", foreground);
root.style.setProperty("--color-brand", color);
root.style.setProperty("--color-brand-foreground", foreground);
root.style.accentColor = color;
}
export function ThemeProvider({
@@ -56,23 +108,45 @@ export function ThemeProvider({
resolveTheme(theme),
);
const [accentColor, setAccentColorState] = React.useState<string>(() => {
return localStorage.getItem("goose-accent-color") ?? "#3b82f6";
});
const [accentColorPreference, setAccentColorPreference] =
React.useState<string>(() => {
return (
normalizeHexColor(localStorage.getItem("goose-accent-color")) ??
DEFAULT_ACCENT_COLOR_PREFERENCE
);
});
const [density, setDensityState] = React.useState<Density>(() => {
const stored = localStorage.getItem("goose-density") as Density | null;
return stored ?? "comfortable";
});
const accentColor = React.useMemo(() => {
return accentColorPreference === DEFAULT_ACCENT_COLOR_PREFERENCE
? getDefaultAccentColor(resolvedTheme)
: accentColorPreference;
}, [accentColorPreference, resolvedTheme]);
const setTheme = React.useCallback((newTheme: ThemePreference) => {
localStorage.setItem("goose-theme", newTheme);
setThemeState(newTheme);
}, []);
const setAccentColor = React.useCallback((color: string) => {
localStorage.setItem("goose-accent-color", color);
setAccentColorState(color);
const normalizedColor = normalizeHexColor(color);
if (!normalizedColor) {
localStorage.removeItem("goose-accent-color");
setAccentColorPreference(DEFAULT_ACCENT_COLOR_PREFERENCE);
return;
}
localStorage.setItem("goose-accent-color", normalizedColor);
setAccentColorPreference(normalizedColor);
}, []);
const resetAccentColor = React.useCallback(() => {
localStorage.removeItem("goose-accent-color");
setAccentColorPreference(DEFAULT_ACCENT_COLOR_PREFERENCE);
}, []);
const setDensity = React.useCallback((d: Density) => {
@@ -105,19 +179,18 @@ export function ThemeProvider({
React.useEffect(() => {
const root = window.document.documentElement;
root.style.setProperty("--color-brand", accentColor);
root.style.setProperty(
"--color-brand-foreground",
getContrastColor(accentColor),
);
applyAccentColor(root, accentColor);
}, [accentColor]);
React.useEffect(() => {
const root = window.document.documentElement;
const spacingScale: Record<Density, string> = {
compact: "0.75",
comfortable: "1",
spacious: "1.25",
};
root.style.setProperty("--density-spacing", spacingScale[density]);
}, [accentColor, density]);
}, [density]);
const value = React.useMemo(
() => ({
@@ -125,6 +198,8 @@ export function ThemeProvider({
resolvedTheme,
setTheme,
accentColor,
accentColorPreference,
resetAccentColor,
setAccentColor,
density,
setDensity,
@@ -134,6 +209,8 @@ export function ThemeProvider({
resolvedTheme,
setTheme,
accentColor,
accentColorPreference,
resetAccentColor,
setAccentColor,
density,
setDensity,