@crisp-ui-kit/crisp 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,57 @@
1
- import { defineRecipe, resolveRecipe } from "../../core/index.js";
1
+ import { defineSpec, resolveRecipe, specToRecipe } from "../../core/index.js";
2
2
  import { contract as c } from "../../tokens/index.js";
3
3
  // A progress indicator — a continuous brand-filled track, or a segmented row of
4
4
  // pills (a "Getting started 3/6" bar).
5
- const config = defineRecipe({
5
+ const spec = defineSpec({
6
6
  name: "progress",
7
- base: `display: block; box-sizing: border-box; height: 6px; border-radius: ${c.radius.full}; background: color-mix(in srgb, ${c.fgPrimary} 8%, transparent); overflow: hidden;
8
- & .crisp-progress-fill { display: block; height: 100%; border-radius: ${c.radius.full}; background: ${c.bgBrandSolid}; transition: width 0.25s ease; }
9
- &[data-segmented] { display: flex; gap: ${c.space["1"]}; background: transparent; overflow: visible; }
10
- &[data-segmented] .crisp-progress-seg { flex: 1; height: 100%; border-radius: ${c.radius.full}; background: color-mix(in srgb, ${c.fgPrimary} 8%, transparent); transition: background 0.2s; }
11
- &[data-segmented] .crisp-progress-seg[data-filled] { background: ${c.bgBrandSolid}; }`,
12
- variants: {},
13
- defaultVariants: {},
14
- compoundVariants: [],
7
+ base: {
8
+ display: "block",
9
+ boxSizing: "border-box",
10
+ height: "6px",
11
+ borderRadius: c.radius.full,
12
+ background: `color-mix(in srgb, ${c.fgPrimary} 8%, transparent)`,
13
+ overflow: "hidden",
14
+ },
15
+ parts: [
16
+ {
17
+ slot: "fill",
18
+ selector: "& .crisp-progress-fill",
19
+ decls: {
20
+ display: "block",
21
+ height: "100%",
22
+ borderRadius: c.radius.full,
23
+ background: c.bgBrandSolid,
24
+ transition: "width 0.25s ease",
25
+ },
26
+ },
27
+ {
28
+ selector: "&[data-segmented]",
29
+ decls: {
30
+ display: "flex",
31
+ gap: c.space["1"],
32
+ background: "transparent",
33
+ overflow: "visible",
34
+ },
35
+ },
36
+ {
37
+ slot: "seg",
38
+ selector: "&[data-segmented] .crisp-progress-seg",
39
+ decls: {
40
+ flex: 1,
41
+ height: "100%",
42
+ borderRadius: c.radius.full,
43
+ background: `color-mix(in srgb, ${c.fgPrimary} 8%, transparent)`,
44
+ transition: "background 0.2s",
45
+ },
46
+ },
47
+ {
48
+ slot: "seg",
49
+ selector: "&[data-segmented] .crisp-progress-seg[data-filled]",
50
+ decls: { background: c.bgBrandSolid },
51
+ },
52
+ ],
15
53
  });
54
+ const config = specToRecipe(spec);
16
55
  export const progressRecipe = resolveRecipe(config);
56
+ export { spec };
17
57
  export default config;
@@ -1,13 +1,21 @@
1
- import { defineRecipe, resolveRecipe } from "../../core/index.js";
1
+ import { defineSpec, resolveRecipe, specToRecipe } from "../../core/index.js";
2
2
  import { contract as c } from "../../tokens/index.js";
3
3
  // A spinning ring loading indicator.
4
- const config = defineRecipe({
4
+ const spec = defineSpec({
5
5
  name: "spinner",
6
- globalCss: `@keyframes crisp-spin { to { transform: rotate(360deg); } }`,
7
- base: `display: inline-block; box-sizing: border-box; width: 16px; height: 16px; border-radius: ${c.radius.full}; border: 2px solid color-mix(in srgb, ${c.fgPrimary} 15%, transparent); border-top-color: ${c.fgMuted}; animation: crisp-spin 0.6s linear infinite;`,
8
- variants: {},
9
- defaultVariants: {},
10
- compoundVariants: [],
6
+ globalCss: "@keyframes crisp-spin { to { transform: rotate(360deg); } }",
7
+ base: {
8
+ display: "inline-block",
9
+ boxSizing: "border-box",
10
+ width: "16px",
11
+ height: "16px",
12
+ borderRadius: c.radius.full,
13
+ border: `2px solid color-mix(in srgb, ${c.fgPrimary} 15%, transparent)`,
14
+ borderTopColor: c.fgMuted,
15
+ animation: "crisp-spin 0.6s linear infinite",
16
+ },
11
17
  });
18
+ const config = specToRecipe(spec);
12
19
  export const spinnerRecipe = resolveRecipe(config);
20
+ export { spec };
13
21
  export default config;
@@ -7,6 +7,7 @@ export { placeFloating, } from "./place-floating.js";
7
7
  export { Primitive } from "./primitive.js";
8
8
  export { resolveRecipe } from "./resolve.js";
9
9
  export * from "./rtl.js";
10
+ export { defineSpec, specToRecipe } from "./spec.js";
10
11
  export { useFocusAfterRemoval } from "./use-focus-after-removal.js";
11
12
  export { useFormReset } from "./use-form-reset.js";
12
13
  export { useLabelAssociation } from "./use-label-association.js";
@@ -0,0 +1,66 @@
1
+ const kebab = (p) => p.startsWith("--") ? p : p.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
2
+ /** `{ flexShrink: 0 }` -> `flex-shrink: 0;`, in insertion order. */
3
+ export function declsToCss(decls) {
4
+ return Object.entries(decls)
5
+ .map(([prop, value]) => `${kebab(prop)}: ${value};`)
6
+ .join(" ");
7
+ }
8
+ /**
9
+ * `const` type parameter, not a plain `ComponentSpec` return: widening the
10
+ * variant keys here loses the literal union that `VariantProps<typeof recipe>`
11
+ * hands a component, and `BadgeProps["tone"]` silently degrades from
12
+ * `"neutral" | "brand" | …` to `string`. That is a public API regression that
13
+ * typechecks everywhere except at the call sites it was meant to protect.
14
+ */
15
+ export function defineSpec(spec) {
16
+ return spec;
17
+ }
18
+ /**
19
+ * Compile a spec into the RecipeConfig the existing pipeline already takes, so
20
+ * nothing downstream — the CSS generator, the d.ts generator, `resolveRecipe`,
21
+ * the spec guards — has to know a spec exists. That is what keeps the
22
+ * conversion provable: `dist/` must not move, and CI's
23
+ * `git diff --exit-code dist/` is the gate.
24
+ *
25
+ * The formatting here is not cosmetic freedom. It reproduces exactly what the
26
+ * hand-written template strings emit — root declarations space-joined, each
27
+ * nested rule on its own line indented four spaces — because any other choice
28
+ * rewrites every committed stylesheet and buries the conversion's real diff.
29
+ */
30
+ export function specToRecipe(spec) {
31
+ const root = declsToCss(spec.base);
32
+ const nested = (spec.parts ?? []).map((p) => `${p.selector} { ${declsToCss(p.decls)} }`);
33
+ const base = nested.length ? `${root}\n ${nested.join("\n ")}` : root;
34
+ // A variant value compiles the same way the root does: declarations first,
35
+ // then any nested rules it contributes, so a variant that restyles a part
36
+ // (Badge's solid dot) stays in the variant rather than leaking into base.
37
+ const compileValue = (v) => {
38
+ const decls = "decls" in v ? v.decls : v;
39
+ const nestedParts = "decls" in v ? (v.parts ?? []) : [];
40
+ const head = declsToCss(decls);
41
+ if (!nestedParts.length)
42
+ return head;
43
+ const tail = nestedParts
44
+ .map((p) => `${p.selector} { ${declsToCss(p.decls)} }`)
45
+ .join("\n ");
46
+ return head ? `${head}\n ${tail}` : tail;
47
+ };
48
+ const variants = {};
49
+ for (const [name, values] of Object.entries(spec.variants ?? {})) {
50
+ variants[name] = {};
51
+ for (const [value, v] of Object.entries(values)) {
52
+ variants[name][value] = compileValue(v);
53
+ }
54
+ }
55
+ return {
56
+ name: spec.name,
57
+ base,
58
+ variants: variants,
59
+ defaultVariants: (spec.defaultVariants ?? {}),
60
+ compoundVariants: (spec.compoundVariants ?? []).map(({ decls, ...sel }) => ({
61
+ ...sel,
62
+ css: declsToCss(decls),
63
+ })),
64
+ ...(spec.globalCss ? { globalCss: spec.globalCss } : {}),
65
+ };
66
+ }
@@ -7,39 +7,39 @@ export type BadgeProps = React.ComponentPropsWithoutRef<"span"> & VariantProps<t
7
7
  dot?: boolean;
8
8
  };
9
9
  export declare const Badge: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & VariantProps<import("../..").RecipeFn<{
10
- tone: {
11
- neutral: string;
12
- brand: string;
13
- success: string;
14
- warning: string;
15
- danger: string;
10
+ readonly tone: {
11
+ readonly neutral: string;
12
+ readonly brand: string;
13
+ readonly success: string;
14
+ readonly warning: string;
15
+ readonly danger: string;
16
16
  };
17
- size: {
18
- small: string;
19
- medium: string;
20
- large: string;
17
+ readonly size: {
18
+ readonly small: string;
19
+ readonly medium: string;
20
+ readonly large: string;
21
21
  };
22
- variant: {
23
- soft: string;
24
- solid: string;
22
+ readonly variant: {
23
+ readonly soft: string;
24
+ readonly solid: string;
25
25
  };
26
26
  }> & {
27
27
  config: import("../..").RecipeConfig<{
28
- tone: {
29
- neutral: string;
30
- brand: string;
31
- success: string;
32
- warning: string;
33
- danger: string;
28
+ readonly tone: {
29
+ readonly neutral: string;
30
+ readonly brand: string;
31
+ readonly success: string;
32
+ readonly warning: string;
33
+ readonly danger: string;
34
34
  };
35
- size: {
36
- small: string;
37
- medium: string;
38
- large: string;
35
+ readonly size: {
36
+ readonly small: string;
37
+ readonly medium: string;
38
+ readonly large: string;
39
39
  };
40
- variant: {
41
- soft: string;
42
- solid: string;
40
+ readonly variant: {
41
+ readonly soft: string;
42
+ readonly solid: string;
43
43
  };
44
44
  }>;
45
45
  }> & {
@@ -1,56 +1,180 @@
1
+ declare const spec: {
2
+ readonly name: "badge";
3
+ readonly base: {
4
+ readonly display: "inline-flex";
5
+ readonly alignItems: "center";
6
+ readonly justifyContent: "center";
7
+ readonly gap: `var(--${string})`;
8
+ readonly fontFamily: `var(--${string})`;
9
+ readonly fontWeight: `var(--${string})`;
10
+ readonly letterSpacing: "-0.1px";
11
+ readonly whiteSpace: "nowrap";
12
+ };
13
+ readonly parts: [{
14
+ readonly slot: "dot";
15
+ readonly selector: "& .crisp-badge-dot";
16
+ readonly decls: {
17
+ readonly width: "6px";
18
+ readonly height: "6px";
19
+ readonly flexShrink: 0;
20
+ readonly borderRadius: `var(--${string})`;
21
+ readonly background: "currentColor";
22
+ };
23
+ }];
24
+ readonly variants: {
25
+ readonly tone: {
26
+ readonly neutral: {
27
+ readonly background: `var(--${string})`;
28
+ readonly color: `var(--${string})`;
29
+ readonly boxShadow: `inset 0 0 0 1px var(--${string})`;
30
+ };
31
+ readonly brand: {
32
+ background: string;
33
+ color: `var(--${string})`;
34
+ boxShadow: string;
35
+ };
36
+ readonly success: {
37
+ background: string;
38
+ color: `var(--${string})`;
39
+ boxShadow: string;
40
+ };
41
+ readonly warning: {
42
+ background: string;
43
+ color: `var(--${string})`;
44
+ boxShadow: string;
45
+ };
46
+ readonly danger: {
47
+ background: string;
48
+ color: `var(--${string})`;
49
+ boxShadow: string;
50
+ };
51
+ };
52
+ readonly size: {
53
+ readonly small: {
54
+ readonly height: "16px";
55
+ readonly paddingInline: `var(--${string})`;
56
+ readonly fontSize: `var(--${string})`;
57
+ readonly borderRadius: `var(--${string})`;
58
+ };
59
+ readonly medium: {
60
+ readonly height: "20px";
61
+ readonly paddingInline: `var(--${string})`;
62
+ readonly fontSize: `var(--${string})`;
63
+ readonly borderRadius: `var(--${string})`;
64
+ };
65
+ readonly large: {
66
+ readonly height: `var(--${string})`;
67
+ readonly paddingInline: "9px";
68
+ readonly fontSize: `var(--${string})`;
69
+ readonly borderRadius: `var(--${string})`;
70
+ };
71
+ };
72
+ readonly variant: {
73
+ soft: {};
74
+ solid: {};
75
+ };
76
+ };
77
+ readonly defaultVariants: {
78
+ readonly tone: "neutral";
79
+ readonly size: "medium";
80
+ readonly variant: "soft";
81
+ };
82
+ readonly compoundVariants: [{
83
+ readonly variant: "solid";
84
+ readonly tone: "brand";
85
+ readonly decls: {
86
+ readonly background: `var(--${string})`;
87
+ readonly color: `var(--${string})`;
88
+ readonly boxShadow: "none";
89
+ };
90
+ }, {
91
+ readonly variant: "solid";
92
+ readonly tone: "success";
93
+ readonly decls: {
94
+ readonly background: `var(--${string})`;
95
+ readonly color: `var(--${string})`;
96
+ readonly boxShadow: "none";
97
+ };
98
+ }, {
99
+ readonly variant: "solid";
100
+ readonly tone: "warning";
101
+ readonly decls: {
102
+ readonly background: `var(--${string})`;
103
+ readonly color: `var(--${string})`;
104
+ readonly boxShadow: "none";
105
+ };
106
+ }, {
107
+ readonly variant: "solid";
108
+ readonly tone: "danger";
109
+ readonly decls: {
110
+ readonly background: `var(--${string})`;
111
+ readonly color: `var(--${string})`;
112
+ readonly boxShadow: "none";
113
+ };
114
+ }, {
115
+ readonly variant: "solid";
116
+ readonly tone: "neutral";
117
+ readonly decls: {
118
+ readonly background: `var(--${string})`;
119
+ readonly color: `var(--${string})`;
120
+ readonly boxShadow: "none";
121
+ };
122
+ }];
123
+ };
1
124
  declare const config: import("../..").RecipeConfig<{
2
- tone: {
3
- neutral: string;
4
- brand: string;
5
- success: string;
6
- warning: string;
7
- danger: string;
8
- };
9
- size: {
10
- small: string;
11
- medium: string;
12
- large: string;
13
- };
14
- variant: {
15
- soft: string;
16
- solid: string;
125
+ readonly tone: {
126
+ readonly neutral: string;
127
+ readonly brand: string;
128
+ readonly success: string;
129
+ readonly warning: string;
130
+ readonly danger: string;
131
+ };
132
+ readonly size: {
133
+ readonly small: string;
134
+ readonly medium: string;
135
+ readonly large: string;
136
+ };
137
+ readonly variant: {
138
+ readonly soft: string;
139
+ readonly solid: string;
17
140
  };
18
141
  }>;
19
142
  export declare const badgeRecipe: import("../..").RecipeFn<{
20
- tone: {
21
- neutral: string;
22
- brand: string;
23
- success: string;
24
- warning: string;
25
- danger: string;
26
- };
27
- size: {
28
- small: string;
29
- medium: string;
30
- large: string;
31
- };
32
- variant: {
33
- soft: string;
34
- solid: string;
143
+ readonly tone: {
144
+ readonly neutral: string;
145
+ readonly brand: string;
146
+ readonly success: string;
147
+ readonly warning: string;
148
+ readonly danger: string;
149
+ };
150
+ readonly size: {
151
+ readonly small: string;
152
+ readonly medium: string;
153
+ readonly large: string;
154
+ };
155
+ readonly variant: {
156
+ readonly soft: string;
157
+ readonly solid: string;
35
158
  };
36
159
  }> & {
37
160
  config: import("../..").RecipeConfig<{
38
- tone: {
39
- neutral: string;
40
- brand: string;
41
- success: string;
42
- warning: string;
43
- danger: string;
44
- };
45
- size: {
46
- small: string;
47
- medium: string;
48
- large: string;
49
- };
50
- variant: {
51
- soft: string;
52
- solid: string;
161
+ readonly tone: {
162
+ readonly neutral: string;
163
+ readonly brand: string;
164
+ readonly success: string;
165
+ readonly warning: string;
166
+ readonly danger: string;
167
+ };
168
+ readonly size: {
169
+ readonly small: string;
170
+ readonly medium: string;
171
+ readonly large: string;
172
+ };
173
+ readonly variant: {
174
+ readonly soft: string;
175
+ readonly solid: string;
53
176
  };
54
177
  }>;
55
178
  };
179
+ export { spec };
56
180
  export default config;
@@ -1,3 +1,27 @@
1
- declare const config: import("../..").RecipeConfig<{}>;
2
- export declare const dividerRecipe: import("../..").RecipeFn<{}>;
1
+ declare const spec: {
2
+ readonly name: "divider";
3
+ readonly base: {
4
+ readonly flexShrink: 0;
5
+ readonly border: "none";
6
+ readonly margin: 0;
7
+ readonly background: `var(--${string})`;
8
+ };
9
+ readonly parts: [{
10
+ readonly selector: "&[data-orientation=\"horizontal\"]";
11
+ readonly decls: {
12
+ readonly width: "100%";
13
+ readonly height: "1px";
14
+ };
15
+ }, {
16
+ readonly selector: "&[data-orientation=\"vertical\"]";
17
+ readonly decls: {
18
+ readonly width: "1px";
19
+ readonly alignSelf: "stretch";
20
+ readonly minHeight: "1em";
21
+ };
22
+ }];
23
+ };
24
+ declare const config: import("../..").RecipeConfig<Record<string, Record<string, string>>>;
25
+ export declare const dividerRecipe: import("../..").RecipeFn<Record<string, Record<string, string>>>;
26
+ export { spec };
3
27
  export default config;
@@ -1,3 +1,50 @@
1
- declare const config: import("../..").RecipeConfig<{}>;
2
- export declare const progressRecipe: import("../..").RecipeFn<{}>;
1
+ declare const spec: {
2
+ readonly name: "progress";
3
+ readonly base: {
4
+ readonly display: "block";
5
+ readonly boxSizing: "border-box";
6
+ readonly height: "6px";
7
+ readonly borderRadius: `var(--${string})`;
8
+ readonly background: `color-mix(in srgb, var(--${string}) 8%, transparent)`;
9
+ readonly overflow: "hidden";
10
+ };
11
+ readonly parts: [{
12
+ readonly slot: "fill";
13
+ readonly selector: "& .crisp-progress-fill";
14
+ readonly decls: {
15
+ readonly display: "block";
16
+ readonly height: "100%";
17
+ readonly borderRadius: `var(--${string})`;
18
+ readonly background: `var(--${string})`;
19
+ readonly transition: "width 0.25s ease";
20
+ };
21
+ }, {
22
+ readonly selector: "&[data-segmented]";
23
+ readonly decls: {
24
+ readonly display: "flex";
25
+ readonly gap: `var(--${string})`;
26
+ readonly background: "transparent";
27
+ readonly overflow: "visible";
28
+ };
29
+ }, {
30
+ readonly slot: "seg";
31
+ readonly selector: "&[data-segmented] .crisp-progress-seg";
32
+ readonly decls: {
33
+ readonly flex: 1;
34
+ readonly height: "100%";
35
+ readonly borderRadius: `var(--${string})`;
36
+ readonly background: `color-mix(in srgb, var(--${string}) 8%, transparent)`;
37
+ readonly transition: "background 0.2s";
38
+ };
39
+ }, {
40
+ readonly slot: "seg";
41
+ readonly selector: "&[data-segmented] .crisp-progress-seg[data-filled]";
42
+ readonly decls: {
43
+ background: `var(--${string})`;
44
+ };
45
+ }];
46
+ };
47
+ declare const config: import("../..").RecipeConfig<Record<string, Record<string, string>>>;
48
+ export declare const progressRecipe: import("../..").RecipeFn<Record<string, Record<string, string>>>;
49
+ export { spec };
3
50
  export default config;
@@ -1,3 +1,18 @@
1
- declare const config: import("../..").RecipeConfig<{}>;
2
- export declare const spinnerRecipe: import("../..").RecipeFn<{}>;
1
+ declare const spec: {
2
+ readonly name: "spinner";
3
+ readonly globalCss: "@keyframes crisp-spin { to { transform: rotate(360deg); } }";
4
+ readonly base: {
5
+ readonly display: "inline-block";
6
+ readonly boxSizing: "border-box";
7
+ readonly width: "16px";
8
+ readonly height: "16px";
9
+ readonly borderRadius: `var(--${string})`;
10
+ readonly border: `2px solid color-mix(in srgb, var(--${string}) 15%, transparent)`;
11
+ readonly borderTopColor: `var(--${string})`;
12
+ readonly animation: "crisp-spin 0.6s linear infinite";
13
+ };
14
+ };
15
+ declare const config: import("../..").RecipeConfig<Record<string, Record<string, string>>>;
16
+ export declare const spinnerRecipe: import("../..").RecipeFn<Record<string, Record<string, string>>>;
17
+ export { spec };
3
18
  export default config;
@@ -7,6 +7,7 @@ export { type FloatingAlign, type PlaceFloatingOptions, placeFloating, } from ".
7
7
  export { Primitive } from "./primitive";
8
8
  export { resolveRecipe } from "./resolve";
9
9
  export * from "./rtl";
10
+ export { defineSpec, specToRecipe } from "./spec";
10
11
  export type * from "./types";
11
12
  export { useFocusAfterRemoval } from "./use-focus-after-removal";
12
13
  export { useFormReset } from "./use-form-reset";
@@ -0,0 +1,83 @@
1
+ import type { RecipeConfig } from "./types";
2
+ /**
3
+ * A component spec: the layer above a recipe, where style intent is DATA
4
+ * rather than a CSS string.
5
+ *
6
+ * ADR 0002 §2. A string cannot be asked a question — `build/emit-lynx.mjs`
7
+ * mangled every at-rule and the Spinner never span (#174) because the only
8
+ * thing it could do to CSS text was rewrite it and hope. A spec is a list of
9
+ * properties, so a target that cannot express one can REFUSE it and fail the
10
+ * build, which is the difference between a broken build and a silently wrong
11
+ * one.
12
+ *
13
+ * Two deliberate limits:
14
+ *
15
+ * - `parts` is an ORDERED array, not a map of slots. The emitted CSS depends on
16
+ * rule order (`&[data-segmented] .seg` must follow `&[data-segmented]`), and
17
+ * a map of named slots cannot express an interleaving of child-slot rules and
18
+ * root state rules. The `slot` field carries the name a non-CSS target needs
19
+ * for a declared part; `selector` carries what the web needs.
20
+ * - Values stay `string | number`. Resolving a token to its `var(--…)` is the
21
+ * contract's job, already typed, and re-encoding token references here would
22
+ * be a second source of truth for values that lesson 31 says will drift.
23
+ */
24
+ export type SpecValue = string | number;
25
+ /** Declarations in camelCase, the way a native style object spells them. */
26
+ export type SpecDecls = Record<string, SpecValue>;
27
+ export type SpecPart = {
28
+ /** Name of the declared part, for targets with no CSS selectors. */
29
+ slot?: string;
30
+ /** The nested selector, relative to the component root (`&` is the root). */
31
+ selector: string;
32
+ decls: SpecDecls;
33
+ };
34
+ /** One variant value's declarations, plus any nested rules it adds. */
35
+ export type SpecVariantValue = SpecDecls | {
36
+ decls: SpecDecls;
37
+ parts?: SpecPart[];
38
+ };
39
+ export type ComponentSpec = {
40
+ name: string;
41
+ /** Declarations on the component root. */
42
+ base: SpecDecls;
43
+ /** Nested rules, in emission order. */
44
+ parts?: SpecPart[];
45
+ /** variant -> value -> declarations. */
46
+ variants?: Record<string, Record<string, SpecVariantValue>>;
47
+ defaultVariants?: Record<string, string | boolean>;
48
+ /** Selections that apply together, in emission order. */
49
+ compoundVariants?: Array<Record<string, unknown> & {
50
+ decls: SpecDecls;
51
+ }>;
52
+ /** Raw CSS that must sit outside the component rule (`@keyframes`). */
53
+ globalCss?: string;
54
+ };
55
+ /** `{ flexShrink: 0 }` -> `flex-shrink: 0;`, in insertion order. */
56
+ export declare function declsToCss(decls: SpecDecls): string;
57
+ /**
58
+ * `const` type parameter, not a plain `ComponentSpec` return: widening the
59
+ * variant keys here loses the literal union that `VariantProps<typeof recipe>`
60
+ * hands a component, and `BadgeProps["tone"]` silently degrades from
61
+ * `"neutral" | "brand" | …` to `string`. That is a public API regression that
62
+ * typechecks everywhere except at the call sites it was meant to protect.
63
+ */
64
+ export declare function defineSpec<const S extends ComponentSpec>(spec: S): S;
65
+ /** The recipe variant record a spec implies: same keys, values compiled to CSS. */
66
+ export type SpecVariants<S extends ComponentSpec> = S["variants"] extends Record<string, Record<string, SpecVariantValue>> ? {
67
+ [K in keyof S["variants"]]: {
68
+ [P in keyof S["variants"][K]]: string;
69
+ };
70
+ } : Record<string, Record<string, string>>;
71
+ /**
72
+ * Compile a spec into the RecipeConfig the existing pipeline already takes, so
73
+ * nothing downstream — the CSS generator, the d.ts generator, `resolveRecipe`,
74
+ * the spec guards — has to know a spec exists. That is what keeps the
75
+ * conversion provable: `dist/` must not move, and CI's
76
+ * `git diff --exit-code dist/` is the gate.
77
+ *
78
+ * The formatting here is not cosmetic freedom. It reproduces exactly what the
79
+ * hand-written template strings emit — root declarations space-joined, each
80
+ * nested rule on its own line indented four spaces — because any other choice
81
+ * rewrites every committed stylesheet and buries the conversion's real diff.
82
+ */
83
+ export declare function specToRecipe<const S extends ComponentSpec>(spec: S): RecipeConfig<SpecVariants<S>>;