clava 0.2.4 → 0.3.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,29 @@
1
1
  # clava
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Removed `keys`
6
+
7
+ **BREAKING** if you're reading `keys` from [`cv`](https://clava.style/docs/reference/cv) components.
8
+
9
+ 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`.
10
+
11
+ Before:
12
+
13
+ ```ts
14
+ button.keys;
15
+ ```
16
+
17
+ After:
18
+
19
+ ```ts
20
+ button.propKeys;
21
+ ```
22
+
23
+ ### Re-run `cv` computed callbacks when they change variants
24
+
25
+ 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.
26
+
3
27
  ## 0.2.4
4
28
 
5
29
  - 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
+ - [Extending Components](#extending-components)
20
+ - [Computed Variants](#computed-variants)
21
+ - [Computed Logic](#computed-logic)
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`, `computedVariants`, `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
+ Variant and computed style results 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, or `{ class, style }` objects. Use `null` in an extending component to disable inherited variants or inherited variant values.
230
+
231
+ ## Extending Components
232
+
233
+ 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.
234
+
235
+ ```ts
236
+ const baseButton = cv({
237
+ class: "button",
238
+ variants: {
239
+ size: {
240
+ sm: "button-sm",
241
+ lg: "button-lg",
242
+ },
243
+ intent: {
244
+ neutral: "button-neutral",
245
+ brand: "button-brand",
246
+ },
247
+ },
248
+ defaultVariants: {
249
+ size: "sm",
250
+ intent: "neutral",
251
+ },
252
+ });
253
+
254
+ const iconButton = cv({
255
+ extend: [baseButton],
256
+ class: "icon-button",
257
+ variants: {
258
+ size: {
259
+ sm: "icon-button-sm",
260
+ },
261
+ intent: {
262
+ brand: null,
263
+ },
264
+ },
265
+ defaultVariants: {
266
+ size: "lg",
267
+ },
268
+ });
269
+
270
+ iconButton({ size: "sm" }).class;
271
+ // "button icon-button button-sm button-neutral icon-button-sm"
272
+
273
+ iconButton({ intent: "brand" });
274
+ // TypeScript error: "brand" was disabled by the child component.
275
+ ```
276
+
277
+ Set an inherited variant to `null` to remove it entirely:
278
+
279
+ ```ts
280
+ const plainButton = cv({
281
+ extend: [baseButton],
282
+ variants: {
283
+ intent: null,
284
+ },
285
+ });
286
+ ```
287
+
288
+ You can extend any component mode, including `baseButton.jsx`, `baseButton.html`, and `baseButton.htmlObj`.
289
+
290
+ ## Computed Variants
291
+
292
+ Use `computedVariants` when a prop value should generate class/style output dynamically. The function parameter defines the prop type.
293
+
294
+ ```ts
295
+ const grid = cv({
296
+ class: "grid",
297
+ computedVariants: {
298
+ columns: (value: number) => ({
299
+ class: `grid-cols-${value}`,
300
+ style: { "--grid-columns": `${value}` },
301
+ }),
302
+ color: (value: string | null) => {
303
+ return value ? `text-${value}` : "text-current";
304
+ },
305
+ },
306
+ });
307
+
308
+ grid({ columns: 3, color: null });
309
+ // {
310
+ // class: "grid grid-cols-3 text-current",
311
+ // style: { "--grid-columns": "3" },
312
+ // }
313
+ ```
314
+
315
+ Computed variants can return any class value, `{ class, style }`, a default Clava component result, `null`, or `undefined`. A `computedVariants` entry with the same key as an extended variant replaces that inherited variant's prop type and output.
316
+
317
+ ## Computed Logic
318
+
319
+ Use `computed` 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
+ computed: ({
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 computed callback changes variants, Clava re-runs the computed 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`, `computedVariants`, `defaultVariants`, and `computed`.
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 computed variant 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`.
package/dist/index.d.ts CHANGED
@@ -28,10 +28,11 @@ type ComponentResultValue<K extends AllComponentResultKeys> = K extends "style"
28
28
  type NullableComponentResult = { [K in AllComponentResultKeys]?: ComponentResultValue<K> | null };
29
29
  type ComponentProps<V = {}> = VariantValues<V> & NullableComponentResult;
30
30
  type GetVariants<V> = (variants?: VariantValues<V>) => VariantValues<V>;
31
+ type ComponentPropKey<R extends ComponentResult> = keyof R | (R extends StyleClassProps ? "className" : never);
31
32
  type KeySourceArray = readonly string[];
32
33
  type KeySourceComponent = {
33
- keys: readonly (string | number | symbol)[];
34
- variantKeys: readonly (string | number | symbol)[];
34
+ propKeys: readonly string[];
35
+ variantKeys: readonly string[];
35
36
  getVariants: () => Record<string, unknown>;
36
37
  };
37
38
  type KeySource = KeySourceArray | KeySourceComponent;
@@ -39,7 +40,7 @@ type IsComponent<S> = S extends {
39
40
  getVariants: () => unknown;
40
41
  } ? true : false;
41
42
  type SourceKeys<S> = S extends readonly (infer K)[] ? K : S extends {
42
- keys: readonly (infer K)[];
43
+ propKeys: readonly (infer K)[];
43
44
  } ? K : never;
44
45
  type SourceVariantKeys<S> = S extends readonly (infer K)[] ? K : S extends {
45
46
  variantKeys: readonly (infer K)[];
@@ -57,9 +58,8 @@ interface ModalComponent<V, R extends ComponentResult> {
57
58
  class: (props?: ComponentProps<V>) => string;
58
59
  style: (props?: ComponentProps<V>) => R["style"];
59
60
  getVariants: GetVariants<V>;
60
- keys: (keyof V | keyof NullableComponentResult)[];
61
61
  variantKeys: (keyof V)[];
62
- propKeys: (keyof V | keyof NullableComponentResult)[];
62
+ propKeys: (keyof V | ComponentPropKey<R>)[];
63
63
  }
64
64
  interface CVComponent<V extends Variants = {}, CV extends ComputedVariants = {}, E extends AnyComponent[] = [], R extends ComponentResult = StyleClassProps> extends ModalComponent<MergeVariants<V, CV, E>, R> {
65
65
  jsx: ModalComponent<MergeVariants<V, CV, E>, JSXProps>;