@edux-design/comments 0.1.1
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/CHANGELOG.md +17 -0
- package/README.md +61 -0
- package/dist/index.d.mts +52 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +579 -0
- package/dist/index.mjs +556 -0
- package/package.json +51 -0
- package/src/demos/Comment.stories.jsx +472 -0
- package/src/demos/CommentComposer.stories.jsx +62 -0
- package/src/elements/Comment.jsx +234 -0
- package/src/elements/Comment.test.jsx +137 -0
- package/src/elements/CommentComposer.jsx +184 -0
- package/src/elements/CommentComposer.test.jsx +125 -0
- package/src/elements/CommentMention.jsx +217 -0
- package/src/elements/CommentMention.test.jsx +138 -0
- package/src/elements/CommentMentionExtension.jsx +56 -0
- package/src/index.js +3 -0
- package/tsconfig.json +10 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
// src/elements/Comment.jsx
|
|
2
|
+
import React, {
|
|
3
|
+
forwardRef,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useState,
|
|
7
|
+
createContext,
|
|
8
|
+
useContext,
|
|
9
|
+
isValidElement
|
|
10
|
+
} from "react";
|
|
11
|
+
import { cx } from "@edux-design/utils";
|
|
12
|
+
import { Avatar } from "@edux-design/avatars";
|
|
13
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
var VARIANT_STYLES = {
|
|
15
|
+
default: "border-border-minimal bg-bg-base",
|
|
16
|
+
resolved: "border-border-success-base bg-bg-success-subtle"
|
|
17
|
+
};
|
|
18
|
+
var CommentContext = createContext({
|
|
19
|
+
editable: true,
|
|
20
|
+
onResolve: void 0
|
|
21
|
+
});
|
|
22
|
+
var useCommentContext = () => useContext(CommentContext);
|
|
23
|
+
var Comment = forwardRef(function Comment2({
|
|
24
|
+
author = "Author",
|
|
25
|
+
timestamp,
|
|
26
|
+
avatar,
|
|
27
|
+
avatarName,
|
|
28
|
+
avatarProps,
|
|
29
|
+
body,
|
|
30
|
+
replies,
|
|
31
|
+
variant = "default",
|
|
32
|
+
resolved = false,
|
|
33
|
+
onResolve,
|
|
34
|
+
editable = true,
|
|
35
|
+
children,
|
|
36
|
+
className,
|
|
37
|
+
...props
|
|
38
|
+
}, ref) {
|
|
39
|
+
const childArray = React.Children.toArray(children);
|
|
40
|
+
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
|
+
);
|
|
46
|
+
const bodyNodes = childArray.filter((child) => child !== actionsSlot);
|
|
47
|
+
const bodyContent = body ?? bodyNodes;
|
|
48
|
+
const hasBodyContent = body !== void 0 ? Boolean(body) : bodyNodes.length > 0;
|
|
49
|
+
const isResolved = resolved || variant === "resolved";
|
|
50
|
+
const resolvedVariant = VARIANT_STYLES[isResolved ? "resolved" : variant] ?? VARIANT_STYLES.default;
|
|
51
|
+
const resolvedAvatarName = avatarName ?? (typeof author === "string" ? author : void 0);
|
|
52
|
+
const shouldRenderAvatar = Boolean(avatar || avatarProps || resolvedAvatarName);
|
|
53
|
+
const replyItems = useMemo(
|
|
54
|
+
() => React.Children.toArray(replies).filter(
|
|
55
|
+
(reply) => reply !== null && reply !== void 0 && reply !== false
|
|
56
|
+
),
|
|
57
|
+
[replies]
|
|
58
|
+
);
|
|
59
|
+
const replyCount = replyItems.length;
|
|
60
|
+
const shouldCollapseReplies = replyCount > 2;
|
|
61
|
+
const [showAllReplies, setShowAllReplies] = useState(!shouldCollapseReplies);
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
setShowAllReplies(!shouldCollapseReplies);
|
|
64
|
+
}, [shouldCollapseReplies]);
|
|
65
|
+
const visibleReplies = shouldCollapseReplies && !showAllReplies ? [replyItems[replyCount - 1]] : replyItems;
|
|
66
|
+
return /* @__PURE__ */ jsx(CommentContext.Provider, { value: { editable, onResolve }, children: /* @__PURE__ */ jsxs(
|
|
67
|
+
"div",
|
|
68
|
+
{
|
|
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
|
+
]
|
|
106
|
+
}
|
|
107
|
+
) });
|
|
108
|
+
});
|
|
109
|
+
var CommentActions = ({ children, className, ...props }) => /* @__PURE__ */ jsx(
|
|
110
|
+
"div",
|
|
111
|
+
{
|
|
112
|
+
className: cx(
|
|
113
|
+
"flex flex-wrap items-center gap-8 text-xs text-fg-subtle",
|
|
114
|
+
className
|
|
115
|
+
),
|
|
116
|
+
...props,
|
|
117
|
+
children
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
var CommentReply = ({ className, ...props }) => /* @__PURE__ */ jsx(
|
|
121
|
+
"button",
|
|
122
|
+
{
|
|
123
|
+
type: "button",
|
|
124
|
+
className: cx("text-xs text-fg-info-base cursor-pointer", className),
|
|
125
|
+
...props,
|
|
126
|
+
children: "Reply"
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
var CommentEdit = ({ className, ...props }) => /* @__PURE__ */ jsx(
|
|
130
|
+
"button",
|
|
131
|
+
{
|
|
132
|
+
type: "button",
|
|
133
|
+
className: cx("text-xs text-fg-info-base cursor-pointer", className),
|
|
134
|
+
...props,
|
|
135
|
+
children: "Edit"
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
var CommentResolve = ({
|
|
139
|
+
className,
|
|
140
|
+
disabled,
|
|
141
|
+
onClick,
|
|
142
|
+
...props
|
|
143
|
+
}) => {
|
|
144
|
+
const { editable, onResolve } = useCommentContext();
|
|
145
|
+
const isDisabled = disabled || !editable;
|
|
146
|
+
const handleClick = onClick ?? onResolve;
|
|
147
|
+
return /* @__PURE__ */ jsx(
|
|
148
|
+
"button",
|
|
149
|
+
{
|
|
150
|
+
type: "button",
|
|
151
|
+
className: cx(
|
|
152
|
+
"text-xs font-semibold text-fg-success-base cursor-pointer",
|
|
153
|
+
className
|
|
154
|
+
),
|
|
155
|
+
onClick: handleClick,
|
|
156
|
+
disabled: isDisabled,
|
|
157
|
+
"aria-disabled": isDisabled,
|
|
158
|
+
...props,
|
|
159
|
+
children: "Resolve"
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
Comment.Actions = CommentActions;
|
|
164
|
+
Comment.Reply = CommentReply;
|
|
165
|
+
Comment.Edit = CommentEdit;
|
|
166
|
+
Comment.Resolve = CommentResolve;
|
|
167
|
+
CommentActions.displayName = "Comment.Actions";
|
|
168
|
+
CommentReply.displayName = "Comment.Reply";
|
|
169
|
+
CommentEdit.displayName = "Comment.Edit";
|
|
170
|
+
CommentResolve.displayName = "Comment.Resolve";
|
|
171
|
+
|
|
172
|
+
// src/elements/CommentComposer.jsx
|
|
173
|
+
import React3, {
|
|
174
|
+
forwardRef as forwardRef2,
|
|
175
|
+
useMemo as useMemo2,
|
|
176
|
+
useRef,
|
|
177
|
+
createContext as createContext2,
|
|
178
|
+
useContext as useContext2,
|
|
179
|
+
useEffect as useEffect2
|
|
180
|
+
} from "react";
|
|
181
|
+
import { cx as cx2 } from "@edux-design/utils";
|
|
182
|
+
import { Avatar as Avatar2 } from "@edux-design/avatars";
|
|
183
|
+
import { Button } from "@edux-design/buttons";
|
|
184
|
+
import { useEditor, EditorContent } from "@tiptap/react";
|
|
185
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
186
|
+
import Placeholder from "@tiptap/extension-placeholder";
|
|
187
|
+
|
|
188
|
+
// src/elements/CommentMentionExtension.jsx
|
|
189
|
+
import React2 from "react";
|
|
190
|
+
import { Node, mergeAttributes } from "@tiptap/core";
|
|
191
|
+
import { ReactNodeViewRenderer, NodeViewWrapper } from "@tiptap/react";
|
|
192
|
+
import { Mention } from "@edux-design/mentions";
|
|
193
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
194
|
+
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) ?? "";
|
|
198
|
+
return /* @__PURE__ */ jsx2(
|
|
199
|
+
NodeViewWrapper,
|
|
200
|
+
{
|
|
201
|
+
as: "span",
|
|
202
|
+
className: "inline-flex items-center align-middle -translate-y-2",
|
|
203
|
+
children: /* @__PURE__ */ jsxs2(Mention, { handle, size: "small", children: [
|
|
204
|
+
"@",
|
|
205
|
+
handle,
|
|
206
|
+
" \xB7 ",
|
|
207
|
+
label
|
|
208
|
+
] })
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
};
|
|
212
|
+
var CommentMentionExtension = Node.create({
|
|
213
|
+
name: "commentMention",
|
|
214
|
+
group: "inline",
|
|
215
|
+
inline: true,
|
|
216
|
+
atom: true,
|
|
217
|
+
selectable: false,
|
|
218
|
+
addAttributes() {
|
|
219
|
+
return {
|
|
220
|
+
id: { default: null },
|
|
221
|
+
handle: { default: "" },
|
|
222
|
+
label: { default: "" }
|
|
223
|
+
};
|
|
224
|
+
},
|
|
225
|
+
parseHTML() {
|
|
226
|
+
return [{ tag: "span[data-comment-mention]" }];
|
|
227
|
+
},
|
|
228
|
+
renderHTML({ HTMLAttributes }) {
|
|
229
|
+
const handle = HTMLAttributes.handle || "";
|
|
230
|
+
const label = HTMLAttributes.label || `@${handle}`;
|
|
231
|
+
return [
|
|
232
|
+
"span",
|
|
233
|
+
mergeAttributes(HTMLAttributes, {
|
|
234
|
+
"data-comment-mention": "true",
|
|
235
|
+
"data-handle": handle
|
|
236
|
+
}),
|
|
237
|
+
label
|
|
238
|
+
];
|
|
239
|
+
},
|
|
240
|
+
addNodeView() {
|
|
241
|
+
return ReactNodeViewRenderer(MentionNodeView);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// src/elements/CommentComposer.jsx
|
|
246
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
247
|
+
var CommentComposerContext = createContext2(null);
|
|
248
|
+
var useCommentComposer = () => {
|
|
249
|
+
const context = useContext2(CommentComposerContext);
|
|
250
|
+
if (!context) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
"CommentComposer components must be used within a <CommentComposer />."
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return context;
|
|
256
|
+
};
|
|
257
|
+
var CommentComposer = forwardRef2(function CommentComposer2({
|
|
258
|
+
value,
|
|
259
|
+
onChange,
|
|
260
|
+
onSubmit,
|
|
261
|
+
onCancel,
|
|
262
|
+
placeholder = "Write a comment...",
|
|
263
|
+
rows = 4,
|
|
264
|
+
disabled = false,
|
|
265
|
+
isSubmitting = false,
|
|
266
|
+
submitLabel = "Post",
|
|
267
|
+
cancelLabel = "Cancel",
|
|
268
|
+
avatar,
|
|
269
|
+
avatarProps,
|
|
270
|
+
actions,
|
|
271
|
+
className,
|
|
272
|
+
children,
|
|
273
|
+
...props
|
|
274
|
+
}, ref) {
|
|
275
|
+
const hasValue = value === void 0 ? true : Boolean(value && value.trim().length);
|
|
276
|
+
const isSubmitDisabled = disabled || isSubmitting || !hasValue;
|
|
277
|
+
const editor = useEditor({
|
|
278
|
+
extensions: [
|
|
279
|
+
StarterKit,
|
|
280
|
+
CommentMentionExtension,
|
|
281
|
+
Placeholder.configure({
|
|
282
|
+
placeholder
|
|
283
|
+
})
|
|
284
|
+
],
|
|
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
|
+
},
|
|
299
|
+
onUpdate: ({ editor: editor2 }) => {
|
|
300
|
+
onChange == null ? void 0 : onChange({ target: { value: editor2.getText() } });
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
const containerRef = useRef(null);
|
|
304
|
+
useEffect2(() => {
|
|
305
|
+
if (typeof ref === "function") {
|
|
306
|
+
ref(containerRef.current);
|
|
307
|
+
} else if (ref) {
|
|
308
|
+
ref.current = containerRef.current;
|
|
309
|
+
}
|
|
310
|
+
}, [ref]);
|
|
311
|
+
useEffect2(() => {
|
|
312
|
+
if (!editor) return;
|
|
313
|
+
if (value === "") {
|
|
314
|
+
editor.commands.clearContent();
|
|
315
|
+
}
|
|
316
|
+
}, [editor, value]);
|
|
317
|
+
const contextValue = useMemo2(
|
|
318
|
+
() => ({
|
|
319
|
+
editor,
|
|
320
|
+
placeholder,
|
|
321
|
+
editorContainerRef: containerRef
|
|
322
|
+
}),
|
|
323
|
+
[editor, placeholder]
|
|
324
|
+
);
|
|
325
|
+
return /* @__PURE__ */ jsx3(CommentComposerContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs3(
|
|
326
|
+
"form",
|
|
327
|
+
{
|
|
328
|
+
className: cx2(
|
|
329
|
+
"relative flex gap-12 rounded-default border border-border-minimal bg-bg-base p-12",
|
|
330
|
+
className
|
|
331
|
+
),
|
|
332
|
+
onSubmit: (event) => {
|
|
333
|
+
event.preventDefault();
|
|
334
|
+
if (isSubmitDisabled) return;
|
|
335
|
+
onSubmit == null ? void 0 : onSubmit(event);
|
|
336
|
+
},
|
|
337
|
+
...props,
|
|
338
|
+
children: [
|
|
339
|
+
(avatar || avatarProps) && /* @__PURE__ */ jsx3("div", { className: "shrink-0", children: avatar ?? /* @__PURE__ */ jsx3(Avatar2, { size: "small", ...avatarProps }) }),
|
|
340
|
+
/* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
|
|
341
|
+
/* @__PURE__ */ jsx3("div", { ref: containerRef, className: "relative", children: /* @__PURE__ */ jsx3(EditorContent, { editor }) }),
|
|
342
|
+
/* @__PURE__ */ jsxs3("div", { className: "mt-8 flex flex-wrap items-center justify-between gap-8", children: [
|
|
343
|
+
actions ? /* @__PURE__ */ jsx3("div", { className: "flex flex-wrap items-center gap-8 text-xs text-fg-subtle", children: actions }) : /* @__PURE__ */ jsx3("span", {}),
|
|
344
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-8", children: [
|
|
345
|
+
onCancel && /* @__PURE__ */ jsx3(
|
|
346
|
+
Button,
|
|
347
|
+
{
|
|
348
|
+
type: "button",
|
|
349
|
+
size: "small",
|
|
350
|
+
isSecondary: true,
|
|
351
|
+
onClick: onCancel,
|
|
352
|
+
disabled: disabled || isSubmitting,
|
|
353
|
+
children: cancelLabel
|
|
354
|
+
}
|
|
355
|
+
),
|
|
356
|
+
/* @__PURE__ */ jsx3(Button, { type: "submit", size: "small", disabled: isSubmitDisabled, children: isSubmitting ? "Posting..." : submitLabel })
|
|
357
|
+
] })
|
|
358
|
+
] })
|
|
359
|
+
] }),
|
|
360
|
+
children
|
|
361
|
+
]
|
|
362
|
+
}
|
|
363
|
+
) });
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// src/elements/CommentMention.jsx
|
|
367
|
+
import React4, { useMemo as useMemo3, useState as useState2, useCallback, useEffect as useEffect3, useRef as useRef2 } from "react";
|
|
368
|
+
import { Menu, MenuList, MenuItem } from "@edux-design/menus";
|
|
369
|
+
import { Avatar as Avatar3 } from "@edux-design/avatars";
|
|
370
|
+
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 });
|
|
379
|
+
const menuListRef = useRef2(null);
|
|
380
|
+
const rangeRef = useRef2(null);
|
|
381
|
+
const peopleByHandle = useMemo3(
|
|
382
|
+
() => new Map(peopleList.map((person) => [person.handle.toLowerCase(), person])),
|
|
383
|
+
[peopleList]
|
|
384
|
+
);
|
|
385
|
+
const filteredPeople = useMemo3(() => {
|
|
386
|
+
if (!query) return peopleList;
|
|
387
|
+
const lowered = query.toLowerCase();
|
|
388
|
+
return peopleList.filter(
|
|
389
|
+
(person) => person.handle.toLowerCase().startsWith(lowered)
|
|
390
|
+
);
|
|
391
|
+
}, [peopleList, query]);
|
|
392
|
+
const updateMenuState = useCallback(() => {
|
|
393
|
+
var _a, _b;
|
|
394
|
+
if (!editor) return;
|
|
395
|
+
const { from } = editor.state.selection;
|
|
396
|
+
const { parent, parentOffset, pos } = editor.state.selection.$from;
|
|
397
|
+
if (!parent.isTextblock) {
|
|
398
|
+
setMenuOpen(false);
|
|
399
|
+
setQuery("");
|
|
400
|
+
setRange(null);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const textBefore = parent.textBetween(0, parentOffset, "\n", "\0");
|
|
404
|
+
const match = textBefore.match(/@([\w-]*)$/);
|
|
405
|
+
if (match) {
|
|
406
|
+
const handleQuery = match[1] ?? "";
|
|
407
|
+
setQuery(handleQuery);
|
|
408
|
+
setMenuOpen(true);
|
|
409
|
+
const fromPos = pos - match[0].length;
|
|
410
|
+
const nextRange = { from: fromPos, to: pos };
|
|
411
|
+
setRange(nextRange);
|
|
412
|
+
rangeRef.current = nextRange;
|
|
413
|
+
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);
|
|
415
|
+
if (containerRect) {
|
|
416
|
+
setAnchorStyle({
|
|
417
|
+
left: coords.left - containerRect.left + 12,
|
|
418
|
+
top: coords.bottom - containerRect.top + 4
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
setMenuOpen(false);
|
|
424
|
+
setQuery("");
|
|
425
|
+
setRange(null);
|
|
426
|
+
rangeRef.current = null;
|
|
427
|
+
}, [editor]);
|
|
428
|
+
const handleSelectPerson = useCallback(
|
|
429
|
+
(person) => {
|
|
430
|
+
if (!editor || !rangeRef.current) return;
|
|
431
|
+
const selectionRange = rangeRef.current;
|
|
432
|
+
editor.commands.focus();
|
|
433
|
+
editor.commands.setTextSelection(selectionRange);
|
|
434
|
+
editor.commands.insertContentAt(
|
|
435
|
+
selectionRange,
|
|
436
|
+
[
|
|
437
|
+
{
|
|
438
|
+
type: "commentMention",
|
|
439
|
+
attrs: {
|
|
440
|
+
id: person.id,
|
|
441
|
+
handle: person.handle,
|
|
442
|
+
label: person.name
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
{ type: "text", text: " " }
|
|
446
|
+
],
|
|
447
|
+
{ updateSelection: false }
|
|
448
|
+
);
|
|
449
|
+
const nextPos = selectionRange.from + 2;
|
|
450
|
+
requestAnimationFrame(() => {
|
|
451
|
+
editor.commands.setTextSelection(nextPos);
|
|
452
|
+
editor.view.focus();
|
|
453
|
+
setTimeout(() => editor.view.focus(), 0);
|
|
454
|
+
});
|
|
455
|
+
setMenuOpen(false);
|
|
456
|
+
setQuery("");
|
|
457
|
+
setRange(null);
|
|
458
|
+
rangeRef.current = null;
|
|
459
|
+
},
|
|
460
|
+
[editor]
|
|
461
|
+
);
|
|
462
|
+
useEffect3(() => {
|
|
463
|
+
if (!editor) return;
|
|
464
|
+
updateMenuState();
|
|
465
|
+
editor.on("transaction", updateMenuState);
|
|
466
|
+
editor.on("selectionUpdate", updateMenuState);
|
|
467
|
+
return () => {
|
|
468
|
+
editor.off("transaction", updateMenuState);
|
|
469
|
+
editor.off("selectionUpdate", updateMenuState);
|
|
470
|
+
};
|
|
471
|
+
}, [editor, updateMenuState]);
|
|
472
|
+
useEffect3(() => {
|
|
473
|
+
var _a;
|
|
474
|
+
if (!editor) return;
|
|
475
|
+
const handleKeyDown = (event) => {
|
|
476
|
+
var _a2;
|
|
477
|
+
if (!menuOpen) return;
|
|
478
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
|
479
|
+
event.preventDefault();
|
|
480
|
+
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;
|
|
483
|
+
list.focus();
|
|
484
|
+
const direction = event.key === "ArrowDown" ? 1 : -1;
|
|
485
|
+
const targetIndex = direction === 1 ? 0 : items.length - 1;
|
|
486
|
+
(_a2 = items[targetIndex]) == null ? void 0 : _a2.focus();
|
|
487
|
+
};
|
|
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]);
|
|
492
|
+
const menuContent = useMemo3(
|
|
493
|
+
() => /* @__PURE__ */ jsx4(
|
|
494
|
+
Menu,
|
|
495
|
+
{
|
|
496
|
+
open: menuOpen && filteredPeople.length > 0,
|
|
497
|
+
onOpenChange: setMenuOpen,
|
|
498
|
+
button: /* @__PURE__ */ jsx4(
|
|
499
|
+
"button",
|
|
500
|
+
{
|
|
501
|
+
type: "button",
|
|
502
|
+
"aria-hidden": true,
|
|
503
|
+
className: "absolute h-1 w-1 opacity-0",
|
|
504
|
+
style: {
|
|
505
|
+
left: `${anchorStyle.left}px`,
|
|
506
|
+
top: `${anchorStyle.top}px`,
|
|
507
|
+
pointerEvents: "none"
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
),
|
|
511
|
+
align: "start",
|
|
512
|
+
side: "bottom",
|
|
513
|
+
trapFocus: false,
|
|
514
|
+
disableOutsidePointerEvents: false,
|
|
515
|
+
onOpenAutoFocus: (event) => event.preventDefault(),
|
|
516
|
+
contentProps: {
|
|
517
|
+
sideOffset: 4,
|
|
518
|
+
avoidCollisions: true,
|
|
519
|
+
collisionPadding: 12,
|
|
520
|
+
onCloseAutoFocus: (event) => event.preventDefault()
|
|
521
|
+
},
|
|
522
|
+
children: /* @__PURE__ */ jsx4(MenuList, { ref: menuListRef, className: "min-w-[220px]", children: filteredPeople.map((person) => /* @__PURE__ */ jsx4(
|
|
523
|
+
MenuItem,
|
|
524
|
+
{
|
|
525
|
+
onMouseDown: (event) => {
|
|
526
|
+
event.preventDefault();
|
|
527
|
+
event.stopPropagation();
|
|
528
|
+
handleSelectPerson(person);
|
|
529
|
+
},
|
|
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
|
|
539
|
+
] })
|
|
540
|
+
] })
|
|
541
|
+
] })
|
|
542
|
+
},
|
|
543
|
+
person.id
|
|
544
|
+
)) })
|
|
545
|
+
}
|
|
546
|
+
),
|
|
547
|
+
[filteredPeople, handleSelectPerson, menuOpen]
|
|
548
|
+
);
|
|
549
|
+
if (!editor) return null;
|
|
550
|
+
return menuContent;
|
|
551
|
+
};
|
|
552
|
+
export {
|
|
553
|
+
Comment,
|
|
554
|
+
CommentComposer,
|
|
555
|
+
CommentMention
|
|
556
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@edux-design/comments",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"sideEffects": [
|
|
6
|
+
"**/*.css"
|
|
7
|
+
],
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"lint": "eslint . --max-warnings 0",
|
|
18
|
+
"build": "tsup src/index.js --format esm,cjs --dts",
|
|
19
|
+
"dev": "tsup src/index.js --watch --format esm,cjs --dts",
|
|
20
|
+
"generate:component": "turbo gen react-component",
|
|
21
|
+
"check-types": "tsc --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "^5.9.2",
|
|
25
|
+
"tailwindcss": "^4.1.12",
|
|
26
|
+
"postcss": "^8.5.6",
|
|
27
|
+
"tsup": "^8.5.0",
|
|
28
|
+
"@tailwindcss/cli": "^4.1.12",
|
|
29
|
+
"@tailwindcss/postcss": "^4.1.12",
|
|
30
|
+
"@tiptap/core": "^2.14.0",
|
|
31
|
+
"@tiptap/react": "^2.14.0",
|
|
32
|
+
"@tiptap/starter-kit": "^2.14.0",
|
|
33
|
+
"@tiptap/extension-placeholder": "^2.14.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@edux-design/avatars": "*",
|
|
37
|
+
"@edux-design/buttons": "*",
|
|
38
|
+
"@edux-design/forms": "*",
|
|
39
|
+
"@edux-design/mentions": "*",
|
|
40
|
+
"@edux-design/menus": "*",
|
|
41
|
+
"@edux-design/utils": "*"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"react": "^19.1.0",
|
|
45
|
+
"react-dom": "^19.1.0",
|
|
46
|
+
"@tiptap/core": "^2.14.0",
|
|
47
|
+
"@tiptap/react": "^2.14.0",
|
|
48
|
+
"@tiptap/starter-kit": "^2.14.0",
|
|
49
|
+
"@tiptap/extension-placeholder": "^2.14.0"
|
|
50
|
+
}
|
|
51
|
+
}
|