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
@@ -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
- className={cn("flex w-full items-center gap-1 p-1", className)}
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
- "shrink-0 h-[32px] w-[32px] rounded-[4px]",
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-[20px] transition-all duration-100 ease-in-out",
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] flex-col gap-[10px]", headerClassName)}>
62
- <div className={cn(titleBadge({ color }))}>
63
- <span className="flex items-center gap-1.5">
64
- {icon}
65
- {title}
66
- </span>
67
- </div>
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
- <SelectPrimitive.Viewport>{children}</SelectPrimitive.Viewport>
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
- <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
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-[8px]",
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-scroll",
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
- "border-border-presentation-global-primary",
235
- "bg-background-presentation-form-base",
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-[4px] justify-between items-center outline-none",
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-none",
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-[6px] [&_span]:h-[32px] [&_span]:w-[32px] [&_p]:typography-body-large-regular [&_p]:px-[4px]",
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 "../utils/cn";
14
- import { Themes } from "../utils/types";
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 "../utils/markdownParser";
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
- "[&_.codex-editor__redactor]:min-h-[inherit] [&_.codex-editor__redactor]:pb-[100px]",
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] p-2",
732
- M: "min-h-[300px] p-3",
733
- L: "min-h-[400px] p-4",
734
- XL: "min-h-[500px] p-5",
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
  </>