@edux-design/comments 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -17,10 +17,14 @@ var VARIANT_STYLES = {
17
17
  };
18
18
  var CommentContext = createContext({
19
19
  editable: true,
20
- onResolve: void 0
20
+ onResolve: void 0,
21
+ resolved: false
21
22
  });
22
23
  var useCommentContext = () => useContext(CommentContext);
23
- var Comment = forwardRef(function Comment2({
24
+ var flattenReplies = (children) => React.Children.toArray(children).flatMap(
25
+ (child) => isValidElement(child) && child.type === React.Fragment ? flattenReplies(child.props.children) : [child]
26
+ );
27
+ function CommentComponent({
24
28
  author = "Author",
25
29
  timestamp,
26
30
  avatar,
@@ -38,10 +42,7 @@ var Comment = forwardRef(function Comment2({
38
42
  }, ref) {
39
43
  const childArray = React.Children.toArray(children);
40
44
  const actionsSlot = childArray.find(
41
- (child) => {
42
- var _a;
43
- return isValidElement(child) && (child.type === CommentActions || ((_a = child.type) == null ? void 0 : _a.displayName) === "Comment.Actions");
44
- }
45
+ (child) => isValidElement(child) && (child.type === CommentActions || child.type?.displayName === "Comment.Actions")
45
46
  );
46
47
  const bodyNodes = childArray.filter((child) => child !== actionsSlot);
47
48
  const bodyContent = body ?? bodyNodes;
@@ -49,9 +50,11 @@ var Comment = forwardRef(function Comment2({
49
50
  const isResolved = resolved || variant === "resolved";
50
51
  const resolvedVariant = VARIANT_STYLES[isResolved ? "resolved" : variant] ?? VARIANT_STYLES.default;
51
52
  const resolvedAvatarName = avatarName ?? (typeof author === "string" ? author : void 0);
52
- const shouldRenderAvatar = Boolean(avatar || avatarProps || resolvedAvatarName);
53
+ const shouldRenderAvatar = Boolean(
54
+ avatar || avatarProps || resolvedAvatarName
55
+ );
53
56
  const replyItems = useMemo(
54
- () => React.Children.toArray(replies).filter(
57
+ () => flattenReplies(replies).filter(
55
58
  (reply) => reply !== null && reply !== void 0 && reply !== false
56
59
  ),
57
60
  [replies]
@@ -63,49 +66,55 @@ var Comment = forwardRef(function Comment2({
63
66
  setShowAllReplies(!shouldCollapseReplies);
64
67
  }, [shouldCollapseReplies]);
65
68
  const visibleReplies = shouldCollapseReplies && !showAllReplies ? [replyItems[replyCount - 1]] : replyItems;
66
- return /* @__PURE__ */ jsx(CommentContext.Provider, { value: { editable, onResolve }, children: /* @__PURE__ */ jsxs(
67
- "div",
69
+ return /* @__PURE__ */ jsx(
70
+ CommentContext.Provider,
68
71
  {
69
- ref,
70
- className: cx(
71
- "flex gap-12 rounded-default border p-12",
72
- resolvedVariant,
73
- className
74
- ),
75
- "data-variant": variant,
76
- ...props,
77
- children: [
78
- shouldRenderAvatar && /* @__PURE__ */ jsx("div", { className: "shrink-0", children: avatar ?? /* @__PURE__ */ jsx(Avatar, { name: resolvedAvatarName, size: "small", ...avatarProps }) }),
79
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
80
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-6", children: [
81
- /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-fg-base", children: author }),
82
- timestamp && /* @__PURE__ */ jsx("span", { className: "text-xs text-fg-subtle", children: timestamp })
83
- ] }),
84
- hasBodyContent && !isResolved ? /* @__PURE__ */ jsx("div", { className: "mt-6 text-sm leading-relaxed text-fg-base", children: bodyContent }) : null,
85
- isResolved ? /* @__PURE__ */ jsx("div", { className: "mt-6 text-xs font-semibold uppercase tracking-wide text-fg-success-base", children: "Resolved" }) : null,
86
- actionsSlot ? /* @__PURE__ */ jsx("div", { className: "mt-8", children: actionsSlot }) : null,
87
- replies && !isResolved ? /* @__PURE__ */ jsxs("div", { className: "mt-10 border-l border-border-minimal pl-12", children: [
88
- shouldCollapseReplies && !showAllReplies && /* @__PURE__ */ jsxs(
89
- "button",
90
- {
91
- type: "button",
92
- className: "mb-8 text-xs font-semibold text-fg-info-base",
93
- onClick: () => setShowAllReplies(true),
94
- "aria-expanded": showAllReplies,
95
- children: [
96
- "Show all ",
97
- replyCount,
98
- " replies"
99
- ]
100
- }
101
- ),
102
- visibleReplies
103
- ] }) : null
104
- ] })
105
- ]
72
+ value: { editable, onResolve, resolved: isResolved },
73
+ children: /* @__PURE__ */ jsxs(
74
+ "div",
75
+ {
76
+ ref,
77
+ className: cx(
78
+ "flex gap-12 rounded-default border p-12",
79
+ resolvedVariant,
80
+ className
81
+ ),
82
+ "data-variant": variant,
83
+ ...props,
84
+ children: [
85
+ shouldRenderAvatar && /* @__PURE__ */ jsx("div", { className: "shrink-0", children: avatar ?? /* @__PURE__ */ jsx(Avatar, { name: resolvedAvatarName, size: "small", ...avatarProps }) }),
86
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
87
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-6", children: [
88
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-fg-base", children: author }),
89
+ timestamp && /* @__PURE__ */ jsx("span", { className: "text-xs text-fg-subtle", children: timestamp })
90
+ ] }),
91
+ hasBodyContent && !isResolved ? /* @__PURE__ */ jsx("div", { className: "mt-6 text-sm leading-relaxed text-fg-base", children: bodyContent }) : null,
92
+ isResolved ? /* @__PURE__ */ jsx("div", { className: "mt-6 text-xs font-semibold uppercase tracking-wide text-fg-success-base", children: "Resolved" }) : null,
93
+ actionsSlot ? /* @__PURE__ */ jsx("div", { className: "mt-8", children: actionsSlot }) : null,
94
+ replies && !isResolved ? /* @__PURE__ */ jsxs("div", { className: "mt-[40px] border-l border-border-minimal pl-[20px]", children: [
95
+ shouldCollapseReplies && !showAllReplies && /* @__PURE__ */ jsxs(
96
+ "button",
97
+ {
98
+ type: "button",
99
+ className: "mb-8 text-xs font-semibold text-fg-info-base",
100
+ onClick: () => setShowAllReplies(true),
101
+ "aria-expanded": showAllReplies,
102
+ children: [
103
+ "Show all ",
104
+ replyCount,
105
+ " replies"
106
+ ]
107
+ }
108
+ ),
109
+ visibleReplies
110
+ ] }) : null
111
+ ] })
112
+ ]
113
+ }
114
+ )
106
115
  }
107
- ) });
108
- });
116
+ );
117
+ }
109
118
  var CommentActions = ({ children, className, ...props }) => /* @__PURE__ */ jsx(
110
119
  "div",
111
120
  {
@@ -139,31 +148,39 @@ var CommentResolve = ({
139
148
  className,
140
149
  disabled,
141
150
  onClick,
151
+ label,
142
152
  ...props
143
153
  }) => {
144
- const { editable, onResolve } = useCommentContext();
154
+ const { editable, onResolve, resolved } = useCommentContext();
145
155
  const isDisabled = disabled || !editable;
146
156
  const handleClick = onClick ?? onResolve;
157
+ const isResolved = Boolean(resolved);
158
+ const actionLabel = label ?? (isResolved ? "Re-open" : "Resolve");
159
+ const toneClass = isResolved ? "text-fg-info-base" : "text-fg-success-base";
147
160
  return /* @__PURE__ */ jsx(
148
161
  "button",
149
162
  {
150
163
  type: "button",
151
164
  className: cx(
152
- "text-xs font-semibold text-fg-success-base cursor-pointer",
165
+ "text-xs font-semibold cursor-pointer",
166
+ toneClass,
153
167
  className
154
168
  ),
155
169
  onClick: handleClick,
156
170
  disabled: isDisabled,
157
171
  "aria-disabled": isDisabled,
158
172
  ...props,
159
- children: "Resolve"
173
+ children: actionLabel
160
174
  }
161
175
  );
162
176
  };
163
- Comment.Actions = CommentActions;
164
- Comment.Reply = CommentReply;
165
- Comment.Edit = CommentEdit;
166
- Comment.Resolve = CommentResolve;
177
+ var Comment = Object.assign(forwardRef(CommentComponent), {
178
+ Actions: CommentActions,
179
+ Reply: CommentReply,
180
+ Edit: CommentEdit,
181
+ Resolve: CommentResolve
182
+ });
183
+ Comment.displayName = "Comment";
167
184
  CommentActions.displayName = "Comment.Actions";
168
185
  CommentReply.displayName = "Comment.Reply";
169
186
  CommentEdit.displayName = "Comment.Edit";
@@ -176,7 +193,9 @@ import React3, {
176
193
  useRef,
177
194
  createContext as createContext2,
178
195
  useContext as useContext2,
179
- useEffect as useEffect2
196
+ useEffect as useEffect2,
197
+ useImperativeHandle,
198
+ useState as useState2
180
199
  } from "react";
181
200
  import { cx as cx2 } from "@edux-design/utils";
182
201
  import { Avatar as Avatar2 } from "@edux-design/avatars";
@@ -192,9 +211,8 @@ import { ReactNodeViewRenderer, NodeViewWrapper } from "@tiptap/react";
192
211
  import { Mention } from "@edux-design/mentions";
193
212
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
194
213
  var MentionNodeView = ({ node }) => {
195
- var _a, _b;
196
- const handle = ((_a = node.attrs) == null ? void 0 : _a.handle) ?? "";
197
- const label = ((_b = node.attrs) == null ? void 0 : _b.label) ?? "";
214
+ const handle = node.attrs?.handle ?? "";
215
+ const label = node.attrs?.label ?? "";
198
216
  return /* @__PURE__ */ jsx2(
199
217
  NodeViewWrapper,
200
218
  {
@@ -237,6 +255,11 @@ var CommentMentionExtension = Node.create({
237
255
  label
238
256
  ];
239
257
  },
258
+ renderText({ node }) {
259
+ const handle = node.attrs.handle || "";
260
+ const label = node.attrs.label || "";
261
+ return `@${handle || label || "mention"}`;
262
+ },
240
263
  addNodeView() {
241
264
  return ReactNodeViewRenderer(MentionNodeView);
242
265
  }
@@ -254,87 +277,237 @@ var useCommentComposer = () => {
254
277
  }
255
278
  return context;
256
279
  };
257
- var CommentComposer = forwardRef2(function CommentComposer2({
280
+ function readValue(editor) {
281
+ const mentions = [];
282
+ editor.state.doc.descendants((node) => {
283
+ if (node.type.name === "commentMention") {
284
+ mentions.push({
285
+ id: node.attrs.id ?? null,
286
+ handle: node.attrs.handle ?? "",
287
+ label: node.attrs.label ?? ""
288
+ });
289
+ }
290
+ });
291
+ const legacyText = editor.getText();
292
+ const text = editor.getText({
293
+ blockSeparator: "\n",
294
+ textSerializers: {
295
+ commentMention: ({ node }) => {
296
+ const { handle = "", label = "" } = node.attrs;
297
+ return label ? `@${handle} \xB7 ${label}` : `@${handle}`;
298
+ }
299
+ }
300
+ }).replace(/\u00a0/g, " ").replace(/[\u200b-\u200d\ufeff]/g, "").replace(/\r?\n/g, " ");
301
+ return {
302
+ text,
303
+ legacyText,
304
+ html: editor.getHTML(),
305
+ json: editor.getJSON(),
306
+ mentions,
307
+ isEmpty: !text.trim()
308
+ };
309
+ }
310
+ function stringContent(value, format) {
311
+ if (format !== "text") return value;
312
+ return {
313
+ type: "doc",
314
+ content: value.split(/\r?\n/).map((line) => ({
315
+ type: "paragraph",
316
+ ...line ? { content: [{ type: "text", text: line }] } : {}
317
+ }))
318
+ };
319
+ }
320
+ function CommentComposerComponent({
258
321
  value,
322
+ valueFormat = "html",
323
+ richValue,
259
324
  onChange,
325
+ onValueChange,
260
326
  onSubmit,
261
327
  onCancel,
328
+ composerRef,
329
+ editorLabel = "Comment",
330
+ editorAttributes: suppliedEditorAttributes,
331
+ autoFocus = false,
332
+ externalSubmit = false,
333
+ submitOnModEnter = false,
262
334
  placeholder = "Write a comment...",
263
335
  rows = 4,
264
336
  disabled = false,
265
337
  isSubmitting = false,
266
338
  submitLabel = "Post",
339
+ submittingLabel = "Posting...",
267
340
  cancelLabel = "Cancel",
268
341
  avatar,
269
342
  avatarProps,
270
343
  actions,
271
344
  className,
272
345
  children,
346
+ onKeyDown,
347
+ onKeyDownCapture,
273
348
  ...props
274
349
  }, ref) {
275
- const hasValue = value === void 0 ? true : Boolean(value && value.trim().length);
276
- const isSubmitDisabled = disabled || isSubmitting || !hasValue;
350
+ const containerRef = useRef(null);
351
+ const formRef = useRef(null);
352
+ const pendingFocusRef = useRef(null);
353
+ const handledShortcutEvents = useRef(/* @__PURE__ */ new WeakSet());
354
+ const callbacks = useRef({ onChange, onValueChange });
355
+ callbacks.current = { onChange, onValueChange };
356
+ const pendingEchoesRef = useRef([]);
357
+ const controlledRef = useRef(false);
358
+ controlledRef.current = value !== void 0 || richValue !== void 0;
359
+ const placeholderRef = useRef(placeholder);
360
+ placeholderRef.current = placeholder;
361
+ const [hasValue, setHasValue] = useState2(Boolean(richValue || value?.trim()));
362
+ const locked = disabled || isSubmitting;
363
+ const editorAttributes = useMemo2(
364
+ () => ({
365
+ ...suppliedEditorAttributes,
366
+ "data-comment-editor": "true",
367
+ role: "textbox",
368
+ "aria-label": editorLabel,
369
+ "aria-multiline": "true",
370
+ "aria-disabled": String(locked),
371
+ class: cx2(
372
+ "w-full bg-bg-base text-fg-base border-2 rounded-md border-border-base",
373
+ "outline-none transition-all duration-300 ease-in-out motion-reduce:transition-none",
374
+ "pl-[20px] py-12 font-normal text-base leading-relaxed overflow-y-auto",
375
+ "focus:shadow-focus focus-visible:shadow-focus",
376
+ locked && "bg-bg-subtle border-border-subtle text-fg-inactive"
377
+ ),
378
+ style: `height: ${Math.max(1, rows) * 30}px`
379
+ }),
380
+ [editorLabel, locked, rows, suppliedEditorAttributes]
381
+ );
382
+ const isSubmitDisabled = locked || !hasValue;
277
383
  const editor = useEditor({
384
+ immediatelyRender: false,
278
385
  extensions: [
279
386
  StarterKit,
280
387
  CommentMentionExtension,
281
- Placeholder.configure({
282
- placeholder
283
- })
388
+ Placeholder.configure({ placeholder: () => placeholderRef.current })
284
389
  ],
285
- content: value ?? "",
286
- editable: !disabled,
287
- editorProps: {
288
- attributes: {
289
- class: cx2(
290
- "w-full bg-white border-2 rounded-md border-border-base",
291
- "outline-none transition-all duration-300 ease-in-out",
292
- "pl-5 py-3 font-normal text-base leading-relaxed",
293
- "h-[120px] overflow-y-auto",
294
- "focus:shadow-focus focus-visible:shadow-focus",
295
- disabled && "bg-bg-subtle border-border-subtle text-fg-inactive"
296
- )
297
- }
298
- },
390
+ content: richValue ?? stringContent(value ?? "", valueFormat),
391
+ editable: !locked,
392
+ editorProps: { attributes: editorAttributes },
299
393
  onUpdate: ({ editor: editor2 }) => {
300
- onChange == null ? void 0 : onChange({ target: { value: editor2.getText() } });
394
+ const next = readValue(editor2);
395
+ if (controlledRef.current && (callbacks.current.onChange || callbacks.current.onValueChange)) {
396
+ pendingEchoesRef.current.push(next);
397
+ if (pendingEchoesRef.current.length > 100)
398
+ pendingEchoesRef.current.shift();
399
+ }
400
+ setHasValue(!next.isEmpty);
401
+ callbacks.current.onChange?.({ target: { value: next.legacyText } });
402
+ callbacks.current.onValueChange?.(next);
301
403
  }
302
404
  });
303
- const containerRef = useRef(null);
405
+ useImperativeHandle(ref, () => containerRef.current, []);
406
+ useImperativeHandle(
407
+ composerRef,
408
+ () => ({
409
+ focus: (position = "end") => {
410
+ if (locked) return false;
411
+ if (!editor) {
412
+ pendingFocusRef.current = position;
413
+ return true;
414
+ }
415
+ return editor.commands.focus(position);
416
+ },
417
+ clear: () => !locked && Boolean(editor?.commands.clearContent(true)),
418
+ getValue: () => editor ? readValue(editor) : null,
419
+ submit: () => {
420
+ if (!locked) formRef.current?.requestSubmit();
421
+ }
422
+ }),
423
+ [editor, locked]
424
+ );
304
425
  useEffect2(() => {
305
- if (typeof ref === "function") {
306
- ref(containerRef.current);
307
- } else if (ref) {
308
- ref.current = containerRef.current;
309
- }
310
- }, [ref]);
426
+ if (!editor) return;
427
+ editor.setEditable(!locked, false);
428
+ editor.setOptions({
429
+ editorProps: {
430
+ ...editor.options.editorProps,
431
+ attributes: editorAttributes
432
+ }
433
+ });
434
+ }, [editor, locked, editorAttributes, placeholder]);
311
435
  useEffect2(() => {
312
436
  if (!editor) return;
313
- if (value === "") {
314
- editor.commands.clearContent();
437
+ const next = richValue ?? value;
438
+ if (next === void 0) return;
439
+ const current = readValue(editor);
440
+ const echoIndex = pendingEchoesRef.current.findIndex(
441
+ (snapshot) => richValue !== void 0 ? JSON.stringify(snapshot.json) === JSON.stringify(richValue) : snapshot.text === value || snapshot.legacyText === value
442
+ );
443
+ if (echoIndex !== -1) {
444
+ pendingEchoesRef.current.splice(0, echoIndex + 1);
445
+ setHasValue(!current.isEmpty);
446
+ return;
315
447
  }
316
- }, [editor, value]);
448
+ pendingEchoesRef.current = [];
449
+ const equal = richValue !== void 0 ? JSON.stringify(current.json) === JSON.stringify(richValue) : current.text === value || current.legacyText === value;
450
+ if (!equal)
451
+ editor.commands.setContent(
452
+ richValue ?? stringContent(value, valueFormat),
453
+ { emitUpdate: false }
454
+ );
455
+ setHasValue(!readValue(editor).isEmpty);
456
+ }, [editor, value, richValue, valueFormat]);
457
+ useEffect2(() => {
458
+ if (editor) setHasValue(!readValue(editor).isEmpty);
459
+ }, [editor]);
460
+ useEffect2(() => {
461
+ if (!editor || locked) return;
462
+ if (!autoFocus && pendingFocusRef.current === null) return;
463
+ const position = pendingFocusRef.current ?? "end";
464
+ pendingFocusRef.current = null;
465
+ const frame = requestAnimationFrame(() => {
466
+ if (!editor.isDestroyed && editor.isEditable)
467
+ editor.commands.focus(position);
468
+ });
469
+ return () => cancelAnimationFrame(frame);
470
+ }, [editor, autoFocus, locked]);
317
471
  const contextValue = useMemo2(
318
472
  () => ({
319
473
  editor,
320
474
  placeholder,
475
+ editable: !locked,
321
476
  editorContainerRef: containerRef
322
477
  }),
323
- [editor, placeholder]
478
+ [editor, placeholder, locked]
324
479
  );
325
480
  return /* @__PURE__ */ jsx3(CommentComposerContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs3(
326
481
  "form",
327
482
  {
483
+ ...props,
484
+ ref: formRef,
485
+ "data-comment-surface": "composer",
486
+ "aria-busy": isSubmitting || void 0,
328
487
  className: cx2(
329
488
  "relative flex gap-12 rounded-default border border-border-minimal bg-bg-base p-12",
330
489
  className
331
490
  ),
491
+ onKeyDown: (event) => {
492
+ if (!handledShortcutEvents.current.has(event.nativeEvent))
493
+ onKeyDown?.(event);
494
+ },
495
+ onKeyDownCapture: (event) => {
496
+ onKeyDownCapture?.(event);
497
+ if (!event.defaultPrevented && submitOnModEnter && !event.nativeEvent.isComposing && (event.ctrlKey || event.metaKey) && event.key === "Enter") {
498
+ handledShortcutEvents.current.add(event.nativeEvent);
499
+ onKeyDown?.(event);
500
+ if (event.defaultPrevented) return;
501
+ event.preventDefault();
502
+ if (!locked && editor && !readValue(editor).isEmpty)
503
+ formRef.current?.requestSubmit();
504
+ }
505
+ },
332
506
  onSubmit: (event) => {
333
507
  event.preventDefault();
334
- if (isSubmitDisabled) return;
335
- onSubmit == null ? void 0 : onSubmit(event);
508
+ if (!locked && editor && !readValue(editor).isEmpty)
509
+ onSubmit?.(event);
336
510
  },
337
- ...props,
338
511
  children: [
339
512
  (avatar || avatarProps) && /* @__PURE__ */ jsx3("div", { className: "shrink-0", children: avatar ?? /* @__PURE__ */ jsx3(Avatar2, { size: "small", ...avatarProps }) }),
340
513
  /* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
@@ -349,11 +522,11 @@ var CommentComposer = forwardRef2(function CommentComposer2({
349
522
  size: "small",
350
523
  isSecondary: true,
351
524
  onClick: onCancel,
352
- disabled: disabled || isSubmitting,
525
+ disabled: locked,
353
526
  children: cancelLabel
354
527
  }
355
528
  ),
356
- /* @__PURE__ */ jsx3(Button, { type: "submit", size: "small", disabled: isSubmitDisabled, children: isSubmitting ? "Posting..." : submitLabel })
529
+ !externalSubmit && /* @__PURE__ */ jsx3(Button, { type: "submit", size: "small", disabled: isSubmitDisabled, children: isSubmitting ? submittingLabel : submitLabel })
357
530
  ] })
358
531
  ] })
359
532
  ] }),
@@ -361,27 +534,38 @@ var CommentComposer = forwardRef2(function CommentComposer2({
361
534
  ]
362
535
  }
363
536
  ) });
364
- });
537
+ }
538
+ var CommentComposer = forwardRef2(CommentComposerComponent);
365
539
 
366
540
  // src/elements/CommentMention.jsx
367
- import React4, { useMemo as useMemo3, useState as useState2, useCallback, useEffect as useEffect3, useRef as useRef2 } from "react";
541
+ import React4, {
542
+ useMemo as useMemo3,
543
+ useState as useState3,
544
+ useCallback,
545
+ useEffect as useEffect3,
546
+ useRef as useRef2
547
+ } from "react";
368
548
  import { Menu, MenuList, MenuItem } from "@edux-design/menus";
369
549
  import { Avatar as Avatar3 } from "@edux-design/avatars";
370
550
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
371
- var CommentMention = ({
372
- peopleList = []
373
- }) => {
374
- const { editor, editorContainerRef } = useCommentComposer();
375
- const [menuOpen, setMenuOpen] = useState2(false);
376
- const [query, setQuery] = useState2("");
377
- const [range, setRange] = useState2(null);
378
- const [anchorStyle, setAnchorStyle] = useState2({ left: 0, top: 0 });
551
+ var CommentMention = ({ peopleList = [], onQueryChange }) => {
552
+ const { editor, editorContainerRef, editable = true } = useCommentComposer();
553
+ const [menuOpen, setMenuOpen] = useState3(false);
554
+ const [query, setQuery] = useState3("");
555
+ const [anchorStyle, setAnchorStyle] = useState3({ left: 0, top: 0 });
379
556
  const menuListRef = useRef2(null);
380
557
  const rangeRef = useRef2(null);
381
- const peopleByHandle = useMemo3(
382
- () => new Map(peopleList.map((person) => [person.handle.toLowerCase(), person])),
383
- [peopleList]
384
- );
558
+ const lastQueryRef = useRef2("");
559
+ const dismissedRangeRef = useRef2(null);
560
+ const focusFrameRef = useRef2(null);
561
+ const focusTimeoutRef = useRef2(null);
562
+ const dismissMenu = useCallback(() => {
563
+ const activeRange = rangeRef.current;
564
+ dismissedRangeRef.current = activeRange ? `${activeRange.from}:${activeRange.to}` : null;
565
+ setMenuOpen(false);
566
+ if (editor && !editor.isDestroyed && editor.isEditable !== false)
567
+ editor.view.focus();
568
+ }, [editor]);
385
569
  const filteredPeople = useMemo3(() => {
386
570
  if (!query) return peopleList;
387
571
  const lowered = query.toLowerCase();
@@ -390,28 +574,32 @@ var CommentMention = ({
390
574
  );
391
575
  }, [peopleList, query]);
392
576
  const updateMenuState = useCallback(() => {
393
- var _a, _b;
394
- if (!editor) return;
395
- const { from } = editor.state.selection;
577
+ if (!editor || !editable || editor.isEditable === false) {
578
+ setMenuOpen(false);
579
+ rangeRef.current = null;
580
+ return;
581
+ }
396
582
  const { parent, parentOffset, pos } = editor.state.selection.$from;
397
583
  if (!parent.isTextblock) {
398
584
  setMenuOpen(false);
399
585
  setQuery("");
400
- setRange(null);
401
586
  return;
402
587
  }
403
588
  const textBefore = parent.textBetween(0, parentOffset, "\n", "\0");
404
589
  const match = textBefore.match(/@([\w-]*)$/);
405
590
  if (match) {
406
591
  const handleQuery = match[1] ?? "";
592
+ if (onQueryChange && handleQuery !== lastQueryRef.current) {
593
+ lastQueryRef.current = handleQuery;
594
+ onQueryChange(handleQuery);
595
+ }
407
596
  setQuery(handleQuery);
408
- setMenuOpen(true);
409
597
  const fromPos = pos - match[0].length;
410
598
  const nextRange = { from: fromPos, to: pos };
411
- setRange(nextRange);
599
+ setMenuOpen(dismissedRangeRef.current !== `${fromPos}:${pos}`);
412
600
  rangeRef.current = nextRange;
413
601
  const coords = editor.view.coordsAtPos(fromPos);
414
- const containerRect = (_b = (_a = editorContainerRef == null ? void 0 : editorContainerRef.current) == null ? void 0 : _a.getBoundingClientRect) == null ? void 0 : _b.call(_a);
602
+ const containerRect = editorContainerRef?.current?.getBoundingClientRect?.();
415
603
  if (containerRect) {
416
604
  setAnchorStyle({
417
605
  left: coords.left - containerRect.left + 12,
@@ -422,12 +610,16 @@ var CommentMention = ({
422
610
  }
423
611
  setMenuOpen(false);
424
612
  setQuery("");
425
- setRange(null);
426
613
  rangeRef.current = null;
427
- }, [editor]);
614
+ if (onQueryChange && lastQueryRef.current !== "") {
615
+ lastQueryRef.current = "";
616
+ onQueryChange("");
617
+ }
618
+ }, [editor, editorContainerRef, onQueryChange, editable]);
428
619
  const handleSelectPerson = useCallback(
429
620
  (person) => {
430
- if (!editor || !rangeRef.current) return;
621
+ if (!editor || !editable || editor.isEditable === false || !rangeRef.current)
622
+ return;
431
623
  const selectionRange = rangeRef.current;
432
624
  editor.commands.focus();
433
625
  editor.commands.setTextSelection(selectionRange);
@@ -447,15 +639,32 @@ var CommentMention = ({
447
639
  { updateSelection: false }
448
640
  );
449
641
  const nextPos = selectionRange.from + 2;
450
- requestAnimationFrame(() => {
642
+ editor.view.focus();
643
+ cancelAnimationFrame(focusFrameRef.current);
644
+ clearTimeout(focusTimeoutRef.current);
645
+ focusFrameRef.current = requestAnimationFrame(() => {
646
+ if (editor.isDestroyed || editor.isEditable === false) return;
451
647
  editor.commands.setTextSelection(nextPos);
452
648
  editor.view.focus();
453
- setTimeout(() => editor.view.focus(), 0);
649
+ focusTimeoutRef.current = setTimeout(() => {
650
+ if (!editor.isDestroyed && editor.isEditable !== false)
651
+ editor.view.focus();
652
+ }, 0);
454
653
  });
455
654
  setMenuOpen(false);
456
655
  setQuery("");
457
- setRange(null);
458
656
  rangeRef.current = null;
657
+ if (onQueryChange && lastQueryRef.current !== "") {
658
+ lastQueryRef.current = "";
659
+ onQueryChange("");
660
+ }
661
+ },
662
+ [editor, onQueryChange, editable]
663
+ );
664
+ useEffect3(
665
+ () => () => {
666
+ cancelAnimationFrame(focusFrameRef.current);
667
+ clearTimeout(focusTimeoutRef.current);
459
668
  },
460
669
  [editor]
461
670
  );
@@ -470,25 +679,30 @@ var CommentMention = ({
470
679
  };
471
680
  }, [editor, updateMenuState]);
472
681
  useEffect3(() => {
473
- var _a;
474
682
  if (!editor) return;
475
683
  const handleKeyDown = (event) => {
476
- var _a2;
477
684
  if (!menuOpen) return;
685
+ if (event.key === "Escape") {
686
+ event.preventDefault();
687
+ event.stopPropagation();
688
+ dismissMenu();
689
+ return;
690
+ }
478
691
  if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
479
692
  event.preventDefault();
693
+ event.stopPropagation();
480
694
  const list = menuListRef.current;
481
- const items = list == null ? void 0 : list.querySelectorAll("[data-menu-item]");
482
- if (!list || !(items == null ? void 0 : items.length)) return;
695
+ const items = list?.querySelectorAll("[data-menu-item]");
696
+ if (!list || !items?.length) return;
483
697
  list.focus();
484
698
  const direction = event.key === "ArrowDown" ? 1 : -1;
485
699
  const targetIndex = direction === 1 ? 0 : items.length - 1;
486
- (_a2 = items[targetIndex]) == null ? void 0 : _a2.focus();
700
+ items[targetIndex]?.focus();
487
701
  };
488
- const dom = (_a = editor.view) == null ? void 0 : _a.dom;
489
- dom == null ? void 0 : dom.addEventListener("keydown", handleKeyDown);
490
- return () => dom == null ? void 0 : dom.removeEventListener("keydown", handleKeyDown);
491
- }, [editor, menuOpen]);
702
+ const dom = editor.view?.dom;
703
+ dom?.addEventListener("keydown", handleKeyDown, true);
704
+ return () => dom?.removeEventListener("keydown", handleKeyDown, true);
705
+ }, [editor, menuOpen, dismissMenu]);
492
706
  const menuContent = useMemo3(
493
707
  () => /* @__PURE__ */ jsx4(
494
708
  Menu,
@@ -500,6 +714,7 @@ var CommentMention = ({
500
714
  {
501
715
  type: "button",
502
716
  "aria-hidden": true,
717
+ tabIndex: -1,
503
718
  className: "absolute h-1 w-1 opacity-0",
504
719
  style: {
505
720
  left: `${anchorStyle.left}px`,
@@ -519,32 +734,46 @@ var CommentMention = ({
519
734
  collisionPadding: 12,
520
735
  onCloseAutoFocus: (event) => event.preventDefault()
521
736
  },
522
- children: /* @__PURE__ */ jsx4(MenuList, { ref: menuListRef, className: "min-w-[220px]", children: filteredPeople.map((person) => /* @__PURE__ */ jsx4(
523
- MenuItem,
737
+ children: /* @__PURE__ */ jsx4(
738
+ MenuList,
524
739
  {
525
- onMouseDown: (event) => {
526
- event.preventDefault();
527
- event.stopPropagation();
528
- handleSelectPerson(person);
740
+ ref: menuListRef,
741
+ className: "min-w-[220px]",
742
+ onKeyDownCapture: (event) => {
743
+ if (event.key === "Escape") {
744
+ event.preventDefault();
745
+ event.stopPropagation();
746
+ dismissMenu();
747
+ }
529
748
  },
530
- onClick: () => handleSelectPerson(person),
531
- onPointerDown: (event) => event.preventDefault(),
532
- children: /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-8", children: [
533
- /* @__PURE__ */ jsx4(Avatar3, { size: "small", ...person.avatarProps }),
534
- /* @__PURE__ */ jsxs4("span", { className: "flex flex-col", children: [
535
- /* @__PURE__ */ jsx4("span", { className: "text-sm text-fg-base", children: person.name }),
536
- /* @__PURE__ */ jsxs4("span", { className: "text-xs text-fg-subtle", children: [
537
- "@",
538
- person.handle
749
+ children: filteredPeople.map((person) => /* @__PURE__ */ jsx4(
750
+ MenuItem,
751
+ {
752
+ onMouseDown: (event) => {
753
+ event.preventDefault();
754
+ event.stopPropagation();
755
+ handleSelectPerson(person);
756
+ },
757
+ onClick: () => handleSelectPerson(person),
758
+ onPointerDown: (event) => event.preventDefault(),
759
+ children: /* @__PURE__ */ jsxs4("span", { className: "flex items-center gap-8", children: [
760
+ /* @__PURE__ */ jsx4(Avatar3, { size: "small", ...person.avatarProps }),
761
+ /* @__PURE__ */ jsxs4("span", { className: "flex flex-col", children: [
762
+ /* @__PURE__ */ jsx4("span", { className: "text-sm text-fg-base", children: person.name }),
763
+ /* @__PURE__ */ jsxs4("span", { className: "text-xs text-fg-subtle", children: [
764
+ "@",
765
+ person.handle
766
+ ] })
767
+ ] })
539
768
  ] })
540
- ] })
541
- ] })
542
- },
543
- person.id
544
- )) })
769
+ },
770
+ person.id
771
+ ))
772
+ }
773
+ )
545
774
  }
546
775
  ),
547
- [filteredPeople, handleSelectPerson, menuOpen]
776
+ [filteredPeople, handleSelectPerson, menuOpen, anchorStyle, dismissMenu]
548
777
  );
549
778
  if (!editor) return null;
550
779
  return menuContent;