Skip to content

feat(gamut-styles)!: brands, overrides and Original Codecademy on the color system - #52

Draft
dreamwasp wants to merge 13 commits into
cass-gmt-1825-color-systemfrom
cass-gmt-1825-color-brands
Draft

dreamwasp wants to merge 13 commits into
cass-gmt-1825-color-systemfrom
cass-gmt-1825-color-brands

Conversation

@dreamwasp

@dreamwasp dreamwasp commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Builds on #50 so the generated system can carry the old themes: a brand supplies only a palette, every brand shares one role mapping, and manual overrides keep colors the generator wouldn't produce. Adds originalCodecademy, old core rebuilt as a brand. Also moves mode switches to an attribute and renames the color variables to --color-* (singular, matching main). Draft, stacked on #50.

Brands

  • colorSystem/brands/ — BRANDS drives the build; every brand has to pass every check or nothing is emitted. tokens.css emits roles once and a palette-only block per brand under [data-brand].
  • colorSystem/brand.ts, system.ts — overrides.palette (an exact hex in place of a generated step), overrides.roles (a role repointed in one mode) and overrides.gradients (a gradient role's circles moved to another slot, keeping design's grades, sizes and positions). Rounded to what tokens.css prints and guarded to round-trip; unknown roles or steps throw. brand.sources reports how far each supplied hex lands from its nearest step.
  • brands/originalCodecademy.ts, themes/originalCodecademy.ts:
    • old core's colors per slot, with yellow pinned exactly and old core's yellow dark-mode primary;
    • navy and beige not pinned (design, 2026-10-29), so their generated steps are used;
    • the two-color gradient pairs primary with accent-alt instead of the yellow accent (design, 2026-10-28);
    • old core's fonts, weights and elevation.
  • themes/createBrandTheme.ts — one factory for every brand theme; skillsoftTheme uses it too.
  • emit.ts — brand gradients are written against palette variables (color-mix(in oklab, …)), so one gradient follows every brand (pixel diff against the old literal gradients: max 1/255).

Runtime

  • ColorMode — on generated themes, a mode switch sets data-color-mode and data-brand and injects no role variables. 100 nested switches, server-rendered: 7.6 → 4.2 ms, 5 KB → 46 B of injected CSS. Old themes behave as before.
  • Background — fixes a bug that's also on main (see fix(gamut-styles): Background paints the outer mode's color for an alias bg #51): an alias bg that flips the mode painted the flipped mode's color.

Breaking

  • variance createTheme, gamut-styles — color variables are --color-*, the same prefix as main. variance's createTheme uses the color prefix for the color scale, and its mode types follow it.
  • tokens.css — all blocks inside @layer tokens, with reset, base, tokens, recipes, utilities declared up front (Panda's order). Emotion's styles stay unlayered and win.

Design sync

  • colorSystem/migration.ts — re-synced to sandbox 15c2195. Since c363244: a pending kind for shadows, and status alerts carry their ink on the message and on a floating border.
  • gamut/src/Alert:
    • General moves to surface-brand-subtle.
    • Gradient alerts are a size container with the gradient on a layer behind the content, so the cqw radii measure the alert, not the viewport.
    • Error, success and notice put text-<status> on the message and on a floating border. General, feature and subtle stay text-default.
    • The CTA follows the Alert's own mode instead of being forced dark, so light alerts get light-mode buttons.

Tests

  • __tests__/stylingContract.test.tsx — css, variant, states, system props, themed, modeColorProps, _getColorValue, ColorMode, Background and the mode hooks on all seven themes in both modes. Every color they write has to resolve through every var() to a real color.
  • validate-tokens.ts — every check per brand, including a round-trip of each brand's roles through tokens.css.

Storybook and docs

  • .storybook — an "Original Codecademy (color spike)" theme option.
  • gamut-agent-tools skills, colorSystem/README.md, the CSS contract story — --color-*.
  • CSS contract story — Surfaces splits the surface roles from the raw palette fills, which aren't roles.

Known gaps

  • A floating general or feature Alert's border is text-default, following design's coded preview. Design's map still has a shared "Floating border: border-default" slot.
  • Elevation reads shadow-primary, which has no role yet (design has it as pending), so shadows render undefined on the new themes.

PR Checklist

  • Related to designs: gamut-sandbox PR #34 (head 15c2195)
  • Related to JIRA ticket: GMT-1825
  • Changeset added (or empty changeset if no release)
  • 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
  • Package previews from this PR are passing end-to-end tests in all relevant consuming repositories

Testing Instructions

  1. yarn workspace @skillsoft/gamut-styles validate:tokens → both brands pass every check, including the tokens.css round-trip.
  2. yarn nx test gamut-styles (281 tests), yarn nx test variance (104), and npx jest packages/gamut/src/Alert (16).
  3. Storybook, theme toolbar → Original Codecademy (color spike), both modes:
    • Molecules / Alert: Feature's gradient is blue to violet, not blue to yellow. Error's message is red and its CTA is a light-mode button in light mode.
    • Inspect a ColorMode element: it carries data-color-mode and data-brand="originalCodecademy", and its class has no --color-* role declarations.
  4. Compare with today's core: the comparison page shows every role and every old name side by side, with ΔE.
  5. Toolbar → Core: unchanged from feat(gamut-styles)!: generated color system with tokens.css as the color contract #50.

🤖 Generated with Claude Code

… color system

- Brands: every brand shares one role mapping and supplies a palette,
  emitted under [data-brand]. Palette and role overrides go through the
  same checks.
- originalCodecademy: old core rebuilt as a brand, with core's fonts,
  weights and elevation.
- ColorMode sets data-color-mode and data-brand on generated themes
  instead of injecting role variables.
- Background paints the outer mode's palette color for an alias bg.
- BREAKING: --color-* variables are renamed to --colors-* (variance too);
  tokens.css rules live in @layer tokens.
- migration.ts re-synced to gamut-sandbox c363244. Alert General moves to
  surface-brand-subtle.
- Styling contract test across seven themes in both modes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ands

# Conflicts:
#	packages/gamut-styles/src/colorSystem/__tests__/system.test.ts
#	packages/gamut-styles/src/themes/skillsoft.ts
dreamwasp and others added 2 commits October 29, 2026 10:26
… pairs primary with accent-alt

Brands can move a gradient role's circles to another slot
(`overrides.gradients`), keeping design's grades, sizes and positions.
ColorSystem carries each brand's resolved gradients, and the emitters,
the migration contrast checks and tokens.css read from it. A brand whose
gradient differs from the default's gets a `--gradient-*` line under
`[data-brand][data-color-mode]`, like a role override.

Original Codecademy's two-color gradient takes accent-alt in place of
the yellow accent, per design, so it reads blue to violet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ps pinning navy and beige

Design feedback on the comparison page (2026-10-29), and design's
migration map at sandbox 15c2195:

- Status alerts (error, success, notice) put their status ink on the
  message, and a floating Alert's border takes the same ink. General,
  feature and subtle keep text-default.
- The CTA follows the Alert's own color mode. Forcing it dark only made
  sense on the old saturated fills; on the tints, light-mode alerts get
  light-mode buttons.
- The ported migration map is synced to 15c2195 (Alert message slot,
  Alert and floating-surface summaries).
- Original Codecademy no longer pins navy-800 and beige. Pinned, they
  carried much more chroma than the generated surfaces beside them;
  the generated steps at their hues are used as is. Yellow stays exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dreamwasp added a commit that referenced this pull request Oct 29, 2026
Design feedback on the comparison page (2026-10-29): pinned navy-800
carries much more chroma than the generated surfaces beside it. As for
Original Codecademy (#52), lxStudio uses the generated step at navy's hue.
Admin and platform inherit Original Codecademy's pins, so they follow;
platform's blue-0 and the near-white pages are nearly achromatic and stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@dreamwasp dreamwasp left a comment

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.

Fixes from design feedback on the comparison pages (2026-10-29), plus design's migration map at sandbox 15c2195. Notes on the lines below.

const buttonColorMode = isSubtleVariant ? currentColorMode : 'dark';

/*
* The tints follow the color mode, so the CTA does too: it inherits the

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.

Design feedback (comparison page, 2026-10-29): now that the alert fills are mode-following tints, light-mode alerts need light-mode buttons. The CTA was forced to mode="dark" for every non-subtle variant, a holdover from the old saturated fills. It now inherits the mode the Alert's Background sets. The test checks that no forced mode's variables land on the button.

* (design's preview at sandbox 15c2195; its shared "Floating border" slot
* still says border-default and is out of date).
*/
const alertInk = ({ ink, placement, theme }: InkProps) => {

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.

Design's map at sandbox 15c2195: status alerts (error, success, notice) put their status ink on the message, not text-default, and a floating Alert's border takes the same ink. General, feature and subtle keep text-default.

Note: the map's shared "Floating border" slot still says border-default, but design's coded preview and the Alert summary both use the message ink. I followed the preview, so a floating general or feature alert's edge is text-default.

kind: 'text',
legacy: 'auto (Background)',
role: 'text-default',
role: `text-${status}`,

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.

Synced to design's sandbox at 15c2195 (commit c5190b6, "Alert: status ink on message text, floating border matches text"). This message slot plus the Alert and floating-surface summaries are the whole diff; the components section now matches the sandbox exactly. validate:tokens still passes: each status ink clears 4.5:1 on its own tint for every brand.

overrides: {
/* Each at the grade whose fixed lightness is nearest. */
palette: {
'accent-300': '#FFD300', // yellow-500

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.

Design feedback (comparison page, 2026-10-29): pinned navy-800 at neutral-dark-1100 carried much more chroma than the generated sunken and raised surfaces next to it, which made block tabs and table zebra rows look off. Beige had the same problem, less strongly. Both pins are gone; the generated steps at navy's and beige's hues are used as is. Yellow stays exact. Admin and platform (#58) inherit this.

},
},
gradients: {
'surface-brand': { accent: 'accent-alt' },

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.

Feature (and general) alerts on Original Codecademy render this gradient: primary + accent-alt rather than blue to yellow. Design approved the override on the brand comparison page (2026-10-29). The components screenshot page was captured before this landed, so its feature alerts still show the yellow pair until it's retaken.

…ndbox update

Ports gamut-sandbox df33b47 (design, 2026-11-03):

- backdrop is renamed shadow-scrim; shadow-key and shadow-ambient are
  added at design's draft alphas (key 0.16 / 0.32, ambient 0.12 / 0.24).
- Brand themes draw elevation in the shadow roles, so shadows no longer
  render undefined. Skillsoft gets the soft raised shadow (design's Card
  geometry); Original Codecademy keeps old core's hard offset, with
  shadow-ambient transparent in both modes.
- A role override can set an alpha as well as a ref, and the build emits
  the translucent palette values overrides introduce.
- The migration map follows the sandbox: shadow-primary/secondary split
  onto the shadow roles, Card collapses to default and brand, overlays
  take a border-subtle hairline, Modal and Flyout use shadow-scrim.
- The parity fixture is re-dumped at df33b47; the generator is unchanged.
- The styling contract test only applies a brand's own role overrides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@dreamwasp dreamwasp left a comment

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.

Port of design's sandbox update, 2026-11-03 (gamut-sandbox df33b47). Notes on the lines below.

* is a fill for the element behind a modal, not a box-shadow color.
*/
{
token: 'shadow-key',

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.

Design's sandbox update, 2026-11-03: backdrop becomes shadow-scrim, and shadow-key / shadow-ambient are the two layers every elevation shadow is drawn in. Alphas are design's current values (key 0.16 / 0.32, ambient 0.12 / 0.24), still marked draft in the sandbox. The parity fixture was re-dumped at df33b47 and all 45 roles match.

},
},
dark: {
'shadow-ambient': { ref: 'black', alpha: 0 },

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.

Design's sandbox update, 2026-11-03: the Codecademy theme's shadow is a hard offset with no soft falloff, so shadow-ambient resolves to transparent and the layer stays in every elevation. This is why role overrides can now set an alpha (system.ts).

const valueFor = (r: Role, mode: Mode): RoleValue => {
const o = roleOverrides[mode]?.[r.token];
if (!o) return r[mode];
return typeof o === 'object' && 'alpha' in o

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.

A bare ref still keeps the role's alpha; { ref, alpha } replaces it. emit.ts now emits the translucent palette values an override introduces (palette-neutral-dark-1100-a0), which it didn't before.

`${key} ${colors['shadow-key']}, ${ambient} ${colors['shadow-ambient']}`;

/* The soft raised shadow: design's Card geometry at rest and on hover. */
export const softElevation: ElevationScaleFactory = ({ colors }) => {

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.

Design's sandbox update, 2026-11-03: Card's preview geometry, rest 0 1px 4px + 0 2px 7px and hover 0 2px 6px + 0 6px 16px, drawn in the shadow roles. Fixes brand themes' shadows rendering undefined (they read shadow-primary, which brand themes don't define). Geometry stays here until elevation becomes CSS variables.

* in `shadow-key`. The brand makes `shadow-ambient` transparent, and the
* ambient layer stays at zero size.
*/
export const offsetElevation: ElevationScaleFactory = ({ colors }) => {

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.

Design's sandbox update, 2026-11-03: the hard offset is for the Codecademy theme only; every other brand gets the soft shadow. Old core's geometry, drawn in shadow-key, with a zero-size ambient layer so the layer count matches.

@@ -229,17 +230,15 @@ export const TOKEN_MAP: TokenMapping[] = [
// ---------------------------------------------------------------- shadow
{
legacy: 'shadow-primary',

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.

Design's sandbox update, 2026-11-03: shadow-primary and shadow-secondary go from pending to a split onto shadow-key + shadow-ambient.

],
},
{
name: 'Interactive',

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.

Design's sandbox update, 2026-11-03: Card collapses to default and brand, every card has a border-subtle hairline, and an interactive card adds the raised shadow at rest and the veil plus raised-hover shadow on hover; the border no longer changes. Components follow in #54.

s('Fill', 'fill', 'background-contrast', 'surface-raised'),
s('Text', 'text', 'text', 'text-default'),
s('Border + beak', 'border', 'border-primary', 'border-default'),
s('Border + beak', 'border', 'border-primary', 'border-subtle'),

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.

Design's sandbox update, 2026-11-03: menus, popovers, tips, toasts and the SelectDropdown menu drop to a border-subtle hairline, and their shadow separates them. Controls keep border-default. Components follow in #54.

dreamwasp added a commit that referenced this pull request Nov 4, 2026
…-legacy-brands

- admin, platform and lxStudio take the soft elevation drawn in the shadow
  roles; only Original Codecademy keeps the hard offset (design's sandbox,
  df33b47: the hard offset is the Codecademy theme's only).
- Original Codecademy's overrides split into coreFamilyOverrides (the pins,
  the yellow dark primary, the gradient), which admin and platform share,
  and its own transparent shadow-ambient, which they don't.
- Tokens regenerated; theme snapshots updated for the shadow roles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dreamwasp and others added 2 commits November 4, 2026 08:53
…CTA offset

Design gives the Codecademy theme "the hard offset" at raised depth
(sandbox df33b47, CTAButton) without its geometry. Raised is now old
core's CTAButton offset at rest (-4px) and raised-hover its hover offset
(-8px, with the Card lift), drawn in shadow-key. Floating surfaces and
interactive cards get a visible edge in this theme too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dreamwasp and others added 2 commits November 4, 2026 11:19
… names its source

The check that the generator reproduces design's sandbox lived only in
the validate:tokens report, which CI doesn't run, and its fixture was
named for one sandbox commit, so every port renamed it and edited the
script.

- `__tests__/sandboxParity.test.ts` compares every palette step and every
  role, both modes, for the default brand, and lists each difference.
- `__fixtures__/sandbox.json` records its repo, branch, commit and dump
  date; the test names that source.
- `yarn workspace @skillsoft/gamut-styles dump:sandbox <checkout>`
  refreshes the fixture from any sandbox checkout. On df33b47 it
  reproduces the old fixture exactly.
- validate:tokens drops the parity section; the README says how to take a
  sandbox update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dreamwasp and others added 4 commits November 9, 2026 14:42
Resolves three conflicts:
- Background.tsx: take the color-system comment wording.
- colorSystem/README.md: the color-system README, plus the brands and parity section from this branch.
- Alert tests: the color tests move to Alert.test.tsx, where main moved the file.

The color-system tests now expect the --colors- variable prefix this branch emits.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
The stack emitted --colors-* (the color scale's name) while main uses
--color-*. This restores the singular prefix everywhere: the generator's
Style Dictionary prefix, ColorMode's scale key, the variance package (back to
main), gamut-styles, the Alert and codemod-facing names, and the docs.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…ands

# Conflicts:
#	packages/.storybook/preview.ts
#	packages/.storybook/theming/GamutThemeProvider.tsx
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>

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