@open-mercato/ui 0.6.1-develop.3246.1.dbef9d7392 → 0.6.1-develop.3260.1.dc719cac84

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 (164) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/__integration__/TC-AI-UI-003-aichat-registry.spec.tsx +13 -0
  3. package/dist/ai/AiAssistantLauncher.js +8 -0
  4. package/dist/ai/AiAssistantLauncher.js.map +2 -2
  5. package/dist/ai/AiChat.js +226 -58
  6. package/dist/ai/AiChat.js.map +2 -2
  7. package/dist/ai/AiDock.js +3 -2
  8. package/dist/ai/AiDock.js.map +2 -2
  9. package/dist/ai/ModelPicker.js +186 -0
  10. package/dist/ai/ModelPicker.js.map +7 -0
  11. package/dist/ai/index.js +3 -1
  12. package/dist/ai/index.js.map +2 -2
  13. package/dist/ai/useAiChat.js +12 -16
  14. package/dist/ai/useAiChat.js.map +2 -2
  15. package/dist/backend/CrudForm.js +46 -129
  16. package/dist/backend/CrudForm.js.map +3 -3
  17. package/dist/backend/DataTable.js +10 -3
  18. package/dist/backend/DataTable.js.map +2 -2
  19. package/dist/backend/EmptyState.js +1 -47
  20. package/dist/backend/EmptyState.js.map +2 -2
  21. package/dist/backend/FilterOverlay.js +31 -26
  22. package/dist/backend/FilterOverlay.js.map +2 -2
  23. package/dist/backend/FlashMessages.js +23 -25
  24. package/dist/backend/FlashMessages.js.map +2 -2
  25. package/dist/backend/JsonBuilder.js +4 -2
  26. package/dist/backend/JsonBuilder.js.map +2 -2
  27. package/dist/backend/LogList.js +49 -0
  28. package/dist/backend/LogList.js.map +7 -0
  29. package/dist/backend/crud/CollapsibleGroup.js +13 -3
  30. package/dist/backend/crud/CollapsibleGroup.js.map +2 -2
  31. package/dist/backend/detail/InlineEditors.js +80 -32
  32. package/dist/backend/detail/InlineEditors.js.map +2 -2
  33. package/dist/backend/inputs/ComboboxInput.js +60 -6
  34. package/dist/backend/inputs/ComboboxInput.js.map +2 -2
  35. package/dist/backend/inputs/DatePicker.js +6 -142
  36. package/dist/backend/inputs/DatePicker.js.map +2 -2
  37. package/dist/backend/inputs/DateTimePicker.js +6 -182
  38. package/dist/backend/inputs/DateTimePicker.js.map +2 -2
  39. package/dist/backend/inputs/TagsInput.js +16 -19
  40. package/dist/backend/inputs/TagsInput.js.map +2 -2
  41. package/dist/backend/inputs/TimeInput.js +1 -1
  42. package/dist/backend/inputs/TimeInput.js.map +2 -2
  43. package/dist/backend/inputs/TimePicker.js +83 -72
  44. package/dist/backend/inputs/TimePicker.js.map +2 -2
  45. package/dist/backend/utils/customFieldForms.js +2 -2
  46. package/dist/backend/utils/customFieldForms.js.map +1 -1
  47. package/dist/portal/components/PortalEmptyState.js +13 -9
  48. package/dist/portal/components/PortalEmptyState.js.map +2 -2
  49. package/dist/primitives/accordion.js +137 -0
  50. package/dist/primitives/accordion.js.map +7 -0
  51. package/dist/primitives/alert.js +180 -12
  52. package/dist/primitives/alert.js.map +2 -2
  53. package/dist/primitives/calendar.js +122 -32
  54. package/dist/primitives/calendar.js.map +2 -2
  55. package/dist/primitives/compact-select.js +47 -0
  56. package/dist/primitives/compact-select.js.map +7 -0
  57. package/dist/primitives/counter-input.js +174 -0
  58. package/dist/primitives/counter-input.js.map +7 -0
  59. package/dist/primitives/date-picker-helpers.js +79 -0
  60. package/dist/primitives/date-picker-helpers.js.map +7 -0
  61. package/dist/primitives/date-picker.js +216 -0
  62. package/dist/primitives/date-picker.js.map +7 -0
  63. package/dist/primitives/date-range-picker.js +239 -0
  64. package/dist/primitives/date-range-picker.js.map +7 -0
  65. package/dist/primitives/digit-input.js +157 -0
  66. package/dist/primitives/digit-input.js.map +7 -0
  67. package/dist/primitives/empty-state.js +93 -0
  68. package/dist/primitives/empty-state.js.map +7 -0
  69. package/dist/primitives/inline-input.js +29 -0
  70. package/dist/primitives/inline-input.js.map +7 -0
  71. package/dist/primitives/inline-select.js +41 -0
  72. package/dist/primitives/inline-select.js.map +7 -0
  73. package/dist/primitives/notification-stack.js +116 -0
  74. package/dist/primitives/notification-stack.js.map +7 -0
  75. package/dist/primitives/notification.js +62 -0
  76. package/dist/primitives/notification.js.map +7 -0
  77. package/dist/primitives/rich-editor.js +1230 -0
  78. package/dist/primitives/rich-editor.js.map +7 -0
  79. package/dist/primitives/select.js +1 -0
  80. package/dist/primitives/select.js.map +2 -2
  81. package/dist/primitives/skeleton.js +55 -0
  82. package/dist/primitives/skeleton.js.map +7 -0
  83. package/dist/primitives/tag-input.js +186 -0
  84. package/dist/primitives/tag-input.js.map +7 -0
  85. package/dist/primitives/tag.js +66 -13
  86. package/dist/primitives/tag.js.map +2 -2
  87. package/dist/primitives/time-picker.js +793 -0
  88. package/dist/primitives/time-picker.js.map +7 -0
  89. package/package.json +4 -3
  90. package/src/ai/AiAssistantLauncher.tsx +8 -0
  91. package/src/ai/AiChat.tsx +229 -11
  92. package/src/ai/AiDock.tsx +3 -2
  93. package/src/ai/ModelPicker.tsx +248 -0
  94. package/src/ai/__tests__/AiAssistantLauncher.test.tsx +58 -0
  95. package/src/ai/__tests__/AiChat.conversation.test.tsx +16 -0
  96. package/src/ai/__tests__/AiChat.debug.test.tsx +16 -0
  97. package/src/ai/__tests__/AiChat.registry.test.tsx +16 -0
  98. package/src/ai/__tests__/AiChat.test.tsx +180 -0
  99. package/src/ai/__tests__/AiChat.thinking-unmount.test.tsx +16 -0
  100. package/src/ai/__tests__/AiDock.test.tsx +12 -1
  101. package/src/ai/__tests__/ModelPicker.test.tsx +252 -0
  102. package/src/ai/index.ts +1 -0
  103. package/src/ai/useAiChat.ts +29 -16
  104. package/src/backend/CrudForm.tsx +61 -113
  105. package/src/backend/DataTable.tsx +17 -5
  106. package/src/backend/EmptyState.tsx +13 -63
  107. package/src/backend/FilterOverlay.tsx +38 -22
  108. package/src/backend/FlashMessages.tsx +23 -24
  109. package/src/backend/JsonBuilder.tsx +4 -2
  110. package/src/backend/LogList.tsx +109 -0
  111. package/src/backend/__tests__/CrudForm.datetime.test.tsx +12 -7
  112. package/src/backend/__tests__/FilterOverlay.dateRange.test.tsx +89 -0
  113. package/src/backend/__tests__/LogList.test.tsx +163 -0
  114. package/src/backend/crud/CollapsibleGroup.tsx +17 -3
  115. package/src/backend/detail/InlineEditors.tsx +78 -25
  116. package/src/backend/detail/__tests__/InlineEditors.test.tsx +81 -1
  117. package/src/backend/inputs/ComboboxInput.tsx +81 -9
  118. package/src/backend/inputs/DatePicker.tsx +28 -155
  119. package/src/backend/inputs/DateTimePicker.tsx +25 -193
  120. package/src/backend/inputs/TagsInput.tsx +10 -13
  121. package/src/backend/inputs/TimeInput.tsx +2 -1
  122. package/src/backend/inputs/TimePicker.tsx +99 -70
  123. package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +133 -0
  124. package/src/backend/inputs/__tests__/TimePicker.test.tsx +2 -2
  125. package/src/backend/utils/__tests__/richTextSanitizer.test.ts +5 -2
  126. package/src/backend/utils/customFieldForms.ts +2 -2
  127. package/src/portal/components/PortalEmptyState.tsx +27 -22
  128. package/src/primitives/__tests__/accordion.test.tsx +197 -0
  129. package/src/primitives/__tests__/alert.test.tsx +204 -0
  130. package/src/primitives/__tests__/compact-select.test.tsx +98 -0
  131. package/src/primitives/__tests__/counter-input.test.tsx +165 -0
  132. package/src/primitives/__tests__/date-picker.test.tsx +221 -0
  133. package/src/primitives/__tests__/date-range-picker.test.tsx +173 -0
  134. package/src/primitives/__tests__/digit-input.test.tsx +168 -0
  135. package/src/primitives/__tests__/empty-state.test.tsx +154 -0
  136. package/src/primitives/__tests__/inline-input.test.tsx +100 -0
  137. package/src/primitives/__tests__/inline-select.test.tsx +103 -0
  138. package/src/primitives/__tests__/notification-stack.test.tsx +174 -0
  139. package/src/primitives/__tests__/notification.test.tsx +119 -0
  140. package/src/primitives/__tests__/rich-editor.test.tsx +411 -0
  141. package/src/primitives/__tests__/skeleton.test.tsx +76 -0
  142. package/src/primitives/__tests__/tag-input.test.tsx +198 -0
  143. package/src/primitives/__tests__/tag.test.tsx +123 -0
  144. package/src/primitives/__tests__/time-picker.test.tsx +392 -0
  145. package/src/primitives/accordion.tsx +225 -0
  146. package/src/primitives/alert.tsx +234 -24
  147. package/src/primitives/calendar.tsx +146 -38
  148. package/src/primitives/compact-select.tsx +59 -0
  149. package/src/primitives/counter-input.tsx +211 -0
  150. package/src/primitives/date-picker-helpers.ts +96 -0
  151. package/src/primitives/date-picker.tsx +290 -0
  152. package/src/primitives/date-range-picker.tsx +281 -0
  153. package/src/primitives/digit-input.tsx +208 -0
  154. package/src/primitives/empty-state.tsx +140 -0
  155. package/src/primitives/inline-input.tsx +55 -0
  156. package/src/primitives/inline-select.tsx +74 -0
  157. package/src/primitives/notification-stack.tsx +185 -0
  158. package/src/primitives/notification.tsx +136 -0
  159. package/src/primitives/rich-editor.tsx +1578 -0
  160. package/src/primitives/select.tsx +1 -0
  161. package/src/primitives/skeleton.tsx +70 -0
  162. package/src/primitives/tag-input.tsx +223 -0
  163. package/src/primitives/tag.tsx +69 -10
  164. package/src/primitives/time-picker.tsx +1059 -0
@@ -0,0 +1,1230 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import {
5
+ AlignCenter,
6
+ AlignJustify,
7
+ AlignLeft,
8
+ AlignRight,
9
+ AtSign,
10
+ Bold,
11
+ ChevronDown,
12
+ Code,
13
+ FileCode,
14
+ HelpCircle,
15
+ Image as ImageIcon,
16
+ Italic,
17
+ Link,
18
+ List,
19
+ ListChecks,
20
+ ListOrdered,
21
+ Maximize2,
22
+ MessageCircle,
23
+ Minus,
24
+ MoreVertical,
25
+ Quote,
26
+ Strikethrough,
27
+ Table as TableIcon,
28
+ Underline
29
+ } from "lucide-react";
30
+ import { cva } from "class-variance-authority";
31
+ import { cn } from "@open-mercato/shared/lib/utils";
32
+ import {
33
+ sanitizeHtmlRichText,
34
+ sanitizeRichTextHref,
35
+ sanitizeRichTextPasteContent
36
+ } from "@open-mercato/ui/backend/utils/richTextSanitizer";
37
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover.js";
38
+ import {
39
+ Tooltip,
40
+ TooltipContent,
41
+ TooltipProvider,
42
+ TooltipTrigger
43
+ } from "./tooltip.js";
44
+ import {
45
+ Dialog,
46
+ DialogContent,
47
+ DialogFooter,
48
+ DialogHeader,
49
+ DialogTitle
50
+ } from "./dialog.js";
51
+ import { Input } from "./input.js";
52
+ import { Button } from "./button.js";
53
+ const RICH_EDITOR_COLOR_PALETTE = {
54
+ gray: "#7b7b7b",
55
+ black: "#171717",
56
+ white: "#ffffff",
57
+ blue: "#6366f1",
58
+ orange: "#f59e0b",
59
+ red: "#ef4444",
60
+ green: "#22c55e",
61
+ yellow: "#f6b51e",
62
+ purple: "#7d52f4",
63
+ sky: "#47c2ff",
64
+ pink: "#fb4ba3",
65
+ teal: "#22d3bb"
66
+ };
67
+ const COLOR_KEYS = [
68
+ "gray",
69
+ "black",
70
+ "white",
71
+ "blue",
72
+ "orange",
73
+ "red",
74
+ "green",
75
+ "yellow",
76
+ "purple",
77
+ "sky",
78
+ "pink",
79
+ "teal"
80
+ ];
81
+ const COLOR_LABELS_EN = {
82
+ gray: "Gray",
83
+ black: "Black",
84
+ white: "White",
85
+ blue: "Blue",
86
+ orange: "Orange",
87
+ red: "Red",
88
+ green: "Green",
89
+ yellow: "Yellow",
90
+ purple: "Purple",
91
+ sky: "Sky",
92
+ pink: "Pink",
93
+ teal: "Teal"
94
+ };
95
+ const ALIGN_ICONS = {
96
+ left: AlignLeft,
97
+ center: AlignCenter,
98
+ right: AlignRight,
99
+ justify: AlignJustify
100
+ };
101
+ const FONT_SIZE_OPTIONS = ["12px", "14px", "16px", "18px", "20px", "24px"];
102
+ const EMPTY_SELECTION_STATE = {
103
+ bold: false,
104
+ italic: false,
105
+ underline: false,
106
+ strikethrough: false,
107
+ unorderedList: false,
108
+ orderedList: false,
109
+ blockquote: false,
110
+ code: false,
111
+ heading: "",
112
+ align: "left"
113
+ };
114
+ const RichEditorContext = React.createContext(null);
115
+ function useRichEditorContext(component) {
116
+ const ctx = React.useContext(RichEditorContext);
117
+ if (!ctx) {
118
+ throw new Error(`${component} must be rendered inside <RichEditor>`);
119
+ }
120
+ return ctx;
121
+ }
122
+ const DEFAULT_LABELS = {
123
+ bold: "Bold",
124
+ italic: "Italic",
125
+ underline: "Underline",
126
+ strikethrough: "Strikethrough",
127
+ unorderedList: "Bullet list",
128
+ orderedList: "Numbered list",
129
+ checklist: "Checklist",
130
+ blockquote: "Quote",
131
+ code: "Code",
132
+ inlineCode: "Inline code",
133
+ codeBlock: "Code block",
134
+ horizontalRule: "Horizontal rule",
135
+ image: "Image",
136
+ imageUrlPrompt: "Enter image URL",
137
+ table: "Table",
138
+ heading: "Header",
139
+ heading1: "Heading 1",
140
+ heading2: "Heading 2",
141
+ heading3: "Heading 3",
142
+ paragraph: "Paragraph",
143
+ link: "Link",
144
+ linkUrlPrompt: "Enter URL",
145
+ color: "Text color",
146
+ textColor: "Color",
147
+ fontSize: "Font size",
148
+ align: "Align",
149
+ alignLeft: "Align left",
150
+ alignCenter: "Align center",
151
+ alignRight: "Align right",
152
+ alignJustify: "Justify",
153
+ comment: "Add comment",
154
+ mention: "Mention",
155
+ more: "More",
156
+ help: "Help",
157
+ fullscreen: "Toggle fullscreen",
158
+ placeholder: "Placeholder text...",
159
+ colors: COLOR_LABELS_EN
160
+ };
161
+ function deriveSelectionState() {
162
+ if (typeof document === "undefined" || !document.queryCommandState) {
163
+ return EMPTY_SELECTION_STATE;
164
+ }
165
+ try {
166
+ const block = document.queryCommandValue?.("formatBlock") ?? "";
167
+ const lower = String(block).toLowerCase().replace(/[<>]/g, "");
168
+ const heading = lower === "h1" || lower === "h2" || lower === "h3" ? lower : "";
169
+ const align = document.queryCommandState("justifyCenter") ? "center" : document.queryCommandState("justifyRight") ? "right" : document.queryCommandState("justifyFull") ? "justify" : "left";
170
+ return {
171
+ bold: document.queryCommandState("bold"),
172
+ italic: document.queryCommandState("italic"),
173
+ underline: document.queryCommandState("underline"),
174
+ strikethrough: document.queryCommandState("strikeThrough"),
175
+ unorderedList: document.queryCommandState("insertUnorderedList"),
176
+ orderedList: document.queryCommandState("insertOrderedList"),
177
+ blockquote: lower === "blockquote",
178
+ code: lower === "pre",
179
+ heading,
180
+ align
181
+ };
182
+ } catch {
183
+ return EMPTY_SELECTION_STATE;
184
+ }
185
+ }
186
+ const RichEditor = React.memo(
187
+ function RichEditor2({
188
+ value = "",
189
+ onChange,
190
+ variant = "standard",
191
+ placeholder,
192
+ minRows = 4,
193
+ disabled = false,
194
+ className,
195
+ contentClassName,
196
+ labels: labelsProp,
197
+ onComment,
198
+ onMention,
199
+ moreMenu,
200
+ onFullscreen,
201
+ onImageInsert,
202
+ maxLength,
203
+ children,
204
+ id,
205
+ name,
206
+ "aria-invalid": ariaInvalid
207
+ }) {
208
+ const labels = React.useMemo(
209
+ () => ({
210
+ ...DEFAULT_LABELS,
211
+ ...labelsProp ?? {},
212
+ colors: { ...DEFAULT_LABELS.colors, ...labelsProp?.colors ?? {} }
213
+ }),
214
+ [labelsProp]
215
+ );
216
+ const editorRef = React.useRef(null);
217
+ const applyingExternal = React.useRef(false);
218
+ const typingRef = React.useRef(false);
219
+ const savedSelectionRef = React.useRef(null);
220
+ const [selection, setSelection] = React.useState(EMPTY_SELECTION_STATE);
221
+ const [promptDialog, setPromptDialog] = React.useState(null);
222
+ React.useEffect(() => {
223
+ const el = editorRef.current;
224
+ if (!el) return;
225
+ const sanitizedValue = sanitizeHtmlRichText(value);
226
+ if (!typingRef.current && el.innerHTML !== sanitizedValue) {
227
+ applyingExternal.current = true;
228
+ el.innerHTML = sanitizedValue;
229
+ requestAnimationFrame(() => {
230
+ applyingExternal.current = false;
231
+ });
232
+ }
233
+ }, [value]);
234
+ const refreshSelectionState = React.useCallback(() => {
235
+ setSelection(deriveSelectionState());
236
+ }, []);
237
+ const exec = React.useCallback(
238
+ (command, arg) => {
239
+ const el = editorRef.current;
240
+ if (!el || disabled) return;
241
+ el.focus();
242
+ try {
243
+ document.execCommand("styleWithCSS", false, "true");
244
+ } catch {
245
+ }
246
+ try {
247
+ document.execCommand(command, false, arg);
248
+ } catch {
249
+ }
250
+ refreshSelectionState();
251
+ },
252
+ [disabled, refreshSelectionState]
253
+ );
254
+ const requestUrlPrompt = React.useCallback(
255
+ (request) => {
256
+ if (typeof window !== "undefined") {
257
+ const sel = window.getSelection();
258
+ if (sel && sel.rangeCount > 0 && editorRef.current?.contains(sel.anchorNode)) {
259
+ savedSelectionRef.current = sel.getRangeAt(0).cloneRange();
260
+ } else {
261
+ savedSelectionRef.current = null;
262
+ }
263
+ }
264
+ setPromptDialog({ ...request, value: "" });
265
+ },
266
+ []
267
+ );
268
+ const ctx = React.useMemo(
269
+ () => ({ exec, selection, disabled, requestUrlPrompt }),
270
+ [exec, selection, disabled, requestUrlPrompt]
271
+ );
272
+ React.useEffect(() => {
273
+ if (typeof document === "undefined") return;
274
+ const onSelectionChange = () => {
275
+ if (!editorRef.current) return;
276
+ const sel = window.getSelection();
277
+ if (!sel || !sel.anchorNode) return;
278
+ if (!editorRef.current.contains(sel.anchorNode)) return;
279
+ refreshSelectionState();
280
+ };
281
+ document.addEventListener("selectionchange", onSelectionChange);
282
+ return () => {
283
+ document.removeEventListener("selectionchange", onSelectionChange);
284
+ };
285
+ }, [refreshSelectionState]);
286
+ const onKeyDown = React.useCallback(
287
+ (e) => {
288
+ const isMod = e.metaKey || e.ctrlKey;
289
+ if (!isMod) return;
290
+ const k = e.key.toLowerCase();
291
+ if (k === "b") {
292
+ e.preventDefault();
293
+ exec("bold");
294
+ } else if (k === "i") {
295
+ e.preventDefault();
296
+ exec("italic");
297
+ } else if (k === "u") {
298
+ e.preventDefault();
299
+ exec("underline");
300
+ }
301
+ },
302
+ [exec]
303
+ );
304
+ const onPaste = React.useCallback(
305
+ (e) => {
306
+ const html = e.clipboardData.getData("text/html");
307
+ const text = e.clipboardData.getData("text/plain");
308
+ const sanitizedPaste = sanitizeRichTextPasteContent(html, text);
309
+ if (!sanitizedPaste) return;
310
+ e.preventDefault();
311
+ exec(sanitizedPaste.command, sanitizedPaste.value);
312
+ },
313
+ [exec]
314
+ );
315
+ const onBlur = React.useCallback(() => {
316
+ const el = editorRef.current;
317
+ if (!el) return;
318
+ typingRef.current = false;
319
+ const sanitized = sanitizeHtmlRichText(el.innerHTML);
320
+ if (el.innerHTML !== sanitized) {
321
+ applyingExternal.current = true;
322
+ el.innerHTML = sanitized;
323
+ requestAnimationFrame(() => {
324
+ applyingExternal.current = false;
325
+ });
326
+ }
327
+ onChange(sanitized);
328
+ }, [onChange]);
329
+ const presetItems = usePresetToolbarItems({
330
+ variant,
331
+ labels,
332
+ onComment,
333
+ onMention,
334
+ onFullscreen,
335
+ onImageInsert,
336
+ exec,
337
+ selection,
338
+ requestUrlPrompt
339
+ });
340
+ const plaintextLength = React.useMemo(() => {
341
+ if (!maxLength) return 0;
342
+ if (typeof document === "undefined") return 0;
343
+ const tmp = document.createElement("div");
344
+ tmp.innerHTML = sanitizeHtmlRichText(value);
345
+ return (tmp.textContent ?? "").length;
346
+ }, [value, maxLength]);
347
+ return /* @__PURE__ */ jsx(RichEditorContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxs(TooltipProvider, { delayDuration: 400, children: [
348
+ /* @__PURE__ */ jsx(
349
+ "div",
350
+ {
351
+ className: cn("w-full space-y-2", disabled && "opacity-60", className),
352
+ "data-slot": "rich-editor",
353
+ "data-disabled": disabled ? "true" : "false",
354
+ "aria-invalid": ariaInvalid,
355
+ children: variant === "custom" ? children : /* @__PURE__ */ jsxs(Fragment, { children: [
356
+ /* @__PURE__ */ jsx(RichEditorAutoToolbar, { items: presetItems, labels, moreMenu }),
357
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
358
+ /* @__PURE__ */ jsx(
359
+ RichEditorContent,
360
+ {
361
+ ref: editorRef,
362
+ placeholder: placeholder ?? labels.placeholder,
363
+ minRows,
364
+ disabled,
365
+ className: contentClassName,
366
+ onKeyDown,
367
+ onPaste,
368
+ onBlur,
369
+ onInput: () => {
370
+ if (!applyingExternal.current) typingRef.current = true;
371
+ },
372
+ id,
373
+ name
374
+ }
375
+ ),
376
+ maxLength ? /* @__PURE__ */ jsxs(
377
+ "span",
378
+ {
379
+ className: cn(
380
+ "pointer-events-none absolute bottom-2 right-3 select-none text-xs leading-4 text-muted-foreground",
381
+ plaintextLength > maxLength && "text-destructive"
382
+ ),
383
+ "data-slot": "rich-editor-counter",
384
+ "aria-live": "polite",
385
+ children: [
386
+ plaintextLength,
387
+ "/",
388
+ maxLength
389
+ ]
390
+ }
391
+ ) : null
392
+ ] })
393
+ ] })
394
+ }
395
+ ),
396
+ promptDialog !== null ? /* @__PURE__ */ jsx(
397
+ Dialog,
398
+ {
399
+ open: true,
400
+ onOpenChange: (open) => {
401
+ if (!open) setPromptDialog(null);
402
+ },
403
+ children: /* @__PURE__ */ jsxs(
404
+ DialogContent,
405
+ {
406
+ className: "sm:max-w-md",
407
+ onKeyDown: (e) => {
408
+ if (e.key === "Enter" && promptDialog) {
409
+ e.preventDefault();
410
+ const url = sanitizeRichTextHref(promptDialog.value);
411
+ const onConfirm = promptDialog.onConfirm;
412
+ setPromptDialog(null);
413
+ if (!url) return;
414
+ const el = editorRef.current;
415
+ const range = savedSelectionRef.current;
416
+ if (el) {
417
+ el.focus();
418
+ if (range) {
419
+ const sel = window.getSelection();
420
+ sel?.removeAllRanges();
421
+ sel?.addRange(range);
422
+ }
423
+ }
424
+ onConfirm(url);
425
+ }
426
+ },
427
+ children: [
428
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: promptDialog?.title ?? "" }) }),
429
+ /* @__PURE__ */ jsx(
430
+ Input,
431
+ {
432
+ autoFocus: true,
433
+ type: "url",
434
+ value: promptDialog?.value ?? "",
435
+ placeholder: promptDialog?.placeholder ?? "https://\u2026",
436
+ onChange: (e) => {
437
+ const next = e.target.value;
438
+ setPromptDialog((prev) => prev ? { ...prev, value: next } : prev);
439
+ }
440
+ }
441
+ ),
442
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
443
+ /* @__PURE__ */ jsx(
444
+ Button,
445
+ {
446
+ type: "button",
447
+ variant: "outline",
448
+ onClick: () => setPromptDialog(null),
449
+ children: "Cancel"
450
+ }
451
+ ),
452
+ /* @__PURE__ */ jsx(
453
+ Button,
454
+ {
455
+ type: "button",
456
+ onClick: () => {
457
+ if (!promptDialog) return;
458
+ const url = sanitizeRichTextHref(promptDialog.value);
459
+ const onConfirm = promptDialog.onConfirm;
460
+ setPromptDialog(null);
461
+ if (!url) return;
462
+ const el = editorRef.current;
463
+ const range = savedSelectionRef.current;
464
+ if (el) {
465
+ el.focus();
466
+ if (range) {
467
+ const sel = window.getSelection();
468
+ sel?.removeAllRanges();
469
+ sel?.addRange(range);
470
+ }
471
+ }
472
+ onConfirm(url);
473
+ },
474
+ children: "OK"
475
+ }
476
+ )
477
+ ] })
478
+ ]
479
+ }
480
+ )
481
+ }
482
+ ) : null
483
+ ] }) });
484
+ },
485
+ (prev, next) => prev.value === next.value && prev.disabled === next.disabled && prev.variant === next.variant && prev.maxLength === next.maxLength
486
+ );
487
+ RichEditor.displayName = "RichEditor";
488
+ const RichEditorToolbar = React.forwardRef(
489
+ ({ className, children, ...props }, ref) => /* @__PURE__ */ jsx(
490
+ "div",
491
+ {
492
+ ref,
493
+ role: "toolbar",
494
+ "aria-label": "Rich text formatting",
495
+ className: cn(
496
+ // Figma 166331:4006 reference: standalone card with rounded-8 + border + shadow-xs.
497
+ // 2px outer padding, 2px gap between items.
498
+ "flex w-fit max-w-full flex-wrap items-center gap-0.5 overflow-x-auto rounded-lg border border-border bg-card p-0.5 shadow-xs",
499
+ className
500
+ ),
501
+ "data-slot": "rich-editor-toolbar",
502
+ ...props,
503
+ children
504
+ }
505
+ )
506
+ );
507
+ RichEditorToolbar.displayName = "RichEditorToolbar";
508
+ const richEditorItemVariants = cva(
509
+ // Figma Rich Editor Items (166251 family): 28×h, rounded-6, bg-white default,
510
+ // bg-weak-50 (= bg-muted token) on hover, on toggle-active (data-active=true,
511
+ // e.g. Bold inside a bold selection), and on dropdown-open
512
+ // (data-state=open from Radix PopoverTrigger). Text / icon colours move from
513
+ // sub-600 (= text-muted-foreground, #5c5c5c) in the default state to
514
+ // strong-950 (= text-foreground, #171717) in the active / open states per
515
+ // Figma 166261:22214 / 166261:22217 reference cells.
516
+ "group/rich-editor-item inline-flex h-7 shrink-0 items-center justify-center rounded-md bg-transparent text-sm font-medium leading-5 text-muted-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-40 data-[active=true]:bg-muted data-[active=true]:text-foreground data-[state=open]:bg-muted data-[state=open]:text-foreground",
517
+ {
518
+ variants: {
519
+ type: {
520
+ // Figma 166251:2698 — pl-[10px] pr-[4px] py-[4px], gap-[2px].
521
+ text: "min-w-0 gap-0.5 pl-2.5 pr-1 py-1 tracking-[-0.084px] [&_svg]:size-4",
522
+ // Figma 166251:2700 — square icon button p-[4px].
523
+ icon: "aspect-square p-1 [&_svg]:size-4",
524
+ // Figma 166251:3337 — icon + caret p-[4px] gap-[2px].
525
+ dropdown: "gap-0.5 p-1 [&_svg]:size-4",
526
+ // Figma 166331:4437 — color swatch + caret pl-[8px] pr-[4px] py-[4px].
527
+ color: "gap-0.5 pl-2 pr-1 py-1 [&_svg]:size-4"
528
+ }
529
+ },
530
+ defaultVariants: { type: "icon" }
531
+ }
532
+ );
533
+ const RichEditorButton = React.forwardRef(
534
+ ({ className, type, active, tooltipLabel, children, onMouseDown, ...props }, ref) => {
535
+ const button = /* @__PURE__ */ jsx(
536
+ "button",
537
+ {
538
+ ref,
539
+ type: "button",
540
+ onMouseDown: (e) => {
541
+ e.preventDefault();
542
+ onMouseDown?.(e);
543
+ },
544
+ "data-active": active ? "true" : "false",
545
+ "aria-pressed": active,
546
+ className: cn(richEditorItemVariants({ type }), className),
547
+ ...props,
548
+ children
549
+ }
550
+ );
551
+ if (!tooltipLabel) return button;
552
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
553
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: button }),
554
+ /* @__PURE__ */ jsx(TooltipContent, { side: "top", sideOffset: 6, children: tooltipLabel })
555
+ ] });
556
+ }
557
+ );
558
+ RichEditorButton.displayName = "RichEditorButton";
559
+ const RichEditorIconButton = React.forwardRef(
560
+ ({ icon, ariaLabel, command, commandArg, onActivate, active, onClick, ...props }, ref) => {
561
+ const { exec, disabled } = useRichEditorContext("RichEditorIconButton");
562
+ return /* @__PURE__ */ jsx(
563
+ RichEditorButton,
564
+ {
565
+ ref,
566
+ type: "icon",
567
+ active,
568
+ "aria-label": ariaLabel,
569
+ disabled: disabled || props.disabled,
570
+ onClick: (e) => {
571
+ if (command) exec(command, commandArg);
572
+ onActivate?.();
573
+ onClick?.(e);
574
+ },
575
+ ...props,
576
+ children: /* @__PURE__ */ jsx("span", { "data-slot": "rich-editor-item-icon", "aria-hidden": "true", children: icon })
577
+ }
578
+ );
579
+ }
580
+ );
581
+ RichEditorIconButton.displayName = "RichEditorIconButton";
582
+ const RichEditorTextDropdown = React.forwardRef(
583
+ ({ label, ariaLabel, menu, onActivate, ...props }, ref) => {
584
+ const { disabled } = useRichEditorContext("RichEditorTextDropdown");
585
+ const button = /* @__PURE__ */ jsxs(
586
+ RichEditorButton,
587
+ {
588
+ ref,
589
+ type: "text",
590
+ "aria-label": ariaLabel,
591
+ disabled: disabled || props.disabled,
592
+ onClick: onActivate,
593
+ ...props,
594
+ children: [
595
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: label }),
596
+ /* @__PURE__ */ jsx(ChevronDown, { className: "opacity-60 transition-transform group-data-[state=open]/rich-editor-item:rotate-180", "aria-hidden": "true" })
597
+ ]
598
+ }
599
+ );
600
+ if (!menu) return button;
601
+ return /* @__PURE__ */ jsxs(Popover, { children: [
602
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: button }),
603
+ /* @__PURE__ */ jsx(PopoverContent, { align: "start", sideOffset: 6, className: "w-44 p-1", children: menu })
604
+ ] });
605
+ }
606
+ );
607
+ RichEditorTextDropdown.displayName = "RichEditorTextDropdown";
608
+ const RichEditorDropdownButton = React.forwardRef(
609
+ ({ icon, ariaLabel, menu, showChevron, ...props }, ref) => {
610
+ const { disabled } = useRichEditorContext("RichEditorDropdownButton");
611
+ return /* @__PURE__ */ jsxs(Popover, { children: [
612
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
613
+ RichEditorButton,
614
+ {
615
+ ref,
616
+ type: "dropdown",
617
+ "aria-label": ariaLabel,
618
+ disabled: disabled || props.disabled,
619
+ ...props,
620
+ children: [
621
+ /* @__PURE__ */ jsx("span", { "data-slot": "rich-editor-item-icon", "aria-hidden": "true", children: icon }),
622
+ showChevron === false ? null : /* @__PURE__ */ jsx(ChevronDown, { className: "opacity-60 transition-transform group-data-[state=open]/rich-editor-item:rotate-180", "aria-hidden": "true" })
623
+ ]
624
+ }
625
+ ) }),
626
+ /* @__PURE__ */ jsx(PopoverContent, { align: "start", sideOffset: 6, className: "w-44 p-1", children: menu })
627
+ ] });
628
+ }
629
+ );
630
+ RichEditorDropdownButton.displayName = "RichEditorDropdownButton";
631
+ const RichEditorColorButton = React.forwardRef(
632
+ ({ colorValue, ariaLabel, command = "foreColor", onSelect, colorLabels, palette = COLOR_KEYS, ...props }, ref) => {
633
+ const { exec, disabled } = useRichEditorContext("RichEditorColorButton");
634
+ const [lastSelected, setLastSelected] = React.useState(null);
635
+ const effectiveValue = colorValue ?? lastSelected;
636
+ const swatchColor = effectiveValue ? RICH_EDITOR_COLOR_PALETTE[effectiveValue] : RICH_EDITOR_COLOR_PALETTE.blue;
637
+ return /* @__PURE__ */ jsxs(Popover, { children: [
638
+ /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
639
+ RichEditorButton,
640
+ {
641
+ ref,
642
+ type: "color",
643
+ "aria-label": ariaLabel,
644
+ disabled: disabled || props.disabled,
645
+ ...props,
646
+ children: [
647
+ /* @__PURE__ */ jsx(
648
+ "span",
649
+ {
650
+ className: "inline-block size-4 shrink-0 rounded-full border border-black/10",
651
+ style: { backgroundColor: swatchColor },
652
+ "data-slot": "rich-editor-color-swatch",
653
+ "aria-hidden": "true"
654
+ }
655
+ ),
656
+ /* @__PURE__ */ jsx(ChevronDown, { className: "opacity-60 transition-transform group-data-[state=open]/rich-editor-item:rotate-180", "aria-hidden": "true" })
657
+ ]
658
+ }
659
+ ) }),
660
+ /* @__PURE__ */ jsx(PopoverContent, { align: "start", sideOffset: 6, className: "w-36 p-1", children: /* @__PURE__ */ jsx(
661
+ RichEditorColorPalette,
662
+ {
663
+ value: effectiveValue,
664
+ labels: colorLabels,
665
+ palette,
666
+ onChange: (key) => {
667
+ if (key) exec(command, RICH_EDITOR_COLOR_PALETTE[key]);
668
+ setLastSelected(key);
669
+ onSelect?.(key);
670
+ }
671
+ }
672
+ ) })
673
+ ] });
674
+ }
675
+ );
676
+ RichEditorColorButton.displayName = "RichEditorColorButton";
677
+ function RichEditorColorPalette({ value, onChange, labels, palette, className }) {
678
+ const resolvedLabels = { ...COLOR_LABELS_EN, ...labels ?? {} };
679
+ const keys = palette && palette.length > 0 ? palette : COLOR_KEYS;
680
+ return /* @__PURE__ */ jsx(
681
+ "div",
682
+ {
683
+ className: cn("flex flex-col gap-0.5", className),
684
+ "data-slot": "rich-editor-color-palette",
685
+ role: "listbox",
686
+ "aria-label": "Color palette",
687
+ children: keys.map((key) => {
688
+ const isActive = value === key;
689
+ return /* @__PURE__ */ jsxs(
690
+ "button",
691
+ {
692
+ type: "button",
693
+ role: "option",
694
+ "aria-selected": isActive,
695
+ "aria-label": resolvedLabels[key],
696
+ onMouseDown: (e) => e.preventDefault(),
697
+ onClick: () => onChange?.(key),
698
+ className: cn(
699
+ "flex items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/40",
700
+ isActive && "font-medium text-foreground"
701
+ ),
702
+ "data-color-key": key,
703
+ children: [
704
+ /* @__PURE__ */ jsx(
705
+ "span",
706
+ {
707
+ className: "size-4 shrink-0 rounded-full border border-black/10",
708
+ style: { backgroundColor: RICH_EDITOR_COLOR_PALETTE[key] },
709
+ "aria-hidden": "true"
710
+ }
711
+ ),
712
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: resolvedLabels[key] })
713
+ ]
714
+ },
715
+ key
716
+ );
717
+ })
718
+ }
719
+ );
720
+ }
721
+ const RichEditorMenuItem = React.forwardRef(
722
+ ({ className, icon, children, destructive, onMouseDown, ...props }, ref) => /* @__PURE__ */ jsxs(
723
+ "button",
724
+ {
725
+ ref,
726
+ type: "button",
727
+ role: "menuitem",
728
+ onMouseDown: (e) => {
729
+ e.preventDefault();
730
+ onMouseDown?.(e);
731
+ },
732
+ className: cn(
733
+ "flex w-full cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-left text-sm text-foreground transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50",
734
+ destructive && "text-destructive hover:bg-destructive/10",
735
+ className
736
+ ),
737
+ "data-slot": "rich-editor-menu-item",
738
+ ...props,
739
+ children: [
740
+ icon ? /* @__PURE__ */ jsx(
741
+ "span",
742
+ {
743
+ className: "inline-flex size-4 shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-4",
744
+ "data-slot": "rich-editor-menu-item-icon",
745
+ "aria-hidden": "true",
746
+ children: icon
747
+ }
748
+ ) : null,
749
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children })
750
+ ]
751
+ }
752
+ )
753
+ );
754
+ RichEditorMenuItem.displayName = "RichEditorMenuItem";
755
+ function RichEditorDivider({ className }) {
756
+ return /* @__PURE__ */ jsx(
757
+ "span",
758
+ {
759
+ role: "separator",
760
+ "aria-orientation": "vertical",
761
+ className: cn("mx-px inline-block h-4 w-px shrink-0 bg-border", className),
762
+ "data-slot": "rich-editor-divider"
763
+ }
764
+ );
765
+ }
766
+ const RichEditorContent = React.forwardRef(
767
+ ({ className, placeholder, minRows = 4, disabled, onBlur, onInput, onKeyDown, onPaste, id, name, ...props }, ref) => {
768
+ const minHeight = `${Math.max(1, minRows) * 1.5}rem`;
769
+ return /* @__PURE__ */ jsx(
770
+ "div",
771
+ {
772
+ ref,
773
+ contentEditable: !disabled,
774
+ suppressContentEditableWarning: true,
775
+ spellCheck: true,
776
+ role: "textbox",
777
+ "aria-multiline": "true",
778
+ "data-slot": "rich-editor-content",
779
+ "data-placeholder": placeholder ?? "",
780
+ "data-name": name,
781
+ id,
782
+ className: cn(
783
+ // Figma reference: content lives in its own bordered card with rounded-8 + shadow-xs,
784
+ // visually separated from the toolbar by the small vertical gap from the parent
785
+ // wrapper's `space-y-2`.
786
+ "w-full max-w-none rounded-lg border border-border bg-card px-3 py-3 text-sm leading-6 text-foreground shadow-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40",
787
+ // The project does not include `@tailwindcss/typography`, so we have to
788
+ // re-implement the rich text rendering ourselves via descendant-arbitrary
789
+ // selectors. Cover every tag the toolbar can produce so the user sees
790
+ // the formatting instantly after each execCommand.
791
+ "[&_h1]:mt-4 [&_h1]:mb-2 [&_h1]:text-2xl [&_h1]:font-bold",
792
+ "[&_h2]:mt-3 [&_h2]:mb-2 [&_h2]:text-xl [&_h2]:font-semibold",
793
+ "[&_h3]:mt-3 [&_h3]:mb-1 [&_h3]:text-lg [&_h3]:font-semibold",
794
+ "[&_p]:my-1",
795
+ "[&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-6",
796
+ "[&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-6",
797
+ "[&_li]:my-0.5",
798
+ // Task list checklist override — when <ul data-task-list> we drop the
799
+ // disc bullet so the <input type=checkbox> child stands alone.
800
+ "[&_ul[data-task-list]]:list-none [&_ul[data-task-list]]:pl-0",
801
+ "[&_ul[data-task-list]_input[type=checkbox]]:mr-2",
802
+ "[&_blockquote]:my-2 [&_blockquote]:border-l-4 [&_blockquote]:border-border [&_blockquote]:pl-4 [&_blockquote]:italic [&_blockquote]:text-muted-foreground",
803
+ "[&_pre]:my-2 [&_pre]:rounded [&_pre]:bg-muted [&_pre]:p-3 [&_pre]:font-mono [&_pre]:text-xs",
804
+ "[&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:font-mono [&_code]:text-xs",
805
+ "[&_a]:text-primary [&_a]:underline",
806
+ "[&_hr]:my-3 [&_hr]:border-border",
807
+ "[&_img]:my-2 [&_img]:h-auto [&_img]:max-w-full [&_img]:rounded",
808
+ "[&_table]:my-2 [&_table]:border-collapse",
809
+ "[&_td]:border [&_td]:border-border [&_td]:px-2 [&_td]:py-1",
810
+ "[&_th]:border [&_th]:border-border [&_th]:bg-muted [&_th]:px-2 [&_th]:py-1",
811
+ "data-[placeholder]:before:pointer-events-none data-[placeholder]:empty:before:content-[attr(data-placeholder)] data-[placeholder]:empty:before:text-muted-foreground",
812
+ className
813
+ ),
814
+ style: { minHeight },
815
+ onBlur,
816
+ onInput,
817
+ onKeyDown,
818
+ onPaste,
819
+ ...props
820
+ }
821
+ );
822
+ }
823
+ );
824
+ RichEditorContent.displayName = "RichEditorContent";
825
+ function renderToolbarItem(item) {
826
+ switch (item.kind) {
827
+ case "divider":
828
+ return /* @__PURE__ */ jsx(RichEditorDivider, {}, item.key);
829
+ case "iconButton":
830
+ return /* @__PURE__ */ jsx(
831
+ RichEditorIconButton,
832
+ {
833
+ icon: item.icon,
834
+ ariaLabel: item.ariaLabel,
835
+ tooltipLabel: item.tooltipLabel,
836
+ active: item.active,
837
+ command: item.command,
838
+ commandArg: item.commandArg,
839
+ onActivate: item.onActivate
840
+ },
841
+ item.key
842
+ );
843
+ case "textDropdown":
844
+ return /* @__PURE__ */ jsx(
845
+ RichEditorTextDropdown,
846
+ {
847
+ ariaLabel: item.ariaLabel,
848
+ label: item.label,
849
+ menu: item.menu
850
+ },
851
+ item.key
852
+ );
853
+ case "dropdownButton":
854
+ return /* @__PURE__ */ jsx(
855
+ RichEditorDropdownButton,
856
+ {
857
+ icon: item.icon,
858
+ ariaLabel: item.ariaLabel,
859
+ menu: item.menu,
860
+ showChevron: item.showChevron
861
+ },
862
+ item.key
863
+ );
864
+ case "colorButton":
865
+ return /* @__PURE__ */ jsx(
866
+ RichEditorColorButton,
867
+ {
868
+ ariaLabel: item.ariaLabel,
869
+ colorLabels: item.colorLabels
870
+ },
871
+ item.key
872
+ );
873
+ }
874
+ }
875
+ function trimTrailingDividers(items) {
876
+ let end = items.length;
877
+ while (end > 0 && items[end - 1].kind === "divider") end -= 1;
878
+ return items.slice(0, end);
879
+ }
880
+ function trimLeadingDividers(items) {
881
+ let start = 0;
882
+ while (start < items.length && items[start].kind === "divider") start += 1;
883
+ return items.slice(start);
884
+ }
885
+ function usePresetToolbarItems({
886
+ variant,
887
+ labels,
888
+ onComment,
889
+ onMention,
890
+ onFullscreen,
891
+ onImageInsert,
892
+ exec,
893
+ selection,
894
+ requestUrlPrompt
895
+ }) {
896
+ const onLink = React.useCallback(() => {
897
+ requestUrlPrompt({
898
+ kind: "link",
899
+ title: labels.linkUrlPrompt,
900
+ placeholder: "https://example.com",
901
+ onConfirm: (url) => exec("createLink", url)
902
+ });
903
+ }, [requestUrlPrompt, exec, labels.linkUrlPrompt]);
904
+ const onImage = React.useCallback(() => {
905
+ if (onImageInsert) {
906
+ onImageInsert();
907
+ return;
908
+ }
909
+ requestUrlPrompt({
910
+ kind: "image",
911
+ title: labels.imageUrlPrompt,
912
+ placeholder: "https://example.com/image.png",
913
+ onConfirm: (url) => exec("insertImage", url)
914
+ });
915
+ }, [requestUrlPrompt, exec, labels.imageUrlPrompt, onImageInsert]);
916
+ const onInsertTable = React.useCallback(() => {
917
+ const html = '<table style="border-collapse:collapse;width:100%"><tbody><tr><td style="border:1px solid #ebebeb;padding:8px">&nbsp;</td><td style="border:1px solid #ebebeb;padding:8px">&nbsp;</td><td style="border:1px solid #ebebeb;padding:8px">&nbsp;</td></tr><tr><td style="border:1px solid #ebebeb;padding:8px">&nbsp;</td><td style="border:1px solid #ebebeb;padding:8px">&nbsp;</td><td style="border:1px solid #ebebeb;padding:8px">&nbsp;</td></tr><tr><td style="border:1px solid #ebebeb;padding:8px">&nbsp;</td><td style="border:1px solid #ebebeb;padding:8px">&nbsp;</td><td style="border:1px solid #ebebeb;padding:8px">&nbsp;</td></tr></tbody></table><p>&nbsp;</p>';
918
+ exec("insertHTML", html);
919
+ }, [exec]);
920
+ const onInsertChecklist = React.useCallback(() => {
921
+ const html = '<ul data-task-list="true" style="list-style:none;padding-left:0"><li><label><input type="checkbox" /> </label></li></ul>';
922
+ exec("insertHTML", html);
923
+ }, [exec]);
924
+ const onInsertInlineCode = React.useCallback(() => {
925
+ const selectionText = typeof window !== "undefined" ? window.getSelection()?.toString() ?? "" : "";
926
+ if (!selectionText) {
927
+ exec("insertHTML", "<code>&nbsp;</code>");
928
+ return;
929
+ }
930
+ exec("insertHTML", `<code>${selectionText}</code>`);
931
+ }, [exec]);
932
+ const showHeading = variant === "full" || variant === "standard";
933
+ const showFontSize = variant === "full";
934
+ const showColor = variant === "full";
935
+ const showStrike = variant === "full";
936
+ const showHr = variant === "full";
937
+ const showQuoteAndCode = variant === "full";
938
+ const showImageTable = variant === "full";
939
+ const showChecklist = variant === "full";
940
+ const showOrdered = variant === "full" || variant === "standard" || variant === "basic";
941
+ const showAlign = variant === "full";
942
+ const showComment = variant === "full";
943
+ const commentHandler = onComment ?? (() => exec("insertText", "[comment: ]"));
944
+ const showMention = variant === "full";
945
+ const mentionHandler = onMention ?? (() => exec("insertText", "@"));
946
+ const showLink = variant === "full" || variant === "standard" || variant === "basic";
947
+ const showLists = variant !== "minimal";
948
+ const showHelp = variant === "full";
949
+ const showFullscreen = variant === "full" && onFullscreen !== void 0;
950
+ const keyboardShortcutsHelp = /* @__PURE__ */ jsxs("div", { className: "flex w-64 flex-col gap-1 p-1 text-sm", role: "menu", children: [
951
+ /* @__PURE__ */ jsx("div", { className: "px-2 py-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: "Keyboard" }),
952
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between rounded px-2 py-1 hover:bg-muted", children: [
953
+ /* @__PURE__ */ jsx("span", { children: labels.bold }),
954
+ /* @__PURE__ */ jsx("kbd", { className: "rounded border bg-card px-1.5 py-0.5 text-xs", children: "\u2318 B" })
955
+ ] }),
956
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between rounded px-2 py-1 hover:bg-muted", children: [
957
+ /* @__PURE__ */ jsx("span", { children: labels.italic }),
958
+ /* @__PURE__ */ jsx("kbd", { className: "rounded border bg-card px-1.5 py-0.5 text-xs", children: "\u2318 I" })
959
+ ] }),
960
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between rounded px-2 py-1 hover:bg-muted", children: [
961
+ /* @__PURE__ */ jsx("span", { children: labels.underline }),
962
+ /* @__PURE__ */ jsx("kbd", { className: "rounded border bg-card px-1.5 py-0.5 text-xs", children: "\u2318 U" })
963
+ ] })
964
+ ] });
965
+ const headingMenu = /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", role: "menu", children: [
966
+ /* @__PURE__ */ jsx(
967
+ "button",
968
+ {
969
+ type: "button",
970
+ role: "menuitem",
971
+ className: "cursor-pointer rounded px-2 py-1 text-left text-sm hover:bg-muted",
972
+ onMouseDown: (e) => e.preventDefault(),
973
+ onClick: () => exec("formatBlock", "<p>"),
974
+ children: labels.paragraph
975
+ }
976
+ ),
977
+ /* @__PURE__ */ jsx(
978
+ "button",
979
+ {
980
+ type: "button",
981
+ role: "menuitem",
982
+ className: "cursor-pointer rounded px-2 py-1 text-left text-lg font-semibold hover:bg-muted",
983
+ onMouseDown: (e) => e.preventDefault(),
984
+ onClick: () => exec("formatBlock", "<h1>"),
985
+ children: labels.heading1
986
+ }
987
+ ),
988
+ /* @__PURE__ */ jsx(
989
+ "button",
990
+ {
991
+ type: "button",
992
+ role: "menuitem",
993
+ className: "cursor-pointer rounded px-2 py-1 text-left text-base font-semibold hover:bg-muted",
994
+ onMouseDown: (e) => e.preventDefault(),
995
+ onClick: () => exec("formatBlock", "<h2>"),
996
+ children: labels.heading2
997
+ }
998
+ ),
999
+ /* @__PURE__ */ jsx(
1000
+ "button",
1001
+ {
1002
+ type: "button",
1003
+ role: "menuitem",
1004
+ className: "cursor-pointer rounded px-2 py-1 text-left text-sm font-semibold hover:bg-muted",
1005
+ onMouseDown: (e) => e.preventDefault(),
1006
+ onClick: () => exec("formatBlock", "<h3>"),
1007
+ children: labels.heading3
1008
+ }
1009
+ )
1010
+ ] });
1011
+ const headingLabel = (() => {
1012
+ switch (selection.heading) {
1013
+ case "h1":
1014
+ return labels.heading1;
1015
+ case "h2":
1016
+ return labels.heading2;
1017
+ case "h3":
1018
+ return labels.heading3;
1019
+ default:
1020
+ return labels.heading;
1021
+ }
1022
+ })();
1023
+ const fontSizeMenu = /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-0.5", role: "menu", children: FONT_SIZE_OPTIONS.map((size) => /* @__PURE__ */ jsx(
1024
+ "button",
1025
+ {
1026
+ type: "button",
1027
+ role: "menuitem",
1028
+ className: "cursor-pointer rounded px-2 py-1 text-left text-sm hover:bg-muted",
1029
+ onMouseDown: (e) => e.preventDefault(),
1030
+ onClick: () => {
1031
+ exec("removeFormat");
1032
+ const px = parseInt(size, 10);
1033
+ if (px > 0) {
1034
+ exec("insertHTML", `<span style="font-size:${px}px">${window.getSelection()?.toString() ?? ""}</span>`);
1035
+ }
1036
+ },
1037
+ children: size
1038
+ },
1039
+ size
1040
+ )) });
1041
+ const alignMenu = /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-0.5", role: "menu", children: ["left", "center", "right", "justify"].map((option) => {
1042
+ const Icon = ALIGN_ICONS[option];
1043
+ const command = option === "justify" ? "justifyFull" : `justify${option[0].toUpperCase()}${option.slice(1)}`;
1044
+ const label = option === "left" ? labels.alignLeft : option === "center" ? labels.alignCenter : option === "right" ? labels.alignRight : labels.alignJustify;
1045
+ return /* @__PURE__ */ jsxs(
1046
+ "button",
1047
+ {
1048
+ type: "button",
1049
+ role: "menuitem",
1050
+ className: "flex cursor-pointer items-center gap-2 rounded px-2 py-1 text-left text-sm hover:bg-muted",
1051
+ onMouseDown: (e) => e.preventDefault(),
1052
+ onClick: () => exec(command),
1053
+ children: [
1054
+ /* @__PURE__ */ jsx(Icon, { className: "size-4", "aria-hidden": "true" }),
1055
+ /* @__PURE__ */ jsx("span", { children: label })
1056
+ ]
1057
+ },
1058
+ option
1059
+ );
1060
+ }) });
1061
+ const AlignActiveIcon = ALIGN_ICONS[selection.align];
1062
+ if (variant === "custom") return [];
1063
+ const items = [];
1064
+ if (showHeading) {
1065
+ items.push({ kind: "textDropdown", key: "heading", ariaLabel: labels.heading, label: headingLabel, menu: headingMenu });
1066
+ items.push({ kind: "divider", key: "d-heading" });
1067
+ }
1068
+ if (showFontSize) {
1069
+ items.push({ kind: "textDropdown", key: "fontSize", ariaLabel: labels.fontSize, label: "14px", menu: fontSizeMenu });
1070
+ items.push({ kind: "divider", key: "d-fontSize" });
1071
+ }
1072
+ if (showColor) {
1073
+ items.push({ kind: "colorButton", key: "color", ariaLabel: labels.color, colorLabels: labels.colors });
1074
+ items.push({ kind: "divider", key: "d-color" });
1075
+ }
1076
+ items.push({ kind: "iconButton", key: "bold", icon: /* @__PURE__ */ jsx(Bold, {}), command: "bold", ariaLabel: labels.bold, tooltipLabel: labels.bold, active: selection.bold });
1077
+ items.push({ kind: "iconButton", key: "italic", icon: /* @__PURE__ */ jsx(Italic, {}), command: "italic", ariaLabel: labels.italic, tooltipLabel: labels.italic, active: selection.italic });
1078
+ items.push({ kind: "iconButton", key: "underline", icon: /* @__PURE__ */ jsx(Underline, {}), command: "underline", ariaLabel: labels.underline, tooltipLabel: labels.underline, active: selection.underline });
1079
+ if (showStrike) {
1080
+ items.push({ kind: "iconButton", key: "strike", icon: /* @__PURE__ */ jsx(Strikethrough, {}), command: "strikeThrough", ariaLabel: labels.strikethrough, tooltipLabel: labels.strikethrough, active: selection.strikethrough });
1081
+ }
1082
+ if (showLists) {
1083
+ items.push({ kind: "divider", key: "d-lists" });
1084
+ items.push({ kind: "iconButton", key: "ul", icon: /* @__PURE__ */ jsx(List, {}), command: "insertUnorderedList", ariaLabel: labels.unorderedList, tooltipLabel: labels.unorderedList, active: selection.unorderedList });
1085
+ if (showOrdered) {
1086
+ items.push({ kind: "iconButton", key: "ol", icon: /* @__PURE__ */ jsx(ListOrdered, {}), command: "insertOrderedList", ariaLabel: labels.orderedList, tooltipLabel: labels.orderedList, active: selection.orderedList });
1087
+ }
1088
+ }
1089
+ if (showHr || showQuoteAndCode) {
1090
+ items.push({ kind: "divider", key: "d-structure" });
1091
+ if (showHr) {
1092
+ items.push({ kind: "iconButton", key: "hr", icon: /* @__PURE__ */ jsx(Minus, {}), ariaLabel: labels.horizontalRule, tooltipLabel: labels.horizontalRule, onActivate: () => exec("insertHorizontalRule") });
1093
+ }
1094
+ if (showQuoteAndCode) {
1095
+ items.push({ kind: "iconButton", key: "quote", icon: /* @__PURE__ */ jsx(Quote, {}), ariaLabel: labels.blockquote, tooltipLabel: labels.blockquote, active: selection.blockquote, onActivate: () => exec("formatBlock", selection.blockquote ? "<p>" : "<blockquote>") });
1096
+ items.push({ kind: "iconButton", key: "inlineCode", icon: /* @__PURE__ */ jsx(Code, {}), ariaLabel: labels.inlineCode, tooltipLabel: labels.inlineCode, onActivate: onInsertInlineCode });
1097
+ items.push({ kind: "iconButton", key: "codeBlock", icon: /* @__PURE__ */ jsx(FileCode, {}), ariaLabel: labels.codeBlock, tooltipLabel: labels.codeBlock, active: selection.code, onActivate: () => exec("formatBlock", selection.code ? "<p>" : "<pre>") });
1098
+ }
1099
+ }
1100
+ if (showImageTable) {
1101
+ items.push({ kind: "divider", key: "d-media" });
1102
+ items.push({ kind: "iconButton", key: "image", icon: /* @__PURE__ */ jsx(ImageIcon, {}), ariaLabel: labels.image, tooltipLabel: labels.image, onActivate: onImage });
1103
+ items.push({ kind: "iconButton", key: "table", icon: /* @__PURE__ */ jsx(TableIcon, {}), ariaLabel: labels.table, tooltipLabel: labels.table, onActivate: onInsertTable });
1104
+ }
1105
+ if (showChecklist) {
1106
+ items.push({ kind: "iconButton", key: "checklist", icon: /* @__PURE__ */ jsx(ListChecks, {}), ariaLabel: labels.checklist, tooltipLabel: labels.checklist, onActivate: onInsertChecklist });
1107
+ }
1108
+ if (showAlign) {
1109
+ items.push({ kind: "divider", key: "d-align" });
1110
+ items.push({ kind: "dropdownButton", key: "align", icon: /* @__PURE__ */ jsx(AlignActiveIcon, {}), ariaLabel: labels.align, menu: alignMenu });
1111
+ }
1112
+ if (showLink || showComment || showMention) {
1113
+ items.push({ kind: "divider", key: "d-anchors" });
1114
+ }
1115
+ if (showComment) {
1116
+ items.push({ kind: "iconButton", key: "comment", icon: /* @__PURE__ */ jsx(MessageCircle, {}), ariaLabel: labels.comment, tooltipLabel: labels.comment, onActivate: commentHandler });
1117
+ }
1118
+ if (showLink) {
1119
+ items.push({ kind: "iconButton", key: "link", icon: /* @__PURE__ */ jsx(Link, {}), ariaLabel: labels.link, tooltipLabel: labels.link, onActivate: onLink });
1120
+ }
1121
+ if (showMention) {
1122
+ items.push({ kind: "iconButton", key: "mention", icon: /* @__PURE__ */ jsx(AtSign, {}), ariaLabel: labels.mention, tooltipLabel: labels.mention, onActivate: mentionHandler });
1123
+ }
1124
+ if (showHelp || showFullscreen) {
1125
+ items.push({ kind: "divider", key: "d-trailing" });
1126
+ if (showHelp) {
1127
+ items.push({ kind: "dropdownButton", key: "help", icon: /* @__PURE__ */ jsx(HelpCircle, {}), ariaLabel: labels.help, menu: keyboardShortcutsHelp, showChevron: false });
1128
+ }
1129
+ if (showFullscreen && onFullscreen) {
1130
+ items.push({ kind: "iconButton", key: "fullscreen", icon: /* @__PURE__ */ jsx(Maximize2, {}), ariaLabel: labels.fullscreen, tooltipLabel: labels.fullscreen, onActivate: onFullscreen });
1131
+ }
1132
+ }
1133
+ return trimTrailingDividers(items);
1134
+ }
1135
+ function RichEditorAutoToolbar({
1136
+ items,
1137
+ labels,
1138
+ moreMenu
1139
+ }) {
1140
+ const measureRef = React.useRef(null);
1141
+ const containerRef = React.useRef(null);
1142
+ const [firstHidden, setFirstHidden] = React.useState(null);
1143
+ React.useLayoutEffect(() => {
1144
+ if (typeof window === "undefined") return;
1145
+ if (typeof ResizeObserver === "undefined") return;
1146
+ const container = containerRef.current;
1147
+ const measureEl = measureRef.current;
1148
+ if (!container || !measureEl) return;
1149
+ const measure = () => {
1150
+ const containerWidth = container.clientWidth;
1151
+ if (containerWidth === 0) return;
1152
+ const allEls = Array.from(measureEl.children);
1153
+ if (allEls.length === 0) return;
1154
+ const moreEl = allEls[allEls.length - 1];
1155
+ const itemEls = allEls.slice(0, -1);
1156
+ const moreReserved = moreEl.offsetWidth + 4;
1157
+ const containerPadding = 4;
1158
+ let totalWidth = 0;
1159
+ for (const el of itemEls) totalWidth += el.offsetWidth + 2;
1160
+ if (totalWidth + containerPadding <= containerWidth + 2) {
1161
+ setFirstHidden(null);
1162
+ return;
1163
+ }
1164
+ let accumulated = 0;
1165
+ for (let i = 0; i < itemEls.length; i++) {
1166
+ accumulated += itemEls[i].offsetWidth + 2;
1167
+ if (accumulated > containerWidth - moreReserved - containerPadding) {
1168
+ setFirstHidden(i);
1169
+ return;
1170
+ }
1171
+ }
1172
+ setFirstHidden(null);
1173
+ };
1174
+ measure();
1175
+ const ro = new ResizeObserver(measure);
1176
+ ro.observe(container);
1177
+ return () => ro.disconnect();
1178
+ }, [items]);
1179
+ const visibleItems = firstHidden === null ? items : trimTrailingDividers(items.slice(0, firstHidden));
1180
+ const hiddenItems = firstHidden === null ? [] : trimLeadingDividers(items.slice(firstHidden));
1181
+ const hasOverflow = hiddenItems.length > 0;
1182
+ const showMoreButton = hasOverflow || !!moreMenu;
1183
+ return /* @__PURE__ */ jsxs("div", { className: "relative w-full", children: [
1184
+ /* @__PURE__ */ jsxs(
1185
+ "div",
1186
+ {
1187
+ ref: measureRef,
1188
+ "aria-hidden": "true",
1189
+ className: "pointer-events-none invisible absolute left-0 top-0 flex flex-row flex-nowrap items-center gap-0.5",
1190
+ children: [
1191
+ items.map(renderToolbarItem),
1192
+ /* @__PURE__ */ jsx(RichEditorDropdownButton, { icon: /* @__PURE__ */ jsx(MoreVertical, {}), ariaLabel: labels.more, showChevron: false, menu: /* @__PURE__ */ jsx("div", {}) })
1193
+ ]
1194
+ }
1195
+ ),
1196
+ /* @__PURE__ */ jsxs(RichEditorToolbar, { ref: containerRef, className: "w-full flex-nowrap overflow-hidden", children: [
1197
+ visibleItems.map(renderToolbarItem),
1198
+ showMoreButton ? /* @__PURE__ */ jsx(
1199
+ RichEditorDropdownButton,
1200
+ {
1201
+ icon: /* @__PURE__ */ jsx(MoreVertical, {}),
1202
+ ariaLabel: labels.more,
1203
+ showChevron: false,
1204
+ menu: /* @__PURE__ */ jsxs("div", { className: "flex max-w-[320px] flex-row flex-wrap items-center gap-0.5 p-1", children: [
1205
+ moreMenu ? /* @__PURE__ */ jsxs(Fragment, { children: [
1206
+ /* @__PURE__ */ jsx("div", { className: "w-full", children: moreMenu }),
1207
+ hiddenItems.length > 0 ? /* @__PURE__ */ jsx("div", { className: "my-1 h-px w-full bg-border" }) : null
1208
+ ] }) : null,
1209
+ hiddenItems.map(renderToolbarItem)
1210
+ ] })
1211
+ }
1212
+ ) : null
1213
+ ] })
1214
+ ] });
1215
+ }
1216
+ export {
1217
+ RICH_EDITOR_COLOR_PALETTE,
1218
+ RichEditor,
1219
+ RichEditorColorButton,
1220
+ RichEditorColorPalette,
1221
+ RichEditorContent,
1222
+ RichEditorDivider,
1223
+ RichEditorDropdownButton,
1224
+ RichEditorIconButton,
1225
+ RichEditorMenuItem,
1226
+ RichEditorTextDropdown,
1227
+ RichEditorToolbar,
1228
+ richEditorItemVariants
1229
+ };
1230
+ //# sourceMappingURL=rich-editor.js.map