apostil 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.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/bin/apostil.js +2 -0
- package/dist/adapters/localStorage.d.ts +5 -0
- package/dist/adapters/localStorage.js +23 -0
- package/dist/adapters/localStorage.js.map +1 -0
- package/dist/adapters/nextjs.d.ts +22 -0
- package/dist/adapters/nextjs.js +75 -0
- package/dist/adapters/nextjs.js.map +1 -0
- package/dist/adapters/rest.d.ts +9 -0
- package/dist/adapters/rest.js +8 -0
- package/dist/adapters/rest.js.map +1 -0
- package/dist/chunk-ASP7WAEG.js +42 -0
- package/dist/chunk-ASP7WAEG.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +233 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +1281 -0
- package/dist/index.js.map +1 -0
- package/dist/types-oQRt3lYH.d.ts +30 -0
- package/package.json +71 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1281 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
createRestAdapter
|
|
4
|
+
} from "./chunk-ASP7WAEG.js";
|
|
5
|
+
|
|
6
|
+
// src/context.tsx
|
|
7
|
+
import {
|
|
8
|
+
createContext,
|
|
9
|
+
useContext,
|
|
10
|
+
useState,
|
|
11
|
+
useEffect,
|
|
12
|
+
useCallback,
|
|
13
|
+
useRef
|
|
14
|
+
} from "react";
|
|
15
|
+
|
|
16
|
+
// src/utils.ts
|
|
17
|
+
var counter = 0;
|
|
18
|
+
function generateId() {
|
|
19
|
+
return `${Date.now()}-${++counter}-${Math.random().toString(36).slice(2, 7)}`;
|
|
20
|
+
}
|
|
21
|
+
var USER_KEY = "apostil-user";
|
|
22
|
+
function loadUser() {
|
|
23
|
+
if (typeof window === "undefined") return null;
|
|
24
|
+
try {
|
|
25
|
+
const raw = localStorage.getItem(USER_KEY);
|
|
26
|
+
return raw ? JSON.parse(raw) : null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function saveUser(user) {
|
|
32
|
+
if (typeof window === "undefined") return;
|
|
33
|
+
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
|
34
|
+
}
|
|
35
|
+
var USER_COLORS = [
|
|
36
|
+
"#df461c",
|
|
37
|
+
"#2563eb",
|
|
38
|
+
"#16a34a",
|
|
39
|
+
"#9333ea",
|
|
40
|
+
"#ea580c",
|
|
41
|
+
"#0891b2",
|
|
42
|
+
"#c026d3",
|
|
43
|
+
"#4f46e5",
|
|
44
|
+
"#059669",
|
|
45
|
+
"#dc2626"
|
|
46
|
+
];
|
|
47
|
+
function getRandomColor() {
|
|
48
|
+
return USER_COLORS[Math.floor(Math.random() * USER_COLORS.length)];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/debug.ts
|
|
52
|
+
var PREFIX = "[apostil]";
|
|
53
|
+
function isDebug() {
|
|
54
|
+
if (typeof window === "undefined") return false;
|
|
55
|
+
try {
|
|
56
|
+
return localStorage.getItem("apostil-debug") === "true";
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
var debug = {
|
|
62
|
+
log(...args) {
|
|
63
|
+
if (isDebug()) console.log(PREFIX, ...args);
|
|
64
|
+
},
|
|
65
|
+
warn(...args) {
|
|
66
|
+
if (isDebug()) console.warn(PREFIX, ...args);
|
|
67
|
+
},
|
|
68
|
+
error(...args) {
|
|
69
|
+
console.error(PREFIX, ...args);
|
|
70
|
+
},
|
|
71
|
+
/** Enable/disable debug logging */
|
|
72
|
+
enable() {
|
|
73
|
+
if (typeof window !== "undefined") {
|
|
74
|
+
localStorage.setItem("apostil-debug", "true");
|
|
75
|
+
console.log(PREFIX, "debug logging enabled \u2014 reload to take effect");
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
disable() {
|
|
79
|
+
if (typeof window !== "undefined") {
|
|
80
|
+
localStorage.removeItem("apostil-debug");
|
|
81
|
+
console.log(PREFIX, "debug logging disabled");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
if (typeof window !== "undefined") {
|
|
86
|
+
window.__apostil_debug = debug;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/context.tsx
|
|
90
|
+
import { jsx } from "react/jsx-runtime";
|
|
91
|
+
var defaultAdapter = createRestAdapter("/api/apostil");
|
|
92
|
+
var ApostilContext = createContext(null);
|
|
93
|
+
function ApostilProvider({
|
|
94
|
+
pageId,
|
|
95
|
+
storage,
|
|
96
|
+
children
|
|
97
|
+
}) {
|
|
98
|
+
const adapter = storage ?? defaultAdapter;
|
|
99
|
+
const [threads, setThreads] = useState([]);
|
|
100
|
+
const [user, setUserState] = useState(null);
|
|
101
|
+
const [commentMode, setCommentMode] = useState(false);
|
|
102
|
+
const [activeThreadId, setActiveThreadId] = useState(null);
|
|
103
|
+
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
104
|
+
const [loaded, setLoaded] = useState(false);
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const saved = loadUser();
|
|
107
|
+
if (saved) setUserState(saved);
|
|
108
|
+
}, []);
|
|
109
|
+
const pageIdRef = useRef(pageId);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
pageIdRef.current = pageId;
|
|
112
|
+
setLoaded(false);
|
|
113
|
+
debug.log("loading threads for pageId:", pageId);
|
|
114
|
+
adapter.load(pageId).then((t) => {
|
|
115
|
+
if (pageIdRef.current === pageId) {
|
|
116
|
+
debug.log("loaded", t.length, "threads for", pageId);
|
|
117
|
+
setThreads(t);
|
|
118
|
+
setLoaded(true);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}, [pageId]);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (loaded && pageIdRef.current === pageId && threads.length > 0) {
|
|
124
|
+
debug.log("saving", threads.length, "threads for pageId:", pageId);
|
|
125
|
+
adapter.save(pageId, threads);
|
|
126
|
+
}
|
|
127
|
+
}, [threads, pageId, loaded]);
|
|
128
|
+
const setUser = useCallback((name) => {
|
|
129
|
+
const u = { id: generateId(), name, color: getRandomColor() };
|
|
130
|
+
setUserState(u);
|
|
131
|
+
saveUser(u);
|
|
132
|
+
}, []);
|
|
133
|
+
const addThread = useCallback(
|
|
134
|
+
(pinX, pinY, body, targetId, targetLabel) => {
|
|
135
|
+
if (!user) return;
|
|
136
|
+
const threadId = generateId();
|
|
137
|
+
const thread = {
|
|
138
|
+
id: threadId,
|
|
139
|
+
pageId,
|
|
140
|
+
pinX,
|
|
141
|
+
pinY,
|
|
142
|
+
targetId,
|
|
143
|
+
targetLabel,
|
|
144
|
+
resolved: false,
|
|
145
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
146
|
+
comments: [{
|
|
147
|
+
id: generateId(),
|
|
148
|
+
threadId,
|
|
149
|
+
author: user,
|
|
150
|
+
body,
|
|
151
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
152
|
+
}]
|
|
153
|
+
};
|
|
154
|
+
debug.log("new thread:", { threadId, pinX, pinY, targetId, targetLabel, body });
|
|
155
|
+
setThreads((prev) => [...prev, thread]);
|
|
156
|
+
setActiveThreadId(threadId);
|
|
157
|
+
setCommentMode(false);
|
|
158
|
+
},
|
|
159
|
+
[user, pageId]
|
|
160
|
+
);
|
|
161
|
+
const addReply = useCallback(
|
|
162
|
+
(threadId, body) => {
|
|
163
|
+
if (!user) return;
|
|
164
|
+
setThreads(
|
|
165
|
+
(prev) => prev.map(
|
|
166
|
+
(t) => t.id === threadId ? { ...t, comments: [...t.comments, { id: generateId(), threadId, author: user, body, createdAt: (/* @__PURE__ */ new Date()).toISOString() }] } : t
|
|
167
|
+
)
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
[user]
|
|
171
|
+
);
|
|
172
|
+
const resolveThread = useCallback((threadId) => {
|
|
173
|
+
setThreads((prev) => prev.map((t) => t.id === threadId ? { ...t, resolved: !t.resolved } : t));
|
|
174
|
+
setActiveThreadId(null);
|
|
175
|
+
}, []);
|
|
176
|
+
const deleteThread = useCallback((threadId) => {
|
|
177
|
+
setThreads((prev) => prev.filter((t) => t.id !== threadId));
|
|
178
|
+
setActiveThreadId(null);
|
|
179
|
+
}, []);
|
|
180
|
+
const unresolvedCount = threads.filter((t) => !t.resolved).length;
|
|
181
|
+
return /* @__PURE__ */ jsx(ApostilContext.Provider, { value: {
|
|
182
|
+
threads,
|
|
183
|
+
user,
|
|
184
|
+
commentMode,
|
|
185
|
+
activeThreadId,
|
|
186
|
+
sidebarOpen,
|
|
187
|
+
setCommentMode,
|
|
188
|
+
setActiveThreadId,
|
|
189
|
+
setSidebarOpen,
|
|
190
|
+
addThread,
|
|
191
|
+
addReply,
|
|
192
|
+
resolveThread,
|
|
193
|
+
deleteThread,
|
|
194
|
+
setUser,
|
|
195
|
+
unresolvedCount
|
|
196
|
+
}, children });
|
|
197
|
+
}
|
|
198
|
+
function useApostil() {
|
|
199
|
+
const ctx = useContext(ApostilContext);
|
|
200
|
+
if (!ctx) throw new Error("useApostil must be used within ApostilProvider");
|
|
201
|
+
return ctx;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/hooks/use-comments.ts
|
|
205
|
+
function useComments() {
|
|
206
|
+
const { threads, addThread, addReply, resolveThread, deleteThread, unresolvedCount } = useApostil();
|
|
207
|
+
return {
|
|
208
|
+
threads,
|
|
209
|
+
openThreads: threads.filter((t) => !t.resolved),
|
|
210
|
+
resolvedThreads: threads.filter((t) => t.resolved),
|
|
211
|
+
addThread,
|
|
212
|
+
addReply,
|
|
213
|
+
resolveThread,
|
|
214
|
+
deleteThread,
|
|
215
|
+
unresolvedCount
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/hooks/use-comment-mode.ts
|
|
220
|
+
function useCommentMode() {
|
|
221
|
+
const { commentMode, setCommentMode, sidebarOpen, setSidebarOpen } = useApostil();
|
|
222
|
+
return {
|
|
223
|
+
commentMode,
|
|
224
|
+
setCommentMode,
|
|
225
|
+
toggleCommentMode: () => setCommentMode(!commentMode),
|
|
226
|
+
sidebarOpen,
|
|
227
|
+
setSidebarOpen,
|
|
228
|
+
toggleSidebar: () => setSidebarOpen(!sidebarOpen)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/components/comment-overlay.tsx
|
|
233
|
+
import { useState as useState6, useCallback as useCallback4, useRef as useRef4, useEffect as useEffect5 } from "react";
|
|
234
|
+
|
|
235
|
+
// src/components/comment-pin.tsx
|
|
236
|
+
import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
|
|
237
|
+
import { createPortal } from "react-dom";
|
|
238
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
239
|
+
function findTargetElement(targetId) {
|
|
240
|
+
try {
|
|
241
|
+
const el = document.querySelector(targetId);
|
|
242
|
+
if (el instanceof HTMLElement) return el;
|
|
243
|
+
} catch {
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const el = document.querySelector(`[data-comment-target="${CSS.escape(targetId)}"]`);
|
|
247
|
+
if (el instanceof HTMLElement) return el;
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
function resolveOverlayPosition(thread, overlayEl) {
|
|
253
|
+
if (!overlayEl) return null;
|
|
254
|
+
const overlayRect = overlayEl.getBoundingClientRect();
|
|
255
|
+
return {
|
|
256
|
+
left: thread.pinX / 100 * overlayRect.width,
|
|
257
|
+
top: thread.pinY / 100 * overlayRect.height
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function resolvePosition(thread, overlayEl) {
|
|
261
|
+
if (!overlayEl) return null;
|
|
262
|
+
const overlayRect = overlayEl.getBoundingClientRect();
|
|
263
|
+
if (thread.targetId) {
|
|
264
|
+
const target = findTargetElement(thread.targetId);
|
|
265
|
+
if (target) {
|
|
266
|
+
const targetRect = target.getBoundingClientRect();
|
|
267
|
+
return {
|
|
268
|
+
left: targetRect.left - overlayRect.left + thread.pinX / 100 * targetRect.width,
|
|
269
|
+
top: targetRect.top - overlayRect.top + thread.pinY / 100 * targetRect.height
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
left: thread.pinX / 100 * overlayRect.width,
|
|
276
|
+
top: thread.pinY / 100 * overlayRect.height
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function PinButton({
|
|
280
|
+
thread,
|
|
281
|
+
index,
|
|
282
|
+
isActive,
|
|
283
|
+
onClick
|
|
284
|
+
}) {
|
|
285
|
+
const authorColor = thread.comments[0]?.author.color ?? "#df461c";
|
|
286
|
+
return /* @__PURE__ */ jsxs("button", { onClick, className: "group", style: { position: "relative" }, children: [
|
|
287
|
+
/* @__PURE__ */ jsx2(
|
|
288
|
+
"div",
|
|
289
|
+
{
|
|
290
|
+
className: `
|
|
291
|
+
flex items-center justify-center
|
|
292
|
+
w-7 h-7 rounded-full text-white text-xs font-semibold
|
|
293
|
+
shadow-lg cursor-pointer
|
|
294
|
+
transition-all duration-200
|
|
295
|
+
${isActive ? "scale-125 ring-2 ring-white ring-offset-2" : "hover:scale-110"}
|
|
296
|
+
${thread.resolved ? "opacity-40" : ""}
|
|
297
|
+
`,
|
|
298
|
+
style: { backgroundColor: authorColor },
|
|
299
|
+
children: index + 1
|
|
300
|
+
}
|
|
301
|
+
),
|
|
302
|
+
!thread.resolved && !isActive && /* @__PURE__ */ jsx2(
|
|
303
|
+
"div",
|
|
304
|
+
{
|
|
305
|
+
className: "absolute inset-0 rounded-full animate-ping opacity-20",
|
|
306
|
+
style: { backgroundColor: authorColor }
|
|
307
|
+
}
|
|
308
|
+
),
|
|
309
|
+
thread.targetLabel && /* @__PURE__ */ jsx2("div", { className: "absolute -bottom-6 left-1/2 -translate-x-1/2 whitespace-nowrap\n opacity-0 group-hover:opacity-100 transition-opacity\n text-[10px] bg-neutral-800 text-white px-1.5 py-0.5 rounded pointer-events-none", children: thread.targetLabel })
|
|
310
|
+
] });
|
|
311
|
+
}
|
|
312
|
+
function TargetedPin({
|
|
313
|
+
thread,
|
|
314
|
+
index
|
|
315
|
+
}) {
|
|
316
|
+
const { activeThreadId, setActiveThreadId } = useApostil();
|
|
317
|
+
const isActive = activeThreadId === thread.id;
|
|
318
|
+
const [targetEl, setTargetEl] = useState2(null);
|
|
319
|
+
useEffect2(() => {
|
|
320
|
+
if (!thread.targetId) return;
|
|
321
|
+
function tryFind() {
|
|
322
|
+
const el = findTargetElement(thread.targetId);
|
|
323
|
+
if (el) {
|
|
324
|
+
const pos = getComputedStyle(el).position;
|
|
325
|
+
if (pos === "static") el.style.position = "relative";
|
|
326
|
+
setTargetEl(el);
|
|
327
|
+
} else {
|
|
328
|
+
setTargetEl(null);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
tryFind();
|
|
332
|
+
const observer = new MutationObserver(() => tryFind());
|
|
333
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
334
|
+
return () => observer.disconnect();
|
|
335
|
+
}, [thread.targetId]);
|
|
336
|
+
if (!targetEl) return null;
|
|
337
|
+
return createPortal(
|
|
338
|
+
/* @__PURE__ */ jsx2(
|
|
339
|
+
"div",
|
|
340
|
+
{
|
|
341
|
+
className: "absolute pointer-events-auto",
|
|
342
|
+
style: {
|
|
343
|
+
left: `${thread.pinX}%`,
|
|
344
|
+
top: `${thread.pinY}%`,
|
|
345
|
+
transform: "translate(-50%, -50%)",
|
|
346
|
+
zIndex: 10
|
|
347
|
+
},
|
|
348
|
+
children: /* @__PURE__ */ jsx2(
|
|
349
|
+
PinButton,
|
|
350
|
+
{
|
|
351
|
+
thread,
|
|
352
|
+
index,
|
|
353
|
+
isActive,
|
|
354
|
+
onClick: (e) => {
|
|
355
|
+
e.stopPropagation();
|
|
356
|
+
setActiveThreadId(isActive ? null : thread.id);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
),
|
|
362
|
+
targetEl
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
function OverlayPin({
|
|
366
|
+
thread,
|
|
367
|
+
index,
|
|
368
|
+
overlayRef
|
|
369
|
+
}) {
|
|
370
|
+
const { activeThreadId, setActiveThreadId } = useApostil();
|
|
371
|
+
const isActive = activeThreadId === thread.id;
|
|
372
|
+
const [pos, setPos] = useState2(null);
|
|
373
|
+
const updatePos = useCallback2(() => {
|
|
374
|
+
setPos(resolveOverlayPosition(thread, overlayRef.current));
|
|
375
|
+
}, [thread, overlayRef]);
|
|
376
|
+
useEffect2(() => {
|
|
377
|
+
updatePos();
|
|
378
|
+
window.addEventListener("resize", updatePos);
|
|
379
|
+
return () => window.removeEventListener("resize", updatePos);
|
|
380
|
+
}, [updatePos]);
|
|
381
|
+
if (!pos) return null;
|
|
382
|
+
return /* @__PURE__ */ jsx2(
|
|
383
|
+
"div",
|
|
384
|
+
{
|
|
385
|
+
className: "absolute pointer-events-auto",
|
|
386
|
+
style: {
|
|
387
|
+
left: pos.left,
|
|
388
|
+
top: pos.top,
|
|
389
|
+
transform: "translate(-50%, -50%)",
|
|
390
|
+
zIndex: 60
|
|
391
|
+
},
|
|
392
|
+
children: /* @__PURE__ */ jsx2(
|
|
393
|
+
PinButton,
|
|
394
|
+
{
|
|
395
|
+
thread,
|
|
396
|
+
index,
|
|
397
|
+
isActive,
|
|
398
|
+
onClick: (e) => {
|
|
399
|
+
e.stopPropagation();
|
|
400
|
+
setActiveThreadId(isActive ? null : thread.id);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
function CommentPin({
|
|
408
|
+
thread,
|
|
409
|
+
index,
|
|
410
|
+
overlayRef
|
|
411
|
+
}) {
|
|
412
|
+
if (thread.targetId) {
|
|
413
|
+
return /* @__PURE__ */ jsx2(TargetedPin, { thread, index }, `targeted-${thread.id}`);
|
|
414
|
+
}
|
|
415
|
+
return /* @__PURE__ */ jsx2(OverlayPin, { thread, index, overlayRef }, `overlay-${thread.id}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/components/comment-thread.tsx
|
|
419
|
+
import { useEffect as useEffect4, useRef as useRef3, useState as useState4, useCallback as useCallback3 } from "react";
|
|
420
|
+
import { Check, Trash2, Undo2 } from "lucide-react";
|
|
421
|
+
|
|
422
|
+
// src/components/comment-composer.tsx
|
|
423
|
+
import { useState as useState3, useRef as useRef2, useEffect as useEffect3 } from "react";
|
|
424
|
+
import { Send } from "lucide-react";
|
|
425
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
426
|
+
function CommentComposer({
|
|
427
|
+
onSubmit,
|
|
428
|
+
placeholder = "Add a comment...",
|
|
429
|
+
autoFocus = false
|
|
430
|
+
}) {
|
|
431
|
+
const [value, setValue] = useState3("");
|
|
432
|
+
const inputRef = useRef2(null);
|
|
433
|
+
useEffect3(() => {
|
|
434
|
+
if (autoFocus) {
|
|
435
|
+
inputRef.current?.focus();
|
|
436
|
+
}
|
|
437
|
+
}, [autoFocus]);
|
|
438
|
+
const handleSubmit = () => {
|
|
439
|
+
const trimmed = value.trim();
|
|
440
|
+
if (!trimmed) return;
|
|
441
|
+
onSubmit(trimmed);
|
|
442
|
+
setValue("");
|
|
443
|
+
};
|
|
444
|
+
return /* @__PURE__ */ jsxs2("div", { className: "flex gap-2 items-end", children: [
|
|
445
|
+
/* @__PURE__ */ jsx3(
|
|
446
|
+
"textarea",
|
|
447
|
+
{
|
|
448
|
+
ref: inputRef,
|
|
449
|
+
value,
|
|
450
|
+
onChange: (e) => setValue(e.target.value),
|
|
451
|
+
onKeyDown: (e) => {
|
|
452
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
453
|
+
e.preventDefault();
|
|
454
|
+
handleSubmit();
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
placeholder,
|
|
458
|
+
rows: 1,
|
|
459
|
+
className: "flex-1 resize-none rounded-lg border border-neutral-200 bg-white px-3 py-2 text-sm\n placeholder:text-neutral-400 focus:outline-none focus:ring-2 focus:ring-neutral-300\n min-h-[36px] max-h-[120px]"
|
|
460
|
+
}
|
|
461
|
+
),
|
|
462
|
+
/* @__PURE__ */ jsx3(
|
|
463
|
+
"button",
|
|
464
|
+
{
|
|
465
|
+
onClick: handleSubmit,
|
|
466
|
+
disabled: !value.trim(),
|
|
467
|
+
className: "flex items-center justify-center w-8 h-8 rounded-lg\n bg-neutral-900 text-white disabled:opacity-30\n hover:bg-neutral-700 transition-colors shrink-0",
|
|
468
|
+
children: /* @__PURE__ */ jsx3(Send, { className: "w-3.5 h-3.5" })
|
|
469
|
+
}
|
|
470
|
+
)
|
|
471
|
+
] });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/components/comment-thread.tsx
|
|
475
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
476
|
+
function timeAgo(iso) {
|
|
477
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
478
|
+
const mins = Math.floor(diff / 6e4);
|
|
479
|
+
if (mins < 1) return "just now";
|
|
480
|
+
if (mins < 60) return `${mins}m ago`;
|
|
481
|
+
const hours = Math.floor(mins / 60);
|
|
482
|
+
if (hours < 24) return `${hours}h ago`;
|
|
483
|
+
const days = Math.floor(hours / 24);
|
|
484
|
+
return `${days}d ago`;
|
|
485
|
+
}
|
|
486
|
+
function ApostilThreadPopover({
|
|
487
|
+
thread,
|
|
488
|
+
overlayRef
|
|
489
|
+
}) {
|
|
490
|
+
const { activeThreadId, setActiveThreadId, addReply, resolveThread, deleteThread, user } = useApostil();
|
|
491
|
+
const ref = useRef3(null);
|
|
492
|
+
const isOpen = activeThreadId === thread.id;
|
|
493
|
+
const [pos, setPos] = useState4(null);
|
|
494
|
+
const updatePos = useCallback3(() => {
|
|
495
|
+
setPos(resolvePosition(thread, overlayRef.current));
|
|
496
|
+
}, [thread, overlayRef]);
|
|
497
|
+
useEffect4(() => {
|
|
498
|
+
if (!isOpen) return;
|
|
499
|
+
updatePos();
|
|
500
|
+
window.addEventListener("resize", updatePos);
|
|
501
|
+
document.addEventListener("scroll", updatePos, true);
|
|
502
|
+
return () => {
|
|
503
|
+
window.removeEventListener("resize", updatePos);
|
|
504
|
+
document.removeEventListener("scroll", updatePos, true);
|
|
505
|
+
};
|
|
506
|
+
}, [isOpen, updatePos]);
|
|
507
|
+
useEffect4(() => {
|
|
508
|
+
if (!isOpen) return;
|
|
509
|
+
const handler = (e) => {
|
|
510
|
+
if (ref.current && !ref.current.contains(e.target)) {
|
|
511
|
+
setActiveThreadId(null);
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
const timer = setTimeout(() => document.addEventListener("mousedown", handler), 0);
|
|
515
|
+
return () => {
|
|
516
|
+
clearTimeout(timer);
|
|
517
|
+
document.removeEventListener("mousedown", handler);
|
|
518
|
+
};
|
|
519
|
+
}, [isOpen, setActiveThreadId]);
|
|
520
|
+
if (!isOpen || !pos) return null;
|
|
521
|
+
return /* @__PURE__ */ jsx4(
|
|
522
|
+
"div",
|
|
523
|
+
{
|
|
524
|
+
ref,
|
|
525
|
+
className: "absolute z-[70] ml-5 -mt-3",
|
|
526
|
+
style: { left: pos.left, top: pos.top },
|
|
527
|
+
onClick: (e) => e.stopPropagation(),
|
|
528
|
+
children: /* @__PURE__ */ jsxs3("div", { className: "w-80 bg-white rounded-xl shadow-2xl border border-neutral-200 overflow-hidden", children: [
|
|
529
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between px-4 py-2.5 border-b border-neutral-100 bg-neutral-50", children: [
|
|
530
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
|
|
531
|
+
/* @__PURE__ */ jsxs3("span", { className: "text-xs font-medium text-neutral-500", children: [
|
|
532
|
+
thread.comments.length,
|
|
533
|
+
" ",
|
|
534
|
+
thread.comments.length === 1 ? "comment" : "comments"
|
|
535
|
+
] }),
|
|
536
|
+
thread.targetLabel && /* @__PURE__ */ jsx4("span", { className: "text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-medium", children: thread.targetLabel }),
|
|
537
|
+
thread.resolved && /* @__PURE__ */ jsx4("span", { className: "text-[10px] text-emerald-600 font-medium", children: "Resolved" })
|
|
538
|
+
] }),
|
|
539
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex gap-1", children: [
|
|
540
|
+
/* @__PURE__ */ jsx4(
|
|
541
|
+
"button",
|
|
542
|
+
{
|
|
543
|
+
onClick: () => resolveThread(thread.id),
|
|
544
|
+
className: "p-1 rounded hover:bg-neutral-200 transition-colors",
|
|
545
|
+
title: thread.resolved ? "Reopen" : "Resolve",
|
|
546
|
+
children: thread.resolved ? /* @__PURE__ */ jsx4(Undo2, { className: "w-3.5 h-3.5 text-neutral-500" }) : /* @__PURE__ */ jsx4(Check, { className: "w-3.5 h-3.5 text-emerald-600" })
|
|
547
|
+
}
|
|
548
|
+
),
|
|
549
|
+
/* @__PURE__ */ jsx4(
|
|
550
|
+
"button",
|
|
551
|
+
{
|
|
552
|
+
onClick: () => deleteThread(thread.id),
|
|
553
|
+
className: "p-1 rounded hover:bg-red-50 transition-colors",
|
|
554
|
+
title: "Delete thread",
|
|
555
|
+
children: /* @__PURE__ */ jsx4(Trash2, { className: "w-3.5 h-3.5 text-neutral-400 hover:text-red-500" })
|
|
556
|
+
}
|
|
557
|
+
)
|
|
558
|
+
] })
|
|
559
|
+
] }),
|
|
560
|
+
/* @__PURE__ */ jsx4("div", { className: "max-h-64 overflow-y-auto", children: thread.comments.map((comment) => /* @__PURE__ */ jsxs3("div", { className: "px-4 py-3 border-b border-neutral-50 last:border-0", children: [
|
|
561
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 mb-1", children: [
|
|
562
|
+
/* @__PURE__ */ jsx4(
|
|
563
|
+
"div",
|
|
564
|
+
{
|
|
565
|
+
className: "w-5 h-5 rounded-full flex items-center justify-center text-white text-[10px] font-semibold shrink-0",
|
|
566
|
+
style: { backgroundColor: comment.author.color },
|
|
567
|
+
children: comment.author.name[0]?.toUpperCase()
|
|
568
|
+
}
|
|
569
|
+
),
|
|
570
|
+
/* @__PURE__ */ jsx4("span", { className: "text-xs font-medium text-neutral-800", children: comment.author.name }),
|
|
571
|
+
/* @__PURE__ */ jsx4("span", { className: "text-[10px] text-neutral-400 ml-auto", children: timeAgo(comment.createdAt) })
|
|
572
|
+
] }),
|
|
573
|
+
/* @__PURE__ */ jsx4("p", { className: "text-sm text-neutral-700 leading-relaxed pl-7", children: comment.body })
|
|
574
|
+
] }, comment.id)) }),
|
|
575
|
+
user && !thread.resolved && /* @__PURE__ */ jsx4("div", { className: "px-3 py-2.5 border-t border-neutral-100 bg-neutral-50/50", children: /* @__PURE__ */ jsx4(
|
|
576
|
+
CommentComposer,
|
|
577
|
+
{
|
|
578
|
+
onSubmit: (body) => addReply(thread.id, body),
|
|
579
|
+
placeholder: "Reply...",
|
|
580
|
+
autoFocus: true
|
|
581
|
+
}
|
|
582
|
+
) })
|
|
583
|
+
] })
|
|
584
|
+
}
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/components/user-prompt.tsx
|
|
589
|
+
import { useState as useState5 } from "react";
|
|
590
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
591
|
+
function UserPrompt() {
|
|
592
|
+
const { user, setUser, commentMode } = useApostil();
|
|
593
|
+
const [name, setName] = useState5("");
|
|
594
|
+
if (user || !commentMode) return null;
|
|
595
|
+
const handleSubmit = () => {
|
|
596
|
+
const trimmed = name.trim();
|
|
597
|
+
if (!trimmed) return;
|
|
598
|
+
setUser(trimmed);
|
|
599
|
+
};
|
|
600
|
+
return /* @__PURE__ */ jsx5("div", { className: "absolute inset-0 z-[80] flex items-center justify-center bg-black/20 backdrop-blur-[2px]", children: /* @__PURE__ */ jsxs4("div", { className: "bg-white rounded-xl shadow-2xl border border-neutral-200 p-6 w-80", children: [
|
|
601
|
+
/* @__PURE__ */ jsx5("h3", { className: "text-sm font-semibold text-neutral-900 mb-1", children: "What's your name?" }),
|
|
602
|
+
/* @__PURE__ */ jsx5("p", { className: "text-xs text-neutral-500 mb-4", children: "This will be shown with your comments." }),
|
|
603
|
+
/* @__PURE__ */ jsx5(
|
|
604
|
+
"input",
|
|
605
|
+
{
|
|
606
|
+
value: name,
|
|
607
|
+
onChange: (e) => setName(e.target.value),
|
|
608
|
+
onKeyDown: (e) => {
|
|
609
|
+
if (e.key === "Enter") handleSubmit();
|
|
610
|
+
},
|
|
611
|
+
placeholder: "Enter your name",
|
|
612
|
+
autoFocus: true,
|
|
613
|
+
className: "w-full rounded-lg border border-neutral-200 px-3 py-2 text-sm\n placeholder:text-neutral-400 focus:outline-none focus:ring-2 focus:ring-neutral-300 mb-3"
|
|
614
|
+
}
|
|
615
|
+
),
|
|
616
|
+
/* @__PURE__ */ jsx5(
|
|
617
|
+
"button",
|
|
618
|
+
{
|
|
619
|
+
onClick: handleSubmit,
|
|
620
|
+
disabled: !name.trim(),
|
|
621
|
+
className: "w-full py-2 rounded-lg bg-neutral-900 text-white text-sm font-medium\n disabled:opacity-30 hover:bg-neutral-700 transition-colors",
|
|
622
|
+
children: "Continue"
|
|
623
|
+
}
|
|
624
|
+
)
|
|
625
|
+
] }) });
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/components/comment-overlay.tsx
|
|
629
|
+
import { Fragment, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
630
|
+
var cachedHighZ = 0;
|
|
631
|
+
var cacheTimestamp = 0;
|
|
632
|
+
function getHighestZIndex() {
|
|
633
|
+
const now = Date.now();
|
|
634
|
+
if (now - cacheTimestamp < 500) return cachedHighZ;
|
|
635
|
+
let max = 0;
|
|
636
|
+
const els = document.querySelectorAll("[style*='z-index'], [class*='z-']");
|
|
637
|
+
for (let i = 0; i < els.length; i++) {
|
|
638
|
+
const z = parseInt(getComputedStyle(els[i]).zIndex, 10);
|
|
639
|
+
if (!isNaN(z) && z > max) max = z;
|
|
640
|
+
}
|
|
641
|
+
cachedHighZ = Math.max(max, 100);
|
|
642
|
+
cacheTimestamp = now;
|
|
643
|
+
return cachedHighZ;
|
|
644
|
+
}
|
|
645
|
+
var SEMANTIC_TAGS = /* @__PURE__ */ new Set([
|
|
646
|
+
"SECTION",
|
|
647
|
+
"NAV",
|
|
648
|
+
"ASIDE",
|
|
649
|
+
"HEADER",
|
|
650
|
+
"FOOTER",
|
|
651
|
+
"MAIN",
|
|
652
|
+
"ARTICLE",
|
|
653
|
+
"FORM",
|
|
654
|
+
"DIALOG",
|
|
655
|
+
"DETAILS"
|
|
656
|
+
]);
|
|
657
|
+
var MIN_TARGET_SIZE = 50;
|
|
658
|
+
var MAX_VIEWPORT_RATIO = 0.85;
|
|
659
|
+
function isVisualPanel(el) {
|
|
660
|
+
const style = getComputedStyle(el);
|
|
661
|
+
if (/auto|scroll/.test(style.overflow + style.overflowY + style.overflowX)) return true;
|
|
662
|
+
if (style.borderWidth && style.borderWidth !== "0px" && style.borderStyle !== "none") return true;
|
|
663
|
+
if (style.borderRadius && style.borderRadius !== "0px") return true;
|
|
664
|
+
if (style.backgroundColor && style.backgroundColor !== "rgba(0, 0, 0, 0)" && style.backgroundColor !== "transparent") return true;
|
|
665
|
+
if (style.boxShadow && style.boxShadow !== "none") return true;
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
function inferLabel(el) {
|
|
669
|
+
const heading = el.querySelector("h1, h2, h3, h4, h5, h6, [class*='title'], [class*='heading']");
|
|
670
|
+
if (heading) {
|
|
671
|
+
const text = heading.textContent?.trim();
|
|
672
|
+
if (text && text.length <= 40) return text;
|
|
673
|
+
}
|
|
674
|
+
const firstText = el.querySelector("span, p, label, strong");
|
|
675
|
+
if (firstText) {
|
|
676
|
+
const text = firstText.textContent?.trim();
|
|
677
|
+
if (text && text.length <= 30) return text;
|
|
678
|
+
}
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
function getElementId(el) {
|
|
682
|
+
const manual = el.getAttribute("data-comment-target");
|
|
683
|
+
if (manual) return manual;
|
|
684
|
+
if (el.id) return `#${el.id}`;
|
|
685
|
+
const label = el.getAttribute("aria-label");
|
|
686
|
+
if (label) return `${el.tagName.toLowerCase()}[aria-label="${label}"]`;
|
|
687
|
+
const parts = [];
|
|
688
|
+
let cur = el;
|
|
689
|
+
for (let depth = 0; cur && depth < 5; depth++) {
|
|
690
|
+
if (cur.id) {
|
|
691
|
+
parts.unshift(`#${cur.id}`);
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
const p = cur.parentElement;
|
|
695
|
+
if (p) {
|
|
696
|
+
const siblings = Array.from(p.children);
|
|
697
|
+
const idx = siblings.indexOf(cur);
|
|
698
|
+
parts.unshift(`${cur.tagName.toLowerCase()}:nth-child(${idx + 1})`);
|
|
699
|
+
} else {
|
|
700
|
+
parts.unshift(cur.tagName.toLowerCase());
|
|
701
|
+
}
|
|
702
|
+
cur = p;
|
|
703
|
+
}
|
|
704
|
+
return parts.join(" > ");
|
|
705
|
+
}
|
|
706
|
+
function getElementLabel(el) {
|
|
707
|
+
const manual = el.getAttribute("data-comment-label");
|
|
708
|
+
if (manual) return manual;
|
|
709
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
710
|
+
if (ariaLabel) return ariaLabel;
|
|
711
|
+
const inferred = inferLabel(el);
|
|
712
|
+
if (inferred) return inferred;
|
|
713
|
+
const role = el.getAttribute("role");
|
|
714
|
+
if (el.id) {
|
|
715
|
+
const pretty = el.id.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
716
|
+
return role ? `${pretty} (${role})` : pretty;
|
|
717
|
+
}
|
|
718
|
+
const tag = el.tagName.toLowerCase();
|
|
719
|
+
if (SEMANTIC_TAGS.has(el.tagName)) return role ? `${tag} (${role})` : tag;
|
|
720
|
+
if (role) return role;
|
|
721
|
+
return tag;
|
|
722
|
+
}
|
|
723
|
+
function findCommentTarget(el, boundary) {
|
|
724
|
+
let current = el;
|
|
725
|
+
const candidates = [];
|
|
726
|
+
let depth = 0;
|
|
727
|
+
while (current && current !== boundary && current !== document.body) {
|
|
728
|
+
const rect = current.getBoundingClientRect();
|
|
729
|
+
const isBigEnough = rect.width >= MIN_TARGET_SIZE && rect.height >= MIN_TARGET_SIZE;
|
|
730
|
+
const isTooBig = rect.width > window.innerWidth * MAX_VIEWPORT_RATIO && rect.height > window.innerHeight * MAX_VIEWPORT_RATIO;
|
|
731
|
+
if (isBigEnough && !isTooBig) {
|
|
732
|
+
let score = 0;
|
|
733
|
+
if (current.getAttribute("data-comment-target")) score = 100;
|
|
734
|
+
else if (current.id) score = 80;
|
|
735
|
+
else if (current.getAttribute("aria-label")) score = 70;
|
|
736
|
+
else if (current.getAttribute("role")) score = 60;
|
|
737
|
+
else if (SEMANTIC_TAGS.has(current.tagName)) score = 50;
|
|
738
|
+
else if (isVisualPanel(current)) score = 40;
|
|
739
|
+
if (score > 0) {
|
|
740
|
+
candidates.push({ el: current, score, depth });
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
current = current.parentElement;
|
|
744
|
+
depth++;
|
|
745
|
+
}
|
|
746
|
+
if (candidates.length === 0) return null;
|
|
747
|
+
candidates.sort((a, b) => {
|
|
748
|
+
const scoreA = a.score + Math.max(0, 10 - a.depth);
|
|
749
|
+
const scoreB = b.score + Math.max(0, 10 - b.depth);
|
|
750
|
+
return scoreB - scoreA;
|
|
751
|
+
});
|
|
752
|
+
const best = candidates[0];
|
|
753
|
+
const targetId = getElementId(best.el);
|
|
754
|
+
const targetLabel = getElementLabel(best.el);
|
|
755
|
+
debug.log(" candidates:", candidates.map((c) => ({
|
|
756
|
+
tag: c.el.tagName.toLowerCase(),
|
|
757
|
+
id: c.el.id || void 0,
|
|
758
|
+
class: c.el.className?.toString().slice(0, 60) || void 0,
|
|
759
|
+
score: c.score,
|
|
760
|
+
depth: c.depth,
|
|
761
|
+
label: getElementLabel(c.el)
|
|
762
|
+
})));
|
|
763
|
+
return {
|
|
764
|
+
targetId,
|
|
765
|
+
targetLabel,
|
|
766
|
+
element: best.el
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function CommentOverlay() {
|
|
770
|
+
const { threads, commentMode, setCommentMode, user, addThread, setActiveThreadId } = useApostil();
|
|
771
|
+
const overlayRef = useRef4(null);
|
|
772
|
+
const [pendingPin, setPendingPin] = useState6(null);
|
|
773
|
+
const [pendingPixel, setPendingPixel] = useState6(null);
|
|
774
|
+
const handleClick = useCallback4(
|
|
775
|
+
(e) => {
|
|
776
|
+
if (!commentMode || !overlayRef.current) return;
|
|
777
|
+
const overlayRect = overlayRef.current.getBoundingClientRect();
|
|
778
|
+
const overlay = overlayRef.current;
|
|
779
|
+
overlay.style.pointerEvents = "none";
|
|
780
|
+
const elementBelow = document.elementFromPoint(e.clientX, e.clientY);
|
|
781
|
+
overlay.style.pointerEvents = "";
|
|
782
|
+
debug.log(" click at", { clientX: e.clientX, clientY: e.clientY });
|
|
783
|
+
debug.log(" element below overlay:", elementBelow);
|
|
784
|
+
if (elementBelow) {
|
|
785
|
+
const path = [];
|
|
786
|
+
let walk = elementBelow;
|
|
787
|
+
while (walk && walk !== document.body) {
|
|
788
|
+
const attrs = [walk.tagName.toLowerCase()];
|
|
789
|
+
if (walk.id) attrs.push(`#${walk.id}`);
|
|
790
|
+
if (walk.getAttribute("data-comment-target")) attrs.push(`[data-comment-target="${walk.getAttribute("data-comment-target")}"]`);
|
|
791
|
+
if (walk.getAttribute("aria-label")) attrs.push(`[aria-label="${walk.getAttribute("aria-label")}"]`);
|
|
792
|
+
if (walk.getAttribute("role")) attrs.push(`[role="${walk.getAttribute("role")}"]`);
|
|
793
|
+
if (SEMANTIC_TAGS.has(walk.tagName)) attrs.push("(semantic)");
|
|
794
|
+
path.push(attrs.join(""));
|
|
795
|
+
walk = walk.parentElement;
|
|
796
|
+
}
|
|
797
|
+
debug.log(" DOM path:", path.join(" \u2192 "));
|
|
798
|
+
}
|
|
799
|
+
const target = elementBelow ? findCommentTarget(elementBelow, overlayRef.current) : null;
|
|
800
|
+
if (target) {
|
|
801
|
+
const targetRect = target.element.getBoundingClientRect();
|
|
802
|
+
const x = (e.clientX - targetRect.left) / targetRect.width * 100;
|
|
803
|
+
const y = (e.clientY - targetRect.top) / targetRect.height * 100;
|
|
804
|
+
debug.log(" \u2705 target found:", {
|
|
805
|
+
targetId: target.targetId,
|
|
806
|
+
targetLabel: target.targetLabel,
|
|
807
|
+
element: target.element,
|
|
808
|
+
relativePos: { x: x.toFixed(1), y: y.toFixed(1) },
|
|
809
|
+
targetRect: { w: targetRect.width, h: targetRect.height }
|
|
810
|
+
});
|
|
811
|
+
setPendingPin({ x, y, targetId: target.targetId, targetLabel: target.targetLabel });
|
|
812
|
+
} else {
|
|
813
|
+
const x = (e.clientX - overlayRect.left) / overlayRect.width * 100;
|
|
814
|
+
const y = (e.clientY - overlayRect.top) / overlayRect.height * 100;
|
|
815
|
+
debug.log(" \u26A0\uFE0F no target found, using overlay-relative position:", {
|
|
816
|
+
x: x.toFixed(1),
|
|
817
|
+
y: y.toFixed(1)
|
|
818
|
+
});
|
|
819
|
+
setPendingPin({ x, y });
|
|
820
|
+
}
|
|
821
|
+
setPendingPixel({
|
|
822
|
+
left: e.clientX - overlayRect.left,
|
|
823
|
+
top: e.clientY - overlayRect.top
|
|
824
|
+
});
|
|
825
|
+
setActiveThreadId(null);
|
|
826
|
+
},
|
|
827
|
+
[commentMode, setActiveThreadId]
|
|
828
|
+
);
|
|
829
|
+
const handleNewComment = useCallback4(
|
|
830
|
+
(body) => {
|
|
831
|
+
if (!pendingPin) return;
|
|
832
|
+
debug.log(" saving thread:", {
|
|
833
|
+
pinX: pendingPin.x.toFixed(1),
|
|
834
|
+
pinY: pendingPin.y.toFixed(1),
|
|
835
|
+
targetId: pendingPin.targetId ?? "(none)",
|
|
836
|
+
targetLabel: pendingPin.targetLabel ?? "(none)",
|
|
837
|
+
body
|
|
838
|
+
});
|
|
839
|
+
addThread(pendingPin.x, pendingPin.y, body, pendingPin.targetId, pendingPin.targetLabel);
|
|
840
|
+
setPendingPin(null);
|
|
841
|
+
setPendingPixel(null);
|
|
842
|
+
},
|
|
843
|
+
[pendingPin, addThread]
|
|
844
|
+
);
|
|
845
|
+
const cancelPending = useCallback4(() => {
|
|
846
|
+
setPendingPin(null);
|
|
847
|
+
setPendingPixel(null);
|
|
848
|
+
setCommentMode(false);
|
|
849
|
+
}, [setCommentMode]);
|
|
850
|
+
useEffect5(() => {
|
|
851
|
+
const handler = (e) => {
|
|
852
|
+
const tag = e.target?.tagName;
|
|
853
|
+
if (e.key === "Escape") {
|
|
854
|
+
if (pendingPin) {
|
|
855
|
+
setPendingPin(null);
|
|
856
|
+
setPendingPixel(null);
|
|
857
|
+
setCommentMode(false);
|
|
858
|
+
} else if (commentMode) {
|
|
859
|
+
setCommentMode(false);
|
|
860
|
+
}
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (tag === "INPUT" || tag === "TEXTAREA") return;
|
|
864
|
+
if (e.key === "c" || e.key === "C") {
|
|
865
|
+
if (!commentMode) {
|
|
866
|
+
debug.log(" comment mode ON");
|
|
867
|
+
setActiveThreadId(null);
|
|
868
|
+
setCommentMode(true);
|
|
869
|
+
} else {
|
|
870
|
+
debug.log(" comment mode OFF");
|
|
871
|
+
setCommentMode(false);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
document.addEventListener("keydown", handler);
|
|
876
|
+
return () => document.removeEventListener("keydown", handler);
|
|
877
|
+
}, [commentMode, pendingPin, setCommentMode, setActiveThreadId]);
|
|
878
|
+
const sortedThreads = [...threads].sort((a, b) => {
|
|
879
|
+
if (a.resolved !== b.resolved) return a.resolved ? 1 : -1;
|
|
880
|
+
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
881
|
+
});
|
|
882
|
+
return /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
883
|
+
/* @__PURE__ */ jsx6(UserPrompt, {}),
|
|
884
|
+
/* @__PURE__ */ jsxs5(
|
|
885
|
+
"div",
|
|
886
|
+
{
|
|
887
|
+
ref: overlayRef,
|
|
888
|
+
className: `fixed inset-0 ${commentMode ? "cursor-crosshair pointer-events-auto" : "pointer-events-none"}`,
|
|
889
|
+
style: { zIndex: commentMode ? getHighestZIndex() + 10 : 55 },
|
|
890
|
+
onMouseDown: handleClick,
|
|
891
|
+
children: [
|
|
892
|
+
sortedThreads.map((thread, i) => /* @__PURE__ */ jsxs5("div", { className: "pointer-events-auto", children: [
|
|
893
|
+
/* @__PURE__ */ jsx6(CommentPin, { thread, index: i, overlayRef }),
|
|
894
|
+
/* @__PURE__ */ jsx6(ApostilThreadPopover, { thread, overlayRef })
|
|
895
|
+
] }, thread.id)),
|
|
896
|
+
pendingPin && pendingPixel && user && /* @__PURE__ */ jsxs5(
|
|
897
|
+
"div",
|
|
898
|
+
{
|
|
899
|
+
className: "absolute z-[70] pointer-events-auto",
|
|
900
|
+
style: {
|
|
901
|
+
left: pendingPixel.left,
|
|
902
|
+
top: pendingPixel.top
|
|
903
|
+
},
|
|
904
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
905
|
+
onClick: (e) => e.stopPropagation(),
|
|
906
|
+
children: [
|
|
907
|
+
/* @__PURE__ */ jsx6(
|
|
908
|
+
"div",
|
|
909
|
+
{
|
|
910
|
+
className: "absolute -translate-x-1/2 -translate-y-1/2 w-7 h-7 rounded-full\n flex items-center justify-center text-white text-xs font-semibold\n shadow-lg ring-2 ring-white ring-offset-2 animate-bounce",
|
|
911
|
+
style: { backgroundColor: user.color },
|
|
912
|
+
children: "+"
|
|
913
|
+
}
|
|
914
|
+
),
|
|
915
|
+
/* @__PURE__ */ jsx6("div", { className: "absolute ml-5 -mt-3 w-72", children: /* @__PURE__ */ jsxs5("div", { className: "bg-white rounded-xl shadow-2xl border border-neutral-200 p-3", children: [
|
|
916
|
+
/* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2 mb-2", children: [
|
|
917
|
+
/* @__PURE__ */ jsx6("p", { className: "text-xs text-neutral-500", children: "New comment" }),
|
|
918
|
+
pendingPin.targetLabel && /* @__PURE__ */ jsx6("span", { className: "text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-medium", children: pendingPin.targetLabel })
|
|
919
|
+
] }),
|
|
920
|
+
/* @__PURE__ */ jsx6(
|
|
921
|
+
CommentComposer,
|
|
922
|
+
{
|
|
923
|
+
onSubmit: handleNewComment,
|
|
924
|
+
placeholder: "What's on your mind?",
|
|
925
|
+
autoFocus: true
|
|
926
|
+
}
|
|
927
|
+
),
|
|
928
|
+
/* @__PURE__ */ jsx6(
|
|
929
|
+
"button",
|
|
930
|
+
{
|
|
931
|
+
onClick: cancelPending,
|
|
932
|
+
className: "mt-2 text-xs text-neutral-400 hover:text-neutral-600 transition-colors",
|
|
933
|
+
children: "Cancel"
|
|
934
|
+
}
|
|
935
|
+
)
|
|
936
|
+
] }) })
|
|
937
|
+
]
|
|
938
|
+
}
|
|
939
|
+
)
|
|
940
|
+
]
|
|
941
|
+
}
|
|
942
|
+
),
|
|
943
|
+
commentMode && !pendingPin && /* @__PURE__ */ jsx6("div", { className: "absolute bottom-6 left-1/2 -translate-x-1/2 z-[60] pointer-events-none", children: /* @__PURE__ */ jsx6("div", { className: "bg-neutral-900/80 text-white text-sm px-4 py-2 rounded-full backdrop-blur-sm", children: "Click anywhere to add a comment" }) })
|
|
944
|
+
] });
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// src/components/comment-toggle.tsx
|
|
948
|
+
import { MessageSquare, List, X } from "lucide-react";
|
|
949
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
950
|
+
function CommentToggle() {
|
|
951
|
+
const {
|
|
952
|
+
commentMode,
|
|
953
|
+
setCommentMode,
|
|
954
|
+
sidebarOpen,
|
|
955
|
+
setSidebarOpen,
|
|
956
|
+
unresolvedCount,
|
|
957
|
+
setActiveThreadId
|
|
958
|
+
} = useApostil();
|
|
959
|
+
return /* @__PURE__ */ jsxs6("div", { className: "absolute bottom-5 right-5 z-[65] flex flex-col gap-2 items-end", children: [
|
|
960
|
+
/* @__PURE__ */ jsx7(
|
|
961
|
+
"button",
|
|
962
|
+
{
|
|
963
|
+
onClick: () => setSidebarOpen(!sidebarOpen),
|
|
964
|
+
className: `flex items-center justify-center w-10 h-10 rounded-full shadow-lg
|
|
965
|
+
transition-all duration-200 hover:scale-105
|
|
966
|
+
${sidebarOpen ? "bg-neutral-900 text-white" : "bg-white text-neutral-600 hover:bg-neutral-50 border border-neutral-200"}`,
|
|
967
|
+
title: "Toggle comment list",
|
|
968
|
+
children: /* @__PURE__ */ jsx7(List, { className: "w-4 h-4" })
|
|
969
|
+
}
|
|
970
|
+
),
|
|
971
|
+
/* @__PURE__ */ jsxs6(
|
|
972
|
+
"button",
|
|
973
|
+
{
|
|
974
|
+
onClick: () => {
|
|
975
|
+
if (commentMode) {
|
|
976
|
+
setCommentMode(false);
|
|
977
|
+
} else {
|
|
978
|
+
setActiveThreadId(null);
|
|
979
|
+
setCommentMode(true);
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
className: `relative flex items-center justify-center w-12 h-12 rounded-full shadow-lg
|
|
983
|
+
transition-all duration-200 hover:scale-105
|
|
984
|
+
${commentMode ? "bg-neutral-900 text-white ring-2 ring-neutral-400" : "bg-white text-neutral-700 hover:bg-neutral-50 border border-neutral-200"}`,
|
|
985
|
+
title: commentMode ? "Exit comment mode" : "Add comment",
|
|
986
|
+
children: [
|
|
987
|
+
commentMode ? /* @__PURE__ */ jsx7(X, { className: "w-5 h-5" }) : /* @__PURE__ */ jsx7(MessageSquare, { className: "w-5 h-5" }),
|
|
988
|
+
unresolvedCount > 0 && !commentMode && /* @__PURE__ */ jsx7("span", { className: "absolute -top-1 -right-1 min-w-[18px] h-[18px] rounded-full\n bg-red-500 text-white text-[10px] font-semibold\n flex items-center justify-center px-1", children: unresolvedCount })
|
|
989
|
+
]
|
|
990
|
+
}
|
|
991
|
+
)
|
|
992
|
+
] });
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// src/components/comment-sidebar.tsx
|
|
996
|
+
import { useState as useState7, useEffect as useEffect6 } from "react";
|
|
997
|
+
import { X as X2, Check as Check2, Undo2 as Undo22, MessageSquare as MessageSquare2, Globe, FileText } from "lucide-react";
|
|
998
|
+
import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
999
|
+
function timeAgo2(iso) {
|
|
1000
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
1001
|
+
const mins = Math.floor(diff / 6e4);
|
|
1002
|
+
if (mins < 1) return "just now";
|
|
1003
|
+
if (mins < 60) return `${mins}m ago`;
|
|
1004
|
+
const hours = Math.floor(mins / 60);
|
|
1005
|
+
if (hours < 24) return `${hours}h ago`;
|
|
1006
|
+
const days = Math.floor(hours / 24);
|
|
1007
|
+
return `${days}d ago`;
|
|
1008
|
+
}
|
|
1009
|
+
function pageIdToDisplay(pageId) {
|
|
1010
|
+
return pageId.replace(/--/g, "/").replace(/-/g, ".");
|
|
1011
|
+
}
|
|
1012
|
+
function CommentSidebar() {
|
|
1013
|
+
const {
|
|
1014
|
+
threads,
|
|
1015
|
+
sidebarOpen,
|
|
1016
|
+
setSidebarOpen,
|
|
1017
|
+
setActiveThreadId,
|
|
1018
|
+
resolveThread
|
|
1019
|
+
} = useApostil();
|
|
1020
|
+
const [tab, setTab] = useState7("page");
|
|
1021
|
+
const [allPages, setAllPages] = useState7([]);
|
|
1022
|
+
const [loadingAll, setLoadingAll] = useState7(false);
|
|
1023
|
+
useEffect6(() => {
|
|
1024
|
+
if (!sidebarOpen || tab !== "all") return;
|
|
1025
|
+
setLoadingAll(true);
|
|
1026
|
+
async function fetchAll() {
|
|
1027
|
+
try {
|
|
1028
|
+
const res = await fetch("/api/apostil");
|
|
1029
|
+
if (res.ok) {
|
|
1030
|
+
const data = await res.json();
|
|
1031
|
+
setAllPages(data);
|
|
1032
|
+
}
|
|
1033
|
+
} catch {
|
|
1034
|
+
}
|
|
1035
|
+
setLoadingAll(false);
|
|
1036
|
+
}
|
|
1037
|
+
fetchAll();
|
|
1038
|
+
}, [sidebarOpen, tab]);
|
|
1039
|
+
if (!sidebarOpen) return null;
|
|
1040
|
+
const openThreads = threads.filter((t) => !t.resolved);
|
|
1041
|
+
const resolvedThreads = threads.filter((t) => t.resolved);
|
|
1042
|
+
return /* @__PURE__ */ jsxs7("div", { className: "absolute top-0 right-0 bottom-0 w-80 z-[75] bg-white border-l border-neutral-200 shadow-xl flex flex-col", children: [
|
|
1043
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between px-4 py-3 border-b border-neutral-100", children: [
|
|
1044
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
|
|
1045
|
+
/* @__PURE__ */ jsx8(MessageSquare2, { className: "w-4 h-4 text-neutral-500" }),
|
|
1046
|
+
/* @__PURE__ */ jsx8("span", { className: "text-sm font-semibold text-neutral-900", children: "Comments" })
|
|
1047
|
+
] }),
|
|
1048
|
+
/* @__PURE__ */ jsx8(
|
|
1049
|
+
"button",
|
|
1050
|
+
{
|
|
1051
|
+
onClick: () => setSidebarOpen(false),
|
|
1052
|
+
className: "p-1 rounded hover:bg-neutral-100 transition-colors",
|
|
1053
|
+
children: /* @__PURE__ */ jsx8(X2, { className: "w-4 h-4 text-neutral-500" })
|
|
1054
|
+
}
|
|
1055
|
+
)
|
|
1056
|
+
] }),
|
|
1057
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex border-b border-neutral-100", children: [
|
|
1058
|
+
/* @__PURE__ */ jsxs7(
|
|
1059
|
+
"button",
|
|
1060
|
+
{
|
|
1061
|
+
onClick: () => setTab("page"),
|
|
1062
|
+
className: `flex-1 flex items-center justify-center gap-1.5 py-2 text-xs font-medium transition-colors ${tab === "page" ? "text-neutral-900 border-b-2 border-neutral-900" : "text-neutral-400 hover:text-neutral-600"}`,
|
|
1063
|
+
children: [
|
|
1064
|
+
/* @__PURE__ */ jsx8(FileText, { className: "w-3 h-3" }),
|
|
1065
|
+
"This Page",
|
|
1066
|
+
openThreads.length > 0 && /* @__PURE__ */ jsx8("span", { className: "text-[10px] bg-red-50 text-red-600 px-1.5 py-px rounded-full", children: openThreads.length })
|
|
1067
|
+
]
|
|
1068
|
+
}
|
|
1069
|
+
),
|
|
1070
|
+
/* @__PURE__ */ jsxs7(
|
|
1071
|
+
"button",
|
|
1072
|
+
{
|
|
1073
|
+
onClick: () => setTab("all"),
|
|
1074
|
+
className: `flex-1 flex items-center justify-center gap-1.5 py-2 text-xs font-medium transition-colors ${tab === "all" ? "text-neutral-900 border-b-2 border-neutral-900" : "text-neutral-400 hover:text-neutral-600"}`,
|
|
1075
|
+
children: [
|
|
1076
|
+
/* @__PURE__ */ jsx8(Globe, { className: "w-3 h-3" }),
|
|
1077
|
+
"All Pages"
|
|
1078
|
+
]
|
|
1079
|
+
}
|
|
1080
|
+
)
|
|
1081
|
+
] }),
|
|
1082
|
+
/* @__PURE__ */ jsx8("div", { className: "flex-1 overflow-y-auto", children: tab === "page" ? /* @__PURE__ */ jsx8(
|
|
1083
|
+
PageThreads,
|
|
1084
|
+
{
|
|
1085
|
+
threads,
|
|
1086
|
+
openThreads,
|
|
1087
|
+
resolvedThreads,
|
|
1088
|
+
onSelect: setActiveThreadId,
|
|
1089
|
+
onResolve: resolveThread
|
|
1090
|
+
}
|
|
1091
|
+
) : /* @__PURE__ */ jsx8(
|
|
1092
|
+
AllPagesView,
|
|
1093
|
+
{
|
|
1094
|
+
pages: allPages,
|
|
1095
|
+
loading: loadingAll
|
|
1096
|
+
}
|
|
1097
|
+
) })
|
|
1098
|
+
] });
|
|
1099
|
+
}
|
|
1100
|
+
function PageThreads({
|
|
1101
|
+
threads,
|
|
1102
|
+
openThreads,
|
|
1103
|
+
resolvedThreads,
|
|
1104
|
+
onSelect,
|
|
1105
|
+
onResolve
|
|
1106
|
+
}) {
|
|
1107
|
+
if (threads.length === 0) {
|
|
1108
|
+
return /* @__PURE__ */ jsx8("div", { className: "p-6 text-center text-sm text-neutral-400", children: "No comments on this page." });
|
|
1109
|
+
}
|
|
1110
|
+
return /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
1111
|
+
openThreads.length > 0 && /* @__PURE__ */ jsxs7("div", { children: [
|
|
1112
|
+
/* @__PURE__ */ jsxs7("div", { className: "px-4 py-2 text-[10px] font-semibold text-neutral-400 uppercase tracking-wider", children: [
|
|
1113
|
+
"Open (",
|
|
1114
|
+
openThreads.length,
|
|
1115
|
+
")"
|
|
1116
|
+
] }),
|
|
1117
|
+
openThreads.map((thread) => /* @__PURE__ */ jsx8(
|
|
1118
|
+
ThreadItem,
|
|
1119
|
+
{
|
|
1120
|
+
thread,
|
|
1121
|
+
onSelect: () => onSelect(thread.id),
|
|
1122
|
+
onResolve: () => onResolve(thread.id)
|
|
1123
|
+
},
|
|
1124
|
+
thread.id
|
|
1125
|
+
))
|
|
1126
|
+
] }),
|
|
1127
|
+
resolvedThreads.length > 0 && /* @__PURE__ */ jsxs7("div", { children: [
|
|
1128
|
+
/* @__PURE__ */ jsxs7("div", { className: "px-4 py-2 text-[10px] font-semibold text-neutral-400 uppercase tracking-wider", children: [
|
|
1129
|
+
"Resolved (",
|
|
1130
|
+
resolvedThreads.length,
|
|
1131
|
+
")"
|
|
1132
|
+
] }),
|
|
1133
|
+
resolvedThreads.map((thread) => /* @__PURE__ */ jsx8(
|
|
1134
|
+
ThreadItem,
|
|
1135
|
+
{
|
|
1136
|
+
thread,
|
|
1137
|
+
onSelect: () => onSelect(thread.id),
|
|
1138
|
+
onResolve: () => onResolve(thread.id),
|
|
1139
|
+
resolved: true
|
|
1140
|
+
},
|
|
1141
|
+
thread.id
|
|
1142
|
+
))
|
|
1143
|
+
] })
|
|
1144
|
+
] });
|
|
1145
|
+
}
|
|
1146
|
+
function AllPagesView({
|
|
1147
|
+
pages,
|
|
1148
|
+
loading
|
|
1149
|
+
}) {
|
|
1150
|
+
if (loading) {
|
|
1151
|
+
return /* @__PURE__ */ jsx8("div", { className: "p-6 text-center text-sm text-neutral-400", children: "Loading..." });
|
|
1152
|
+
}
|
|
1153
|
+
if (pages.length === 0) {
|
|
1154
|
+
return /* @__PURE__ */ jsx8("div", { className: "p-6 text-center text-sm text-neutral-400", children: "No comments in this project yet." });
|
|
1155
|
+
}
|
|
1156
|
+
const totalOpen = pages.reduce((s, p) => s + p.threads.filter((t) => !t.resolved).length, 0);
|
|
1157
|
+
return /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
1158
|
+
totalOpen > 0 && /* @__PURE__ */ jsxs7("div", { className: "px-4 py-2 text-[10px] font-semibold text-neutral-400 uppercase tracking-wider", children: [
|
|
1159
|
+
totalOpen,
|
|
1160
|
+
" open across ",
|
|
1161
|
+
pages.length,
|
|
1162
|
+
" pages"
|
|
1163
|
+
] }),
|
|
1164
|
+
pages.map((page) => {
|
|
1165
|
+
const open = page.threads.filter((t) => !t.resolved);
|
|
1166
|
+
const resolved = page.threads.filter((t) => t.resolved);
|
|
1167
|
+
const displayName = pageIdToDisplay(page.pageId);
|
|
1168
|
+
return /* @__PURE__ */ jsxs7("div", { className: "border-b border-neutral-50", children: [
|
|
1169
|
+
/* @__PURE__ */ jsxs7("div", { className: "px-4 py-2.5 flex items-center justify-between bg-neutral-50/50", children: [
|
|
1170
|
+
/* @__PURE__ */ jsx8("span", { className: "text-xs font-semibold text-neutral-700 truncate", children: displayName }),
|
|
1171
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1.5", children: [
|
|
1172
|
+
open.length > 0 && /* @__PURE__ */ jsx8("span", { className: "text-[10px] bg-red-50 text-red-600 px-1.5 py-px rounded-full font-medium", children: open.length }),
|
|
1173
|
+
resolved.length > 0 && /* @__PURE__ */ jsx8("span", { className: "text-[10px] bg-neutral-100 text-neutral-500 px-1.5 py-px rounded-full font-medium", children: resolved.length })
|
|
1174
|
+
] })
|
|
1175
|
+
] }),
|
|
1176
|
+
[...open, ...resolved].map((thread) => {
|
|
1177
|
+
const firstComment = thread.comments[0];
|
|
1178
|
+
if (!firstComment) return null;
|
|
1179
|
+
const isResolved = thread.resolved;
|
|
1180
|
+
return /* @__PURE__ */ jsxs7(
|
|
1181
|
+
"div",
|
|
1182
|
+
{
|
|
1183
|
+
onClick: () => {
|
|
1184
|
+
const path = "/" + page.pageId.replace(/--/g, "/");
|
|
1185
|
+
window.location.href = path + "#apostil-" + thread.id;
|
|
1186
|
+
},
|
|
1187
|
+
className: `px-4 py-2.5 border-b border-neutral-50 cursor-pointer hover:bg-neutral-50 transition-colors ${isResolved ? "opacity-50" : ""}`,
|
|
1188
|
+
children: [
|
|
1189
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between mb-1", children: [
|
|
1190
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
|
|
1191
|
+
/* @__PURE__ */ jsx8(
|
|
1192
|
+
"div",
|
|
1193
|
+
{
|
|
1194
|
+
className: "w-4 h-4 rounded-full flex items-center justify-center text-white text-[8px] font-semibold",
|
|
1195
|
+
style: { backgroundColor: firstComment.author.color },
|
|
1196
|
+
children: firstComment.author.name[0]?.toUpperCase()
|
|
1197
|
+
}
|
|
1198
|
+
),
|
|
1199
|
+
/* @__PURE__ */ jsx8("span", { className: "text-xs font-medium text-neutral-700", children: firstComment.author.name })
|
|
1200
|
+
] }),
|
|
1201
|
+
/* @__PURE__ */ jsx8("span", { className: "text-[10px] text-neutral-400", children: timeAgo2(firstComment.createdAt) })
|
|
1202
|
+
] }),
|
|
1203
|
+
/* @__PURE__ */ jsx8("p", { className: "text-xs text-neutral-600 line-clamp-2 pl-6", children: firstComment.body }),
|
|
1204
|
+
thread.comments.length > 1 && /* @__PURE__ */ jsxs7("span", { className: "text-[10px] text-neutral-400 pl-6", children: [
|
|
1205
|
+
thread.comments.length - 1,
|
|
1206
|
+
" ",
|
|
1207
|
+
thread.comments.length - 1 === 1 ? "reply" : "replies"
|
|
1208
|
+
] })
|
|
1209
|
+
]
|
|
1210
|
+
},
|
|
1211
|
+
thread.id
|
|
1212
|
+
);
|
|
1213
|
+
})
|
|
1214
|
+
] }, page.pageId);
|
|
1215
|
+
})
|
|
1216
|
+
] });
|
|
1217
|
+
}
|
|
1218
|
+
function ThreadItem({
|
|
1219
|
+
thread,
|
|
1220
|
+
onSelect,
|
|
1221
|
+
onResolve,
|
|
1222
|
+
resolved
|
|
1223
|
+
}) {
|
|
1224
|
+
const firstComment = thread.comments[0];
|
|
1225
|
+
if (!firstComment) return null;
|
|
1226
|
+
return /* @__PURE__ */ jsxs7(
|
|
1227
|
+
"div",
|
|
1228
|
+
{
|
|
1229
|
+
onClick: onSelect,
|
|
1230
|
+
className: `px-4 py-3 border-b border-neutral-50 cursor-pointer hover:bg-neutral-50 transition-colors
|
|
1231
|
+
${resolved ? "opacity-60" : ""}`,
|
|
1232
|
+
children: [
|
|
1233
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between mb-1", children: [
|
|
1234
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2", children: [
|
|
1235
|
+
/* @__PURE__ */ jsx8(
|
|
1236
|
+
"div",
|
|
1237
|
+
{
|
|
1238
|
+
className: "w-4 h-4 rounded-full flex items-center justify-center text-white text-[8px] font-semibold",
|
|
1239
|
+
style: { backgroundColor: firstComment.author.color },
|
|
1240
|
+
children: firstComment.author.name[0]?.toUpperCase()
|
|
1241
|
+
}
|
|
1242
|
+
),
|
|
1243
|
+
/* @__PURE__ */ jsx8("span", { className: "text-xs font-medium text-neutral-700", children: firstComment.author.name })
|
|
1244
|
+
] }),
|
|
1245
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-1", children: [
|
|
1246
|
+
/* @__PURE__ */ jsx8("span", { className: "text-[10px] text-neutral-400", children: timeAgo2(firstComment.createdAt) }),
|
|
1247
|
+
/* @__PURE__ */ jsx8(
|
|
1248
|
+
"button",
|
|
1249
|
+
{
|
|
1250
|
+
onClick: (e) => {
|
|
1251
|
+
e.stopPropagation();
|
|
1252
|
+
onResolve();
|
|
1253
|
+
},
|
|
1254
|
+
className: "p-0.5 rounded hover:bg-neutral-200 transition-colors",
|
|
1255
|
+
children: resolved ? /* @__PURE__ */ jsx8(Undo22, { className: "w-3 h-3 text-neutral-400" }) : /* @__PURE__ */ jsx8(Check2, { className: "w-3 h-3 text-emerald-600" })
|
|
1256
|
+
}
|
|
1257
|
+
)
|
|
1258
|
+
] })
|
|
1259
|
+
] }),
|
|
1260
|
+
thread.targetLabel && /* @__PURE__ */ jsx8("span", { className: "inline-block text-[10px] bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded font-medium ml-6 mb-1", children: thread.targetLabel }),
|
|
1261
|
+
/* @__PURE__ */ jsx8("p", { className: "text-xs text-neutral-600 line-clamp-2 pl-6", children: firstComment.body }),
|
|
1262
|
+
thread.comments.length > 1 && /* @__PURE__ */ jsxs7("span", { className: "text-[10px] text-neutral-400 pl-6", children: [
|
|
1263
|
+
thread.comments.length - 1,
|
|
1264
|
+
" ",
|
|
1265
|
+
thread.comments.length - 1 === 1 ? "reply" : "replies"
|
|
1266
|
+
] })
|
|
1267
|
+
]
|
|
1268
|
+
}
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
export {
|
|
1272
|
+
ApostilProvider,
|
|
1273
|
+
CommentOverlay,
|
|
1274
|
+
CommentSidebar,
|
|
1275
|
+
CommentToggle,
|
|
1276
|
+
debug,
|
|
1277
|
+
useApostil,
|
|
1278
|
+
useCommentMode,
|
|
1279
|
+
useComments
|
|
1280
|
+
};
|
|
1281
|
+
//# sourceMappingURL=index.js.map
|