torch-glare 2.4.1 → 2.4.3

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 (85) 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/context.ts +68 -0
  8. package/apps/lib/components/FormBuilder/fields/ChoiceFields.tsx +60 -0
  9. package/apps/lib/components/FormBuilder/fields/ColorField.tsx +59 -0
  10. package/apps/lib/components/FormBuilder/fields/CustomField.tsx +11 -0
  11. package/apps/lib/components/FormBuilder/fields/DateField.tsx +32 -0
  12. package/apps/lib/components/FormBuilder/fields/FieldArray.tsx +74 -0
  13. package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +148 -0
  14. package/apps/lib/components/FormBuilder/fields/FileField.tsx +39 -0
  15. package/apps/lib/components/FormBuilder/fields/OptionListFields.tsx +132 -0
  16. package/apps/lib/components/FormBuilder/fields/OtpField.tsx +31 -0
  17. package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +83 -0
  18. package/apps/lib/components/FormBuilder/fields/RichTextEditorField.tsx +31 -0
  19. package/apps/lib/components/FormBuilder/fields/SelectField.tsx +91 -0
  20. package/apps/lib/components/FormBuilder/fields/SignatureField.tsx +157 -0
  21. package/apps/lib/components/FormBuilder/fields/SliderField.tsx +67 -0
  22. package/apps/lib/components/FormBuilder/fields/SwitchBoxField.tsx +41 -0
  23. package/apps/lib/components/FormBuilder/fields/TableField.tsx +308 -0
  24. package/apps/lib/components/FormBuilder/fields/TextField.tsx +213 -0
  25. package/apps/lib/components/FormBuilder/fields/TreeSelectField.tsx +43 -0
  26. package/apps/lib/components/FormBuilder/fields/countries.ts +303 -0
  27. package/apps/lib/components/FormBuilder/fields/index.ts +25 -0
  28. package/apps/lib/components/FormBuilder/form-builder.tsx +279 -0
  29. package/apps/lib/components/FormBuilder/header.tsx +101 -0
  30. package/apps/lib/components/FormBuilder/index.ts +33 -0
  31. package/apps/lib/components/FormBuilder/numberFormat.ts +16 -0
  32. package/apps/lib/components/FormBuilder/stepper.tsx +290 -0
  33. package/apps/lib/components/FormBuilder/submit.tsx +38 -0
  34. package/apps/lib/components/FormBuilder/types.ts +261 -0
  35. package/apps/lib/components/FormRenderer/FormDrawer.tsx +128 -0
  36. package/apps/lib/components/FormRenderer/form-renderer.tsx +111 -0
  37. package/apps/lib/components/FormRenderer/index.ts +4 -0
  38. package/apps/lib/components/FormRenderer/types.ts +77 -0
  39. package/apps/lib/components/FormSummary.tsx +282 -0
  40. package/apps/lib/components/ImageAttachment.tsx +36 -61
  41. package/apps/lib/components/Label.tsx +49 -42
  42. package/apps/lib/components/Popover.tsx +6 -2
  43. package/apps/lib/components/RadioCard.tsx +2 -0
  44. package/apps/lib/components/SearchableSelect.tsx +16 -5
  45. package/apps/lib/components/SectionBlock.tsx +16 -8
  46. package/apps/lib/components/Select.tsx +41 -120
  47. package/apps/lib/components/TextEditor/RichTextField.tsx +46 -0
  48. package/apps/lib/components/{TextEditor.tsx → TextEditor/TextEditor.tsx} +63 -9
  49. package/apps/lib/components/TextEditor/TextEditorToolbar.tsx +429 -0
  50. package/apps/lib/components/TextEditor/editor-tools/AlignmentTune.ts +70 -0
  51. package/apps/lib/components/TextEditor/editor-tools/ColorInlineTool.ts +50 -0
  52. package/apps/lib/components/TextEditor/editor-tools/StrikethroughInlineTool.ts +48 -0
  53. package/apps/lib/components/TextEditor/editor-tools/inlineFormat.ts +98 -0
  54. package/apps/lib/{types → components/TextEditor}/editorjs.d.ts +19 -0
  55. package/apps/lib/components/TextEditor/index.ts +7 -0
  56. package/apps/lib/components/Textarea.tsx +1 -1
  57. package/apps/lib/layouts/FieldSection.tsx +25 -22
  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/dist/src/shared/tailwindInit.d.ts.map +1 -1
  62. package/dist/src/shared/tailwindInit.js +3 -0
  63. package/dist/src/shared/tailwindInit.js.map +1 -1
  64. package/docs/components/card.md +4 -2
  65. package/docs/components/chart-block-tool.md +5 -4
  66. package/docs/components/color-picker.md +101 -0
  67. package/docs/components/conclusion-header.md +80 -0
  68. package/docs/components/drawer.md +153 -102
  69. package/docs/components/form-builder.md +260 -0
  70. package/docs/components/form-renderer.md +234 -0
  71. package/docs/components/form-summary.md +123 -0
  72. package/docs/components/image-attachment.md +10 -4
  73. package/docs/components/searchable-select.md +50 -46
  74. package/docs/components/table-dnd-wrapper.md +5 -3
  75. package/docs/components/text-editor.md +18 -0
  76. package/docs/how-to/form-and-list-recipes.md +26 -19
  77. package/docs/how-to/forms-with-form-builder.md +411 -0
  78. package/docs/how-to/guides.md +153 -179
  79. package/docs/reference/tailwind-plugins.md +11 -1
  80. package/docs/tutorials/building-first-form.md +150 -159
  81. package/docs/tutorials/getting-started.md +9 -0
  82. package/package.json +1 -1
  83. /package/apps/lib/components/{ChartBlockTool.ts → TextEditor/ChartBlockTool.ts} +0 -0
  84. /package/apps/lib/components/{TableDnDWrapper.ts → TextEditor/TableDnDWrapper.ts} +0 -0
  85. /package/apps/lib/{utils → components/TextEditor}/markdownParser.ts +0 -0
@@ -0,0 +1,303 @@
1
+ /**
2
+ * ISO 3166-1 countries + their E.164 calling codes, for `FormBuilder.Phone`.
3
+ *
4
+ * Data is stored compactly as `[isoCode, name, dial]`; the flag emoji is derived from the ISO code
5
+ * (regional-indicator letters) rather than stored. Dial codes are NOT unique (US/Canada share +1,
6
+ * etc.) — that's why the picker keys options by the unique ISO `code`, and `countryByDial` resolves
7
+ * a stored dial back to a representative country.
8
+ */
9
+
10
+ export interface Country {
11
+ /** ISO 3166-1 alpha-2 code, unique — e.g. "US". */
12
+ code: string;
13
+ name: string;
14
+ /** E.164 calling code with leading "+", e.g. "+1". */
15
+ dial: string;
16
+ }
17
+
18
+ // [ISO code, name, dial(no +)]
19
+ const DATA: ReadonlyArray<readonly [string, string, string]> = [
20
+ ["AF", "Afghanistan", "93"],
21
+ ["AX", "Åland Islands", "358"],
22
+ ["AL", "Albania", "355"],
23
+ ["DZ", "Algeria", "213"],
24
+ ["AS", "American Samoa", "1"],
25
+ ["AD", "Andorra", "376"],
26
+ ["AO", "Angola", "244"],
27
+ ["AI", "Anguilla", "1"],
28
+ ["AG", "Antigua and Barbuda", "1"],
29
+ ["AR", "Argentina", "54"],
30
+ ["AM", "Armenia", "374"],
31
+ ["AW", "Aruba", "297"],
32
+ ["AU", "Australia", "61"],
33
+ ["AT", "Austria", "43"],
34
+ ["AZ", "Azerbaijan", "994"],
35
+ ["BS", "Bahamas", "1"],
36
+ ["BH", "Bahrain", "973"],
37
+ ["BD", "Bangladesh", "880"],
38
+ ["BB", "Barbados", "1"],
39
+ ["BY", "Belarus", "375"],
40
+ ["BE", "Belgium", "32"],
41
+ ["BZ", "Belize", "501"],
42
+ ["BJ", "Benin", "229"],
43
+ ["BM", "Bermuda", "1"],
44
+ ["BT", "Bhutan", "975"],
45
+ ["BO", "Bolivia", "591"],
46
+ ["BA", "Bosnia and Herzegovina", "387"],
47
+ ["BW", "Botswana", "267"],
48
+ ["BR", "Brazil", "55"],
49
+ ["IO", "British Indian Ocean Territory", "246"],
50
+ ["VG", "British Virgin Islands", "1"],
51
+ ["BN", "Brunei", "673"],
52
+ ["BG", "Bulgaria", "359"],
53
+ ["BF", "Burkina Faso", "226"],
54
+ ["BI", "Burundi", "257"],
55
+ ["KH", "Cambodia", "855"],
56
+ ["CM", "Cameroon", "237"],
57
+ ["CA", "Canada", "1"],
58
+ ["CV", "Cape Verde", "238"],
59
+ ["KY", "Cayman Islands", "1"],
60
+ ["CF", "Central African Republic", "236"],
61
+ ["TD", "Chad", "235"],
62
+ ["CL", "Chile", "56"],
63
+ ["CN", "China", "86"],
64
+ ["CX", "Christmas Island", "61"],
65
+ ["CC", "Cocos (Keeling) Islands", "61"],
66
+ ["CO", "Colombia", "57"],
67
+ ["KM", "Comoros", "269"],
68
+ ["CG", "Congo - Brazzaville", "242"],
69
+ ["CD", "Congo - Kinshasa", "243"],
70
+ ["CK", "Cook Islands", "682"],
71
+ ["CR", "Costa Rica", "506"],
72
+ ["CI", "Côte d’Ivoire", "225"],
73
+ ["HR", "Croatia", "385"],
74
+ ["CU", "Cuba", "53"],
75
+ ["CW", "Curaçao", "599"],
76
+ ["CY", "Cyprus", "357"],
77
+ ["CZ", "Czechia", "420"],
78
+ ["DK", "Denmark", "45"],
79
+ ["DJ", "Djibouti", "253"],
80
+ ["DM", "Dominica", "1"],
81
+ ["DO", "Dominican Republic", "1"],
82
+ ["EC", "Ecuador", "593"],
83
+ ["EG", "Egypt", "20"],
84
+ ["SV", "El Salvador", "503"],
85
+ ["GQ", "Equatorial Guinea", "240"],
86
+ ["ER", "Eritrea", "291"],
87
+ ["EE", "Estonia", "372"],
88
+ ["SZ", "Eswatini", "268"],
89
+ ["ET", "Ethiopia", "251"],
90
+ ["FK", "Falkland Islands", "500"],
91
+ ["FO", "Faroe Islands", "298"],
92
+ ["FJ", "Fiji", "679"],
93
+ ["FI", "Finland", "358"],
94
+ ["FR", "France", "33"],
95
+ ["GF", "French Guiana", "594"],
96
+ ["PF", "French Polynesia", "689"],
97
+ ["GA", "Gabon", "241"],
98
+ ["GM", "Gambia", "220"],
99
+ ["GE", "Georgia", "995"],
100
+ ["DE", "Germany", "49"],
101
+ ["GH", "Ghana", "233"],
102
+ ["GI", "Gibraltar", "350"],
103
+ ["GR", "Greece", "30"],
104
+ ["GL", "Greenland", "299"],
105
+ ["GD", "Grenada", "1"],
106
+ ["GP", "Guadeloupe", "590"],
107
+ ["GU", "Guam", "1"],
108
+ ["GT", "Guatemala", "502"],
109
+ ["GG", "Guernsey", "44"],
110
+ ["GN", "Guinea", "224"],
111
+ ["GW", "Guinea-Bissau", "245"],
112
+ ["GY", "Guyana", "592"],
113
+ ["HT", "Haiti", "509"],
114
+ ["HN", "Honduras", "504"],
115
+ ["HK", "Hong Kong", "852"],
116
+ ["HU", "Hungary", "36"],
117
+ ["IS", "Iceland", "354"],
118
+ ["IN", "India", "91"],
119
+ ["ID", "Indonesia", "62"],
120
+ ["IR", "Iran", "98"],
121
+ ["IQ", "Iraq", "964"],
122
+ ["IE", "Ireland", "353"],
123
+ ["IM", "Isle of Man", "44"],
124
+ ["IT", "Italy", "39"],
125
+ ["JM", "Jamaica", "1"],
126
+ ["JP", "Japan", "81"],
127
+ ["JE", "Jersey", "44"],
128
+ ["JO", "Jordan", "962"],
129
+ ["KZ", "Kazakhstan", "7"],
130
+ ["KE", "Kenya", "254"],
131
+ ["KI", "Kiribati", "686"],
132
+ ["XK", "Kosovo", "383"],
133
+ ["KW", "Kuwait", "965"],
134
+ ["KG", "Kyrgyzstan", "996"],
135
+ ["LA", "Laos", "856"],
136
+ ["LV", "Latvia", "371"],
137
+ ["LB", "Lebanon", "961"],
138
+ ["LS", "Lesotho", "266"],
139
+ ["LR", "Liberia", "231"],
140
+ ["LY", "Libya", "218"],
141
+ ["LI", "Liechtenstein", "423"],
142
+ ["LT", "Lithuania", "370"],
143
+ ["LU", "Luxembourg", "352"],
144
+ ["MO", "Macau", "853"],
145
+ ["MG", "Madagascar", "261"],
146
+ ["MW", "Malawi", "265"],
147
+ ["MY", "Malaysia", "60"],
148
+ ["MV", "Maldives", "960"],
149
+ ["ML", "Mali", "223"],
150
+ ["MT", "Malta", "356"],
151
+ ["MH", "Marshall Islands", "692"],
152
+ ["MQ", "Martinique", "596"],
153
+ ["MR", "Mauritania", "222"],
154
+ ["MU", "Mauritius", "230"],
155
+ ["YT", "Mayotte", "262"],
156
+ ["MX", "Mexico", "52"],
157
+ ["FM", "Micronesia", "691"],
158
+ ["MD", "Moldova", "373"],
159
+ ["MC", "Monaco", "377"],
160
+ ["MN", "Mongolia", "976"],
161
+ ["ME", "Montenegro", "382"],
162
+ ["MS", "Montserrat", "1"],
163
+ ["MA", "Morocco", "212"],
164
+ ["MZ", "Mozambique", "258"],
165
+ ["MM", "Myanmar (Burma)", "95"],
166
+ ["NA", "Namibia", "264"],
167
+ ["NR", "Nauru", "674"],
168
+ ["NP", "Nepal", "977"],
169
+ ["NL", "Netherlands", "31"],
170
+ ["NC", "New Caledonia", "687"],
171
+ ["NZ", "New Zealand", "64"],
172
+ ["NI", "Nicaragua", "505"],
173
+ ["NE", "Niger", "227"],
174
+ ["NG", "Nigeria", "234"],
175
+ ["NU", "Niue", "683"],
176
+ ["NF", "Norfolk Island", "672"],
177
+ ["KP", "North Korea", "850"],
178
+ ["MK", "North Macedonia", "389"],
179
+ ["MP", "Northern Mariana Islands", "1"],
180
+ ["NO", "Norway", "47"],
181
+ ["OM", "Oman", "968"],
182
+ ["PK", "Pakistan", "92"],
183
+ ["PW", "Palau", "680"],
184
+ ["PS", "Palestine", "970"],
185
+ ["PA", "Panama", "507"],
186
+ ["PG", "Papua New Guinea", "675"],
187
+ ["PY", "Paraguay", "595"],
188
+ ["PE", "Peru", "51"],
189
+ ["PH", "Philippines", "63"],
190
+ ["PL", "Poland", "48"],
191
+ ["PT", "Portugal", "351"],
192
+ ["PR", "Puerto Rico", "1"],
193
+ ["QA", "Qatar", "974"],
194
+ ["RE", "Réunion", "262"],
195
+ ["RO", "Romania", "40"],
196
+ ["RU", "Russia", "7"],
197
+ ["RW", "Rwanda", "250"],
198
+ ["WS", "Samoa", "685"],
199
+ ["SM", "San Marino", "378"],
200
+ ["ST", "São Tomé and Príncipe", "239"],
201
+ ["SA", "Saudi Arabia", "966"],
202
+ ["SN", "Senegal", "221"],
203
+ ["RS", "Serbia", "381"],
204
+ ["SC", "Seychelles", "248"],
205
+ ["SL", "Sierra Leone", "232"],
206
+ ["SG", "Singapore", "65"],
207
+ ["SX", "Sint Maarten", "1"],
208
+ ["SK", "Slovakia", "421"],
209
+ ["SI", "Slovenia", "386"],
210
+ ["SB", "Solomon Islands", "677"],
211
+ ["SO", "Somalia", "252"],
212
+ ["ZA", "South Africa", "27"],
213
+ ["KR", "South Korea", "82"],
214
+ ["SS", "South Sudan", "211"],
215
+ ["ES", "Spain", "34"],
216
+ ["LK", "Sri Lanka", "94"],
217
+ ["BL", "St. Barthélemy", "590"],
218
+ ["SH", "St. Helena", "290"],
219
+ ["KN", "St. Kitts and Nevis", "1"],
220
+ ["LC", "St. Lucia", "1"],
221
+ ["MF", "St. Martin", "590"],
222
+ ["PM", "St. Pierre and Miquelon", "508"],
223
+ ["VC", "St. Vincent and the Grenadines", "1"],
224
+ ["SD", "Sudan", "249"],
225
+ ["SR", "Suriname", "597"],
226
+ ["SE", "Sweden", "46"],
227
+ ["CH", "Switzerland", "41"],
228
+ ["SY", "Syria", "963"],
229
+ ["TW", "Taiwan", "886"],
230
+ ["TJ", "Tajikistan", "992"],
231
+ ["TZ", "Tanzania", "255"],
232
+ ["TH", "Thailand", "66"],
233
+ ["TL", "Timor-Leste", "670"],
234
+ ["TG", "Togo", "228"],
235
+ ["TK", "Tokelau", "690"],
236
+ ["TO", "Tonga", "676"],
237
+ ["TT", "Trinidad and Tobago", "1"],
238
+ ["TN", "Tunisia", "216"],
239
+ ["TR", "Turkey", "90"],
240
+ ["TM", "Turkmenistan", "993"],
241
+ ["TC", "Turks and Caicos Islands", "1"],
242
+ ["TV", "Tuvalu", "688"],
243
+ ["UG", "Uganda", "256"],
244
+ ["UA", "Ukraine", "380"],
245
+ ["AE", "United Arab Emirates", "971"],
246
+ ["GB", "United Kingdom", "44"],
247
+ ["US", "United States", "1"],
248
+ ["UY", "Uruguay", "598"],
249
+ ["UZ", "Uzbekistan", "998"],
250
+ ["VU", "Vanuatu", "678"],
251
+ ["VA", "Vatican City", "39"],
252
+ ["VE", "Venezuela", "58"],
253
+ ["VN", "Vietnam", "84"],
254
+ ["VI", "U.S. Virgin Islands", "1"],
255
+ ["WF", "Wallis and Futuna", "681"],
256
+ ["YE", "Yemen", "967"],
257
+ ["ZM", "Zambia", "260"],
258
+ ["ZW", "Zimbabwe", "263"],
259
+ ];
260
+
261
+ /** The full list, sorted by name. */
262
+ export const COUNTRIES: Country[] = DATA.map(([code, name, dial]) => ({
263
+ code,
264
+ name,
265
+ dial: `+${dial}`,
266
+ })).sort((a, b) => a.name.localeCompare(b.name));
267
+
268
+ const BY_CODE = new Map(COUNTRIES.map((c) => [c.code, c]));
269
+
270
+ /** Flag emoji from an ISO 3166-1 alpha-2 code (regional-indicator letters). */
271
+ export function flagEmoji(code: string): string {
272
+ return code
273
+ .toUpperCase()
274
+ .replace(/[^A-Z]/g, "")
275
+ .replace(/./g, (c) => String.fromCodePoint(127397 + c.charCodeAt(0)));
276
+ }
277
+
278
+ export function countryByCode(code: string): Country | undefined {
279
+ return BY_CODE.get(code);
280
+ }
281
+
282
+ // For dial codes shared by several countries, the one to show when resolving a stored dial back.
283
+ const PREFERRED_BY_DIAL: Record<string, string> = {
284
+ "+964": "IQ",
285
+ "+1": "US",
286
+ "+7": "RU",
287
+ "+44": "GB",
288
+ "+61": "AU",
289
+ "+39": "IT",
290
+ "+212": "MA",
291
+ "+262": "RE",
292
+ "+358": "FI",
293
+ "+590": "GP",
294
+ "+596": "MQ",
295
+ "+599": "CW",
296
+ };
297
+
298
+ /** A representative country for a dial code (dial codes aren't unique). */
299
+ export function countryByDial(dial: string): Country | undefined {
300
+ const preferred = PREFERRED_BY_DIAL[dial];
301
+ if (preferred) return BY_CODE.get(preferred);
302
+ return COUNTRIES.find((c) => c.dial === dial);
303
+ }
@@ -0,0 +1,25 @@
1
+ export {
2
+ TextField,
3
+ EmailField,
4
+ PasswordField,
5
+ NumberField,
6
+ CurrencyField,
7
+ TextareaField,
8
+ } from "./TextField";
9
+ export { SelectField, SearchableSelectField, MultiSelectField } from "./SelectField";
10
+ export { CheckboxField, RadioCardsField } from "./ChoiceFields";
11
+ export { RadioListField, CheckboxGroupField } from "./OptionListFields";
12
+ export { SwitchBoxField } from "./SwitchBoxField";
13
+ export { DateField } from "./DateField";
14
+ export { OtpField } from "./OtpField";
15
+ export { TreeSelectField } from "./TreeSelectField";
16
+ export { SliderField } from "./SliderField";
17
+ export { ColorField } from "./ColorField";
18
+ export { SignatureField } from "./SignatureField";
19
+ export { PhoneField } from "./PhoneField";
20
+ export { FieldArray } from "./FieldArray";
21
+ export { TableField } from "./TableField";
22
+ export { FileField } from "./FileField";
23
+ export { RichTextField } from "./RichTextEditorField";
24
+ export { CustomField } from "./CustomField";
25
+ export { FieldShell } from "./FieldShell";
@@ -0,0 +1,279 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ReactNode } from "react";
5
+ import { useForm, type FieldValues } from "react-hook-form";
6
+
7
+ import { cn } from "../../utils/cn";
8
+ import { Form } from "../Form";
9
+ import { SectionBlock, type SectionColor } from "../SectionBlock";
10
+ import { LoadingContext, DirectionContext, StepperContext, FormIdContext } from "./context";
11
+ import { Header } from "./header";
12
+ import type { FormBuilderRootProps } from "./types";
13
+ import {
14
+ TextField,
15
+ EmailField,
16
+ PasswordField,
17
+ NumberField,
18
+ CurrencyField,
19
+ TextareaField,
20
+ SelectField,
21
+ SearchableSelectField,
22
+ MultiSelectField,
23
+ CheckboxField,
24
+ RadioCardsField,
25
+ RadioListField,
26
+ CheckboxGroupField,
27
+ SwitchBoxField,
28
+ DateField,
29
+ OtpField,
30
+ TreeSelectField,
31
+ SliderField,
32
+ ColorField,
33
+ SignatureField,
34
+ PhoneField,
35
+ FieldArray,
36
+ TableField,
37
+ FileField,
38
+ RichTextField,
39
+ CustomField,
40
+ } from "./fields";
41
+ import {
42
+ Stepper,
43
+ Step,
44
+ Back,
45
+ Next,
46
+ StepperNav,
47
+ StepSlot,
48
+ useStepperState,
49
+ isStepElement,
50
+ isStepperElement,
51
+ } from "./stepper";
52
+ import { SubmitButton } from "./submit";
53
+
54
+ // ─── Section ─────────────────────────────────────────────────────────────────
55
+
56
+ export interface SectionProps {
57
+ title?: ReactNode;
58
+ color?: SectionColor;
59
+ icon?: ReactNode;
60
+ children: ReactNode;
61
+ }
62
+
63
+ /** `FormBuilder.Section` — a titled Glare SectionBlock grouping fields. */
64
+ function Section({ title, color, icon, children }: SectionProps) {
65
+ return (
66
+ <SectionBlock title={title} color={color} icon={icon}>
67
+ {children}
68
+ </SectionBlock>
69
+ );
70
+ }
71
+
72
+ function isHeaderElement(node: React.ReactNode): node is React.ReactElement {
73
+ return (
74
+ React.isValidElement(node) &&
75
+ (node.type as { __isFormHeader?: boolean })?.__isFormHeader === true
76
+ );
77
+ }
78
+
79
+ // ─── Root ────────────────────────────────────────────────────────────────────
80
+
81
+ function FormBuilderRoot<T extends FieldValues = FieldValues>({
82
+ children,
83
+ id,
84
+ form: formProp,
85
+ onSubmit,
86
+ onInvalid,
87
+ resolver,
88
+ defaultValues,
89
+ values,
90
+ loading = false,
91
+ fieldDirection,
92
+ resetOnSuccess,
93
+ conclusion,
94
+ className,
95
+ }: FormBuilderRootProps<T>) {
96
+ // Hooks can't be conditional, so always create one; `formProp` wins when given
97
+ // (the caller hoisted `useForm` to share values with something outside the form).
98
+ const ownForm = useForm<T>({ resolver, defaultValues, values });
99
+ const form = formProp ?? ownForm;
100
+ // Don't force a direction — left undefined, FieldSection uses its responsive `flexible`
101
+ // layout (stacked on small screens, two-column at `lg`). Only a caller-set `fieldDirection`
102
+ // (e.g. vertical inside a drawer) pins it.
103
+ const direction = fieldDirection;
104
+
105
+ const handleValid = async (v: T) => {
106
+ await onSubmit(v);
107
+ if (resetOnSuccess) form.reset();
108
+ };
109
+
110
+ // Split out a FormBuilder.Header (renders absolutely) and detect a Stepper child.
111
+ const childArray = React.Children.toArray(children);
112
+ const header = childArray.find(isHeaderElement);
113
+ const rest = childArray.filter((n) => !isHeaderElement(n));
114
+
115
+ const stepperEl = rest.find(isStepperElement);
116
+ const stepChildren = stepperEl ? React.Children.toArray(stepperEl.props.children) : [];
117
+ const steps = stepChildren.filter(isStepElement);
118
+ const stepExtras = stepChildren.filter((n) => !isStepElement(n));
119
+ const isStepper = !!stepperEl;
120
+
121
+ // Stepper state is lifted HERE so the nav can live in its own grid column, outside the
122
+ // `<form>`. Called unconditionally (inert when there are no steps) to keep hooks order stable.
123
+ const stepper = useStepperState(steps, form.trigger as Parameters<typeof useStepperState>[1]);
124
+
125
+ // The stepper nav is its own column beside the fields, inside the form surface.
126
+ const nav = isStepper ? <StepperNav /> : null;
127
+
128
+ // The fields the `<form>` wraps: the stepper's steps (+ any custom footer extras like
129
+ // Back/Next), or the plain children. The Submit itself lives outside the form (see FormRenderer).
130
+ const fields = isStepper ? (
131
+ <>
132
+ {steps.map((step, i) => (
133
+ <StepSlot key={i} index={i} active={i === stepper.currentStep}>
134
+ {step.props.children}
135
+ </StepSlot>
136
+ ))}
137
+ {stepExtras}
138
+ </>
139
+ ) : (
140
+ rest
141
+ );
142
+
143
+ // The fields column caps at 1200px and centers — as the middle column of the grid, and
144
+ // standalone.
145
+ const fieldsInner = (
146
+ <div className="mx-auto flex w-full max-w-[1100px] flex-col gap-4 px-[48px]">{fields}</div>
147
+ );
148
+
149
+ const formEl = (
150
+ <form id={id} className="w-full min-w-0" onSubmit={form.handleSubmit(handleValid, onInvalid)}>
151
+ {fieldsInner}
152
+ </form>
153
+ );
154
+
155
+ // Inside the form surface: the stepper rail beside the fields. Columns never wrap — the
156
+ // layout stays side-by-side at every screen size (the fields column shrinks instead).
157
+ const bodyInner = nav ? (
158
+ <div className="grid w-full grid-cols-[1fr_minmax(0,1100px)_1fr] gap-8">
159
+ {nav}
160
+ {formEl}
161
+ {/* Empty third column — balances the rail's gutter so the middle column is centred. */}
162
+ <div />
163
+ </div>
164
+ ) : (
165
+ formEl
166
+ );
167
+
168
+ const surface = header ? (
169
+ // Scroll shell: the absolute header floats over the scrollable body. No fixed height — it
170
+ // fills whatever the parent gives it (like the drawer's panel fills its tray) and the body
171
+ // scrolls internally.
172
+ <div className="relative isolate flex h-full w-full flex-col overflow-hidden rounded-2xl bg-background-presentation-body-primary">
173
+ {header}
174
+ <div className="relative z-[1] flex min-h-0 w-full flex-1 flex-col overflow-y-auto px-6 py-6 pt-[72px] scrollbar-hide">
175
+ {bodyInner}
176
+ </div>
177
+ </div>
178
+ ) : (
179
+ bodyInner
180
+ );
181
+
182
+ // The conclusion lives OUTSIDE the form surface — its own panel beside it, exactly like the
183
+ // drawer's tray (`FormDrawer` puts the conclusion next to the form panel with a 6px gutter).
184
+ // It never wraps below the form: the two stay side-by-side at every screen size.
185
+ const body = conclusion ? (
186
+ <div className="flex h-full flex-row items-stretch gap-[6px]">
187
+ <div className="min-h-0 min-w-0 flex-1">{surface}</div>
188
+ <div className="flex min-h-0">{conclusion}</div>
189
+ </div>
190
+ ) : (
191
+ surface
192
+ );
193
+
194
+ // `className` lands on the OUTERMOST element — the one a parent lays out (e.g. `flex-1 min-h-0`
195
+ // to fill a flex column). `h-full` fills a parent that has a definite height.
196
+ const outerClassName = cn("h-full w-full @container", className);
197
+
198
+ const tree = (
199
+ <Form {...form}>
200
+ <div className={outerClassName}>{body}</div>
201
+ </Form>
202
+ );
203
+
204
+ return (
205
+ <FormIdContext.Provider value={id}>
206
+ <LoadingContext.Provider value={loading}>
207
+ <DirectionContext.Provider value={direction}>
208
+ {isStepper ? (
209
+ <StepperContext.Provider value={stepper}>{tree}</StepperContext.Provider>
210
+ ) : (
211
+ tree
212
+ )}
213
+ </DirectionContext.Provider>
214
+ </LoadingContext.Provider>
215
+ </FormIdContext.Provider>
216
+ );
217
+ }
218
+
219
+ /**
220
+ * FormBuilder — a compound, composition-based form. Author a form as JSX:
221
+ *
222
+ * ```tsx
223
+ * <FormBuilder onSubmit={fn} resolver={r} defaultValues={d}>
224
+ * <FormBuilder.Section title="Identity" color="Blue">
225
+ * <FormBuilder.Text name="name" label="Name" required />
226
+ * </FormBuilder.Section>
227
+ * <FormBuilder.Submit>Save</FormBuilder.Submit>
228
+ * </FormBuilder>
229
+ * ```
230
+ *
231
+ * Steps are components (`FormBuilder.Stepper` + `FormBuilder.Step`). FormBuilder is
232
+ * drawer-unaware — to show a form in a drawer, wrap it in `FormRenderer`'s `FormDrawer`
233
+ * (or use `FormRenderer` with `display: "drawer"`).
234
+ */
235
+ export const FormBuilder = Object.assign(FormBuilderRoot, {
236
+ // fields
237
+ Text: TextField,
238
+ Email: EmailField,
239
+ Password: PasswordField,
240
+ Number: NumberField,
241
+ Currency: CurrencyField,
242
+ Textarea: TextareaField,
243
+ Select: SelectField,
244
+ SearchableSelect: SearchableSelectField,
245
+ MultiSelect: MultiSelectField,
246
+ Tags: MultiSelectField,
247
+ RadioList: RadioListField,
248
+ RadioCards: RadioCardsField,
249
+ Checkbox: CheckboxField,
250
+ CheckboxGroup: CheckboxGroupField,
251
+ SwitchBox: SwitchBoxField,
252
+ Otp: OtpField,
253
+ TreeSelect: TreeSelectField,
254
+ Slider: SliderField,
255
+ Color: ColorField,
256
+ Signature: SignatureField,
257
+ Phone: PhoneField,
258
+ FieldArray: FieldArray,
259
+ Table: TableField,
260
+ Date: DateField,
261
+ DateRange: (props: Parameters<typeof DateField>[0]) => <DateField {...props} mode="range" />,
262
+ DateMultiple: (props: Parameters<typeof DateField>[0]) => (
263
+ <DateField {...props} mode="multiple" />
264
+ ),
265
+ DateTime: (props: Parameters<typeof DateField>[0]) => <DateField {...props} timePicker />,
266
+ File: FileField,
267
+ Image: (props: Parameters<typeof FileField>[0]) => <FileField {...props} image />,
268
+ RichText: RichTextField,
269
+ Custom: CustomField,
270
+ // layout
271
+ Section,
272
+ Header,
273
+ Submit: SubmitButton,
274
+ // stepper
275
+ Stepper,
276
+ Step,
277
+ Back,
278
+ Next,
279
+ });