@longd/layout-ui 0.1.0 → 0.1.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 (37) hide show
  1. package/README.md +257 -1
  2. package/dist/CATEditor-C-b6vybW.d.cts +381 -0
  3. package/dist/CATEditor-CLp6jZAf.d.ts +381 -0
  4. package/dist/chunk-BLJWR4ZV.js +11 -0
  5. package/dist/chunk-BLJWR4ZV.js.map +1 -0
  6. package/dist/{chunk-CZ3IMHZ6.js → chunk-H7SY4VJU.js} +7 -11
  7. package/dist/chunk-H7SY4VJU.js.map +1 -0
  8. package/dist/chunk-YXQGAND3.js +137 -0
  9. package/dist/chunk-YXQGAND3.js.map +1 -0
  10. package/dist/chunk-ZME2TTK5.js +2527 -0
  11. package/dist/chunk-ZME2TTK5.js.map +1 -0
  12. package/dist/index.cjs +2612 -3
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.css +504 -0
  15. package/dist/index.css.map +1 -0
  16. package/dist/index.d.cts +3 -0
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.js +13 -2
  19. package/dist/layout/cat-editor.cjs +2669 -0
  20. package/dist/layout/cat-editor.cjs.map +1 -0
  21. package/dist/layout/cat-editor.css +504 -0
  22. package/dist/layout/cat-editor.css.map +1 -0
  23. package/dist/layout/cat-editor.d.cts +28 -0
  24. package/dist/layout/cat-editor.d.ts +28 -0
  25. package/dist/layout/cat-editor.js +29 -0
  26. package/dist/layout/cat-editor.js.map +1 -0
  27. package/dist/layout/select.cjs +2 -1
  28. package/dist/layout/select.cjs.map +1 -1
  29. package/dist/layout/select.js +2 -1
  30. package/dist/utils/detect-quotes.cjs +162 -0
  31. package/dist/utils/detect-quotes.cjs.map +1 -0
  32. package/dist/utils/detect-quotes.d.cts +88 -0
  33. package/dist/utils/detect-quotes.d.ts +88 -0
  34. package/dist/utils/detect-quotes.js +9 -0
  35. package/dist/utils/detect-quotes.js.map +1 -0
  36. package/package.json +39 -3
  37. package/dist/chunk-CZ3IMHZ6.js.map +0 -1
@@ -0,0 +1,2527 @@
1
+ import {
2
+ cn
3
+ } from "./chunk-BLJWR4ZV.js";
4
+ import {
5
+ detectQuotes
6
+ } from "./chunk-YXQGAND3.js";
7
+
8
+ // src/layout/cat-editor/CATEditor.tsx
9
+ import {
10
+ forwardRef,
11
+ useCallback as useCallback3,
12
+ useEffect as useEffect3,
13
+ useImperativeHandle,
14
+ useMemo as useMemo2,
15
+ useRef as useRef4,
16
+ useState as useState2
17
+ } from "react";
18
+ import {
19
+ $createParagraphNode as $createParagraphNode2,
20
+ $createRangeSelection as $createRangeSelection2,
21
+ $createTextNode as $createTextNode3,
22
+ $getNodeByKey,
23
+ $getRoot as $getRoot3,
24
+ $setSelection as $setSelection2,
25
+ COMMAND_PRIORITY_CRITICAL,
26
+ KEY_DOWN_COMMAND
27
+ } from "lexical";
28
+ import { LexicalComposer } from "@lexical/react/LexicalComposer";
29
+ import { useLexicalComposerContext as useLexicalComposerContext3 } from "@lexical/react/LexicalComposerContext";
30
+ import { ContentEditable } from "@lexical/react/LexicalContentEditable";
31
+ import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
32
+ import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
33
+ import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
34
+ import { PlainTextPlugin } from "@lexical/react/LexicalPlainTextPlugin";
35
+
36
+ // src/layout/cat-editor/constants.ts
37
+ var CODEPOINT_DISPLAY_MAP = {
38
+ 0: "\u2400",
39
+ 9: "\u21E5",
40
+ 10: "\u21A9",
41
+ 12: "\u240C",
42
+ 13: "\u21B5",
43
+ 160: "\u237D",
44
+ 8194: "\u2423",
45
+ 8195: "\u2423",
46
+ 8201: "\xB7",
47
+ 8202: "\xB7",
48
+ 8203: "\u2205",
49
+ 8204: "\u2298",
50
+ 8205: "\u2295",
51
+ 8288: "\u2040",
52
+ 12288: "\u25A1",
53
+ 65279: "\u25CA"
54
+ };
55
+ var _codepointOverrides;
56
+ function setCodepointOverrides(overrides) {
57
+ _codepointOverrides = overrides;
58
+ }
59
+ function getEffectiveCodepointMap() {
60
+ return _codepointOverrides ? { ...CODEPOINT_DISPLAY_MAP, ..._codepointOverrides } : CODEPOINT_DISPLAY_MAP;
61
+ }
62
+ var NL_MARKER_PREFIX = "__nl-";
63
+ function replaceInvisibleChars(text, overrides) {
64
+ const map = overrides ? { ...CODEPOINT_DISPLAY_MAP, ...overrides } : getEffectiveCodepointMap();
65
+ let result = "";
66
+ for (const ch of text) {
67
+ const cp = ch.codePointAt(0) ?? 0;
68
+ result += map[cp] ?? ch;
69
+ }
70
+ return result;
71
+ }
72
+
73
+ // src/layout/cat-editor/highlight-node.ts
74
+ import { TextNode } from "lexical";
75
+ var HighlightNode = class _HighlightNode extends TextNode {
76
+ __highlightTypes;
77
+ __ruleIds;
78
+ __displayText;
79
+ static getType() {
80
+ return "highlight";
81
+ }
82
+ static clone(node) {
83
+ return new _HighlightNode(
84
+ node.__text,
85
+ node.__highlightTypes,
86
+ node.__ruleIds,
87
+ node.__displayText,
88
+ node.__key
89
+ );
90
+ }
91
+ constructor(text, highlightTypes, ruleIds, displayText, key) {
92
+ super(text, key);
93
+ this.__highlightTypes = highlightTypes;
94
+ this.__ruleIds = ruleIds;
95
+ this.__displayText = displayText ?? "";
96
+ }
97
+ createDOM(config) {
98
+ const dom = super.createDOM(config);
99
+ dom.classList.add("cat-highlight");
100
+ for (const t of this.__highlightTypes.split(",")) {
101
+ dom.classList.add(`cat-highlight-${t}`);
102
+ if (t.startsWith("glossary-")) {
103
+ dom.classList.add("cat-highlight-glossary");
104
+ }
105
+ if (t.startsWith("spellcheck-")) {
106
+ dom.classList.add("cat-highlight-spellcheck");
107
+ }
108
+ }
109
+ if (this.__highlightTypes.includes(",")) {
110
+ dom.classList.add("cat-highlight-nested");
111
+ }
112
+ dom.dataset.highlightTypes = this.__highlightTypes;
113
+ dom.dataset.ruleIds = this.__ruleIds;
114
+ if (this.__ruleIds.startsWith(NL_MARKER_PREFIX)) {
115
+ dom.style.userSelect = "none";
116
+ dom.classList.add("cat-highlight-nl-marker");
117
+ }
118
+ if (this.__displayText) {
119
+ dom.dataset.display = this.__displayText;
120
+ }
121
+ if (this.__highlightTypes.split(",").includes("tag-collapsed")) {
122
+ dom.textContent = "\u200B";
123
+ dom.contentEditable = "false";
124
+ }
125
+ if (this.__highlightTypes.split(",").includes("special-char")) {
126
+ if (this.__text === " ") {
127
+ dom.classList.add("cat-highlight-space-char");
128
+ dom.style.position = "relative";
129
+ } else {
130
+ const replaced = replaceInvisibleChars(this.__text);
131
+ if (replaced !== this.__text) {
132
+ dom.textContent = replaced;
133
+ }
134
+ }
135
+ }
136
+ if (this.__highlightTypes.split(",").includes("quote") && this.__displayText) {
137
+ dom.classList.add("cat-highlight-quote-char");
138
+ dom.textContent = "\u200B";
139
+ dom.contentEditable = "false";
140
+ }
141
+ return dom;
142
+ }
143
+ updateDOM(prevNode, dom, config) {
144
+ const updated = super.updateDOM(prevNode, dom, config);
145
+ if (prevNode.__highlightTypes !== this.__highlightTypes) {
146
+ for (const t of prevNode.__highlightTypes.split(",")) {
147
+ dom.classList.remove(`cat-highlight-${t}`);
148
+ if (t.startsWith("glossary-")) {
149
+ dom.classList.remove("cat-highlight-glossary");
150
+ }
151
+ if (t.startsWith("spellcheck-")) {
152
+ dom.classList.remove("cat-highlight-spellcheck");
153
+ }
154
+ }
155
+ dom.classList.remove("cat-highlight-nested");
156
+ for (const t of this.__highlightTypes.split(",")) {
157
+ dom.classList.add(`cat-highlight-${t}`);
158
+ if (t.startsWith("glossary-")) {
159
+ dom.classList.add("cat-highlight-glossary");
160
+ }
161
+ if (t.startsWith("spellcheck-")) {
162
+ dom.classList.add("cat-highlight-spellcheck");
163
+ }
164
+ }
165
+ if (this.__highlightTypes.includes(",")) {
166
+ dom.classList.add("cat-highlight-nested");
167
+ }
168
+ dom.dataset.highlightTypes = this.__highlightTypes;
169
+ }
170
+ if (prevNode.__ruleIds !== this.__ruleIds) {
171
+ dom.dataset.ruleIds = this.__ruleIds;
172
+ }
173
+ if (prevNode.__displayText !== this.__displayText) {
174
+ if (this.__displayText) {
175
+ dom.dataset.display = this.__displayText;
176
+ } else {
177
+ delete dom.dataset.display;
178
+ }
179
+ }
180
+ if (this.__highlightTypes.split(",").includes("tag-collapsed")) {
181
+ dom.textContent = "\u200B";
182
+ dom.contentEditable = "false";
183
+ } else if (dom.contentEditable === "false") {
184
+ dom.removeAttribute("contenteditable");
185
+ }
186
+ if (this.__highlightTypes.split(",").includes("special-char")) {
187
+ if (this.__text === " ") {
188
+ dom.classList.add("cat-highlight-space-char");
189
+ dom.style.position = "relative";
190
+ } else {
191
+ const replaced = replaceInvisibleChars(this.__text);
192
+ if (replaced !== this.__text) {
193
+ dom.textContent = replaced;
194
+ }
195
+ }
196
+ }
197
+ if (this.__highlightTypes.split(",").includes("quote") && this.__displayText) {
198
+ dom.classList.add("cat-highlight-quote-char");
199
+ dom.textContent = "\u200B";
200
+ dom.contentEditable = "false";
201
+ } else if (prevNode.__highlightTypes.split(",").includes("quote")) {
202
+ dom.classList.remove("cat-highlight-quote-char");
203
+ if (dom.contentEditable === "false") {
204
+ dom.removeAttribute("contenteditable");
205
+ }
206
+ }
207
+ return updated;
208
+ }
209
+ static importJSON(json) {
210
+ const node = new _HighlightNode(
211
+ json.text,
212
+ json.highlightTypes,
213
+ json.ruleIds,
214
+ json.displayText
215
+ );
216
+ node.setFormat(json.format);
217
+ node.setDetail(json.detail);
218
+ node.setMode(json.mode);
219
+ node.setStyle(json.style);
220
+ return node;
221
+ }
222
+ exportJSON() {
223
+ return {
224
+ ...super.exportJSON(),
225
+ type: "highlight",
226
+ highlightTypes: this.__highlightTypes,
227
+ ruleIds: this.__ruleIds,
228
+ displayText: this.__displayText
229
+ };
230
+ }
231
+ /** NL-marker nodes must not leak the display symbol ↩ into
232
+ * clipboard or getTextContent() calls — they are purely visual. */
233
+ getTextContent() {
234
+ if (this.__ruleIds.startsWith(NL_MARKER_PREFIX)) return "";
235
+ return super.getTextContent();
236
+ }
237
+ canInsertTextBefore() {
238
+ if (this.__ruleIds.startsWith(NL_MARKER_PREFIX)) return false;
239
+ if (this.getMode() === "token") return false;
240
+ return true;
241
+ }
242
+ canInsertTextAfter() {
243
+ if (this.__ruleIds.startsWith(NL_MARKER_PREFIX)) return false;
244
+ if (this.getMode() === "token") return false;
245
+ return true;
246
+ }
247
+ isTextEntity() {
248
+ return false;
249
+ }
250
+ };
251
+ function $createHighlightNode(text, highlightTypes, ruleIds, displayText, forceToken) {
252
+ const node = new HighlightNode(text, highlightTypes, ruleIds, displayText);
253
+ if (highlightTypes.split(",").includes("special-char") || ruleIds.startsWith(NL_MARKER_PREFIX) || forceToken) {
254
+ node.setMode("token");
255
+ }
256
+ return node;
257
+ }
258
+ function $isHighlightNode(node) {
259
+ return node instanceof HighlightNode;
260
+ }
261
+
262
+ // src/layout/cat-editor/mention-node.ts
263
+ import { $applyNodeReplacement, TextNode as TextNode2 } from "lexical";
264
+ var DEFAULT_MENTION_SERIALIZE = (id) => `@{${id}}`;
265
+ var DEFAULT_MENTION_PATTERN = /@\{([^}]+)\}/g;
266
+ var _mentionNodeConfig = {};
267
+ function setMentionNodeConfig(config) {
268
+ _mentionNodeConfig = config;
269
+ }
270
+ function getMentionModelText(id) {
271
+ return (_mentionNodeConfig.serialize ?? DEFAULT_MENTION_SERIALIZE)(id);
272
+ }
273
+ function getMentionPattern() {
274
+ const src = _mentionNodeConfig.pattern ?? DEFAULT_MENTION_PATTERN;
275
+ return new RegExp(src.source, src.flags);
276
+ }
277
+ function renderDefaultMentionDOM(element, _mentionId, mentionName) {
278
+ element.textContent = "";
279
+ const label = document.createElement("span");
280
+ label.className = "cat-mention-label";
281
+ label.textContent = `@${mentionName}`;
282
+ element.appendChild(label);
283
+ }
284
+ function $convertMentionElement(domNode) {
285
+ const mentionId = domNode.getAttribute("data-mention-id");
286
+ const mentionName = domNode.getAttribute("data-mention-name");
287
+ if (mentionId !== null) {
288
+ const node = $createMentionNode(mentionId, mentionName ?? mentionId);
289
+ return { node };
290
+ }
291
+ return null;
292
+ }
293
+ var MentionNode = class _MentionNode extends TextNode2 {
294
+ __mentionId;
295
+ __mentionName;
296
+ static getType() {
297
+ return "mention";
298
+ }
299
+ static clone(node) {
300
+ return new _MentionNode(
301
+ node.__mentionId,
302
+ node.__mentionName,
303
+ node.__text,
304
+ node.__key
305
+ );
306
+ }
307
+ static importJSON(serializedNode) {
308
+ return $createMentionNode(
309
+ serializedNode.mentionId,
310
+ serializedNode.mentionName
311
+ ).updateFromJSON(serializedNode);
312
+ }
313
+ constructor(mentionId, mentionName, text, key) {
314
+ super(text ?? getMentionModelText(mentionId), key);
315
+ this.__mentionId = mentionId;
316
+ this.__mentionName = mentionName;
317
+ }
318
+ exportJSON() {
319
+ return {
320
+ ...super.exportJSON(),
321
+ mentionId: this.__mentionId,
322
+ mentionName: this.__mentionName
323
+ };
324
+ }
325
+ createDOM(config) {
326
+ const dom = super.createDOM(config);
327
+ dom.className = "cat-mention-node";
328
+ dom.spellcheck = false;
329
+ dom.contentEditable = "false";
330
+ dom.setAttribute("data-mention-id", this.__mentionId);
331
+ dom.setAttribute("data-mention-name", this.__mentionName);
332
+ this._renderInnerDOM(dom);
333
+ return dom;
334
+ }
335
+ updateDOM(prevNode, dom, _config) {
336
+ if (prevNode.__mentionId !== this.__mentionId || prevNode.__mentionName !== this.__mentionName) {
337
+ dom.setAttribute("data-mention-id", this.__mentionId);
338
+ dom.setAttribute("data-mention-name", this.__mentionName);
339
+ this._renderInnerDOM(dom);
340
+ }
341
+ return false;
342
+ }
343
+ /** Fill the span with visible content (default: @label, or custom). */
344
+ _renderInnerDOM(element) {
345
+ const renderer = _mentionNodeConfig.renderDOM;
346
+ if (renderer) {
347
+ const handled = renderer(element, this.__mentionId, this.__mentionName);
348
+ if (handled) return;
349
+ }
350
+ renderDefaultMentionDOM(element, this.__mentionId, this.__mentionName);
351
+ }
352
+ exportDOM() {
353
+ const element = document.createElement("span");
354
+ element.setAttribute("data-mention", "true");
355
+ element.setAttribute("data-mention-id", this.__mentionId);
356
+ element.setAttribute("data-mention-name", this.__mentionName);
357
+ element.textContent = this.__text;
358
+ return { element };
359
+ }
360
+ static importDOM() {
361
+ return {
362
+ span: (domNode) => {
363
+ if (!domNode.hasAttribute("data-mention")) {
364
+ return null;
365
+ }
366
+ return {
367
+ conversion: $convertMentionElement,
368
+ priority: 1
369
+ };
370
+ }
371
+ };
372
+ }
373
+ isTextEntity() {
374
+ return true;
375
+ }
376
+ canInsertTextBefore() {
377
+ return false;
378
+ }
379
+ canInsertTextAfter() {
380
+ return false;
381
+ }
382
+ };
383
+ function $createMentionNode(mentionId, mentionName, textContent) {
384
+ const node = new MentionNode(
385
+ mentionId,
386
+ mentionName,
387
+ textContent ?? getMentionModelText(mentionId)
388
+ );
389
+ node.setMode("token").toggleDirectionless();
390
+ return $applyNodeReplacement(node);
391
+ }
392
+ function $isMentionNode(node) {
393
+ return node instanceof MentionNode;
394
+ }
395
+
396
+ // src/layout/cat-editor/mention-plugin.tsx
397
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
398
+ import * as ReactDOM from "react-dom";
399
+ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
400
+ import {
401
+ LexicalTypeaheadMenuPlugin,
402
+ MenuOption,
403
+ useBasicTypeaheadTriggerMatch
404
+ } from "@lexical/react/LexicalTypeaheadMenuPlugin";
405
+ import { useVirtualizer } from "@tanstack/react-virtual";
406
+ import { $createTextNode } from "lexical";
407
+ import { jsx, jsxs } from "react/jsx-runtime";
408
+ var SUGGESTION_LIST_LENGTH_LIMIT = 50;
409
+ var ITEM_HEIGHT = 36;
410
+ var MentionTypeaheadOption = class extends MenuOption {
411
+ user;
412
+ constructor(user) {
413
+ super(user.id);
414
+ this.user = user;
415
+ }
416
+ };
417
+ function MentionAvatar({
418
+ user,
419
+ size = 24
420
+ }) {
421
+ if (user.avatar) {
422
+ return /* @__PURE__ */ jsx(
423
+ "span",
424
+ {
425
+ className: "inline-flex items-center justify-center",
426
+ style: { width: size, height: size },
427
+ children: user.avatar()
428
+ }
429
+ );
430
+ }
431
+ const initials = user.name.split(/\s+/).map((w) => w[0]).slice(0, 2).join("").toUpperCase();
432
+ return /* @__PURE__ */ jsx(
433
+ "span",
434
+ {
435
+ className: "inline-flex items-center justify-center rounded-full bg-muted text-muted-foreground font-medium",
436
+ style: { width: size, height: size, fontSize: size * 0.4 },
437
+ children: initials
438
+ }
439
+ );
440
+ }
441
+ function MentionMenuItem({
442
+ option,
443
+ isSelected,
444
+ onClick,
445
+ onMouseEnter
446
+ }) {
447
+ return /* @__PURE__ */ jsxs(
448
+ "li",
449
+ {
450
+ tabIndex: -1,
451
+ className: `flex items-center gap-2 px-2 py-1.5 text-sm cursor-default select-none rounded-sm ${isSelected ? "bg-accent text-accent-foreground" : "text-popover-foreground"}`,
452
+ ref: option.setRefElement,
453
+ role: "option",
454
+ "aria-selected": isSelected,
455
+ onMouseEnter,
456
+ onClick,
457
+ children: [
458
+ /* @__PURE__ */ jsx(MentionAvatar, { user: option.user, size: 22 }),
459
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: option.user.name })
460
+ ]
461
+ },
462
+ option.key
463
+ );
464
+ }
465
+ function VirtualizedMentionMenu({
466
+ options,
467
+ selectedIndex,
468
+ selectOptionAndCleanUp,
469
+ setHighlightedIndex
470
+ }) {
471
+ const parentRef = useRef(null);
472
+ const virtualizer = useVirtualizer({
473
+ count: options.length,
474
+ getScrollElement: () => parentRef.current,
475
+ estimateSize: () => ITEM_HEIGHT,
476
+ overscan: 8
477
+ });
478
+ useEffect(() => {
479
+ if (selectedIndex !== null && selectedIndex >= 0) {
480
+ virtualizer.scrollToIndex(selectedIndex, { align: "auto" });
481
+ }
482
+ }, [selectedIndex, virtualizer]);
483
+ return /* @__PURE__ */ jsx(
484
+ "div",
485
+ {
486
+ ref: parentRef,
487
+ className: "max-h-[280px] overflow-y-auto overflow-x-hidden",
488
+ children: /* @__PURE__ */ jsx(
489
+ "ul",
490
+ {
491
+ role: "listbox",
492
+ "aria-label": "Mention suggestions",
493
+ style: {
494
+ height: `${virtualizer.getTotalSize()}px`,
495
+ position: "relative",
496
+ width: "100%"
497
+ },
498
+ children: virtualizer.getVirtualItems().map((vItem) => {
499
+ const option = options[vItem.index];
500
+ return /* @__PURE__ */ jsx(
501
+ "div",
502
+ {
503
+ style: {
504
+ position: "absolute",
505
+ top: 0,
506
+ left: 0,
507
+ width: "100%",
508
+ transform: `translateY(${vItem.start}px)`
509
+ },
510
+ ref: virtualizer.measureElement,
511
+ "data-index": vItem.index,
512
+ children: /* @__PURE__ */ jsx(
513
+ MentionMenuItem,
514
+ {
515
+ option,
516
+ isSelected: selectedIndex === vItem.index,
517
+ onClick: () => {
518
+ setHighlightedIndex(vItem.index);
519
+ selectOptionAndCleanUp(option);
520
+ },
521
+ onMouseEnter: () => {
522
+ setHighlightedIndex(vItem.index);
523
+ }
524
+ }
525
+ )
526
+ },
527
+ option.key
528
+ );
529
+ })
530
+ }
531
+ )
532
+ }
533
+ );
534
+ }
535
+ function checkForMentionMatch(text, trigger) {
536
+ const escaped = trigger.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
537
+ const regex = new RegExp(
538
+ `(^|\\s|\\()([${escaped}]((?:[^${escaped}\\s]){0,75}))$`
539
+ );
540
+ const match = regex.exec(text);
541
+ if (match !== null) {
542
+ const maybeLeadingWhitespace = match[1];
543
+ const matchingString = match[3];
544
+ return {
545
+ leadOffset: match.index + maybeLeadingWhitespace.length,
546
+ matchingString,
547
+ replaceableString: match[2]
548
+ };
549
+ }
550
+ return null;
551
+ }
552
+ function MentionPlugin({
553
+ users,
554
+ trigger = "@",
555
+ onMentionInsert
556
+ }) {
557
+ const [editor] = useLexicalComposerContext();
558
+ const [queryString, setQueryString] = useState(null);
559
+ const checkForSlashTriggerMatch = useBasicTypeaheadTriggerMatch("/", {
560
+ minLength: 0
561
+ });
562
+ const results = useMemo(() => {
563
+ if (queryString === null)
564
+ return users.slice(0, SUGGESTION_LIST_LENGTH_LIMIT);
565
+ const q = queryString.toLowerCase();
566
+ return users.filter((u) => u.name.toLowerCase().includes(q)).slice(0, SUGGESTION_LIST_LENGTH_LIMIT);
567
+ }, [users, queryString]);
568
+ const options = useMemo(
569
+ () => results.map((user) => new MentionTypeaheadOption(user)),
570
+ [results]
571
+ );
572
+ const onSelectOption = useCallback(
573
+ (selectedOption, nodeToReplace, closeMenu) => {
574
+ editor.update(() => {
575
+ const mentionNode = $createMentionNode(
576
+ selectedOption.user.id,
577
+ selectedOption.user.name
578
+ );
579
+ if (nodeToReplace) {
580
+ nodeToReplace.replace(mentionNode);
581
+ }
582
+ const spaceNode = $createTextNode(" ");
583
+ mentionNode.insertAfter(spaceNode);
584
+ spaceNode.selectEnd();
585
+ closeMenu();
586
+ });
587
+ onMentionInsert?.(selectedOption.user);
588
+ },
589
+ [editor, onMentionInsert]
590
+ );
591
+ const checkForMentionTrigger = useCallback(
592
+ (text) => {
593
+ const slashMatch = checkForSlashTriggerMatch(text, editor);
594
+ if (slashMatch !== null) {
595
+ return null;
596
+ }
597
+ return checkForMentionMatch(text, trigger);
598
+ },
599
+ [checkForSlashTriggerMatch, editor, trigger]
600
+ );
601
+ return /* @__PURE__ */ jsx(
602
+ LexicalTypeaheadMenuPlugin,
603
+ {
604
+ onQueryChange: setQueryString,
605
+ onSelectOption,
606
+ triggerFn: checkForMentionTrigger,
607
+ options,
608
+ menuRenderFn: (anchorElementRef, { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }) => anchorElementRef.current && options.length ? ReactDOM.createPortal(
609
+ /* @__PURE__ */ jsx("div", { className: "cat-mention-popover", children: /* @__PURE__ */ jsx(
610
+ VirtualizedMentionMenu,
611
+ {
612
+ options,
613
+ selectedIndex,
614
+ selectOptionAndCleanUp,
615
+ setHighlightedIndex
616
+ }
617
+ ) }),
618
+ anchorElementRef.current
619
+ ) : null
620
+ }
621
+ );
622
+ }
623
+
624
+ // src/layout/cat-editor/plugins.tsx
625
+ import { useLexicalComposerContext as useLexicalComposerContext2 } from "@lexical/react/LexicalComposerContext";
626
+ import { $isParentElementRTL } from "@lexical/selection";
627
+ import {
628
+ $addUpdateTag,
629
+ $createParagraphNode,
630
+ $createRangeSelection,
631
+ $createTextNode as $createTextNode2,
632
+ $getRoot as $getRoot2,
633
+ $getSelection,
634
+ $isRangeSelection,
635
+ $setSelection,
636
+ COMMAND_PRIORITY_HIGH,
637
+ KEY_ARROW_LEFT_COMMAND,
638
+ KEY_ARROW_RIGHT_COMMAND,
639
+ PASTE_COMMAND,
640
+ SELECTION_CHANGE_COMMAND
641
+ } from "lexical";
642
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2 } from "react";
643
+
644
+ // src/layout/cat-editor/compute-segments.ts
645
+ var TAG_RE = /<(\/?)([a-zA-Z][a-zA-Z0-9]*)\b[^>]*?(\/?)>/g;
646
+ function detectAndPairTags(text, _detectInner = true) {
647
+ const allTags = [];
648
+ TAG_RE.lastIndex = 0;
649
+ let m;
650
+ while ((m = TAG_RE.exec(text)) !== null) {
651
+ const isClosing = m[1] === "/";
652
+ const isSelfClosing = m[3] === "/" || !isClosing && m[0].endsWith("/>");
653
+ allTags.push({
654
+ start: m.index,
655
+ end: m.index + m[0].length,
656
+ tagName: m[2].toLowerCase(),
657
+ isClosing,
658
+ isSelfClosing,
659
+ originalText: m[0]
660
+ });
661
+ }
662
+ let nextNum = 1;
663
+ const stack = [];
664
+ const result = [];
665
+ for (let i = 0; i < allTags.length; i++) {
666
+ const tag = allTags[i];
667
+ if (tag.isSelfClosing) {
668
+ const num = nextNum++;
669
+ result.push({
670
+ ...tag,
671
+ tagNumber: num,
672
+ displayText: `<${num}/>`
673
+ });
674
+ } else if (!tag.isClosing) {
675
+ const num = nextNum++;
676
+ stack.push({ name: tag.tagName, num, idx: i });
677
+ } else {
678
+ let matchIdx = -1;
679
+ for (let j = stack.length - 1; j >= 0; j--) {
680
+ if (stack[j].name === tag.tagName) {
681
+ matchIdx = j;
682
+ break;
683
+ }
684
+ }
685
+ if (matchIdx >= 0) {
686
+ const openEntry = stack[matchIdx];
687
+ stack.splice(matchIdx, 1);
688
+ const openTag = allTags[openEntry.idx];
689
+ result.push({
690
+ ...openTag,
691
+ tagNumber: openEntry.num,
692
+ displayText: `<${openEntry.num}>`
693
+ });
694
+ result.push({
695
+ ...tag,
696
+ tagNumber: openEntry.num,
697
+ displayText: `</${openEntry.num}>`
698
+ });
699
+ }
700
+ }
701
+ }
702
+ result.sort((a, b) => a.start - b.start);
703
+ return result;
704
+ }
705
+ var HTML_TAG_CLASSIFY = /^<(\/?)([a-zA-Z][a-zA-Z0-9]*)\b[^>]*?(\/?)>$/;
706
+ function detectCustomTags(text, patternSource) {
707
+ let re;
708
+ try {
709
+ re = new RegExp(patternSource, "g");
710
+ } catch {
711
+ return [];
712
+ }
713
+ const matches = [];
714
+ let m;
715
+ while ((m = re.exec(text)) !== null) {
716
+ if (m[0].length === 0) {
717
+ re.lastIndex++;
718
+ continue;
719
+ }
720
+ const htmlMatch = HTML_TAG_CLASSIFY.exec(m[0]);
721
+ if (htmlMatch) {
722
+ matches.push({
723
+ start: m.index,
724
+ end: m.index + m[0].length,
725
+ text: m[0],
726
+ htmlName: htmlMatch[2].toLowerCase(),
727
+ isClosing: htmlMatch[1] === "/",
728
+ isSelfClosing: htmlMatch[3] === "/" || !htmlMatch[1] && m[0].endsWith("/>")
729
+ });
730
+ } else {
731
+ matches.push({
732
+ start: m.index,
733
+ end: m.index + m[0].length,
734
+ text: m[0],
735
+ htmlName: null,
736
+ isClosing: false,
737
+ isSelfClosing: false
738
+ });
739
+ }
740
+ }
741
+ let nextNum = 1;
742
+ const stack = [];
743
+ const result = [];
744
+ for (let i = 0; i < matches.length; i++) {
745
+ const raw = matches[i];
746
+ if (!raw.htmlName) {
747
+ const num = nextNum++;
748
+ result.push({
749
+ start: raw.start,
750
+ end: raw.end,
751
+ tagName: raw.text,
752
+ tagNumber: num,
753
+ isClosing: false,
754
+ isSelfClosing: false,
755
+ originalText: raw.text,
756
+ displayText: `<${num}>`
757
+ });
758
+ continue;
759
+ }
760
+ if (raw.isSelfClosing) {
761
+ const num = nextNum++;
762
+ result.push({
763
+ start: raw.start,
764
+ end: raw.end,
765
+ tagName: raw.htmlName,
766
+ tagNumber: num,
767
+ isClosing: false,
768
+ isSelfClosing: true,
769
+ originalText: raw.text,
770
+ displayText: `<${num}/>`
771
+ });
772
+ } else if (!raw.isClosing) {
773
+ const num = nextNum++;
774
+ stack.push({ name: raw.htmlName, num, idx: i });
775
+ } else {
776
+ let matchIdx = -1;
777
+ for (let j = stack.length - 1; j >= 0; j--) {
778
+ if (stack[j].name === raw.htmlName) {
779
+ matchIdx = j;
780
+ break;
781
+ }
782
+ }
783
+ if (matchIdx >= 0) {
784
+ const openEntry = stack[matchIdx];
785
+ stack.splice(matchIdx, 1);
786
+ const openRaw = matches[openEntry.idx];
787
+ result.push({
788
+ start: openRaw.start,
789
+ end: openRaw.end,
790
+ tagName: openRaw.htmlName,
791
+ tagNumber: openEntry.num,
792
+ isClosing: false,
793
+ isSelfClosing: false,
794
+ originalText: openRaw.text,
795
+ displayText: `<${openEntry.num}>`
796
+ });
797
+ result.push({
798
+ start: raw.start,
799
+ end: raw.end,
800
+ tagName: raw.htmlName,
801
+ tagNumber: openEntry.num,
802
+ isClosing: true,
803
+ isSelfClosing: false,
804
+ originalText: raw.text,
805
+ displayText: `</${openEntry.num}>`
806
+ });
807
+ }
808
+ }
809
+ }
810
+ result.sort((a, b) => a.start - b.start);
811
+ return result;
812
+ }
813
+ function computeHighlightSegments(text, rules) {
814
+ const rawRanges = [];
815
+ for (const rule of rules) {
816
+ if (rule.type === "spellcheck") {
817
+ for (const v of rule.validations) {
818
+ if (v.start < 0 || v.start >= v.end || !v.content) continue;
819
+ let matchStart = -1;
820
+ let matchEnd = -1;
821
+ if (v.end <= text.length && text.slice(v.start, v.end) === v.content) {
822
+ matchStart = v.start;
823
+ matchEnd = v.end;
824
+ } else {
825
+ const searchRadius = Math.max(64, v.content.length * 4);
826
+ const searchFrom = Math.max(0, v.start - searchRadius);
827
+ const searchTo = Math.min(text.length, v.end + searchRadius);
828
+ const regionLower = text.slice(searchFrom, searchTo).toLowerCase();
829
+ const contentLower = v.content.toLowerCase();
830
+ const idx = regionLower.indexOf(contentLower);
831
+ if (idx !== -1) {
832
+ matchStart = searchFrom + idx;
833
+ matchEnd = matchStart + v.content.length;
834
+ }
835
+ }
836
+ if (matchStart >= 0) {
837
+ rawRanges.push({
838
+ start: matchStart,
839
+ end: matchEnd,
840
+ annotation: {
841
+ type: "spellcheck",
842
+ id: `sc-${matchStart}-${matchEnd}`,
843
+ data: v
844
+ }
845
+ });
846
+ }
847
+ }
848
+ } else if (rule.type === "glossary") {
849
+ const { label, entries } = rule;
850
+ for (const entry of entries) {
851
+ if (!entry.term && !entry.pattern) continue;
852
+ if (entry.pattern) {
853
+ let re;
854
+ try {
855
+ re = new RegExp(entry.pattern, "g");
856
+ } catch {
857
+ continue;
858
+ }
859
+ let m;
860
+ while ((m = re.exec(text)) !== null) {
861
+ rawRanges.push({
862
+ start: m.index,
863
+ end: m.index + m[0].length,
864
+ annotation: {
865
+ type: "glossary",
866
+ id: `gl-${label}-${m.index}-${m.index + m[0].length}`,
867
+ data: {
868
+ label,
869
+ term: entry.term || entry.pattern,
870
+ description: entry.description
871
+ }
872
+ }
873
+ });
874
+ if (m[0].length === 0) re.lastIndex++;
875
+ }
876
+ } else {
877
+ let idx = 0;
878
+ while ((idx = text.indexOf(entry.term, idx)) !== -1) {
879
+ rawRanges.push({
880
+ start: idx,
881
+ end: idx + entry.term.length,
882
+ annotation: {
883
+ type: "glossary",
884
+ id: `gl-${label}-${idx}-${idx + entry.term.length}`,
885
+ data: {
886
+ label,
887
+ term: entry.term,
888
+ description: entry.description
889
+ }
890
+ }
891
+ });
892
+ idx += entry.term.length;
893
+ }
894
+ }
895
+ }
896
+ } else if (rule.type === "tag") {
897
+ const pairs = rule.pattern ? detectCustomTags(text, rule.pattern) : detectAndPairTags(text, rule.detectInner ?? true);
898
+ for (const p of pairs) {
899
+ const isHtml = !rule.pattern || p.tagName !== p.originalText;
900
+ rawRanges.push({
901
+ start: p.start,
902
+ end: p.end,
903
+ annotation: {
904
+ type: "tag",
905
+ id: `tag-${p.start}-${p.end}`,
906
+ data: {
907
+ tagNumber: p.tagNumber,
908
+ tagName: p.tagName,
909
+ isClosing: p.isClosing,
910
+ isSelfClosing: p.isSelfClosing,
911
+ originalText: p.originalText,
912
+ displayText: p.displayText,
913
+ isHtml
914
+ }
915
+ }
916
+ });
917
+ }
918
+ } else if (rule.type === "quote") {
919
+ const quoteMap = detectQuotes(text, rule.detectOptions);
920
+ const seen = /* @__PURE__ */ new Set();
921
+ for (const [, qr] of quoteMap) {
922
+ if (seen.has(qr.start)) continue;
923
+ seen.add(qr.start);
924
+ const mapping = qr.quoteType === "single" ? rule.singleQuote : rule.doubleQuote;
925
+ const originalChar = qr.quoteType === "single" ? "'" : '"';
926
+ rawRanges.push({
927
+ start: qr.start,
928
+ end: qr.start + 1,
929
+ annotation: {
930
+ type: "quote",
931
+ id: `q-${qr.quoteType}-open-${qr.start}`,
932
+ data: {
933
+ quoteType: qr.quoteType,
934
+ position: "opening",
935
+ originalChar,
936
+ replacementChar: mapping.opening
937
+ }
938
+ }
939
+ });
940
+ if (qr.closed && qr.end !== null) {
941
+ rawRanges.push({
942
+ start: qr.end,
943
+ end: qr.end + 1,
944
+ annotation: {
945
+ type: "quote",
946
+ id: `q-${qr.quoteType}-close-${qr.end}`,
947
+ data: {
948
+ quoteType: qr.quoteType,
949
+ position: "closing",
950
+ originalChar,
951
+ replacementChar: mapping.closing
952
+ }
953
+ }
954
+ });
955
+ }
956
+ }
957
+ } else if (rule.type === "special-char") {
958
+ const allEntries = [...rule.entries];
959
+ for (const entry of allEntries) {
960
+ const flags = entry.pattern.flags.includes("g") ? entry.pattern.flags : entry.pattern.flags + "g";
961
+ const re = new RegExp(entry.pattern.source, flags);
962
+ let m;
963
+ while ((m = re.exec(text)) !== null) {
964
+ const matchStr = m[0];
965
+ const cp = matchStr.split("").map(
966
+ (c) => "U+" + (c.codePointAt(0) ?? 0).toString(16).toUpperCase().padStart(4, "0")
967
+ ).join(" ");
968
+ rawRanges.push({
969
+ start: m.index,
970
+ end: m.index + matchStr.length,
971
+ annotation: {
972
+ type: "special-char",
973
+ id: `sp-${m.index}-${m.index + matchStr.length}`,
974
+ data: { name: entry.name, char: matchStr, codePoint: cp }
975
+ }
976
+ });
977
+ }
978
+ }
979
+ } else if (rule.type === "link") {
980
+ const defaultPattern = String.raw`https?:\/\/[^\s<>"']+|www\.[^\s<>"']+`;
981
+ const patternSource = rule.pattern ?? defaultPattern;
982
+ let re;
983
+ try {
984
+ re = new RegExp(patternSource, "gi");
985
+ } catch {
986
+ continue;
987
+ }
988
+ let m;
989
+ while ((m = re.exec(text)) !== null) {
990
+ if (m[0].length === 0) {
991
+ re.lastIndex++;
992
+ continue;
993
+ }
994
+ let matched = m[0];
995
+ const trailingPunct = /[.,;:!?)}\]]+$/;
996
+ const trailingMatch = trailingPunct.exec(matched);
997
+ if (trailingMatch) {
998
+ matched = matched.slice(0, -trailingMatch[0].length);
999
+ }
1000
+ const end = m.index + matched.length;
1001
+ const url = matched.startsWith("www.") ? "https://" + matched : matched;
1002
+ rawRanges.push({
1003
+ start: m.index,
1004
+ end,
1005
+ annotation: {
1006
+ type: "link",
1007
+ id: `link-${m.index}-${end}`,
1008
+ data: {
1009
+ url,
1010
+ displayText: matched
1011
+ }
1012
+ }
1013
+ });
1014
+ }
1015
+ }
1016
+ }
1017
+ if (rawRanges.length === 0) return [];
1018
+ const tagRules = rules.filter((r) => r.type === "tag");
1019
+ const tagsCollapsed = tagRules.some((r) => r.collapsed);
1020
+ const tagRanges = rawRanges.filter((r) => r.annotation.type === "tag");
1021
+ const quoteDetectInTags = rules.filter((r) => r.type === "quote").some((r) => r.detectInTags);
1022
+ let filteredRanges = rawRanges;
1023
+ if (tagRanges.length > 0) {
1024
+ filteredRanges = rawRanges.filter((r) => {
1025
+ if (r.annotation.type === "tag") return true;
1026
+ if (r.annotation.type === "quote" && !quoteDetectInTags) {
1027
+ return !tagRanges.some((t) => r.start >= t.start && r.end <= t.end);
1028
+ }
1029
+ if (tagsCollapsed) {
1030
+ return !tagRanges.some((t) => r.start < t.end && r.end > t.start);
1031
+ }
1032
+ return true;
1033
+ });
1034
+ }
1035
+ const points = /* @__PURE__ */ new Set();
1036
+ for (const r of filteredRanges) {
1037
+ points.add(r.start);
1038
+ points.add(r.end);
1039
+ }
1040
+ const sortedPoints = [...points].sort((a, b) => a - b);
1041
+ const segments = [];
1042
+ for (let i = 0; i < sortedPoints.length - 1; i++) {
1043
+ const segStart = sortedPoints[i];
1044
+ const segEnd = sortedPoints[i + 1];
1045
+ const annotations = filteredRanges.filter((r) => r.start <= segStart && r.end >= segEnd).map((r) => r.annotation);
1046
+ if (annotations.length > 0) {
1047
+ segments.push({ start: segStart, end: segEnd, annotations });
1048
+ }
1049
+ }
1050
+ return segments;
1051
+ }
1052
+
1053
+ // src/layout/cat-editor/selection-helpers.ts
1054
+ import { $getRoot } from "lexical";
1055
+ function $isCEFalseToken(node) {
1056
+ const types = node.__highlightTypes.split(",");
1057
+ if (types.includes("tag-collapsed")) return true;
1058
+ if (types.includes("quote") && node.__displayText) return true;
1059
+ return false;
1060
+ }
1061
+ function $pointToGlobalOffset(nodeKey, offset) {
1062
+ const root = $getRoot();
1063
+ const paragraphs = root.getChildren();
1064
+ let global = 0;
1065
+ for (let pi = 0; pi < paragraphs.length; pi++) {
1066
+ if (pi > 0) global += 1;
1067
+ const p = paragraphs[pi];
1068
+ if (p.getKey() === nodeKey) {
1069
+ if ("getChildren" in p) {
1070
+ const children = p.getChildren();
1071
+ let childChars = 0;
1072
+ for (let ci = 0; ci < Math.min(offset, children.length); ci++) {
1073
+ const child = children[ci];
1074
+ if ($isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX))
1075
+ continue;
1076
+ childChars += child.getTextContent().length;
1077
+ }
1078
+ return global + childChars;
1079
+ }
1080
+ return global;
1081
+ }
1082
+ if (!("getChildren" in p)) continue;
1083
+ for (const child of p.getChildren()) {
1084
+ const isNlMarker = $isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX);
1085
+ if (child.getKey() === nodeKey) {
1086
+ if (isNlMarker) return global;
1087
+ if ($isHighlightNode(child) && child.getMode() === "token") {
1088
+ return global + (offset > 0 ? child.getTextContent().length : 0);
1089
+ }
1090
+ if ($isMentionNode(child)) {
1091
+ return global + (offset > 0 ? child.getTextContent().length : 0);
1092
+ }
1093
+ return global + offset;
1094
+ }
1095
+ if (isNlMarker) continue;
1096
+ global += child.getTextContent().length;
1097
+ }
1098
+ }
1099
+ return global;
1100
+ }
1101
+ function $globalOffsetToPoint(target) {
1102
+ const root = $getRoot();
1103
+ const paragraphs = root.getChildren();
1104
+ let remaining = target;
1105
+ for (let pi = 0; pi < paragraphs.length; pi++) {
1106
+ if (pi > 0) {
1107
+ if (remaining <= 0) {
1108
+ const p2 = paragraphs[pi];
1109
+ if ("getChildren" in p2) {
1110
+ for (const child of p2.getChildren()) {
1111
+ if ($isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX))
1112
+ continue;
1113
+ return { key: child.getKey(), offset: 0, type: "text" };
1114
+ }
1115
+ }
1116
+ return { key: paragraphs[pi].getKey(), offset: 0, type: "element" };
1117
+ }
1118
+ remaining -= 1;
1119
+ }
1120
+ const p = paragraphs[pi];
1121
+ if (!("getChildren" in p)) continue;
1122
+ const allChildren = p.getChildren();
1123
+ for (let ci = 0; ci < allChildren.length; ci++) {
1124
+ const child = allChildren[ci];
1125
+ if ($isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX))
1126
+ continue;
1127
+ const len = child.getTextContent().length;
1128
+ if ($isHighlightNode(child) && $isCEFalseToken(child)) {
1129
+ if (remaining <= 0) {
1130
+ return { key: p.getKey(), offset: ci, type: "element" };
1131
+ }
1132
+ remaining -= len;
1133
+ continue;
1134
+ }
1135
+ if ($isMentionNode(child)) {
1136
+ if (remaining <= 0) {
1137
+ return { key: p.getKey(), offset: ci, type: "element" };
1138
+ }
1139
+ remaining -= len;
1140
+ continue;
1141
+ }
1142
+ if ($isHighlightNode(child) && child.getMode() === "token") {
1143
+ if (remaining > len) {
1144
+ remaining -= len;
1145
+ continue;
1146
+ }
1147
+ return {
1148
+ key: child.getKey(),
1149
+ offset: remaining <= 0 ? 0 : remaining >= len ? len : remaining <= len / 2 ? 0 : len,
1150
+ type: "text"
1151
+ };
1152
+ }
1153
+ if (remaining <= len) {
1154
+ return {
1155
+ key: child.getKey(),
1156
+ offset: Math.max(0, remaining),
1157
+ type: "text"
1158
+ };
1159
+ }
1160
+ remaining -= len;
1161
+ }
1162
+ if (remaining <= 0) {
1163
+ let afterIdx = allChildren.length;
1164
+ for (let ci = allChildren.length - 1; ci >= 0; ci--) {
1165
+ const c = allChildren[ci];
1166
+ if ($isHighlightNode(c) && c.__ruleIds.startsWith(NL_MARKER_PREFIX)) {
1167
+ afterIdx = ci;
1168
+ } else {
1169
+ break;
1170
+ }
1171
+ }
1172
+ return { key: p.getKey(), offset: afterIdx, type: "element" };
1173
+ }
1174
+ }
1175
+ for (let pi = paragraphs.length - 1; pi >= 0; pi--) {
1176
+ const p = paragraphs[pi];
1177
+ if ("getChildren" in p) {
1178
+ const allChildren = p.getChildren();
1179
+ for (let ci = allChildren.length - 1; ci >= 0; ci--) {
1180
+ const child = allChildren[ci];
1181
+ if ($isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX))
1182
+ continue;
1183
+ if ($isHighlightNode(child) && $isCEFalseToken(child)) continue;
1184
+ if ($isMentionNode(child)) continue;
1185
+ return {
1186
+ key: child.getKey(),
1187
+ offset: child.getTextContent().length,
1188
+ type: "text"
1189
+ };
1190
+ }
1191
+ let afterIdx = allChildren.length;
1192
+ for (let ci = allChildren.length - 1; ci >= 0; ci--) {
1193
+ const c = allChildren[ci];
1194
+ if ($isHighlightNode(c) && c.__ruleIds.startsWith(NL_MARKER_PREFIX)) {
1195
+ afterIdx = ci;
1196
+ } else {
1197
+ break;
1198
+ }
1199
+ }
1200
+ return { key: p.getKey(), offset: afterIdx, type: "element" };
1201
+ }
1202
+ }
1203
+ return null;
1204
+ }
1205
+
1206
+ // src/layout/cat-editor/plugins.tsx
1207
+ function HighlightsPlugin({
1208
+ rules,
1209
+ annotationMapRef,
1210
+ codepointDisplayMap
1211
+ }) {
1212
+ const [editor] = useLexicalComposerContext2();
1213
+ const rafRef = useRef2(null);
1214
+ const applyHighlights = useCallback2(() => {
1215
+ setCodepointOverrides(codepointDisplayMap);
1216
+ const editorElement = editor.getRootElement();
1217
+ const editorHasFocus = editorElement != null && editorElement.contains(editorElement.ownerDocument.activeElement);
1218
+ editor.update(
1219
+ () => {
1220
+ $addUpdateTag("cat-highlights");
1221
+ const root = $getRoot2();
1222
+ const paragraphs = root.getChildren();
1223
+ const lines = [];
1224
+ const savedMentions = [];
1225
+ let collectOffset = 0;
1226
+ for (let pIdx = 0; pIdx < paragraphs.length; pIdx++) {
1227
+ const p = paragraphs[pIdx];
1228
+ let lineText = "";
1229
+ if ("getChildren" in p) {
1230
+ for (const child of p.getChildren()) {
1231
+ if ($isHighlightNode(child) && child.__ruleIds.startsWith(NL_MARKER_PREFIX)) {
1232
+ continue;
1233
+ }
1234
+ if ($isMentionNode(child)) {
1235
+ const text = child.getTextContent();
1236
+ savedMentions.push({
1237
+ start: collectOffset + lineText.length,
1238
+ end: collectOffset + lineText.length + text.length,
1239
+ mentionId: child.__mentionId,
1240
+ mentionName: child.__mentionName,
1241
+ text
1242
+ });
1243
+ }
1244
+ lineText += child.getTextContent();
1245
+ }
1246
+ } else {
1247
+ lineText = p.getTextContent();
1248
+ }
1249
+ lines.push(lineText);
1250
+ collectOffset += lineText.length + 1;
1251
+ }
1252
+ const fullText = lines.join("\n");
1253
+ const mentionRule = rules.find(
1254
+ (r) => r.type === "mention"
1255
+ );
1256
+ if (mentionRule) {
1257
+ const pattern = getMentionPattern();
1258
+ let match;
1259
+ while ((match = pattern.exec(fullText)) !== null) {
1260
+ const matchStart = match.index;
1261
+ const matchEnd = matchStart + match[0].length;
1262
+ const matchId = match[1];
1263
+ const alreadySaved = savedMentions.some(
1264
+ (m) => m.start === matchStart && m.end === matchEnd
1265
+ );
1266
+ if (alreadySaved) continue;
1267
+ const user = mentionRule.users.find((u) => u.id === matchId);
1268
+ if (user) {
1269
+ savedMentions.push({
1270
+ start: matchStart,
1271
+ end: matchEnd,
1272
+ mentionId: user.id,
1273
+ mentionName: user.name,
1274
+ text: match[0]
1275
+ });
1276
+ }
1277
+ }
1278
+ }
1279
+ let segmentText = fullText;
1280
+ for (let mi = savedMentions.length - 1; mi >= 0; mi--) {
1281
+ const m = savedMentions[mi];
1282
+ segmentText = segmentText.slice(0, m.start) + "".repeat(m.end - m.start) + segmentText.slice(m.end);
1283
+ }
1284
+ const segments = computeHighlightSegments(segmentText, rules);
1285
+ const newMap = /* @__PURE__ */ new Map();
1286
+ for (const seg of segments) {
1287
+ for (const ann of seg.annotations) {
1288
+ newMap.set(ann.id, ann);
1289
+ }
1290
+ }
1291
+ annotationMapRef.current = newMap;
1292
+ const prevSelection = $getSelection();
1293
+ let savedAnchor = null;
1294
+ let savedFocus = null;
1295
+ if ($isRangeSelection(prevSelection)) {
1296
+ savedAnchor = $pointToGlobalOffset(
1297
+ prevSelection.anchor.key,
1298
+ prevSelection.anchor.offset
1299
+ );
1300
+ savedFocus = $pointToGlobalOffset(
1301
+ prevSelection.focus.key,
1302
+ prevSelection.focus.offset
1303
+ );
1304
+ }
1305
+ root.clear();
1306
+ if (fullText.length === 0) {
1307
+ const p = $createParagraphNode();
1308
+ p.append($createTextNode2(""));
1309
+ root.append(p);
1310
+ return;
1311
+ }
1312
+ const emittedMentionStarts = /* @__PURE__ */ new Set();
1313
+ const appendWithMentions = (paragraph, rangeStart, rangeEnd, makeNode) => {
1314
+ const overlapping = savedMentions.filter(
1315
+ (m) => m.start < rangeEnd && m.end > rangeStart && !emittedMentionStarts.has(m.start)
1316
+ );
1317
+ if (overlapping.length === 0) {
1318
+ const text = fullText.slice(rangeStart, rangeEnd);
1319
+ if (text.length > 0) {
1320
+ paragraph.append(makeNode(text));
1321
+ }
1322
+ return;
1323
+ }
1324
+ overlapping.sort((a, b) => a.start - b.start);
1325
+ let cursor = rangeStart;
1326
+ for (const m of overlapping) {
1327
+ const mStart = Math.max(m.start, rangeStart);
1328
+ const mEnd = Math.min(m.end, rangeEnd);
1329
+ if (mStart > cursor) {
1330
+ const beforeText = fullText.slice(cursor, mStart);
1331
+ if (beforeText.length > 0) {
1332
+ paragraph.append(makeNode(beforeText));
1333
+ }
1334
+ }
1335
+ paragraph.append(
1336
+ $createMentionNode(m.mentionId, m.mentionName, m.text)
1337
+ );
1338
+ emittedMentionStarts.add(m.start);
1339
+ cursor = mEnd;
1340
+ }
1341
+ if (cursor < rangeEnd) {
1342
+ const afterText = fullText.slice(cursor, rangeEnd);
1343
+ if (afterText.length > 0) {
1344
+ paragraph.append(makeNode(afterText));
1345
+ }
1346
+ }
1347
+ };
1348
+ const textLines = fullText.split("\n");
1349
+ let globalOffset = 0;
1350
+ for (const line of textLines) {
1351
+ const paragraph = $createParagraphNode();
1352
+ const lineStart = globalOffset;
1353
+ const lineEnd = globalOffset + line.length;
1354
+ const lineSegments = segments.filter(
1355
+ (s) => s.start < lineEnd && s.end > lineStart
1356
+ );
1357
+ let pos = lineStart;
1358
+ for (const seg of lineSegments) {
1359
+ const sStart = Math.max(seg.start, lineStart);
1360
+ const sEnd = Math.min(seg.end, lineEnd);
1361
+ if (sStart > pos) {
1362
+ appendWithMentions(
1363
+ paragraph,
1364
+ pos,
1365
+ sStart,
1366
+ (text) => $createTextNode2(text)
1367
+ );
1368
+ }
1369
+ const tagAnn = seg.annotations.find((a) => a.type === "tag");
1370
+ const tagRule = rules.find((r) => r.type === "tag");
1371
+ const tagsCollapsed = !!tagRule?.collapsed;
1372
+ const collapseScope = tagRule?.collapseScope ?? "all";
1373
+ const thisTagCollapsed = tagsCollapsed && !!tagAnn && (collapseScope === "all" || tagAnn.data.isHtml);
1374
+ const typesArr = [
1375
+ ...new Set(
1376
+ seg.annotations.map((a) => {
1377
+ if (a.type === "glossary") return `glossary-${a.data.label}`;
1378
+ if (a.type === "spellcheck")
1379
+ return `spellcheck-${a.data.categoryId}`;
1380
+ return a.type;
1381
+ })
1382
+ )
1383
+ ];
1384
+ if (thisTagCollapsed) {
1385
+ typesArr.push("tag-collapsed");
1386
+ }
1387
+ const types = typesArr.join(",");
1388
+ const ids = seg.annotations.map((a) => a.id).join(",");
1389
+ const tagDisplayText = tagAnn?.type === "tag" && thisTagCollapsed ? tagAnn.data.displayText : void 0;
1390
+ const isTagToken = thisTagCollapsed;
1391
+ const quoteAnn = seg.annotations.find((a) => a.type === "quote");
1392
+ const quoteDisplayText = quoteAnn?.type === "quote" ? quoteAnn.data.replacementChar : void 0;
1393
+ const isQuoteToken = !!quoteAnn;
1394
+ const containingMention = savedMentions.find(
1395
+ (m) => m.start <= sStart && m.end >= sEnd
1396
+ );
1397
+ if (containingMention) {
1398
+ if (!emittedMentionStarts.has(containingMention.start)) {
1399
+ paragraph.append(
1400
+ $createMentionNode(
1401
+ containingMention.mentionId,
1402
+ containingMention.mentionName,
1403
+ containingMention.text
1404
+ )
1405
+ );
1406
+ emittedMentionStarts.add(containingMention.start);
1407
+ }
1408
+ } else {
1409
+ appendWithMentions(
1410
+ paragraph,
1411
+ sStart,
1412
+ sEnd,
1413
+ (text) => $createHighlightNode(
1414
+ text,
1415
+ types,
1416
+ ids,
1417
+ tagDisplayText ?? quoteDisplayText,
1418
+ isTagToken || isQuoteToken
1419
+ )
1420
+ );
1421
+ }
1422
+ pos = sEnd;
1423
+ }
1424
+ if (pos < lineEnd) {
1425
+ appendWithMentions(
1426
+ paragraph,
1427
+ pos,
1428
+ lineEnd,
1429
+ (text) => $createTextNode2(text)
1430
+ );
1431
+ }
1432
+ const nlPos = lineEnd;
1433
+ if (nlPos < fullText.length && fullText[nlPos] === "\n") {
1434
+ const nlSegments = segments.filter(
1435
+ (s) => s.start <= nlPos && s.end > nlPos
1436
+ );
1437
+ if (nlSegments.length > 0) {
1438
+ const nlAnns = nlSegments.flatMap((s) => s.annotations);
1439
+ const types = [
1440
+ ...new Set(
1441
+ nlAnns.map((a) => {
1442
+ if (a.type === "glossary") return `glossary-${a.data.label}`;
1443
+ if (a.type === "spellcheck")
1444
+ return `spellcheck-${a.data.categoryId}`;
1445
+ return a.type;
1446
+ })
1447
+ )
1448
+ ].join(",");
1449
+ const ids = nlAnns.map((a) => a.id).join(",");
1450
+ const symbol = CODEPOINT_DISPLAY_MAP[10];
1451
+ if (paragraph.getChildrenSize() === 0) {
1452
+ paragraph.append($createTextNode2(""));
1453
+ }
1454
+ paragraph.append(
1455
+ $createHighlightNode(symbol, types, NL_MARKER_PREFIX + ids)
1456
+ );
1457
+ }
1458
+ }
1459
+ if (paragraph.getChildrenSize() === 0) {
1460
+ paragraph.append($createTextNode2(""));
1461
+ }
1462
+ root.append(paragraph);
1463
+ globalOffset = lineEnd + 1;
1464
+ }
1465
+ if (editorHasFocus && savedAnchor !== null && savedFocus !== null) {
1466
+ const anchorPt = $globalOffsetToPoint(savedAnchor);
1467
+ const focusPt = $globalOffsetToPoint(savedFocus);
1468
+ if (anchorPt && focusPt) {
1469
+ const sel = $createRangeSelection();
1470
+ sel.anchor.set(anchorPt.key, anchorPt.offset, anchorPt.type);
1471
+ sel.focus.set(focusPt.key, focusPt.offset, focusPt.type);
1472
+ $setSelection(sel);
1473
+ }
1474
+ } else {
1475
+ $setSelection(null);
1476
+ }
1477
+ },
1478
+ { tag: "historic" }
1479
+ );
1480
+ }, [editor, rules, annotationMapRef]);
1481
+ useEffect2(() => {
1482
+ applyHighlights();
1483
+ }, [applyHighlights]);
1484
+ useEffect2(() => {
1485
+ const unregister = editor.registerUpdateListener(
1486
+ ({ tags, dirtyElements, dirtyLeaves }) => {
1487
+ if (tags.has("cat-highlights")) return;
1488
+ if (dirtyElements.size === 0 && dirtyLeaves.size === 0) return;
1489
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
1490
+ rafRef.current = requestAnimationFrame(() => {
1491
+ applyHighlights();
1492
+ });
1493
+ }
1494
+ );
1495
+ return () => {
1496
+ unregister();
1497
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
1498
+ };
1499
+ }, [editor, applyHighlights]);
1500
+ useEffect2(() => {
1501
+ return editor.registerCommand(
1502
+ PASTE_COMMAND,
1503
+ (event) => {
1504
+ const clipboardData = event.clipboardData;
1505
+ if (!clipboardData) return false;
1506
+ const text = clipboardData.getData("text/plain");
1507
+ if (text && text !== text.replace(/\n+$/, "")) {
1508
+ event.preventDefault();
1509
+ const trimmed = text.replace(/\n+$/, "");
1510
+ editor.update(() => {
1511
+ const selection = $getSelection();
1512
+ if ($isRangeSelection(selection)) {
1513
+ selection.insertRawText(trimmed);
1514
+ }
1515
+ });
1516
+ return true;
1517
+ }
1518
+ return false;
1519
+ },
1520
+ COMMAND_PRIORITY_HIGH
1521
+ );
1522
+ }, [editor]);
1523
+ return null;
1524
+ }
1525
+ function EditorRefPlugin({
1526
+ editorRef,
1527
+ savedSelectionRef
1528
+ }) {
1529
+ const [editor] = useLexicalComposerContext2();
1530
+ useEffect2(() => {
1531
+ editorRef.current = editor;
1532
+ }, [editor, editorRef]);
1533
+ useEffect2(() => {
1534
+ return editor.registerUpdateListener(({ editorState }) => {
1535
+ editorState.read(() => {
1536
+ const sel = $getSelection();
1537
+ if ($isRangeSelection(sel)) {
1538
+ savedSelectionRef.current = {
1539
+ anchor: $pointToGlobalOffset(sel.anchor.key, sel.anchor.offset),
1540
+ focus: $pointToGlobalOffset(sel.focus.key, sel.focus.offset)
1541
+ };
1542
+ }
1543
+ });
1544
+ });
1545
+ }, [editor, savedSelectionRef]);
1546
+ return null;
1547
+ }
1548
+ function $isNonEditableNode(node) {
1549
+ if (!$isHighlightNode(node)) return false;
1550
+ if (node.__ruleIds.startsWith(NL_MARKER_PREFIX)) return true;
1551
+ const types = node.__highlightTypes.split(",");
1552
+ if (types.includes("tag-collapsed")) return true;
1553
+ if (types.includes("quote") && node.__displayText) return true;
1554
+ return false;
1555
+ }
1556
+ function $clampPointAwayFromNonEditable(point) {
1557
+ if (point.type === "element") return null;
1558
+ const node = point.getNode();
1559
+ if (!$isNonEditableNode(node)) return null;
1560
+ const parent = node.getParent();
1561
+ if (parent && "getChildren" in parent) {
1562
+ const siblings = parent.getChildren();
1563
+ const idx = siblings.findIndex((s) => s.getKey() === node.getKey());
1564
+ if (idx >= 0) {
1565
+ const elemOffset = point.offset > 0 ? idx + 1 : idx;
1566
+ return {
1567
+ key: parent.getKey(),
1568
+ offset: elemOffset,
1569
+ type: "element"
1570
+ };
1571
+ }
1572
+ }
1573
+ return null;
1574
+ }
1575
+ function $findNextEditable(startNode) {
1576
+ if ($isHighlightNode(startNode) && startNode.__ruleIds.startsWith(NL_MARKER_PREFIX)) {
1577
+ const paragraph2 = startNode.getParent();
1578
+ const nextParagraph = paragraph2?.getNextSibling();
1579
+ if (nextParagraph && "getChildren" in nextParagraph) {
1580
+ const first = nextParagraph.getFirstChild();
1581
+ if (first && !$isNonEditableNode(first)) {
1582
+ return { key: first.getKey(), offset: 0, type: "text" };
1583
+ }
1584
+ if (first) {
1585
+ return {
1586
+ key: nextParagraph.getKey(),
1587
+ offset: 0,
1588
+ type: "element"
1589
+ };
1590
+ }
1591
+ }
1592
+ return null;
1593
+ }
1594
+ const next = startNode.getNextSibling();
1595
+ if (next && !$isNonEditableNode(next)) {
1596
+ return { key: next.getKey(), offset: 0, type: "text" };
1597
+ }
1598
+ const paragraph = startNode.getParent();
1599
+ if (paragraph && "getChildren" in paragraph) {
1600
+ const children = paragraph.getChildren();
1601
+ const idx = children.findIndex((s) => s.getKey() === startNode.getKey());
1602
+ if (idx >= 0) {
1603
+ return { key: paragraph.getKey(), offset: idx + 1, type: "element" };
1604
+ }
1605
+ }
1606
+ return null;
1607
+ }
1608
+ function $findPrevEditable(startNode) {
1609
+ const prev = startNode.getPreviousSibling();
1610
+ if (prev && !$isNonEditableNode(prev)) {
1611
+ return {
1612
+ key: prev.getKey(),
1613
+ offset: prev.getTextContent().length,
1614
+ type: "text"
1615
+ };
1616
+ }
1617
+ const paragraph = startNode.getParent();
1618
+ if (paragraph && "getChildren" in paragraph) {
1619
+ const children = paragraph.getChildren();
1620
+ const idx = children.findIndex((s) => s.getKey() === startNode.getKey());
1621
+ if (idx >= 0) {
1622
+ return { key: paragraph.getKey(), offset: idx, type: "element" };
1623
+ }
1624
+ }
1625
+ return null;
1626
+ }
1627
+ function NLMarkerNavigationPlugin() {
1628
+ const [editor] = useLexicalComposerContext2();
1629
+ useEffect2(() => {
1630
+ const unregRight = editor.registerCommand(
1631
+ KEY_ARROW_RIGHT_COMMAND,
1632
+ (event) => {
1633
+ const selection = $getSelection();
1634
+ if (!$isRangeSelection(selection) || !selection.isCollapsed())
1635
+ return false;
1636
+ const isRTL = $isParentElementRTL(selection);
1637
+ const { anchor } = selection;
1638
+ const node = anchor.getNode();
1639
+ let adjacentNode = null;
1640
+ if (isRTL) {
1641
+ if (anchor.type === "text") {
1642
+ if (anchor.offset > 0) return false;
1643
+ adjacentNode = node.getPreviousSibling();
1644
+ } else {
1645
+ const children = node.getChildren();
1646
+ adjacentNode = children[anchor.offset - 1] ?? null;
1647
+ }
1648
+ } else {
1649
+ if (anchor.type === "text") {
1650
+ if (anchor.offset < node.getTextContent().length) return false;
1651
+ adjacentNode = node.getNextSibling();
1652
+ } else {
1653
+ const children = node.getChildren();
1654
+ adjacentNode = children[anchor.offset] ?? null;
1655
+ }
1656
+ }
1657
+ if (adjacentNode && $isNonEditableNode(adjacentNode)) {
1658
+ const target = isRTL ? $findPrevEditable(adjacentNode) : $findNextEditable(adjacentNode);
1659
+ if (target) {
1660
+ selection.anchor.set(target.key, target.offset, target.type);
1661
+ selection.focus.set(target.key, target.offset, target.type);
1662
+ event.preventDefault();
1663
+ return true;
1664
+ }
1665
+ return false;
1666
+ }
1667
+ if ($isNonEditableNode(node)) {
1668
+ const target = isRTL ? $findPrevEditable(node) : $findNextEditable(node);
1669
+ if (target) {
1670
+ selection.anchor.set(target.key, target.offset, target.type);
1671
+ selection.focus.set(target.key, target.offset, target.type);
1672
+ event.preventDefault();
1673
+ return true;
1674
+ }
1675
+ return false;
1676
+ }
1677
+ return false;
1678
+ },
1679
+ COMMAND_PRIORITY_HIGH
1680
+ );
1681
+ const unregLeft = editor.registerCommand(
1682
+ KEY_ARROW_LEFT_COMMAND,
1683
+ (event) => {
1684
+ const selection = $getSelection();
1685
+ if (!$isRangeSelection(selection) || !selection.isCollapsed())
1686
+ return false;
1687
+ const isRTL = $isParentElementRTL(selection);
1688
+ const { anchor } = selection;
1689
+ const node = anchor.getNode();
1690
+ let adjacentNode = null;
1691
+ if (isRTL) {
1692
+ if (anchor.type === "text") {
1693
+ if (anchor.offset < node.getTextContent().length) return false;
1694
+ adjacentNode = node.getNextSibling();
1695
+ } else {
1696
+ const children = node.getChildren();
1697
+ adjacentNode = children[anchor.offset] ?? null;
1698
+ }
1699
+ } else {
1700
+ if (anchor.type === "text") {
1701
+ if (anchor.offset > 0) return false;
1702
+ adjacentNode = node.getPreviousSibling();
1703
+ } else {
1704
+ const children = node.getChildren();
1705
+ adjacentNode = children[anchor.offset - 1] ?? null;
1706
+ }
1707
+ }
1708
+ if (adjacentNode && $isNonEditableNode(adjacentNode)) {
1709
+ const target = isRTL ? $findNextEditable(adjacentNode) : $findPrevEditable(adjacentNode);
1710
+ if (target) {
1711
+ selection.anchor.set(target.key, target.offset, target.type);
1712
+ selection.focus.set(target.key, target.offset, target.type);
1713
+ event.preventDefault();
1714
+ return true;
1715
+ }
1716
+ return false;
1717
+ }
1718
+ if ($isNonEditableNode(node)) {
1719
+ const target = isRTL ? $findNextEditable(node) : $findPrevEditable(node);
1720
+ if (target) {
1721
+ selection.anchor.set(target.key, target.offset, target.type);
1722
+ selection.focus.set(target.key, target.offset, target.type);
1723
+ event.preventDefault();
1724
+ return true;
1725
+ }
1726
+ return false;
1727
+ }
1728
+ return false;
1729
+ },
1730
+ COMMAND_PRIORITY_HIGH
1731
+ );
1732
+ const unregSel = editor.registerCommand(
1733
+ SELECTION_CHANGE_COMMAND,
1734
+ () => {
1735
+ const selection = $getSelection();
1736
+ if (!$isRangeSelection(selection)) return false;
1737
+ const anchorFix = $clampPointAwayFromNonEditable(selection.anchor);
1738
+ const focusFix = $clampPointAwayFromNonEditable(selection.focus);
1739
+ if (anchorFix) {
1740
+ selection.anchor.set(anchorFix.key, anchorFix.offset, anchorFix.type);
1741
+ }
1742
+ if (focusFix) {
1743
+ selection.focus.set(focusFix.key, focusFix.offset, focusFix.type);
1744
+ }
1745
+ return false;
1746
+ },
1747
+ COMMAND_PRIORITY_HIGH
1748
+ );
1749
+ return () => {
1750
+ unregRight();
1751
+ unregLeft();
1752
+ unregSel();
1753
+ };
1754
+ }, [editor]);
1755
+ return null;
1756
+ }
1757
+
1758
+ // src/layout/cat-editor/popover.tsx
1759
+ import * as React from "react";
1760
+ import { useLayoutEffect, useRef as useRef3 } from "react";
1761
+ import { createPopper } from "@popperjs/core";
1762
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1763
+ function SpellCheckPopoverContent({
1764
+ data,
1765
+ onSuggestionClick
1766
+ }) {
1767
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-2.5 p-3 max-w-sm", children: [
1768
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
1769
+ /* @__PURE__ */ jsx2("span", { className: "cat-badge cat-badge-spell", children: data.shortMessage || "Spelling" }),
1770
+ /* @__PURE__ */ jsx2("span", { className: "text-[11px] text-muted-foreground", children: data.categoryId })
1771
+ ] }),
1772
+ /* @__PURE__ */ jsx2("p", { className: "text-sm leading-relaxed text-foreground", children: data.message }),
1773
+ data.content && /* @__PURE__ */ jsxs2("p", { className: "text-xs text-muted-foreground", children: [
1774
+ "Found:",
1775
+ " ",
1776
+ /* @__PURE__ */ jsx2("code", { className: "rounded bg-muted px-1 py-0.5 font-mono text-destructive-foreground", children: data.content })
1777
+ ] }),
1778
+ data.suggestions.length > 0 && /* @__PURE__ */ jsxs2("div", { className: "space-y-1.5", children: [
1779
+ /* @__PURE__ */ jsx2("p", { className: "text-xs font-medium text-muted-foreground", children: "Suggestions:" }),
1780
+ /* @__PURE__ */ jsx2("div", { className: "flex flex-wrap gap-1", children: data.suggestions.map((s, i) => /* @__PURE__ */ jsx2(
1781
+ "button",
1782
+ {
1783
+ type: "button",
1784
+ className: "cat-suggestion-btn",
1785
+ onClick: () => onSuggestionClick(s.value),
1786
+ children: s.value
1787
+ },
1788
+ i
1789
+ )) })
1790
+ ] }),
1791
+ data.dictionaries && data.dictionaries.length > 0 && /* @__PURE__ */ jsxs2("p", { className: "text-[11px] text-muted-foreground", children: [
1792
+ "Dictionaries: ",
1793
+ data.dictionaries.join(", ")
1794
+ ] })
1795
+ ] });
1796
+ }
1797
+ function KeywordsPopoverContent({
1798
+ data
1799
+ }) {
1800
+ const displayLabel = data.label.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1801
+ return /* @__PURE__ */ jsxs2("div", { className: "p-3 max-w-xs space-y-2", children: [
1802
+ /* @__PURE__ */ jsx2(
1803
+ "span",
1804
+ {
1805
+ className: `cat-badge cat-badge-glossary cat-badge-glossary-${data.label}`,
1806
+ children: displayLabel
1807
+ }
1808
+ ),
1809
+ /* @__PURE__ */ jsxs2("p", { className: "text-sm leading-relaxed text-foreground", children: [
1810
+ "Term:",
1811
+ " ",
1812
+ /* @__PURE__ */ jsx2("strong", { className: "font-semibold text-foreground", children: data.term })
1813
+ ] }),
1814
+ data.description && /* @__PURE__ */ jsx2("p", { className: "text-xs text-muted-foreground leading-relaxed", children: data.description })
1815
+ ] });
1816
+ }
1817
+ function SpecialCharPopoverContent({
1818
+ data
1819
+ }) {
1820
+ const cp = data.char.codePointAt(0) ?? 0;
1821
+ const effectiveMap = getEffectiveCodepointMap();
1822
+ const displaySymbol = effectiveMap[cp] ?? (data.char.trim() === "" ? "\xB7" : data.char);
1823
+ return /* @__PURE__ */ jsxs2("div", { className: "p-3 max-w-xs space-y-3", children: [
1824
+ /* @__PURE__ */ jsx2("span", { className: "cat-badge cat-badge-special-char", children: "Special Char" }),
1825
+ /* @__PURE__ */ jsx2("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsx2("span", { className: "inline-flex items-center justify-center min-w-12 min-h-12 rounded-lg border-2 border-border bg-muted px-3 py-2 text-2xl font-bold font-mono text-foreground select-none", children: displaySymbol }) }),
1826
+ /* @__PURE__ */ jsx2("p", { className: "text-sm leading-relaxed text-foreground text-center", children: /* @__PURE__ */ jsx2("strong", { className: "font-semibold", children: data.name }) }),
1827
+ /* @__PURE__ */ jsx2("div", { className: "flex items-center justify-center gap-3 text-xs text-muted-foreground", children: /* @__PURE__ */ jsx2("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono", children: data.codePoint }) })
1828
+ ] });
1829
+ }
1830
+ function TagPopoverContent({
1831
+ data
1832
+ }) {
1833
+ const isPlaceholder = !data.isClosing && !data.isSelfClosing && data.tagName === data.originalText;
1834
+ return /* @__PURE__ */ jsxs2("div", { className: "p-3 max-w-xs space-y-2", children: [
1835
+ /* @__PURE__ */ jsxs2("span", { className: "cat-badge cat-badge-tag", children: [
1836
+ isPlaceholder ? "Placeholder" : "Tag",
1837
+ " #",
1838
+ data.tagNumber
1839
+ ] }),
1840
+ /* @__PURE__ */ jsxs2("p", { className: "text-sm leading-relaxed text-foreground", children: [
1841
+ isPlaceholder ? "Placeholder" : data.isClosing ? "Closing tag" : data.isSelfClosing ? "Self-closing tag" : "Opening tag",
1842
+ ":",
1843
+ " ",
1844
+ /* @__PURE__ */ jsx2("strong", { className: "font-semibold text-foreground", children: data.originalText })
1845
+ ] }),
1846
+ /* @__PURE__ */ jsxs2("p", { className: "text-xs text-muted-foreground", children: [
1847
+ "Collapsed:",
1848
+ " ",
1849
+ /* @__PURE__ */ jsx2("code", { className: "rounded bg-muted px-1 py-0.5 font-mono", children: data.displayText })
1850
+ ] }),
1851
+ /* @__PURE__ */ jsxs2("p", { className: "text-xs text-muted-foreground break-all", children: [
1852
+ "Original:",
1853
+ " ",
1854
+ /* @__PURE__ */ jsx2("code", { className: "rounded bg-muted px-1 py-0.5 font-mono", children: data.originalText })
1855
+ ] })
1856
+ ] });
1857
+ }
1858
+ function LinkPopoverContent({
1859
+ data,
1860
+ onOpen
1861
+ }) {
1862
+ return /* @__PURE__ */ jsxs2("div", { className: "p-3 max-w-xs space-y-2", children: [
1863
+ /* @__PURE__ */ jsx2("span", { className: "cat-badge cat-badge-link", children: "Link" }),
1864
+ /* @__PURE__ */ jsx2("p", { className: "text-sm leading-relaxed text-foreground break-all", children: /* @__PURE__ */ jsx2("code", { className: "rounded bg-muted px-1 py-0.5 font-mono text-xs", children: data.url }) }),
1865
+ /* @__PURE__ */ jsx2("button", { type: "button", className: "cat-suggestion-btn", onClick: onOpen, children: "Open link \u2197" })
1866
+ ] });
1867
+ }
1868
+ function QuotePopoverContent({
1869
+ data
1870
+ }) {
1871
+ return /* @__PURE__ */ jsxs2("div", { className: "p-3 max-w-xs space-y-2", children: [
1872
+ /* @__PURE__ */ jsx2(
1873
+ "span",
1874
+ {
1875
+ className: `cat-badge cat-badge-quote cat-badge-quote-${data.quoteType}`,
1876
+ children: data.quoteType === "single" ? "Single Quote" : "Double Quote"
1877
+ }
1878
+ ),
1879
+ /* @__PURE__ */ jsxs2("p", { className: "text-sm leading-relaxed text-foreground", children: [
1880
+ data.position === "opening" ? "Opening" : "Closing",
1881
+ " quote"
1882
+ ] }),
1883
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 text-xs text-muted-foreground", children: [
1884
+ /* @__PURE__ */ jsx2("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono", children: data.originalChar }),
1885
+ /* @__PURE__ */ jsx2("span", { children: "\u2192" }),
1886
+ /* @__PURE__ */ jsx2("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono", children: data.replacementChar })
1887
+ ] })
1888
+ ] });
1889
+ }
1890
+ function HighlightPopover({
1891
+ state,
1892
+ annotationMap,
1893
+ onSuggestionClick,
1894
+ onLinkOpen,
1895
+ onDismiss,
1896
+ onPopoverEnter,
1897
+ renderPopoverContent,
1898
+ dir
1899
+ }) {
1900
+ const popoverRef = useRef3(null);
1901
+ const popperRef = useRef3(null);
1902
+ useLayoutEffect(() => {
1903
+ const el = popoverRef.current;
1904
+ if (!el) {
1905
+ popperRef.current?.destroy();
1906
+ popperRef.current = null;
1907
+ return;
1908
+ }
1909
+ const ar = state.anchorRect;
1910
+ const virtualEl = {
1911
+ getBoundingClientRect: () => ({
1912
+ top: ar?.top ?? state.y,
1913
+ left: ar?.left ?? state.x,
1914
+ bottom: ar?.bottom ?? state.y,
1915
+ right: ar?.right ?? state.x,
1916
+ width: ar?.width ?? 0,
1917
+ height: ar?.height ?? 0,
1918
+ x: ar?.left ?? state.x,
1919
+ y: ar?.top ?? state.y,
1920
+ toJSON: () => {
1921
+ }
1922
+ })
1923
+ };
1924
+ el.style.visibility = "hidden";
1925
+ if (popperRef.current) {
1926
+ popperRef.current.state.elements.reference = virtualEl;
1927
+ } else {
1928
+ popperRef.current = createPopper(virtualEl, el, {
1929
+ strategy: "fixed",
1930
+ placement: "bottom-start",
1931
+ modifiers: [
1932
+ { name: "offset", options: { offset: [0, 6] } },
1933
+ { name: "preventOverflow", options: { padding: 16 } },
1934
+ { name: "flip", options: { padding: 16 } }
1935
+ ]
1936
+ });
1937
+ }
1938
+ popperRef.current.forceUpdate();
1939
+ el.style.visibility = "";
1940
+ }, [state.visible, state.x, state.y, state.anchorRect]);
1941
+ useLayoutEffect(() => {
1942
+ return () => {
1943
+ popperRef.current?.destroy();
1944
+ popperRef.current = null;
1945
+ };
1946
+ }, []);
1947
+ if (!state.visible) return null;
1948
+ const annotations = state.ruleIds.map((id) => annotationMap.get(id)).filter((a) => a != null);
1949
+ if (annotations.length === 0) return null;
1950
+ return /* @__PURE__ */ jsx2(
1951
+ "div",
1952
+ {
1953
+ ref: popoverRef,
1954
+ className: "cat-popover",
1955
+ dir,
1956
+ style: {
1957
+ position: "fixed",
1958
+ left: 0,
1959
+ top: 0,
1960
+ zIndex: 1e3,
1961
+ visibility: "hidden"
1962
+ },
1963
+ onMouseEnter: () => onPopoverEnter(),
1964
+ onMouseLeave: () => onDismiss(),
1965
+ children: annotations.map((ann, i) => {
1966
+ const custom = renderPopoverContent?.({
1967
+ annotation: ann,
1968
+ onSuggestionClick: (s) => onSuggestionClick(s, ann.id)
1969
+ });
1970
+ return /* @__PURE__ */ jsxs2(React.Fragment, { children: [
1971
+ i > 0 && /* @__PURE__ */ jsx2("hr", { className: "border-border my-0" }),
1972
+ custom != null ? custom : ann.type === "spellcheck" ? /* @__PURE__ */ jsx2(
1973
+ SpellCheckPopoverContent,
1974
+ {
1975
+ data: ann.data,
1976
+ onSuggestionClick: (s) => onSuggestionClick(s, ann.id)
1977
+ }
1978
+ ) : ann.type === "glossary" ? /* @__PURE__ */ jsx2(KeywordsPopoverContent, { data: ann.data }) : ann.type === "tag" ? /* @__PURE__ */ jsx2(TagPopoverContent, { data: ann.data }) : ann.type === "quote" ? /* @__PURE__ */ jsx2(QuotePopoverContent, { data: ann.data }) : ann.type === "link" ? /* @__PURE__ */ jsx2(
1979
+ LinkPopoverContent,
1980
+ {
1981
+ data: ann.data,
1982
+ onOpen: () => {
1983
+ if (onLinkOpen) {
1984
+ onLinkOpen(ann.data.url);
1985
+ } else {
1986
+ window.open(ann.data.url, "_blank", "noopener,noreferrer");
1987
+ }
1988
+ }
1989
+ }
1990
+ ) : /* @__PURE__ */ jsx2(SpecialCharPopoverContent, { data: ann.data })
1991
+ ] }, ann.id);
1992
+ })
1993
+ }
1994
+ );
1995
+ }
1996
+
1997
+ // src/layout/cat-editor/CATEditor.tsx
1998
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1999
+ var CATEditor = forwardRef(
2000
+ function CATEditor2({
2001
+ initialText = "",
2002
+ rules = [],
2003
+ onChange,
2004
+ onSuggestionApply,
2005
+ codepointDisplayMap,
2006
+ renderPopoverContent,
2007
+ onLinkClick,
2008
+ openLinksOnClick = true,
2009
+ onMentionClick,
2010
+ onMentionInsert,
2011
+ mentionSerialize,
2012
+ mentionPattern,
2013
+ renderMentionDOM,
2014
+ placeholder = "Start typing or paste text here\u2026",
2015
+ className,
2016
+ dir,
2017
+ popoverDir: popoverDirProp = "ltr",
2018
+ jpFont = false,
2019
+ editable: editableProp,
2020
+ readOnlySelectable = false,
2021
+ onKeyDown: onKeyDownProp,
2022
+ readOnly: readOnlyLegacy = false
2023
+ }, ref) {
2024
+ const isEditable = editableProp !== void 0 ? editableProp : !readOnlyLegacy;
2025
+ const annotationMapRef = useRef4(/* @__PURE__ */ new Map());
2026
+ const editorRef = useRef4(null);
2027
+ const savedSelectionRef = useRef4(null);
2028
+ const containerRef = useRef4(null);
2029
+ const dismissTimerRef = useRef4(null);
2030
+ useEffect3(() => {
2031
+ setMentionNodeConfig({
2032
+ renderDOM: renderMentionDOM,
2033
+ serialize: mentionSerialize,
2034
+ pattern: mentionPattern
2035
+ });
2036
+ }, [renderMentionDOM, mentionSerialize, mentionPattern]);
2037
+ const flashIdRef = useRef4(null);
2038
+ const flashTimerRef = useRef4(null);
2039
+ const flashEditUnregRef = useRef4(null);
2040
+ const applyFlashClass = useCallback3((annotationId) => {
2041
+ const container = containerRef.current;
2042
+ if (!container) return;
2043
+ container.querySelectorAll(".cat-highlight-flash").forEach((el) => el.classList.remove("cat-highlight-flash"));
2044
+ container.querySelectorAll(".cat-highlight").forEach((el) => {
2045
+ const ids = el.getAttribute("data-rule-ids");
2046
+ if (ids && ids.split(",").includes(annotationId)) {
2047
+ el.classList.add("cat-highlight-flash");
2048
+ }
2049
+ });
2050
+ }, []);
2051
+ const clearFlashInner = useCallback3(() => {
2052
+ flashIdRef.current = null;
2053
+ if (flashTimerRef.current) {
2054
+ clearTimeout(flashTimerRef.current);
2055
+ flashTimerRef.current = null;
2056
+ }
2057
+ if (flashEditUnregRef.current) {
2058
+ flashEditUnregRef.current();
2059
+ flashEditUnregRef.current = null;
2060
+ }
2061
+ containerRef.current?.querySelectorAll(".cat-highlight-flash").forEach((el) => el.classList.remove("cat-highlight-flash"));
2062
+ }, []);
2063
+ const [popoverState, setPopoverState] = useState2({
2064
+ visible: false,
2065
+ x: 0,
2066
+ y: 0,
2067
+ ruleIds: []
2068
+ });
2069
+ useImperativeHandle(
2070
+ ref,
2071
+ () => ({
2072
+ insertText: (text) => {
2073
+ const editor = editorRef.current;
2074
+ if (!editor) return;
2075
+ editor.update(() => {
2076
+ const saved = savedSelectionRef.current;
2077
+ if (saved) {
2078
+ const anchorPt = $globalOffsetToPoint(saved.anchor);
2079
+ const focusPt = $globalOffsetToPoint(saved.focus);
2080
+ if (anchorPt && focusPt) {
2081
+ const anchorNode = $getNodeByKey(anchorPt.key);
2082
+ if (anchorNode && $isHighlightNode(anchorNode) && anchorNode.getMode() === "token" && saved.anchor === saved.focus) {
2083
+ const newText = $createTextNode3(text);
2084
+ if (anchorPt.offset === 0 || anchorPt.offset < anchorNode.getTextContentSize()) {
2085
+ anchorNode.insertBefore(newText);
2086
+ } else {
2087
+ anchorNode.insertAfter(newText);
2088
+ }
2089
+ newText.selectEnd();
2090
+ return;
2091
+ }
2092
+ const sel2 = $createRangeSelection2();
2093
+ sel2.anchor.set(anchorPt.key, anchorPt.offset, anchorPt.type);
2094
+ sel2.focus.set(focusPt.key, focusPt.offset, focusPt.type);
2095
+ $setSelection2(sel2);
2096
+ sel2.insertText(text);
2097
+ return;
2098
+ }
2099
+ }
2100
+ const root = $getRoot3();
2101
+ const lastChild = root.getLastChild();
2102
+ if (lastChild) {
2103
+ lastChild.selectEnd();
2104
+ }
2105
+ const sel = $createRangeSelection2();
2106
+ $setSelection2(sel);
2107
+ sel.insertText(text);
2108
+ });
2109
+ editor.focus();
2110
+ },
2111
+ focus: () => {
2112
+ editorRef.current?.focus();
2113
+ },
2114
+ getText: () => {
2115
+ let text = "";
2116
+ editorRef.current?.getEditorState().read(() => {
2117
+ text = $getRoot3().getTextContent();
2118
+ });
2119
+ return text;
2120
+ },
2121
+ flashHighlight: (annotationId, durationMs = 5e3) => {
2122
+ clearFlashInner();
2123
+ flashIdRef.current = annotationId;
2124
+ applyFlashClass(annotationId);
2125
+ flashTimerRef.current = setTimeout(() => {
2126
+ clearFlashInner();
2127
+ }, durationMs);
2128
+ const editor = editorRef.current;
2129
+ if (editor) {
2130
+ flashEditUnregRef.current = editor.registerUpdateListener(
2131
+ ({ tags }) => {
2132
+ if (tags.has("cat-highlights")) {
2133
+ if (flashIdRef.current) {
2134
+ requestAnimationFrame(
2135
+ () => applyFlashClass(flashIdRef.current)
2136
+ );
2137
+ }
2138
+ return;
2139
+ }
2140
+ clearFlashInner();
2141
+ }
2142
+ );
2143
+ }
2144
+ },
2145
+ replaceAll: (search, replacement) => {
2146
+ const editor = editorRef.current;
2147
+ if (!editor || !search) return 0;
2148
+ let count = 0;
2149
+ editor.update(() => {
2150
+ const root = $getRoot3();
2151
+ const fullText = root.getTextContent();
2152
+ let idx = 0;
2153
+ while ((idx = fullText.indexOf(search, idx)) !== -1) {
2154
+ count++;
2155
+ idx += search.length;
2156
+ }
2157
+ if (count === 0) return;
2158
+ const newText = fullText.split(search).join(replacement);
2159
+ root.clear();
2160
+ const lines = newText.split("\n");
2161
+ for (const line of lines) {
2162
+ const p = $createParagraphNode2();
2163
+ p.append($createTextNode3(line));
2164
+ root.append(p);
2165
+ }
2166
+ });
2167
+ return count;
2168
+ },
2169
+ clearFlash: () => {
2170
+ clearFlashInner();
2171
+ }
2172
+ }),
2173
+ [applyFlashClass, clearFlashInner]
2174
+ );
2175
+ const initialConfig = useMemo2(
2176
+ () => ({
2177
+ namespace: "CATEditor",
2178
+ theme: {
2179
+ root: "cat-editor-root",
2180
+ paragraph: "cat-editor-paragraph",
2181
+ text: {
2182
+ base: "cat-editor-text"
2183
+ }
2184
+ },
2185
+ nodes: [HighlightNode, MentionNode],
2186
+ // When readOnlySelectable, Lexical must be editable so the caret
2187
+ // and selection work — we block mutations via KEY_DOWN_COMMAND.
2188
+ editable: isEditable || readOnlySelectable,
2189
+ onError: (error) => {
2190
+ console.error("CATEditor Lexical error:", error);
2191
+ },
2192
+ editorState: () => {
2193
+ const root = $getRoot3();
2194
+ const lines = initialText.split("\n");
2195
+ for (const line of lines) {
2196
+ const p = $createParagraphNode2();
2197
+ p.append($createTextNode3(line));
2198
+ root.append(p);
2199
+ }
2200
+ }
2201
+ }),
2202
+ []
2203
+ // intentional: initialConfig should not change
2204
+ );
2205
+ const isOverHighlightRef = useRef4(false);
2206
+ const isOverPopoverRef = useRef4(false);
2207
+ const scheduleHide = useCallback3(() => {
2208
+ if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
2209
+ dismissTimerRef.current = setTimeout(() => {
2210
+ if (!isOverHighlightRef.current && !isOverPopoverRef.current) {
2211
+ setPopoverState((prev) => ({ ...prev, visible: false }));
2212
+ }
2213
+ }, 400);
2214
+ }, []);
2215
+ const cancelHide = useCallback3(() => {
2216
+ if (dismissTimerRef.current) {
2217
+ clearTimeout(dismissTimerRef.current);
2218
+ dismissTimerRef.current = null;
2219
+ }
2220
+ }, []);
2221
+ useEffect3(() => {
2222
+ const container = containerRef.current;
2223
+ if (!container) return;
2224
+ const handleMouseOver = (e) => {
2225
+ const target = e.target.closest(".cat-highlight");
2226
+ if (!target) {
2227
+ if (isOverHighlightRef.current) {
2228
+ isOverHighlightRef.current = false;
2229
+ scheduleHide();
2230
+ }
2231
+ return;
2232
+ }
2233
+ const ruleIdsAttr = target.getAttribute("data-rule-ids");
2234
+ if (!ruleIdsAttr) return;
2235
+ const ruleIds = [
2236
+ ...new Set(
2237
+ ruleIdsAttr.split(",").map(
2238
+ (id) => id.startsWith(NL_MARKER_PREFIX) ? id.slice(NL_MARKER_PREFIX.length) : id
2239
+ )
2240
+ )
2241
+ ];
2242
+ isOverHighlightRef.current = true;
2243
+ cancelHide();
2244
+ const rect = target.getBoundingClientRect();
2245
+ setPopoverState({
2246
+ visible: true,
2247
+ x: rect.left,
2248
+ y: rect.bottom,
2249
+ anchorRect: {
2250
+ top: rect.top,
2251
+ left: rect.left,
2252
+ bottom: rect.bottom,
2253
+ right: rect.right,
2254
+ width: rect.width,
2255
+ height: rect.height
2256
+ },
2257
+ ruleIds
2258
+ });
2259
+ };
2260
+ const handleMouseOut = (e) => {
2261
+ const related = e.relatedTarget;
2262
+ if (related?.closest(".cat-highlight")) return;
2263
+ isOverHighlightRef.current = false;
2264
+ scheduleHide();
2265
+ };
2266
+ container.addEventListener("mouseover", handleMouseOver);
2267
+ container.addEventListener("mouseout", handleMouseOut);
2268
+ return () => {
2269
+ container.removeEventListener("mouseover", handleMouseOver);
2270
+ container.removeEventListener("mouseout", handleMouseOut);
2271
+ if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current);
2272
+ };
2273
+ }, [scheduleHide, cancelHide]);
2274
+ useEffect3(() => {
2275
+ const container = containerRef.current;
2276
+ if (!container) return;
2277
+ const handleClick = (e) => {
2278
+ if (openLinksOnClick) {
2279
+ const highlightTarget = e.target.closest(
2280
+ ".cat-highlight"
2281
+ );
2282
+ if (highlightTarget) {
2283
+ const ruleIdsAttr = highlightTarget.getAttribute("data-rule-ids");
2284
+ if (ruleIdsAttr) {
2285
+ const ids = ruleIdsAttr.split(",");
2286
+ for (const id of ids) {
2287
+ const ann = annotationMapRef.current.get(id);
2288
+ if (!ann) continue;
2289
+ if (ann.type === "link") {
2290
+ e.preventDefault();
2291
+ if (onLinkClick) {
2292
+ onLinkClick(ann.data.url);
2293
+ } else {
2294
+ window.open(ann.data.url, "_blank", "noopener,noreferrer");
2295
+ }
2296
+ return;
2297
+ }
2298
+ }
2299
+ }
2300
+ }
2301
+ }
2302
+ const mentionTarget = e.target.closest(
2303
+ ".cat-mention-node"
2304
+ );
2305
+ if (mentionTarget) {
2306
+ const mentionId = mentionTarget.getAttribute("data-mention-id");
2307
+ const mentionName = mentionTarget.getAttribute("data-mention-name");
2308
+ if (mentionId && mentionName) {
2309
+ e.preventDefault();
2310
+ onMentionClick?.(mentionId, mentionName);
2311
+ return;
2312
+ }
2313
+ }
2314
+ };
2315
+ container.addEventListener("click", handleClick);
2316
+ return () => {
2317
+ container.removeEventListener("click", handleClick);
2318
+ };
2319
+ }, [onLinkClick, openLinksOnClick, onMentionClick]);
2320
+ const handleSuggestionClick = useCallback3(
2321
+ (suggestion, ruleId) => {
2322
+ const editor = editorRef.current;
2323
+ if (!editor) return;
2324
+ let replacedRange;
2325
+ editor.update(() => {
2326
+ const root = $getRoot3();
2327
+ const allNodes = root.getAllTextNodes();
2328
+ for (const node of allNodes) {
2329
+ if ($isHighlightNode(node) && node.__ruleIds.split(",").includes(ruleId)) {
2330
+ const globalOffset = $pointToGlobalOffset(node.getKey(), 0);
2331
+ const originalContent = node.getTextContent();
2332
+ replacedRange = {
2333
+ start: globalOffset,
2334
+ end: globalOffset + originalContent.length,
2335
+ content: originalContent
2336
+ };
2337
+ const textNode = $createTextNode3(suggestion);
2338
+ node.replace(textNode);
2339
+ break;
2340
+ }
2341
+ }
2342
+ });
2343
+ setPopoverState((prev) => ({ ...prev, visible: false }));
2344
+ if (replacedRange !== void 0) {
2345
+ const annotation = annotationMapRef.current.get(ruleId);
2346
+ if (annotation) {
2347
+ onSuggestionApply?.(
2348
+ ruleId,
2349
+ suggestion,
2350
+ replacedRange,
2351
+ annotation.type
2352
+ );
2353
+ }
2354
+ }
2355
+ },
2356
+ [onSuggestionApply]
2357
+ );
2358
+ const handleChange = useCallback3(
2359
+ (editorState) => {
2360
+ if (!onChange) return;
2361
+ editorState.read(() => {
2362
+ const root = $getRoot3();
2363
+ onChange(root.getTextContent());
2364
+ });
2365
+ },
2366
+ [onChange]
2367
+ );
2368
+ return /* @__PURE__ */ jsxs3(
2369
+ "div",
2370
+ {
2371
+ ref: containerRef,
2372
+ className: cn(
2373
+ "cat-editor-container",
2374
+ jpFont && "cat-editor-jp-font",
2375
+ className
2376
+ ),
2377
+ dir,
2378
+ children: [
2379
+ /* @__PURE__ */ jsx3(LexicalComposer, { initialConfig, children: /* @__PURE__ */ jsxs3("div", { className: "cat-editor-inner", children: [
2380
+ /* @__PURE__ */ jsx3(
2381
+ PlainTextPlugin,
2382
+ {
2383
+ contentEditable: /* @__PURE__ */ jsx3(
2384
+ ContentEditable,
2385
+ {
2386
+ className: cn(
2387
+ "cat-editor-editable",
2388
+ !isEditable && !readOnlySelectable && "cat-editor-readonly",
2389
+ !isEditable && readOnlySelectable && "cat-editor-readonly-selectable"
2390
+ )
2391
+ }
2392
+ ),
2393
+ placeholder: /* @__PURE__ */ jsx3("div", { className: "cat-editor-placeholder", children: placeholder }),
2394
+ ErrorBoundary: LexicalErrorBoundary
2395
+ }
2396
+ ),
2397
+ /* @__PURE__ */ jsx3(HistoryPlugin, {}),
2398
+ /* @__PURE__ */ jsx3(OnChangePlugin, { onChange: handleChange }),
2399
+ /* @__PURE__ */ jsx3(
2400
+ HighlightsPlugin,
2401
+ {
2402
+ rules,
2403
+ annotationMapRef,
2404
+ codepointDisplayMap
2405
+ }
2406
+ ),
2407
+ /* @__PURE__ */ jsx3(
2408
+ EditorRefPlugin,
2409
+ {
2410
+ editorRef,
2411
+ savedSelectionRef
2412
+ }
2413
+ ),
2414
+ /* @__PURE__ */ jsx3(NLMarkerNavigationPlugin, {}),
2415
+ !isEditable && readOnlySelectable && /* @__PURE__ */ jsx3(ReadOnlySelectablePlugin, {}),
2416
+ onKeyDownProp && /* @__PURE__ */ jsx3(KeyDownPlugin, { onKeyDown: onKeyDownProp }),
2417
+ dir && dir !== "auto" && /* @__PURE__ */ jsx3(DirectionPlugin, { dir }),
2418
+ rules.filter((r) => r.type === "mention").map((mentionRule, i) => /* @__PURE__ */ jsx3(
2419
+ MentionPlugin,
2420
+ {
2421
+ users: mentionRule.users,
2422
+ trigger: mentionRule.trigger,
2423
+ onMentionInsert
2424
+ },
2425
+ `mention-${i}`
2426
+ ))
2427
+ ] }) }),
2428
+ /* @__PURE__ */ jsx3(
2429
+ HighlightPopover,
2430
+ {
2431
+ state: popoverState,
2432
+ annotationMap: annotationMapRef.current,
2433
+ onSuggestionClick: handleSuggestionClick,
2434
+ onLinkOpen: onLinkClick,
2435
+ onDismiss: () => {
2436
+ isOverPopoverRef.current = false;
2437
+ scheduleHide();
2438
+ },
2439
+ onPopoverEnter: () => {
2440
+ isOverPopoverRef.current = true;
2441
+ cancelHide();
2442
+ },
2443
+ renderPopoverContent,
2444
+ dir: popoverDirProp === "inherit" ? dir : popoverDirProp
2445
+ }
2446
+ )
2447
+ ]
2448
+ }
2449
+ );
2450
+ }
2451
+ );
2452
+ var CATEditor_default = CATEditor;
2453
+ function ReadOnlySelectablePlugin() {
2454
+ const [editor] = useLexicalComposerContext3();
2455
+ useEffect3(() => {
2456
+ return editor.registerCommand(
2457
+ KEY_DOWN_COMMAND,
2458
+ (event) => {
2459
+ if (event.key.startsWith("Arrow") || event.key === "Home" || event.key === "End" || event.key === "PageUp" || event.key === "PageDown" || event.key === "Shift" || event.key === "Control" || event.key === "Alt" || event.key === "Meta" || event.key === "Tab" || event.key === "Escape" || event.key === "F5" || event.key === "F12" || // Ctrl/Cmd shortcuts that don't mutate: copy, select-all, find
2460
+ (event.ctrlKey || event.metaKey) && (event.key === "c" || event.key === "a" || event.key === "f" || event.key === "g")) {
2461
+ return false;
2462
+ }
2463
+ event.preventDefault();
2464
+ return true;
2465
+ },
2466
+ COMMAND_PRIORITY_CRITICAL
2467
+ );
2468
+ }, [editor]);
2469
+ useEffect3(() => {
2470
+ const root = editor.getRootElement();
2471
+ if (!root) return;
2472
+ const block = (e) => e.preventDefault();
2473
+ root.addEventListener("paste", block);
2474
+ root.addEventListener("cut", block);
2475
+ root.addEventListener("drop", block);
2476
+ return () => {
2477
+ root.removeEventListener("paste", block);
2478
+ root.removeEventListener("cut", block);
2479
+ root.removeEventListener("drop", block);
2480
+ };
2481
+ }, [editor]);
2482
+ return null;
2483
+ }
2484
+ function KeyDownPlugin({
2485
+ onKeyDown
2486
+ }) {
2487
+ const [editor] = useLexicalComposerContext3();
2488
+ useEffect3(() => {
2489
+ return editor.registerCommand(
2490
+ KEY_DOWN_COMMAND,
2491
+ (event) => {
2492
+ return onKeyDown(event);
2493
+ },
2494
+ COMMAND_PRIORITY_CRITICAL
2495
+ );
2496
+ }, [editor, onKeyDown]);
2497
+ return null;
2498
+ }
2499
+ function DirectionPlugin({ dir }) {
2500
+ const [editor] = useLexicalComposerContext3();
2501
+ useEffect3(() => {
2502
+ editor.update(() => {
2503
+ $getRoot3().setDirection(dir);
2504
+ });
2505
+ return () => {
2506
+ editor.update(() => {
2507
+ $getRoot3().setDirection(null);
2508
+ });
2509
+ };
2510
+ }, [editor, dir]);
2511
+ return null;
2512
+ }
2513
+
2514
+ export {
2515
+ CODEPOINT_DISPLAY_MAP,
2516
+ getEffectiveCodepointMap,
2517
+ setMentionNodeConfig,
2518
+ getMentionModelText,
2519
+ getMentionPattern,
2520
+ MentionNode,
2521
+ $createMentionNode,
2522
+ $isMentionNode,
2523
+ MentionPlugin,
2524
+ CATEditor,
2525
+ CATEditor_default
2526
+ };
2527
+ //# sourceMappingURL=chunk-ZME2TTK5.js.map