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 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`.