torch-glare 2.4.1 → 2.4.2

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.
Files changed (79) hide show
  1. package/apps/lib/components/Button.tsx +1 -1
  2. package/apps/lib/components/Card.tsx +47 -20
  3. package/apps/lib/components/ColorPicker.tsx +441 -0
  4. package/apps/lib/components/ConclusionHeader.tsx +148 -0
  5. package/apps/lib/components/DatePicker.tsx +2 -0
  6. package/apps/lib/components/Drawer.tsx +66 -24
  7. package/apps/lib/components/FormBuilder/DisplayField.tsx +40 -0
  8. package/apps/lib/components/FormBuilder/context.ts +71 -0
  9. package/apps/lib/components/FormBuilder/fields/ChoiceFields.tsx +65 -0
  10. package/apps/lib/components/FormBuilder/fields/ColorField.tsx +76 -0
  11. package/apps/lib/components/FormBuilder/fields/CustomField.tsx +16 -0
  12. package/apps/lib/components/FormBuilder/fields/DateField.tsx +34 -0
  13. package/apps/lib/components/FormBuilder/fields/FieldArray.tsx +79 -0
  14. package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +174 -0
  15. package/apps/lib/components/FormBuilder/fields/FileField.tsx +40 -0
  16. package/apps/lib/components/FormBuilder/fields/OptionListFields.tsx +139 -0
  17. package/apps/lib/components/FormBuilder/fields/OtpField.tsx +32 -0
  18. package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +82 -0
  19. package/apps/lib/components/FormBuilder/fields/RichTextEditorField.tsx +37 -0
  20. package/apps/lib/components/FormBuilder/fields/SelectField.tsx +101 -0
  21. package/apps/lib/components/FormBuilder/fields/SignatureField.tsx +175 -0
  22. package/apps/lib/components/FormBuilder/fields/SliderField.tsx +72 -0
  23. package/apps/lib/components/FormBuilder/fields/SwitchBoxField.tsx +42 -0
  24. package/apps/lib/components/FormBuilder/fields/TableField.tsx +312 -0
  25. package/apps/lib/components/FormBuilder/fields/TextField.tsx +222 -0
  26. package/apps/lib/components/FormBuilder/fields/TreeSelectField.tsx +49 -0
  27. package/apps/lib/components/FormBuilder/fields/countries.ts +303 -0
  28. package/apps/lib/components/FormBuilder/fields/index.ts +25 -0
  29. package/apps/lib/components/FormBuilder/form-builder.tsx +292 -0
  30. package/apps/lib/components/FormBuilder/header.tsx +105 -0
  31. package/apps/lib/components/FormBuilder/index.ts +37 -0
  32. package/apps/lib/components/FormBuilder/numberFormat.ts +16 -0
  33. package/apps/lib/components/FormBuilder/stepper.tsx +217 -0
  34. package/apps/lib/components/FormBuilder/submit.tsx +41 -0
  35. package/apps/lib/components/FormBuilder/types.ts +264 -0
  36. package/apps/lib/components/FormBuilder/viewFormat.tsx +137 -0
  37. package/apps/lib/components/FormRenderer/FormDrawer.tsx +121 -0
  38. package/apps/lib/components/FormRenderer/form-renderer.tsx +113 -0
  39. package/apps/lib/components/FormRenderer/index.ts +9 -0
  40. package/apps/lib/components/FormRenderer/types.ts +79 -0
  41. package/apps/lib/components/FormSummary.tsx +282 -0
  42. package/apps/lib/components/ImageAttachment.tsx +36 -61
  43. package/apps/lib/components/Label.tsx +49 -42
  44. package/apps/lib/components/RadioCard.tsx +2 -0
  45. package/apps/lib/components/SearchableSelect.tsx +9 -3
  46. package/apps/lib/components/SectionBlock.tsx +16 -8
  47. package/apps/lib/components/Select.tsx +41 -120
  48. package/apps/lib/components/TextEditor/RichTextField.tsx +46 -0
  49. package/apps/lib/components/{TextEditor.tsx → TextEditor/TextEditor.tsx} +63 -9
  50. package/apps/lib/components/TextEditor/TextEditorToolbar.tsx +429 -0
  51. package/apps/lib/components/TextEditor/editor-tools/AlignmentTune.ts +70 -0
  52. package/apps/lib/components/TextEditor/editor-tools/ColorInlineTool.ts +50 -0
  53. package/apps/lib/components/TextEditor/editor-tools/StrikethroughInlineTool.ts +48 -0
  54. package/apps/lib/components/TextEditor/editor-tools/inlineFormat.ts +98 -0
  55. package/apps/lib/{types → components/TextEditor}/editorjs.d.ts +19 -0
  56. package/apps/lib/components/TextEditor/index.ts +7 -0
  57. package/apps/lib/components/Textarea.tsx +1 -1
  58. package/apps/lib/registry.json +51 -58
  59. package/apps/lib/tsconfig.tsbuildinfo +1 -0
  60. package/apps/lib/utils/color.ts +175 -0
  61. package/docs/components/card.md +4 -2
  62. package/docs/components/chart-block-tool.md +5 -4
  63. package/docs/components/color-picker.md +101 -0
  64. package/docs/components/conclusion-header.md +80 -0
  65. package/docs/components/drawer.md +153 -102
  66. package/docs/components/form-builder.md +270 -0
  67. package/docs/components/form-renderer.md +228 -0
  68. package/docs/components/form-summary.md +123 -0
  69. package/docs/components/image-attachment.md +10 -4
  70. package/docs/components/table-dnd-wrapper.md +5 -3
  71. package/docs/components/text-editor.md +18 -0
  72. package/docs/how-to/form-and-list-recipes.md +26 -19
  73. package/docs/how-to/forms-with-form-builder.md +408 -0
  74. package/docs/how-to/guides.md +153 -179
  75. package/docs/tutorials/building-first-form.md +150 -159
  76. package/package.json +1 -1
  77. /package/apps/lib/components/{ChartBlockTool.ts → TextEditor/ChartBlockTool.ts} +0 -0
  78. /package/apps/lib/components/{TableDnDWrapper.ts → TextEditor/TableDnDWrapper.ts} +0 -0
  79. /package/apps/lib/{utils → components/TextEditor}/markdownParser.ts +0 -0
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Pure color-math helpers for the `ColorPicker` component (and anything else that
3
+ * needs to move between hex, RGB, HSV and HSL). No dependencies, no DOM — safe to
4
+ * unit-test and to import on the server.
5
+ *
6
+ * Conventions:
7
+ * - `RGBA` channels are 0–255 (`r`,`g`,`b`) and 0–1 (`a`).
8
+ * - `HSV`/`HSL` use `h` 0–360, everything else 0–100.
9
+ * - Hex output is uppercase, `#rrggbb` when fully opaque and `#rrggbbaa` otherwise.
10
+ */
11
+
12
+ export interface RGBA {
13
+ r: number;
14
+ g: number;
15
+ b: number;
16
+ a: number;
17
+ }
18
+
19
+ export interface HSV {
20
+ h: number;
21
+ s: number;
22
+ v: number;
23
+ }
24
+
25
+ export interface HSL {
26
+ h: number;
27
+ s: number;
28
+ l: number;
29
+ }
30
+
31
+ /** Clamp `n` into the inclusive `[min, max]` range. */
32
+ export function clamp(n: number, min: number, max: number): number {
33
+ return n < min ? min : n > max ? max : n;
34
+ }
35
+
36
+ /** Round to `digits` decimal places (default 0). */
37
+ export function round(n: number, digits = 0): number {
38
+ const f = 10 ** digits;
39
+ return Math.round(n * f) / f;
40
+ }
41
+
42
+ const toHex2 = (n: number) => clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0");
43
+
44
+ /**
45
+ * Parse a hex color into `RGBA`. Accepts `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`
46
+ * with or without the leading `#`. Returns `null` for anything unparseable.
47
+ */
48
+ export function parseHex(input: string): RGBA | null {
49
+ if (typeof input !== "string") return null;
50
+ let hex = input.trim().replace(/^#/, "");
51
+
52
+ // Expand shorthand (#rgb / #rgba → #rrggbb(aa)).
53
+ if (hex.length === 3 || hex.length === 4) {
54
+ hex = hex
55
+ .split("")
56
+ .map((c) => c + c)
57
+ .join("");
58
+ }
59
+
60
+ if (hex.length !== 6 && hex.length !== 8) return null;
61
+ if (!/^[0-9a-fA-F]+$/.test(hex)) return null;
62
+
63
+ const r = parseInt(hex.slice(0, 2), 16);
64
+ const g = parseInt(hex.slice(2, 4), 16);
65
+ const b = parseInt(hex.slice(4, 6), 16);
66
+ const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1;
67
+ return { r, g, b, a: round(a, 2) };
68
+ }
69
+
70
+ /** Format `RGBA` as an uppercase hex string — `#rrggbb` if opaque, else `#rrggbbaa`. */
71
+ export function formatHex({ r, g, b, a }: RGBA): string {
72
+ const base = `#${toHex2(r)}${toHex2(g)}${toHex2(b)}`.toUpperCase();
73
+ if (a >= 1) return base;
74
+ return `${base}${toHex2(clamp(a, 0, 1) * 255).toUpperCase()}`;
75
+ }
76
+
77
+ /** RGB (0–255) → HSV (h 0–360, s/v 0–100). Alpha is carried through unchanged elsewhere. */
78
+ export function rgbToHsv({ r, g, b }: { r: number; g: number; b: number }): HSV {
79
+ const rn = r / 255;
80
+ const gn = g / 255;
81
+ const bn = b / 255;
82
+ const max = Math.max(rn, gn, bn);
83
+ const min = Math.min(rn, gn, bn);
84
+ const d = max - min;
85
+
86
+ let h = 0;
87
+ if (d !== 0) {
88
+ if (max === rn) h = ((gn - bn) / d) % 6;
89
+ else if (max === gn) h = (bn - rn) / d + 2;
90
+ else h = (rn - gn) / d + 4;
91
+ h *= 60;
92
+ if (h < 0) h += 360;
93
+ }
94
+ const s = max === 0 ? 0 : d / max;
95
+ // Keep 2 decimals: HSV is the picker's internal state, so rounding to whole degrees/percents
96
+ // here would visibly shift a color that was only ever passed in (e.g. #10B981 → #11BA82).
97
+ return { h: round(h, 2), s: round(s * 100, 2), v: round(max * 100, 2) };
98
+ }
99
+
100
+ /** HSV (h 0–360, s/v 0–100) → RGB (0–255). */
101
+ export function hsvToRgb({ h, s, v }: HSV): { r: number; g: number; b: number } {
102
+ const hn = ((h % 360) + 360) % 360;
103
+ const sn = clamp(s, 0, 100) / 100;
104
+ const vn = clamp(v, 0, 100) / 100;
105
+
106
+ const c = vn * sn;
107
+ const x = c * (1 - Math.abs(((hn / 60) % 2) - 1));
108
+ const m = vn - c;
109
+
110
+ let rp = 0;
111
+ let gp = 0;
112
+ let bp = 0;
113
+ if (hn < 60) [rp, gp, bp] = [c, x, 0];
114
+ else if (hn < 120) [rp, gp, bp] = [x, c, 0];
115
+ else if (hn < 180) [rp, gp, bp] = [0, c, x];
116
+ else if (hn < 240) [rp, gp, bp] = [0, x, c];
117
+ else if (hn < 300) [rp, gp, bp] = [x, 0, c];
118
+ else [rp, gp, bp] = [c, 0, x];
119
+
120
+ return {
121
+ r: round((rp + m) * 255),
122
+ g: round((gp + m) * 255),
123
+ b: round((bp + m) * 255),
124
+ };
125
+ }
126
+
127
+ /** RGB (0–255) → HSL (h 0–360, s/l 0–100). */
128
+ export function rgbToHsl({ r, g, b }: { r: number; g: number; b: number }): HSL {
129
+ const rn = r / 255;
130
+ const gn = g / 255;
131
+ const bn = b / 255;
132
+ const max = Math.max(rn, gn, bn);
133
+ const min = Math.min(rn, gn, bn);
134
+ const d = max - min;
135
+ const l = (max + min) / 2;
136
+
137
+ let h = 0;
138
+ let s = 0;
139
+ if (d !== 0) {
140
+ s = d / (1 - Math.abs(2 * l - 1));
141
+ if (max === rn) h = ((gn - bn) / d) % 6;
142
+ else if (max === gn) h = (bn - rn) / d + 2;
143
+ else h = (rn - gn) / d + 4;
144
+ h *= 60;
145
+ if (h < 0) h += 360;
146
+ }
147
+ return { h: round(h), s: round(s * 100), l: round(l * 100) };
148
+ }
149
+
150
+ /** HSL (h 0–360, s/l 0–100) → RGB (0–255). */
151
+ export function hslToRgb({ h, s, l }: HSL): { r: number; g: number; b: number } {
152
+ const hn = ((h % 360) + 360) % 360;
153
+ const sn = clamp(s, 0, 100) / 100;
154
+ const ln = clamp(l, 0, 100) / 100;
155
+
156
+ const c = (1 - Math.abs(2 * ln - 1)) * sn;
157
+ const x = c * (1 - Math.abs(((hn / 60) % 2) - 1));
158
+ const m = ln - c / 2;
159
+
160
+ let rp = 0;
161
+ let gp = 0;
162
+ let bp = 0;
163
+ if (hn < 60) [rp, gp, bp] = [c, x, 0];
164
+ else if (hn < 120) [rp, gp, bp] = [x, c, 0];
165
+ else if (hn < 180) [rp, gp, bp] = [0, c, x];
166
+ else if (hn < 240) [rp, gp, bp] = [0, x, c];
167
+ else if (hn < 300) [rp, gp, bp] = [x, 0, c];
168
+ else [rp, gp, bp] = [c, 0, x];
169
+
170
+ return {
171
+ r: round((rp + m) * 255),
172
+ g: round((gp + m) * 255),
173
+ b: round((bp + m) * 255),
174
+ };
175
+ }
@@ -12,7 +12,7 @@ dependencies:
12
12
 
13
13
  # Card
14
14
 
15
- > A flexible container component for grouping and displaying content. Supports compound architecture with header, description, and content sections. Features hover states and polymorphic rendering.
15
+ > A flexible container component for grouping and displaying content. Supports compound architecture with header, description, and content sections, and polymorphic rendering. Static by default — opt into hover states with `variant="clickable"`.
16
16
 
17
17
  ## Installation
18
18
 
@@ -74,10 +74,11 @@ function Example() {
74
74
  ### Clickable Card
75
75
 
76
76
  ```typescript
77
+ // `variant="clickable"` gives the hover highlight + pointer cursor.
77
78
  <Card
78
79
  as="button"
80
+ variant="clickable"
79
81
  onClick={() => console.log('Card clicked')}
80
- className="cursor-pointer"
81
82
  >
82
83
  <CardHeader>Interactive Card</CardHeader>
83
84
  <CardDescription>Click me to perform an action</CardDescription>
@@ -205,6 +206,7 @@ function ImageCard() {
205
206
 
206
207
  | Prop | Type | Default | Description |
207
208
  |------|------|---------|-------------|
209
+ | `variant` | `'default' \| 'clickable'` | `'default'` | `'default'` is a static container with **no hover effect**. `'clickable'` adds the hover border highlight + pointer cursor — use it when the whole card is an interactive target. |
208
210
  | `as` | `React.ElementType` | `'section'` | Element type to render as |
209
211
  | `asChild` | `boolean` | `false` | Merge props onto child element |
210
212
  | `htmlFor` | `string` | - | For label association when as="label" |
@@ -20,15 +20,16 @@ TORCH Glare is a copy-in library: the CLI copies this module's source into your
20
20
 
21
21
  ```bash
22
22
  npx torch-glare@latest init
23
- npx torch-glare@latest add ChartBlockTool
23
+ npx torch-glare@latest add TextEditor
24
24
  ```
25
25
 
26
+ `ChartBlockTool` ships as part of the `TextEditor` folder, so `add TextEditor` copies it in.
26
27
  It depends on the `chart.js` npm package (installed automatically by the CLI).
27
28
 
28
29
  ## Import
29
30
 
30
31
  ```typescript
31
- import ChartBlockTool, { ChartBlockData } from "@/components/ChartBlockTool";
32
+ import { ChartBlockTool, type ChartBlockData } from "@/components/TextEditor";
32
33
  ```
33
34
 
34
35
  ## Quick Examples
@@ -37,7 +38,7 @@ import ChartBlockTool, { ChartBlockData } from "@/components/ChartBlockTool";
37
38
 
38
39
  ```typescript
39
40
  import EditorJS from "@editorjs/editorjs";
40
- import ChartBlockTool from "@/components/ChartBlockTool";
41
+ import { ChartBlockTool } from "@/components/TextEditor";
41
42
 
42
43
  const editor = new EditorJS({
43
44
  holder: "editor",
@@ -56,7 +57,7 @@ editors. The block persists as `ChartBlockData` in the editor's saved output.
56
57
  ### The saved data shape
57
58
 
58
59
  ```typescript
59
- import { ChartBlockData } from "@/components/ChartBlockTool";
60
+ import type { ChartBlockData } from "@/components/TextEditor";
60
61
 
61
62
  const block: ChartBlockData = {
62
63
  chartType: "bar",
@@ -0,0 +1,101 @@
1
+ ---
2
+ title: ColorPicker
3
+ description: A full Figma-style color picker with a saturation/value area, hue and opacity sliders, an eyedropper, switchable HEX/RGB/HSL inputs, and preset swatches
4
+ group: Forms
5
+ keywords: [color-picker, color, palette, hex, rgb, hsl, hsv, opacity, alpha, eyedropper, swatch]
6
+ ---
7
+
8
+ # ColorPicker
9
+
10
+ > A full Figma-style color palette opened from **a trigger you supply**: a draggable
11
+ > saturation/value area, a hue slider, an optional opacity slider, an eyedropper (where the
12
+ > browser supports it), switchable **HEX / RGB / HSL** numeric inputs, and preset swatches.
13
+ > The value is always a hex string.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npx torch-glare@latest init
19
+ npx torch-glare@latest add ColorPicker
20
+ ```
21
+
22
+ `add` also copies its dependencies — `utils/color` (the pure hex/RGB/HSV/HSL math), plus the
23
+ `Popover` and `Input` components.
24
+
25
+ ## Import
26
+
27
+ ```tsx
28
+ import { ColorPicker } from "@/components/ColorPicker";
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ColorPicker renders **no trigger of its own** — you pass one as its child (rendered via `asChild`,
34
+ so it must forward ref/props to a DOM node). The current color is handed to that child based on
35
+ what it is:
36
+
37
+ | Child | Receives |
38
+ |---|---|
39
+ | an **input** — native `<input>`/`<textarea>`, or any element already given a `value` prop | the hex as **`value`** (plus an `onChange` that commits a typed hex, if you didn't supply one) |
40
+ | **anything else** | the hex as **`children`** — unless it already has children, which are left untouched |
41
+
42
+ ```tsx
43
+ const [color, setColor] = useState("#3B82F6");
44
+
45
+ // input child → gets `value={color}`
46
+ <ColorPicker value={color} onChange={setColor}>
47
+ <input className="…" />
48
+ </ColorPicker>
49
+
50
+ // non-input child → gets the hex as its content
51
+ <ColorPicker value={color} onChange={setColor}>
52
+ <Button variant="BorderStyle" />
53
+ </ColorPicker>
54
+
55
+ // a child with its own content keeps it — render the value yourself
56
+ <ColorPicker value={color} onChange={setColor}>
57
+ <button type="button">
58
+ <span style={{ backgroundColor: color }} />
59
+ {color}
60
+ </button>
61
+ </ColorPicker>
62
+ ```
63
+
64
+ With presets and opacity disabled (always emits `#rrggbb`):
65
+
66
+ ```tsx
67
+ <ColorPicker
68
+ value={color}
69
+ onChange={setColor}
70
+ alpha={false}
71
+ presets={["#005ECC", "#047854", "#E30C30", "#F5A623"]}
72
+ >
73
+ <Button variant="BorderStyle" />
74
+ </ColorPicker>
75
+ ```
76
+
77
+ ## Props
78
+
79
+ | Prop | Type | Default | Description |
80
+ | ----------- | -------------------------- | ------------- | --------------------------------------------------------------------------- |
81
+ | `children` | `ReactElement` | — (required) | The trigger. Gets the hex as `value` if it's an input, else as `children`. |
82
+ | `value` | `string` | `"#000000"` | Current color as a hex string (`#rrggbb` or `#rrggbbaa`). |
83
+ | `onChange` | `(hex: string) => void` | — | Called on every change. Emits `#rrggbbaa` only when opacity < 100%. |
84
+ | `presets` | `string[]` | — | Quick-pick swatches shown at the bottom of the panel. |
85
+ | `alpha` | `boolean` | `true` | Show the opacity slider + input and allow 8-digit output. |
86
+ | `disabled` | `boolean` | `false` | Disable the trigger and all controls. |
87
+ | `theme` | `"dark" \| "light" \| "default"` | — | Applied as `data-theme`. |
88
+
89
+ ## Value format
90
+
91
+ The value stays `#rrggbb` while fully opaque (backward-compatible with plain hex fields) and is
92
+ promoted to `#rrggbbaa` only when opacity drops below 100%. Feeding a 3-, 6-, or 8-digit hex
93
+ (with or without `#`) back in via `value` is always accepted.
94
+
95
+ ## Notes
96
+
97
+ - The **eyedropper** button appears only where the native [`EyeDropper` API](https://developer.mozilla.org/docs/Web/API/EyeDropper)
98
+ is available (Chromium/Edge); it is hidden on Firefox/Safari.
99
+ - Used by `FormBuilder.Color` — see [form-builder.md](./form-builder.md).
100
+ - Color math lives in `utils/color` (`parseHex`, `formatHex`, `rgbToHsv`, `hsvToRgb`,
101
+ `rgbToHsl`, `hslToRgb`) and is reusable on its own.
@@ -0,0 +1,80 @@
1
+ ---
2
+ title: ConclusionHeader
3
+ description: A clickable section header with a chevron toggle — white-alpha hover surfaces, chevron down when open and sideways when closed, and a "Disabled" badge when disabled. Direction-aware (LTR/RTL). Used for each FormSummary group title.
4
+ component: true
5
+ group: Layout & Containers
6
+ keywords: [conclusion-header, collapsible, header, disclosure, accordion, toggle, chevron, expand, collapse, section]
7
+ ---
8
+
9
+ # ConclusionHeader
10
+
11
+ A clickable **section header** that toggles a collapsible region, per the
12
+ *Conclusion-Header-2.0* design. Its surfaces are **button overlays** (not an input field): at
13
+ rest the label stands alone, and on hover it gains a `Button/ContStyle-Hover` pill while the
14
+ chevron gains a `Button/Secondary` box. The chevron is hidden at rest while open, shows
15
+ **sideways** when closed (left in LTR, right in RTL), and flips to **down** on hover. When
16
+ `disabled` the label greys out, truncates at 120px, and a gray-subtle `Badge` replaces the
17
+ chevron.
18
+
19
+ It renders only the header — you own the collapsible body. It works **uncontrolled**
20
+ (manages its own open state) or **controlled** (pass `open` + `onOpenChange`):
21
+
22
+ ```tsx
23
+ // Uncontrolled — reflect the state with onOpenChange
24
+ const [open, setOpen] = useState(true)
25
+
26
+ <ConclusionHeader label="Invoice" defaultOpen onOpenChange={setOpen} />
27
+ {open && <section>…body…</section>}
28
+
29
+ // Controlled — you drive `open`
30
+ <ConclusionHeader label="Invoice" open={open} onOpenChange={setOpen} />
31
+ ```
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ npx torch-glare@latest init
37
+ npx torch-glare@latest add ConclusionHeader
38
+ ```
39
+
40
+ ## Imports
41
+
42
+ ```tsx
43
+ import { ConclusionHeader } from '@/components/ConclusionHeader'
44
+ ```
45
+
46
+ ## Props
47
+
48
+ | Prop | Type | Default | Notes |
49
+ |---|---|---|---|
50
+ | `label` | `ReactNode` | — | The header text. |
51
+ | `open` | `boolean` | — | **Controlled** open state. Omit to let the header manage its own. |
52
+ | `defaultOpen` | `boolean` | `true` | Initial open state when **uncontrolled**. |
53
+ | `onOpenChange` | `(open: boolean) => void` | — | Called with the next open state on click. |
54
+ | `disabled` | `boolean` | `false` | Greys the label and shows a `Badge` instead of the chevron. |
55
+ | `disabledLabel` | `string` | `"Disabled"` | Badge text when `disabled`. |
56
+ | `theme` | `'dark' \| 'light' \| 'default'` | — | Theme override; otherwise inherits the nearest `data-theme`. |
57
+
58
+ Extra props spread to the underlying `<button>`, which is what the header renders — so it is
59
+ focusable, toggles on <kbd>Enter</kbd>/<kbd>Space</kbd>, and exposes `aria-expanded` for
60
+ free. A `ref` is forwarded to that `HTMLButtonElement`.
61
+
62
+ ## Behaviour
63
+
64
+ - **Open** (rest): label only — no surface, no chevron.
65
+ - **Open hover**: `Button/ContStyle-Hover` pill (label inset 4px) + `Button/Secondary`
66
+ chevron box with a **down** chevron.
67
+ - **Closed** (rest): label + a bare **sideways** chevron (`<` LTR / `>` RTL), no box.
68
+ - **Closed hover**: same surfaces as open hover; the chevron flips to down.
69
+ - **Disabled**: greyed label truncated at 120px + a `● Disabled` badge sitting beside it
70
+ (not at the row's end); not clickable.
71
+
72
+ Direction is inherited from the ambient `dir` — the design's RTL variants are the LTR ones
73
+ mirrored, which the component's logical properties produce automatically.
74
+
75
+ ## Composition
76
+
77
+ Uses the design-system tokens directly (`Button/Secondary`, `Button/ContStyle-Hover`) rather
78
+ than the input/button components, and reuses [Badge](./badge.md) for the disabled state.
79
+ [FormSummary](./form-summary.md) uses it as each **group** title, so every group of totals
80
+ collapses independently.