From aef75e92cb5f9739c9483243463e473949cbe69e Mon Sep 17 00:00:00 2001 From: dreamwasp Date: Mon, 9 Nov 2026 11:23:46 -0500 Subject: [PATCH 1/2] feat(gamut-styles): groundwork for static component styles Adds defineRecipe, compileRecipe, checkRecipe and StaticStyles. No component uses them yet. The jest base setup skips window.scrollTo in node-environment tests. Co-Authored-By: Claude Haiku 5.5 --- .changeset/static-styles-groundwork.md | 9 + packages/gamut-styles/src/index.ts | 1 + packages/gamut-styles/src/static/README.md | 80 ++++++++ .../gamut-styles/src/static/StaticStyles.tsx | 74 +++++++ .../__tests__/StaticStyles.ssr.test.tsx | 36 ++++ .../src/static/__tests__/recipe.test.tsx | 157 +++++++++++++++ .../gamut-styles/src/static/compileRecipe.ts | 128 ++++++++++++ .../gamut-styles/src/static/defineRecipe.ts | 182 ++++++++++++++++++ packages/gamut-styles/src/static/index.ts | 3 + script/jest/base-setup.js | 5 +- 10 files changed, 674 insertions(+), 1 deletion(-) create mode 100644 .changeset/static-styles-groundwork.md create mode 100644 packages/gamut-styles/src/static/README.md create mode 100644 packages/gamut-styles/src/static/StaticStyles.tsx create mode 100644 packages/gamut-styles/src/static/__tests__/StaticStyles.ssr.test.tsx create mode 100644 packages/gamut-styles/src/static/__tests__/recipe.test.tsx create mode 100644 packages/gamut-styles/src/static/compileRecipe.ts create mode 100644 packages/gamut-styles/src/static/defineRecipe.ts create mode 100644 packages/gamut-styles/src/static/index.ts 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-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..62ef3c0a5c --- /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(--colors-)`, 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 `