clava 0.2.4 → 0.4.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.
- package/CHANGELOG.md +90 -0
- package/README.md +552 -0
- package/dist/index.d.ts +22 -30
- package/dist/index.js +356 -171
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/rolldown.config.ts +1 -0
- package/src/index.ts +718 -285
- package/src/types.ts +44 -49
- package/tests/_utils.ts +1 -3
- package/tests/build.test.ts +18 -0
- package/tests/component-api.test.ts +284 -15
- package/tests/extend.test.ts +6 -6
- package/tests/{computed-variants.test.ts → function-variants.test.ts} +105 -25
- package/tests/prototype-pollution.test.ts +6 -6
- package/tests/{computed.test.ts → refine.test.ts} +517 -100
- package/tests/solid.test.ts +30 -0
- package/tests/split-props.test.ts +73 -1
- package/tests/variants-inference.test.ts +252 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,95 @@
|
|
|
1
1
|
# clava
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Removed `computedVariants` in favor of function values in `variants`
|
|
6
|
+
|
|
7
|
+
**BREAKING** if you're using the `computedVariants` config option.
|
|
8
|
+
|
|
9
|
+
Define a function directly inside [`variants`](https://clava.style/docs/reference/cv#variants) — it now acts as a function variant. The function's parameter type defines the prop type and replaces any inherited variant for the same key.
|
|
10
|
+
|
|
11
|
+
Before:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const grid = cv({
|
|
15
|
+
variants: {
|
|
16
|
+
color: { red: "text-red", blue: "text-blue" },
|
|
17
|
+
},
|
|
18
|
+
computedVariants: {
|
|
19
|
+
columns: (value: number) => `grid-cols-${value}`,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
After:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const grid = cv({
|
|
28
|
+
variants: {
|
|
29
|
+
color: { red: "text-red", blue: "text-blue" },
|
|
30
|
+
columns: (value: number) => `grid-cols-${value}`,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Renamed `computed` to `refine`
|
|
36
|
+
|
|
37
|
+
**BREAKING** if you use the `computed` config field on [`cv`](https://clava.style/docs/reference/cv).
|
|
38
|
+
|
|
39
|
+
The `computed` field previously collided with `computedVariants`, even though the two have very different semantics: `computedVariants` is a map of pure per-variant transformer functions, while `computed` is a single imperative callback that can mutate variants, set defaults, and emit class/style output across re-runs until variants stabilize. The new name `refine` describes that iterative refinement and removes the collision.
|
|
40
|
+
|
|
41
|
+
Rename the `computed` field to `refine`. The callback signature and context (`variants`, `setVariants`, `setDefaultVariants`, `addClass`, `addStyle`) are unchanged.
|
|
42
|
+
|
|
43
|
+
Before:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const button = cv({
|
|
47
|
+
variants: { size: { sm: "sm", lg: "lg" } },
|
|
48
|
+
computed: ({ variants, addClass }) => {
|
|
49
|
+
if (variants.size === "lg") {
|
|
50
|
+
addClass("is-large");
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
After:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const button = cv({
|
|
60
|
+
variants: { size: { sm: "sm", lg: "lg" } },
|
|
61
|
+
refine: ({ variants, addClass }) => {
|
|
62
|
+
if (variants.size === "lg") {
|
|
63
|
+
addClass("is-large");
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 0.3.0
|
|
70
|
+
|
|
71
|
+
### Removed `keys`
|
|
72
|
+
|
|
73
|
+
**BREAKING** if you're reading `keys` from [`cv`](https://clava.style/docs/reference/cv) components.
|
|
74
|
+
|
|
75
|
+
Use `propKeys` instead. `propKeys` is now the only API for style props plus variant props and has accurate HTML and HTML object types for libraries such as Solid's `splitProps`.
|
|
76
|
+
|
|
77
|
+
Before:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
button.keys;
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
After:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
button.propKeys;
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Re-run `cv` computed callbacks when they change variants
|
|
90
|
+
|
|
91
|
+
This makes later reads in the same component chain, including [`getVariants()`](https://clava.style/docs/reference/getVariants) and extended components, use the latest values. Re-runs are capped at 50 iterations, after which Clava stops and logs a development warning.
|
|
92
|
+
|
|
3
93
|
## 0.2.4
|
|
4
94
|
|
|
5
95
|
- Fixed [`cv`](https://clava.style/docs/reference/cv) variant props inferred from array class values to use boolean shorthand props.
|
package/README.md
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
# Clava
|
|
2
|
+
|
|
3
|
+
Type-safe class and style variants for framework components. Clava turns variant props into class/style prop objects, works with any class naming system, and keeps the generated API easy for TypeScript and editors to understand.
|
|
4
|
+
|
|
5
|
+
Clava is an ESM package. Import from the package root:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { cv, cx, create, splitProps } from "clava";
|
|
9
|
+
import type { Variant, VariantProps } from "clava";
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Contents
|
|
13
|
+
|
|
14
|
+
- [Install](#install)
|
|
15
|
+
- [Quick Start](#quick-start)
|
|
16
|
+
- [Output Modes](#output-modes)
|
|
17
|
+
- [Classes And Styles](#classes-and-styles)
|
|
18
|
+
- [Variants](#variants)
|
|
19
|
+
- [Function Variants](#function-variants)
|
|
20
|
+
- [Extending Components](#extending-components)
|
|
21
|
+
- [Refine](#refine)
|
|
22
|
+
- [Splitting Props](#splitting-props)
|
|
23
|
+
- [React](#react)
|
|
24
|
+
- [Solid](#solid)
|
|
25
|
+
- [`create()` And `cx()`](#create-and-cx)
|
|
26
|
+
- [Type Helpers](#type-helpers)
|
|
27
|
+
- [API Summary](#api-summary)
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pnpm add clava
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npm install clava
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
yarn add clava
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
Use `cv()` to create a callable style component. The default callable component returns normalized `{ class, style }` props.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { cv } from "clava";
|
|
49
|
+
|
|
50
|
+
const button = cv({
|
|
51
|
+
class: "button",
|
|
52
|
+
style: { borderRadius: "6px" },
|
|
53
|
+
variants: {
|
|
54
|
+
size: {
|
|
55
|
+
sm: "button-sm",
|
|
56
|
+
lg: { class: "button-lg", style: { fontSize: "16px" } },
|
|
57
|
+
},
|
|
58
|
+
intent: {
|
|
59
|
+
primary: "button-primary",
|
|
60
|
+
danger: "button-danger",
|
|
61
|
+
},
|
|
62
|
+
disabled: {
|
|
63
|
+
true: "button-disabled",
|
|
64
|
+
false: "",
|
|
65
|
+
},
|
|
66
|
+
fluid: "button-fluid",
|
|
67
|
+
},
|
|
68
|
+
defaultVariants: {
|
|
69
|
+
size: "sm",
|
|
70
|
+
intent: "primary",
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
button({ size: "lg", disabled: true, fluid: true, className: "mt-2" });
|
|
75
|
+
// {
|
|
76
|
+
// class: "button button-lg button-primary button-disabled button-fluid mt-2",
|
|
77
|
+
// style: { borderRadius: "6px", fontSize: "16px" },
|
|
78
|
+
// }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Variant prop types are inferred from the `variants`, `defaultVariants`, and `extend` configuration. Invalid variant keys and values are TypeScript errors and are ignored at runtime.
|
|
82
|
+
|
|
83
|
+
Input props may use `class` or `className` in any output mode. Both are appended to the generated class string.
|
|
84
|
+
|
|
85
|
+
## Output Modes
|
|
86
|
+
|
|
87
|
+
Every Clava component has four output modes:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
const label = cv({
|
|
91
|
+
class: "label",
|
|
92
|
+
style: { fontSize: "14px", "--accent": "red" },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
label();
|
|
96
|
+
// { class: "label", style: { fontSize: "14px", "--accent": "red" } }
|
|
97
|
+
|
|
98
|
+
label.jsx();
|
|
99
|
+
// { className: "label", style: { fontSize: "14px", "--accent": "red" } }
|
|
100
|
+
|
|
101
|
+
label.html();
|
|
102
|
+
// { class: "label", style: "font-size: 14px; --accent: red;" }
|
|
103
|
+
|
|
104
|
+
label.htmlObj();
|
|
105
|
+
// { class: "label", style: { "font-size": "14px", "--accent": "red" } }
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Use the default callable component when you want Clava's own normalized shape. Use `.jsx()` for React-style `className` props, `.html()` when you need an HTML style string, and `.htmlObj()` when you need hyphenated CSS property names.
|
|
109
|
+
|
|
110
|
+
Each mode also exposes helpers:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
button.class({ size: "lg" });
|
|
114
|
+
// "button button-lg button-primary"
|
|
115
|
+
|
|
116
|
+
button.style({ size: "lg" });
|
|
117
|
+
// { borderRadius: "6px", fontSize: "16px" }
|
|
118
|
+
|
|
119
|
+
button.getVariants({ size: "lg" });
|
|
120
|
+
// { disabled: false, size: "lg", intent: "primary" }
|
|
121
|
+
|
|
122
|
+
button.propKeys;
|
|
123
|
+
// ["class", "className", "style", "size", "intent", "disabled", "fluid"]
|
|
124
|
+
|
|
125
|
+
button.variantKeys;
|
|
126
|
+
// ["size", "intent", "disabled", "fluid"]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`propKeys` includes style props plus variant props for that mode. `variantKeys` includes only variant props.
|
|
130
|
+
|
|
131
|
+
## Classes And Styles
|
|
132
|
+
|
|
133
|
+
`class` values are passed through `clsx`, so strings, arrays, nested arrays, and falsy values work the same way they do in `clsx`.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const box = cv({
|
|
137
|
+
class: ["box", ["rounded", false && "hidden"]],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
box().class;
|
|
141
|
+
// "box rounded"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Config styles use camelCase CSS property names and string values. CSS custom properties are supported.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const card = cv({
|
|
148
|
+
style: {
|
|
149
|
+
paddingBlock: "8px",
|
|
150
|
+
"--card-accent": "oklch(62% 0.2 250)",
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Style results from `variants` and `refine` must use an explicit `{ style }` wrapper. A raw object like `{ backgroundColor: "red" }` is not a style result by itself.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
const chip = cv({
|
|
159
|
+
variants: {
|
|
160
|
+
tone: {
|
|
161
|
+
info: {
|
|
162
|
+
class: "chip-info",
|
|
163
|
+
style: { color: "blue" },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
User `style` props can be JSX-style objects, HTML style strings, or hyphenated style objects. User styles are merged last, so they override generated style keys.
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
chip({
|
|
174
|
+
tone: "info",
|
|
175
|
+
style: "color: navy; margin-top: 4px;",
|
|
176
|
+
});
|
|
177
|
+
// { class: "chip-info", style: { color: "navy", marginTop: "4px" } }
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Variants
|
|
181
|
+
|
|
182
|
+
Object variants infer prop values from their keys. String keys named `"true"` and `"false"` become boolean props.
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
const badge = cv({
|
|
186
|
+
variants: {
|
|
187
|
+
tone: {
|
|
188
|
+
neutral: "badge-neutral",
|
|
189
|
+
success: "badge-success",
|
|
190
|
+
},
|
|
191
|
+
selected: {
|
|
192
|
+
true: "badge-selected",
|
|
193
|
+
false: "badge-unselected",
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
defaultVariants: {
|
|
197
|
+
tone: "neutral",
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
badge({ tone: "success", selected: true }).class;
|
|
202
|
+
// "badge-success badge-selected"
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
A variant with a `"false"` branch implicitly defaults to `false` when no default is set. Passing `undefined` does not override a default variant.
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
badge().class;
|
|
209
|
+
// "badge-neutral badge-unselected"
|
|
210
|
+
|
|
211
|
+
badge({ tone: undefined }).class;
|
|
212
|
+
// "badge-neutral badge-unselected"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
String and array shorthand variants are boolean variants that emit only when the prop is `true`.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
const item = cv({
|
|
219
|
+
variants: {
|
|
220
|
+
active: "item-active",
|
|
221
|
+
interactive: ["item-interactive", "focus-visible:ring"],
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
item({ active: true, interactive: true }).class;
|
|
226
|
+
// "item-active item-interactive focus-visible:ring"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Variant values can be class values, arrays, `{ class, style }` objects, or [functions](#function-variants). Use `null` in an extending component to disable inherited variants or inherited variant values.
|
|
230
|
+
|
|
231
|
+
## Function Variants
|
|
232
|
+
|
|
233
|
+
Add a function as a variant value when the prop should generate class/style output dynamically. The function's parameter type defines the prop type.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
const grid = cv({
|
|
237
|
+
class: "grid",
|
|
238
|
+
variants: {
|
|
239
|
+
columns: (value: number) => ({
|
|
240
|
+
class: `grid-cols-${value}`,
|
|
241
|
+
style: { "--grid-columns": `${value}` },
|
|
242
|
+
}),
|
|
243
|
+
color: (value: string | null) => {
|
|
244
|
+
return value ? `text-${value}` : "text-current";
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
grid({ columns: 3, color: null });
|
|
250
|
+
// {
|
|
251
|
+
// class: "grid grid-cols-3 text-current",
|
|
252
|
+
// style: { "--grid-columns": "3" },
|
|
253
|
+
// }
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Function variants can return any class value, `{ class, style }`, a default Clava component result, `null`, or `undefined`. A function variant with the same key as an extended variant replaces that inherited variant's prop type and output.
|
|
257
|
+
|
|
258
|
+
## Extending Components
|
|
259
|
+
|
|
260
|
+
Use `extend` to compose existing Clava components. Extended base classes are ordered before the child class, and extended variant output is applied before child variant output.
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
const baseButton = cv({
|
|
264
|
+
class: "button",
|
|
265
|
+
variants: {
|
|
266
|
+
size: {
|
|
267
|
+
sm: "button-sm",
|
|
268
|
+
lg: "button-lg",
|
|
269
|
+
},
|
|
270
|
+
intent: {
|
|
271
|
+
neutral: "button-neutral",
|
|
272
|
+
brand: "button-brand",
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
defaultVariants: {
|
|
276
|
+
size: "sm",
|
|
277
|
+
intent: "neutral",
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const iconButton = cv({
|
|
282
|
+
extend: [baseButton],
|
|
283
|
+
class: "icon-button",
|
|
284
|
+
variants: {
|
|
285
|
+
size: {
|
|
286
|
+
sm: "icon-button-sm",
|
|
287
|
+
},
|
|
288
|
+
intent: {
|
|
289
|
+
brand: null,
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
defaultVariants: {
|
|
293
|
+
size: "lg",
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
iconButton({ size: "sm" }).class;
|
|
298
|
+
// "button icon-button button-sm button-neutral icon-button-sm"
|
|
299
|
+
|
|
300
|
+
iconButton({ intent: "brand" });
|
|
301
|
+
// TypeScript error: "brand" was disabled by the child component.
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Set an inherited variant to `null` to remove it entirely:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
const plainButton = cv({
|
|
308
|
+
extend: [baseButton],
|
|
309
|
+
variants: {
|
|
310
|
+
intent: null,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
You can extend any component mode, including `baseButton.jsx`, `baseButton.html`, and `baseButton.htmlObj`.
|
|
316
|
+
|
|
317
|
+
## Refine
|
|
318
|
+
|
|
319
|
+
Use `refine` for compound conditions, dependent defaults, and final class/style adjustments. It receives the resolved variant values for the component and can return class/style output.
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
const toolbarButton = cv({
|
|
323
|
+
extend: [baseButton],
|
|
324
|
+
variants: {
|
|
325
|
+
pressed: {
|
|
326
|
+
true: "toolbar-button-pressed",
|
|
327
|
+
false: "",
|
|
328
|
+
},
|
|
329
|
+
loading: "toolbar-button-loading",
|
|
330
|
+
},
|
|
331
|
+
refine: ({
|
|
332
|
+
variants,
|
|
333
|
+
setVariants,
|
|
334
|
+
setDefaultVariants,
|
|
335
|
+
addClass,
|
|
336
|
+
addStyle,
|
|
337
|
+
}) => {
|
|
338
|
+
if (variants.loading) {
|
|
339
|
+
setVariants({ pressed: false });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (variants.size === "lg") {
|
|
343
|
+
setDefaultVariants({ intent: "neutral" });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (variants.pressed && variants.intent === "brand") {
|
|
347
|
+
addClass("toolbar-button-brand-pressed");
|
|
348
|
+
addStyle({ transform: "translateY(1px)" });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return variants.loading ? "is-loading" : null;
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
`setVariants()` overrides explicit props. `setDefaultVariants()` overrides static `defaultVariants` and inherited defaults, but it does not override a prop the user explicitly passed unless that prop value is `undefined`. `addClass()` and `addStyle()` append output without changing resolved variant values. `getVariants()` includes values changed by `setVariants()` and `setDefaultVariants()`.
|
|
357
|
+
|
|
358
|
+
When a `refine` callback changes variants, Clava re-runs the refine chain so later reads see the latest values. Re-runs are capped at 50 iterations, after which Clava stops and logs a warning in development.
|
|
359
|
+
|
|
360
|
+
## Splitting Props
|
|
361
|
+
|
|
362
|
+
Use `splitProps()` to separate variant/style props from DOM or framework props without manually maintaining prop-name lists.
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
import type { ComponentProps } from "react";
|
|
366
|
+
import { type VariantProps, cv, splitProps } from "clava";
|
|
367
|
+
|
|
368
|
+
const button = cv({
|
|
369
|
+
class: "button",
|
|
370
|
+
variants: {
|
|
371
|
+
size: {
|
|
372
|
+
sm: "button-sm",
|
|
373
|
+
lg: "button-lg",
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
}).jsx;
|
|
377
|
+
|
|
378
|
+
type ButtonProps = ComponentProps<"button"> & VariantProps<typeof button>;
|
|
379
|
+
|
|
380
|
+
function Button(props: ButtonProps) {
|
|
381
|
+
const [variantProps, buttonProps] = splitProps(props, button);
|
|
382
|
+
return <button {...buttonProps} {...button(variantProps)} />;
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
The first component source claims variant props plus styling props (`class`, `className`, and `style`, depending on the mode). Later component sources receive only their variant props. Array sources receive exactly the listed keys and do not claim styling props.
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
const [buttonProps, fieldProps, rest] = splitProps(props, button, field);
|
|
390
|
+
// buttonProps: button variants + class/style props
|
|
391
|
+
// fieldProps: field variants only
|
|
392
|
+
// rest: props not claimed by either component
|
|
393
|
+
|
|
394
|
+
const [dataProps, variantProps, otherProps] = splitProps(
|
|
395
|
+
props,
|
|
396
|
+
["id", "data-testid"],
|
|
397
|
+
button,
|
|
398
|
+
);
|
|
399
|
+
// dataProps: id and data-testid
|
|
400
|
+
// variantProps: button variants + class/style props
|
|
401
|
+
// otherProps: remaining props
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
`splitProps()` only moves props that are actually present in the input object. It does not inject `defaultVariants`; call `component.getVariants()` when you need resolved variant values.
|
|
405
|
+
|
|
406
|
+
## React
|
|
407
|
+
|
|
408
|
+
Use `.jsx` for React components because it returns `className` and a camelCase style object.
|
|
409
|
+
|
|
410
|
+
```tsx
|
|
411
|
+
import type { ComponentProps } from "react";
|
|
412
|
+
import { type VariantProps, cv, splitProps } from "clava";
|
|
413
|
+
|
|
414
|
+
const button = cv({
|
|
415
|
+
class: "button",
|
|
416
|
+
style: { fontSize: "16px" },
|
|
417
|
+
variants: {
|
|
418
|
+
size: {
|
|
419
|
+
sm: "button-sm",
|
|
420
|
+
md: "button-md",
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
}).jsx;
|
|
424
|
+
|
|
425
|
+
interface ButtonProps
|
|
426
|
+
extends ComponentProps<"button">, VariantProps<typeof button> {}
|
|
427
|
+
|
|
428
|
+
function Button(props: ButtonProps) {
|
|
429
|
+
const [variantProps, buttonProps] = splitProps(props, button);
|
|
430
|
+
return <button {...buttonProps} {...button(variantProps)} />;
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## Solid
|
|
435
|
+
|
|
436
|
+
Use `.htmlObj` for Solid components when you want `class` and hyphenated style object output.
|
|
437
|
+
|
|
438
|
+
```tsx
|
|
439
|
+
import type { ComponentProps } from "solid-js";
|
|
440
|
+
import { type VariantProps, cv, splitProps } from "clava";
|
|
441
|
+
|
|
442
|
+
const button = cv({
|
|
443
|
+
class: "button",
|
|
444
|
+
style: { fontSize: "16px" },
|
|
445
|
+
variants: {
|
|
446
|
+
size: {
|
|
447
|
+
sm: "button-sm",
|
|
448
|
+
md: "button-md",
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
}).htmlObj;
|
|
452
|
+
|
|
453
|
+
type ButtonProps = ComponentProps<"button"> & VariantProps<typeof button>;
|
|
454
|
+
|
|
455
|
+
function Button(props: ButtonProps) {
|
|
456
|
+
const [variantProps, buttonProps] = splitProps(props, button);
|
|
457
|
+
return <button {...buttonProps} {...button(variantProps)} />;
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## `create()` And `cx()`
|
|
462
|
+
|
|
463
|
+
The package-level `cv` and `cx` come from `create()` with no class transform. Use `create({ transformClass })` when every generated class string should pass through a transform, such as a prefixer or CSS-module lookup.
|
|
464
|
+
|
|
465
|
+
```ts
|
|
466
|
+
import { create } from "clava";
|
|
467
|
+
|
|
468
|
+
const { cv, cx } = create({
|
|
469
|
+
transformClass: (className) => {
|
|
470
|
+
return className
|
|
471
|
+
.split(" ")
|
|
472
|
+
.filter(Boolean)
|
|
473
|
+
.map((name) => `tw-${name}`)
|
|
474
|
+
.join(" ");
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
cx("px-2", ["font-bold", false && "hidden"]);
|
|
479
|
+
// "tw-px-2 tw-font-bold"
|
|
480
|
+
|
|
481
|
+
const title = cv({ class: "text-lg font-semibold" });
|
|
482
|
+
title().class;
|
|
483
|
+
// "tw-text-lg tw-font-semibold"
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
When a component created by one factory extends a component created by another factory, the extended component's transform is preserved for its own classes and the parent transform still runs on the final joined string.
|
|
487
|
+
|
|
488
|
+
## Type Helpers
|
|
489
|
+
|
|
490
|
+
Use `VariantProps<typeof component>` to add a Clava component's variant props to framework component props.
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
import type { ComponentProps } from "react";
|
|
494
|
+
import type { VariantProps } from "clava";
|
|
495
|
+
|
|
496
|
+
type ButtonProps = ComponentProps<"button"> & VariantProps<typeof button>;
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Use `Variant<typeof component, "key">` to constrain a new variant map to the same values as another component's variant.
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
import { type Variant, cv } from "clava";
|
|
503
|
+
|
|
504
|
+
const button = cv({
|
|
505
|
+
variants: {
|
|
506
|
+
size: {
|
|
507
|
+
sm: "button-sm",
|
|
508
|
+
lg: "button-lg",
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const icon = cv({
|
|
514
|
+
extend: [button],
|
|
515
|
+
variants: {
|
|
516
|
+
size: {
|
|
517
|
+
sm: "icon-sm",
|
|
518
|
+
lg: "icon-lg",
|
|
519
|
+
} satisfies Variant<typeof button, "size">,
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
The package also exports `ClassValue`, `StyleValue`, `StyleClassProps`, `StyleClassValue`, `JSXProps`, `HTMLProps`, `HTMLObjProps`, `CVComponent`, and `CVConfig`.
|
|
525
|
+
|
|
526
|
+
## API Summary
|
|
527
|
+
|
|
528
|
+
`cv(config?)` creates a typed Clava component. Supported config keys are `extend`, `class`, `style`, `variants`, `defaultVariants`, and `refine`.
|
|
529
|
+
|
|
530
|
+
`component(props?)` returns `{ class, style }` with normalized camelCase style keys.
|
|
531
|
+
|
|
532
|
+
`component.jsx(props?)` returns `{ className, style }`.
|
|
533
|
+
|
|
534
|
+
`component.html(props?)` returns `{ class, style }`, where `style` is a CSS string.
|
|
535
|
+
|
|
536
|
+
`component.htmlObj(props?)` returns `{ class, style }`, where `style` is a hyphenated CSS property object.
|
|
537
|
+
|
|
538
|
+
`component.class(props?)` returns only the resolved class string.
|
|
539
|
+
|
|
540
|
+
`component.style(props?)` returns only the resolved style value for that component mode.
|
|
541
|
+
|
|
542
|
+
`component.getVariants(props?)` returns resolved variant values after static defaults, inherited defaults, and `refine` updates.
|
|
543
|
+
|
|
544
|
+
`component.propKeys` lists style props plus variant props for that component mode.
|
|
545
|
+
|
|
546
|
+
`component.variantKeys` lists only variant prop keys.
|
|
547
|
+
|
|
548
|
+
`splitProps(props, source1, ...sources)` returns one object per source plus a final rest object.
|
|
549
|
+
|
|
550
|
+
`cx(...classes)` joins class values with `clsx` and applies the factory's `transformClass`.
|
|
551
|
+
|
|
552
|
+
`create(options?)` returns isolated `{ cv, cx }` helpers. The only option is `transformClass?: (className: string) => string`.
|