Skip to content

feat(gamut-brand)!: one tokens.css per brand, and white-label tooling - #66

Draft
dreamwasp wants to merge 5 commits into
cass-gmt-1825-color-legacy-brandsfrom
cass-gmt-1825-color-white-label
Draft

dreamwasp wants to merge 5 commits into
cass-gmt-1825-color-legacy-brandsfrom
cass-gmt-1825-color-white-label

Conversation

@dreamwasp

@dreamwasp dreamwasp commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Two connected changes. Every brand now ships its own tokens.css, instead of all brands sharing one file. And the color generator moves into a new package, @skillsoft/gamut-brand, so a team can build its own brand (white-labeling) at build time and keep gamut's types.

A team's brand can't be baked into a file gamut ships, so per-brand output and a public generator are one piece of work.

Per-brand CSS

  • tokens.css holds one brand: its palette on :root, [data-brand], its initial mode's roles on :root, then the other mode. A page starts in the right brand and mode with nothing set on <html>.
  • A brand's role overrides sit in its mode blocks, so a bare data-color-mode keeps them.
  • Generated output moves to src/tokens/generated/<brand>/, and a page carries only its own brand. The split was checked against the old shared file: all five brands, both modes, identical values.
  • initialMode moves onto the brand, because its CSS has to know it at build time. Platform starts dark in its CSS too, so a server-rendered page starts dark.
  • The DTCG mode files now include each brand's role overrides. Before, every brand got the default roles.

@skillsoft/gamut-brand

  • New package, build time only (a dev dependency). It holds the generator, the roles, the checks and gamut's five brands, moved from gamut-styles/src/colorSystem.
  • defineBrand({ name, hues }) types a brand. gamut-brand build <file> writes values.ts, tokens.css and DTCG JSON. --watch regenerates on change and only rewrites files whose contents changed. --check writes nothing and fails CI when the files are stale.
  • A brand that fails a check isn't written. Every check runs again on the brand's own tokens.css read back, so what ships is what was checked.
  • createBrandTheme, BrandBase and BrandValues are exported from gamut-styles. A team's theme has the same type as gamut's own, so system props, css(), variant() and states() stay typed.
  • gamut-styles' build:tokens is now a loop over gamut's five brands using the same API.

Tests

  • generator.test.ts holds the generator to its rules, at every 5° of hue:
    • fixed lightness per grade, and chroma inside sRGB
    • the separation guarantee
    • one role mapping for every brand, apart from its overrides
    • every check passing, for our brands and for a primary-only brand
  • These replace the sandbox parity test. Parity only mattered while porting, so nothing in the package points at the sandbox anymore.
  • cli.test.ts runs the CLI end to end: build, check, unchanged files, a refused brand, a bad name.
  • gamut-styles has a test that the generated files are current, and that each brand ships only itself.

Breaking

  • Generated file paths: @skillsoft/gamut-styles/tokens/<brand>.css for a brand's CSS (tokens.css stays the default brand), and tokens/<brand>/{palette,light,dark}.tokens.json.
  • initialMode moves from createBrandTheme to the brand. BrandBase now requires gamut's font family and font weight keys.

Merge order: after #58, before #54. #54, #63, #59 and #73 already have this merged in.


PR Checklist

  • Related to designs:
  • Related to JIRA ticket: GMT-1825
  • Version plan added/updated (.changeset/color-system-white-label.md)
  • I have run this code to verify it works
  • This PR includes unit tests for the code change
  • This PR includes testing instructions for the code change

Testing Instructions

  1. npx nx run-many -t test,lint,build,verify-package -p gamut-brand gamut-styles: all pass.
  2. yarn workspace @skillsoft/gamut-styles build:tokens --check: "generated tokens are current".
  3. Open packages/gamut-styles/src/tokens/generated/originalCodecademy/tokens.css. It has one brand's palette, its light roles on :root, and the dark mode's yellow action-primary.
  4. Try a team brand: write defineBrand({ name: 'acme', hues: { primary: '#0A5CFF' } }) to a file and run gamut-brand build on it. Then change the hue with --watch running and see only the changed files rewritten.

🤖 Generated with Claude Code

dreamwasp and others added 3 commits November 5, 2026 11:20
Each brand theme now carries and injects only its own brand's CSS (about
21 KB, down from 56 KB for every brand at once). The CSS puts the brand's
palette and initial mode on :root, and folds its role overrides into its
mode blocks. Every brand resolves to the same values as before in both modes.

The generator, role table, checks, sandbox fixture and gamut's brand inputs
move from gamut-styles' src/colorSystem to a new build-time package,
@skillsoft/gamut-brand. Its `gamut-brand build` CLI writes a brand's
values.ts, tokens.css and DTCG JSON (--watch, --check), and refuses a brand
that fails a check, including on its own tokens.css read back.
gamut-styles' build:tokens uses the same writeBrand on gamut's five brands.

- createBrandTheme, BrandBase and BrandValues are exported. A team's theme
  has the same type as gamut's, so system props stay typed.
- BREAKING: generated files move to one folder per brand
  (tokens/<brand>.css, tokens/<brand>/*.tokens.json; tokens.css stays the
  default brand). initialMode moves from the theme onto the brand.
- DTCG mode files include each brand's role overrides.
- Role names in brand overrides are typed (ROLE_TOKENS).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sandbox parity only matters while the port is in development. The
generator is now held to its own rules for any hue (fixed lightness per
grade, chroma inside sRGB, the separation guarantee, one role mapping
for every brand, every check passing) instead of a saved copy of the
sandbox's output. The checked-in generated files still catch value drift.

Drops the sandbox fixture, dump script and comments that pointed at
sandbox commits and files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…s' into cass-gmt-1825-color-white-label

# Conflicts:
#	.changeset/color-system-brands.md
#	packages/gamut-brand/src/__tests__/system.test.ts
#	packages/gamut-styles/scripts/build-tokens.ts
#	packages/gamut-styles/scripts/validate-tokens.ts
#	packages/gamut-styles/src/colorSystem/README.md
#	packages/gamut-styles/src/themes/__tests__/__snapshots__/theme.test.ts.snap
#	packages/gamut-styles/src/themes/__tests__/brandElevation.test.ts
#	packages/gamut-styles/src/themes/__tests__/theme.test.ts
#	packages/gamut-styles/src/themes/platform.ts
#	packages/gamut-styles/src/tokens/generated/tokens.css
#	packages/gamut-styles/src/tokens/generated/tokensCss.ts

This branch has not been deployed

No deployments
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.

1 participant