Skip to content

Commit d3137b3

Browse files
committed
refactor(theme): rename Theme→ThemePalette, ColorMode→ColorTheme
Clarify the two independent axes of the theming system: - ThemePalette (default/cream/bloom): developer-configured brand palette - ColorTheme (light/dark/system): user-facing color scheme preference Rename internal variables, props, constants, and test IDs to align with the new terminology. Public hook API (useTheme → theme/resolvedTheme/setTheme) remains unchanged for backward compatibility.
1 parent a6b3da5 commit d3137b3

10 files changed

Lines changed: 162 additions & 155 deletions

File tree

.changeset/theme-mode-palette-axes.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ Introduce a **theme** (palette) system with built-in `default`, `cream`, and `bl
66

77
#### Color mode (end-user preference, persisted)
88

9-
- `useColorMode()` hook returns `{ mode, resolvedMode, setMode }`.
9+
- `useTheme()` hook continues to return `{ theme, resolvedTheme, setTheme }` (backward compatible).
1010
- `<AppShell defaultColorMode="system">` sets the initial preference; user choice is persisted to localStorage.
1111

12-
#### Theme (developer configuration, not persisted)
12+
#### Theme / Palette (developer configuration, not persisted)
1313

14-
- `useTheme()` hook returns `{ theme }` (read-only).
1514
- `<AppShell defaultTheme="default">` selects the brand palette; not stored in localStorage so a stale value never shadows the configured brand.
15+
- Palette is applied via `data-theme` attribute on `<html>`.
16+
17+
#### AppearanceSwitcher
18+
19+
- New `<AppearanceSwitcher />` component for toggling color mode.

examples/nextjs-app/src/app/dashboard/[[...props]]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const App = () => {
1313
const appShellConfig: AppShellProps = {
1414
title: "AppShell",
1515
basePath: "dashboard",
16-
defaultTheme: "cream",
16+
defaultThemePalette: "cream",
1717
modules: [customPageModule],
1818
rootComponent: () => <div>Dashboard Home (accessible at /dashboard)</div>,
1919
settingsResources: [profileResource],

examples/vite-app/src/App.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ const searchSources: SearchSource[] = [
2525

2626
const App = () => {
2727
return (
28-
<AppShell title="File-Based Routing Demo" searchSources={searchSources} defaultTheme="bloom">
28+
<AppShell
29+
title="File-Based Routing Demo"
30+
searchSources={searchSources}
31+
defaultThemePalette="bloom"
32+
>
2933
<SidebarLayout
3034
sidebar={
3135
<DefaultSidebar>

packages/core/src/components/appearance-switcher.test.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { cleanup, render, screen, waitFor } from "@testing-library/react";
22
import userEvent from "@testing-library/user-event";
33
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
44

5-
import { COLOR_MODE_OPTIONS, ThemeProvider } from "@/contexts/theme-context";
5+
import { COLOR_THEME_OPTIONS, ThemeProvider } from "@/contexts/theme-context";
66
import { AppearanceSwitcher } from "./appearance-switcher";
77

88
/** happy-dom / Node can omit a full `localStorage`; ThemeProvider persists via it. */
@@ -67,18 +67,18 @@ describe("AppearanceSwitcher", () => {
6767
const user = userEvent.setup();
6868

6969
render(
70-
<ThemeProvider defaultColorMode="light">
70+
<ThemeProvider defaultColorTheme="light">
7171
<AppearanceSwitcher />
7272
</ThemeProvider>,
7373
);
7474

7575
await user.click(screen.getByRole("button", { name: "Appearance" }));
7676

7777
await waitFor(() => {
78-
expect(screen.getAllByRole("menuitemradio").length).toBe(COLOR_MODE_OPTIONS.length);
78+
expect(screen.getAllByRole("menuitemradio").length).toBe(COLOR_THEME_OPTIONS.length);
7979
});
8080

81-
for (const opt of COLOR_MODE_OPTIONS) {
81+
for (const opt of COLOR_THEME_OPTIONS) {
8282
expect(screen.getByRole("menuitemradio", { name: opt.label })).toBeDefined();
8383
}
8484
});
@@ -104,7 +104,7 @@ describe("AppearanceSwitcher", () => {
104104

105105
it("exposes the resolved mode on the trigger when system mode is selected", () => {
106106
render(
107-
<ThemeProvider defaultColorMode="system">
107+
<ThemeProvider defaultColorTheme="system">
108108
<AppearanceSwitcher />
109109
</ThemeProvider>,
110110
);
@@ -118,7 +118,7 @@ describe("AppearanceSwitcher", () => {
118118
const user = userEvent.setup();
119119

120120
render(
121-
<ThemeProvider defaultColorMode="light">
121+
<ThemeProvider defaultColorTheme="light">
122122
<AppearanceSwitcher />
123123
</ThemeProvider>,
124124
);
@@ -134,6 +134,6 @@ describe("AppearanceSwitcher", () => {
134134
await waitFor(() => {
135135
expect(document.documentElement.classList.contains("dark")).toBe(true);
136136
});
137-
expect(localStorage.getItem("appshell-ui-mode")).toBe("dark");
137+
expect(localStorage.getItem("appshell-ui-theme")).toBe("dark");
138138
});
139139
});

packages/core/src/components/appearance-switcher.tsx

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,33 @@ import { Menu } from "@/components/menu";
44
import { Button } from "@/components/button";
55
import { cn } from "@/lib/utils";
66
import {
7-
useColorMode,
8-
type ResolvedColorMode,
9-
type ColorMode,
10-
COLOR_MODE_OPTIONS,
7+
useTheme,
8+
type ResolvedColorTheme,
9+
type ColorTheme,
10+
COLOR_THEME_OPTIONS,
1111
} from "@/contexts/theme-context";
1212

13-
const RESOLVED_MODE_SHORT: Record<ResolvedColorMode, string> = {
13+
const RESOLVED_COLOR_THEME_SHORT: Record<ResolvedColorTheme, string> = {
1414
light: "Light",
1515
dark: "Dark",
1616
};
1717

1818
/**
19-
* Decorative dual swatches for the mode picker. Light/dark show their surface
19+
* Decorative dual swatches for the color theme picker. Light/dark show their surface
2020
* tones; system splits the two to signal "follows the OS".
2121
* Kept as static hex previews so they render before any stylesheet loads.
2222
*/
23-
const MODE_PREVIEW: Record<ColorMode, { readonly a: `#${string}`; readonly b: `#${string}` }> = {
23+
const COLOR_THEME_PREVIEW: Record<
24+
ColorTheme,
25+
{ readonly a: `#${string}`; readonly b: `#${string}` }
26+
> = {
2427
light: { a: "#ffffff", b: "#d4d4d8" },
2528
dark: { a: "#3f3f46", b: "#71717a" },
2629
system: { a: "#ffffff", b: "#3f3f46" },
2730
};
2831

29-
function isMode(value: string): value is ColorMode {
30-
return COLOR_MODE_OPTIONS.some((o) => o.value === value);
32+
function isColorTheme(value: string): value is ColorTheme {
33+
return COLOR_THEME_OPTIONS.some((o) => o.value === value);
3134
}
3235

3336
/** Shared radio-item chrome — used by both the mode and font grids. */
@@ -42,8 +45,8 @@ function radioItemClasses(active: boolean) {
4245
);
4346
}
4447

45-
function ModePreviewSwatches({ modeId }: { modeId: ColorMode }) {
46-
const { a, b } = MODE_PREVIEW[modeId];
48+
function ColorThemePreviewSwatches({ colorTheme }: { colorTheme: ColorTheme }) {
49+
const { a, b } = COLOR_THEME_PREVIEW[colorTheme];
4750
return (
4851
<div
4952
className="astw:flex astw:h-10 astw:w-full astw:max-w-[5.5rem] astw:items-center astw:justify-center astw:gap-1.5 astw:rounded-md astw:border astw:border-border/80 astw:bg-card astw:px-2"
@@ -62,15 +65,15 @@ function ModePreviewSwatches({ modeId }: { modeId: ColorMode }) {
6265
}
6366

6467
/**
65-
* Appearance menu. The end-user controls **mode** (light / dark / system);
68+
* Appearance menu. The end-user controls **color theme** (light / dark / system);
6669
* the color palette/brand is a developer configuration, so it is not shown here.
6770
*/
6871
function AppearanceSwitcher() {
69-
const { mode, resolvedMode, setMode } = useColorMode();
72+
const { theme, resolvedTheme, setTheme } = useTheme();
7073

7174
const triggerTitle =
72-
mode === "system"
73-
? `Following system — currently ${RESOLVED_MODE_SHORT[resolvedMode]}`
75+
theme === "system"
76+
? `Following system — currently ${RESOLVED_COLOR_THEME_SHORT[resolvedTheme]}`
7477
: "Choose appearance";
7578

7679
return (
@@ -97,21 +100,21 @@ function AppearanceSwitcher() {
97100
<Menu.GroupLabel className="astw:px-0 astw:pb-2 astw:pt-0">Appearance</Menu.GroupLabel>
98101
<Menu.RadioGroup
99102
className="astw:grid astw:grid-cols-3 astw:gap-2"
100-
value={mode}
103+
value={theme}
101104
onValueChange={(value) => {
102-
if (typeof value === "string" && isMode(value)) setMode(value);
105+
if (typeof value === "string" && isColorTheme(value)) setTheme(value);
103106
}}
104107
>
105-
{COLOR_MODE_OPTIONS.map((opt) => (
108+
{COLOR_THEME_OPTIONS.map((opt) => (
106109
<Menu.RadioItem
107110
key={opt.value}
108111
value={opt.value}
109-
className={radioItemClasses(mode === opt.value)}
112+
className={radioItemClasses(theme === opt.value)}
110113
>
111114
<Menu.RadioItemIndicator className="astw:sr-only">
112115
{opt.label}
113116
</Menu.RadioItemIndicator>
114-
<ModePreviewSwatches modeId={opt.value} />
117+
<ColorThemePreviewSwatches colorTheme={opt.value} />
115118
<span className="astw:block astw:w-full astw:truncate astw:px-0.5">
116119
{opt.label}
117120
</span>

packages/core/src/components/appshell.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
type ContextData,
1818
} from "@/contexts/appshell-context";
1919
import { RouterContainer } from "@/routing/router";
20-
import { ThemeProvider, type ColorMode, type Theme } from "@/contexts/theme-context";
20+
import { ThemeProvider, type ColorTheme, type ThemePalette } from "@/contexts/theme-context";
2121
import { BreadcrumbOverrideProvider } from "@/contexts/breadcrumb-context";
2222
import { CommandPaletteProvider, type SearchSource } from "@/contexts/command-palette-context";
2323
import { BuiltInCommandPalette } from "@/components/command-palette";
@@ -179,25 +179,25 @@ type SharedAppShellProps = React.PropsWithChildren<{
179179
searchSources?: readonly SearchSource[];
180180

181181
/**
182-
* Initial color mode before any value is loaded from localStorage (`appshell-ui-mode`).
182+
* Initial color mode before any value is loaded from localStorage (`appshell-ui-theme`).
183183
* This is the end-user accessibility preference; does not replace a stored preference.
184184
*
185185
* One of **`light`**, **`dark`**, or **`system`** (follows the OS). Works with any palette.
186186
*
187187
* @default "system"
188188
*/
189-
defaultColorMode?: ColorMode;
189+
defaultColorTheme?: ColorTheme;
190190

191191
/**
192192
* Color palette / brand (developer configuration), applied as `data-theme`.
193193
* One of **`default`**, **`cream`**, or **`bloom`** — each ships both light and dark
194-
* variants, so the active variant follows {@link AppShellProps.defaultColorMode}.
194+
* variants, so the active variant follows {@link AppShellProps.defaultColorTheme}.
195195
* This is set by the product (not a user-facing picker), so it is driven by this
196196
* prop and not persisted to localStorage.
197197
*
198198
* @default "default"
199199
*/
200-
defaultTheme?: Theme;
200+
defaultThemePalette?: ThemePalette;
201201
}>;
202202

203203
/**
@@ -342,8 +342,8 @@ export const AppShell = (props: AppShellProps) => {
342342
<BreadcrumbOverrideProvider>
343343
<CommandPaletteProvider searchSources={props.searchSources}>
344344
<ThemeProvider
345-
defaultColorMode={props.defaultColorMode}
346-
defaultTheme={props.defaultTheme}
345+
defaultColorTheme={props.defaultColorTheme}
346+
defaultThemePalette={props.defaultThemePalette}
347347
>
348348
<RouterContainer>
349349
{props.children}

packages/core/src/components/sonner.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { useColorMode } from "@/contexts/theme-context";
1+
import { useTheme } from "@/contexts/theme-context";
22
import { Toaster as Sonner, ToasterProps } from "sonner";
33

44
const Toaster = ({ ...props }: ToasterProps) => {
5-
const { resolvedMode } = useColorMode();
5+
const { resolvedTheme } = useTheme();
66

77
return (
88
<Sonner
9-
theme={resolvedMode as ToasterProps["theme"]}
9+
theme={resolvedTheme as ToasterProps["theme"]}
1010
className="astw:toaster astw:group"
1111
style={
1212
{

0 commit comments

Comments
 (0)