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,174 @@
1
+ "use client";
2
+
3
+ import { ReactNode, useEffect } from "react";
4
+ import {
5
+ useFormContext,
6
+ useFormState,
7
+ type ControllerRenderProps,
8
+ type ControllerFieldState,
9
+ type FieldErrors,
10
+ type FieldValues,
11
+ type FieldPath,
12
+ } from "react-hook-form";
13
+
14
+ import { FieldSection } from "../../../layouts/FieldSection";
15
+ import { FormField, FormItem, FormControl } from "../../Form";
16
+ import { FieldHint } from "../../FieldHint";
17
+ import { Tooltip } from "../../Tooltip";
18
+ import { DisplayField } from "../DisplayField";
19
+ import { useMode, useDirection, useStepRegistry, useCell } from "../context";
20
+ import type { FieldView } from "../viewFormat";
21
+
22
+ export interface FieldShellProps {
23
+ name: string;
24
+ label?: ReactNode;
25
+ description?: ReactNode;
26
+ required?: boolean;
27
+ fullWidth?: boolean;
28
+ hidden?: boolean;
29
+ /** Force the field's layout direction, overriding the form's `useDirection()` context. */
30
+ direction?: "horizontal" | "vertical" | "flexible";
31
+ /** Edit-mode input, wired to the react-hook-form field. */
32
+ children: (
33
+ field: ControllerRenderProps<FieldValues, string>,
34
+ fieldState: ControllerFieldState,
35
+ ) => ReactNode;
36
+ /** Read-only rendering (view mode) computed from the current value. */
37
+ view?: (value: unknown) => FieldView;
38
+ }
39
+
40
+ /**
41
+ * Shared wrapper for every `FormBuilder.*` field: the FieldSection row + the RHF
42
+ * `FormField`/`FormItem`/`FormControl`/`FormMessage` scaffolding in edit mode, or
43
+ * `DisplayField` in view mode. Also registers the field name into the enclosing
44
+ * `FormBuilder.Step` (if any) so the stepper can validate per step.
45
+ */
46
+ export function FieldShell({
47
+ name,
48
+ label,
49
+ description,
50
+ required,
51
+ fullWidth,
52
+ hidden,
53
+ direction: directionProp,
54
+ children,
55
+ view,
56
+ }: FieldShellProps) {
57
+ const form = useFormContext();
58
+ const mode = useMode();
59
+ const cell = useCell();
60
+ const ctxDirection = useDirection();
61
+ // A field may pin its own direction (e.g. RichText forces vertical), else the form's. When
62
+ // neither is set this stays `undefined` — FieldSection then falls back to its responsive
63
+ // `flexible` layout rather than a fixed two-column one.
64
+ const direction = directionProp ?? ctxDirection;
65
+ const step = useStepRegistry();
66
+
67
+ // Read this field's error at the shell level (not inside the FormField render) so
68
+ // the FieldHint can go in FieldSection's `childrenUnderLabel` — under the label.
69
+ // Subscribe to the whole `errors` object and resolve the field's path ourselves:
70
+ // `getFieldState(name)` misses controls that register late (DatePicker, RichText),
71
+ // whereas the errors object always carries every failing key from the resolver.
72
+ const { errors } = useFormState({ control: form.control });
73
+ const fieldError = resolveFieldError(errors, name);
74
+
75
+ useEffect(() => {
76
+ if (!step) return;
77
+ step.register(name);
78
+ return () => step.unregister(name);
79
+ }, [step, name]);
80
+
81
+ if (hidden) return null;
82
+
83
+ // Cell mode (inside FormBuilder.Table): render just the control — no FieldSection label/row.
84
+ // Errors surface as a tooltip on the control rather than a stacked FieldHint, so a row stays
85
+ // one line tall. Step registration + view formatting above still apply.
86
+ if (cell) {
87
+ if (mode === "view") {
88
+ const value = form.getValues(name);
89
+ const v = view ? view(value) : { value: value == null ? "" : String(value) };
90
+ return <DisplayField value={v.value} valueNode={v.valueNode} />;
91
+ }
92
+ return (
93
+ <FormField
94
+ control={form.control}
95
+ name={name as FieldPath<FieldValues>}
96
+ render={({ field, fieldState }) => (
97
+ <FormItem className="w-full">
98
+ <FormControl>
99
+ <Tooltip
100
+ open={Boolean(fieldError)}
101
+ text={fieldError ?? ""}
102
+ toolTipSide="top"
103
+ variant="highlight"
104
+ >
105
+ <div className="w-full">{children(field, fieldState)}</div>
106
+ </Tooltip>
107
+ </FormControl>
108
+ </FormItem>
109
+ )}
110
+ />
111
+ );
112
+ }
113
+
114
+ if (mode === "view") {
115
+ const value = form.getValues(name);
116
+ const v = view ? view(value) : { value: value == null ? "" : String(value) };
117
+ return (
118
+ <FieldSection
119
+ label={label}
120
+ secondaryLabel={description}
121
+ direction={direction}
122
+ className={fullWidth ? "max-w-full" : undefined}
123
+ >
124
+ <DisplayField value={v.value} valueNode={v.valueNode} />
125
+ </FieldSection>
126
+ );
127
+ }
128
+
129
+ return (
130
+ <FieldSection
131
+ label={label}
132
+ requiredLabel={required ? "(Required)" : undefined}
133
+ secondaryLabel={description}
134
+ direction={direction}
135
+ className={fullWidth ? "max-w-full" : undefined}
136
+ childrenUnderLabel={<FieldError message={fieldError} />}
137
+ >
138
+ <FormField
139
+ control={form.control}
140
+ name={name as FieldPath<FieldValues>}
141
+ render={({ field, fieldState }) => (
142
+ <FormItem className="w-full">
143
+ <FormControl>
144
+ <div className="w-full">{children(field, fieldState)}</div>
145
+ </FormControl>
146
+ </FormItem>
147
+ )}
148
+ />
149
+ </FieldSection>
150
+ );
151
+ }
152
+
153
+ /** Validation error, shown as a `FieldHint` alert (no tooltip); null when there's none. */
154
+ function FieldError({ message }: { message?: string }) {
155
+ if (!message) return null;
156
+ return <FieldHint state="error" label={message} />;
157
+ }
158
+
159
+ /**
160
+ * Resolve a field's error message from the RHF `errors` tree by its dotted `name`
161
+ * (handles nested field-array paths like `contacts.0.email`). Reading the errors
162
+ * object directly is more reliable than `getFieldState` for controls that register
163
+ * late (DatePicker, RichText), whose hints were otherwise missing.
164
+ */
165
+ function resolveFieldError(errors: FieldErrors, name: string): string | undefined {
166
+ const node = name
167
+ .split(".")
168
+ .reduce<unknown>(
169
+ (acc, key) => (acc == null ? undefined : (acc as Record<string, unknown>)[key]),
170
+ errors,
171
+ );
172
+ const message = (node as { message?: unknown } | undefined)?.message;
173
+ return typeof message === "string" ? message : undefined;
174
+ }
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { ImageAttachment } from "../../ImageAttachment";
4
+ import { useLoading } from "../context";
5
+ import { formatFieldView } from "../viewFormat";
6
+ import type { FileFieldProps } from "../types";
7
+ import { FieldShell } from "./FieldShell";
8
+
9
+ /** `FormBuilder.File` / `.Image` — stores the selected `File` (consumer uploads). */
10
+ export function FileField(props: FileFieldProps & { image?: boolean }) {
11
+ const loading = useLoading();
12
+ return (
13
+ <FieldShell {...props} view={(v) => formatFieldView({ kind: "file", value: v })}>
14
+ {(field) => {
15
+ const current: unknown = field.value;
16
+ const fileName = Array.isArray(current)
17
+ ? `${current.length} file(s)`
18
+ : current instanceof File
19
+ ? current.name
20
+ : "";
21
+ return (
22
+ <ImageAttachment
23
+ mainLabel={props.placeholder ?? (props.image ? "Upload image" : "Upload file")}
24
+ secondaryLabel={
25
+ fileName || (typeof props.description === "string" ? props.description : "")
26
+ }
27
+ accept={props.accept ?? (props.image ? "image/*" : undefined)}
28
+ multiple={props.multiple}
29
+ disabled={props.disabled || loading}
30
+ onChange={(e) => {
31
+ const files = (e.target as HTMLInputElement).files;
32
+ if (!files || files.length === 0) return;
33
+ field.onChange(props.multiple ? Array.from(files) : files[0]);
34
+ }}
35
+ />
36
+ );
37
+ }}
38
+ </FieldShell>
39
+ );
40
+ }
@@ -0,0 +1,139 @@
1
+ "use client";
2
+
3
+ import { ReactNode } from "react";
4
+
5
+ import { cn } from "../../../utils/cn";
6
+ import { Label } from "../../Label";
7
+ import { RadioGroup, Radio } from "../../Radio";
8
+ import { Checkbox } from "../../Checkbox";
9
+ import { useLoading } from "../context";
10
+ import { formatFieldView } from "../viewFormat";
11
+ import type { OptionItem, OptionsFieldProps } from "../types";
12
+ import { FieldShell } from "./FieldShell";
13
+
14
+ /**
15
+ * The `#f9f9f9` rounded box that holds the option rows, with a full-width divider
16
+ * between each (design nodes 8999:170512 / 8999:170546).
17
+ */
18
+ function OptionBox({ children }: { children: ReactNode }) {
19
+ return (
20
+ <div
21
+ className={cn(
22
+ "flex w-full flex-col rounded-[8px] px-[10px]",
23
+ "bg-background-presentation-form-field-primary",
24
+ "divide-y divide-border-presentation-global-primary",
25
+ )}
26
+ >
27
+ {children}
28
+ </div>
29
+ );
30
+ }
31
+
32
+ /**
33
+ * One option row: control on the **left**, then the primary label and an optional
34
+ * secondary label. Uses the `Label` component so the row is a real `<label>` — the
35
+ * whole row is clickable and toggles the control (same mechanism as `LabeledCheckBox`
36
+ * / `LabeledRadio`). `w-full` makes the entire row width tappable; `reverseChildren`
37
+ * puts the control first (left) and `justify-end` keeps the content left-packed.
38
+ */
39
+ function OptionRow({
40
+ control,
41
+ label,
42
+ description,
43
+ }: {
44
+ control: ReactNode;
45
+ label?: string;
46
+ description?: string;
47
+ }) {
48
+ return (
49
+ <Label
50
+ label={label}
51
+ secondaryLabel={description}
52
+ labelDirections="horizontal"
53
+ childrenDirections="horizontal"
54
+ reverseChildren
55
+ className="w-full cursor-pointer justify-end gap-[6px] py-[8px]"
56
+ >
57
+ {control}
58
+ </Label>
59
+ );
60
+ }
61
+
62
+ /**
63
+ * `FormBuilder.RadioList` — single-select options (`string`) as a boxed, divided list
64
+ * (design nodes 8999:170512). Uses the Radix `RadioGroup` for roving-focus / arrow-key nav.
65
+ */
66
+ export function RadioListField(props: OptionsFieldProps) {
67
+ const loading = useLoading();
68
+ return (
69
+ <FieldShell
70
+ {...props}
71
+ view={(v) => formatFieldView({ kind: "option", value: v, options: props.options })}
72
+ >
73
+ {(field) => (
74
+ <RadioGroup value={field.value ?? ""} onValueChange={field.onChange} className="w-full">
75
+ <OptionBox>
76
+ {props.options.map((opt: OptionItem) => (
77
+ <OptionRow
78
+ key={opt.value}
79
+ label={opt.label}
80
+ description={opt.description}
81
+ control={
82
+ <Radio
83
+ id={`${props.name}-${opt.value}`}
84
+ value={opt.value}
85
+ size="S"
86
+ disabled={props.disabled || loading}
87
+ />
88
+ }
89
+ />
90
+ ))}
91
+ </OptionBox>
92
+ </RadioGroup>
93
+ )}
94
+ </FieldShell>
95
+ );
96
+ }
97
+
98
+ /**
99
+ * `FormBuilder.CheckboxGroup` — multi-select options as a boxed, divided list (design
100
+ * node 8999:170546). Value is a `string[]` of the selected option values.
101
+ */
102
+ export function CheckboxGroupField(props: OptionsFieldProps) {
103
+ const loading = useLoading();
104
+ return (
105
+ <FieldShell
106
+ {...props}
107
+ view={(v) => formatFieldView({ kind: "multi", value: v, options: props.options })}
108
+ >
109
+ {(field) => {
110
+ const selected: string[] = Array.isArray(field.value) ? field.value : [];
111
+ const toggle = (value: string, checked: boolean) => {
112
+ field.onChange(checked ? [...selected, value] : selected.filter((v) => v !== value));
113
+ };
114
+ return (
115
+ <div role="group" aria-label={typeof props.label === "string" ? props.label : undefined}>
116
+ <OptionBox>
117
+ {props.options.map((opt: OptionItem) => (
118
+ <OptionRow
119
+ key={opt.value}
120
+ label={opt.label}
121
+ description={opt.description}
122
+ control={
123
+ <Checkbox
124
+ id={`${props.name}-${opt.value}`}
125
+ size="S"
126
+ checked={selected.includes(opt.value)}
127
+ onCheckedChange={(checked) => toggle(opt.value, checked === true)}
128
+ disabled={props.disabled || loading}
129
+ />
130
+ }
131
+ />
132
+ ))}
133
+ </OptionBox>
134
+ </div>
135
+ );
136
+ }}
137
+ </FieldShell>
138
+ );
139
+ }
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import { InputOTP, InputOTPGroup, InputOTPSlot } from "../../InputOTP";
4
+ import { useLoading } from "../context";
5
+ import { formatFieldView } from "../viewFormat";
6
+ import type { OtpFieldProps } from "../types";
7
+ import { FieldShell } from "./FieldShell";
8
+
9
+ /** `FormBuilder.Otp` — a code / PIN input (Glare `InputOTP`). Value is a string. */
10
+ export function OtpField(props: OtpFieldProps) {
11
+ const loading = useLoading();
12
+ const length = props.length ?? 6;
13
+
14
+ return (
15
+ <FieldShell {...props} view={(v) => formatFieldView({ kind: "text", value: v })}>
16
+ {(field) => (
17
+ <InputOTP
18
+ maxLength={length}
19
+ value={field.value ?? ""}
20
+ onChange={(v: string) => field.onChange(v)}
21
+ disabled={props.disabled || loading}
22
+ >
23
+ <InputOTPGroup>
24
+ {Array.from({ length }, (_, i) => (
25
+ <InputOTPSlot key={i} index={i} />
26
+ ))}
27
+ </InputOTPGroup>
28
+ </InputOTP>
29
+ )}
30
+ </FieldShell>
31
+ );
32
+ }
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ import { Select, SelectTrigger, SelectContent, SelectItem } from "../../Select";
4
+ import { InputField } from "../../InputField";
5
+ import { useLoading, useCell } from "../context";
6
+ import { formatFieldView } from "../viewFormat";
7
+ import type { PhoneFieldProps } from "../types";
8
+ import { FieldShell } from "./FieldShell";
9
+ import { COUNTRIES, countryByCode, countryByDial, flagEmoji } from "./countries";
10
+
11
+ // Dial codes longest-first, so "+1" doesn't shadow "+1…"-style codes when prefix-matching.
12
+ const DIALS = [...new Set(COUNTRIES.map((c) => c.dial))].sort((a, b) => b.length - a.length);
13
+
14
+ function splitPhone(value: unknown, defaultDial: string): { dial: string; number: string } {
15
+ const s = typeof value === "string" ? value.trim() : "";
16
+ if (!s) return { dial: defaultDial, number: "" };
17
+ const idx = s.indexOf(" ");
18
+ if (s.startsWith("+") && idx > 0) {
19
+ return { dial: s.slice(0, idx), number: s.slice(idx + 1).trim() };
20
+ }
21
+ if (s.startsWith("+")) {
22
+ const dial = DIALS.find((d) => s.startsWith(d));
23
+ if (dial) return { dial, number: s.slice(dial.length).trim() };
24
+ }
25
+ return { dial: defaultDial, number: s };
26
+ }
27
+
28
+ /**
29
+ * `FormBuilder.Phone` — country dial-code (every country) + number. The picker is keyed by the
30
+ * unique ISO code (dial codes repeat); the value is stored as a `"+<dial> <number>"` string.
31
+ */
32
+ export function PhoneField(props: PhoneFieldProps) {
33
+ const loading = useLoading();
34
+ const cell = useCell();
35
+ const defaultDial = props.defaultCountry ?? "+964"; // Iraq
36
+
37
+ return (
38
+ <FieldShell {...props} view={(v) => formatFieldView({ kind: "text", value: v })}>
39
+ {(field) => {
40
+ const { dial, number } = splitPhone(field.value, defaultDial);
41
+ const country = countryByDial(dial);
42
+ const disabled = props.disabled || loading;
43
+ const commit = (d: string, n: string) => field.onChange(n ? `${d} ${n}` : d);
44
+ return (
45
+ <div className="flex w-full min-w-0 items-center gap-2">
46
+ <Select
47
+ value={country?.code ?? ""}
48
+ onValueChange={(code) => commit(countryByCode(code)?.dial ?? dial, number)}
49
+ disabled={disabled}
50
+ >
51
+ {/* Compact trigger — flag + dial. The dropdown rows carry the full country name. */}
52
+ <SelectTrigger size="XL" onTable={cell} className="shrink-0">
53
+ <span className="whitespace-nowrap">
54
+ {country ? `${flagEmoji(country.code)} ${country.dial}` : dial}
55
+ </span>
56
+ </SelectTrigger>
57
+ <SelectContent>
58
+ {COUNTRIES.map((c) => (
59
+ <SelectItem key={c.code} value={c.code}>
60
+ {`${flagEmoji(c.code)} ${c.name} ${c.dial}`}
61
+ </SelectItem>
62
+ ))}
63
+ </SelectContent>
64
+ </Select>
65
+ <div className="min-w-0 flex-1">
66
+ <InputField
67
+ type="tel"
68
+ inputMode="tel"
69
+ value={number}
70
+ onChange={(e) => commit(dial, e.target.value.replace(/[^\d\s-]/g, ""))}
71
+ placeholder={props.placeholder ?? "Phone number"}
72
+ disabled={disabled}
73
+ onTable={cell}
74
+ className="w-full"
75
+ />
76
+ </div>
77
+ </div>
78
+ );
79
+ }}
80
+ </FieldShell>
81
+ );
82
+ }
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import type { OutputData } from "@editorjs/editorjs";
4
+
5
+ // Import the lazy wrapper from its file directly, NOT the folder barrel: the barrel
6
+ // statically re-exports `TextEditor` (which loads EditorJS at module init) and would drag
7
+ // it into the SSR bundle, crashing server rendering. This path stays SSR-safe (lazy inside).
8
+ import { RichTextField as RichTextEditor } from "../../TextEditor/RichTextField";
9
+ import { formatFieldView } from "../viewFormat";
10
+ import type { BaseFieldProps } from "../types";
11
+ import { FieldShell } from "./FieldShell";
12
+
13
+ /**
14
+ * `FormBuilder.RichText` — EditorJS content (value = `OutputData`).
15
+ *
16
+ * Forced to a **vertical, full-width** layout (label on top, editor below spanning the whole
17
+ * section): the editor + its toolbar need the full width, not the cramped right column.
18
+ */
19
+ export function RichTextField(props: BaseFieldProps) {
20
+ return (
21
+ <FieldShell
22
+ {...props}
23
+ direction="vertical"
24
+ fullWidth
25
+ view={(v) => formatFieldView({ kind: "richtext", value: v })}
26
+ >
27
+ {(field) => (
28
+ <RichTextEditor
29
+ data={field.value as OutputData | undefined}
30
+ onChange={field.onChange}
31
+ readOnly={props.disabled}
32
+ placeholder={props.placeholder}
33
+ />
34
+ )}
35
+ </FieldShell>
36
+ );
37
+ }
@@ -0,0 +1,101 @@
1
+ "use client";
2
+
3
+ import { SearchableSelect } from "../../SearchableSelect";
4
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../Select";
5
+ import { BadgeField } from "../../BadgeField";
6
+ import type { Tag } from "../../../hooks/useTagSelection";
7
+ import { useLoading, useCell } from "../context";
8
+ import { formatFieldView } from "../viewFormat";
9
+ import type { SelectFieldProps, SearchableSelectFieldProps, OptionsFieldProps } from "../types";
10
+ import { FieldShell } from "./FieldShell";
11
+
12
+ /** `FormBuilder.Select` — the plain `Select` dropdown. */
13
+ export function SelectField(props: SelectFieldProps) {
14
+ const loading = useLoading();
15
+ const cell = useCell();
16
+
17
+ return (
18
+ <FieldShell
19
+ {...props}
20
+ view={(v) => formatFieldView({ kind: "option", value: v, options: props.options })}
21
+ >
22
+ {(field) => (
23
+ <Select
24
+ value={typeof field.value === "string" ? field.value : ""}
25
+ onValueChange={field.onChange}
26
+ disabled={props.disabled || loading}
27
+ >
28
+ <SelectTrigger size="XL" onTable={cell} className="w-full">
29
+ <SelectValue placeholder={props.placeholder ?? "Select…"} />
30
+ </SelectTrigger>
31
+ <SelectContent>
32
+ {props.options.map((opt) => (
33
+ <SelectItem key={opt.value} value={opt.value}>
34
+ {opt.label}
35
+ </SelectItem>
36
+ ))}
37
+ </SelectContent>
38
+ </Select>
39
+ )}
40
+ </FieldShell>
41
+ );
42
+ }
43
+
44
+ /**
45
+ * `FormBuilder.SearchableSelect` — the `SearchableSelect` combobox: a search box with
46
+ * client-side filtering, or server-side search + infinite scroll via the async props.
47
+ */
48
+ export function SearchableSelectField(props: SearchableSelectFieldProps) {
49
+ const cell = useCell();
50
+ return (
51
+ <FieldShell
52
+ {...props}
53
+ view={(v) => formatFieldView({ kind: "option", value: v, options: props.options })}
54
+ >
55
+ {(field) => (
56
+ <SearchableSelect
57
+ options={props.options}
58
+ value={field.value ?? null}
59
+ onValueChange={(value) => field.onChange(value)}
60
+ placeholder={props.placeholder ?? "Select…"}
61
+ filterClientSide={props.filterClientSide ?? true}
62
+ onSearchChange={props.onSearchChange}
63
+ onLoadMore={props.onLoadMore}
64
+ hasMore={props.hasMore}
65
+ loading={props.loading}
66
+ onTable={cell}
67
+ className="w-full"
68
+ />
69
+ )}
70
+ </FieldShell>
71
+ );
72
+ }
73
+
74
+ /** `FormBuilder.MultiSelect` / `.Tags` — BadgeField, value is `string[]`. */
75
+ export function MultiSelectField(props: OptionsFieldProps) {
76
+ const cell = useCell();
77
+ return (
78
+ <FieldShell
79
+ {...props}
80
+ view={(v) => formatFieldView({ kind: "multi", value: v, options: props.options })}
81
+ >
82
+ {(field) => {
83
+ const selected = new Set<string>(Array.isArray(field.value) ? field.value : []);
84
+ const tags: Tag[] = props.options.map((opt) => ({
85
+ id: opt.value,
86
+ name: opt.label,
87
+ value: opt.value,
88
+ isSelected: selected.has(opt.value),
89
+ }));
90
+ return (
91
+ <BadgeField
92
+ tags={tags}
93
+ onValueChange={(picked) => field.onChange(picked.map((t) => t.value ?? t.id))}
94
+ onTable={cell}
95
+ className="w-full"
96
+ />
97
+ );
98
+ }}
99
+ </FieldShell>
100
+ );
101
+ }