Skip to content

feat(theme): split theming into mode and palette axes#313

Merged
IzumiSy merged 4 commits into
feat/theme-foundationfrom
feat/theme-mode-axis
Jun 15, 2026
Merged

feat(theme): split theming into mode and palette axes#313
IzumiSy merged 4 commits into
feat/theme-foundationfrom
feat/theme-mode-axis

Conversation

@itsprade

@itsprade itsprade commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Splits theming into two independent axesmode (light / dark / system) and theme/palette (default / cream / bloom) — replacing the old single flat Theme union. This is the runtime/API layer; the CSS and component work follow in stacked PRs.

Stack: this PR → feat/theme-css-tokensfeat/theme-component-surfaces, all targeting feat/theme-foundation.

What changed

  • useColorMode() (new) → { mode, resolvedMode, setMode } — the end-user accessibility preference (light/dark/system), persisted under appshell-ui-mode, system-aware via matchMedia.
  • useTheme() now returns the palette{ theme, setTheme } (default/cream/bloom), a developer config. The palette is prop-driven, not persisted, so a stale stored value can't shadow the configured brand.
  • COLOR_MODE_OPTIONS exported; ResolvedTheme removed in favour of ResolvedColorMode.
  • DOM: <html> carries mode as the .light / .dark class and palette as data-theme. system resolves the mode axis only, so any palette follows the OS.
  • <AppShell> gains defaultColorMode (default "system"); defaultTheme now selects the palette (default "default"); defaultFont unchanged.
  • ThemeSwitcher appearance control now shows Light / Dark / System; the Font group is unchanged. Palette is not a user-facing picker.
  • Font axis is untouched (useFont, FONT_OPTIONS, data-font).

Files

  • contexts/theme-context.tsx (+ test) — two-axis model, useColorMode/useTheme.
  • components/theme-switcher.tsx (+ test) — mode group.
  • components/appshell.tsxdefaultColorMode / palette defaultTheme.
  • index.ts — exports (useColorMode, ColorMode, ResolvedColorMode, COLOR_MODE_OPTIONS; Theme = palette).
  • examples/* — set defaultTheme on the demo apps (Cream / Bloom).
  • .changeset/theme-mode-palette-axes.md.

Notes

  • This PR's DOM model (.dark class + data-theme) is what the CSS PR builds on; in isolation the existing theme CSS still applies, so it builds & tests green.
  • Verified: type-check clean, 16/16 theme-context + theme-switcher tests pass.

🤖 Generated with Claude Code

itsprade and others added 2 commits June 15, 2026 11:23
Introduce two independent theming axes: mode (light/dark/system) as the
end-user accessibility preference via useMode, and palette/theme
(default/cream/bloom) as a developer configuration via useTheme. Mode is
persisted; palette is prop-driven. The <html> element carries mode as the
.light/.dark class and palette as data-theme. The font axis is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Merge light.css and dark.css into a single default.css holding both the
light :root block and the .dark override. Organize every theme block
(default, cream, bloom) into tiers: BRAND (primary/secondary + foregrounds,
shell-gradient — the only re-skin knobs), DERIVED (accent/ring/sidebar-*
via color-mix on var(--primary)), SYSTEM (neutral surfaces, accessibility-
locked), and STATUS (semantic, fixed). Give cream and bloom dark variants
that inherit default's status colors. Decouple the shell gradient from
--background via a dedicated --shell-gradient-base token.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@IzumiSy IzumiSy force-pushed the feat/theme-mode-axis branch from 90ed214 to a812837 Compare June 15, 2026 02:23

@IzumiSy IzumiSy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot see review comments

Comment thread packages/core/src/components/appshell.tsx Outdated
Comment thread packages/core/src/contexts/theme-context.tsx Outdated
Comment thread packages/core/src/contexts/theme-context.tsx Outdated
Comment thread packages/core/src/contexts/theme-context.tsx Outdated
Comment thread packages/core/src/contexts/theme-context.tsx Outdated
Comment thread packages/core/src/contexts/theme-context.tsx Outdated
Copilot AI and others added 2 commits June 15, 2026 02:29
Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>
Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>

Copilot AI commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

@copilot see review comments

Addressed in cc9ed83.

Renamed the requested APIs/props to color-mode-specific names:

  • defaultMode -> defaultColorMode
  • useMode -> useColorMode
  • Mode -> ColorMode
  • ResolvedMode -> ResolvedColorMode
  • MODE_OPTIONS -> COLOR_MODE_OPTIONS
  • ModeOption -> ColorModeOption

Updated dependent core usages/tests and the related changeset entry.

Copilot AI requested a review from IzumiSy June 15, 2026 02:33
@IzumiSy IzumiSy merged commit 896680f into feat/theme-foundation Jun 15, 2026
4 checks passed
@IzumiSy IzumiSy deleted the feat/theme-mode-axis branch June 15, 2026 02:56
IzumiSy added a commit that referenced this pull request Jun 15, 2026
* feat(theme): split theming into mode and palette axes

Introduce two independent theming axes: mode (light/dark/system) as the
end-user accessibility preference via useMode, and palette/theme
(default/cream/bloom) as a developer configuration via useTheme. Mode is
persisted; palette is prop-driven. The <html> element carries mode as the
.light/.dark class and palette as data-theme. The font axis is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(theme): merge light/dark CSS and add brand-token tiers (#314)

Merge light.css and dark.css into a single default.css holding both the
light :root block and the .dark override. Organize every theme block
(default, cream, bloom) into tiers: BRAND (primary/secondary + foregrounds,
shell-gradient — the only re-skin knobs), DERIVED (accent/ring/sidebar-*
via color-mix on var(--primary)), SYSTEM (neutral surfaces, accessibility-
locked), and STATUS (semantic, fixed). Give cream and bloom dark variants
that inherit default's status colors. Decouple the shell gradient from
--background via a dedicated --shell-gradient-base token.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: outline rename feedback plan

Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>

* refactor(theme): rename mode APIs to color mode names

Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>
IzumiSy added a commit that referenced this pull request Jun 17, 2026
* feat(theme): split theming into mode and palette axes

Introduce two independent theming axes: mode (light/dark/system) as the
end-user accessibility preference via useMode, and palette/theme
(default/cream/bloom) as a developer configuration via useTheme. Mode is
persisted; palette is prop-driven. The <html> element carries mode as the
.light/.dark class and palette as data-theme. The font axis is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(theme): merge light/dark CSS and add brand-token tiers (#314)

Merge light.css and dark.css into a single default.css holding both the
light :root block and the .dark override. Organize every theme block
(default, cream, bloom) into tiers: BRAND (primary/secondary + foregrounds,
shell-gradient — the only re-skin knobs), DERIVED (accent/ring/sidebar-*
via color-mix on var(--primary)), SYSTEM (neutral surfaces, accessibility-
locked), and STATUS (semantic, fixed). Give cream and bloom dark variants
that inherit default's status colors. Decouple the shell gradient from
--background via a dedicated --shell-gradient-base token.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: outline rename feedback plan

Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>

* refactor(theme): rename mode APIs to color mode names

Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>
IzumiSy added a commit that referenced this pull request Jun 17, 2026
* feat(theme): split theming into mode and palette axes

Introduce two independent theming axes: mode (light/dark/system) as the
end-user accessibility preference via useMode, and palette/theme
(default/cream/bloom) as a developer configuration via useTheme. Mode is
persisted; palette is prop-driven. The <html> element carries mode as the
.light/.dark class and palette as data-theme. The font axis is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(theme): merge light/dark CSS and add brand-token tiers (#314)

Merge light.css and dark.css into a single default.css holding both the
light :root block and the .dark override. Organize every theme block
(default, cream, bloom) into tiers: BRAND (primary/secondary + foregrounds,
shell-gradient — the only re-skin knobs), DERIVED (accent/ring/sidebar-*
via color-mix on var(--primary)), SYSTEM (neutral surfaces, accessibility-
locked), and STATUS (semantic, fixed). Give cream and bloom dark variants
that inherit default's status colors. Decouple the shell gradient from
--background via a dedicated --shell-gradient-base token.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: outline rename feedback plan

Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>

* refactor(theme): rename mode APIs to color mode names

Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>
IzumiSy added a commit that referenced this pull request Jun 24, 2026
…witcher (#306)

* feat(themes): theme foundation — palettes, ThemeProvider refactor, font axis

- Add cream/bloom color palettes with CSS custom properties (theme.css)
- Shell gradient, font-axis CSS, squircle corners, autofill overrides (globals.css)
- Refactor ThemeProvider: Theme/ResolvedTheme/Font types, useFont hook, legacy ID migration, memoized context
- Add defaultTheme/defaultFont props to AppShell
- Export new types and hooks from package index
- Add ThemeProvider unit tests

* refactor(theme): simplify ThemeProvider — remove legacy migration, hardcode storage keys, add system reactivity

* Format files

* feat: add ThemeSwitcher component with color palette and font selection

- Add ThemeSwitcher dropdown menu with 5 color themes and 2 font options
- Extract useFont hook from theme-context for independent font state access
- Replace sidebar-layout's simple light/dark toggle with full ThemeSwitcher
- Add themeSwitcher prop to SidebarLayout for customization/hiding
- Export ThemeSwitcher and useFont from package index
- Add component tests for ThemeSwitcher

* Revert vite-app

* refactor: remove themeSwitcher prop from SidebarLayout

* refactor(core): split theme.css into per-theme files

Extract token definitions and structural overrides into individual
files under src/assets/themes/ (light, dark, cream, bloom). Each
branded theme (cream/bloom) is now fully self-contained with its own
gradient, squircle, and transparency rules.

theme.css becomes a lightweight hub: @import directives + @theme inline
bridge. globals.css retains only theme-agnostic global rules.

* refactor(core): pre-generate font assets and append via closeBundle hook

- Add scripts/generate-fonts.mjs to produce fonts.generated.css and
  copy woff2 files to src/assets/fonts/ from fontsource packages
- Remove fonts.css; globals.css no longer imports fonts directly
  (avoids Vite woff2 resolution warnings during build)
- Add appendFonts Vite plugin that appends fonts.generated.css to
  dist/app-shell.css in closeBundle hook (instant, no font generation)
- Committed generated assets so CI doesn't need to re-run generation
- Add generate:fonts script to package.json

* Fix appendFonts plugin to use proper hooks

* Fix concatenation target

* feat(theme): split theming into mode and palette axes (#313)

* feat(theme): split theming into mode and palette axes

Introduce two independent theming axes: mode (light/dark/system) as the
end-user accessibility preference via useMode, and palette/theme
(default/cream/bloom) as a developer configuration via useTheme. Mode is
persisted; palette is prop-driven. The <html> element carries mode as the
.light/.dark class and palette as data-theme. The font axis is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(theme): merge light/dark CSS and add brand-token tiers (#314)

Merge light.css and dark.css into a single default.css holding both the
light :root block and the .dark override. Organize every theme block
(default, cream, bloom) into tiers: BRAND (primary/secondary + foregrounds,
shell-gradient — the only re-skin knobs), DERIVED (accent/ring/sidebar-*
via color-mix on var(--primary)), SYSTEM (neutral surfaces, accessibility-
locked), and STATUS (semantic, fixed). Give cream and bloom dark variants
that inherit default's status colors. Decouple the shell gradient from
--background via a dedicated --shell-gradient-base token.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: outline rename feedback plan

Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>

* refactor(theme): rename mode APIs to color mode names

Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>

* refactor: move @custom-variant dark to theme.css

* refactor: consolidate token tier docs into theme.css

* refactor(core): simplify theme system — remove font axis, make palette static

- Remove font switching (useFont, FONT_OPTIONS, data-font CSS, FontPreview)
- Rename ThemeSwitcher to AppearanceSwitcher (now only handles color mode)
- Make theme/palette prop-only (remove setTheme, simplify useTheme to read-only)
- Inline DOM updates into handlers instead of separate useEffect
- Move resolveMode/getSystemDark to module scope (lint fix)

* Remove unnecessary exports

* refactor: auto-scan @fontsource-variable packages from dependencies

* Revise changeset

* 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.

* refactor(theme): remove type aliases in index.ts exports, fix changeset prop names

* feat(themes): add i18n labels to AppearanceSwitcher and remove ThemePalette from public exports

* refactor: replace defaultThemePalette prop with static CSS theme imports

Remove runtime palette selection via prop in favor of static CSS imports.
Each theme (default, cream, bloom) is now imported directly:

  import "@tailor-platform/app-shell/themes/cream";

Changes:
- Remove defaultThemePalette prop from AppShell and ThemeProvider
- Remove ThemePalette type, THEME_PALETTE_OPTIONS, data-theme logic
- Change theme CSS selectors from [data-theme=x] to :root
- Wrap default theme in @layer theme.defaults so custom themes always win
- Move structural overrides (gradient, transparent chrome) out of @layer
  to ensure they override Tailwind utilities
- Add subpath pattern export: "./themes/*" -> "./dist/themes/*.css"
- Update examples and e2e to use CSS imports instead of prop

* chore(core): remove generated font assets

* Revert style loading

* feat(theme): accent tokens, palette tier organisation, and appearance icons (#323)

* feat(theme): explicit accent tiers, palette template, and appearance icons

Organise palette CSS into numbered tiers (Brand → Shell → Derived → System →
Palette → Semantic → Structural) with `_template.css` as the new-palette
checklist. Set explicit `--accent` per palette and mode for reliable
sidebar/menu selection. Add `--alert-*` tokens to default palette CSS.
Replace AppearanceSwitcher menu swatches with Sun/Moon/Monitor icons. Add
Color demo page and styling-theming docs for palette authoring.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(alert): wire variants to semantic --alert-* tokens

Use CSS variable tokens for all alert variants including neutral
(--alert-neutral-* aliases muted). Lighter dark-mode foregrounds from
default.css apply automatically; drop hardcoded Tailwind color classes.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>

* Remove unnecessary changeset

* refactor(themes): inherit shared aliases from default

---------

Co-authored-by: pradeep <6978383+itsprade@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: IzumiSy <982850+IzumiSy@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Sean Hasselback <seanhasselback@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants