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/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