Skip to content

feat(themes): theme foundation with mode/palette axes and AppearanceSwitcher#306

Open
IzumiSy wants to merge 26 commits into
mainfrom
feat/theme-foundation
Open

feat(themes): theme foundation with mode/palette axes and AppearanceSwitcher#306
IzumiSy wants to merge 26 commits into
mainfrom
feat/theme-foundation

Conversation

@IzumiSy

@IzumiSy IzumiSy commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Foundational layer for the Tailor brand appearance system. Introduces color theme as the user preference axis, static theme palettes selected via CSS imports, and a new AppearanceSwitcher component.

Font delivery is intentionally not part of this PR. Inter is already handled on main, and this branch no longer adds generated font CSS, copied woff2 assets, or a font generation build step.

Screenshots

AppearanceSwitcher

スクリーンショット 2026-06-15 13 07 30

Changes

Theming Architecture

Axis Type Values How to configure Persisted Applied to <html> as
Color theme End-user preference light / dark / system defaultColorTheme prop + localStorage Yes (appshell-ui-theme) .light / .dark class
Theme palette Developer config (static) default / cream / bloom CSS import No CSS custom properties on :root

Every palette ships both light and dark variants. The active variant is chosen by the color theme axis.

Theme Palette Selection

Palettes are selected by importing CSS files. The default palette is included in @tailor-platform/app-shell/styles; branded palettes override it through the ./themes/* export.

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

The subpath pattern "./themes/*" maps to ./dist/themes/*.css, so new palettes can be added without changing package.json.

Naming Convention

The word "theme" has historically meant the light/dark/system preference in AppShell. To preserve compatibility, useTheme() and the appshell-ui-theme localStorage key remain in place.

  • Color theme (ColorTheme) is the axis that was previously just called "theme".
  • Theme palette is the brand/product axis, selected statically via CSS import instead of a runtime prop.

Per-Theme CSS Architecture

Theme tokens now live in one file per palette:

src/assets/
  themes/
    default.css
    cream.css
    bloom.css
  theme.css

Each theme file declares CSS custom properties in four tiers:

  • BRAND: primary/secondary plus foregrounds, the only re-skin knobs
  • DERIVED: computed from BRAND via var() and color-mix
  • SYSTEM: neutral surfaces/text, accessibility-locked and brand-independent
  • STATUS: semantic status colors, charts, radius, and shadows

CSS Cascade Strategy

  • default.css is imported inside @layer theme.defaults, giving it the lowest priority.
  • Branded theme CSS files are unlayered, so their :root variables override layered defaults regardless of source order.
  • Structural overrides are also unlayered so they can beat Tailwind utilities.

AppShell Globals

globals.css is now theme-agnostic and limited to shared app behavior: Tailwind imports, border compatibility, z-index tokens, body styling, scrollbar styling, autofill override, and sidebar active-item utility styling.

ThemeProvider

  • ColorTheme type: "light" | "dark" | "system"
  • useTheme() returns { theme, resolvedTheme, setTheme }
  • Storage key remains appshell-ui-theme
  • system mode reacts to matchMedia changes in real time
  • Palette state/props are removed; palette selection is CSS-driven

AppearanceSwitcher

  • Replaces the previous light/dark toggle in SidebarLayout
  • Shows Light / Dark / System color theme options
  • Includes decorative swatch previews
  • Supports i18n via defineI18nLabels (en / ja)

@IzumiSy IzumiSy force-pushed the feat/theme-foundation branch from ae33b86 to ec264a9 Compare June 4, 2026 02:35
@IzumiSy IzumiSy requested a review from a team June 8, 2026 05:10
@IzumiSy IzumiSy changed the title feat(themes): theme foundation — palettes, ThemeProvider refactor, font axis feat(themes): theme foundation — mode/palette axes, AppearanceSwitcher, bundled fonts Jun 15, 2026
@IzumiSy IzumiSy force-pushed the feat/theme-foundation branch from dd07167 to a6b3da5 Compare June 15, 2026 04:04
@IzumiSy

IzumiSy commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

/review

@IzumiSy IzumiSy self-assigned this Jun 15, 2026
@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

API Design Review completed successfully!

@github-actions github-actions Bot 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.

Generated by API Design Review for issue #306 · 652.2 AIC · ⌖ 13 AIC · ⊞ 26.2K
Comment /review to run again

Comment thread .changeset/theme-mode-palette-axes.md
Comment thread packages/core/src/contexts/theme-context.tsx Outdated
Comment thread .changeset/theme-mode-palette-axes.md Outdated
Comment thread packages/core/src/contexts/theme-context.tsx Outdated

@interacsean interacsean 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.

Just some thoughts on the css files

html[data-theme="bloom"] {
color-scheme: light;

/* ───── BRAND — override to re-skin ───── */

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.

Is this comment valid? Is this brand-specific overrides of default css variables?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yep — that's the intent. default.css is the base token set, and branded palettes override only the palette-specific brand/system/palette values. I updated the file comment to make that layering clearer in 1ec3b56.

--shell-gradient-tint: rgb(255, 255, 255);

/* ───── DERIVED from brand (do not edit; re-resolves in dark) ───── */
--accent: color-mix(in srgb, var(--primary) 15%, var(--card));

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.

Is this DERIVED section always the same? My gut tells me sometimes these formulas won't always work the same for different color tones

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call. I removed the generic DERIVED block in 1ec3b56. --accent stays explicit per palette now, while the shared aliases (--ring, --sidebar-*) inherit from default.css; the gradient-only helpers were inlined into the structural gradient block.

--shell-gradient-end: var(--shell-gradient-tint);

/* ───── SYSTEM — neutral surfaces/text (accessibility-locked) ───── */
--background: rgba(250, 250, 250, 1);

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.

Can these not inherit somehow from a default? It looks like many of these are defined in multiple files but are mostly the same values

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yep — pushed this direction in 1ec3b56. --ring and --sidebar-* now inherit from default.css, and the branded palette files only keep palette-specific overrides.

--popover-foreground: rgba(250, 250, 250, 1);
--muted: rgba(38, 38, 38, 1);
--muted-foreground: rgba(163, 163, 163, 1);
--border: rgba(255, 255, 255, 0.10000000149011612);

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.

wow what a number 🤣

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That caught our eyes, but the value itself was carried over from the old ones that were in theme.css, so maybe fine?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Rounded those export-noise values in 1ec3b56 (0.10000000149011612 -> 0.1, 0.15000000596046448 -> 0.15).

@IzumiSy IzumiSy force-pushed the feat/theme-foundation branch from e6453fa to 495dbf7 Compare June 17, 2026 03:52
@IzumiSy IzumiSy changed the title feat(themes): theme foundation — mode/palette axes, AppearanceSwitcher, bundled fonts feat(themes): theme foundation with mode/palette axes and AppearanceSwitcher Jun 17, 2026
IzumiSy and others added 18 commits June 17, 2026 12:58
…nt 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
- 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
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.
- 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
* 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>
…e 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)
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.
IzumiSy added 4 commits June 17, 2026 12:58
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
@IzumiSy IzumiSy force-pushed the feat/theme-foundation branch from 495dbf7 to 67eba3b Compare June 17, 2026 03:58
IzumiSy and others added 4 commits June 17, 2026 14:56
… 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>
@IzumiSy IzumiSy requested a review from interacsean June 19, 2026 06:03
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