Skip to content

Commit afb191f

Browse files
committed
Feat: rules() props runtime #131
1 parent a277d8b commit afb191f

File tree

5 files changed

+315
-36
lines changed

5 files changed

+315
-36
lines changed

packages/css/src/rules/createRuntimeFn.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import type { CSSRule } from "@mincho-js/transform-to-vanilla";
12
import type {
23
PatternResult,
34
RecipeClassNames,
45
RuntimeFn,
56
VariantGroups,
67
VariantSelection,
7-
VariantObjectSelection
8+
VariantObjectSelection,
9+
ComplexPropDefinitions,
10+
PropDefinitionOutput,
11+
PropTarget
812
} from "./types";
913
import { mapValues, transformVariantSelection } from "./utils";
1014

@@ -22,10 +26,13 @@ const shouldApplyCompound = <Variants extends VariantGroups>(
2226
return true;
2327
};
2428

25-
export const createRuntimeFn = <Variants extends VariantGroups>(
26-
config: PatternResult<Variants>
27-
): RuntimeFn<Variants> => {
28-
const runtimeFn: RuntimeFn<Variants> = (options) => {
29+
export const createRuntimeFn = <
30+
Variants extends VariantGroups,
31+
Props extends ComplexPropDefinitions<PropTarget | undefined>
32+
>(
33+
config: PatternResult<Variants, Props>
34+
): RuntimeFn<Variants, Props> => {
35+
const runtimeFn: RuntimeFn<Variants, Props> = (options) => {
2936
let className = config.defaultClassName;
3037

3138
const selections: VariantObjectSelection<Variants> = {
@@ -68,6 +75,19 @@ export const createRuntimeFn = <Variants extends VariantGroups>(
6875
return className;
6976
};
7077

78+
runtimeFn.props = (props) => {
79+
const result: CSSRule = {};
80+
for (const [propName, propValue] of Object.entries(props)) {
81+
const varName =
82+
config.propVars[propName as keyof PropDefinitionOutput<Props>];
83+
84+
if (varName !== undefined) {
85+
result[varName] = propValue as string;
86+
}
87+
}
88+
return result;
89+
};
90+
7191
runtimeFn.variants = () => Object.keys(config.variantClassNames);
7292

7393
runtimeFn.classNames = {

packages/css/src/rules/index.ts

Lines changed: 187 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import deepmerge from "@fastify/deepmerge";
2+
import { createVar, fallbackVar } from "@vanilla-extract/css";
23
import { addFunctionSerializer } from "@vanilla-extract/css/functionSerializer";
34
import { setFileScope } from "@vanilla-extract/css/fileScope";
4-
import type { ComplexCSSRule } from "@mincho-js/transform-to-vanilla";
5+
import type {
6+
ComplexCSSRule,
7+
CSSRule,
8+
PureCSSVarKey
9+
} from "@mincho-js/transform-to-vanilla";
510

611
import { css, cssVariants } from "../css";
7-
import { className } from "../utils";
12+
import { className, getVarName } from "../utils";
813
import { createRuntimeFn } from "./createRuntimeFn";
914
import type {
1015
PatternOptions,
@@ -15,7 +20,10 @@ import type {
1520
VariantSelection,
1621
VariantObjectSelection,
1722
ComplexPropDefinitions,
23+
PropDefinition,
24+
PropDefinitionOutput,
1825
PropTarget,
26+
PropVars,
1927
ConditionalVariants,
2028
Serializable
2129
} from "./types";
@@ -34,25 +42,48 @@ export function rules<
3442
>(
3543
options: PatternOptions<Variants, ToggleVariants, Props>,
3644
debugId?: string
37-
): RuntimeFn<ConditionalVariants<Variants, ToggleVariants>> {
45+
): RuntimeFn<
46+
ConditionalVariants<Variants, ToggleVariants>,
47+
Exclude<Props, undefined>
48+
> {
3849
const {
3950
toggles = {},
4051
variants = {},
4152
defaultVariants = {},
4253
compoundVariants = [],
54+
props = {},
4355
base,
4456
...baseStyles
4557
} = options;
4658

59+
type PureProps = Exclude<Props, undefined>;
60+
const propVars = {} as PropVars<PureProps>;
61+
const propStyles: CSSRule = {};
62+
if (Array.isArray(props)) {
63+
for (const prop of props) {
64+
if (typeof prop === "string") {
65+
const propVar = createVar(`${debugId}_${prop}`);
66+
propVars[prop as keyof PropDefinitionOutput<PureProps>] =
67+
getVarName(propVar);
68+
// @ts-expect-error Expression produces a union type that is too complex to represent.ts(2590)
69+
propStyles[prop] = propVar;
70+
} else {
71+
processPropObject(prop, propVars, propStyles, debugId);
72+
}
73+
}
74+
} else {
75+
processPropObject(props, propVars, propStyles, debugId);
76+
}
77+
4778
let defaultClassName: string;
4879
if (!base || typeof base === "string") {
49-
const baseClassName = css(baseStyles, debugId);
80+
const baseClassName = css([baseStyles, propStyles], debugId);
5081
defaultClassName = base ? `${baseClassName} ${base}` : baseClassName;
5182
} else {
5283
defaultClassName = css(
5384
Array.isArray(base)
54-
? [baseStyles, ...base]
55-
: mergeObject(baseStyles, base),
85+
? [baseStyles, ...base, propStyles]
86+
: [mergeObject(baseStyles, base), propStyles],
5687
debugId
5788
);
5889
}
@@ -92,18 +123,20 @@ export function rules<
92123
]);
93124
}
94125

95-
const config: PatternResult<CombinedVariants> = {
126+
const config: PatternResult<CombinedVariants, PureProps> = {
96127
defaultClassName,
97128
variantClassNames,
98129
defaultVariants: transformVariantSelection(defaultVariants),
99-
compoundVariants: compounds
130+
compoundVariants: compounds,
131+
propVars
100132
};
101133

102134
return addFunctionSerializer<
103-
RuntimeFn<ConditionalVariants<Variants, ToggleVariants>>
135+
RuntimeFn<ConditionalVariants<Variants, ToggleVariants>, PureProps>
104136
>(
105137
createRuntimeFn(config) as RuntimeFn<
106-
ConditionalVariants<Variants, ToggleVariants>
138+
ConditionalVariants<Variants, ToggleVariants>,
139+
PureProps
107140
>,
108141
{
109142
importPath: "@mincho-js/css/rules/createRuntimeFn",
@@ -114,6 +147,25 @@ export function rules<
114147
}
115148
export const recipe = rules;
116149

150+
function processPropObject<Target extends PropTarget>(
151+
props: PropDefinition<Target>,
152+
propVars: Record<string, PureCSSVarKey>,
153+
propStyles: CSSRule,
154+
debugId?: string
155+
) {
156+
Object.entries(props).forEach(([propName, propValue]) => {
157+
const propVar = createVar(`${debugId}_${propName}`);
158+
propVars[propName] = getVarName(propVar);
159+
160+
const isBaseValue = propValue?.base !== undefined;
161+
propValue?.targets.forEach((target) => {
162+
propStyles[target] = isBaseValue
163+
? fallbackVar(propVar, `${propValue.base}`)
164+
: propVar;
165+
});
166+
});
167+
}
168+
117169
// == Tests ====================================================================
118170
// Ignore errors when compiling to CommonJS.
119171
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -131,8 +183,9 @@ if (import.meta.vitest) {
131183
const result = rules({ base: { color: "red" } }, debugId);
132184

133185
assert.isFunction(result);
134-
assert.hasAllKeys(result, ["variants", "classNames"]);
186+
assert.hasAllKeys(result, ["props", "variants", "classNames"]);
135187
assert.hasAllKeys(result.classNames, ["base", "variants"]);
188+
assert.isFunction(result.props);
136189

137190
expect(result()).toMatch(className(debugId));
138191
expect(result.classNames.base).toMatch(className(debugId));
@@ -144,8 +197,9 @@ if (import.meta.vitest) {
144197
const result = rules({ color: "red" }, debugId);
145198

146199
assert.isFunction(result);
147-
assert.hasAllKeys(result, ["variants", "classNames"]);
200+
assert.hasAllKeys(result, ["props", "variants", "classNames"]);
148201
assert.hasAllKeys(result.classNames, ["base", "variants"]);
202+
assert.isFunction(result.props);
149203

150204
expect(result()).toMatch(className(debugId));
151205
expect(result.classNames.base).toMatch(className(debugId));
@@ -177,8 +231,9 @@ if (import.meta.vitest) {
177231

178232
// Base check
179233
assert.isFunction(result);
180-
assert.hasAllKeys(result, ["variants", "classNames"]);
234+
assert.hasAllKeys(result, ["props", "variants", "classNames"]);
181235
assert.hasAllKeys(result.classNames, ["base", "variants"]);
236+
assert.isFunction(result.props);
182237

183238
expect(result()).toMatch(className(debugId));
184239
expect(result.classNames.base).toMatch(className(debugId));
@@ -273,8 +328,9 @@ if (import.meta.vitest) {
273328

274329
// Base check
275330
assert.isFunction(result);
276-
assert.hasAllKeys(result, ["variants", "classNames"]);
331+
assert.hasAllKeys(result, ["props", "variants", "classNames"]);
277332
assert.hasAllKeys(result.classNames, ["base", "variants"]);
333+
assert.isFunction(result.props);
278334

279335
expect(result()).toMatch(className(debugId));
280336
expect(result.classNames.base).toMatch(className(debugId));
@@ -337,8 +393,9 @@ if (import.meta.vitest) {
337393

338394
// Base check
339395
assert.isFunction(result);
340-
assert.hasAllKeys(result, ["variants", "classNames"]);
396+
assert.hasAllKeys(result, ["props", "variants", "classNames"]);
341397
assert.hasAllKeys(result.classNames, ["base", "variants"]);
398+
assert.isFunction(result.props);
342399

343400
expect(result()).toMatch(className(debugId, `${debugId}_disabled_true`));
344401
expect(result.classNames.base).toMatch(className(debugId));
@@ -438,8 +495,9 @@ if (import.meta.vitest) {
438495

439496
// Base check
440497
assert.isFunction(result);
441-
assert.hasAllKeys(result, ["variants", "classNames"]);
498+
assert.hasAllKeys(result, ["props", "variants", "classNames"]);
442499
assert.hasAllKeys(result.classNames, ["base", "variants"]);
500+
assert.isFunction(result.props);
443501

444502
expect(result()).toMatch(className(debugId));
445503
expect(result.classNames.base).toMatch(className(debugId));
@@ -543,5 +601,118 @@ if (import.meta.vitest) {
543601
className(debugId, `${debugId}_outlined_true`)
544602
);
545603
});
604+
605+
it("Props", () => {
606+
const result1 = rules(
607+
{
608+
props: ["color", "background"]
609+
},
610+
debugId
611+
);
612+
613+
assert.isFunction(result1);
614+
assert.hasAllKeys(result1, ["props", "variants", "classNames"]);
615+
assert.hasAllKeys(result1.classNames, ["base", "variants"]);
616+
assert.isFunction(result1.props);
617+
618+
Object.entries(
619+
result1.props({
620+
color: "red"
621+
})
622+
).forEach(([varName, propValue]) => {
623+
// Partial
624+
expect(propValue).toBe("red");
625+
expect(varName).toMatch(className(`--${debugId}_color`));
626+
});
627+
Object.entries(
628+
result1.props({
629+
color: "red",
630+
background: "blue"
631+
})
632+
).forEach(([varName, propValue]) => {
633+
// Fully
634+
expect(propValue).toBeOneOf(["red", "blue"]);
635+
636+
if (propValue === "red") {
637+
expect(varName).toMatch(className(`--${debugId}_color`));
638+
}
639+
if (propValue === "blue") {
640+
expect(varName).toMatch(className(`--${debugId}_background`));
641+
}
642+
});
643+
Object.entries(
644+
result1.props({
645+
// @ts-expect-error Not valid property
646+
"something-else": "red"
647+
})
648+
).forEach(([varName, propValue]) => {
649+
expect(varName).toBeUndefined();
650+
expect(propValue).toBeUndefined();
651+
});
652+
653+
const result2 = rules(
654+
{
655+
props: {
656+
rounded: { targets: ["borderRadius"] },
657+
size: { base: 0, targets: ["padding", "margin"] }
658+
}
659+
},
660+
debugId
661+
);
662+
Object.entries(
663+
result2.props({
664+
rounded: "999px",
665+
size: "2rem"
666+
})
667+
).forEach(([varName, propValue]) => {
668+
// Fully
669+
expect(propValue).toBeOneOf(["999px", "2rem"]);
670+
671+
if (propValue === "999px") {
672+
expect(varName).toMatch(className(`--${debugId}_rounded`));
673+
}
674+
if (propValue === "2rem") {
675+
expect(varName).toMatch(className(`--${debugId}_size`));
676+
}
677+
});
678+
679+
const result3 = rules(
680+
{
681+
props: [
682+
"color",
683+
"background",
684+
{
685+
rounded: { targets: ["borderRadius"] },
686+
size: { base: 0, targets: ["padding", "margin"] }
687+
}
688+
]
689+
},
690+
debugId
691+
);
692+
Object.entries(
693+
result3.props({
694+
color: "red",
695+
background: "blue",
696+
rounded: "999px",
697+
size: "2rem"
698+
})
699+
).forEach(([varName, propValue]) => {
700+
// Fully
701+
expect(propValue).toBeOneOf(["red", "blue", "999px", "2rem"]);
702+
703+
if (propValue === "red") {
704+
expect(varName).toMatch(className(`--${debugId}_color`));
705+
}
706+
if (propValue === "blue") {
707+
expect(varName).toMatch(className(`--${debugId}_background`));
708+
}
709+
if (propValue === "999px") {
710+
expect(varName).toMatch(className(`--${debugId}_rounded`));
711+
}
712+
if (propValue === "2rem") {
713+
expect(varName).toMatch(className(`--${debugId}_size`));
714+
}
715+
});
716+
});
546717
});
547718
}

0 commit comments

Comments
 (0)