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.
- package/apps/lib/components/Button.tsx +1 -1
- package/apps/lib/components/Card.tsx +47 -20
- package/apps/lib/components/ColorPicker.tsx +441 -0
- package/apps/lib/components/ConclusionHeader.tsx +148 -0
- package/apps/lib/components/DatePicker.tsx +2 -0
- package/apps/lib/components/Drawer.tsx +66 -24
- package/apps/lib/components/FormBuilder/DisplayField.tsx +40 -0
- package/apps/lib/components/FormBuilder/context.ts +71 -0
- package/apps/lib/components/FormBuilder/fields/ChoiceFields.tsx +65 -0
- package/apps/lib/components/FormBuilder/fields/ColorField.tsx +76 -0
- package/apps/lib/components/FormBuilder/fields/CustomField.tsx +16 -0
- package/apps/lib/components/FormBuilder/fields/DateField.tsx +34 -0
- package/apps/lib/components/FormBuilder/fields/FieldArray.tsx +79 -0
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +174 -0
- package/apps/lib/components/FormBuilder/fields/FileField.tsx +40 -0
- package/apps/lib/components/FormBuilder/fields/OptionListFields.tsx +139 -0
- package/apps/lib/components/FormBuilder/fields/OtpField.tsx +32 -0
- package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +82 -0
- package/apps/lib/components/FormBuilder/fields/RichTextEditorField.tsx +37 -0
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +101 -0
- package/apps/lib/components/FormBuilder/fields/SignatureField.tsx +175 -0
- package/apps/lib/components/FormBuilder/fields/SliderField.tsx +72 -0
- package/apps/lib/components/FormBuilder/fields/SwitchBoxField.tsx +42 -0
- package/apps/lib/components/FormBuilder/fields/TableField.tsx +312 -0
- package/apps/lib/components/FormBuilder/fields/TextField.tsx +222 -0
- package/apps/lib/components/FormBuilder/fields/TreeSelectField.tsx +49 -0
- package/apps/lib/components/FormBuilder/fields/countries.ts +303 -0
- package/apps/lib/components/FormBuilder/fields/index.ts +25 -0
- package/apps/lib/components/FormBuilder/form-builder.tsx +292 -0
- package/apps/lib/components/FormBuilder/header.tsx +105 -0
- package/apps/lib/components/FormBuilder/index.ts +37 -0
- package/apps/lib/components/FormBuilder/numberFormat.ts +16 -0
- package/apps/lib/components/FormBuilder/stepper.tsx +217 -0
- package/apps/lib/components/FormBuilder/submit.tsx +41 -0
- package/apps/lib/components/FormBuilder/types.ts +264 -0
- package/apps/lib/components/FormBuilder/viewFormat.tsx +137 -0
- package/apps/lib/components/FormRenderer/FormDrawer.tsx +121 -0
- package/apps/lib/components/FormRenderer/form-renderer.tsx +113 -0
- package/apps/lib/components/FormRenderer/index.ts +9 -0
- package/apps/lib/components/FormRenderer/types.ts +79 -0
- package/apps/lib/components/FormSummary.tsx +282 -0
- package/apps/lib/components/ImageAttachment.tsx +36 -61
- package/apps/lib/components/Label.tsx +49 -42
- package/apps/lib/components/RadioCard.tsx +2 -0
- package/apps/lib/components/SearchableSelect.tsx +9 -3
- package/apps/lib/components/SectionBlock.tsx +16 -8
- package/apps/lib/components/Select.tsx +41 -120
- package/apps/lib/components/TextEditor/RichTextField.tsx +46 -0
- package/apps/lib/components/{TextEditor.tsx → TextEditor/TextEditor.tsx} +63 -9
- package/apps/lib/components/TextEditor/TextEditorToolbar.tsx +429 -0
- package/apps/lib/components/TextEditor/editor-tools/AlignmentTune.ts +70 -0
- package/apps/lib/components/TextEditor/editor-tools/ColorInlineTool.ts +50 -0
- package/apps/lib/components/TextEditor/editor-tools/StrikethroughInlineTool.ts +48 -0
- package/apps/lib/components/TextEditor/editor-tools/inlineFormat.ts +98 -0
- package/apps/lib/{types → components/TextEditor}/editorjs.d.ts +19 -0
- package/apps/lib/components/TextEditor/index.ts +7 -0
- package/apps/lib/components/Textarea.tsx +1 -1
- package/apps/lib/registry.json +51 -58
- package/apps/lib/tsconfig.tsbuildinfo +1 -0
- package/apps/lib/utils/color.ts +175 -0
- package/docs/components/card.md +4 -2
- package/docs/components/chart-block-tool.md +5 -4
- package/docs/components/color-picker.md +101 -0
- package/docs/components/conclusion-header.md +80 -0
- package/docs/components/drawer.md +153 -102
- package/docs/components/form-builder.md +270 -0
- package/docs/components/form-renderer.md +228 -0
- package/docs/components/form-summary.md +123 -0
- package/docs/components/image-attachment.md +10 -4
- package/docs/components/table-dnd-wrapper.md +5 -3
- package/docs/components/text-editor.md +18 -0
- package/docs/how-to/form-and-list-recipes.md +26 -19
- package/docs/how-to/forms-with-form-builder.md +408 -0
- package/docs/how-to/guides.md +153 -179
- package/docs/tutorials/building-first-form.md +150 -159
- package/package.json +1 -1
- /package/apps/lib/components/{ChartBlockTool.ts → TextEditor/ChartBlockTool.ts} +0 -0
- /package/apps/lib/components/{TableDnDWrapper.ts → TextEditor/TableDnDWrapper.ts} +0 -0
- /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
|
|
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/
|
|
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/
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
|
267
|
-
|
|
268
|
-
| Account
|
|
269
|
-
| Fiscal period / year | `ri-calendar-line`
|
|
270
|
-
| Voucher / journal
|
|
271
|
-
| Number series
|
|
272
|
-
| Posting rule
|
|
273
|
-
| Exchange rate
|
|
274
|
-
| Bank / cash account
|
|
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
|