Skip to content

Commit fd5fac5

Browse files
authored
Expose recipe's class names (#1104)
1 parent 84ba370 commit fd5fac5

File tree

11 files changed

+270
-21
lines changed

11 files changed

+270
-21
lines changed

.changeset/brave-snakes-yawn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@vanilla-extract/recipes': minor
3+
---
4+
5+
Expose recipe's class names to allow their selection

.changeset/weak-rabbits-design.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@vanilla-extract/recipes': minor
3+
---
4+
5+
Always create a base class name for a recipe

packages/recipes/src/createRuntimeFn.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type {
22
PatternResult,
3+
RecipeClassNames,
34
RuntimeFn,
45
VariantGroups,
56
VariantSelection,
67
} from './types';
8+
import { mapValues } from './utils';
79

810
const shouldApplyCompound = <Variants extends VariantGroups>(
911
compoundCheck: VariantSelection<Variants>,
@@ -64,5 +66,17 @@ export const createRuntimeFn = <Variants extends VariantGroups>(
6466

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

69+
runtimeFn.classNames = {
70+
get base() {
71+
return config.defaultClassName.split(' ')[0];
72+
},
73+
74+
get variants() {
75+
return mapValues(config.variantClassNames, (classNames) =>
76+
mapValues(classNames, (className) => className.split(' ')[0]),
77+
) as RecipeClassNames<Variants>['variants'];
78+
},
79+
};
80+
6781
return runtimeFn;
6882
};

packages/recipes/src/index.ts

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,10 @@ import type {
99
VariantGroups,
1010
VariantSelection,
1111
} from './types';
12+
import { mapValues } from './utils';
1213

1314
export type { RecipeVariants, RuntimeFn } from './types';
1415

15-
function mapValues<Input extends Record<string, any>, OutputValue>(
16-
input: Input,
17-
fn: (value: Input[keyof Input], key: keyof Input) => OutputValue,
18-
): Record<keyof Input, OutputValue> {
19-
const result: any = {};
20-
21-
for (const key in input) {
22-
result[key] = fn(input[key], key);
23-
}
24-
25-
return result;
26-
}
27-
2816
export function recipe<Variants extends VariantGroups>(
2917
options: PatternOptions<Variants>,
3018
debugId?: string,
@@ -33,11 +21,17 @@ export function recipe<Variants extends VariantGroups>(
3321
variants = {},
3422
defaultVariants = {},
3523
compoundVariants = [],
36-
base = '',
24+
base,
3725
} = options;
3826

39-
const defaultClassName =
40-
typeof base === 'string' ? base : style(base, debugId);
27+
let defaultClassName;
28+
29+
if (!base || typeof base === 'string') {
30+
const baseClassName = style({});
31+
defaultClassName = base ? `${baseClassName} ${base}` : baseClassName;
32+
} else {
33+
defaultClassName = style(base, debugId);
34+
}
4135

4236
// @ts-expect-error
4337
const variantClassNames: PatternResult<Variants>['variantClassNames'] =

packages/recipes/src/types.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@ export type VariantSelection<Variants extends VariantGroups> = {
1111
[VariantGroup in keyof Variants]?: BooleanMap<keyof Variants[VariantGroup]>;
1212
};
1313

14+
export type VariantsClassNames<Variants extends VariantGroups> = {
15+
[P in keyof Variants]: {
16+
[PP in keyof Variants[P]]: string;
17+
};
18+
};
19+
1420
export type PatternResult<Variants extends VariantGroups> = {
1521
defaultClassName: string;
16-
variantClassNames: {
17-
[P in keyof Variants]: { [P in keyof Variants[keyof Variants]]: string };
18-
};
22+
variantClassNames: VariantsClassNames<Variants>;
1923
defaultVariants: VariantSelection<Variants>;
2024
compoundVariants: Array<[VariantSelection<Variants>, string]>;
2125
};
@@ -32,9 +36,17 @@ export type PatternOptions<Variants extends VariantGroups> = {
3236
compoundVariants?: Array<CompoundVariant<Variants>>;
3337
};
3438

39+
export type RecipeClassNames<Variants extends VariantGroups> = {
40+
base: string;
41+
variants: VariantsClassNames<Variants>;
42+
};
43+
3544
export type RuntimeFn<Variants extends VariantGroups> = ((
3645
options?: VariantSelection<Variants>,
37-
) => string) & { variants: () => (keyof Variants)[] };
46+
) => string) & {
47+
variants: () => (keyof Variants)[];
48+
classNames: RecipeClassNames<Variants>;
49+
};
3850

3951
export type RecipeVariants<RecipeFn extends RuntimeFn<VariantGroups>> =
4052
Parameters<RecipeFn>[0];

packages/recipes/src/utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export function mapValues<Input extends Record<string, any>, OutputValue>(
2+
input: Input,
3+
fn: (value: Input[keyof Input], key: keyof Input) => OutputValue,
4+
): Record<keyof Input, OutputValue> {
5+
const result: any = {};
6+
7+
for (const key in input) {
8+
result[key] = fn(input[key], key);
9+
}
10+
11+
return result;
12+
}

site/docs/packages/recipes.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,28 @@ export const button = recipe({
118118
});
119119
```
120120

121+
The recipes function also exposes an array property `variants` that includes all the variants from your recipe.
122+
123+
```ts
124+
button.variants();
125+
// -> ['color', 'size']
126+
```
127+
128+
## Recipe class name selection
129+
130+
Recipes function exposes internal class names in `classNames` property.
131+
The property has two predefined props: `base` and `variants`. The `base` prop includes base class name. It is always defined even if you do not have any base styles. The `variants` prop includes class names for each defined variant.
132+
133+
```ts
134+
// app.css.ts
135+
console.log(button.classNames.base);
136+
// -> app_button__129pj250
137+
console.log(button.classNames.variants.color.neutral);
138+
// -> app_button_color_neutral__129pj251
139+
console.log(button.classNames.variants.size.small);
140+
// -> app_button_size_small__129pj254
141+
```
142+
121143
## RecipeVariants
122144

123145
A utility to make use of the recipe’s type interface. This can be useful when typing functions or component props that need to accept recipe values as part of their interface.

tests/compiler/compiler.vitest.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,47 @@ describe('compiler', () => {
300300
`);
301301
});
302302

303+
test('recipes class names', async () => {
304+
const compiler = compilers.default;
305+
306+
const cssPath = path.join(
307+
__dirname,
308+
'fixtures/recipes/recipeClassNames.css.ts',
309+
);
310+
const output = await compiler.processVanillaFile(cssPath);
311+
const { css } = await compiler.getCssForFile(cssPath);
312+
313+
expect(output.source).toMatchInlineSnapshot(`
314+
"import 'fixtures/recipes/recipeClassNames.css.ts.vanilla.css';
315+
import { createRuntimeFn as _7a468 } from '@vanilla-extract/recipes/createRuntimeFn';
316+
export var recipeWithReferences = _7a468({defaultClassName:'recipeClassNames_recipeWithReferences__129pj258',variantClassNames:{first:{true:'recipeClassNames_recipeWithReferences_first_true__129pj259'}},defaultVariants:{},compoundVariants:[]});"
317+
`);
318+
319+
expect(css).toMatchInlineSnapshot(`
320+
".recipeClassNames_basic_rounded_true__129pj257 {
321+
border-radius: 999px;
322+
}
323+
.recipeClassNames_recipeWithReferences__129pj258 {
324+
color: red;
325+
}
326+
.recipeClassNames__129pj250 .recipeClassNames_recipeWithReferences__129pj258 {
327+
color: blue;
328+
}
329+
.recipeClassNames_basic_spaceWithDefault_large__129pj252 .recipeClassNames_recipeWithReferences__129pj258 {
330+
color: yellow;
331+
}
332+
.recipeClassNames_basic_spaceWithoutDefault_small__129pj253 .recipeClassNames_recipeWithReferences__129pj258 {
333+
color: green;
334+
}
335+
.recipeClassNames_basic_color_red__129pj255 .recipeClassNames_recipeWithReferences_first_true__129pj259 {
336+
color: black;
337+
}
338+
.recipeClassNames_basic_spaceWithDefault_large__129pj252.recipeClassNames_basic_rounded_true__129pj257 .recipeClassNames_recipeWithReferences_first_true__129pj259 {
339+
color: white;
340+
}"
341+
`);
342+
});
343+
303344
afterAll(async () => {
304345
await Promise.allSettled(
305346
Object.values(compilers).map((compiler) => compiler.close()),
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { recipe } from '@vanilla-extract/recipes';
2+
3+
const basic = recipe({
4+
variants: {
5+
spaceWithDefault: {
6+
small: {},
7+
large: {},
8+
},
9+
spaceWithoutDefault: {
10+
small: {},
11+
large: {},
12+
},
13+
color: {
14+
red: {},
15+
blue: {},
16+
},
17+
rounded: {
18+
true: { borderRadius: 999 },
19+
},
20+
},
21+
});
22+
23+
export const recipeWithReferences = recipe({
24+
base: {
25+
color: 'red',
26+
selectors: {
27+
[`.${basic.classNames.base} &`]: {
28+
color: 'blue',
29+
},
30+
[`.${basic.classNames.variants.spaceWithDefault.large} &`]: {
31+
color: 'yellow',
32+
},
33+
[`.${basic.classNames.variants.spaceWithoutDefault.small} &`]: {
34+
color: 'green',
35+
},
36+
},
37+
},
38+
variants: {
39+
first: {
40+
true: {
41+
selectors: {
42+
[`.${basic.classNames.variants.color.red} &`]: {
43+
color: 'black',
44+
},
45+
[`.${basic.classNames.variants.spaceWithDefault.large}.${basic.classNames.variants.rounded.true} &`]:
46+
{
47+
color: 'white',
48+
},
49+
},
50+
},
51+
},
52+
},
53+
});

tests/recipes/recipes.css.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,23 @@ export const basic = recipe({
3232
},
3333
],
3434
});
35+
36+
export const empty = recipe({});
37+
38+
export const definedStringBase = recipe({
39+
base: 'definedStringBase',
40+
variants: {
41+
variant: {
42+
simple: 'simple-one',
43+
},
44+
},
45+
});
46+
47+
export const definedStringBaseArray = recipe({
48+
base: ['definedStringBaseInArray_1', 'definedStringBaseInArray_2'],
49+
variants: {
50+
variant: {
51+
simple: ['simple-one', 'simple-two'],
52+
},
53+
},
54+
});

0 commit comments

Comments
 (0)