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,41 @@
1
+ "use client";
2
+
3
+ import { ReactNode } from "react";
4
+
5
+ import { Button } from "../Button";
6
+ import { useFormId, useLoading, useMode } from "./context";
7
+
8
+ export interface SubmitButtonProps {
9
+ children?: ReactNode;
10
+ className?: string;
11
+ loadingText?: ReactNode;
12
+ /**
13
+ * The form to submit, by `id`. Defaults to the enclosing FormBuilder's own form id (via
14
+ * context), so a bare `<FormBuilder.Submit>` works even in a header / action bar rendered
15
+ * **outside** the `<form>` element. Set it explicitly only to target a different form.
16
+ */
17
+ form?: string;
18
+ }
19
+
20
+ /** `FormBuilder.Submit` — a loading-aware submit button, hidden in view mode. */
21
+ export function SubmitButton({ children, className, loadingText, form }: SubmitButtonProps) {
22
+ const loading = useLoading();
23
+ const ctxFormId = useFormId();
24
+ const mode = useMode();
25
+
26
+ if (mode === "view") return null;
27
+
28
+ return (
29
+ <Button
30
+ type="submit"
31
+ // Defaults to the enclosing form's id, so a Save in the header (outside the `<form>`)
32
+ // still submits it via native form-association.
33
+ form={form ?? ctxFormId}
34
+ variant="PrimeStyle"
35
+ is_loading={loading}
36
+ className={className}
37
+ >
38
+ {loading ? (loadingText ?? children ?? "Saving…") : (children ?? "Save")}
39
+ </Button>
40
+ );
41
+ }
@@ -0,0 +1,264 @@
1
+ import { ReactNode } from "react";
2
+ import type {
3
+ ControllerRenderProps,
4
+ ControllerFieldState,
5
+ DefaultValues,
6
+ FieldErrors,
7
+ FieldValues,
8
+ Resolver,
9
+ UseFormReturn,
10
+ } from "react-hook-form";
11
+ import type { FieldDirection, FormBuilderMode } from "./context";
12
+ import type { SectionColor } from "../SectionBlock";
13
+
14
+ /** Props shared by every `FormBuilder.*` field. `name` is the RHF path. */
15
+ export interface BaseFieldProps {
16
+ name: string;
17
+ label?: ReactNode;
18
+ placeholder?: string;
19
+ description?: ReactNode;
20
+ required?: boolean;
21
+ disabled?: boolean;
22
+ hidden?: boolean;
23
+ /** Span the full section width. */
24
+ fullWidth?: boolean;
25
+ }
26
+
27
+ export interface OptionItem {
28
+ label: string;
29
+ value: string;
30
+ icon?: ReactNode;
31
+ /** Secondary row label — shown by `FormBuilder.RadioList` / `.CheckboxGroup`. */
32
+ description?: string;
33
+ }
34
+
35
+ /** `FormBuilder.Select` / `.SearchableSelect` — supports async option loading. */
36
+ /** `FormBuilder.Select` — the plain `Select` dropdown. */
37
+ export interface SelectFieldProps extends BaseFieldProps {
38
+ options: OptionItem[];
39
+ }
40
+
41
+ /**
42
+ * `FormBuilder.SearchableSelect` — the `SearchableSelect` combobox: a search box with
43
+ * client-side filtering, or server-side search + infinite scroll via the async props.
44
+ */
45
+ export interface SearchableSelectFieldProps extends SelectFieldProps {
46
+ /** Filter `options` locally (default). Set false for server-side search. */
47
+ filterClientSide?: boolean;
48
+ /** Debounced as the user types — refetch your data here. */
49
+ onSearchChange?: (query: string) => void;
50
+ /** Called when the list bottom scrolls into view and `hasMore && !loading`. */
51
+ onLoadMore?: () => void;
52
+ hasMore?: boolean;
53
+ loading?: boolean;
54
+ }
55
+
56
+ /** `FormBuilder.MultiSelect` / `.Tags`, `.Radio`. */
57
+ export interface OptionsFieldProps extends BaseFieldProps {
58
+ options: OptionItem[];
59
+ }
60
+
61
+ /** `FormBuilder.Currency`. */
62
+ export interface CurrencyFieldProps extends BaseFieldProps {
63
+ currencySymbol?: ReactNode;
64
+ }
65
+
66
+ /** `FormBuilder.File` / `.Image`. */
67
+ export interface FileFieldProps extends BaseFieldProps {
68
+ accept?: string;
69
+ multiple?: boolean;
70
+ }
71
+
72
+ /** `FormBuilder.Custom` — bring your own control + optional read-only render. */
73
+ export interface CustomFieldProps extends BaseFieldProps {
74
+ render: (args: {
75
+ field: ControllerRenderProps<FieldValues, string>;
76
+ fieldState: ControllerFieldState;
77
+ }) => ReactNode;
78
+ formatView?: (value: unknown) => ReactNode;
79
+ }
80
+
81
+ /** `FormBuilder.Date` / `.DateRange` / `.DateMultiple` / `.DateTime`. */
82
+ export interface DateFieldProps extends BaseFieldProps {
83
+ mode?: "single" | "multiple" | "range";
84
+ timePicker?: boolean;
85
+ /** Display format, e.g. "yyyy/MM/dd" (DatePicker default). */
86
+ dateFormat?: string;
87
+ }
88
+
89
+ /** `FormBuilder.Otp` — code input; value is a string of `length` digits. */
90
+ export interface OtpFieldProps extends BaseFieldProps {
91
+ /** Number of digit slots (default 6). */
92
+ length?: number;
93
+ }
94
+
95
+ /** `FormBuilder.SwitchBox` — a switch inside a `#f9f9f9` field box. */
96
+ export interface SwitchBoxFieldProps extends BaseFieldProps {
97
+ /** Text shown inside the box, left of the divider (the design's "Sub-Field-Header"). */
98
+ subLabel?: string;
99
+ }
100
+
101
+ /** `FormBuilder.Checkbox` — a single boolean checkbox with an optional inline label. */
102
+ export interface CheckboxFieldProps extends BaseFieldProps {
103
+ /** Text shown inline next to the checkbox (in addition to the field `label`). */
104
+ subLabel?: string;
105
+ }
106
+
107
+ /** One option for `FormBuilder.RadioCards`. */
108
+ export interface RadioCardOption {
109
+ label: ReactNode;
110
+ value: string;
111
+ description?: ReactNode;
112
+ disabled?: boolean;
113
+ }
114
+
115
+ /** `FormBuilder.RadioCards` — radio options rendered as cards. */
116
+ export interface RadioCardsFieldProps extends BaseFieldProps {
117
+ options: RadioCardOption[];
118
+ }
119
+
120
+ /** `FormBuilder.TreeSelect` (SearchableTree) — hierarchical single-select; value = node id. */
121
+ export interface TreeSelectFieldProps<T = unknown> extends BaseFieldProps {
122
+ nodes: T[];
123
+ getNodeId: (node: T) => string;
124
+ getNodeLabel: (node: T) => ReactNode;
125
+ /** Nested data: expose each node's children. */
126
+ getNodeChildren?: (node: T) => T[] | undefined;
127
+ /** Flat data: the key on each node pointing at its parent's id. */
128
+ parentIdKey?: string;
129
+ /** Allow selecting branch (folder) nodes too. */
130
+ selectableFolders?: boolean;
131
+ }
132
+
133
+ /** `FormBuilder.Password` — optionally show a strength meter. */
134
+ export interface PasswordFieldProps extends BaseFieldProps {
135
+ strengthMeter?: boolean;
136
+ }
137
+
138
+ /** `FormBuilder.Slider` — numeric slider; value `number` (or `[number, number]` with `range`). */
139
+ export interface SliderFieldProps extends BaseFieldProps {
140
+ min?: number;
141
+ max?: number;
142
+ step?: number;
143
+ /** Two thumbs — value becomes `[number, number]`. */
144
+ range?: boolean;
145
+ /** Appended to the value readout, e.g. `"%"` → `50%`. */
146
+ suffix?: string;
147
+ }
148
+
149
+ /** `FormBuilder.Color` — color picker; value hex `string`. */
150
+ export interface ColorFieldProps extends BaseFieldProps {
151
+ presets?: string[];
152
+ /** Enable the opacity slider and `#rrggbbaa` output (default true). */
153
+ alpha?: boolean;
154
+ }
155
+
156
+ /** `FormBuilder.Phone` — country dial-code + number; value `string`. */
157
+ export interface PhoneFieldProps extends BaseFieldProps {
158
+ defaultCountry?: string;
159
+ }
160
+
161
+ /** `FormBuilder.Signature` — freehand signature pad; value is a PNG data-URL `string`. */
162
+ export interface SignatureFieldProps extends BaseFieldProps {
163
+ /** Stroke color (default `#111827`). */
164
+ penColor?: string;
165
+ }
166
+
167
+ /** `FormBuilder.FieldArray` — a repeating list of sub-fields (RHF `useFieldArray`). */
168
+ export interface FieldArrayProps {
169
+ name: string;
170
+ label?: ReactNode;
171
+ description?: ReactNode;
172
+ hidden?: boolean;
173
+ addLabel?: ReactNode;
174
+ /** Value for a newly-added row. */
175
+ defaultItem?: Record<string, unknown>;
176
+ /** Render a row's sub-fields; use names like `${rowName}.field`. */
177
+ children: (rowName: string, index: number, remove: () => void) => ReactNode;
178
+ }
179
+
180
+ /** One column of `FormBuilder.Table`. `cell` renders any `FormBuilder.*` field for a row. */
181
+ export interface TableColumn {
182
+ header: ReactNode;
183
+ /** Fixed column width in px (otherwise the column sizes to content). */
184
+ width?: number;
185
+ align?: "start" | "center" | "end";
186
+ /**
187
+ * The row-object key this column edits. Set it to make the column header **sortable** — a
188
+ * sort toggle appears and clicking it reorders the rows by this key (asc/desc). Omit for
189
+ * non-sortable columns.
190
+ */
191
+ sortKey?: string;
192
+ /**
193
+ * Render the cell's field. `rowName` is the array path for this row (e.g. `lines.0`), so name
194
+ * sub-fields `${rowName}.<key>`. The field renders "bare" (control only, error as a tooltip).
195
+ */
196
+ cell: (rowName: string, index: number) => ReactNode;
197
+ }
198
+
199
+ /**
200
+ * `FormBuilder.Table` — an editable table field (built on RHF `useFieldArray`). Each row is a
201
+ * record; each column cell is any `FormBuilder.*` field. Supports per-row checkbox selection
202
+ * (+ select-all + bulk delete), drag-drop reordering, and add/remove rows. Renders inside a
203
+ * `SectionBlock`, so place it as a top-level child (not inside a `FormBuilder.Section`).
204
+ */
205
+ export interface TableFieldProps {
206
+ name: string;
207
+ /** SectionBlock title. */
208
+ title?: ReactNode;
209
+ /** SectionBlock color badge (default `"Blue"`). */
210
+ color?: SectionColor;
211
+ icon?: ReactNode;
212
+ description?: ReactNode;
213
+ hidden?: boolean;
214
+ columns: TableColumn[];
215
+ /** Value for a newly-added row. */
216
+ defaultItem?: Record<string, unknown>;
217
+ addLabel?: ReactNode;
218
+ /** Per-row checkboxes + select-all + bulk delete (default `true`). */
219
+ selectable?: boolean;
220
+ /** Drag-drop row reordering (default `true`). */
221
+ reorderable?: boolean;
222
+ }
223
+
224
+ /** Root `<FormBuilder>` props. */
225
+ export interface FormBuilderRootProps<T extends FieldValues = FieldValues> {
226
+ children: ReactNode;
227
+ /** `id` on the underlying `<form>` — lets an outside button submit it via `form={id}`. */
228
+ id?: string;
229
+ /**
230
+ * An existing react-hook-form instance. Pass one when something **outside** the
231
+ * form needs the same values — e.g. a `FormSummary` rendered beside it:
232
+ *
233
+ * ```tsx
234
+ * const form = useForm({ resolver, defaultValues })
235
+ * <FormBuilder form={form} …>…</FormBuilder>
236
+ * <FormSummary form={form}>…</FormSummary>
237
+ * ```
238
+ *
239
+ * When omitted, FormBuilder creates its own from `resolver`/`defaultValues`/`values`.
240
+ */
241
+ form?: UseFormReturn<T>;
242
+ onSubmit: (values: T) => void | Promise<void>;
243
+ onInvalid?: (errors: FieldErrors<T>) => void;
244
+ /** RHF resolver — e.g. `zodResolver(schema)`. Library stays validation-agnostic. */
245
+ resolver?: Resolver<T>;
246
+ defaultValues?: DefaultValues<T>;
247
+ /** Controlled values for edit — the form re-syncs when this changes. */
248
+ values?: T;
249
+ /** Loading flag — Submit shows a spinner and inputs disable. */
250
+ loading?: boolean;
251
+ /** `"edit"` (default) or read-only `"view"`. */
252
+ mode?: FormBuilderMode;
253
+ /** Field row direction. Defaults to horizontal (vertical inside a drawer). */
254
+ fieldDirection?: FieldDirection;
255
+ /** Reset to defaults after a successful submit. */
256
+ resetOnSuccess?: boolean;
257
+ /**
258
+ * A live panel (e.g. `FormSummary`) rendered **outside** the `<form>`, as the right column of the
259
+ * adaptive grid — with a stepper it makes a 3-column layout (nav · fields · conclusion), otherwise
260
+ * 2 columns (fields · conclusion). It reads the shared values via its own `form` prop.
261
+ */
262
+ conclusion?: ReactNode;
263
+ className?: string;
264
+ }
@@ -0,0 +1,137 @@
1
+ import { ReactNode } from "react";
2
+ import type { OutputData } from "@editorjs/editorjs";
3
+
4
+ import { Badge } from "../Badge";
5
+ import { RichTextField } from "../TextEditor/RichTextField";
6
+ import { formatNumber } from "./numberFormat";
7
+
8
+ /** A read-only value for `DisplayField`: a plain string, or a rich node. */
9
+ export interface FieldView {
10
+ value?: string;
11
+ valueNode?: ReactNode;
12
+ }
13
+
14
+ export interface OptionItem {
15
+ label: string;
16
+ value: string;
17
+ icon?: ReactNode;
18
+ }
19
+
20
+ /** The read-only "kind" a field formats as (a superset grouping of field types). */
21
+ export type ViewKind =
22
+ | "text"
23
+ | "number"
24
+ | "currency"
25
+ | "option"
26
+ | "boolean"
27
+ | "date"
28
+ | "daterange"
29
+ | "dates"
30
+ | "multi"
31
+ | "richtext"
32
+ | "file";
33
+
34
+ export interface ViewFormatOptions {
35
+ kind: ViewKind;
36
+ value: unknown;
37
+ options?: OptionItem[];
38
+ currencySymbol?: ReactNode;
39
+ }
40
+
41
+ function optionLabel(options: OptionItem[] | undefined, value: unknown): string {
42
+ const found = options?.find((o) => o.value === value);
43
+ return found ? found.label : value == null ? "" : String(value);
44
+ }
45
+
46
+ function formatDate(value: unknown): string {
47
+ if (!(value instanceof Date) || Number.isNaN(value.getTime())) return "";
48
+ const y = value.getFullYear();
49
+ const m = String(value.getMonth() + 1).padStart(2, "0");
50
+ const d = String(value.getDate()).padStart(2, "0");
51
+ return `${y}/${m}/${d}`;
52
+ }
53
+
54
+ function multiLabels(options: OptionItem[] | undefined, value: unknown): string[] {
55
+ if (!Array.isArray(value)) return [];
56
+ return value.map((v) => {
57
+ if (v != null && typeof v === "object") {
58
+ const o = v as { name?: string; label?: string; value?: string };
59
+ return o.name ?? o.label ?? optionLabel(options, o.value);
60
+ }
61
+ return optionLabel(options, v);
62
+ });
63
+ }
64
+
65
+ /** Formats a field value as read-only content for `mode: "view"`. */
66
+ export function formatFieldView({
67
+ kind,
68
+ value,
69
+ options,
70
+ currencySymbol,
71
+ }: ViewFormatOptions): FieldView {
72
+ switch (kind) {
73
+ case "number":
74
+ return { value: formatNumber(value) };
75
+
76
+ case "currency":
77
+ return {
78
+ value:
79
+ value == null || value === ""
80
+ ? ""
81
+ : `${currencySymbol ? `${currencySymbol} ` : ""}${formatNumber(value)}`,
82
+ };
83
+
84
+ case "option":
85
+ return { value: optionLabel(options, value) };
86
+
87
+ case "boolean":
88
+ return {
89
+ valueNode: <Badge size="S" color={value ? "green" : "gray"} label={value ? "Yes" : "No"} />,
90
+ };
91
+
92
+ case "date":
93
+ return { value: formatDate(value) };
94
+
95
+ case "daterange": {
96
+ const r = value as { from?: unknown; to?: unknown } | null | undefined;
97
+ if (!r || (!r.from && !r.to)) return { value: "" };
98
+ return { value: `${formatDate(r.from)} – ${formatDate(r.to)}` };
99
+ }
100
+
101
+ case "dates": {
102
+ const arr = Array.isArray(value) ? value : [];
103
+ return { value: arr.map(formatDate).filter(Boolean).join(", ") };
104
+ }
105
+
106
+ case "multi": {
107
+ const labels = multiLabels(options, value);
108
+ if (labels.length === 0) return { value: "" };
109
+ return {
110
+ valueNode: (
111
+ <div className="flex flex-wrap gap-1">
112
+ {labels.map((label, i) => (
113
+ <Badge key={`${label}-${i}`} size="S" color="ocean" label={label} />
114
+ ))}
115
+ </div>
116
+ ),
117
+ };
118
+ }
119
+
120
+ case "richtext":
121
+ if (!value) return { value: "" };
122
+ return { valueNode: <RichTextField readOnly data={value as OutputData} /> };
123
+
124
+ case "file": {
125
+ const files = Array.isArray(value) ? value : value ? [value] : [];
126
+ if (files.length === 0) return { value: "" };
127
+ const names = files.map((f) =>
128
+ f instanceof File ? f.name : typeof f === "string" ? f : "file",
129
+ );
130
+ return { value: names.join(", ") };
131
+ }
132
+
133
+ case "text":
134
+ default:
135
+ return { value: value == null ? "" : String(value) };
136
+ }
137
+ }
@@ -0,0 +1,121 @@
1
+ "use client";
2
+
3
+ import { ReactNode } from "react";
4
+
5
+ import {
6
+ Drawer,
7
+ DrawerContent,
8
+ DrawerPanel,
9
+ DrawerTitle,
10
+ DrawerNotch,
11
+ DrawerNotchClose,
12
+ DrawerNotchDivider,
13
+ DrawerNotchPill,
14
+ } from "../Drawer";
15
+ import { FormHeaderBar, type HeaderVariant } from "../FormBuilder/header";
16
+
17
+ export interface FormDrawerProps {
18
+ open: boolean;
19
+ onOpenChange: (open: boolean) => void;
20
+ /** The `<FormBuilder>` (or any content) to host inside the drawer. */
21
+ children: ReactNode;
22
+ /**
23
+ * The conclusion panel rendered **beside** the form, outside the drawer's scrollable
24
+ * body — typically a `<FormSummary>` reading the same hoisted `form`. `FormDrawer` owns
25
+ * the box around it (full height, shrinkable), which is what lets the panel's own
26
+ * `h-full` and internal scrolling work. The panel sizes itself — a `FormSummary` is
27
+ * 228px wide by default — and brings its own (dark) background.
28
+ */
29
+ summary?: ReactNode;
30
+ /** @deprecated Renamed to `summary`. */
31
+ childrenOutside?: ReactNode;
32
+ /**
33
+ * Plain title text (uppercased) in the header's title pill — also the a11y title.
34
+ * A string, because it renders through the same `HeaderBar` as the page form.
35
+ */
36
+ title?: string;
37
+ /** Badge text in the title pill. Defaults from `variant` (New / Edit / View). */
38
+ badge?: string;
39
+ /** Colored badge variant, matching the page form's header. Default `"new"`. */
40
+ variant?: HeaderVariant;
41
+ /** Action buttons shown on the right of the drawer header (e.g. a Save submit). */
42
+ actions?: ReactNode;
43
+ /** Shows an "Open in new tab" pill in the notch when provided. */
44
+ onOpenInNewTab?: () => void;
45
+ }
46
+
47
+ /**
48
+ * A right-anchored Glare drawer for hosting a form. This is a **FormRenderer**
49
+ * concern — `FormBuilder` itself is drawer-unaware (it only displays inputs).
50
+ * Wrap a `<FormBuilder>` in `<FormDrawer>` (and pass `fieldDirection="vertical"`
51
+ * to the form) to show it in a drawer, or let `FormRenderer` do it via
52
+ * `display: "drawer"`.
53
+ *
54
+ * The `wrapperClassName` includes `mt-0 h-auto` to cancel `DrawerContent`'s base
55
+ * bottom-sheet offset (the Glare right-side form-drawer recipe).
56
+ *
57
+ * It owns the arrangement of its two slots: the form goes in a `DrawerPanel` (the light
58
+ * surface), and `summary` sits beside it in the tray with a 6px gutter. The tray itself
59
+ * paints nothing, so each panel brings its own background.
60
+ */
61
+ export function FormDrawer({
62
+ open,
63
+ onOpenChange,
64
+ children,
65
+ summary,
66
+ childrenOutside,
67
+ title = "Form",
68
+ badge,
69
+ variant,
70
+ actions,
71
+ onOpenInNewTab,
72
+ }: FormDrawerProps) {
73
+ const conclusion = summary ?? childrenOutside;
74
+
75
+ return (
76
+ <Drawer open={open} onOpenChange={onOpenChange} direction="right">
77
+ {/* The panel fills the available width (minus the 8px insets), capped at 1048px.
78
+ `gap-[6px]` is the gutter between the form panel and the conclusion beside it. */}
79
+ <DrawerContent
80
+ wrapperClassName="top-2 right-2 bottom-2 left-auto mt-0 h-auto w-[calc(100vw-1rem)] max-w-[1048px]"
81
+ className="gap-[6px]"
82
+ notch={
83
+ <DrawerNotch>
84
+ <DrawerNotchClose onClick={() => onOpenChange(false)} />
85
+ {onOpenInNewTab && (
86
+ <>
87
+ <DrawerNotchDivider />
88
+ <DrawerNotchPill color="Yellow" onClick={onOpenInNewTab}>
89
+ Open in new tab
90
+ <i className="ri-arrow-right-up-line text-[12px]" />
91
+ </DrawerNotchPill>
92
+ </>
93
+ )}
94
+ </DrawerNotch>
95
+ }
96
+ >
97
+ <DrawerPanel className="rounded-tr-[16px] rounded-b-[16px] p-0">
98
+ <div className="relative flex min-h-0 flex-1 flex-col">
99
+ {/* Vaul requires a Drawer.Title for the a11y name; the visible title is the
100
+ HeaderBar below, so this one is for screen readers only. */}
101
+ <DrawerTitle className="sr-only">{title}</DrawerTitle>
102
+
103
+ {/* The SAME floating header the page form uses, so a form's title looks
104
+ identical in either surface. */}
105
+ <FormHeaderBar title={title} label={badge} variant={variant}>
106
+ {actions}
107
+ </FormHeaderBar>
108
+
109
+ {/* pt-[72px] clears the 44px header pill (inset 4px) — same as the page shell. */}
110
+ <div className="h-full overflow-y-auto px-3 pb-[20px] pt-[72px]">{children}</div>
111
+ </div>
112
+ </DrawerPanel>
113
+
114
+ {/* `flex min-h-0` so the conclusion stretches to the tray's full height and can
115
+ still shrink — without `min-h-0` it would grow past the tray instead of
116
+ scrolling inside it. No `flex-1`: the panel sizes itself. */}
117
+ {conclusion && <div className="flex min-h-0">{conclusion}</div>}
118
+ </DrawerContent>
119
+ </Drawer>
120
+ );
121
+ }
@@ -0,0 +1,113 @@
1
+ "use client";
2
+
3
+ import { useId } from "react";
4
+ import { FieldValues } from "react-hook-form";
5
+
6
+ import { FormBuilder } from "../FormBuilder";
7
+ import { FormIdContext, LoadingContext, ModeContext } from "../FormBuilder/context";
8
+ import { FormDrawer } from "./FormDrawer";
9
+ import type { FormRendererProps } from "./types";
10
+
11
+ /**
12
+ * FormRenderer — a thin wrapper around the compound `FormBuilder`. Author the
13
+ * fields as JSX children; FormRenderer owns page-vs-drawer display, the absolute
14
+ * title header, and drawer field direction.
15
+ *
16
+ * FormRenderer never manufactures a Submit — you compose it and hand it to `actions`
17
+ * (`actions={<FormBuilder.Submit>Save</FormBuilder.Submit>}`). It renders in the form's
18
+ * header action pill (page) or the drawer header (drawer), and auto-targets this form.
19
+ */
20
+ export function FormRenderer<T extends FieldValues = FieldValues>({
21
+ children,
22
+ id,
23
+ onSubmit,
24
+ onInvalid,
25
+ resolver,
26
+ defaultValues,
27
+ values,
28
+ mode = "edit",
29
+ loading,
30
+ resetOnSuccess,
31
+ fieldDirection,
32
+ form,
33
+ display = "page",
34
+ header,
35
+ summary,
36
+ className,
37
+ actions,
38
+ open = false,
39
+ onOpenChange,
40
+ title,
41
+ badge,
42
+ onOpenInNewTab,
43
+ }: FormRendererProps<T>) {
44
+ const isDrawer = display === "drawer";
45
+
46
+ // FormRenderer owns the display decision: a drawer form lays out vertically.
47
+ const effectiveDirection = fieldDirection ?? (isDrawer ? "vertical" : undefined);
48
+ // The absolute title header is a page concern — the drawer has its own header.
49
+ const useHeader = !!header && !isDrawer;
50
+ // A stable `id` lets a button OUTSIDE the form submit it via `form={id}` (the header action
51
+ // pill / drawer header). Falls back to a generated id, provided to the Submit via context.
52
+ const autoId = useId();
53
+ const formId = id ?? autoId;
54
+
55
+ const inner = (
56
+ <FormBuilder
57
+ id={formId}
58
+ form={form}
59
+ onSubmit={onSubmit}
60
+ onInvalid={onInvalid}
61
+ resolver={resolver}
62
+ defaultValues={defaultValues}
63
+ values={values}
64
+ mode={mode}
65
+ loading={loading}
66
+ fieldDirection={effectiveDirection}
67
+ resetOnSuccess={resetOnSuccess}
68
+ // Page display only — in a drawer the panel owns the sizing.
69
+ className={!isDrawer ? className : undefined}
70
+ // Page display: FormBuilder lays the conclusion as the grid's right column (a stepper adds
71
+ // the left nav column too). In a drawer the summary goes to FormDrawer's tray instead.
72
+ conclusion={!isDrawer ? summary : undefined}
73
+ >
74
+ {useHeader && (
75
+ <FormBuilder.Header title={header!.title} label={header!.label} variant={header!.variant}>
76
+ {actions}
77
+ </FormBuilder.Header>
78
+ )}
79
+
80
+ {children}
81
+ </FormBuilder>
82
+ );
83
+
84
+ if (isDrawer) {
85
+ return (
86
+ <FormDrawer
87
+ open={open}
88
+ onOpenChange={onOpenChange ?? (() => {})}
89
+ title={title ?? header?.title}
90
+ badge={badge ?? header?.label}
91
+ variant={header?.variant}
92
+ summary={summary}
93
+ // The drawer header sits outside the `<form>`, so re-supply the form/loading/mode context
94
+ // a bare `FormBuilder.Submit` reads (on the page it gets these from the FormBuilder tree).
95
+ actions={
96
+ actions ? (
97
+ <ModeContext.Provider value={mode}>
98
+ <LoadingContext.Provider value={!!loading}>
99
+ <FormIdContext.Provider value={formId}>{actions}</FormIdContext.Provider>
100
+ </LoadingContext.Provider>
101
+ </ModeContext.Provider>
102
+ ) : undefined
103
+ }
104
+ onOpenInNewTab={onOpenInNewTab}
105
+ >
106
+ {inner}
107
+ </FormDrawer>
108
+ );
109
+ }
110
+
111
+ // Page display: FormBuilder already placed `summary` as the grid's conclusion column.
112
+ return inner;
113
+ }
@@ -0,0 +1,9 @@
1
+ export { FormRenderer } from "./form-renderer";
2
+ export { FormDrawer } from "./FormDrawer";
3
+ export type { FormDrawerProps } from "./FormDrawer";
4
+ export type {
5
+ FormRendererProps,
6
+ FormRendererMode,
7
+ FormRendererDisplay,
8
+ FieldDirection,
9
+ } from "./types";