From 820e3e007c21e2095b9d66960cf702497f3cfd07 Mon Sep 17 00:00:00 2001 From: Luke Bennett Date: Thu, 23 Jul 2026 17:30:42 +1000 Subject: [PATCH] Migrate single-part recipes to recipe() and drop recipeInLayer (#224) * Migrate remaining single-part recipes to the recipe() helper Sweep button, button-composed, icon, icon-button, link, loading-spinner, and text from recipeInLayer() to the recipe() architecture from #223, keeping each file's call count and order 1:1 so the emitted stylesheet stays byte-identical. loading-skeleton stays on styleInLayer/globalStyleInLayer since it has no variants to recipe-ify. recipeInLayer is now unused and removed from styles/layered-style.css.ts; styleInLayer and globalStyleInLayer are untouched. Also updates the turbo-generators recipe scaffold template to the new helper. * Remove changeset --- .../react/src/recipes/button-composed.css.ts | 12 ++-- .../@luke-ui/react/src/recipes/button.css.ts | 9 +-- .../react/src/recipes/icon-button.css.ts | 11 ++-- .../@luke-ui/react/src/recipes/icon.css.ts | 8 +-- .../@luke-ui/react/src/recipes/link.css.ts | 9 +-- .../react/src/recipes/loading-spinner.css.ts | 15 ++--- packages/@luke-ui/react/src/recipes/recipe.ts | 6 +- .../@luke-ui/react/src/recipes/text.css.ts | 9 +-- .../react/src/styles/layered-style.css.ts | 63 ------------------- .../src/component-creation-plan.ts | 4 +- 10 files changed, 44 insertions(+), 102 deletions(-) diff --git a/packages/@luke-ui/react/src/recipes/button-composed.css.ts b/packages/@luke-ui/react/src/recipes/button-composed.css.ts index 6a4f4a23..5fe57765 100644 --- a/packages/@luke-ui/react/src/recipes/button-composed.css.ts +++ b/packages/@luke-ui/react/src/recipes/button-composed.css.ts @@ -1,8 +1,8 @@ -import type { RecipeVariants } from '@vanilla-extract/recipes'; -import { recipeInLayer } from '../styles/layered-style.css.js'; import { vars } from '../theme/contract.css.js'; +import type { RecipeSelection } from './recipe.js'; +import { recipe } from './recipe.js'; -export const buttonContent = recipeInLayer('recipes', { +export const buttonContent = recipe({ base: { alignItems: 'center', display: 'inline-flex', @@ -11,7 +11,7 @@ export const buttonContent = recipeInLayer('recipes', { }, }); -export const buttonLabel = recipeInLayer('recipes', { +export const buttonLabel = recipe({ base: { alignItems: 'center', display: 'inline-flex', @@ -31,9 +31,9 @@ export const buttonLabel = recipeInLayer('recipes', { }, }); -export type ButtonLabelVariants = RecipeVariants; +export type ButtonLabelVariants = RecipeSelection; -export const spinnerOverlay = recipeInLayer('recipes', { +export const spinnerOverlay = recipe({ base: { '@media': { '(forced-colors: active)': { diff --git a/packages/@luke-ui/react/src/recipes/button.css.ts b/packages/@luke-ui/react/src/recipes/button.css.ts index fa25d032..aa30387b 100644 --- a/packages/@luke-ui/react/src/recipes/button.css.ts +++ b/packages/@luke-ui/react/src/recipes/button.css.ts @@ -1,7 +1,8 @@ -import type { RecipeVariants } from '@vanilla-extract/recipes'; import { focusRing } from '../styles/focus-ring.js'; -import { recipeInLayer, styleInLayer } from '../styles/layered-style.css.js'; +import { styleInLayer } from '../styles/layered-style.css.js'; import { vars } from '../theme/contract.css.js'; +import type { RecipeSelection } from './recipe.js'; +import { recipe } from './recipe.js'; const base = styleInLayer('recipes', { '@media': { @@ -95,7 +96,7 @@ const base = styleInLayer('recipes', { }); /** Semantic appearance and material recipe shared by Button and IconButton. */ -export const button = recipeInLayer('recipes', { +export const button = recipe({ base, defaultVariants: { appearance: 'solid', @@ -173,7 +174,7 @@ export const button = recipeInLayer('recipes', { ], }); -export type ButtonVariants = RecipeVariants; +export type ButtonVariants = RecipeSelection; type Tone = 'neutral' | 'accent' | 'danger'; type Surface = { diff --git a/packages/@luke-ui/react/src/recipes/icon-button.css.ts b/packages/@luke-ui/react/src/recipes/icon-button.css.ts index 560349c7..69f5852d 100644 --- a/packages/@luke-ui/react/src/recipes/icon-button.css.ts +++ b/packages/@luke-ui/react/src/recipes/icon-button.css.ts @@ -1,6 +1,7 @@ -import type { RecipeVariants } from '@vanilla-extract/recipes'; -import { recipeInLayer, styleInLayer } from '../styles/layered-style.css.js'; +import { styleInLayer } from '../styles/layered-style.css.js'; import { vars } from '../theme/contract.css.js'; +import type { RecipeSelection } from './recipe.js'; +import { recipe } from './recipe.js'; export const iconButtonReset = styleInLayer('utilities', { '@media': { @@ -29,7 +30,7 @@ export const iconButtonReset = styleInLayer('utilities', { }, }); -export const icon = recipeInLayer('recipes', { +export const icon = recipe({ defaultVariants: { isPending: false }, variants: { isPending: { @@ -40,7 +41,7 @@ export const icon = recipeInLayer('recipes', { }); /** Vanilla-extract recipe for the `IconButton` primitive's styles. */ -export const iconButton = recipeInLayer('recipes', { +export const iconButton = recipe({ variants: { size: { medium: { @@ -54,4 +55,4 @@ export const iconButton = recipeInLayer('recipes', { }); /** Variant type for the `IconButton` recipe. */ -export type IconButtonVariants = RecipeVariants; +export type IconButtonVariants = RecipeSelection; diff --git a/packages/@luke-ui/react/src/recipes/icon.css.ts b/packages/@luke-ui/react/src/recipes/icon.css.ts index 42e7a43d..98d9a1dd 100644 --- a/packages/@luke-ui/react/src/recipes/icon.css.ts +++ b/packages/@luke-ui/react/src/recipes/icon.css.ts @@ -1,6 +1,6 @@ -import type { RecipeVariants } from '@vanilla-extract/recipes'; -import { recipeInLayer } from '../styles/layered-style.css.js'; import { vars } from '../theme/contract.css.js'; +import type { RecipeSelection } from './recipe.js'; +import { recipe } from './recipe.js'; /** Shared size dimensions for Icon and LoadingSpinner (icon-aligned sizing). */ export const iconSizeVariants = { @@ -23,7 +23,7 @@ export const iconSizeVariants = { } as const; /** Vanilla-extract recipe for the `Icon` primitive's styles. */ -export const icon = recipeInLayer('recipes', { +export const icon = recipe({ base: { display: 'inline-flex', flexShrink: 0, @@ -37,4 +37,4 @@ export const icon = recipeInLayer('recipes', { }); /** Variant type for the `Icon` recipe. */ -export type IconVariants = RecipeVariants; +export type IconVariants = RecipeSelection; diff --git a/packages/@luke-ui/react/src/recipes/link.css.ts b/packages/@luke-ui/react/src/recipes/link.css.ts index 7dcb2310..a4b53b8a 100644 --- a/packages/@luke-ui/react/src/recipes/link.css.ts +++ b/packages/@luke-ui/react/src/recipes/link.css.ts @@ -1,7 +1,8 @@ -import type { RecipeVariants } from '@vanilla-extract/recipes'; import { focusRing } from '../styles/focus-ring.js'; -import { recipeInLayer, styleInLayer } from '../styles/layered-style.css.js'; +import { styleInLayer } from '../styles/layered-style.css.js'; import { vars } from '../theme/contract.css.js'; +import type { RecipeSelection } from './recipe.js'; +import { recipe } from './recipe.js'; const base = styleInLayer('recipes', { '@media': { @@ -46,7 +47,7 @@ const base = styleInLayer('recipes', { }); /** Vanilla-extract recipe for the `Link` primitive's styles. */ -export const link = recipeInLayer('recipes', { +export const link = recipe({ base, defaultVariants: { isStandalone: false, @@ -99,4 +100,4 @@ export const link = recipeInLayer('recipes', { }); /** Variant type for the `Link` recipe. */ -export type LinkVariants = RecipeVariants; +export type LinkVariants = RecipeSelection; diff --git a/packages/@luke-ui/react/src/recipes/loading-spinner.css.ts b/packages/@luke-ui/react/src/recipes/loading-spinner.css.ts index 83d7c251..d204748d 100644 --- a/packages/@luke-ui/react/src/recipes/loading-spinner.css.ts +++ b/packages/@luke-ui/react/src/recipes/loading-spinner.css.ts @@ -1,8 +1,9 @@ import { keyframes } from '@vanilla-extract/css'; -import type { RecipeVariants } from '@vanilla-extract/recipes'; -import { recipeInLayer, styleInLayer } from '../styles/layered-style.css.js'; +import { styleInLayer } from '../styles/layered-style.css.js'; import { vars } from '../theme/contract.css.js'; import { iconSizeVariants } from './icon.css.js'; +import type { RecipeSelection } from './recipe.js'; +import { recipe } from './recipe.js'; const rotationDuration = '1.2s'; const rubberBandDuration = '2s'; @@ -25,7 +26,7 @@ const base = styleInLayer('recipes', { }); /** Vanilla-extract recipe for the `LoadingSpinner` primitive's styles. */ -export const spinner = recipeInLayer('recipes', { +export const spinner = recipe({ base, defaultVariants: { size: 'medium', @@ -37,14 +38,14 @@ export const spinner = recipeInLayer('recipes', { }); /** Variant type for the `LoadingSpinner` recipe. */ -export type LoadingSpinnerVariants = RecipeVariants; +export type LoadingSpinnerVariants = RecipeSelection; /** @internal */ export const spinAnimationName = keyframes({ to: { transform: 'rotate(360deg)' }, }); -export const spinnerState = recipeInLayer('recipes', { +export const spinnerState = recipe({ defaultVariants: { mode: 'determinate', }, @@ -65,7 +66,7 @@ export const spinnerState = recipeInLayer('recipes', { }, }); -export const svg = recipeInLayer('recipes', { +export const svg = recipe({ base: { blockSize: '100%', display: 'block', @@ -81,7 +82,7 @@ export const rubberBandAnimationName = keyframes({ '100%': { strokeDasharray: '2 100', strokeDashoffset: -100 }, }); -export const indicator = recipeInLayer('recipes', { +export const indicator = recipe({ base: { strokeDasharray: '100 100', }, diff --git a/packages/@luke-ui/react/src/recipes/recipe.ts b/packages/@luke-ui/react/src/recipes/recipe.ts index 40f4fc68..e12a59a4 100644 --- a/packages/@luke-ui/react/src/recipes/recipe.ts +++ b/packages/@luke-ui/react/src/recipes/recipe.ts @@ -14,8 +14,8 @@ import { cx } from '../utils/index.js'; * * `recipe()` runs at build time inside a `.css.ts` module. It splits a slotted * config into one recipe per slot, in declaration order, so its generated CSS and - * class names match hand-written per-slot recipes byte-for-byte, and it applies the - * `recipeInLayer` wrapping inline. Vanilla Extract only serialises `.css.ts` + * class names match hand-written per-slot recipes byte-for-byte, and it wraps every + * emitted style in the `recipes` cascade layer inline. Vanilla Extract only serialises `.css.ts` * exports, and a plain `.ts` helper cannot import a value from a function-exporting * `.css.ts` (such as `layered-style`) without turning that module into a failing * serialization boundary, so the `recipes`-layer wrapping is applied here directly @@ -269,7 +269,7 @@ function buildSlottedDescriptor(config: AnyMultiPartConfig): SlottedRecipeDescri } // --------------------------------------------------------------------------- -// Layer wrapping (mirrors `recipeInLayer` for the `recipes` layer) +// Layer wrapping (wraps every style in the `recipes` layer) // --------------------------------------------------------------------------- type LayeredStyleRule = DistributiveOmit; diff --git a/packages/@luke-ui/react/src/recipes/text.css.ts b/packages/@luke-ui/react/src/recipes/text.css.ts index c9e56c67..ff31bdb1 100644 --- a/packages/@luke-ui/react/src/recipes/text.css.ts +++ b/packages/@luke-ui/react/src/recipes/text.css.ts @@ -1,9 +1,10 @@ import type { ComplexStyleRule } from '@vanilla-extract/css'; -import type { RecipeVariants } from '@vanilla-extract/recipes'; -import { styleInLayer, recipeInLayer } from '../styles/layered-style.css.js'; +import { styleInLayer } from '../styles/layered-style.css.js'; import { vars } from '../theme/contract.css.js'; import { fontSizeSteps } from '../theme/contract.js'; import type { FontSizeStep } from '../theme/contract.js'; +import type { RecipeSelection } from './recipe.js'; +import { recipe } from './recipe.js'; /** Typography size steps. */ export type TextSize = FontSizeStep; @@ -147,7 +148,7 @@ function createLayeredTextStyle({ } /** Vanilla-extract recipe for the `Text` primitive's styles. */ -export const text = recipeInLayer('recipes', { +export const text = recipe({ base, compoundVariants: sizeStepCompoundVariants, defaultVariants: { @@ -219,4 +220,4 @@ export const text = recipeInLayer('recipes', { }); /** Aggregate variant type for the `Text` recipe. */ -export type TextVariants = RecipeVariants; +export type TextVariants = RecipeSelection; diff --git a/packages/@luke-ui/react/src/styles/layered-style.css.ts b/packages/@luke-ui/react/src/styles/layered-style.css.ts index b33bc569..260be8b6 100644 --- a/packages/@luke-ui/react/src/styles/layered-style.css.ts +++ b/packages/@luke-ui/react/src/styles/layered-style.css.ts @@ -1,29 +1,11 @@ import type { GlobalStyleRule, StyleRule } from '@vanilla-extract/css'; import { globalStyle as vanillaGlobalStyle, style as vanillaStyle } from '@vanilla-extract/css'; -import { recipe as vanillaRecipe } from '@vanilla-extract/recipes'; import type { DistributiveOmit } from '../types/distributive-omit.js'; import type { LayerName } from './layers.css.js'; import { layers } from './layers.css.js'; type LayeredStyleRule = DistributiveOmit; type LayeredGlobalStyleRule = DistributiveOmit; -type RecipeStyleRule = LayeredStyleRule | string; -type RecipeVariantDefinitions = Record; -type RecipeVariantGroups = Record; -type BooleanMap = T extends 'true' | 'false' ? boolean : T; -type RecipeVariantSelection = { - [VariantGroup in keyof Variants]?: BooleanMap | undefined; -}; -type RecipeCompoundVariant = { - variants: RecipeVariantSelection; - style: RecipeStyleRule; -}; -type RecipeOptions = { - base?: RecipeStyleRule; - variants?: Variants; - defaultVariants?: RecipeVariantSelection; - compoundVariants?: Array>; -}; function withLayer(layer: LayerName, rule: LayeredStyleRule): StyleRule { return { @@ -41,10 +23,6 @@ function withLayerGlobal(layer: LayerName, rule: LayeredGlobalStyleRule): Global }; } -function withLayerIfStyleRule(layer: LayerName, styleRule: RecipeStyleRule): RecipeStyleRule { - return typeof styleRule === 'string' ? styleRule : withLayer(layer, styleRule); -} - export function globalStyleInLayer( layer: LayerName, selector: string, @@ -56,44 +34,3 @@ export function globalStyleInLayer( export function styleInLayer(layer: LayerName, rule: LayeredStyleRule, debugId?: string): string { return vanillaStyle(withLayer(layer, rule), debugId); } - -export function recipeInLayer( - layer: LayerName, - options: RecipeOptions, - debugId?: string, -) { - const layeredVariants = - options.variants === undefined - ? undefined - : (Object.fromEntries( - Object.entries(options.variants).map(([variantName, variantValues]) => [ - variantName, - Object.fromEntries( - Object.entries(variantValues).map(([variantValue, styleRule]) => [ - variantValue, - withLayerIfStyleRule(layer, styleRule), - ]), - ), - ]), - ) as Variants); - - const layeredCompoundVariants = - options.compoundVariants === undefined - ? undefined - : options.compoundVariants.map((compound) => ({ - ...compound, - style: withLayerIfStyleRule(layer, compound.style), - })); - - return vanillaRecipe( - { - ...options, - ...(options.base === undefined ? {} : { base: withLayerIfStyleRule(layer, options.base) }), - ...(layeredVariants === undefined ? {} : { variants: layeredVariants }), - ...(layeredCompoundVariants === undefined - ? {} - : { compoundVariants: layeredCompoundVariants }), - }, - debugId, - ); -} diff --git a/packages/turbo-generators/src/component-creation-plan.ts b/packages/turbo-generators/src/component-creation-plan.ts index 878a870d..3b63fd5e 100644 --- a/packages/turbo-generators/src/component-creation-plan.ts +++ b/packages/turbo-generators/src/component-creation-plan.ts @@ -275,9 +275,9 @@ import { story } from '../../../../src/${input.name}/${input.name}.story'; } function renderRecipe(input: { camelName: string; pascalName: string }): string { - return `import { recipeInLayer } from '../styles/layered-style.css.js'; + return `import { recipe } from './recipe.js'; -export const ${input.camelName} = recipeInLayer('recipes', { +export const ${input.camelName} = recipe({ \tbase: { \t\tdisplay: 'inline-flex', \t}, -- 2.51.2