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,308 @@
1
+ "use client";
2
+
3
+ import { type ReactNode, useState } from "react";
4
+ import { useFormContext, useFieldArray, type FieldValues, type ArrayPath } from "react-hook-form";
5
+ import { CSS } from "@dnd-kit/utilities";
6
+ import {
7
+ DndContext,
8
+ closestCenter,
9
+ KeyboardSensor,
10
+ PointerSensor,
11
+ useSensor,
12
+ useSensors,
13
+ type DragEndEvent,
14
+ } from "@dnd-kit/core";
15
+ import {
16
+ SortableContext,
17
+ sortableKeyboardCoordinates,
18
+ useSortable,
19
+ verticalListSortingStrategy,
20
+ } from "@dnd-kit/sortable";
21
+
22
+ import { cn } from "../../../utils/cn";
23
+ import { SectionBlock } from "../../SectionBlock";
24
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../Table";
25
+ import { Checkbox } from "../../Checkbox";
26
+ import { Button } from "../../Button";
27
+ import { CellContext, useLoading } from "../context";
28
+ import type { TableColumn, TableFieldProps } from "../types";
29
+
30
+ const ALIGN: Record<NonNullable<TableColumn["align"]>, string> = {
31
+ start: "justify-start text-start",
32
+ center: "justify-center text-center",
33
+ end: "justify-end text-end",
34
+ };
35
+
36
+ /**
37
+ * `FormBuilder.Table` — an editable data-grid field built on the FieldArray pattern.
38
+ * Rows are records; each column cell is any `FormBuilder.*` field, rendered "bare" via
39
+ * `CellContext`. Rows can be checkbox-selected, drag-reordered, added and removed.
40
+ */
41
+ export function TableField(props: TableFieldProps) {
42
+ const { name, columns, selectable = true, reorderable = true } = props;
43
+ const form = useFormContext<FieldValues>();
44
+ const loading = useLoading();
45
+
46
+ const { fields, append, remove, move, replace } = useFieldArray({
47
+ control: form.control,
48
+ name: name as ArrayPath<FieldValues>,
49
+ });
50
+
51
+ const [selected, setSelected] = useState<Set<string>>(new Set());
52
+ const [sort, setSort] = useState<{ key: string; dir: "asc" | "desc" } | null>(null);
53
+
54
+ const sensors = useSensors(
55
+ useSensor(PointerSensor),
56
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
57
+ );
58
+
59
+ if (props.hidden) return null;
60
+
61
+ const showHandle = reorderable;
62
+ const showSelect = selectable;
63
+
64
+ const toggleRow = (id: string) =>
65
+ setSelected((prev) => {
66
+ const next = new Set(prev);
67
+ if (next.has(id)) next.delete(id);
68
+ else next.add(id);
69
+ return next;
70
+ });
71
+
72
+ const allSelected = fields.length > 0 && fields.every((f) => selected.has(f.id));
73
+ const someSelected = fields.some((f) => selected.has(f.id));
74
+ const toggleAll = () => setSelected(allSelected ? new Set() : new Set(fields.map((f) => f.id)));
75
+
76
+ const deleteSelected = () => {
77
+ if (loading) return;
78
+ const indices = fields.map((f, i) => (selected.has(f.id) ? i : -1)).filter((i) => i >= 0);
79
+ remove(indices);
80
+ setSelected(new Set());
81
+ };
82
+
83
+ const onDragEnd = (event: DragEndEvent) => {
84
+ const { active, over } = event;
85
+ if (!over || active.id === over.id) return;
86
+ const oldIndex = fields.findIndex((f) => f.id === active.id);
87
+ const newIndex = fields.findIndex((f) => f.id === over.id);
88
+ if (oldIndex >= 0 && newIndex >= 0) move(oldIndex, newIndex);
89
+ };
90
+
91
+ // Sort the rows by a column's key (asc/desc toggle). `replace` re-keys the field ids,
92
+ // so selection (keyed by id) is cleared afterwards.
93
+ const applySort = (key: string) => {
94
+ if (loading) return;
95
+ const dir: "asc" | "desc" = sort?.key === key && sort.dir === "asc" ? "desc" : "asc";
96
+ const rows = (form.getValues(name) as Record<string, unknown>[] | undefined) ?? [];
97
+ const sorted = [...rows].sort((a, b) => {
98
+ const av = a?.[key];
99
+ const bv = b?.[key];
100
+ if (av == null) return bv == null ? 0 : 1;
101
+ if (bv == null) return -1;
102
+ const cmp =
103
+ typeof av === "number" && typeof bv === "number"
104
+ ? av - bv
105
+ : String(av).localeCompare(String(bv));
106
+ return dir === "asc" ? cmp : -cmp;
107
+ });
108
+ replace(sorted);
109
+ setSort({ key, dir });
110
+ setSelected(new Set());
111
+ };
112
+
113
+ const totalCols = columns.length + (showHandle ? 1 : 0) + (showSelect ? 1 : 0);
114
+
115
+ const rowCells = (rowName: string, index: number) => (
116
+ <>
117
+ {showSelect && (
118
+ <TableCell isDummy>
119
+ <Checkbox
120
+ size="S"
121
+ checked={selected.has(fields[index].id)}
122
+ onCheckedChange={() => toggleRow(fields[index].id)}
123
+ />
124
+ </TableCell>
125
+ )}
126
+ {columns.map((col, ci) => (
127
+ <TableCell
128
+ key={ci}
129
+ style={col.width ? { width: col.width } : undefined}
130
+ childrenClassName={cn(
131
+ "min-w-0 w-full [&:has(input)]:bg-transparent",
132
+ col.align && ALIGN[col.align],
133
+ )}
134
+ >
135
+ <CellContext.Provider value={true}>{col.cell(rowName, index)}</CellContext.Provider>
136
+ </TableCell>
137
+ ))}
138
+ </>
139
+ );
140
+
141
+ const table = (
142
+ <Table>
143
+ <TableHeader>
144
+ <TableRow>
145
+ {showHandle && <TableHead isDummy />}
146
+ {showSelect && (
147
+ <TableHead isDummy>
148
+ <Checkbox
149
+ size="S"
150
+ checked={allSelected ? true : someSelected ? "indeterminate" : false}
151
+ onCheckedChange={toggleAll}
152
+ />
153
+ </TableHead>
154
+ )}
155
+ {columns.map((col, ci) => (
156
+ <TableHead
157
+ key={ci}
158
+ sortType={col.sortKey && sort?.key === col.sortKey ? sort.dir : undefined}
159
+ onSort={col.sortKey ? () => applySort(col.sortKey as string) : undefined}
160
+ >
161
+ {col.header}
162
+ </TableHead>
163
+ ))}
164
+ </TableRow>
165
+ </TableHeader>
166
+
167
+ <TableBody>
168
+ {showHandle ? (
169
+ <SortableContext items={fields.map((f) => f.id)} strategy={verticalListSortingStrategy}>
170
+ {fields.map((f, index) => (
171
+ <SortableRow key={f.id} id={f.id} state={selected.has(f.id) ? "selected" : undefined}>
172
+ {rowCells(`${name}.${index}`, index)}
173
+ </SortableRow>
174
+ ))}
175
+ </SortableContext>
176
+ ) : (
177
+ fields.map((f, index) => (
178
+ <TableRow key={f.id} state={selected.has(f.id) ? "selected" : undefined}>
179
+ {rowCells(`${name}.${index}`, index)}
180
+ </TableRow>
181
+ ))
182
+ )}
183
+
184
+ {fields.length === 0 && (
185
+ <TableRow>
186
+ <TableCell colSpan={totalCols}>
187
+ <span className="typography-body-small-regular text-content-presentation-global-secondary">
188
+ No rows yet.
189
+ </span>
190
+ </TableCell>
191
+ </TableRow>
192
+ )}
193
+
194
+ <TableRow className="h-[40px]">
195
+ <TableCell
196
+ colSpan={totalCols}
197
+ className="border-t-2 border-transparent hover:border-border-presentation-table-action-hover hover:bg-background-presentation-table-acton-hover"
198
+ >
199
+ <button
200
+ type="button"
201
+ disabled={loading}
202
+ onClick={() => append((props.defaultItem ?? {}) as never)}
203
+ className="flex w-full items-center justify-start gap-2 typography-body-medium-semibold text-content-presentation-action-light-primary [&_i]:text-[20px]"
204
+ >
205
+ <i className="ri-add-line" />
206
+ {props.addLabel ?? "Add New"}
207
+ </button>
208
+ </TableCell>
209
+ </TableRow>
210
+ </TableBody>
211
+ </Table>
212
+ );
213
+
214
+ // Header actions on the right of the SectionBlock title row: a "Delete Row" that's disabled
215
+ // until rows are selected, and a primary "Add New".
216
+ const headerActions = (
217
+ <>
218
+ {selectable && (
219
+ <Button
220
+ type="button"
221
+ size="S"
222
+ variant="BorderStyle"
223
+ disabled={loading || selected.size === 0}
224
+ onClick={deleteSelected}
225
+ >
226
+ <i className="ri-delete-bin-line" />
227
+ Delete Row
228
+ </Button>
229
+ )}
230
+ <Button
231
+ type="button"
232
+ size="S"
233
+ variant="BluColStyle"
234
+ disabled={loading}
235
+ onClick={() => append((props.defaultItem ?? {}) as never)}
236
+ >
237
+ <i className="ri-add-line" />
238
+ {props.addLabel ?? "Add New"}
239
+ </Button>
240
+ </>
241
+ );
242
+
243
+ return (
244
+ <SectionBlock
245
+ title={props.title}
246
+ color={props.color}
247
+ icon={props.icon}
248
+ action={headerActions}
249
+ bodyClassName="px-[16px]"
250
+ // The bottom "Add New" footer is the last element, so drop SectionBlock's default
251
+ // bottom padding to keep the table flush with the card edge (matches the Figma).
252
+ containerClassName="pb-0"
253
+ >
254
+ <div className="flex w-full flex-col gap-2">
255
+ {props.description && (
256
+ <p className="typography-body-small-regular text-content-presentation-global-secondary">
257
+ {props.description}
258
+ </p>
259
+ )}
260
+
261
+ <div className="w-full overflow-x-auto">
262
+ {showHandle ? (
263
+ <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
264
+ {table}
265
+ </DndContext>
266
+ ) : (
267
+ table
268
+ )}
269
+ </div>
270
+ </div>
271
+ </SectionBlock>
272
+ );
273
+ }
274
+
275
+ /** A draggable table row — the drag handle lives in the leading `isDummy` cell. */
276
+ function SortableRow({
277
+ id,
278
+ state,
279
+ children,
280
+ }: {
281
+ id: string;
282
+ state?: "selected";
283
+ children: ReactNode;
284
+ }) {
285
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
286
+ id,
287
+ });
288
+
289
+ const style = {
290
+ transform: CSS.Transform.toString(transform),
291
+ transition,
292
+ opacity: isDragging ? 0.5 : 1,
293
+ };
294
+
295
+ return (
296
+ <TableRow ref={setNodeRef} style={style} state={state}>
297
+ <TableCell
298
+ {...attributes}
299
+ {...listeners}
300
+ className="cursor-grab active:cursor-grabbing"
301
+ isDummy
302
+ >
303
+ <i className="ri-draggable text-[18px] text-content-presentation-global-secondary" />
304
+ </TableCell>
305
+ {children}
306
+ </TableRow>
307
+ );
308
+ }
@@ -0,0 +1,213 @@
1
+ "use client";
2
+
3
+ import { ReactNode, useEffect, useLayoutEffect, useRef, useState, type ChangeEvent } from "react";
4
+ import type { ControllerRenderProps, FieldValues } from "react-hook-form";
5
+
6
+ import { InputField } from "../../InputField";
7
+ import { Textarea } from "../../Textarea";
8
+ import { PasswordLevel } from "../../PasswordLevel";
9
+ import { useLoading, useCell } from "../context";
10
+ import { formatNumber } from "../numberFormat";
11
+ import type { BaseFieldProps, CurrencyFieldProps, PasswordFieldProps } from "../types";
12
+ import { FieldShell } from "./FieldShell";
13
+
14
+ // Run before paint on the client; fall back to useEffect on the server (no SSR warning).
15
+ const useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
16
+
17
+ /**
18
+ * A number input that shows a **live** thousands-separated value (e.g. "1,299")
19
+ * while storing the raw `number` in the form. Commas are inserted as you type and
20
+ * the caret is preserved; decimals (and a trailing ".") are kept intact.
21
+ */
22
+ function NumberInput({
23
+ field,
24
+ icon,
25
+ placeholder,
26
+ disabled,
27
+ onTable,
28
+ }: {
29
+ field: ControllerRenderProps<FieldValues, string>;
30
+ icon?: ReactNode;
31
+ placeholder?: string;
32
+ disabled?: boolean;
33
+ onTable?: boolean;
34
+ }) {
35
+ const inputRef = useRef<HTMLInputElement | null>(null);
36
+ const caretRef = useRef<number | null>(null);
37
+ const [text, setText] = useState<string>(() => formatNumber(field.value));
38
+
39
+ // Reflect external value changes (e.g. edit data loading) while not editing.
40
+ useEffect(() => {
41
+ if (document.activeElement === inputRef.current) return;
42
+ setText(formatNumber(field.value));
43
+ }, [field.value]);
44
+
45
+ // Restore the caret after a live reformat (commas shift positions).
46
+ useIsoLayoutEffect(() => {
47
+ if (caretRef.current != null && inputRef.current) {
48
+ inputRef.current.setSelectionRange(caretRef.current, caretRef.current);
49
+ caretRef.current = null;
50
+ }
51
+ });
52
+
53
+ const setRefs = (el: HTMLInputElement | null) => {
54
+ inputRef.current = el;
55
+ if (typeof field.ref === "function") field.ref(el);
56
+ };
57
+
58
+ const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
59
+ const raw = e.target.value;
60
+ const caret = e.target.selectionStart ?? raw.length;
61
+ const digitsBeforeCaret = raw.slice(0, caret).replace(/\D/g, "").length;
62
+
63
+ // Keep digits and a single decimal point.
64
+ let cleaned = raw.replace(/[^\d.]/g, "");
65
+ const dot = cleaned.indexOf(".");
66
+ if (dot !== -1) {
67
+ cleaned = cleaned.slice(0, dot + 1) + cleaned.slice(dot + 1).replace(/\./g, "");
68
+ }
69
+
70
+ let formatted = "";
71
+ if (cleaned !== "") {
72
+ const [intPart, decPart] = cleaned.split(".");
73
+ const intFmt = intPart === "" ? "" : Number(intPart).toLocaleString("en-US");
74
+ formatted = decPart !== undefined ? `${intFmt}.${decPart}` : intFmt;
75
+ }
76
+
77
+ // Caret goes after the same count of digits, skipping the inserted commas.
78
+ let pos = 0;
79
+ let seen = 0;
80
+ while (pos < formatted.length && seen < digitsBeforeCaret) {
81
+ if (/\d/.test(formatted[pos])) seen++;
82
+ pos++;
83
+ }
84
+ caretRef.current = pos;
85
+
86
+ setText(formatted);
87
+ field.onChange(cleaned === "" || cleaned === "." ? undefined : Number(cleaned));
88
+ };
89
+
90
+ return (
91
+ <InputField
92
+ name={field.name}
93
+ ref={setRefs}
94
+ type="text"
95
+ inputMode="decimal"
96
+ icon={icon}
97
+ value={text}
98
+ placeholder={placeholder}
99
+ disabled={disabled}
100
+ onTable={onTable}
101
+ className="w-full"
102
+ onChange={handleChange}
103
+ onBlur={() => field.onBlur()}
104
+ />
105
+ );
106
+ }
107
+
108
+ function TextLike({ props, type }: { props: BaseFieldProps; type: "text" | "email" | "password" }) {
109
+ const loading = useLoading();
110
+ const cell = useCell();
111
+ return (
112
+ <FieldShell {...props}>
113
+ {(field) => (
114
+ <InputField
115
+ {...field}
116
+ type={type}
117
+ value={field.value ?? ""}
118
+ placeholder={props.placeholder}
119
+ disabled={props.disabled || loading}
120
+ onTable={cell}
121
+ className="w-full"
122
+ />
123
+ )}
124
+ </FieldShell>
125
+ );
126
+ }
127
+
128
+ export function TextField(props: BaseFieldProps) {
129
+ return <TextLike props={props} type="text" />;
130
+ }
131
+ export function EmailField(props: BaseFieldProps) {
132
+ return <TextLike props={props} type="email" />;
133
+ }
134
+ export function PasswordField(props: PasswordFieldProps) {
135
+ const loading = useLoading();
136
+ const cell = useCell();
137
+ return (
138
+ <FieldShell {...props}>
139
+ {(field) => (
140
+ <div className="flex w-full flex-col gap-2">
141
+ <InputField
142
+ {...field}
143
+ type="password"
144
+ value={field.value ?? ""}
145
+ placeholder={props.placeholder}
146
+ disabled={props.disabled || loading}
147
+ onTable={cell}
148
+ className="w-full"
149
+ />
150
+ {props.strengthMeter && <PasswordLevel value={field.value ?? ""} />}
151
+ </div>
152
+ )}
153
+ </FieldShell>
154
+ );
155
+ }
156
+
157
+ export function NumberField(props: BaseFieldProps) {
158
+ const loading = useLoading();
159
+ const cell = useCell();
160
+ return (
161
+ <FieldShell {...props}>
162
+ {(field) => (
163
+ <NumberInput
164
+ field={field}
165
+ placeholder={props.placeholder}
166
+ disabled={props.disabled || loading}
167
+ onTable={cell}
168
+ />
169
+ )}
170
+ </FieldShell>
171
+ );
172
+ }
173
+
174
+ export function CurrencyField(props: CurrencyFieldProps) {
175
+ const loading = useLoading();
176
+ const cell = useCell();
177
+ return (
178
+ <FieldShell {...props}>
179
+ {(field) => (
180
+ <NumberInput
181
+ field={field}
182
+ icon={
183
+ props.currencySymbol ? (
184
+ <span className="typography-body-small-medium">{props.currencySymbol}</span>
185
+ ) : undefined
186
+ }
187
+ placeholder={props.placeholder}
188
+ disabled={props.disabled || loading}
189
+ onTable={cell}
190
+ />
191
+ )}
192
+ </FieldShell>
193
+ );
194
+ }
195
+
196
+ export function TextareaField(props: BaseFieldProps & { rows?: number }) {
197
+ const loading = useLoading();
198
+ return (
199
+ <FieldShell {...props}>
200
+ {(field, fieldState) => (
201
+ <Textarea
202
+ {...field}
203
+ value={field.value ?? ""}
204
+ rows={props.rows}
205
+ placeholder={props.placeholder}
206
+ disabled={props.disabled || loading}
207
+ state={fieldState.error ? "negative" : undefined}
208
+ className="w-full"
209
+ />
210
+ )}
211
+ </FieldShell>
212
+ );
213
+ }
@@ -0,0 +1,43 @@
1
+ "use client";
2
+
3
+ import { SearchableTree } from "../../SearchableTree";
4
+ import type { TreeSelectFieldProps } from "../types";
5
+ import { FieldShell } from "./FieldShell";
6
+
7
+ /**
8
+ * `FormBuilder.TreeSelect` — hierarchical single-select (Glare `SearchableTree`).
9
+ * Stores the selected node's id; seeds the picker + view label by finding the
10
+ * node back from that id.
11
+ */
12
+ export function TreeSelectField<T>(props: TreeSelectFieldProps<T>) {
13
+ const findById = (id: unknown): T | null => {
14
+ const target = String(id ?? "");
15
+ if (!target) return null;
16
+ // Works for both nested (getNodeChildren) and flat node lists.
17
+ const stack: T[] = [...props.nodes];
18
+ while (stack.length) {
19
+ const node = stack.pop() as T;
20
+ if (props.getNodeId(node) === target) return node;
21
+ const kids = props.getNodeChildren?.(node);
22
+ if (kids && kids.length) stack.push(...kids);
23
+ }
24
+ return null;
25
+ };
26
+
27
+ return (
28
+ <FieldShell {...props}>
29
+ {(field) => (
30
+ <SearchableTree
31
+ nodes={props.nodes}
32
+ getNodeId={props.getNodeId}
33
+ getNodeLabel={props.getNodeLabel}
34
+ getNodeChildren={props.getNodeChildren}
35
+ parentIdKey={props.parentIdKey as (keyof T & string) | undefined}
36
+ selectableFolders={props.selectableFolders}
37
+ value={findById(field.value)}
38
+ onSelect={(node) => field.onChange(props.getNodeId(node))}
39
+ />
40
+ )}
41
+ </FieldShell>
42
+ );
43
+ }