torch-glare 2.4.1 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/lib/components/Button.tsx +1 -1
- package/apps/lib/components/Card.tsx +47 -20
- package/apps/lib/components/ColorPicker.tsx +441 -0
- package/apps/lib/components/ConclusionHeader.tsx +148 -0
- package/apps/lib/components/DatePicker.tsx +2 -0
- package/apps/lib/components/Drawer.tsx +66 -24
- package/apps/lib/components/FormBuilder/DisplayField.tsx +40 -0
- package/apps/lib/components/FormBuilder/context.ts +71 -0
- package/apps/lib/components/FormBuilder/fields/ChoiceFields.tsx +65 -0
- package/apps/lib/components/FormBuilder/fields/ColorField.tsx +76 -0
- package/apps/lib/components/FormBuilder/fields/CustomField.tsx +16 -0
- package/apps/lib/components/FormBuilder/fields/DateField.tsx +34 -0
- package/apps/lib/components/FormBuilder/fields/FieldArray.tsx +79 -0
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +174 -0
- package/apps/lib/components/FormBuilder/fields/FileField.tsx +40 -0
- package/apps/lib/components/FormBuilder/fields/OptionListFields.tsx +139 -0
- package/apps/lib/components/FormBuilder/fields/OtpField.tsx +32 -0
- package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +82 -0
- package/apps/lib/components/FormBuilder/fields/RichTextEditorField.tsx +37 -0
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +101 -0
- package/apps/lib/components/FormBuilder/fields/SignatureField.tsx +175 -0
- package/apps/lib/components/FormBuilder/fields/SliderField.tsx +72 -0
- package/apps/lib/components/FormBuilder/fields/SwitchBoxField.tsx +42 -0
- package/apps/lib/components/FormBuilder/fields/TableField.tsx +312 -0
- package/apps/lib/components/FormBuilder/fields/TextField.tsx +222 -0
- package/apps/lib/components/FormBuilder/fields/TreeSelectField.tsx +49 -0
- package/apps/lib/components/FormBuilder/fields/countries.ts +303 -0
- package/apps/lib/components/FormBuilder/fields/index.ts +25 -0
- package/apps/lib/components/FormBuilder/form-builder.tsx +292 -0
- package/apps/lib/components/FormBuilder/header.tsx +105 -0
- package/apps/lib/components/FormBuilder/index.ts +37 -0
- package/apps/lib/components/FormBuilder/numberFormat.ts +16 -0
- package/apps/lib/components/FormBuilder/stepper.tsx +217 -0
- package/apps/lib/components/FormBuilder/submit.tsx +41 -0
- package/apps/lib/components/FormBuilder/types.ts +264 -0
- package/apps/lib/components/FormBuilder/viewFormat.tsx +137 -0
- package/apps/lib/components/FormRenderer/FormDrawer.tsx +121 -0
- package/apps/lib/components/FormRenderer/form-renderer.tsx +113 -0
- package/apps/lib/components/FormRenderer/index.ts +9 -0
- package/apps/lib/components/FormRenderer/types.ts +79 -0
- package/apps/lib/components/FormSummary.tsx +282 -0
- package/apps/lib/components/ImageAttachment.tsx +36 -61
- package/apps/lib/components/Label.tsx +49 -42
- package/apps/lib/components/RadioCard.tsx +2 -0
- package/apps/lib/components/SearchableSelect.tsx +9 -3
- package/apps/lib/components/SectionBlock.tsx +16 -8
- package/apps/lib/components/Select.tsx +41 -120
- package/apps/lib/components/TextEditor/RichTextField.tsx +46 -0
- package/apps/lib/components/{TextEditor.tsx → TextEditor/TextEditor.tsx} +63 -9
- package/apps/lib/components/TextEditor/TextEditorToolbar.tsx +429 -0
- package/apps/lib/components/TextEditor/editor-tools/AlignmentTune.ts +70 -0
- package/apps/lib/components/TextEditor/editor-tools/ColorInlineTool.ts +50 -0
- package/apps/lib/components/TextEditor/editor-tools/StrikethroughInlineTool.ts +48 -0
- package/apps/lib/components/TextEditor/editor-tools/inlineFormat.ts +98 -0
- package/apps/lib/{types → components/TextEditor}/editorjs.d.ts +19 -0
- package/apps/lib/components/TextEditor/index.ts +7 -0
- package/apps/lib/components/Textarea.tsx +1 -1
- package/apps/lib/registry.json +51 -58
- package/apps/lib/tsconfig.tsbuildinfo +1 -0
- package/apps/lib/utils/color.ts +175 -0
- package/docs/components/card.md +4 -2
- package/docs/components/chart-block-tool.md +5 -4
- package/docs/components/color-picker.md +101 -0
- package/docs/components/conclusion-header.md +80 -0
- package/docs/components/drawer.md +153 -102
- package/docs/components/form-builder.md +270 -0
- package/docs/components/form-renderer.md +228 -0
- package/docs/components/form-summary.md +123 -0
- package/docs/components/image-attachment.md +10 -4
- package/docs/components/table-dnd-wrapper.md +5 -3
- package/docs/components/text-editor.md +18 -0
- package/docs/how-to/form-and-list-recipes.md +26 -19
- package/docs/how-to/forms-with-form-builder.md +408 -0
- package/docs/how-to/guides.md +153 -179
- package/docs/tutorials/building-first-form.md +150 -159
- package/package.json +1 -1
- /package/apps/lib/components/{ChartBlockTool.ts → TextEditor/ChartBlockTool.ts} +0 -0
- /package/apps/lib/components/{TableDnDWrapper.ts → TextEditor/TableDnDWrapper.ts} +0 -0
- /package/apps/lib/{utils → components/TextEditor}/markdownParser.ts +0 -0
|
@@ -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
|
+
}
|