Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions packages/css/src/rules/createRuntimeFn.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { CSSRule } from "@mincho-js/transform-to-vanilla";
import type {
PatternResult,
RecipeClassNames,
RuntimeFn,
VariantGroups,
VariantSelection,
VariantObjectSelection
VariantObjectSelection,
ComplexPropDefinitions,
PropDefinitionOutput,
PropTarget
} from "./types";
import { mapValues, transformVariantSelection } from "./utils";

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

export const createRuntimeFn = <Variants extends VariantGroups>(
config: PatternResult<Variants>
): RuntimeFn<Variants> => {
const runtimeFn: RuntimeFn<Variants> = (options) => {
export const createRuntimeFn = <
Variants extends VariantGroups,
Props extends ComplexPropDefinitions<PropTarget | undefined>
>(
config: PatternResult<Variants, Props>
): RuntimeFn<Variants, Props> => {
const runtimeFn: RuntimeFn<Variants, Props> = (options) => {
let className = config.defaultClassName;

const selections: VariantObjectSelection<Variants> = {
Expand Down Expand Up @@ -68,6 +75,19 @@ export const createRuntimeFn = <Variants extends VariantGroups>(
return className;
};

runtimeFn.props = (props) => {
const result: CSSRule = {};
for (const [propName, propValue] of Object.entries(props)) {
const varName =
config.propVars[propName as keyof PropDefinitionOutput<Props>];

if (varName !== undefined) {
result[varName] = propValue as string;
}
}
return result;
};

runtimeFn.variants = () => Object.keys(config.variantClassNames);

runtimeFn.classNames = {
Expand Down
203 changes: 187 additions & 16 deletions packages/css/src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import deepmerge from "@fastify/deepmerge";
import { createVar, fallbackVar } from "@vanilla-extract/css";
import { addFunctionSerializer } from "@vanilla-extract/css/functionSerializer";
import { setFileScope } from "@vanilla-extract/css/fileScope";
import type { ComplexCSSRule } from "@mincho-js/transform-to-vanilla";
import type {
ComplexCSSRule,
CSSRule,
PureCSSVarKey
} from "@mincho-js/transform-to-vanilla";

import { css, cssVariants } from "../css";
import { className } from "../utils";
import { className, getVarName } from "../utils";
import { createRuntimeFn } from "./createRuntimeFn";
import type {
PatternOptions,
Expand All @@ -15,7 +20,10 @@ import type {
VariantSelection,
VariantObjectSelection,
ComplexPropDefinitions,
PropDefinition,
PropDefinitionOutput,
PropTarget,
PropVars,
ConditionalVariants,
Serializable
} from "./types";
Expand All @@ -34,25 +42,48 @@ export function rules<
>(
options: PatternOptions<Variants, ToggleVariants, Props>,
debugId?: string
): RuntimeFn<ConditionalVariants<Variants, ToggleVariants>> {
): RuntimeFn<
ConditionalVariants<Variants, ToggleVariants>,
Exclude<Props, undefined>
> {
const {
toggles = {},
variants = {},
defaultVariants = {},
compoundVariants = [],
props = {},
base,
...baseStyles
} = options;

type PureProps = Exclude<Props, undefined>;
const propVars = {} as PropVars<PureProps>;
const propStyles: CSSRule = {};
if (Array.isArray(props)) {
for (const prop of props) {
if (typeof prop === "string") {
const propVar = createVar(`${debugId}_${prop}`);
propVars[prop as keyof PropDefinitionOutput<PureProps>] =
getVarName(propVar);
// @ts-expect-error Expression produces a union type that is too complex to represent.ts(2590)
propStyles[prop] = propVar;
} else {
processPropObject(prop, propVars, propStyles, debugId);
}
}
} else {
processPropObject(props, propVars, propStyles, debugId);
}

let defaultClassName: string;
if (!base || typeof base === "string") {
const baseClassName = css(baseStyles, debugId);
const baseClassName = css([baseStyles, propStyles], debugId);
defaultClassName = base ? `${baseClassName} ${base}` : baseClassName;
} else {
defaultClassName = css(
Array.isArray(base)
? [baseStyles, ...base]
: mergeObject(baseStyles, base),
? [baseStyles, ...base, propStyles]
: [mergeObject(baseStyles, base), propStyles],
debugId
);
}
Expand Down Expand Up @@ -92,18 +123,20 @@ export function rules<
]);
}

const config: PatternResult<CombinedVariants> = {
const config: PatternResult<CombinedVariants, PureProps> = {
defaultClassName,
variantClassNames,
defaultVariants: transformVariantSelection(defaultVariants),
compoundVariants: compounds
compoundVariants: compounds,
propVars
};

return addFunctionSerializer<
RuntimeFn<ConditionalVariants<Variants, ToggleVariants>>
RuntimeFn<ConditionalVariants<Variants, ToggleVariants>, PureProps>
>(
createRuntimeFn(config) as RuntimeFn<
ConditionalVariants<Variants, ToggleVariants>
ConditionalVariants<Variants, ToggleVariants>,
PureProps
>,
{
importPath: "@mincho-js/css/rules/createRuntimeFn",
Expand All @@ -114,6 +147,25 @@ export function rules<
}
export const recipe = rules;

function processPropObject<Target extends PropTarget>(
props: PropDefinition<Target>,
propVars: Record<string, PureCSSVarKey>,
propStyles: CSSRule,
debugId?: string
) {
Object.entries(props).forEach(([propName, propValue]) => {
const propVar = createVar(`${debugId}_${propName}`);
propVars[propName] = getVarName(propVar);

const isBaseValue = propValue?.base !== undefined;
propValue?.targets.forEach((target) => {
propStyles[target] = isBaseValue
? fallbackVar(propVar, `${propValue.base}`)
: propVar;
});
});
}

// == Tests ====================================================================
// Ignore errors when compiling to CommonJS.
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand All @@ -131,8 +183,9 @@ if (import.meta.vitest) {
const result = rules({ base: { color: "red" } }, debugId);

assert.isFunction(result);
assert.hasAllKeys(result, ["variants", "classNames"]);
assert.hasAllKeys(result, ["props", "variants", "classNames"]);
assert.hasAllKeys(result.classNames, ["base", "variants"]);
assert.isFunction(result.props);

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

assert.isFunction(result);
assert.hasAllKeys(result, ["variants", "classNames"]);
assert.hasAllKeys(result, ["props", "variants", "classNames"]);
assert.hasAllKeys(result.classNames, ["base", "variants"]);
assert.isFunction(result.props);

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

// Base check
assert.isFunction(result);
assert.hasAllKeys(result, ["variants", "classNames"]);
assert.hasAllKeys(result, ["props", "variants", "classNames"]);
assert.hasAllKeys(result.classNames, ["base", "variants"]);
assert.isFunction(result.props);

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

// Base check
assert.isFunction(result);
assert.hasAllKeys(result, ["variants", "classNames"]);
assert.hasAllKeys(result, ["props", "variants", "classNames"]);
assert.hasAllKeys(result.classNames, ["base", "variants"]);
assert.isFunction(result.props);

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

// Base check
assert.isFunction(result);
assert.hasAllKeys(result, ["variants", "classNames"]);
assert.hasAllKeys(result, ["props", "variants", "classNames"]);
assert.hasAllKeys(result.classNames, ["base", "variants"]);
assert.isFunction(result.props);

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

// Base check
assert.isFunction(result);
assert.hasAllKeys(result, ["variants", "classNames"]);
assert.hasAllKeys(result, ["props", "variants", "classNames"]);
assert.hasAllKeys(result.classNames, ["base", "variants"]);
assert.isFunction(result.props);

expect(result()).toMatch(className(debugId));
expect(result.classNames.base).toMatch(className(debugId));
Expand Down Expand Up @@ -543,5 +601,118 @@ if (import.meta.vitest) {
className(debugId, `${debugId}_outlined_true`)
);
});

it("Props", () => {
const result1 = rules(
{
props: ["color", "background"]
},
debugId
);

assert.isFunction(result1);
assert.hasAllKeys(result1, ["props", "variants", "classNames"]);
assert.hasAllKeys(result1.classNames, ["base", "variants"]);
assert.isFunction(result1.props);

Object.entries(
result1.props({
color: "red"
})
).forEach(([varName, propValue]) => {
// Partial
expect(propValue).toBe("red");
expect(varName).toMatch(className(`--${debugId}_color`));
});
Object.entries(
result1.props({
color: "red",
background: "blue"
})
).forEach(([varName, propValue]) => {
// Fully
expect(propValue).toBeOneOf(["red", "blue"]);

if (propValue === "red") {
expect(varName).toMatch(className(`--${debugId}_color`));
}
if (propValue === "blue") {
expect(varName).toMatch(className(`--${debugId}_background`));
}
});
Object.entries(
result1.props({
// @ts-expect-error Not valid property
"something-else": "red"
})
).forEach(([varName, propValue]) => {
expect(varName).toBeUndefined();
expect(propValue).toBeUndefined();
});

const result2 = rules(
{
props: {
rounded: { targets: ["borderRadius"] },
size: { base: 0, targets: ["padding", "margin"] }
}
},
debugId
);
Object.entries(
result2.props({
rounded: "999px",
size: "2rem"
})
).forEach(([varName, propValue]) => {
// Fully
expect(propValue).toBeOneOf(["999px", "2rem"]);

if (propValue === "999px") {
expect(varName).toMatch(className(`--${debugId}_rounded`));
}
if (propValue === "2rem") {
expect(varName).toMatch(className(`--${debugId}_size`));
}
});

const result3 = rules(
{
props: [
"color",
"background",
{
rounded: { targets: ["borderRadius"] },
size: { base: 0, targets: ["padding", "margin"] }
}
]
},
debugId
);
Object.entries(
result3.props({
color: "red",
background: "blue",
rounded: "999px",
size: "2rem"
})
).forEach(([varName, propValue]) => {
// Fully
expect(propValue).toBeOneOf(["red", "blue", "999px", "2rem"]);

if (propValue === "red") {
expect(varName).toMatch(className(`--${debugId}_color`));
}
if (propValue === "blue") {
expect(varName).toMatch(className(`--${debugId}_background`));
}
if (propValue === "999px") {
expect(varName).toMatch(className(`--${debugId}_rounded`));
}
if (propValue === "2rem") {
expect(varName).toMatch(className(`--${debugId}_size`));
}
});
});
});
}
Loading