@stll/folio-react 0.0.1-placeholder.0 → 0.1.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.
@@ -0,0 +1,823 @@
1
+ import { t as containedHandler } from "./contained-handler-4uiqUVRn.js";
2
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ import { CheckIcon, MoreVerticalIcon } from "lucide-react";
4
+ import { useLocale, useTranslations } from "use-intl";
5
+ import { closestHtmlElement, queryHtmlElement } from "@stll/folio-core/utils/domGuards";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/components/CommentsSidebar.tsx
8
+ /**
9
+ * Comments Sidebar
10
+ *
11
+ * Floating cards positioned relative to their anchored text in the document.
12
+ * Cards appear at the Y position of their corresponding text.
13
+ * Clicking a card expands it to show reply input and action buttons.
14
+ *
15
+ * Tracked changes are rendered purely inline (Word-style) and do not appear
16
+ * in this sidebar.
17
+ */
18
+ /** Extract plain text from a Comment's paragraph content */
19
+ function getCommentText(paragraphs) {
20
+ if (!paragraphs?.length) return "";
21
+ return paragraphs.flatMap((p) => p.content.filter((c) => c.type === "run").flatMap((r) => "content" in r ? r.content : []).filter((c) => c.type === "text").map((t) => "text" in t ? t.text : "")).join("");
22
+ }
23
+ function formatDate(dateStr, locale) {
24
+ if (!dateStr) return "";
25
+ const d = new Date(dateStr);
26
+ if (Number.isNaN(d.getTime())) return dateStr;
27
+ return new Intl.DateTimeFormat(locale, {
28
+ hour: "numeric",
29
+ minute: "2-digit",
30
+ month: "short",
31
+ day: "numeric"
32
+ }).format(d);
33
+ }
34
+ function getInitials(name) {
35
+ return name.split(/\s+/u).map((w) => w[0]).join("").toUpperCase().slice(0, 2);
36
+ }
37
+ function getCommentParentId(comment) {
38
+ return comment.parentId;
39
+ }
40
+ const AVATAR_COLORS = [
41
+ "#6DCCB1",
42
+ "#79AAD9",
43
+ "#EE789D",
44
+ "#A987D1",
45
+ "#E6A85F",
46
+ "#F2CC8F",
47
+ "#68B3A2",
48
+ "#B07AA1",
49
+ "#59A14F",
50
+ "#FF9DA7",
51
+ "#E15759",
52
+ "#76B7B2"
53
+ ];
54
+ function getAvatarColor(name) {
55
+ let hash = 0;
56
+ for (let i = 0; i < name.length; i++) hash = (name.codePointAt(i) ?? 0) + ((hash << 5) - hash);
57
+ return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];
58
+ }
59
+ const MIN_CARD_GAP = 6;
60
+ const DEFAULT_CARD_HEIGHT = 64;
61
+ const DEFAULT_INPUT_HEIGHT = 104;
62
+ function arePositionMapsEqual(a, b) {
63
+ if (a.size !== b.size) return false;
64
+ for (const [key, value] of b) if (a.get(key) !== value) return false;
65
+ return true;
66
+ }
67
+ const ICON_BUTTON_STYLE = {
68
+ background: "none",
69
+ border: "none",
70
+ cursor: "pointer",
71
+ padding: 4,
72
+ color: "var(--doc-text-muted)",
73
+ display: "flex",
74
+ borderRadius: "50%"
75
+ };
76
+ const CANCEL_BUTTON_STYLE = {
77
+ minHeight: 28,
78
+ padding: "5px 10px",
79
+ fontSize: 12,
80
+ border: "1px solid transparent",
81
+ borderRadius: 6,
82
+ background: "transparent",
83
+ color: "var(--muted-foreground, var(--doc-text-muted))",
84
+ cursor: "pointer",
85
+ fontWeight: 500,
86
+ fontFamily: "inherit"
87
+ };
88
+ const CommentsSidebar = ({ comments, onCommentClick, activeCommentId = null, onCommentReply, onCommentResolve, onCommentDelete, onAddComment, onCancelAddComment, onAcceptChange: _onAcceptChange, onRejectChange: _onRejectChange, onTrackedChangeReply: _onTrackedChangeReply, topOffset = 0, showResolved = false, isAddingComment = false, addCommentYPosition = null, pageWidth = 816, editorContainerRef, anchorPositions }) => {
89
+ const t = useTranslations("folio");
90
+ const locale = useLocale();
91
+ const [replyingTo, setReplyingTo] = useState(null);
92
+ const [replyText, setReplyText] = useState("");
93
+ const [newCommentText, setNewCommentText] = useState("");
94
+ const [expandedCard, setExpandedCard] = useState(null);
95
+ const [menuOpenFor, setMenuOpenFor] = useState(null);
96
+ const [cardPositions, setCardPositions] = useState(/* @__PURE__ */ new Map());
97
+ const [measuredLeft, setMeasuredLeft] = useState(null);
98
+ const [initialPositionsDone, setInitialPositionsDone] = useState(false);
99
+ const knownCardsRef = useRef(/* @__PURE__ */ new Set());
100
+ const lastKnownCardPositionsRef = useRef(/* @__PURE__ */ new Map());
101
+ const sidebarRef = useRef(null);
102
+ const cardRefs = useRef(/* @__PURE__ */ new Map());
103
+ const updateSidebarLeft = useCallback(() => {
104
+ const scrollEl = editorContainerRef?.current;
105
+ const sidebarEl = sidebarRef.current;
106
+ const pageEl = scrollEl ? queryHtmlElement(scrollEl, ".layout-page") : null;
107
+ const offsetParentRaw = sidebarEl?.offsetParent;
108
+ const offsetParent = offsetParentRaw instanceof HTMLElement ? offsetParentRaw : null;
109
+ if (!scrollEl || !pageEl || !offsetParent) {
110
+ setMeasuredLeft(null);
111
+ return;
112
+ }
113
+ const parentRect = offsetParent.getBoundingClientRect();
114
+ const rawLeft = pageEl.getBoundingClientRect().right - parentRect.left + 12;
115
+ const maxVisibleLeft = Math.max(8, parentRect.width - 280 - 8);
116
+ setMeasuredLeft(Math.max(8, Math.min(rawLeft, maxVisibleLeft)));
117
+ }, [editorContainerRef]);
118
+ useLayoutEffect(() => {
119
+ updateSidebarLeft();
120
+ }, [
121
+ updateSidebarLeft,
122
+ pageWidth,
123
+ isAddingComment,
124
+ comments.length
125
+ ]);
126
+ useEffect(() => {
127
+ const scrollEl = editorContainerRef?.current;
128
+ if (!scrollEl) return;
129
+ const resizeObserver = new ResizeObserver(updateSidebarLeft);
130
+ resizeObserver.observe(scrollEl);
131
+ const handleScroll = () => updateSidebarLeft();
132
+ scrollEl.addEventListener("scroll", handleScroll, { passive: true });
133
+ window.addEventListener("resize", handleScroll);
134
+ return () => {
135
+ resizeObserver.disconnect();
136
+ scrollEl.removeEventListener("scroll", handleScroll);
137
+ window.removeEventListener("resize", handleScroll);
138
+ };
139
+ }, [editorContainerRef, updateSidebarLeft]);
140
+ const visibleComments = useMemo(() => comments.filter((c) => {
141
+ const parentId = getCommentParentId(c);
142
+ if (parentId !== null && parentId !== void 0) return false;
143
+ if (c.done && !showResolved) return false;
144
+ return true;
145
+ }), [comments, showResolved]);
146
+ const repliesByParent = useMemo(() => {
147
+ const map = /* @__PURE__ */ new Map();
148
+ for (const c of comments) {
149
+ const parentId = getCommentParentId(c);
150
+ if (parentId !== null && parentId !== void 0) {
151
+ const arr = map.get(parentId);
152
+ if (arr) arr.push(c);
153
+ else map.set(parentId, [c]);
154
+ }
155
+ }
156
+ return map;
157
+ }, [comments]);
158
+ const getReplies = (commentId) => repliesByParent.get(commentId) ?? [];
159
+ const updateCardPositions = useCallback(() => {
160
+ const container = editorContainerRef?.current;
161
+ if (!container) return;
162
+ const pagesEl = container.querySelector(".paged-editor__pages");
163
+ if (!pagesEl) return;
164
+ const containerRect = container.getBoundingClientRect();
165
+ const scrollTop = container.scrollTop;
166
+ const positions = [];
167
+ const pushPosition = (id, targetY, height) => {
168
+ lastKnownCardPositionsRef.current.set(id, targetY);
169
+ positions.push({
170
+ id,
171
+ targetY,
172
+ height
173
+ });
174
+ };
175
+ for (const comment of visibleComments) {
176
+ const cardId = `comment-${comment.id}`;
177
+ const el = pagesEl.querySelector(`[data-comment-id="${comment.id}"]`);
178
+ if (el) {
179
+ pushPosition(cardId, el.getBoundingClientRect().top - containerRect.top + scrollTop, cardRefs.current.get(cardId)?.offsetHeight || DEFAULT_CARD_HEIGHT);
180
+ continue;
181
+ }
182
+ const layoutY = anchorPositions?.get(cardId);
183
+ if (layoutY !== void 0) {
184
+ pushPosition(cardId, layoutY, cardRefs.current.get(cardId)?.offsetHeight || DEFAULT_CARD_HEIGHT);
185
+ continue;
186
+ }
187
+ const lastKnownY = lastKnownCardPositionsRef.current.get(cardId);
188
+ if (lastKnownY !== void 0 && activeCommentId === comment.id) {
189
+ positions.push({
190
+ id: cardId,
191
+ targetY: lastKnownY,
192
+ height: cardRefs.current.get(cardId)?.offsetHeight || DEFAULT_CARD_HEIGHT
193
+ });
194
+ continue;
195
+ }
196
+ const newCommentY = lastKnownCardPositionsRef.current.get("new-comment-input");
197
+ if (activeCommentId === comment.id && newCommentY !== void 0) {
198
+ pushPosition(cardId, newCommentY, cardRefs.current.get(cardId)?.offsetHeight || DEFAULT_CARD_HEIGHT);
199
+ continue;
200
+ }
201
+ }
202
+ if (isAddingComment && addCommentYPosition !== null) {
203
+ positions.push({
204
+ id: "new-comment-input",
205
+ targetY: addCommentYPosition,
206
+ height: cardRefs.current.get("new-comment-input")?.offsetHeight || DEFAULT_INPUT_HEIGHT
207
+ });
208
+ lastKnownCardPositionsRef.current.set("new-comment-input", addCommentYPosition);
209
+ }
210
+ positions.sort((a, b) => a.targetY - b.targetY);
211
+ const resolvedPositions = /* @__PURE__ */ new Map();
212
+ let lastBottom = 0;
213
+ for (const pos of positions) {
214
+ const y = Math.max(pos.targetY, lastBottom + MIN_CARD_GAP);
215
+ resolvedPositions.set(pos.id, y);
216
+ lastBottom = y + pos.height;
217
+ }
218
+ setCardPositions((prev) => arePositionMapsEqual(prev, resolvedPositions) ? prev : resolvedPositions);
219
+ const visiblePositionIds = new Set(resolvedPositions.keys());
220
+ for (const key of lastKnownCardPositionsRef.current.keys()) if (!visiblePositionIds.has(key) && key !== "new-comment-input") lastKnownCardPositionsRef.current.delete(key);
221
+ }, [
222
+ visibleComments,
223
+ editorContainerRef,
224
+ isAddingComment,
225
+ addCommentYPosition,
226
+ anchorPositions,
227
+ activeCommentId
228
+ ]);
229
+ useEffect(() => {
230
+ const container = editorContainerRef?.current;
231
+ if (!container) return;
232
+ const pagesEl = container.querySelector(".paged-editor__pages");
233
+ if (!pagesEl) return;
234
+ const handleDocClick = (e) => {
235
+ const target = e.target;
236
+ if (!(target instanceof Element)) return;
237
+ if (sidebarRef.current?.contains(target)) return;
238
+ if (pagesEl.contains(target)) {
239
+ const commentEl = closestHtmlElement(target, "[data-comment-id]");
240
+ if (commentEl?.dataset["commentId"]) {
241
+ setExpandedCard(`comment-${commentEl.dataset["commentId"]}`);
242
+ onCommentClick?.(Number(commentEl.dataset["commentId"]));
243
+ return;
244
+ }
245
+ }
246
+ setExpandedCard(null);
247
+ setMenuOpenFor(null);
248
+ };
249
+ container.addEventListener("click", handleDocClick);
250
+ return () => container.removeEventListener("click", handleDocClick);
251
+ }, [editorContainerRef, onCommentClick]);
252
+ useEffect(() => {
253
+ const container = editorContainerRef?.current;
254
+ if (!container) return;
255
+ const timerQuick = setTimeout(updateCardPositions, 50);
256
+ const timerFull = setTimeout(() => {
257
+ updateCardPositions();
258
+ setInitialPositionsDone(true);
259
+ }, 400);
260
+ const resizeObserver = new ResizeObserver(() => {
261
+ requestAnimationFrame(updateCardPositions);
262
+ });
263
+ resizeObserver.observe(container);
264
+ return () => {
265
+ clearTimeout(timerQuick);
266
+ clearTimeout(timerFull);
267
+ resizeObserver.disconnect();
268
+ };
269
+ }, [updateCardPositions, editorContainerRef]);
270
+ useEffect(() => {
271
+ const container = editorContainerRef?.current;
272
+ if (!container) return;
273
+ let rafId = null;
274
+ const scheduleUpdate = () => {
275
+ if (rafId !== null) return;
276
+ rafId = requestAnimationFrame(() => {
277
+ rafId = null;
278
+ updateCardPositions();
279
+ });
280
+ };
281
+ container.addEventListener("scroll", scheduleUpdate, { passive: true });
282
+ return () => {
283
+ if (rafId !== null) cancelAnimationFrame(rafId);
284
+ container.removeEventListener("scroll", scheduleUpdate);
285
+ };
286
+ }, [editorContainerRef, updateCardPositions]);
287
+ useEffect(() => {
288
+ const raf = requestAnimationFrame(updateCardPositions);
289
+ return () => cancelAnimationFrame(raf);
290
+ }, [
291
+ expandedCard,
292
+ isAddingComment,
293
+ updateCardPositions
294
+ ]);
295
+ useEffect(() => {
296
+ const targets = [];
297
+ if (expandedCard) {
298
+ const el = cardRefs.current.get(expandedCard);
299
+ if (el) targets.push(el);
300
+ }
301
+ const addEl = cardRefs.current.get("new-comment-input");
302
+ if (addEl) targets.push(addEl);
303
+ if (targets.length === 0) return;
304
+ let rafId;
305
+ const observer = new ResizeObserver(() => {
306
+ cancelAnimationFrame(rafId);
307
+ rafId = requestAnimationFrame(updateCardPositions);
308
+ });
309
+ for (const el of targets) observer.observe(el);
310
+ return () => {
311
+ cancelAnimationFrame(rafId);
312
+ observer.disconnect();
313
+ };
314
+ }, [expandedCard, updateCardPositions]);
315
+ const handleNewCommentSubmit = () => {
316
+ if (newCommentText.trim()) {
317
+ if (onAddComment?.(newCommentText.trim()) !== false) setNewCommentText("");
318
+ }
319
+ };
320
+ useEffect(() => {
321
+ if (activeCommentId === null) return;
322
+ setExpandedCard(`comment-${activeCommentId}`);
323
+ }, [activeCommentId]);
324
+ const handleCardClick = (cardId, commentId) => {
325
+ const nextExpandedCard = expandedCard === cardId ? null : cardId;
326
+ setExpandedCard(nextExpandedCard);
327
+ setMenuOpenFor(null);
328
+ if (commentId !== void 0) onCommentClick?.(nextExpandedCard === null && activeCommentId === commentId ? null : commentId);
329
+ };
330
+ const hasPositions = cardPositions.size > 0;
331
+ const avatarStyle = (name, size = 28) => ({
332
+ width: size,
333
+ height: size,
334
+ borderRadius: "50%",
335
+ backgroundColor: getAvatarColor(name),
336
+ color: "var(--doc-canvas-text)",
337
+ display: "flex",
338
+ alignItems: "center",
339
+ justifyContent: "center",
340
+ fontSize: size === 28 ? 12 : 10,
341
+ fontWeight: 500,
342
+ flexShrink: 0
343
+ });
344
+ const submitButtonStyle = (enabled) => ({
345
+ minHeight: 28,
346
+ padding: "5px 12px",
347
+ fontSize: 12,
348
+ border: enabled ? "1px solid var(--primary, var(--doc-primary))" : "1px solid var(--border, var(--doc-border))",
349
+ borderRadius: 6,
350
+ background: enabled ? "var(--primary, var(--doc-primary))" : "var(--muted, var(--doc-bg))",
351
+ color: enabled ? "var(--primary-foreground, var(--doc-page))" : "var(--muted-foreground, var(--doc-text-muted))",
352
+ cursor: enabled ? "pointer" : "default",
353
+ fontWeight: 500,
354
+ fontFamily: "inherit"
355
+ });
356
+ const cardContainerStyle = (cardId, isExpanded, yPos) => {
357
+ const isKnown = knownCardsRef.current.has(cardId);
358
+ if (yPos !== void 0) knownCardsRef.current.add(cardId);
359
+ const isNewCard = !isKnown && yPos !== void 0;
360
+ const noPosition = hasPositions && yPos === void 0;
361
+ const positionStyle = (() => {
362
+ if (!hasPositions) return { marginBottom: 6 };
363
+ if (yPos !== void 0) return {
364
+ position: "absolute",
365
+ top: yPos,
366
+ left: 0,
367
+ right: 0,
368
+ opacity: 1
369
+ };
370
+ return {
371
+ position: "absolute",
372
+ top: 0,
373
+ left: 0,
374
+ right: 0,
375
+ opacity: 0,
376
+ visibility: "hidden"
377
+ };
378
+ })();
379
+ let transition = "none";
380
+ if (!noPosition && isNewCard) transition = "opacity 0.2s ease, box-shadow 0.2s ease";
381
+ else if (!noPosition && initialPositionsDone) transition = "opacity 0.2s ease, box-shadow 0.2s ease, top 0.15s ease";
382
+ return {
383
+ ...positionStyle,
384
+ padding: isExpanded ? "8px 10px" : "7px 9px",
385
+ borderRadius: 6,
386
+ backgroundColor: "var(--doc-page)",
387
+ cursor: "pointer",
388
+ boxShadow: isExpanded ? "0 1px 2px rgba(60,64,67,0.22), 0 3px 8px rgba(60,64,67,0.12)" : "0 1px 2px rgba(60,64,67,0.16), 0 2px 5px rgba(60,64,67,0.08)",
389
+ transition
390
+ };
391
+ };
392
+ const renderReplies = (replies, isExpanded) => {
393
+ if (replies.length === 0) return null;
394
+ return /* @__PURE__ */ jsxs("div", {
395
+ style: { marginTop: 8 },
396
+ children: [(isExpanded ? replies : replies.slice(-1)).map((reply) => /* @__PURE__ */ jsxs("div", {
397
+ style: {
398
+ marginBottom: isExpanded ? 6 : 0,
399
+ paddingTop: 6,
400
+ borderTop: "1px solid var(--doc-border)"
401
+ },
402
+ children: [/* @__PURE__ */ jsxs("div", {
403
+ style: {
404
+ display: "flex",
405
+ alignItems: "flex-start",
406
+ gap: 8
407
+ },
408
+ children: [/* @__PURE__ */ jsx("div", {
409
+ style: avatarStyle(reply.author || "U", 22),
410
+ children: getInitials(reply.author || "U")
411
+ }), /* @__PURE__ */ jsxs("div", {
412
+ style: {
413
+ flex: 1,
414
+ minWidth: 0
415
+ },
416
+ children: [/* @__PURE__ */ jsx("div", {
417
+ style: {
418
+ fontSize: 12,
419
+ fontWeight: 600,
420
+ color: "var(--doc-text)"
421
+ },
422
+ children: reply.author || "Unknown"
423
+ }), /* @__PURE__ */ jsx("div", {
424
+ style: {
425
+ fontSize: 10,
426
+ color: "var(--doc-text-muted)"
427
+ },
428
+ children: formatDate(reply.date, locale)
429
+ })]
430
+ })]
431
+ }), /* @__PURE__ */ jsx("div", {
432
+ style: {
433
+ fontSize: 12,
434
+ color: "var(--doc-text)",
435
+ lineHeight: "17px",
436
+ marginTop: 4,
437
+ ...!isExpanded ? {
438
+ overflow: "hidden",
439
+ display: "-webkit-box",
440
+ WebkitLineClamp: 2,
441
+ WebkitBoxOrient: "vertical"
442
+ } : {}
443
+ },
444
+ children: getCommentText(reply.content)
445
+ })]
446
+ }, reply.id)), !isExpanded && replies.length > 1 && /* @__PURE__ */ jsx("div", {
447
+ style: {
448
+ fontSize: 11,
449
+ color: "var(--doc-text-muted)",
450
+ marginTop: 4
451
+ },
452
+ children: t("comments.moreReplies", { count: String(replies.length - 1) })
453
+ })]
454
+ });
455
+ };
456
+ const renderReplySection = (replyKey, submitFn) => /* @__PURE__ */ jsx("div", {
457
+ onClick: (e) => e.stopPropagation(),
458
+ role: "presentation",
459
+ onKeyDown: (e) => e.stopPropagation(),
460
+ style: { marginTop: 8 },
461
+ children: replyingTo === replyKey ? /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("input", {
462
+ ref: (el) => el?.focus({ preventScroll: true }),
463
+ type: "text",
464
+ value: replyText,
465
+ onChange: (e) => setReplyText(e.target.value),
466
+ onMouseDown: (e) => e.stopPropagation(),
467
+ onKeyDown: (e) => {
468
+ e.stopPropagation();
469
+ if (e.key === "Enter") {
470
+ e.preventDefault();
471
+ if (replyText.trim() && submitFn) submitFn(replyKey, replyText.trim());
472
+ setReplyText("");
473
+ setReplyingTo(null);
474
+ }
475
+ if (e.key === "Escape") {
476
+ setReplyingTo(null);
477
+ setReplyText("");
478
+ }
479
+ },
480
+ placeholder: t("comments.replyPlaceholder"),
481
+ style: {
482
+ width: "100%",
483
+ border: "1px solid var(--doc-primary)",
484
+ borderRadius: 6,
485
+ outline: "none",
486
+ fontSize: 12,
487
+ padding: "7px 10px",
488
+ boxSizing: "border-box",
489
+ color: "var(--doc-text)",
490
+ backgroundColor: "var(--doc-page)"
491
+ }
492
+ }), /* @__PURE__ */ jsxs("div", {
493
+ style: {
494
+ display: "flex",
495
+ justifyContent: "flex-end",
496
+ gap: 8,
497
+ marginTop: 8
498
+ },
499
+ children: [/* @__PURE__ */ jsx("button", {
500
+ type: "button",
501
+ onClick: (e) => {
502
+ e.stopPropagation();
503
+ setReplyingTo(null);
504
+ setReplyText("");
505
+ },
506
+ style: CANCEL_BUTTON_STYLE,
507
+ children: t("comments.cancel")
508
+ }), /* @__PURE__ */ jsx("button", {
509
+ type: "button",
510
+ onClick: (e) => {
511
+ e.stopPropagation();
512
+ if (replyText.trim() && submitFn) submitFn(replyKey, replyText.trim());
513
+ setReplyText("");
514
+ setReplyingTo(null);
515
+ },
516
+ disabled: !replyText.trim(),
517
+ style: submitButtonStyle(!!replyText.trim()),
518
+ children: t("comments.reply")
519
+ })]
520
+ })] }) : /* @__PURE__ */ jsx("input", {
521
+ readOnly: true,
522
+ onMouseDown: (e) => e.stopPropagation(),
523
+ onClick: (e) => {
524
+ e.stopPropagation();
525
+ setReplyingTo(replyKey);
526
+ },
527
+ placeholder: t("comments.replyPlaceholder"),
528
+ style: {
529
+ width: "100%",
530
+ border: "1px solid var(--doc-border)",
531
+ borderRadius: 6,
532
+ outline: "none",
533
+ fontSize: 12,
534
+ padding: "7px 10px",
535
+ color: "var(--doc-text-subtle)",
536
+ cursor: "text",
537
+ backgroundColor: "var(--doc-page)",
538
+ boxSizing: "border-box"
539
+ }
540
+ })
541
+ });
542
+ const renderCommentCard = (comment) => {
543
+ const replies = getReplies(comment.id);
544
+ const cardId = `comment-${comment.id}`;
545
+ const isExpanded = expandedCard === cardId;
546
+ const isActive = activeCommentId === comment.id;
547
+ const yPos = cardPositions.get(cardId) ?? lastKnownCardPositionsRef.current.get(cardId);
548
+ const cardRef = { get current() {
549
+ return cardRefs.current.get(cardId) ?? null;
550
+ } };
551
+ return /* @__PURE__ */ jsxs("div", {
552
+ ref: (el) => {
553
+ if (el) cardRefs.current.set(cardId, el);
554
+ else cardRefs.current.delete(cardId);
555
+ },
556
+ "data-comment-id": comment.id,
557
+ className: "docx-comment-card",
558
+ onClick: containedHandler(cardRef, () => handleCardClick(cardId, comment.id)),
559
+ onKeyDown: (e) => {
560
+ if (e.key === "Enter" || e.key === " ") handleCardClick(cardId, comment.id);
561
+ },
562
+ onMouseDown: containedHandler(cardRef, (e) => e.stopPropagation()),
563
+ style: {
564
+ ...cardContainerStyle(cardId, isExpanded, yPos),
565
+ opacity: comment.done ? .6 : 1,
566
+ outline: isActive ? "2px solid var(--doc-primary, var(--primary))" : "none",
567
+ outlineOffset: 2
568
+ },
569
+ children: [
570
+ /* @__PURE__ */ jsxs("div", {
571
+ style: {
572
+ display: "flex",
573
+ alignItems: "flex-start",
574
+ gap: 8
575
+ },
576
+ children: [
577
+ /* @__PURE__ */ jsx("div", {
578
+ style: avatarStyle(comment.author || "U"),
579
+ children: getInitials(comment.author || "U")
580
+ }),
581
+ /* @__PURE__ */ jsxs("div", {
582
+ style: {
583
+ flex: 1,
584
+ minWidth: 0
585
+ },
586
+ children: [/* @__PURE__ */ jsx("div", {
587
+ style: {
588
+ fontSize: 12,
589
+ fontWeight: 600,
590
+ color: "var(--doc-text)"
591
+ },
592
+ children: comment.author || "Unknown"
593
+ }), /* @__PURE__ */ jsx("div", {
594
+ style: {
595
+ fontSize: 10,
596
+ color: "var(--doc-text-muted)"
597
+ },
598
+ children: formatDate(comment.date, locale)
599
+ })]
600
+ }),
601
+ isExpanded && /* @__PURE__ */ jsxs("div", {
602
+ style: {
603
+ display: "flex",
604
+ gap: 2,
605
+ marginTop: 1,
606
+ position: "relative"
607
+ },
608
+ children: [
609
+ /* @__PURE__ */ jsx("button", {
610
+ type: "button",
611
+ onClick: (e) => {
612
+ e.stopPropagation();
613
+ onCommentResolve?.(comment.id);
614
+ },
615
+ title: "Resolve",
616
+ style: ICON_BUTTON_STYLE,
617
+ children: /* @__PURE__ */ jsx(CheckIcon, { size: 16 })
618
+ }),
619
+ /* @__PURE__ */ jsx("button", {
620
+ type: "button",
621
+ onClick: (e) => {
622
+ e.stopPropagation();
623
+ setMenuOpenFor(menuOpenFor === cardId ? null : cardId);
624
+ },
625
+ title: "More options",
626
+ style: ICON_BUTTON_STYLE,
627
+ children: /* @__PURE__ */ jsx(MoreVerticalIcon, { size: 16 })
628
+ }),
629
+ menuOpenFor === cardId && /* @__PURE__ */ jsx("div", {
630
+ onClick: (e) => e.stopPropagation(),
631
+ onKeyDown: (e) => e.stopPropagation(),
632
+ onMouseDown: (e) => e.stopPropagation(),
633
+ role: "menu",
634
+ tabIndex: -1,
635
+ style: {
636
+ position: "absolute",
637
+ top: 28,
638
+ right: 0,
639
+ background: "var(--doc-page)",
640
+ borderRadius: 6,
641
+ boxShadow: "0 2px 6px var(--doc-shadow-md, rgba(60,64,67,0.3)), 0 1px 2px var(--doc-shadow-sm, rgba(60,64,67,0.15))",
642
+ zIndex: 100,
643
+ minWidth: 120,
644
+ padding: "4px 0"
645
+ },
646
+ children: /* @__PURE__ */ jsx("button", {
647
+ type: "button",
648
+ onClick: () => {
649
+ setMenuOpenFor(null);
650
+ onCommentDelete?.(comment.id);
651
+ },
652
+ style: {
653
+ display: "block",
654
+ width: "100%",
655
+ padding: "7px 12px",
656
+ border: "none",
657
+ background: "none",
658
+ textAlign: "left",
659
+ fontSize: 12,
660
+ color: "var(--doc-text)",
661
+ cursor: "pointer",
662
+ fontFamily: "inherit"
663
+ },
664
+ onMouseOver: (e) => {
665
+ e.currentTarget.style.backgroundColor = "var(--doc-primary-light)";
666
+ },
667
+ onFocus: (e) => {
668
+ e.currentTarget.style.backgroundColor = "var(--doc-primary-light)";
669
+ },
670
+ onMouseOut: (e) => {
671
+ e.currentTarget.style.backgroundColor = "transparent";
672
+ },
673
+ onBlur: (e) => {
674
+ e.currentTarget.style.backgroundColor = "transparent";
675
+ },
676
+ children: "Delete"
677
+ })
678
+ })
679
+ ]
680
+ })
681
+ ]
682
+ }),
683
+ /* @__PURE__ */ jsx("div", {
684
+ style: {
685
+ fontSize: 12,
686
+ color: "var(--doc-text)",
687
+ lineHeight: "17px",
688
+ marginTop: 5
689
+ },
690
+ children: getCommentText(comment.content)
691
+ }),
692
+ renderReplies(replies, isExpanded),
693
+ isExpanded && !comment.done && renderReplySection(comment.id, onCommentReply)
694
+ ]
695
+ }, comment.id);
696
+ };
697
+ return /* @__PURE__ */ jsx("aside", {
698
+ ref: sidebarRef,
699
+ className: "docx-comments-sidebar",
700
+ "aria-label": "Comments",
701
+ style: {
702
+ position: "absolute",
703
+ top: topOffset,
704
+ left: measuredLeft ?? `calc(50% - 120px + ${pageWidth / 2 + 12}px)`,
705
+ bottom: 0,
706
+ width: 280,
707
+ fontFamily: "inherit",
708
+ zIndex: 40,
709
+ backgroundColor: "transparent",
710
+ overflowY: "visible",
711
+ overflowX: "visible",
712
+ opacity: initialPositionsDone || cardPositions.size > 0 ? 1 : 0,
713
+ pointerEvents: initialPositionsDone || cardPositions.size > 0 ? "auto" : "none",
714
+ transition: "opacity 0.15s ease"
715
+ },
716
+ onMouseDown: containedHandler(sidebarRef, (e) => e.stopPropagation()),
717
+ children: /* @__PURE__ */ jsxs("div", {
718
+ style: { position: "relative" },
719
+ children: [
720
+ isAddingComment && /* @__PURE__ */ jsxs("div", {
721
+ ref: (el) => {
722
+ if (el) cardRefs.current.set("new-comment-input", el);
723
+ else cardRefs.current.delete("new-comment-input");
724
+ },
725
+ style: {
726
+ ...(() => {
727
+ const yPos = cardPositions.get("new-comment-input");
728
+ if (!hasPositions) return { marginBottom: 8 };
729
+ if (yPos !== void 0) return {
730
+ position: "absolute",
731
+ top: yPos,
732
+ left: 0,
733
+ right: 0
734
+ };
735
+ return {
736
+ position: "relative",
737
+ marginBottom: 8
738
+ };
739
+ })(),
740
+ padding: 10,
741
+ borderRadius: 6,
742
+ border: "1px solid var(--border, var(--doc-border))",
743
+ backgroundColor: "var(--popover, var(--doc-page))",
744
+ color: "var(--popover-foreground, var(--doc-text))",
745
+ boxShadow: "0 12px 36px var(--doc-shadow-md, rgba(0,0,0,0.28))",
746
+ zIndex: 50
747
+ },
748
+ children: [/* @__PURE__ */ jsx("textarea", {
749
+ ref: (el) => el?.focus({ preventScroll: true }),
750
+ value: newCommentText,
751
+ onChange: (e) => setNewCommentText(e.target.value),
752
+ onMouseDown: (e) => e.stopPropagation(),
753
+ onKeyDown: (e) => {
754
+ e.stopPropagation();
755
+ if (e.key === "Enter" && !e.shiftKey) {
756
+ e.preventDefault();
757
+ handleNewCommentSubmit();
758
+ }
759
+ if (e.key === "Escape") {
760
+ onCancelAddComment?.();
761
+ setNewCommentText("");
762
+ }
763
+ },
764
+ placeholder: t("comments.addPlaceholder"),
765
+ style: {
766
+ width: "100%",
767
+ border: "1px solid var(--input, var(--doc-border-input))",
768
+ borderRadius: 6,
769
+ outline: "none",
770
+ resize: "none",
771
+ fontSize: 13,
772
+ lineHeight: "18px",
773
+ padding: "8px 9px",
774
+ fontFamily: "inherit",
775
+ minHeight: 64,
776
+ boxSizing: "border-box",
777
+ color: "var(--foreground, var(--doc-text))",
778
+ background: "var(--background, var(--doc-page))"
779
+ }
780
+ }), /* @__PURE__ */ jsxs("div", {
781
+ style: {
782
+ display: "flex",
783
+ justifyContent: "flex-end",
784
+ gap: 8,
785
+ marginTop: 8
786
+ },
787
+ children: [/* @__PURE__ */ jsx("button", {
788
+ type: "button",
789
+ onClick: (e) => {
790
+ e.stopPropagation();
791
+ onCancelAddComment?.();
792
+ setNewCommentText("");
793
+ },
794
+ style: CANCEL_BUTTON_STYLE,
795
+ children: t("comments.cancel")
796
+ }), /* @__PURE__ */ jsx("button", {
797
+ type: "button",
798
+ onClick: (e) => {
799
+ e.stopPropagation();
800
+ handleNewCommentSubmit();
801
+ },
802
+ disabled: !newCommentText.trim(),
803
+ style: submitButtonStyle(!!newCommentText.trim()),
804
+ children: t("comment")
805
+ })]
806
+ })]
807
+ }),
808
+ visibleComments.map((comment) => renderCommentCard(comment)),
809
+ visibleComments.length === 0 && !isAddingComment && /* @__PURE__ */ jsx("div", {
810
+ style: {
811
+ padding: "24px 16px",
812
+ textAlign: "center",
813
+ color: "var(--doc-text-subtle)",
814
+ fontSize: 13
815
+ },
816
+ children: t("comments.noComments")
817
+ })
818
+ ]
819
+ })
820
+ });
821
+ };
822
+ //#endregion
823
+ export { CommentsSidebar };