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;