@arun-dev/headless 0.2.0 → 0.2.1

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/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # @arun-dev/headless
2
+
3
+ Unstyled React behaviour primitives. Components bring their behaviour, keyboard handling and
4
+ accessibility, and nothing else — no CSS, no class names, no colour. The styling is entirely
5
+ yours.
6
+
7
+ Every part takes a `render` prop, spreads unrecognised props onto the element it renders, and
8
+ projects its state as `data-*` attributes, so any styling approach works: plain CSS, CSS modules,
9
+ utility classes, or a component library of your own.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install @arun-dev/headless
15
+ ```
16
+
17
+ Peer dependencies: `react >= 19`, `react-dom >= 19`.
18
+
19
+ ## Components
20
+
21
+ ```tsx
22
+ import { Switch } from '@arun-dev/headless/switch';
23
+
24
+ <Switch.Root defaultChecked onCheckedChange={save} aria-label="Notifications">
25
+ <Switch.Thumb />
26
+ </Switch.Root>;
27
+ ```
28
+
29
+ | Component | Parts | Props |
30
+ | --------- | ----------------------- | --------------------------------------------------------------------------- |
31
+ | `Switch` | `Switch.Root`, `.Thumb` | `checked`, `defaultChecked`, `onCheckedChange`, `disabled`, `name`, `value` |
32
+
33
+ `Switch.Root` renders a native `<button>`, so focus, `Space`, `Enter` and disabled semantics come
34
+ from the platform. It carries `role="switch"` and `aria-checked`, but **no accessible name** —
35
+ wrap it in a `<label>` or pass `aria-label`. A headless component should not guess at your copy.
36
+
37
+ ## State reaches CSS through `data-*`
38
+
39
+ A headless component owns no class names, so state is projected onto the DOM instead. That
40
+ attribute name is the entire contract between behaviour and styling:
41
+
42
+ ```css
43
+ .my-switch[data-checked] {
44
+ background: rebeccapurple;
45
+ }
46
+ ```
47
+
48
+ Both parts of `Switch` emit `data-checked` / `data-unchecked` / `data-disabled`. The negative form
49
+ is emitted deliberately: `:not([data-checked])` would also match any third state added later, so
50
+ matching the state you mean keeps future states additive.
51
+
52
+ ## Controlled and uncontrolled
53
+
54
+ Pass `checked` with `onCheckedChange` and the parent owns the value — the switch will not move on
55
+ its own. Pass `defaultChecked` and the component owns it. `onCheckedChange` fires in both modes.
56
+
57
+ The mode is decided **once, at mount**, and never re-evaluated. A `checked` of `undefined` on the
58
+ first render therefore makes the component uncontrolled for the rest of its life, and every value
59
+ passed afterwards is ignored. When the value arrives asynchronously, coalesce at the call site:
60
+
61
+ ```tsx
62
+ <Switch.Root checked={enabled ?? false} onCheckedChange={setEnabled} />
63
+ ```
64
+
65
+ Mixing the modes, or changing the default after mount, logs a development-only warning.
66
+
67
+ ## Composition
68
+
69
+ Every part takes a `render` prop to change the element, and spreads unrecognised props onto it:
70
+
71
+ ```tsx
72
+ <Switch.Root render={<Tooltip.Trigger />} />
73
+ ```
74
+
75
+ `className` is concatenated, event handlers are chained rather than replaced, `style` is merged,
76
+ and refs are merged — so a `ref` on the `render` element and a `ref` on the component both receive
77
+ the node.
78
+
79
+ ## Engine
80
+
81
+ The primitives the components are built from are exported from the root, for building your own:
82
+
83
+ ```ts
84
+ import {
85
+ useRender,
86
+ useControlled,
87
+ mergeProps,
88
+ getStateAttributes,
89
+ booleanAttribute,
90
+ } from '@arun-dev/headless';
91
+ ```
92
+
93
+ | Export | Purpose |
94
+ | -------------------- | ----------------------------------------------------------------------------- |
95
+ | `useRender` | Resolves what a part renders — merges props, projects state, applies `render` |
96
+ | `useControlled` | One value, controlled or uncontrolled, decided at mount |
97
+ | `mergeProps` | Merges prop objects: handlers chain, `className` concatenates, refs merge |
98
+ | `getStateAttributes` | Projects a state object onto `data-*` attributes via a declared mapping |
99
+ | `booleanAttribute` | The common mapping — one attribute when true, another when false |
@@ -2,7 +2,13 @@ import * as react from 'react';
2
2
  import { ReactNode, ReactElement, Ref } from 'react';
3
3
 
4
4
  interface SwitchRootProps {
5
- /** Controlled state. Provide `onCheckedChange` alongside it. */
5
+ /**
6
+ * Controlled state. Provide `onCheckedChange` alongside it.
7
+ *
8
+ * Never `undefined` once mounted. The mode is latched at mount, so an `undefined`
9
+ * first render makes the switch uncontrolled for good and every value passed later
10
+ * is ignored. Coalesce at the call site — `checked={x ?? false}`.
11
+ */
6
12
  checked?: boolean;
7
13
  /** Initial state when uncontrolled. Read once, at mount. */
8
14
  defaultChecked?: boolean;
@@ -2,7 +2,13 @@ import * as react from 'react';
2
2
  import { ReactNode, ReactElement, Ref } from 'react';
3
3
 
4
4
  interface SwitchRootProps {
5
- /** Controlled state. Provide `onCheckedChange` alongside it. */
5
+ /**
6
+ * Controlled state. Provide `onCheckedChange` alongside it.
7
+ *
8
+ * Never `undefined` once mounted. The mode is latched at mount, so an `undefined`
9
+ * first render makes the switch uncontrolled for good and every value passed later
10
+ * is ignored. Coalesce at the call site — `checked={x ?? false}`.
11
+ */
6
12
  checked?: boolean;
7
13
  /** Initial state when uncontrolled. Read once, at mount. */
8
14
  defaultChecked?: boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arun-dev/headless",
3
- "version": "0.2.0",
4
- "description": "Unstyled React behaviour primitives — the render engine and state plumbing @arun-dev/ui is built on. Ships no CSS and no class names.",
3
+ "version": "0.2.1",
4
+ "description": "Unstyled React behaviour primitives — render engine, controlled/uncontrolled state, and data-* state projection. Ships no CSS and no class names.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {