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
@@ -27,13 +27,15 @@ TORCH Glare is a copy-in library: the CLI copies this module's source into your
27
27
 
28
28
  ```bash
29
29
  npx torch-glare@latest init
30
- npx torch-glare@latest add TableDnDWrapper
30
+ npx torch-glare@latest add TextEditor
31
31
  ```
32
32
 
33
+ `createTableDnDClass` ships as part of the `TextEditor` folder, so `add TextEditor` copies it in.
34
+
33
35
  ## Import
34
36
 
35
37
  ```typescript
36
- import createTableDnDClass from "@/components/TableDnDWrapper";
38
+ import { createTableDnDClass } from "@/components/TextEditor";
37
39
  ```
38
40
 
39
41
  ## Quick Examples
@@ -43,7 +45,7 @@ import createTableDnDClass from "@/components/TableDnDWrapper";
43
45
  ```typescript
44
46
  import EditorJS from "@editorjs/editorjs";
45
47
  import Table from "@editorjs/table";
46
- import createTableDnDClass from "@/components/TableDnDWrapper";
48
+ import { createTableDnDClass } from "@/components/TextEditor";
47
49
 
48
50
  // Wrap the original table tool to add row/column drag-and-drop.
49
51
  const TableWithDnD = createTableDnDClass(Table);
@@ -260,8 +260,26 @@ function MarkdownEditor() {
260
260
  | `tools` | `Record<string, any>` | Default 18+ tools | Override the Editor.js tools configuration |
261
261
  | `minHeight` | `number` | - | Custom min-height in pixels (overrides size) |
262
262
  | `disabled` | `boolean` | `false` | Disables interaction with reduced opacity |
263
+ | `toolbar` | `boolean` | `true` | Show the fixed formatting toolbar. Auto-hidden when `readOnly`/`disabled`. |
263
264
  | `className` | `string` | - | Additional CSS classes |
264
265
 
266
+ ### Formatting toolbar
267
+
268
+ A fixed, sticky toolbar sits at the top of the editor (shown by default; pass `toolbar={false}`
269
+ to hide it, and it is automatically hidden in `readOnly`/`disabled` editors). It drives the
270
+ underlying Editor.js instance and offers:
271
+
272
+ - **History** — undo / redo (via `editorjs-undo`; also bound to `Ctrl/⌘+Z` / `Ctrl/⌘+Y`).
273
+ - **Block type** — a dropdown to switch between Normal text and Heading 1–3 (`blocks.convert`).
274
+ - **Alignment** — left / center / right (a block tune persisted on each block).
275
+ - **Text color** — a swatch palette.
276
+ - **Inline** — bold, italic, underline, strikethrough, clear formatting.
277
+ - **Lists** — bullet and ordered.
278
+ - **Insert image** — from a local file (inserted as an `image` block).
279
+
280
+ Strikethrough and color are backed by small custom inline tools whose `sanitize` allowlists let
281
+ that markup survive `save()`; alignment persists as a block tune.
282
+
265
283
  ### Ref Methods (TextEditorRef)
266
284
 
267
285
  | Method | Signature | Description |
@@ -10,6 +10,13 @@ Battle-tested layouts and component combinations from production frontend work.
10
10
  Use these as the starting point for create dialogs, list/filter pages,
11
11
  line-item grids, and login flows.
12
12
 
13
+ > **For actual forms, reach for [`FormBuilder`](../components/form-builder.md) first.** The
14
+ > multi-column form rows below are a layout escape hatch for the rare case that needs bespoke
15
+ > positioning — they are **not** the default way to build a form. A normal create/edit form is
16
+ > `FormBuilder` fields as JSX children with a resolver; see the
17
+ > [Forms with FormBuilder](./forms-with-form-builder.md) guide. Don't hand-wire `FormField` /
18
+ > `InputField` rows or hold field state in `useState` when `FormBuilder` would do it.
19
+
13
20
  > All examples use the `presentation` color tokens. Never substitute
14
21
  > `*-system-*` tokens or `variant="SystemStyle"` — see the rules banner at
15
22
  > the top of every doc response.
@@ -76,7 +83,9 @@ the same filter bar — a search input + one or more select filters above a
76
83
  data table.
77
84
 
78
85
  ```tsx
79
- {/* Filter bar */}
86
+ {
87
+ /* Filter bar */
88
+ }
80
89
  <div className="flex items-center gap-3">
81
90
  <div className="flex-1 max-w-sm">
82
91
  <InputField
@@ -99,9 +108,11 @@ data table.
99
108
  ]}
100
109
  placeholder="All Statuses"
101
110
  />
102
- </div>
111
+ </div>;
103
112
 
104
- {/* Data table */}
113
+ {
114
+ /* Data table */
115
+ }
105
116
  <Card className="p-0 overflow-hidden">
106
117
  <CardContent className="p-0 overflow-x-auto">
107
118
  <Table className="w-full">
@@ -122,9 +133,7 @@ data table.
122
133
  >
123
134
  <i className="ri-inbox-line text-4xl opacity-60" />
124
135
  <p className="typography-body-medium-regular">No vouchers</p>
125
- <p className="typography-body-small-regular opacity-80">
126
- Try adjusting your filters
127
- </p>
136
+ <p className="typography-body-small-regular opacity-80">Try adjusting your filters</p>
128
137
  </TableCell>
129
138
  </TableRow>
130
139
  ) : (
@@ -144,7 +153,7 @@ data table.
144
153
  </TableBody>
145
154
  </Table>
146
155
  </CardContent>
147
- </Card>
156
+ </Card>;
148
157
  ```
149
158
 
150
159
  Key rules:
@@ -263,15 +272,15 @@ hierarchy. Adding a relevant icon makes dialogs instantly recognizable.
263
272
 
264
273
  Pick an icon that matches the entity:
265
274
 
266
- | Entity | Icon |
267
- |---|---|
268
- | Account | `ri-bank-line` |
269
- | Fiscal period / year | `ri-calendar-line` |
270
- | Voucher / journal | `ri-receipt-line` |
271
- | Number series | `ri-hashtag` |
272
- | Posting rule | `ri-settings-3-line` |
273
- | Exchange rate | `ri-exchange-line` |
274
- | Bank / cash account | `ri-bank-card-line` |
275
+ | Entity | Icon |
276
+ | -------------------- | -------------------- |
277
+ | Account | `ri-bank-line` |
278
+ | Fiscal period / year | `ri-calendar-line` |
279
+ | Voucher / journal | `ri-receipt-line` |
280
+ | Number series | `ri-hashtag` |
281
+ | Posting rule | `ri-settings-3-line` |
282
+ | Exchange rate | `ri-exchange-line` |
283
+ | Bank / cash account | `ri-bank-card-line` |
275
284
 
276
285
  ---
277
286
 
@@ -297,9 +306,7 @@ export function LoginPage() {
297
306
  "to-background-presentation-state-information/10",
298
307
  ].join(" ")}
299
308
  >
300
- <h1 className="typography-headers-large-medium">
301
- Welcome to Torch Finance
302
- </h1>
309
+ <h1 className="typography-headers-large-medium">Welcome to Torch Finance</h1>
303
310
  <ul className="flex flex-col gap-3">
304
311
  {features.map((f) => (
305
312
  <li key={f.title} className="flex items-start gap-3">
@@ -0,0 +1,408 @@
1
+ ---
2
+ title: Forms with FormBuilder, FormRenderer & FormSummary
3
+ description: The canonical way to build forms in TORCH Glare. Author fields as JSX with FormBuilder, add page/drawer chrome with FormRenderer, and show live computed totals with FormSummary. Covers single, stepper, drawer, edit/view, and a full invoice example combining all three.
4
+ keywords:
5
+ [
6
+ form,
7
+ forms,
8
+ formbuilder,
9
+ formrenderer,
10
+ formsummary,
11
+ form-builder,
12
+ form-renderer,
13
+ form-summary,
14
+ validation,
15
+ zod,
16
+ resolver,
17
+ stepper,
18
+ drawer,
19
+ totals,
20
+ invoice,
21
+ calculation,
22
+ react-hook-form,
23
+ ]
24
+ ---
25
+
26
+ # Forms with FormBuilder, FormRenderer & FormSummary
27
+
28
+ **This is the way to build forms in TORCH Glare.** Do not hand-wire
29
+ `FormField` → `FormItem` → `FormControl` → `InputField` rows, and never track field state
30
+ with `useState` — that boilerplate is exactly what `FormBuilder` exists to remove.
31
+
32
+ Three components, layered:
33
+
34
+ | Component | Use it for | When |
35
+ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
36
+ | **`FormBuilder`** | The form itself — fields authored as JSX children. Owns react-hook-form, validation, edit/view, sections, steppers. | Always. This is the base. |
37
+ | **`FormRenderer`** | Wraps `FormBuilder` to add **chrome**: page-vs-drawer display, the title header, an `actions` slot for the Save, and a `summary` slot. | Real forms — prefer it over raw `FormBuilder`. |
38
+ | **`FormSummary`** | A read-only **calculation panel beside the form** — totals that recompute live as the user types. | Invoices, orders, anything with a "conclusion". |
39
+
40
+ Validation is **resolver-agnostic**: pass any react-hook-form resolver
41
+ (`zodResolver(schema)` is typical). The library never depends on zod.
42
+
43
+ ---
44
+
45
+ ## 1. A single-page form
46
+
47
+ Each `FormBuilder.*` field is one JSX child taking at least a `name`, plus `label`,
48
+ `placeholder`, `required`, `disabled`, `hidden`, `fullWidth`. Wrap them in `FormRenderer`,
49
+ give it a `header` for the title bar, and pass the Save via `actions`.
50
+
51
+ ```tsx
52
+ "use client";
53
+
54
+ import { z } from "zod";
55
+ import { zodResolver } from "@hookform/resolvers/zod";
56
+ import { FormBuilder } from "@/components/FormBuilder";
57
+ import { FormRenderer } from "@/components/FormRenderer";
58
+
59
+ const schema = z.object({
60
+ name: z.string().min(1, "Name is required"),
61
+ description: z.string(),
62
+ category: z.string().min(1, "Pick a category"),
63
+ price: z.number().positive("Must be positive").optional(),
64
+ active: z.boolean(),
65
+ agree: z.boolean().refine((v) => v === true, "You must agree to continue"),
66
+ });
67
+ type Values = z.infer<typeof schema>;
68
+
69
+ const DEFAULTS: Values = {
70
+ name: "",
71
+ description: "",
72
+ category: "",
73
+ price: undefined,
74
+ active: true,
75
+ agree: false,
76
+ };
77
+
78
+ const CATEGORY = [
79
+ { label: "Hardware", value: "hardware" },
80
+ { label: "Software", value: "software" },
81
+ ];
82
+
83
+ export function ItemForm({
84
+ onSave,
85
+ saving,
86
+ }: {
87
+ onSave: (v: Values) => Promise<void>;
88
+ saving?: boolean;
89
+ }) {
90
+ return (
91
+ <FormRenderer<Values>
92
+ onSubmit={onSave}
93
+ loading={saving}
94
+ resolver={zodResolver(schema)}
95
+ defaultValues={DEFAULTS}
96
+ header={{ title: "New item", variant: "new" }}
97
+ actions={<FormBuilder.Submit>Save</FormBuilder.Submit>}
98
+ >
99
+ <FormBuilder.Section title="Identity" color="Blue">
100
+ <FormBuilder.Text name="name" label="Name" required placeholder="e.g. Acme Widget" />
101
+ <FormBuilder.Textarea name="description" label="Description" fullWidth />
102
+ </FormBuilder.Section>
103
+
104
+ <FormBuilder.Section title="Classification" color="Red">
105
+ <FormBuilder.Select name="category" label="Category" required options={CATEGORY} />
106
+ <FormBuilder.Currency name="price" label="Base price" currencySymbol="$" />
107
+ </FormBuilder.Section>
108
+
109
+ <FormBuilder.Section title="Settings" color="Purple">
110
+ <FormBuilder.SwitchBox name="active" label="Active" subLabel="Enabled" />
111
+ <FormBuilder.Checkbox name="agree" label="I agree to the terms" required />
112
+ </FormBuilder.Section>
113
+ </FormRenderer>
114
+ );
115
+ }
116
+ ```
117
+
118
+ `FormBuilder.Section` groups fields in a `SectionBlock` (`color` is one of `Blue`, `Yellow`,
119
+ `Green`, `Red`, `Orange`, `Purple`, `Pink`, `Gray`). Pass the Save via `actions` — a
120
+ `FormBuilder.Submit`, which is loading-aware and auto-hides in view mode; it renders in the
121
+ header action pill. (With raw `FormBuilder`, put the same `FormBuilder.Submit` in a
122
+ `FormBuilder.Header`.)
123
+
124
+ ### Field types
125
+
126
+ Every field, its underlying control, and the value your `onSubmit` receives:
127
+
128
+ | Component | Value |
129
+ | ---------------------------------------------------------------- | -------------------------------------------------------------- |
130
+ | `.Text` / `.Email` / `.Password` (`strengthMeter`) | `string` |
131
+ | `.Number` | `number` |
132
+ | `.Currency` (`currencySymbol`) | `number` |
133
+ | `.Textarea` | `string` |
134
+ | `.Select` / `.SearchableSelect` (`options`) | `string` |
135
+ | `.MultiSelect` / `.Tags` (`options`) | `string[]` |
136
+ | `.RadioList` (`options`, optional per-option `description`) | `string` |
137
+ | `.CheckboxGroup` (`options`) | `string[]` |
138
+ | `.RadioCards` (`options` with `description`) | `string` |
139
+ | `.Checkbox` (`subLabel`) | `boolean` |
140
+ | `.SwitchBox` (`subLabel`) | `boolean` |
141
+ | `.Otp` (`length`) | `string` |
142
+ | `.Slider` (`min`, `max`, `step`, `range`, `suffix`) | `number` (or `[number, number]` with `range`) |
143
+ | `.Color` (`presets`, `alpha`) | hex `string` |
144
+ | `.Phone` (`defaultCountry`, defaults to `+964`) | `string` (`"+<dial> <number>"`) |
145
+ | `.Date` | `Date` |
146
+ | `.DateRange` | `{ from, to }` |
147
+ | `.DateMultiple` | `Date[]` |
148
+ | `.DateTime` | `Date` |
149
+ | `.TreeSelect` (`nodes`, `getNodeId`, `getNodeLabel`) | node id (`string`) |
150
+ | `.File` / `.Image` (`accept`, `multiple`) | `File \| File[]` |
151
+ | `.RichText` | EditorJS `OutputData` |
152
+ | `.Signature` (`penColor`) | PNG data-URL `string` |
153
+ | `.FieldArray` (`children` render fn, `defaultItem`) | `object[]` |
154
+ | `.Table` (`columns`, `selectable`, `reorderable`, `defaultItem`) | `object[]` — editable grid; top-level child (not in a Section) |
155
+ | `.Custom` (`render`, `formatView`) | anything |
156
+
157
+ See the [FormBuilder](../components/form-builder.md) doc for the full prop tables.
158
+
159
+ ---
160
+
161
+ ## 2. Title header + action bar
162
+
163
+ `FormRenderer`'s `header` prop renders a title pill on the left: `header={{ title: 'Acme Widget
164
+ Pro', variant: 'edit' }}` (variants: `new`, `edit`, `detail`). The **action pill** on the right is
165
+ whatever you pass to `actions` — put the Save there:
166
+ `actions={<FormBuilder.Submit>Save invoice</FormBuilder.Submit>}`. A bare `FormBuilder.Submit`
167
+ auto-targets the form (via a form-id context), so it submits even though the header renders
168
+ _outside_ the `<form>`.
169
+
170
+ If you use raw `FormBuilder`, the same bar is `FormBuilder.Header` with a `FormBuilder.Submit`
171
+ child.
172
+
173
+ ---
174
+
175
+ ## 3. Stepper (steps are components)
176
+
177
+ Every step's fields stay **mounted and registered** — the whole form is live regardless of
178
+ which step shows; the stepper only toggles visibility. **Navigation is the step buttons
179
+ themselves**: backward is free, clicking forward validates the steps in between and stops at
180
+ the first one with errors. The Save is the header `actions` — it submits every step's fields at
181
+ once, from any step.
182
+
183
+ ```tsx
184
+ <FormRenderer<Values>
185
+ onSubmit={save}
186
+ resolver={zodResolver(schema)}
187
+ defaultValues={DEFAULTS}
188
+ header={{ title: "New item", variant: "new" }}
189
+ actions={<FormBuilder.Submit>Save</FormBuilder.Submit>}
190
+ >
191
+ <FormBuilder.Stepper>
192
+ <FormBuilder.Step title="Identity">
193
+ <FormBuilder.Section title="Identity" color="Blue">
194
+ <FormBuilder.Text name="name" label="Name" required />
195
+ </FormBuilder.Section>
196
+ </FormBuilder.Step>
197
+
198
+ <FormBuilder.Step title="Classification">
199
+ <FormBuilder.Section title="Classification" color="Red">
200
+ <FormBuilder.Select name="category" label="Category" required options={CATEGORY} />
201
+ </FormBuilder.Section>
202
+ </FormBuilder.Step>
203
+ </FormBuilder.Stepper>
204
+ </FormRenderer>
205
+ ```
206
+
207
+ ---
208
+
209
+ ## 4. Edit and read-only view — one markup, both modes
210
+
211
+ - **Edit**: pass `values` (not just `defaultValues`) — the form repopulates when the data
212
+ loads. Add a remount `key` so initial-only inputs (date, rich text) re-seed.
213
+ - **View**: `mode="view"` renders the same children read-only, with no Submit.
214
+
215
+ ```tsx
216
+ <FormRenderer<Values>
217
+ key={record?.id ?? "new"}
218
+ mode={mode} // 'edit' | 'view'
219
+ values={record} // arrives async
220
+ onSubmit={save}
221
+ resolver={zodResolver(schema)}
222
+ defaultValues={DEFAULTS}
223
+ header={{ title: record?.name ?? "New", variant: mode === "view" ? "detail" : "edit" }}
224
+ >
225
+ …the same fields…
226
+ </FormRenderer>
227
+ ```
228
+
229
+ ---
230
+
231
+ ## 5. A form in a drawer
232
+
233
+ Set `display="drawer"` and drive it with `open` / `onOpenChange`. Pass the Save via `actions` —
234
+ it renders in the drawer header, with no manual `id` / `form={id}` wiring:
235
+
236
+ ```tsx
237
+ <FormRenderer<Values>
238
+ display="drawer"
239
+ open={open}
240
+ onOpenChange={setOpen}
241
+ header={{ title: "New item", label: "New", variant: "new" }}
242
+ onSubmit={save}
243
+ resolver={zodResolver(schema)}
244
+ defaultValues={DEFAULTS}
245
+ actions={<FormBuilder.Submit>Save</FormBuilder.Submit>}
246
+ >
247
+ <FormBuilder.Section title="Identity" color="Blue">
248
+
249
+ </FormBuilder.Section>
250
+ </FormRenderer>
251
+ ```
252
+
253
+ Inside a drawer the fields default to a vertical (narrow) layout automatically.
254
+
255
+ ---
256
+
257
+ ## 6. The conclusion panel — live totals beside the form
258
+
259
+ `FormSummary` is a read-only panel of computed rows. Each `FormSummary.Row` declares a
260
+ **`compute(values)`** that runs against the **live** form values, so totals recalculate as the
261
+ user types. Pass it to `FormRenderer`'s `summary` prop.
262
+
263
+ Both the form and the panel must read the same values, so **hoist `useForm`** and hand the
264
+ instance to `FormRenderer` **and** `FormSummary` via their `form` prop:
265
+
266
+ ```tsx
267
+ "use client";
268
+
269
+ import { useForm } from "react-hook-form";
270
+ import { zodResolver } from "@hookform/resolvers/zod";
271
+ import { FormBuilder } from "@/components/FormBuilder";
272
+ import { FormRenderer } from "@/components/FormRenderer";
273
+ import { FormSummary } from "@/components/FormSummary";
274
+
275
+ // The calculations — plain functions of the form values. Keep them out of the JSX.
276
+ const subTotal = (v: Invoice) => v.items.reduce((s, i) => s + (i.qty ?? 0) * (i.price ?? 0), 0);
277
+ const totalDiscount = (v: Invoice) => v.items.reduce((s, i) => s + (i.discount ?? 0), 0);
278
+ const taxable = (v: Invoice) => Math.max(0, subTotal(v) - totalDiscount(v));
279
+ const totalTax = (v: Invoice) => taxable(v) * ((v.taxRate ?? 0) / 100);
280
+ const overallTotal = (v: Invoice) => taxable(v) + totalTax(v);
281
+
282
+ export function InvoiceForm({ save }: { save: (v: Invoice) => Promise<void> }) {
283
+ // Hoisted so both the form and the summary beside it share the same values.
284
+ const form = useForm<Invoice>({ resolver: zodResolver(schema), defaultValues: DEFAULTS });
285
+
286
+ return (
287
+ <FormRenderer<Invoice>
288
+ form={form}
289
+ onSubmit={save}
290
+ fieldDirection="vertical"
291
+ header={{ title: "Invoice", variant: "new" }}
292
+ actions={<FormBuilder.Submit>Save invoice</FormBuilder.Submit>}
293
+ summary={
294
+ <FormSummary form={form} title="Invoice" subtitle="Summary">
295
+ <FormSummary.Group title="Total">
296
+ <FormSummary.Row label="Total Discount" compute={totalDiscount} />
297
+ <FormSummary.Row label="Overall Total" emphasized compute={overallTotal} />
298
+ <FormSummary.Row
299
+ label="Overall Total"
300
+ currency="IQD"
301
+ tone="success"
302
+ decimals={0}
303
+ compute={(v) => overallTotal(v) * (v.iqdRate ?? 0)}
304
+ />
305
+ <FormSummary.Row
306
+ label="Overall Total"
307
+ currency="USD"
308
+ tone="info"
309
+ compute={overallTotal}
310
+ />
311
+ </FormSummary.Group>
312
+
313
+ <FormSummary.Group title="Tax">
314
+ <FormSummary.Row label="Sub Total" compute={subTotal} />
315
+ <FormSummary.Row label="Total Tax" compute={totalTax} />
316
+ </FormSummary.Group>
317
+ </FormSummary>
318
+ }
319
+ >
320
+ <FormBuilder.Section title="Line items" color="Green">
321
+ <FormBuilder.FieldArray
322
+ name="items"
323
+ label="Items"
324
+ addLabel="Add item"
325
+ defaultItem={{ name: "", qty: 1, price: 0, discount: 0 }}
326
+ >
327
+ {(rowName) => (
328
+ <>
329
+ <FormBuilder.Text name={`${rowName}.name`} label="Item" />
330
+ <FormBuilder.Number name={`${rowName}.qty`} label="Qty" />
331
+ <FormBuilder.Currency name={`${rowName}.price`} label="Price" currencySymbol="$" />
332
+ <FormBuilder.Currency
333
+ name={`${rowName}.discount`}
334
+ label="Discount"
335
+ currencySymbol="$"
336
+ />
337
+ </>
338
+ )}
339
+ </FormBuilder.FieldArray>
340
+ </FormBuilder.Section>
341
+
342
+ <FormBuilder.Section title="Rates" color="Purple">
343
+ <FormBuilder.Number name="taxRate" label="Tax rate (%)" />
344
+ <FormBuilder.Number name="iqdRate" label="USD → IQD rate" />
345
+ </FormBuilder.Section>
346
+ </FormRenderer>
347
+ );
348
+ }
349
+ ```
350
+
351
+ Row options: `emphasized` (the primary result), `currency` + `tone`
352
+ (`'neutral' | 'success' | 'info'`), `decimals` (default `2` → `0.00`), `action` (a trailing
353
+ button), `format` (override the number formatting), `value` (a static row).
354
+
355
+ The panel is **read-only** — it contributes nothing to the submitted values. On a page it sits
356
+ beside the form (a `summary` **plus** a `FormBuilder.Stepper` becomes three columns: nav ·
357
+ fields · summary).
358
+
359
+ ---
360
+
361
+ ## 7. Conclusion panel + drawer
362
+
363
+ The same `summary` prop works with `display="drawer"` — `FormRenderer` moves the panel into the
364
+ drawer's tray, beside the form:
365
+
366
+ ```tsx
367
+ <FormRenderer<Invoice>
368
+ display="drawer"
369
+ open={open}
370
+ onOpenChange={setOpen}
371
+ form={form}
372
+ onSubmit={save}
373
+ header={{ title: "New item", badge: "New", variant: "new" }}
374
+ summary={
375
+ <FormSummary form={form} title="Item" subtitle="Summary">
376
+ <FormSummary.Group title="Pricing">
377
+ <FormSummary.Row label="Base price" compute={basePrice} />
378
+ <FormSummary.Row label="Overall Total" emphasized compute={overallTotal} />
379
+ </FormSummary.Group>
380
+ </FormSummary>
381
+ }
382
+ >
383
+ <FormBuilder.Section title="Identity" color="Blue">
384
+
385
+ </FormBuilder.Section>
386
+ </FormRenderer>
387
+ ```
388
+
389
+ ---
390
+
391
+ ## Gotchas
392
+
393
+ - **Hoisting `useForm` disables the remount-`key` reset.** Once the form instance lives in
394
+ your component, a changing `key` no longer resets it. Call `form.reset(DEFAULTS)` instead
395
+ (e.g. when opening a drawer).
396
+ - **`FormSummary` needs the same hoisted `form`** — that's the react-hook-form context it reads
397
+ live values from. Pass the identical instance to `FormRenderer` and `FormSummary`.
398
+ - **Number/currency values are real numbers**, not strings — `onSubmit` receives
399
+ `price: 1299`, not `"1299"`.
400
+ - **`required` is cosmetic.** It renders the "(Required)" tag; actual enforcement is your
401
+ resolver — keep the two in sync.
402
+ - **Never use `variant="SystemStyle"` or `*-system-*` tokens** in app code.
403
+
404
+ ## Related
405
+
406
+ - [FormBuilder](../components/form-builder.md) — every field type and its value shape
407
+ - [FormRenderer](../components/form-renderer.md) — display, header, `actions`, `summary`
408
+ - [FormSummary](../components/form-summary.md) — the calculation panel