diff --git a/.changeset/color-system-brands.md b/.changeset/color-system-brands.md index 41e82e1700..0f902aab2c 100644 --- a/.changeset/color-system-brands.md +++ b/.changeset/color-system-brands.md @@ -4,8 +4,9 @@ '@skillsoft/gamut': minor --- -Brands on the generated color system. +Brands on the generated color system, and `--color-*` variable names. +- **Breaking:** color CSS variables are renamed from `--color-*` to `--color-*`, in `@skillsoft/variance`'s `createTheme` and in `tokens.css`. Raw CSS that reads `var(--color-…)` has to be updated. - **Breaking:** `tokens.css` puts its rules in `@layer tokens`, with `reset, base, tokens, recipes, utilities` declared first. - Adds `originalCodecademyTheme`, today's core colors, fonts and hard-offset elevation rebuilt on the generated system. `tokens.css` carries a palette block per brand under `[data-brand]`. - A brand can override a palette step with an exact hex, or point a role at a different step (and, optionally, a different alpha) in one mode. Overrides go through the same contrast checks as generated values. diff --git a/.changeset/static-styles-groundwork.md b/.changeset/static-styles-groundwork.md new file mode 100644 index 0000000000..9cd50419b5 --- /dev/null +++ b/.changeset/static-styles-groundwork.md @@ -0,0 +1,9 @@ +--- +'@skillsoft/gamut-styles': minor +--- + +Add groundwork for static component styles. No component uses it yet. + +- `defineRecipe` describes a component's styles as data (base, variant groups, defaults, compound variants), in Chakra's and Panda's recipe shape with gamut's style objects. `recipe.styles` styles it through Emotion; `recipe.classNames` gives stable class names. +- `compileRecipe` turns a recipe into plain CSS in `@layer recipes`, and `checkRecipe` reports whether it compiles the same for every brand theme. +- `StaticStyles` puts compiled CSS on the page once, through the Emotion cache, so SSR, nonces and stylis plugins work as before. diff --git a/packages/gamut-agent-tools/skills/gamut-review/SKILL.md b/packages/gamut-agent-tools/skills/gamut-review/SKILL.md index 14210052dd..353ce56c65 100644 --- a/packages/gamut-agent-tools/skills/gamut-review/SKILL.md +++ b/packages/gamut-agent-tools/skills/gamut-review/SKILL.md @@ -19,9 +19,9 @@ Remediation skills: [`gamut-theming`](../gamut-theming/SKILL.md) · [`gamut-colo Resolve the audit root from the user's path if provided, otherwise the current working directory. Look for `DESIGN.md` at that root (not inside `node_modules` or package subfolders unless the audit path is explicitly that folder). -| Result | Action | -| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Found | Report `✓ DESIGN.md present ()`. Proceed with Checks 1–5 using this file for product/theme context. | +| Result | Action | +| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Found | Report `✓ DESIGN.md present ()`. Proceed with Checks 1–5 using this file for product/theme context. | | Missing | Report `✗ DESIGN.md not found` as a blocking finding. Include remediation: from the repo root run `gamut plugin install cursor --theme core` (or `percipio`, `lxstudio`, `admin`, `platform`; also `claude`), or manually copy the matching `DESIGN.*.md` from `@skillsoft/gamut` agent-tools and rename to `DESIGN.md`. Still run Checks 1–3 and 5. For Check 4, list hex violations with `palette:` / `semantic:` only where Appendix A/B apply without product YAML — prefix the Hardcoded colors section with `⚠ low confidence — no DESIGN.md` and do not assume Codecademy Core semantics; do not use Appendix B shortcuts as authoritative. | --- @@ -30,11 +30,11 @@ Resolve the audit root from the user's path if provided, otherwise the current w Read `package.json` at the audit root. Inspect `dependencies`, `devDependencies`, and `peerDependencies` combined. -| Package | Expectation | -| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `@skillsoft/gamut` | Required — core component library | -| `@skillsoft/gamut-styles` | Recommended — design tokens and theme primitives | -| `@skillsoft/variance` | Recommended — style-prop system used by Gamut internals | +| Package | Expectation | +| ------------------------- | ------------------------------------------------------- | +| `@skillsoft/gamut` | Required — core component library | +| `@skillsoft/gamut-styles` | Recommended — design tokens and theme primitives | +| `@skillsoft/variance` | Recommended — style-prop system used by Gamut internals | --- @@ -48,7 +48,7 @@ Search source files (`.ts`, `.tsx`, `.js`, `.jsx`) for these symbols. Skip `node | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GamutProvider` | Required — must appear at least once (app root wrapper) | | `ColorMode` | Recommended — enables semantic light/dark theming | -| `Background` | Recommended — semantic surface color via `@skillsoft/gamut-styles` | +| `Background` | Recommended — semantic surface color via `@skillsoft/gamut-styles` | | `declare module '@emotion/react'` | **Required if TypeScript** — `Theme` must be augmented with the active theme type (e.g. `CoreTheme`) so scale props type-check correctly; grep `.d.ts` and `.ts`/`.tsx` source files for this string. **Recommended if not TypeScript** — note that adopting TypeScript is recommended and that `theme.d.ts` will be needed when it is. | For each found symbol report the first file path where it appears. @@ -61,8 +61,8 @@ For each found symbol report the first file path where it appears. Grep source files for any of these patterns. Each match is an error. -| Pattern | Reason | -| ------------------------------- | ----------------------------------------------------------------------- | +| Pattern | Reason | +| ------------------------------ | ----------------------------------------------------------------------- | | `@skillsoft/gamut/dist/` | Deep dist import — bypasses public API and breaks on internal refactors | | `@skillsoft/gamut/src/` | Deep src import — not part of the published package | | `@skillsoft/gamut-styles/src/` | Deep src import — use the package root | @@ -414,13 +414,13 @@ Reporting: count unique non-Gamut variable names and list the top offenders with Grep test files (`**/__tests__/**/*.{ts,tsx}`, `**/*.test.{ts,tsx}`, `**/*.spec.{ts,tsx}`) for these patterns. Skip `node_modules`, `dist`. -| Pattern | Verdict | Reason | -| ----------------------------------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `jest.mock\(.*@skillsoft/gamut` | Error | Manual mocking bypasses theme context and produces false-positive tests; prefer `setupRtl` from `@skillsoft/gamut-tests` (or a harness + `setupRtl`); use raw `MockGamutProvider` + `render` only for rare one-offs or Storybook mocks | -| `jest.mock\(.*@skillsoft/gamut-styles` | Error | Same issue as above — mocking gamut-styles breaks token resolution | -| `from '@skillsoft/gamut-tests'` | Good — report count of files using it | Correct import for `setupRtl` and `MockGamutProvider` | +| Pattern | Verdict | Reason | +| ----------------------------------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `jest.mock\(.*@skillsoft/gamut` | Error | Manual mocking bypasses theme context and produces false-positive tests; prefer `setupRtl` from `@skillsoft/gamut-tests` (or a harness + `setupRtl`); use raw `MockGamutProvider` + `render` only for rare one-offs or Storybook mocks | +| `jest.mock\(.*@skillsoft/gamut-styles` | Error | Same issue as above — mocking gamut-styles breaks token resolution | +| `from '@skillsoft/gamut-tests'` | Good — report count of files using it | Correct import for `setupRtl` and `MockGamutProvider` | | `from 'component-test-setup'` (without gamut-tests) | Warning | Should import `setupRtl` from `@skillsoft/gamut-tests`, not directly from `component-test-setup` — the gamut-tests wrapper adds `MockGamutProvider` automatically | -| `new GamutProvider` or `)`, and + set `data-color-mode` to `contentModes[palette]`. The palette color is the + same in every mode, so the flip can't change the background under it. +- `Background` reads `theme.contentModes` when the theme has it, and only + falls back to polished's contrast math on themes that don't. +- Example: Storybook `Foundations/Color system/CSS contract`. ## Commands ```sh yarn workspace @skillsoft/gamut-styles build:tokens # regenerate src/tokens/generated +yarn workspace @skillsoft/gamut-styles validate:tokens # full report (CSS round-trip, tenant probes) yarn workspace @skillsoft/gamut-styles dump:sandbox # refresh the sandbox fixture -yarn workspace @skillsoft/gamut-styles validate:tokens # parity with design's reference fixture and CSS round-trip; prints tenant probes -npx nx test gamut-styles # guarantees; values.ts and tokens.css are current +npx nx test gamut-styles # guarantees, sandbox parity, generated files current; in CI ``` -Re-run `build:tokens` after any change to a brand input or to the generator, -and commit the output with the change. +## Naming -## What CI checks +- Primitives: `--color-palette--`, e.g. `--color-palette-primary-800`. + Slots: `neutral-light`, `neutral-dark`, `primary`, `accent`, `accent-alt`, + `danger`, `warning`, `success`. Not for components; use roles. +- Translucent primitives: `--color-palette-primary-600-a6` (6% alpha), `--color-black-a60`. +- Roles: `--color-`, e.g. `--color-action-primary`. +- `white` / `black` keep their bare names (boundary tokens). -- `nx test` runs in CI. It checks the guarantees, and that `values.ts` and - `tokens.css` match the generator. -- It does not check the JSON token files. That's a known gap, listed below. -- `validate:tokens` is not run in CI. It's the full report, run by hand. +The `--color-` prefix on primitives is there because variance's `addColors` +always emits `--color-`; keeping one name for both the Emotion and CSS +routes beat the shorter `--palette-*`. ## Known gaps -- The JSON token files are not covered by the drift test; only `values.ts` and - `tokens.css` are. -- Brand input isn't validated at the boundary. A non-finite hue is accepted and - produces a gray ramp, and a brand `name` is written into generated comments - unchecked. -- The generator's type declarations are emitted into the package build, because - `tsconfig.lib.json` doesn't exclude this folder. Nothing in the public exports - imports them. -- Some exported tables are mutable (`DEFAULT_HUES`, `CHROMA_CURVE`, - `FIXED_LIGHTNESS`, `FALLOFF`, `ROLES`). A consumer that changes one changes - later builds in the same process. -- `migration.ts` is a rename table: each alias is a rename, merge, split or - retire. Only rename and merge are mechanical. The codemod keeps its own copy - of the alias table, so the two can drift. -- The categorical solver isn't ported, so the `viz-*` chart colors aren't - re-solved for a new brand. -- The reference fixture measures component slots on rounded hex, and this - generator measures OKLCH floats. The 200-pairing check and its 0.074 ratio - spread come from that comparison and aren't reproduced by a test in this - package. - -## Naming - -Output names are defined in the gamut-styles README. The generator's own -naming rule: a token's name comes from its slot or role, not from its hue, so -changing a brand's hue doesn't rename anything. +- The categorical solver isn't ported. `viz-1…8` use the slot/grade order the + sandbox solved for Skillsoft's hues; a tenant gets that order unsolved. +- The theme carries sRGB, not OKLCH: `Background` and `BarChart` run + polished's `getContrast` on theme values, and polished can't parse `oklch()`. +- Components that reference core primitives directly (`navy-900`, `gray-100`, + `yellow`, …) render wrong on this theme. Listed in Base Camp `mapping.md`. +- gamut is typed against `skillsoftTheme`, which has no legacy names. The + other themes are still exported but are no longer what components are + checked against. `ColorMode` still works on them (`roleOr` falls back to + the old names). +- `migration.ts` is design's rename table: each alias is a rename, merge, + split or retire. Only rename and merge are mechanical. It isn't shipped in + the tokens output; the codemod reads it from source. +- The sandbox measures component slots on rounded hex; this port measures + OKLCH floats. Same 200 pairings, ratios within 0.074, same pass/fail. diff --git a/packages/gamut-styles/src/index.ts b/packages/gamut-styles/src/index.ts index b704f84fb9..717b16f7b8 100644 --- a/packages/gamut-styles/src/index.ts +++ b/packages/gamut-styles/src/index.ts @@ -5,6 +5,7 @@ export * from './GamutProvider'; export * from './ColorMode'; export * from './Background'; export * from './variance'; +export * from './static'; export * from './cache'; export * from './variables'; export * from './styles'; diff --git a/packages/gamut-styles/src/static/README.md b/packages/gamut-styles/src/static/README.md new file mode 100644 index 0000000000..144b164e9d --- /dev/null +++ b/packages/gamut-styles/src/static/README.md @@ -0,0 +1,80 @@ +# Static styles (groundwork) + +Groundwork for moving gamut's components off runtime Emotion styles. Nothing +uses it yet: no component has moved, and `GamutProvider` still delivers the +color tokens. Notes and the plan are in Base Camp, `gamut/color-tokens/how-it-works.md` +("Moving off Emotion, slowly"). + +Today every gamut component serializes its styles with Emotion on each render. +A component on a recipe ships CSS compiled ahead of time and only picks class +names at render. + +## The pieces + +| Export | What | +| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `defineRecipe` | A component's styles as data: `className`, `base`, `variants` (named groups), `defaultVariants`, `compoundVariants`. Chakra's and Panda's recipe shape, written with the same style objects as `css()` and `variant()` | +| `recipe.styles` | The recipe as an Emotion style function, for `styled.div(recipe.styles)`. Writes what the matching `variant()` calls would | +| `recipe.classNames(props)` | `gamut-chip gamut-chip--intent_danger gamut-chip--size_sm`. Defaults filled in; `false` turns a group off | +| `compileRecipe(recipe)` | Plain CSS for those class names, in `@layer recipes` | +| `checkRecipe(recipe)` | Whether the recipe compiles the same for every brand theme. If not, it can't be static yet | +| `` | Puts compiled CSS on the page once, through the Emotion cache (SSR, nonce and stylis plugins work as they do now) | + +## Moving a component + +1. Rewrite its `variant()` calls as one recipe and style it with + `recipe.styles`. Still Emotion; snapshots and visuals shouldn't change. This + step is safe to ship alone. +2. Add `expect(checkRecipe(recipe).pass).toBe(true)` to its tests. If it fails, + the recipe uses a per-brand value (see the rules) and stays on step 1. +3. Compile it once at module scope and render it: + + ```tsx + const styles = compileRecipe(chip); + + export const Chip = ({ className, ...props }: ChipProps) => { + const [variants, rest] = chip.splitProps(props); + return ( + <> + + + + ); + }; + ``` + + Style props and `styled(Chip)` still work and still win: they're Emotion, + unlayered, and the recipe is in `@layer recipes`. + +4. Later, a build step writes `compileRecipe` output to a generated module (or + a stylesheet), so it doesn't run in the browser at all. Components don't + change for that. + +## Rules for a recipe that compiles + +- **Colors are roles.** They compile to `var(--color-)`, the same for + every brand and mode. +- **No per-brand values.** Fonts, font weights and elevation are still literal + values per theme, so `fontFamily: 'accent'` fails `checkRecipe`. Those move + to CSS variables in phase 2 of the plan. +- **No render-time values.** Nothing from props beyond the variant groups, no + `useTheme()`, no polished math. Those stay in Emotion next to the recipe. +- **Always logical properties.** Static CSS can't follow `useLogicalProperties`, + so `height` compiles to `block-size` and `px` to `padding-inline-*`. Same + rendering in left-to-right horizontal text; under `dir="rtl"` inline spacing + follows the text direction even if the app uses physical mode. +- **`className` is unique,** `gamut-`. It's the class prefix and the + `StaticStyles` id. + +## Not done + +- No build step: `compileRecipe` runs once per module in the browser. +- No boolean variants yet, so `states()` (`disabled`, `active`, …) stays in + Emotion next to the recipe. Panda and Chakra take `true` / `false` keys; + that's the shape to add. +- Delivery is the Emotion cache. React 19's `