From 9c277d0b8d437f8ae702c78ae747253dc86e7bb6 Mon Sep 17 00:00:00 2001 From: Luke Bennett Date: Sun, 26 Jul 2026 09:55:11 +1000 Subject: [PATCH] Delete the recipe code nothing uses, and the leftovers (#272) The recipe engine had two features with no consumers. `extend` was declared on both config shapes and implemented across three functions, including the recursive config merging that was also the only untested logic in the file. Slotted compound variants were similar: the four slot-using recipes declare no compound variants, and the two recipes that do are both single part. Deleting both takes recipe.ts from 457 to 354 lines. Single-part compound variants stay, because button and text use them. Re-add either the day a second recipe needs it. Delete the nine hand-written Text variant types rather than deriving them. Four string unions and three key arrays duplicated what the recipe already knows with nothing type-checking the two against each other, so adding a wrap value or a colour role would have gone stale silently. All nine were exported publicly with no consumers, invisible to knip because the barrel is an entry point. Text's own component already derives its prop types by indexing TextVariants, and button, field and icon already export only the aggregate, so the barrel now matches that convention. input-states.css.ts emits no CSS at all, yet the suffix marked it as having side effects and kept it out of consumer tree shaking. Renamed to input-states.ts. The barrel exported loadingSkeleton as a plain class string among twelve callable recipes, which invited a consumer to call it; it is now loadingSkeletonClassName. Renaming rather than wrapping it in recipe() keeps the emitted CSS untouched. Two recipes also both exported something named `icon`, so icon-button's is now iconButtonIcon. The emitted stylesheet is byte-identical at 78007 bytes and the v2 goldens are unchanged, which is expected: the deleted code paths were never exercised, and the renames do not reach content-hashed class names. --- .../@luke-ui/react/src/icon-button/index.tsx | 2 +- .../react/src/loading-skeleton/index.tsx | 2 +- .../react/src/recipes/combobox.css.ts | 2 +- .../react/src/recipes/icon-button.css.ts | 2 +- packages/@luke-ui/react/src/recipes/index.ts | 15 +-- .../{input-states.css.ts => input-states.ts} | 0 .../recipes/loading-skeleton.browser.test.ts | 6 +- .../react/src/recipes/loading-skeleton.css.ts | 26 ++-- packages/@luke-ui/react/src/recipes/recipe.ts | 123 ++---------------- .../react/src/recipes/text-input.css.ts | 2 +- .../@luke-ui/react/src/recipes/text.css.ts | 42 +----- .../src/styles/stylesheet-contract.test.ts | 2 +- 12 files changed, 37 insertions(+), 187 deletions(-) rename packages/@luke-ui/react/src/recipes/{input-states.css.ts => input-states.ts} (100%) diff --git a/packages/@luke-ui/react/src/icon-button/index.tsx b/packages/@luke-ui/react/src/icon-button/index.tsx index 5811c4b0..96f952e2 100644 --- a/packages/@luke-ui/react/src/icon-button/index.tsx +++ b/packages/@luke-ui/react/src/icon-button/index.tsx @@ -53,7 +53,7 @@ export function IconButton(props: IconButtonProps): JSX.Element { isPending={isPending} size={size} > - + ); } diff --git a/packages/@luke-ui/react/src/loading-skeleton/index.tsx b/packages/@luke-ui/react/src/loading-skeleton/index.tsx index 88b44bf2..5dc3bf03 100644 --- a/packages/@luke-ui/react/src/loading-skeleton/index.tsx +++ b/packages/@luke-ui/react/src/loading-skeleton/index.tsx @@ -77,7 +77,7 @@ export function LoadingSkeleton(props: LoadingSkeletonProps): ReactNode { *`, { +globalStyleInLayer('recipes', `${loadingSkeletonClassName}:not([data-skeleton-inline]) > *`, { ...surface, ...pulse, borderRadius: `var(${skeletonRadiusVar}, 0px)`, @@ -87,7 +87,7 @@ globalStyleInLayer('recipes', `${loadingSkeleton}:not([data-skeleton-inline]) > position: 'relative !important' as 'relative', }); -globalStyleInLayer('recipes', `${loadingSkeleton}:not([data-skeleton-inline]) > * *`, { +globalStyleInLayer('recipes', `${loadingSkeletonClassName}:not([data-skeleton-inline]) > * *`, { '@media': { '(forced-colors: active)': forcedColorsSurface, }, @@ -96,11 +96,15 @@ globalStyleInLayer('recipes', `${loadingSkeleton}:not([data-skeleton-inline]) > // A pseudo-element painted over the child covers visuals the forced styles can't reach (nested backgrounds, // rounded corners); `inset: -1px` also covers the child's border box edges. -globalStyleInLayer('recipes', `${loadingSkeleton}:not([data-skeleton-inline]) > *::after`, { - ...surface, - ...pulse, - borderRadius: `var(${skeletonRadiusVar}, 0px)`, - content: '""', - inset: '-1px', - position: 'absolute', -}); +globalStyleInLayer( + 'recipes', + `${loadingSkeletonClassName}:not([data-skeleton-inline]) > *::after`, + { + ...surface, + ...pulse, + borderRadius: `var(${skeletonRadiusVar}, 0px)`, + content: '""', + inset: '-1px', + position: 'absolute', + }, +); diff --git a/packages/@luke-ui/react/src/recipes/recipe.ts b/packages/@luke-ui/react/src/recipes/recipe.ts index e12a59a4..af7a227e 100644 --- a/packages/@luke-ui/react/src/recipes/recipe.ts +++ b/packages/@luke-ui/react/src/recipes/recipe.ts @@ -54,7 +54,6 @@ interface SinglePartConfig { variants?: Variants; defaultVariants?: VariantSelection; compoundVariants?: Array>; - extend?: SinglePartConfig; } /** The runtime function a single-part `recipe()` returns. */ @@ -73,19 +72,11 @@ type SlotVariantSelection> = { -readonly [Group in keyof Variants]?: BooleanMap | undefined; }; -/** A compound variant for a slotted recipe, whose style is keyed by slot. */ -interface SlotCompoundVariant> { - variants: SlotVariantSelection; - style: SlotStyles; -} - /** Slotted recipe config. */ interface MultiPartConfig> { slots: Record; variants?: Variants; defaultVariants?: SlotVariantSelection; - compoundVariants?: Array>; - extend?: MultiPartConfig>; } /** A single slot function: takes an optional extra class and returns a class string. */ @@ -111,10 +102,6 @@ export interface SlottedConfigInput { slots: Record; variants?: Record>>; defaultVariants?: Record; - compoundVariants?: ReadonlyArray<{ - variants: Record; - style: Record; - }>; } /** Derives the outer variant selection type for a built recipe. */ @@ -175,45 +162,16 @@ function registerSerializer(fn: object, importName: string, args: ReadonlyArray< } function buildSinglePart(config: SinglePartConfig): BuiltRecipe { - const resolved = config.extend ? mergeSingleConfigs(config.extend, config) : config; - return recipeInRecipesLayer({ - ...(resolved.base === undefined ? {} : { base: resolved.base }), - ...(resolved.variants === undefined ? {} : { variants: resolved.variants }), - ...(resolved.defaultVariants === undefined - ? {} - : { defaultVariants: resolved.defaultVariants }), - ...(resolved.compoundVariants === undefined - ? {} - : { compoundVariants: resolved.compoundVariants }), + ...(config.base === undefined ? {} : { base: config.base }), + ...(config.variants === undefined ? {} : { variants: config.variants }), + ...(config.defaultVariants === undefined ? {} : { defaultVariants: config.defaultVariants }), + ...(config.compoundVariants === undefined ? {} : { compoundVariants: config.compoundVariants }), }); } -function mergeSingleConfigs( - base: SinglePartConfig, - override: SinglePartConfig, -): SinglePartConfig { - const variants: VariantGroups = { ...base.variants }; - if (override.variants !== undefined) { - for (const [group, values] of Object.entries(override.variants)) { - variants[group] = { ...variants[group], ...values }; - } - } - - const mergedBase = override.base ?? base.base; - - return { - ...(mergedBase === undefined ? {} : { base: mergedBase }), - ...(Object.keys(variants).length > 0 ? { variants } : {}), - defaultVariants: { ...base.defaultVariants, ...override.defaultVariants }, - compoundVariants: [...(base.compoundVariants ?? []), ...(override.compoundVariants ?? [])], - }; -} - function buildSlottedDescriptor(config: AnyMultiPartConfig): SlottedRecipeDescriptor { - const resolved = config.extend ? mergeConfigs(config.extend, withoutExtend(config)) : config; - - const slotNames = Object.keys(resolved.slots); + const slotNames = Object.keys(config.slots); const slots: Record = {}; const slotGroups: Record> = {}; @@ -221,8 +179,8 @@ function buildSlottedDescriptor(config: AnyMultiPartConfig): SlottedRecipeDescri const variants: Record> = {}; const groupsForSlot: Array = []; - if (resolved.variants !== undefined) { - for (const [group, values] of Object.entries(resolved.variants)) { + if (config.variants !== undefined) { + for (const [group, values] of Object.entries(config.variants)) { const slotValues: Record = {}; let hasSlot = false; @@ -241,26 +199,14 @@ function buildSlottedDescriptor(config: AnyMultiPartConfig): SlottedRecipeDescri } } - const compoundVariants: Array<{ variants: Record; style: RecipeStyleRule }> = - []; - if (resolved.compoundVariants !== undefined) { - for (const compound of resolved.compoundVariants) { - const style = compound.style[slotName]; - if (style !== undefined) { - compoundVariants.push({ variants: compound.variants, style }); - } - } - } - - const defaultVariants = pickGroups(resolved.defaultVariants, groupsForSlot); + const defaultVariants = pickGroups(config.defaultVariants, groupsForSlot); slots[slotName] = recipeInRecipesLayer({ - base: resolved.slots[slotName], + base: config.slots[slotName], ...(groupsForSlot.length > 0 ? { variants } : {}), ...(defaultVariants !== undefined && Object.keys(defaultVariants).length > 0 ? { defaultVariants } : {}), - ...(compoundVariants.length > 0 ? { compoundVariants } : {}), }); slotGroups[slotName] = groupsForSlot; } @@ -336,7 +282,7 @@ function recipeInRecipesLayer(options: RecipeInLayerOptions): BuiltRecipe { } // --------------------------------------------------------------------------- -// extend (single-base inheritance) +// Config shape detection // --------------------------------------------------------------------------- function isMultiPart( @@ -349,55 +295,6 @@ function isObject(value: unknown): value is Record { return typeof value === 'object' && value !== null; } -function withoutExtend(config: AnyMultiPartConfig): AnyMultiPartConfig { - const { extend: _extend, ...rest } = config; - return rest; -} - -/** - * Merges a slotted config's single `extend` base into it. Slots, variant groups, - * variant values, per-slot variant styles, and default variants are combined. The - * extending config takes precedence on same-named keys, and compound variants are - * concatenated. Internal to `extend` resolution. - */ -function mergeConfigs(...configs: Array): AnyMultiPartConfig { - const slots: Record = {}; - const variants: Record>> = {}; - const defaultVariants: SlotVariantSelection> = {}; - const compoundVariants: Array>> = []; - - for (const config of configs) { - const resolved = config.extend ? mergeConfigs(config.extend, withoutExtend(config)) : config; - - Object.assign(slots, resolved.slots); - - if (resolved.variants !== undefined) { - for (const [group, values] of Object.entries(resolved.variants)) { - const mergedGroup = variants[group] ?? {}; - for (const [value, slotStyles] of Object.entries(values)) { - mergedGroup[value] = { ...mergedGroup[value], ...slotStyles }; - } - variants[group] = mergedGroup; - } - } - - if (resolved.defaultVariants !== undefined) { - Object.assign(defaultVariants, resolved.defaultVariants); - } - - if (resolved.compoundVariants !== undefined) { - compoundVariants.push(...resolved.compoundVariants); - } - } - - return { - slots, - ...(Object.keys(variants).length > 0 ? { variants } : {}), - ...(Object.keys(defaultVariants).length > 0 ? { defaultVariants } : {}), - ...(compoundVariants.length > 0 ? { compoundVariants } : {}), - }; -} - // --------------------------------------------------------------------------- // Runtime (referenced by the function serializer at import time) // --------------------------------------------------------------------------- diff --git a/packages/@luke-ui/react/src/recipes/text-input.css.ts b/packages/@luke-ui/react/src/recipes/text-input.css.ts index ff2cfbc1..1985cf35 100644 --- a/packages/@luke-ui/react/src/recipes/text-input.css.ts +++ b/packages/@luke-ui/react/src/recipes/text-input.css.ts @@ -4,7 +4,7 @@ import { composeInputStateSelectors, descendantDisabledSelector, inputStates, -} from './input-states.css.js'; +} from './input-states.js'; import type { RecipeSelection, SlottedConfigInput } from './recipe.js'; import { recipe } from './recipe.js'; diff --git a/packages/@luke-ui/react/src/recipes/text.css.ts b/packages/@luke-ui/react/src/recipes/text.css.ts index 435254ef..8a755e14 100644 --- a/packages/@luke-ui/react/src/recipes/text.css.ts +++ b/packages/@luke-ui/react/src/recipes/text.css.ts @@ -7,43 +7,6 @@ import type { RecipeSelection } from './recipe.js'; import { recipe } from './recipe.js'; import { visuallyHiddenStyle } from './visually-hidden.css.js'; -/** Typography size steps. */ -export type TextSize = FontSizeStep; - -/** Semantic text colours. */ -export type TextColor = - | 'primary' - | 'secondary' - | 'accent' - | 'info' - | 'success' - | 'warning' - | 'danger'; -/** Semantic font-weight roles. */ -export type TextFontWeight = 'body' | 'label' | 'heading' | 'emphasis'; -/** Logical text alignment values. */ -export type TextAlign = 'start' | 'center' | 'end'; -/** Text wrapping values. */ -export type TextWrap = 'unset' | 'balance' | 'pretty'; - -const textDecorationKeys = ['none', 'underline', 'line-through', 'inherit'] as const; -/** Text decoration variant values. */ -export type TextDecoration = (typeof textDecorationKeys)[number]; - -const textTransformKeys = ['none', 'capitalize', 'uppercase', 'lowercase', 'inherit'] as const; -/** Text transform variant values. */ -export type TextTransform = (typeof textTransformKeys)[number]; - -const fontVariantNumericKeys = [ - 'unset', - 'diagonal-fractions', - 'ordinal', - 'slashed-zero', - 'tabular-nums', -] as const; -/** Numeric glyph variant values. */ -export type TextFontVariantNumeric = (typeof fontVariantNumericKeys)[number]; - const lineClampNone = {} satisfies ComplexStyleRule; const lineClampSingleLine = { display: 'block', @@ -73,9 +36,6 @@ const lineClampVariants = { 5: lineClampMultiLine(5), } as const; -/** Text line-clamp variant values. */ -export type TextLineClampVariant = keyof typeof lineClampVariants; - const base = styleInLayer('recipes', { color: vars.color.text.primary, fontFamily: vars.font.family, @@ -109,7 +69,7 @@ const sizeVariants = Object.fromEntries( lineHeight: vars.font[size].lineHeight, }, ]), -) as Record; +) as Record; const sizeStepCompoundVariants = fontSizeSteps.map((size) => { const { baselineTrim, capHeightTrim, fontSize, lineHeight } = vars.font[size]; diff --git a/packages/@luke-ui/react/src/styles/stylesheet-contract.test.ts b/packages/@luke-ui/react/src/styles/stylesheet-contract.test.ts index e1aab068..6abfe552 100644 --- a/packages/@luke-ui/react/src/styles/stylesheet-contract.test.ts +++ b/packages/@luke-ui/react/src/styles/stylesheet-contract.test.ts @@ -20,7 +20,7 @@ test('builds the public stylesheet with the retained layer contract', async () = const stylesheet = await readFile(new URL('../../dist/stylesheet.css', import.meta.url), 'utf8'); const recipes = await import('@luke-ui/react/recipes'); const styles = await import('@luke-ui/react/styles'); - const recipeClasses = [...recipes.icon().split(' '), recipes.loadingSkeleton]; + const recipeClasses = [...recipes.icon().split(' '), recipes.loadingSkeletonClassName]; const textClassesBySize = Object.fromEntries( fontSizeSteps.map((size) => [size, recipes.text({ size }).split(' ')]), ) as TextClassesBySize; -- 2.51.2