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
|
@@ -38,6 +38,8 @@ interface Props {
|
|
|
38
38
|
theme?: Themes;
|
|
39
39
|
dir?: string;
|
|
40
40
|
className?: string;
|
|
41
|
+
/** Transparent border/background so the trigger blends into a table cell. */
|
|
42
|
+
onTable?: boolean;
|
|
41
43
|
|
|
42
44
|
// --- Async / backend pagination (all optional; static `options` still works) ---
|
|
43
45
|
/**
|
|
@@ -105,6 +107,7 @@ export function SearchableSelect({
|
|
|
105
107
|
theme,
|
|
106
108
|
dir,
|
|
107
109
|
className,
|
|
110
|
+
onTable,
|
|
108
111
|
filterClientSide = true,
|
|
109
112
|
onSearchChange,
|
|
110
113
|
searchDebounceMs = 300,
|
|
@@ -175,11 +178,13 @@ export function SearchableSelect({
|
|
|
175
178
|
data-theme={theme}
|
|
176
179
|
variant={variant}
|
|
177
180
|
size={size === "XS" ? "S" : size}
|
|
181
|
+
onTable={onTable}
|
|
178
182
|
ref={groupRef as never}
|
|
179
183
|
onFocus={(e: React.FocusEvent<HTMLDivElement>) =>
|
|
180
184
|
setDropdownWidth(e.currentTarget.offsetWidth)
|
|
181
185
|
}
|
|
182
|
-
|
|
186
|
+
// Trigger styling mirrors the `Select` component's trigger so the two read identically.
|
|
187
|
+
className={cn("flex w-full items-center gap-1 rounded-[6px] p-[4px]", className)}
|
|
183
188
|
>
|
|
184
189
|
{icon && <Icon>{icon}</Icon>}
|
|
185
190
|
<Input
|
|
@@ -218,13 +223,14 @@ export function SearchableSelect({
|
|
|
218
223
|
}
|
|
219
224
|
}}
|
|
220
225
|
className={cn(
|
|
221
|
-
|
|
226
|
+
// Same box + open-state fill as the Select trigger's chevron.
|
|
227
|
+
"h-[32px] w-[32px] shrink-0",
|
|
222
228
|
open && "bg-background-presentation-action-hover text-white",
|
|
223
229
|
)}
|
|
224
230
|
>
|
|
225
231
|
<i
|
|
226
232
|
className={cn(
|
|
227
|
-
"ri-arrow-down-s-line text-[
|
|
233
|
+
"ri-arrow-down-s-line !text-[26px] transition-all duration-100 ease-in-out",
|
|
228
234
|
open && "rotate-180",
|
|
229
235
|
)}
|
|
230
236
|
/>
|
|
@@ -27,6 +27,8 @@ export interface SectionBlockProps extends Omit<HTMLAttributes<HTMLDivElement>,
|
|
|
27
27
|
color?: SectionColor;
|
|
28
28
|
title?: ReactNode;
|
|
29
29
|
icon?: ReactNode;
|
|
30
|
+
/** Right-aligned content on the title row — e.g. action buttons. */
|
|
31
|
+
action?: ReactNode;
|
|
30
32
|
containerClassName?: string;
|
|
31
33
|
headerClassName?: string;
|
|
32
34
|
bodyClassName?: string;
|
|
@@ -38,6 +40,7 @@ export const SectionBlock = forwardRef<HTMLDivElement, SectionBlockProps>(
|
|
|
38
40
|
children,
|
|
39
41
|
color,
|
|
40
42
|
title,
|
|
43
|
+
action,
|
|
41
44
|
className,
|
|
42
45
|
containerClassName,
|
|
43
46
|
headerClassName,
|
|
@@ -57,14 +60,19 @@ export const SectionBlock = forwardRef<HTMLDivElement, SectionBlockProps>(
|
|
|
57
60
|
)}
|
|
58
61
|
{...props}
|
|
59
62
|
>
|
|
60
|
-
{title && (
|
|
61
|
-
<div className={cn("flex px-[6px]
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
{(title || action) && (
|
|
64
|
+
<div className={cn("flex px-[6px] items-center justify-between gap-3", headerClassName)}>
|
|
65
|
+
{title ? (
|
|
66
|
+
<div className={cn(titleBadge({ color }))}>
|
|
67
|
+
<span className="flex items-center gap-1.5">
|
|
68
|
+
{icon}
|
|
69
|
+
{title}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
) : (
|
|
73
|
+
<span />
|
|
74
|
+
)}
|
|
75
|
+
{action && <div className="flex shrink-0 items-center gap-2">{action}</div>}
|
|
68
76
|
</div>
|
|
69
77
|
)}
|
|
70
78
|
<div className={cn("flex px-[42px] flex-col gap-[2px] px-[42px]", bodyClassName)}>
|
|
@@ -5,6 +5,7 @@ import { cn } from "../utils/cn";
|
|
|
5
5
|
import { cva, VariantProps } from "class-variance-authority";
|
|
6
6
|
import { Button } from "./Button";
|
|
7
7
|
import { Tooltip } from "./Tooltip";
|
|
8
|
+
import { MenuItemStyles } from "./DropdownMenu";
|
|
8
9
|
import { Themes } from "../utils/types";
|
|
9
10
|
|
|
10
11
|
const Select = SelectPrimitive.Root;
|
|
@@ -31,6 +32,7 @@ const SelectTrigger = React.forwardRef<
|
|
|
31
32
|
errors,
|
|
32
33
|
theme,
|
|
33
34
|
icon,
|
|
35
|
+
onTable,
|
|
34
36
|
...props
|
|
35
37
|
},
|
|
36
38
|
ref,
|
|
@@ -45,6 +47,7 @@ const SelectTrigger = React.forwardRef<
|
|
|
45
47
|
size,
|
|
46
48
|
variant,
|
|
47
49
|
error: errors !== undefined,
|
|
50
|
+
onTable,
|
|
48
51
|
}),
|
|
49
52
|
className,
|
|
50
53
|
)}
|
|
@@ -61,6 +64,7 @@ const SelectTrigger = React.forwardRef<
|
|
|
61
64
|
<Button
|
|
62
65
|
as={"span"}
|
|
63
66
|
buttonType="icon"
|
|
67
|
+
size={"L"}
|
|
64
68
|
className={cn([
|
|
65
69
|
"group-aria-expanded:bg-background-presentation-action-hover",
|
|
66
70
|
"group-aria-expanded:text-white",
|
|
@@ -137,7 +141,10 @@ const SelectContent = React.forwardRef<
|
|
|
137
141
|
position={position}
|
|
138
142
|
{...props}
|
|
139
143
|
>
|
|
140
|
-
|
|
144
|
+
{/* Dedicated scroll viewport + boxed group, matching SearchableSelect's menu surface. */}
|
|
145
|
+
<SelectPrimitive.Viewport className="overflow-y-auto overflow-x-hidden rounded-[10px] scrollbar-hide">
|
|
146
|
+
<div className="flex flex-col gap-[1px] overflow-hidden rounded-[10px]">{children}</div>
|
|
147
|
+
</SelectPrimitive.Viewport>
|
|
141
148
|
</SelectPrimitive.Content>
|
|
142
149
|
</SelectPrimitive.Portal>
|
|
143
150
|
),
|
|
@@ -158,22 +165,21 @@ SelectLabel.displayName = "SelectLabel";
|
|
|
158
165
|
|
|
159
166
|
const SelectItem = React.forwardRef<
|
|
160
167
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
161
|
-
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> &
|
|
162
|
-
VariantProps<typeof SelectItemStyles>
|
|
168
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> & VariantProps<typeof MenuItemStyles>
|
|
163
169
|
>(({ className, children, size = "M", variant = "Default", active, ...props }, ref) => (
|
|
170
|
+
// Same structure as DropdownMenuItem / SearchableSelect: MenuItemStyles on the element
|
|
171
|
+
// + a single inner <div> the styles target via [&>div], and a check on the selected row.
|
|
164
172
|
<SelectPrimitive.Item
|
|
165
173
|
ref={ref}
|
|
166
|
-
className={cn(
|
|
167
|
-
SelectItemStyles({
|
|
168
|
-
variant,
|
|
169
|
-
active,
|
|
170
|
-
size,
|
|
171
|
-
}),
|
|
172
|
-
className,
|
|
173
|
-
)}
|
|
174
|
+
className={cn(MenuItemStyles({ variant, active, size }), "shrink-0", className)}
|
|
174
175
|
{...props}
|
|
175
176
|
>
|
|
176
|
-
<
|
|
177
|
+
<div>
|
|
178
|
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
179
|
+
<SelectPrimitive.ItemIndicator className="ml-auto flex shrink-0">
|
|
180
|
+
<i className="ri-check-line text-[16px]" />
|
|
181
|
+
</SelectPrimitive.ItemIndicator>
|
|
182
|
+
</div>
|
|
177
183
|
</SelectPrimitive.Item>
|
|
178
184
|
));
|
|
179
185
|
|
|
@@ -206,34 +212,36 @@ export {
|
|
|
206
212
|
|
|
207
213
|
// NOTE: radix select as DropDownButton
|
|
208
214
|
|
|
215
|
+
// Panel surface mirrors SearchableSelect's `menuContentStyles` (translucent, backdrop-blurred,
|
|
216
|
+
// borderless, rounded-14). `min-w`/`z-index` are kept for the Radix Select portal.
|
|
209
217
|
const SelectContentStyles = cva(
|
|
210
218
|
[
|
|
211
219
|
"p-1",
|
|
212
|
-
"rounded-[
|
|
220
|
+
"rounded-[14px]",
|
|
221
|
+
// Match the dropdown to the trigger's width (Radix popper exposes it as a CSS var),
|
|
222
|
+
// but never narrower than 240px.
|
|
223
|
+
"w-[var(--radix-select-trigger-width)]",
|
|
213
224
|
"min-w-[240px]",
|
|
214
|
-
"border",
|
|
225
|
+
"border-0",
|
|
215
226
|
"outline-none",
|
|
216
|
-
"overflow-
|
|
227
|
+
"overflow-hidden",
|
|
228
|
+
"backdrop-blur-[21px]",
|
|
229
|
+
"flex flex-col gap-1",
|
|
217
230
|
"data-[state=open]:animate-in",
|
|
218
|
-
"data-[state=closed]:animate-out",
|
|
219
|
-
"data-[state=closed]:fade-out-0",
|
|
220
231
|
"data-[state=open]:fade-in-0",
|
|
221
|
-
"overflow-x-hidden",
|
|
222
|
-
"scrollbar-hide",
|
|
223
232
|
"z-[1000]",
|
|
233
|
+
"max-h-[368px]",
|
|
224
234
|
],
|
|
225
235
|
{
|
|
226
236
|
variants: {
|
|
227
237
|
variant: {
|
|
228
238
|
SystemStyle: [
|
|
229
|
-
"border-border-system-global-secondary",
|
|
230
239
|
"bg-background-system-body-primary",
|
|
231
240
|
"shadow-[0px_0px_18px_0px_rgba(0,0,0,0.75)]",
|
|
232
241
|
],
|
|
233
242
|
PresentationStyle: [
|
|
234
|
-
"
|
|
235
|
-
"
|
|
236
|
-
"shadow-[0px_0px_10px_0px_rgba(0,0,0,0.4),0px_4px_4px_0px_rgba(0,0,0,0.2)]",
|
|
243
|
+
"bg-[rgba(61,64,69,0.72)]",
|
|
244
|
+
"shadow-[0_0_32px_2px_rgba(0,0,0,0.20),0_0_48px_2px_rgba(0,0,0,0.05)]",
|
|
237
245
|
],
|
|
238
246
|
},
|
|
239
247
|
defaultVariants: {
|
|
@@ -243,102 +251,9 @@ const SelectContentStyles = cva(
|
|
|
243
251
|
},
|
|
244
252
|
);
|
|
245
253
|
|
|
246
|
-
const SelectItemStyles = cva(
|
|
247
|
-
[
|
|
248
|
-
"text-content-presentation-action-light-primary",
|
|
249
|
-
"outline-none",
|
|
250
|
-
"border",
|
|
251
|
-
"border-transparent",
|
|
252
|
-
"flex",
|
|
253
|
-
"gap-[8px]",
|
|
254
|
-
"items-center",
|
|
255
|
-
"justify-start",
|
|
256
|
-
"text-overflow",
|
|
257
|
-
"overflow-hidden",
|
|
258
|
-
"px-[12px]",
|
|
259
|
-
"rounded-[4px]",
|
|
260
|
-
"transition-all",
|
|
261
|
-
"ease-in-out",
|
|
262
|
-
"duration-300",
|
|
263
|
-
],
|
|
264
|
-
{
|
|
265
|
-
variants: {
|
|
266
|
-
variant: {
|
|
267
|
-
Default: [
|
|
268
|
-
"text-content-presentation-action-light-primary",
|
|
269
|
-
"bg-background-presentation-action-dropdown-primary",
|
|
270
|
-
"hover:bg-background-presentation-action-hover",
|
|
271
|
-
"hover:text-content-presentation-global-primary-inverse",
|
|
272
|
-
"focus:bg-background-presentation-action-hover",
|
|
273
|
-
"focus:text-content-presentation-global-primary-inverse",
|
|
274
|
-
"disabled:text-content-presentation-state-disabled",
|
|
275
|
-
"disabled:bg-white-00",
|
|
276
|
-
],
|
|
277
|
-
Warning: [
|
|
278
|
-
"text-content-presentation-state-information",
|
|
279
|
-
"hover:bg-background-presentation-state-information-primary",
|
|
280
|
-
"focus:bg-background-presentation-state-information-primary",
|
|
281
|
-
"focus:text-content-presentation-global-primary-inverse",
|
|
282
|
-
"hover:text-content-presentation-global-primary-inverse",
|
|
283
|
-
],
|
|
284
|
-
Negative: [
|
|
285
|
-
"text-content-presentation-state-negative",
|
|
286
|
-
"hover:bg-background-presentation-state-negative-primary",
|
|
287
|
-
"hover:text-content-presentation-global-primary-inverse",
|
|
288
|
-
"focus:bg-background-presentation-state-negative-primary",
|
|
289
|
-
"focus:text-content-presentation-global-primary-inverse",
|
|
290
|
-
"active:text-content-presentation-state-negative",
|
|
291
|
-
],
|
|
292
|
-
SystemStyle: [
|
|
293
|
-
"bg-background-system-body-primary",
|
|
294
|
-
"text-content-system-global-primary",
|
|
295
|
-
"hover:bg-background-system-action-secondary-hover",
|
|
296
|
-
"hover:text-content-system-action-primary-hover",
|
|
297
|
-
"hover:border-border-system-action-primary-hover",
|
|
298
|
-
"focus:bg-background-system-action-secondary-hover",
|
|
299
|
-
"focus:text-content-system-action-primary-hover",
|
|
300
|
-
"focus:border-border-system-action-primary-hover",
|
|
301
|
-
"disabled:bg-background-system-body-secondary",
|
|
302
|
-
"disabled:text-content-system-global-disabled",
|
|
303
|
-
],
|
|
304
|
-
},
|
|
305
|
-
size: {
|
|
306
|
-
S: ["typography-body-small-regular", "h-[24px]"],
|
|
307
|
-
M: ["typography-body-medium-regular", "h-[32px]"],
|
|
308
|
-
},
|
|
309
|
-
|
|
310
|
-
disabled: {
|
|
311
|
-
true: ["text-content-presentation-state-disabled", "bg-white-00"],
|
|
312
|
-
},
|
|
313
|
-
|
|
314
|
-
active: {
|
|
315
|
-
true: [
|
|
316
|
-
"bg-background-presentation-action-selected",
|
|
317
|
-
"text-content-presentation-action-light-primary",
|
|
318
|
-
],
|
|
319
|
-
},
|
|
320
|
-
|
|
321
|
-
defaultVariants: {
|
|
322
|
-
variant: "Default",
|
|
323
|
-
size: "M",
|
|
324
|
-
active: false,
|
|
325
|
-
disabled: false,
|
|
326
|
-
},
|
|
327
|
-
},
|
|
328
|
-
compoundVariants: [
|
|
329
|
-
{
|
|
330
|
-
active: true,
|
|
331
|
-
variant: "Warning",
|
|
332
|
-
className: ["text-content-presentation-state-negative"],
|
|
333
|
-
},
|
|
334
|
-
],
|
|
335
|
-
},
|
|
336
|
-
);
|
|
337
|
-
|
|
338
254
|
const PopoverTriggerStyles = cva(
|
|
339
255
|
[
|
|
340
|
-
"flex flex-row rounded-[
|
|
341
|
-
"rounded-[4px]",
|
|
256
|
+
"flex flex-row rounded-[8px] justify-between items-center outline-none",
|
|
342
257
|
"[&_span]:text-content-presentation-action-light-primary",
|
|
343
258
|
"typography-body-small-regular",
|
|
344
259
|
"[&_p]:px-[10px] [&_p]:whitespace-nowrap",
|
|
@@ -354,9 +269,11 @@ const PopoverTriggerStyles = cva(
|
|
|
354
269
|
variant: {
|
|
355
270
|
PresentationStyle: [
|
|
356
271
|
"bg-background-presentation-form-field-primary",
|
|
272
|
+
"border-border-presentation-action-primary",
|
|
357
273
|
"hover:bg-background-presentation-form-field-hover",
|
|
274
|
+
"hover:border-border-presentation-action-hover",
|
|
358
275
|
"focus:bg-background-presentation-form-field-hover",
|
|
359
|
-
"border-
|
|
276
|
+
"focus:border-border-presentation-state-focus",
|
|
360
277
|
],
|
|
361
278
|
SystemStyle: [
|
|
362
279
|
"bg-black-alpha-20",
|
|
@@ -376,12 +293,16 @@ const PopoverTriggerStyles = cva(
|
|
|
376
293
|
"hover:caret-border-presentation-state-negative",
|
|
377
294
|
],
|
|
378
295
|
},
|
|
296
|
+
// Transparent border/background so the trigger blends into a table cell.
|
|
297
|
+
onTable: {
|
|
298
|
+
true: ["border-transparent", "bg-transparent"],
|
|
299
|
+
},
|
|
379
300
|
size: {
|
|
380
|
-
S: ["[&_span]:h-[22px] [&_span]:w-[22px] [&_p]:typography-body-small-medium"],
|
|
301
|
+
S: ["rounded-[6px] [&_span]:h-[22px] [&_span]:w-[22px] [&_p]:typography-body-small-medium"],
|
|
381
302
|
M: ["[&_span]:h-[26px] [&_span]:w-[26px] [&_p]:typography-body-medium-medium"],
|
|
382
303
|
L: ["[&_span]:h-[28px] [&_span]:w-[28px] [&_p]:typography-body-large-medium"],
|
|
383
304
|
XL: [
|
|
384
|
-
"h-[40px] p-[4px] rounded-[
|
|
305
|
+
"h-[40px] p-[4px] rounded-[8px] [&_span]:h-[32px] [&_span]:w-[32px] [&_p]:typography-body-large-regular [&_p]:px-[4px]",
|
|
385
306
|
],
|
|
386
307
|
},
|
|
387
308
|
},
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { lazy, Suspense, useEffect, useState } from "react";
|
|
4
|
+
import type { OutputData } from "@editorjs/editorjs";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Client-only, lazily-loaded wrapper around the EditorJS `TextEditor`.
|
|
8
|
+
*
|
|
9
|
+
* EditorJS references DOM globals (`Element`) at module-load time, which crashes
|
|
10
|
+
* server-side rendering. Statically importing `TextEditor` in the field registry
|
|
11
|
+
* would pull it into the SSR bundle; loading it lazily + only after mount keeps
|
|
12
|
+
* the builder SSR-safe — the editor is imported on the client, on demand.
|
|
13
|
+
*/
|
|
14
|
+
const TextEditorLazy = lazy(() => import("./TextEditor").then((m) => ({ default: m.TextEditor })));
|
|
15
|
+
|
|
16
|
+
export interface RichTextFieldProps {
|
|
17
|
+
data?: OutputData;
|
|
18
|
+
onChange?: (data: OutputData) => void;
|
|
19
|
+
readOnly?: boolean;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
/** Editor size — controls min-height + padding (the toolbar gutter). Default "M". */
|
|
22
|
+
size?: "S" | "M" | "L" | "XL";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const placeholderBox = (
|
|
26
|
+
<div className="min-h-[200px] w-full rounded-[6px] border border-border-presentation-action-primary bg-background-presentation-form-field-primary" />
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export function RichTextField({ size = "M", ...props }: RichTextFieldProps) {
|
|
30
|
+
const [mounted, setMounted] = useState(false);
|
|
31
|
+
useEffect(() => setMounted(true), []);
|
|
32
|
+
|
|
33
|
+
if (!mounted) return placeholderBox;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Suspense fallback={placeholderBox}>
|
|
37
|
+
<TextEditorLazy
|
|
38
|
+
size={size}
|
|
39
|
+
className="border border-border-presentation-action-primary"
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
</Suspense>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
RichTextField.displayName = "RichTextField";
|
|
@@ -10,10 +10,10 @@ import React, {
|
|
|
10
10
|
useState,
|
|
11
11
|
} from "react";
|
|
12
12
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
13
|
-
import { cn } from "
|
|
14
|
-
import { Themes } from "
|
|
13
|
+
import { cn } from "../../utils/cn";
|
|
14
|
+
import { Themes } from "../../utils/types";
|
|
15
15
|
import EditorJS, { type OutputData } from "@editorjs/editorjs";
|
|
16
|
-
import { isMarkdown, parseMarkdownToBlocks } from "
|
|
16
|
+
import { isMarkdown, parseMarkdownToBlocks } from "./markdownParser";
|
|
17
17
|
|
|
18
18
|
// ─── Tool Imports ────────────────────────────────────────────────────────────
|
|
19
19
|
import Header from "@editorjs/header";
|
|
@@ -34,7 +34,14 @@ import Marker from "@editorjs/marker";
|
|
|
34
34
|
import InlineCode from "@editorjs/inline-code";
|
|
35
35
|
import Underline from "@editorjs/underline";
|
|
36
36
|
import TextVariantTune from "@editorjs/text-variant-tune";
|
|
37
|
+
// `editorjs-undo` touches DOM globals at module-eval, so it is imported dynamically
|
|
38
|
+
// (browser-only) in `onReady`; keep only the type here.
|
|
39
|
+
import type Undo from "editorjs-undo";
|
|
37
40
|
import ChartBlockTool from "./ChartBlockTool";
|
|
41
|
+
import StrikethroughInlineTool from "./editor-tools/StrikethroughInlineTool";
|
|
42
|
+
import ColorInlineTool from "./editor-tools/ColorInlineTool";
|
|
43
|
+
import AlignmentTune from "./editor-tools/AlignmentTune";
|
|
44
|
+
import { TextEditorToolbar } from "./TextEditorToolbar";
|
|
38
45
|
|
|
39
46
|
// ─── RTL Detection ───────────────────────────────────────────────────────────
|
|
40
47
|
const RTL_REGEX = /[\u0591-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC]/;
|
|
@@ -172,11 +179,21 @@ const getDefaultTools = (): Record<string, any> => ({
|
|
|
172
179
|
marker: { class: Marker, shortcut: "CMD+SHIFT+M" },
|
|
173
180
|
inlineCode: { class: InlineCode, shortcut: "CMD+SHIFT+I" },
|
|
174
181
|
underline: { class: Underline },
|
|
182
|
+
// Registered so their `sanitize` unions into every block's save config (strike/color
|
|
183
|
+
// markup persists) and they appear in the inline toolbar; the fixed toolbar drives them too.
|
|
184
|
+
strikethrough: { class: StrikethroughInlineTool, shortcut: "CMD+SHIFT+X" },
|
|
185
|
+
color: { class: ColorInlineTool },
|
|
175
186
|
textVariant: { class: TextVariantTune },
|
|
187
|
+
// Per-block text alignment; wired into `tunes` below so every block gets it.
|
|
188
|
+
align: { class: AlignmentTune },
|
|
176
189
|
});
|
|
177
190
|
|
|
178
191
|
// ─── Auto-direction CSS ──────────────────────────────────────────────────────
|
|
179
192
|
const AUTO_DIR_STYLES = `
|
|
193
|
+
/* Block alignment (AlignmentTune sets data-align on the block content wrapper). */
|
|
194
|
+
.torch-text-editor .cdx-align-wrap[data-align="center"] { text-align: center; }
|
|
195
|
+
.torch-text-editor .cdx-align-wrap[data-align="right"] { text-align: right; }
|
|
196
|
+
.torch-text-editor .cdx-align-wrap[data-align="left"] { text-align: left; }
|
|
180
197
|
.torch-text-editor [contenteditable="true"][dir="rtl"] {
|
|
181
198
|
unicode-bidi: plaintext;
|
|
182
199
|
}
|
|
@@ -691,7 +708,8 @@ const textEditorStyles = cva(
|
|
|
691
708
|
"relative w-full rounded-[6px]",
|
|
692
709
|
"border-0 transition-all duration-200 ease-in-out",
|
|
693
710
|
"[&_.codex-editor]:min-h-[inherit]",
|
|
694
|
-
|
|
711
|
+
// Content padding lives on the redactor (not the container) so the toolbar stays full-bleed.
|
|
712
|
+
"[&_.codex-editor__redactor]:min-h-[inherit] [&_.codex-editor__redactor]:px-[16px] [&_.codex-editor__redactor]:pt-[12px] [&_.codex-editor__redactor]:pb-[100px]",
|
|
695
713
|
"[&_.ce-paragraph]:typography-body-medium-regular",
|
|
696
714
|
],
|
|
697
715
|
{
|
|
@@ -727,11 +745,13 @@ const textEditorStyles = cva(
|
|
|
727
745
|
"[&_.ce-delimiter]:border-border-presentation-action-primary",
|
|
728
746
|
],
|
|
729
747
|
},
|
|
748
|
+
// No padding on the container — the fixed toolbar must go edge-to-edge. Content
|
|
749
|
+
// padding is applied to the EditorJS redactor below instead.
|
|
730
750
|
size: {
|
|
731
|
-
S: "min-h-[200px]
|
|
732
|
-
M: "min-h-[300px]
|
|
733
|
-
L: "min-h-[400px]
|
|
734
|
-
XL: "min-h-[500px]
|
|
751
|
+
S: "min-h-[200px]",
|
|
752
|
+
M: "min-h-[300px]",
|
|
753
|
+
L: "min-h-[400px]",
|
|
754
|
+
XL: "min-h-[500px]",
|
|
735
755
|
},
|
|
736
756
|
disabled: {
|
|
737
757
|
true: "cursor-not-allowed opacity-60 pointer-events-none",
|
|
@@ -764,6 +784,8 @@ interface TextEditorProps
|
|
|
764
784
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
765
785
|
tools?: Record<string, any>;
|
|
766
786
|
minHeight?: number;
|
|
787
|
+
/** Show the fixed formatting toolbar (default `true`; hidden when `readOnly`/`disabled`). */
|
|
788
|
+
toolbar?: boolean;
|
|
767
789
|
}
|
|
768
790
|
|
|
769
791
|
// ─── Component ───────────────────────────────────────────────────────────────
|
|
@@ -783,11 +805,14 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
783
805
|
autofocus = false,
|
|
784
806
|
tools,
|
|
785
807
|
minHeight,
|
|
808
|
+
toolbar = true,
|
|
786
809
|
...props
|
|
787
810
|
},
|
|
788
811
|
ref,
|
|
789
812
|
) => {
|
|
790
813
|
const editorRef = useRef<EditorJS | null>(null);
|
|
814
|
+
const undoRef = useRef<Undo | null>(null);
|
|
815
|
+
const [toolbarReady, setToolbarReady] = useState(false);
|
|
791
816
|
const holderRef = useRef<HTMLDivElement>(null);
|
|
792
817
|
const observerRef = useRef<MutationObserver | null>(null);
|
|
793
818
|
const [holderId] = useState(() => `torch-editor-${Math.random().toString(36).substring(2, 9)}`);
|
|
@@ -981,10 +1006,13 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
981
1006
|
if (isInitializing.current || editorRef.current) return;
|
|
982
1007
|
isInitializing.current = true;
|
|
983
1008
|
|
|
1009
|
+
// Only enable the `align` tune when using the default tools (a caller-supplied `tools`
|
|
1010
|
+
// map may not register it).
|
|
1011
|
+
const usingDefaultTools = !toolsRef.current;
|
|
984
1012
|
const editor = new EditorJS({
|
|
985
1013
|
holder: holderId,
|
|
986
1014
|
tools: toolsRef.current ?? getDefaultTools(),
|
|
987
|
-
tunes: ["textVariant"],
|
|
1015
|
+
tunes: usingDefaultTools ? ["textVariant", "align"] : ["textVariant"],
|
|
988
1016
|
data: initialDataRef.current || undefined,
|
|
989
1017
|
readOnly: readOnlyRef.current,
|
|
990
1018
|
placeholder: placeholderRef.current,
|
|
@@ -999,6 +1027,21 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
999
1027
|
setupMarkdownPaste(holderEl);
|
|
1000
1028
|
}
|
|
1001
1029
|
|
|
1030
|
+
// History (undo/redo) for the fixed toolbar + Ctrl+Z/Y. Not for readOnly.
|
|
1031
|
+
// Dynamically imported because the package evaluates DOM globals at module load.
|
|
1032
|
+
if (!readOnlyRef.current) {
|
|
1033
|
+
import("editorjs-undo")
|
|
1034
|
+
.then(({ default: UndoCls }) => {
|
|
1035
|
+
if (!editorRef.current) return;
|
|
1036
|
+
undoRef.current = new UndoCls({ editor: editorRef.current });
|
|
1037
|
+
if (initialDataRef.current) undoRef.current.initialize(initialDataRef.current);
|
|
1038
|
+
})
|
|
1039
|
+
.catch(() => {
|
|
1040
|
+
undoRef.current = null;
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
setToolbarReady(true);
|
|
1044
|
+
|
|
1002
1045
|
onReadyRef.current?.();
|
|
1003
1046
|
},
|
|
1004
1047
|
onChange: () => {
|
|
@@ -1048,6 +1091,8 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
1048
1091
|
editorRef.current.destroy();
|
|
1049
1092
|
editorRef.current = null;
|
|
1050
1093
|
}
|
|
1094
|
+
undoRef.current = null;
|
|
1095
|
+
setToolbarReady(false);
|
|
1051
1096
|
isInitializing.current = false;
|
|
1052
1097
|
};
|
|
1053
1098
|
}, [initEditor]);
|
|
@@ -1098,6 +1143,15 @@ export const TextEditor = forwardRef<TextEditorRef, TextEditorProps>(
|
|
|
1098
1143
|
style={minHeight ? { minHeight: `${minHeight}px` } : undefined}
|
|
1099
1144
|
{...props}
|
|
1100
1145
|
>
|
|
1146
|
+
{toolbar && !readOnly && !disabled && (
|
|
1147
|
+
<TextEditorToolbar
|
|
1148
|
+
editorRef={editorRef}
|
|
1149
|
+
undoRef={undoRef}
|
|
1150
|
+
holderRef={holderRef}
|
|
1151
|
+
ready={toolbarReady}
|
|
1152
|
+
theme={theme}
|
|
1153
|
+
/>
|
|
1154
|
+
)}
|
|
1101
1155
|
<div id={holderId} />
|
|
1102
1156
|
</div>
|
|
1103
1157
|
</>
|