diff --git a/.changeset/strong-cases-grin.md b/.changeset/strong-cases-grin.md new file mode 100644 index 00000000..7bff9ea0 --- /dev/null +++ b/.changeset/strong-cases-grin.md @@ -0,0 +1,8 @@ +--- +"@mincho-js/css": patch +--- + +**rules** + +## New +- Add `rules.multiple()` API \ No newline at end of file diff --git a/packages/css/src/css/index.ts b/packages/css/src/css/index.ts index 2dc2725d..9fbef62c 100644 --- a/packages/css/src/css/index.ts +++ b/packages/css/src/css/index.ts @@ -123,15 +123,15 @@ function cssWith( return cssRaw(cssFunction(style)); } - function cssWithVariants< + function cssWithMultiple< StyleMap extends Record >(styleMap: StyleMap, debugId?: string): Record; - function cssWithVariants< + function cssWithMultiple< Data extends Record, Key extends keyof Data, MapData extends (value: Data[Key], key: Key) => ComplexCSSRule >(data: Data, mapData: MapData, debugId?: string): Record; - function cssWithVariants< + function cssWithMultiple< Data extends Record, MapData extends ( value: unknown, @@ -155,7 +155,7 @@ function cssWith( return Object.assign(cssWithImpl, { raw: cssWithRaw, - multiple: cssWithVariants + multiple: cssWithMultiple }); } @@ -165,7 +165,7 @@ export const css = Object.assign(cssImpl, { with: cssWith }); -// == CSS Variants ============================================================= +// == CSS Multiple ============================================================= // TODO: Need to optimize // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#smart_self-overwriting_lazy_getters // https://github.com/vanilla-extract-css/vanilla-extract/blob/master/packages/css/src/style.ts @@ -174,8 +174,7 @@ export function cssMultiple< >(styleMap: StyleMap, debugId?: string): Record; export function cssMultiple< Data extends Record, - Key extends keyof Data, - MapData extends (value: Data[Key], key: Key) => ComplexCSSRule + MapData extends (value: Data[keyof Data], key: keyof Data) => ComplexCSSRule >(data: Data, mapData: MapData, debugId?: string): Record; export function cssMultiple< StyleMap extends Record, @@ -192,22 +191,21 @@ export function cssMultiple< if (isMapDataFunction(mapDataOrDebugId)) { const data = styleMapOrData as Data; const mapData = mapDataOrDebugId; - return processVariants(data, mapData, debugId); + return processMultiple(data, mapData, debugId); } else { const styleMap = styleMapOrData as StyleMap; const debugId = mapDataOrDebugId; - return processVariants(styleMap, (style) => style, debugId); + return processMultiple(styleMap, (style) => style, debugId); } } function isMapDataFunction< Data extends Record, - Key extends keyof Data, - MapData extends (value: Data[Key], key: Key) => ComplexCSSRule + MapData extends (value: Data[keyof Data], key: keyof Data) => ComplexCSSRule >(mapDataOrDebugId?: MapData | string): mapDataOrDebugId is MapData { return typeof mapDataOrDebugId === "function"; } -function processVariants( +function processMultiple( items: Record, transformItem: (item: T, key: string | number) => ComplexCSSRule, debugId?: string diff --git a/packages/css/src/rules/index.ts b/packages/css/src/rules/index.ts index 41c05941..9cfa4f7d 100644 --- a/packages/css/src/rules/index.ts +++ b/packages/css/src/rules/index.ts @@ -27,7 +27,8 @@ import type { PropVars, Serializable, VariantStringMap, - VariantStyle + VariantStyle, + RecipeStyleRule } from "./types.js"; import { mapValues, @@ -185,6 +186,163 @@ export function rulesImpl< ); } +// Improved type-safe transformations that preserve pattern structure +type RuntimeFnFromPatternOptions = + Options extends PatternOptions< + infer Variants extends VariantGroups | undefined, + infer ToggleVariants extends VariantDefinitions | undefined, + infer Props extends ComplexPropDefinitions | undefined + > + ? RuntimeFn< + ConditionalVariants, + Exclude + > + : never; + +type TransformPatternMap< + T extends Record< + string | number, + PatternOptions< + VariantGroups | undefined, + VariantDefinitions | undefined, + ComplexPropDefinitions | undefined + > + > +> = { + [K in keyof T]: RuntimeFnFromPatternOptions; +}; + +type TransformDataMapping< + Data extends Record, + MapData extends ( + value: Data[keyof Data], + key: keyof Data + ) => PatternOptions< + VariantGroups | undefined, + VariantDefinitions | undefined, + ComplexPropDefinitions | undefined + > +> = { + [K in keyof Data]: MapData extends (value: Data[K], key: K) => infer Options + ? RuntimeFnFromPatternOptions + : RuntimeFnFromPatternOptions>; +}; + +export function rulesMultiple< + PatternMap extends Record< + string | number, + PatternOptions< + VariantGroups | undefined, + VariantDefinitions | undefined, + ComplexPropDefinitions | undefined + > + > +>(patternMap: PatternMap, debugId?: string): TransformPatternMap; +export function rulesMultiple< + Data extends Record, + MapData extends ( + value: Data[keyof Data], + key: keyof Data + ) => PatternOptions< + VariantGroups | undefined, + VariantDefinitions | undefined, + ComplexPropDefinitions | undefined + > +>( + data: Data, + mapData: MapData, + debugId?: string +): TransformDataMapping; +export function rulesMultiple< + PatternMap extends Record< + string | number, + PatternOptions< + VariantGroups | undefined, + VariantDefinitions | undefined, + ComplexPropDefinitions | undefined + > + >, + Data extends Record, + MapData extends ( + value: unknown, + key: string | number | symbol + ) => PatternOptions< + VariantGroups | undefined, + VariantDefinitions | undefined, + ComplexPropDefinitions | undefined + > +>( + patternMapOrData: PatternMap | Data, + mapDataOrDebugId?: MapData | string, + debugId?: string +): TransformPatternMap | TransformDataMapping { + if (isMapDataFunction(mapDataOrDebugId)) { + const data = patternMapOrData as Data; + const mapData = mapDataOrDebugId; + return processMultipleRules(data, mapData, debugId) as TransformDataMapping< + Data, + MapData + >; + } else { + const patternMap = patternMapOrData as PatternMap; + const debugId = mapDataOrDebugId; + return processMultipleRules( + patternMap, + (pattern) => pattern, + debugId + ) as TransformPatternMap; + } +} + +function isMapDataFunction< + Data extends Record, + Key extends keyof Data, + MapData extends ( + value: Data[Key], + key: Key + ) => PatternOptions< + VariantGroups | undefined, + VariantDefinitions | undefined, + ComplexPropDefinitions | undefined + > +>(mapDataOrDebugId?: MapData | string): mapDataOrDebugId is MapData { + return typeof mapDataOrDebugId === "function"; +} +function processMultipleRules< + T, + Variants extends VariantGroups | undefined = undefined, + ToggleVariants extends VariantDefinitions | undefined = undefined, + Props extends ComplexPropDefinitions | undefined = undefined +>( + items: Record, + transformItem: ( + value: T, + key: string | number + ) => PatternOptions, + debugId?: string +): Record< + string | number, + RuntimeFn< + ConditionalVariants, + Exclude + > +> { + const patternsMap: Record< + string | number, + RuntimeFn< + ConditionalVariants, + Exclude + > + > = {}; + + for (const key in items) { + const pattern = transformItem(items[key], key); + patternsMap[key] = rulesImpl(pattern, getDebugName(debugId, key)); + } + + return patternsMap; +} + function rulesRaw< Variants extends VariantGroups | undefined = undefined, ToggleVariants extends VariantDefinitions | undefined = undefined, @@ -194,6 +352,7 @@ function rulesRaw< } export const rules = Object.assign(rulesImpl, { + multiple: rulesMultiple, raw: rulesRaw }); @@ -220,7 +379,7 @@ function processPropObject( } function processCompoundStyle( - style: ComplexCSSRule | string, + style: RecipeStyleRule, debugId: string | undefined, index: number ): string { @@ -1025,4 +1184,160 @@ if (import.meta.vitest) { rules(ruleObj4); // Ensure it can be used with rules() }); }); + + describe.concurrent("rules.multiple()", () => { + it("Empty pattern map", () => { + const result = rules.multiple({}, debugId); + + assert.isEmpty(result); + expect(Object.keys(result)).to.have.lengthOf(0); + }); + + it("Static pattern map", () => { + const result = rules.multiple( + { + button: { + base: { padding: 12 }, + variants: { + variant: { + primary: { background: "blue" }, + secondary: { background: "gray" } + } + } + }, + input: { + base: { padding: 8 }, + variants: { + state: { + error: { borderColor: "red" }, + success: { borderColor: "green" } + } + } + } + }, + debugId + ); + + assert.hasAllKeys(result, ["button", "input"]); + + // Each result should be a function (RuntimeFn) + assert.isFunction(result.button); + assert.isFunction(result.input); + + // Check button pattern + assert.hasAllKeys(result.button, ["props", "variants", "classNames"]); + assert.hasAllKeys(result.button.classNames, ["base", "variants"]); + expect(result.button()).toMatch(className(`${debugId}_button`)); + expect(result.button.classNames.base).toMatch( + className(`${debugId}_button`) + ); + assert.hasAllKeys(result.button.classNames.variants, ["variant"]); + assert.hasAllKeys(result.button.classNames.variants.variant, [ + "primary", + "secondary" + ]); + + // Check input pattern + assert.hasAllKeys(result.input, ["props", "variants", "classNames"]); + assert.hasAllKeys(result.input.classNames, ["base", "variants"]); + expect(result.input()).toMatch(className(`${debugId}_input`)); + expect(result.input.classNames.base).toMatch( + className(`${debugId}_input`) + ); + assert.hasAllKeys(result.input.classNames.variants, ["state"]); + assert.hasAllKeys(result.input.classNames.variants.state, [ + "error", + "success" + ]); + + // Test usage + expect(result.button({ variant: "primary" })).toMatch( + className(`${debugId}_button`, `${debugId}_button_variant_primary`) + ); + expect(result.input({ state: "error" })).toMatch( + className(`${debugId}_input`, `${debugId}_input_state_error`) + ); + }); + + it("Data mapping with theme variations", () => { + const result = rules.multiple( + { + light: { bg: "#ffffff", text: "#000000" }, + dark: { bg: "#1a1a1a", text: "#ffffff" } + }, + (colors, _themeName) => ({ + base: { + backgroundColor: colors.bg, + color: colors.text + }, + variants: { + emphasis: { + subtle: { opacity: 0.7 }, + strong: { fontWeight: "bold" } + } + } + }), + debugId + ); + + assert.hasAllKeys(result, ["light", "dark"]); + + // Each result should be a function (RuntimeFn) + assert.isFunction(result.light); + assert.isFunction(result.dark); + + // Check light theme pattern + assert.hasAllKeys(result.light, ["props", "variants", "classNames"]); + expect(result.light()).toMatch(className(`${debugId}_light`)); + assert.hasAllKeys(result.light.classNames.variants, ["emphasis"]); + assert.hasAllKeys(result.light.classNames.variants.emphasis, [ + "subtle", + "strong" + ]); + + // Check dark theme pattern + assert.hasAllKeys(result.dark, ["props", "variants", "classNames"]); + expect(result.dark()).toMatch(className(`${debugId}_dark`)); + assert.hasAllKeys(result.dark.classNames.variants, ["emphasis"]); + assert.hasAllKeys(result.dark.classNames.variants.emphasis, [ + "subtle", + "strong" + ]); + + // Test usage + expect(result.light({ emphasis: "strong" })).toMatch( + className(`${debugId}_light`, `${debugId}_light_emphasis_strong`) + ); + expect(result.dark({ emphasis: "subtle" })).toMatch( + className(`${debugId}_dark`, `${debugId}_dark_emphasis_subtle`) + ); + }); + + it("Size system patterns", () => { + const result = rules.multiple( + { xs: 4, sm: 8, md: 16 }, + (spacing, _size) => ({ + variants: { + direction: { + all: { padding: spacing }, + horizontal: { paddingLeft: spacing, paddingRight: spacing } + } + } + }), + debugId + ); + + assert.hasAllKeys(result, ["xs", "sm", "md"]); + + // Each result should be a function (RuntimeFn) + assert.isFunction(result.xs); + assert.isFunction(result.sm); + assert.isFunction(result.md); + + // Test usage + expect(result.md({ direction: "horizontal" })).toMatch( + className(`${debugId}_md`, `${debugId}_md_direction_horizontal`) + ); + }); + }); } diff --git a/packages/css/src/rules/types.ts b/packages/css/src/rules/types.ts index 569aab7d..ec452820 100644 --- a/packages/css/src/rules/types.ts +++ b/packages/css/src/rules/types.ts @@ -28,7 +28,7 @@ export type Serializable = } | ReadonlyArray; -type RecipeStyleRule = ComplexCSSRule | string; +export type RecipeStyleRule = ComplexCSSRule | string; export type VariantStyle< VariantNames extends string,