@washi-ui/react 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,981 @@
1
+ // src/useWashi.ts
2
+ import { useRef, useState, useEffect, useCallback } from "react";
3
+ import {
4
+ Washi
5
+ } from "@washi-ui/core";
6
+ function useWashi(options) {
7
+ const {
8
+ adapter,
9
+ initialMode = "view",
10
+ mountOptions,
11
+ onPinPlaced,
12
+ onCommentClick,
13
+ onCommentUpdate,
14
+ onCommentDelete
15
+ } = options;
16
+ const iframeRef = useRef(null);
17
+ const washiRef = useRef(null);
18
+ const [mode, setModeState] = useState(initialMode);
19
+ const [comments, setComments] = useState([]);
20
+ const [isReady, setIsReady] = useState(false);
21
+ const [error, setError] = useState(null);
22
+ useEffect(() => {
23
+ washiRef.current = new Washi(adapter);
24
+ return () => {
25
+ washiRef.current?.unmount();
26
+ washiRef.current = null;
27
+ };
28
+ }, [adapter]);
29
+ useEffect(() => {
30
+ const iframe = iframeRef.current;
31
+ const washi = washiRef.current;
32
+ if (!iframe || !washi) return;
33
+ const handleLoad = async () => {
34
+ try {
35
+ await washi.mount(iframe, mountOptions);
36
+ setComments(washi.getComments());
37
+ if (initialMode !== "view") {
38
+ washi.setMode(initialMode);
39
+ }
40
+ setIsReady(true);
41
+ setError(null);
42
+ } catch (err) {
43
+ setError(err instanceof Error ? err : new Error(String(err)));
44
+ setIsReady(false);
45
+ }
46
+ };
47
+ if (iframe.contentDocument?.readyState === "complete") {
48
+ handleLoad();
49
+ } else {
50
+ iframe.addEventListener("load", handleLoad);
51
+ }
52
+ return () => {
53
+ iframe.removeEventListener("load", handleLoad);
54
+ };
55
+ }, [mountOptions, initialMode]);
56
+ useEffect(() => {
57
+ const washi = washiRef.current;
58
+ if (!washi || !isReady) return;
59
+ const unsubscribers = [];
60
+ if (onPinPlaced) {
61
+ unsubscribers.push(
62
+ washi.on("pin:placed", (event) => {
63
+ onPinPlaced(event);
64
+ })
65
+ );
66
+ }
67
+ if (onCommentClick) {
68
+ unsubscribers.push(
69
+ washi.on("comment:clicked", (comment) => {
70
+ onCommentClick(comment);
71
+ })
72
+ );
73
+ }
74
+ if (onCommentUpdate) {
75
+ unsubscribers.push(
76
+ washi.on(
77
+ "comment:updated",
78
+ (data) => {
79
+ onCommentUpdate(data);
80
+ }
81
+ )
82
+ );
83
+ }
84
+ if (onCommentDelete) {
85
+ unsubscribers.push(
86
+ washi.on("comment:deleted", (id) => {
87
+ onCommentDelete(id);
88
+ })
89
+ );
90
+ }
91
+ return () => {
92
+ unsubscribers.forEach((unsub) => unsub());
93
+ };
94
+ }, [isReady, onPinPlaced, onCommentClick, onCommentUpdate, onCommentDelete]);
95
+ const setMode = useCallback((newMode) => {
96
+ const washi = washiRef.current;
97
+ if (!washi) return;
98
+ try {
99
+ washi.setMode(newMode);
100
+ setModeState(newMode);
101
+ } catch (err) {
102
+ setError(err instanceof Error ? err : new Error(String(err)));
103
+ }
104
+ }, []);
105
+ const addComment = useCallback(async (input) => {
106
+ const washi = washiRef.current;
107
+ if (!washi) throw new Error("Washi is not initialized");
108
+ const comment = await washi.addComment(input);
109
+ setComments(washi.getComments());
110
+ return comment;
111
+ }, []);
112
+ const updateComment = useCallback(
113
+ async (id, updates) => {
114
+ const washi = washiRef.current;
115
+ if (!washi) throw new Error("Washi is not initialized");
116
+ await washi.updateComment(id, updates);
117
+ setComments(washi.getComments());
118
+ },
119
+ []
120
+ );
121
+ const deleteComment = useCallback(async (id) => {
122
+ const washi = washiRef.current;
123
+ if (!washi) throw new Error("Washi is not initialized");
124
+ await washi.deleteComment(id);
125
+ setComments(washi.getComments());
126
+ }, []);
127
+ return {
128
+ iframeRef,
129
+ mode,
130
+ setMode,
131
+ comments,
132
+ addComment,
133
+ updateComment,
134
+ deleteComment,
135
+ isReady,
136
+ error
137
+ };
138
+ }
139
+
140
+ // src/WashiProvider.tsx
141
+ import {
142
+ createContext,
143
+ useContext,
144
+ useRef as useRef2,
145
+ useState as useState2,
146
+ useEffect as useEffect2,
147
+ useCallback as useCallback2,
148
+ useMemo
149
+ } from "react";
150
+ import {
151
+ Washi as Washi2
152
+ } from "@washi-ui/core";
153
+ import { jsx } from "react/jsx-runtime";
154
+ var WashiContext = createContext(null);
155
+ function WashiProvider({
156
+ adapter,
157
+ initialMode = "view",
158
+ mountOptions,
159
+ children
160
+ }) {
161
+ const washiRef = useRef2(null);
162
+ if (!washiRef.current) {
163
+ washiRef.current = new Washi2(adapter);
164
+ }
165
+ const iframeRef = useRef2(null);
166
+ const iframeLoadCleanupRef = useRef2(null);
167
+ const washiEventCleanupsRef = useRef2([]);
168
+ const pinPlacedCallbacksRef = useRef2(
169
+ /* @__PURE__ */ new Set()
170
+ );
171
+ const clickCallbacksRef = useRef2(/* @__PURE__ */ new Set());
172
+ const [mode, setModeState] = useState2(initialMode);
173
+ const [comments, setComments] = useState2([]);
174
+ const [isReady, setIsReady] = useState2(false);
175
+ const [error, setError] = useState2(null);
176
+ const [activeComment, setActiveCommentState] = useState2(null);
177
+ const [iframeElState, setIframeElState] = useState2(null);
178
+ useEffect2(() => {
179
+ return () => {
180
+ washiRef.current?.unmount();
181
+ };
182
+ }, []);
183
+ const refreshComments = useCallback2(() => {
184
+ const washi = washiRef.current;
185
+ if (washi) {
186
+ setComments(washi.getComments());
187
+ }
188
+ }, []);
189
+ const registerIframe = useCallback2(
190
+ (iframe) => {
191
+ iframeLoadCleanupRef.current?.();
192
+ iframeLoadCleanupRef.current = null;
193
+ washiEventCleanupsRef.current.forEach((fn) => fn());
194
+ washiEventCleanupsRef.current = [];
195
+ iframeRef.current = iframe;
196
+ setIframeElState(iframe);
197
+ if (!iframe) {
198
+ if (washiRef.current) {
199
+ washiRef.current.unmount();
200
+ setIsReady(false);
201
+ setComments([]);
202
+ }
203
+ return;
204
+ }
205
+ if (!washiRef.current) return;
206
+ const washi = washiRef.current;
207
+ const handleLoad = async () => {
208
+ try {
209
+ await washi.mount(iframe, mountOptions);
210
+ if (iframeRef.current !== iframe) return;
211
+ setComments(washi.getComments());
212
+ if (initialMode !== "view") {
213
+ washi.setMode(initialMode);
214
+ }
215
+ setIsReady(true);
216
+ setError(null);
217
+ washiEventCleanupsRef.current.push(
218
+ washi.on("pin:placed", (event) => {
219
+ pinPlacedCallbacksRef.current.forEach((cb) => cb(event));
220
+ })
221
+ );
222
+ washiEventCleanupsRef.current.push(
223
+ washi.on("comment:clicked", (comment) => {
224
+ clickCallbacksRef.current.forEach((cb) => cb(comment));
225
+ })
226
+ );
227
+ washiEventCleanupsRef.current.push(
228
+ washi.on("comment:updated", () => {
229
+ setComments(washi.getComments());
230
+ })
231
+ );
232
+ washiEventCleanupsRef.current.push(
233
+ washi.on("comment:deleted", () => {
234
+ setComments(washi.getComments());
235
+ })
236
+ );
237
+ } catch (err) {
238
+ setError(err instanceof Error ? err : new Error(String(err)));
239
+ setIsReady(false);
240
+ }
241
+ };
242
+ if (iframe.contentDocument?.readyState === "complete") {
243
+ handleLoad();
244
+ } else {
245
+ iframe.addEventListener("load", handleLoad);
246
+ iframeLoadCleanupRef.current = () => iframe.removeEventListener("load", handleLoad);
247
+ }
248
+ },
249
+ [mountOptions, initialMode]
250
+ );
251
+ const setMode = useCallback2((newMode) => {
252
+ const washi = washiRef.current;
253
+ if (!washi) return;
254
+ try {
255
+ washi.setMode(newMode);
256
+ setModeState(newMode);
257
+ } catch (err) {
258
+ setError(err instanceof Error ? err : new Error(String(err)));
259
+ }
260
+ }, []);
261
+ const addComment = useCallback2(async (input) => {
262
+ const washi = washiRef.current;
263
+ if (!washi) throw new Error("Washi is not initialized");
264
+ const comment = await washi.addComment(input);
265
+ setComments(washi.getComments());
266
+ return comment;
267
+ }, []);
268
+ const updateComment = useCallback2(
269
+ async (id, updates) => {
270
+ const washi = washiRef.current;
271
+ if (!washi) throw new Error("Washi is not initialized");
272
+ await washi.updateComment(id, updates);
273
+ setComments(washi.getComments());
274
+ },
275
+ []
276
+ );
277
+ const deleteComment = useCallback2(async (id) => {
278
+ const washi = washiRef.current;
279
+ if (!washi) throw new Error("Washi is not initialized");
280
+ await washi.deleteComment(id);
281
+ setComments(washi.getComments());
282
+ }, []);
283
+ const onPinPlaced = useCallback2(
284
+ (callback) => {
285
+ pinPlacedCallbacksRef.current.add(callback);
286
+ return () => {
287
+ pinPlacedCallbacksRef.current.delete(callback);
288
+ };
289
+ },
290
+ []
291
+ );
292
+ const onCommentClick = useCallback2(
293
+ (callback) => {
294
+ clickCallbacksRef.current.add(callback);
295
+ return () => {
296
+ clickCallbacksRef.current.delete(callback);
297
+ };
298
+ },
299
+ []
300
+ );
301
+ const setActivePin = useCallback2((commentId) => {
302
+ const washi = washiRef.current;
303
+ if (washi) {
304
+ washi.setActivePin(commentId);
305
+ }
306
+ }, []);
307
+ const getCommentIndex = useCallback2((commentId) => {
308
+ const washi = washiRef.current;
309
+ if (washi) {
310
+ return washi.getCommentIndex(commentId);
311
+ }
312
+ return -1;
313
+ }, []);
314
+ const setActiveComment = useCallback2((comment) => {
315
+ setActiveCommentState(comment);
316
+ const washi = washiRef.current;
317
+ if (washi) {
318
+ washi.setActivePin(comment?.id ?? null);
319
+ }
320
+ }, []);
321
+ const value = useMemo(
322
+ () => ({
323
+ washi: washiRef.current,
324
+ iframeEl: iframeElState,
325
+ mode,
326
+ setMode,
327
+ comments,
328
+ addComment,
329
+ updateComment,
330
+ deleteComment,
331
+ refreshComments,
332
+ isReady,
333
+ error,
334
+ registerIframe,
335
+ onPinPlaced,
336
+ onCommentClick,
337
+ setActivePin,
338
+ getCommentIndex,
339
+ activeComment,
340
+ setActiveComment
341
+ }),
342
+ [
343
+ iframeElState,
344
+ mode,
345
+ setMode,
346
+ comments,
347
+ addComment,
348
+ updateComment,
349
+ deleteComment,
350
+ refreshComments,
351
+ isReady,
352
+ error,
353
+ registerIframe,
354
+ onPinPlaced,
355
+ onCommentClick,
356
+ setActivePin,
357
+ getCommentIndex,
358
+ activeComment,
359
+ setActiveComment
360
+ ]
361
+ );
362
+ return /* @__PURE__ */ jsx(WashiContext.Provider, { value, children });
363
+ }
364
+ function useWashiContext() {
365
+ const context = useContext(WashiContext);
366
+ if (!context) {
367
+ throw new Error("useWashiContext must be used within a WashiProvider");
368
+ }
369
+ return context;
370
+ }
371
+
372
+ // src/WashiFrame.tsx
373
+ import { useEffect as useEffect3, useRef as useRef3 } from "react";
374
+ import { jsx as jsx2 } from "react/jsx-runtime";
375
+ function WashiFrame({ src, className, style, ...props }) {
376
+ const { registerIframe } = useWashiContext();
377
+ const iframeRef = useRef3(null);
378
+ useEffect3(() => {
379
+ if (iframeRef.current) {
380
+ registerIframe(iframeRef.current);
381
+ }
382
+ return () => {
383
+ registerIframe(null);
384
+ };
385
+ }, [registerIframe]);
386
+ return /* @__PURE__ */ jsx2(
387
+ "iframe",
388
+ {
389
+ ref: iframeRef,
390
+ src,
391
+ className,
392
+ style,
393
+ ...props
394
+ }
395
+ );
396
+ }
397
+
398
+ // src/CommentList.tsx
399
+ import React2 from "react";
400
+ import { Fragment, jsx as jsx3 } from "react/jsx-runtime";
401
+ function CommentList({
402
+ renderComment,
403
+ filter,
404
+ sort,
405
+ emptyState,
406
+ className,
407
+ style
408
+ }) {
409
+ const { comments, updateComment, deleteComment } = useWashiContext();
410
+ let displayComments = [...comments];
411
+ if (filter) {
412
+ displayComments = displayComments.filter(filter);
413
+ }
414
+ if (sort) {
415
+ displayComments.sort(sort);
416
+ }
417
+ if (displayComments.length === 0 && emptyState) {
418
+ return /* @__PURE__ */ jsx3(Fragment, { children: emptyState });
419
+ }
420
+ return /* @__PURE__ */ jsx3("div", { className, style, children: displayComments.map((comment) => {
421
+ const actions = {
422
+ onResolve: async () => {
423
+ await updateComment(comment.id, { resolved: !comment.resolved });
424
+ },
425
+ onDelete: async () => {
426
+ await deleteComment(comment.id);
427
+ },
428
+ onUpdate: async (updates) => {
429
+ await updateComment(comment.id, updates);
430
+ }
431
+ };
432
+ return /* @__PURE__ */ jsx3(React2.Fragment, { children: renderComment(comment, actions) }, comment.id);
433
+ }) });
434
+ }
435
+
436
+ // src/WashiToolBubble.tsx
437
+ import { useState as useState3 } from "react";
438
+ import { jsx as jsx4, jsxs } from "react/jsx-runtime";
439
+ var positionStyles = {
440
+ "bottom-right": { bottom: 24, right: 24 },
441
+ "bottom-left": { bottom: 24, left: 24 },
442
+ "top-right": { top: 24, right: 24 },
443
+ "top-left": { top: 24, left: 24 }
444
+ };
445
+ function PencilIcon() {
446
+ return /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
447
+ /* @__PURE__ */ jsx4("path", { d: "M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" }),
448
+ /* @__PURE__ */ jsx4("path", { d: "M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" })
449
+ ] });
450
+ }
451
+ function ChatIcon() {
452
+ return /* @__PURE__ */ jsx4("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx4("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) });
453
+ }
454
+ function WashiToolBubble({
455
+ position = "bottom-right",
456
+ sidebarOpen = false,
457
+ onSidebarToggle,
458
+ accentColor = "#667eea"
459
+ }) {
460
+ const { mode, setMode, isReady, comments } = useWashiContext();
461
+ const [hoveredBtn, setHoveredBtn] = useState3(null);
462
+ const isAnnotate = mode === "annotate";
463
+ const commentCount = comments.length;
464
+ const badgeLabel = commentCount > 99 ? "99+" : String(commentCount);
465
+ const pillStyle = {
466
+ position: "fixed",
467
+ ...positionStyles[position],
468
+ zIndex: 9998,
469
+ display: "flex",
470
+ alignItems: "center",
471
+ gap: "4px",
472
+ padding: "6px",
473
+ borderRadius: "999px",
474
+ backgroundColor: "rgba(255,255,255,0.92)",
475
+ backdropFilter: "blur(8px)",
476
+ WebkitBackdropFilter: "blur(8px)",
477
+ boxShadow: "0 4px 16px rgba(0,0,0,0.12), 0 1px 4px rgba(0,0,0,0.08)",
478
+ border: "1px solid rgba(0,0,0,0.06)"
479
+ };
480
+ const btnBase = {
481
+ position: "relative",
482
+ display: "flex",
483
+ alignItems: "center",
484
+ justifyContent: "center",
485
+ width: 40,
486
+ height: 40,
487
+ borderRadius: "50%",
488
+ border: "none",
489
+ cursor: "pointer",
490
+ transition: "background-color 0.15s, box-shadow 0.15s, color 0.15s",
491
+ outline: "none"
492
+ };
493
+ const pencilActive = isAnnotate;
494
+ const pencilDisabled = !isReady;
495
+ const pencilStyle = {
496
+ ...btnBase,
497
+ backgroundColor: pencilActive ? accentColor : hoveredBtn === "pencil" ? "rgba(0,0,0,0.06)" : "transparent",
498
+ color: pencilActive ? "#fff" : "#374151",
499
+ opacity: pencilDisabled ? 0.5 : 1,
500
+ cursor: pencilDisabled ? "not-allowed" : "pointer",
501
+ boxShadow: pencilActive ? `0 0 0 3px ${accentColor}33` : "none"
502
+ };
503
+ const chatStyle = {
504
+ ...btnBase,
505
+ backgroundColor: sidebarOpen ? "rgba(0,0,0,0.06)" : hoveredBtn === "chat" ? "rgba(0,0,0,0.06)" : "transparent",
506
+ color: "#374151"
507
+ };
508
+ const badgeStyle = {
509
+ position: "absolute",
510
+ top: 4,
511
+ right: 4,
512
+ minWidth: 16,
513
+ height: 16,
514
+ borderRadius: "999px",
515
+ backgroundColor: accentColor,
516
+ color: "#fff",
517
+ fontSize: "10px",
518
+ fontWeight: 700,
519
+ display: "flex",
520
+ alignItems: "center",
521
+ justifyContent: "center",
522
+ padding: "0 3px",
523
+ lineHeight: 1,
524
+ pointerEvents: "none"
525
+ };
526
+ return /* @__PURE__ */ jsxs("div", { style: pillStyle, children: [
527
+ /* @__PURE__ */ jsx4(
528
+ "button",
529
+ {
530
+ style: pencilStyle,
531
+ disabled: pencilDisabled,
532
+ title: isAnnotate ? "Exit annotate mode" : "Enter annotate mode",
533
+ onClick: () => !pencilDisabled && setMode(isAnnotate ? "view" : "annotate"),
534
+ onMouseEnter: () => setHoveredBtn("pencil"),
535
+ onMouseLeave: () => setHoveredBtn(null),
536
+ children: /* @__PURE__ */ jsx4(PencilIcon, {})
537
+ }
538
+ ),
539
+ /* @__PURE__ */ jsxs(
540
+ "button",
541
+ {
542
+ style: chatStyle,
543
+ title: sidebarOpen ? "Close comments" : "Open comments",
544
+ onClick: onSidebarToggle,
545
+ onMouseEnter: () => setHoveredBtn("chat"),
546
+ onMouseLeave: () => setHoveredBtn(null),
547
+ children: [
548
+ /* @__PURE__ */ jsx4(ChatIcon, {}),
549
+ commentCount > 0 && /* @__PURE__ */ jsx4("span", { style: badgeStyle, children: badgeLabel })
550
+ ]
551
+ }
552
+ )
553
+ ] });
554
+ }
555
+
556
+ // src/WashiCommentsSidebar.tsx
557
+ import { useState as useState4, useEffect as useEffect4 } from "react";
558
+ import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
559
+ var BUBBLE_EDGE = 24;
560
+ var BUBBLE_HEIGHT = 52;
561
+ var GAP = 10;
562
+ function getPanelBounds(position) {
563
+ const reserved = BUBBLE_EDGE + BUBBLE_HEIGHT + GAP;
564
+ switch (position) {
565
+ case "bottom-right":
566
+ return { top: 0, right: 0, bottom: reserved };
567
+ case "bottom-left":
568
+ return { top: 0, left: 0, bottom: reserved };
569
+ case "top-right":
570
+ return { top: reserved, right: 0, bottom: 0 };
571
+ case "top-left":
572
+ return { top: reserved, left: 0, bottom: 0 };
573
+ }
574
+ }
575
+ var KEYFRAMES_ID = "__washi-panel-kf__";
576
+ if (typeof document !== "undefined" && !document.getElementById(KEYFRAMES_ID)) {
577
+ const style = document.createElement("style");
578
+ style.id = KEYFRAMES_ID;
579
+ style.textContent = `
580
+ @keyframes washi-panel-in-right { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
581
+ @keyframes washi-panel-out-right { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } }
582
+ @keyframes washi-panel-in-left { from { transform: translateX(-100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
583
+ @keyframes washi-panel-out-left { from { transform: translateX(0); opacity: 1; } to { transform: translateX(-100%); opacity: 0; } }
584
+ `;
585
+ document.head.appendChild(style);
586
+ }
587
+ var ANIM_DURATION = 240;
588
+ function WashiCommentsSidebar({
589
+ open,
590
+ onClose,
591
+ position = "bottom-right",
592
+ accentColor = "#667eea"
593
+ }) {
594
+ const { comments, updateComment, deleteComment } = useWashiContext();
595
+ const [visible, setVisible] = useState4(open);
596
+ const [phase, setPhase] = useState4(open ? "in" : "out");
597
+ useEffect4(() => {
598
+ if (open) {
599
+ setVisible(true);
600
+ const raf = requestAnimationFrame(() => setPhase("in"));
601
+ return () => cancelAnimationFrame(raf);
602
+ } else {
603
+ setPhase("out");
604
+ const t = setTimeout(() => setVisible(false), ANIM_DURATION);
605
+ return () => clearTimeout(t);
606
+ }
607
+ }, [open]);
608
+ if (!visible) return null;
609
+ const sortedComments = [...comments].sort((a, b) => a.createdAt - b.createdAt);
610
+ const side = position.endsWith("right") ? "right" : "left";
611
+ const animName = phase === "in" ? `washi-panel-in-${side}` : `washi-panel-out-${side}`;
612
+ const panelStyle = {
613
+ position: "fixed",
614
+ ...getPanelBounds(position),
615
+ width: 320,
616
+ zIndex: 9999,
617
+ display: "flex",
618
+ flexDirection: "column",
619
+ backgroundColor: "#f9fafb",
620
+ boxShadow: side === "right" ? "-4px 0 24px rgba(0,0,0,0.10)" : "4px 0 24px rgba(0,0,0,0.10)",
621
+ borderLeft: side === "right" ? "1px solid #e5e7eb" : "none",
622
+ borderRight: side === "left" ? "1px solid #e5e7eb" : "none",
623
+ animation: `${animName} ${ANIM_DURATION}ms cubic-bezier(0.4,0,0.2,1) forwards`,
624
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
625
+ };
626
+ return /* @__PURE__ */ jsxs2("div", { style: panelStyle, children: [
627
+ /* @__PURE__ */ jsxs2("div", { style: {
628
+ display: "flex",
629
+ alignItems: "center",
630
+ justifyContent: "space-between",
631
+ padding: "16px 20px",
632
+ borderBottom: "1px solid #e5e7eb",
633
+ backgroundColor: "#fff",
634
+ flexShrink: 0
635
+ }, children: [
636
+ /* @__PURE__ */ jsxs2("span", { style: { fontSize: "1rem", fontWeight: 600, color: "#1f2937" }, children: [
637
+ "Comments (",
638
+ comments.length,
639
+ ")"
640
+ ] }),
641
+ /* @__PURE__ */ jsx5(
642
+ "button",
643
+ {
644
+ onClick: onClose,
645
+ title: "Close",
646
+ style: {
647
+ display: "flex",
648
+ alignItems: "center",
649
+ justifyContent: "center",
650
+ width: 30,
651
+ height: 30,
652
+ border: "none",
653
+ borderRadius: 6,
654
+ backgroundColor: "transparent",
655
+ cursor: "pointer",
656
+ color: "#9ca3af",
657
+ fontSize: 14,
658
+ padding: 0
659
+ },
660
+ children: "\u2715"
661
+ }
662
+ )
663
+ ] }),
664
+ /* @__PURE__ */ jsx5("div", { style: { flex: 1, overflowY: "auto", padding: "12px" }, children: sortedComments.length === 0 ? /* @__PURE__ */ jsxs2("div", { style: {
665
+ textAlign: "center",
666
+ padding: "40px 20px",
667
+ color: "#9ca3af",
668
+ fontSize: "0.875rem"
669
+ }, children: [
670
+ /* @__PURE__ */ jsx5("p", { style: { marginBottom: 8 }, children: "No comments yet." }),
671
+ /* @__PURE__ */ jsx5("p", { children: "Use the annotate tool to add one." })
672
+ ] }) : sortedComments.map((comment, index) => {
673
+ const color = comment.color || accentColor;
674
+ const borderColor = comment.resolved ? "#10b981" : color;
675
+ const badgeBg = comment.resolved ? "#10b981" : color;
676
+ return /* @__PURE__ */ jsxs2(
677
+ "div",
678
+ {
679
+ style: {
680
+ backgroundColor: "#fff",
681
+ border: "1px solid #e5e7eb",
682
+ borderLeft: `3px solid ${borderColor}`,
683
+ borderRadius: 8,
684
+ padding: "12px",
685
+ marginBottom: 10,
686
+ opacity: comment.resolved ? 0.7 : 1
687
+ },
688
+ children: [
689
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: 8 }, children: [
690
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center" }, children: [
691
+ /* @__PURE__ */ jsx5("span", { style: {
692
+ display: "inline-flex",
693
+ alignItems: "center",
694
+ justifyContent: "center",
695
+ width: 20,
696
+ height: 20,
697
+ borderRadius: "50%",
698
+ backgroundColor: badgeBg,
699
+ color: "#fff",
700
+ fontSize: "0.7rem",
701
+ fontWeight: 700,
702
+ marginRight: 8,
703
+ flexShrink: 0
704
+ }, children: comment.resolved ? "\u2713" : index + 1 }),
705
+ /* @__PURE__ */ jsxs2("span", { style: { fontSize: "0.75rem", color: "#6b7280" }, children: [
706
+ "(",
707
+ comment.x.toFixed(1),
708
+ "%, ",
709
+ comment.y.toFixed(1),
710
+ "%)"
711
+ ] })
712
+ ] }),
713
+ comment.resolved && /* @__PURE__ */ jsx5("span", { style: { fontSize: "0.75rem", color: "#059669", fontWeight: 500 }, children: "Resolved" })
714
+ ] }),
715
+ /* @__PURE__ */ jsx5("p", { style: { fontSize: "0.875rem", color: "#374151", marginBottom: 10, lineHeight: 1.5 }, children: comment.text }),
716
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", gap: 8 }, children: [
717
+ /* @__PURE__ */ jsx5(
718
+ "button",
719
+ {
720
+ style: {
721
+ padding: "4px 10px",
722
+ fontSize: "0.75rem",
723
+ border: "none",
724
+ borderRadius: 4,
725
+ cursor: "pointer",
726
+ backgroundColor: "#e0e7ff",
727
+ color: "#4f46e5"
728
+ },
729
+ onClick: () => updateComment(comment.id, { resolved: !comment.resolved }),
730
+ children: comment.resolved ? "Unresolve" : "Resolve"
731
+ }
732
+ ),
733
+ /* @__PURE__ */ jsx5(
734
+ "button",
735
+ {
736
+ style: {
737
+ padding: "4px 10px",
738
+ fontSize: "0.75rem",
739
+ border: "none",
740
+ borderRadius: 4,
741
+ cursor: "pointer",
742
+ backgroundColor: "#fee2e2",
743
+ color: "#dc2626"
744
+ },
745
+ onClick: () => deleteComment(comment.id),
746
+ children: "Delete"
747
+ }
748
+ )
749
+ ] })
750
+ ]
751
+ },
752
+ comment.id
753
+ );
754
+ }) })
755
+ ] });
756
+ }
757
+
758
+ // src/WashiPinDialog.tsx
759
+ import { useState as useState5, useEffect as useEffect5 } from "react";
760
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
761
+ function WashiPinDialog({ accentColor = "#667eea", onComment }) {
762
+ const { onPinPlaced, addComment, setMode, iframeEl } = useWashiContext();
763
+ const [pending, setPending] = useState5(null);
764
+ const [text, setText] = useState5("");
765
+ useEffect5(() => {
766
+ return onPinPlaced((event) => {
767
+ const containerW = iframeEl?.clientWidth ?? window.innerWidth;
768
+ const containerH = iframeEl?.clientHeight ?? window.innerHeight;
769
+ setText("");
770
+ setPending({
771
+ x: event.x,
772
+ y: event.y,
773
+ pixelX: event.x / 100 * containerW,
774
+ pixelY: event.y / 100 * containerH,
775
+ containerW,
776
+ containerH
777
+ });
778
+ });
779
+ }, [onPinPlaced, iframeEl]);
780
+ if (!pending) return null;
781
+ const handleSubmit = async () => {
782
+ if (!text.trim()) return;
783
+ const comment = await addComment({ x: pending.x, y: pending.y, text: text.trim(), color: accentColor });
784
+ setPending(null);
785
+ setMode("view");
786
+ onComment?.(comment);
787
+ };
788
+ const handleCancel = () => setPending(null);
789
+ const handleKeyDown = (e) => {
790
+ if (e.key === "Enter" && e.metaKey) handleSubmit();
791
+ if (e.key === "Escape") handleCancel();
792
+ };
793
+ const POPOVER_WIDTH = 280;
794
+ const left = Math.min(
795
+ Math.max(pending.pixelX - POPOVER_WIDTH / 2, 8),
796
+ pending.containerW - POPOVER_WIDTH - 8
797
+ );
798
+ const showAbove = pending.pixelY > pending.containerH * 0.65;
799
+ const top = showAbove ? pending.pixelY - 172 : pending.pixelY + 16;
800
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
801
+ /* @__PURE__ */ jsx6("div", { style: { position: "fixed", inset: 0, zIndex: 9999 }, onClick: handleCancel }),
802
+ /* @__PURE__ */ jsxs3(
803
+ "div",
804
+ {
805
+ style: {
806
+ position: "fixed",
807
+ left,
808
+ top,
809
+ width: POPOVER_WIDTH,
810
+ zIndex: 1e4,
811
+ backgroundColor: "#fff",
812
+ borderRadius: 12,
813
+ boxShadow: "0 8px 32px rgba(0,0,0,0.18), 0 2px 8px rgba(0,0,0,0.10)",
814
+ border: "1px solid rgba(0,0,0,0.06)",
815
+ padding: "14px 16px",
816
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
817
+ },
818
+ children: [
819
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 10 }, children: [
820
+ /* @__PURE__ */ jsx6("span", { style: { fontSize: "0.875rem", fontWeight: 600, color: "#1f2937" }, children: "Add comment" }),
821
+ /* @__PURE__ */ jsx6(
822
+ "button",
823
+ {
824
+ onClick: handleCancel,
825
+ style: {
826
+ border: "none",
827
+ background: "none",
828
+ cursor: "pointer",
829
+ color: "#9ca3af",
830
+ fontSize: 14,
831
+ padding: "2px 4px",
832
+ borderRadius: 4
833
+ },
834
+ children: "\u2715"
835
+ }
836
+ )
837
+ ] }),
838
+ /* @__PURE__ */ jsx6(
839
+ "textarea",
840
+ {
841
+ style: {
842
+ width: "100%",
843
+ height: 80,
844
+ padding: "8px 10px",
845
+ border: "1px solid #e5e7eb",
846
+ borderRadius: 8,
847
+ fontSize: "0.8125rem",
848
+ resize: "none",
849
+ boxSizing: "border-box",
850
+ lineHeight: 1.5,
851
+ fontFamily: "inherit",
852
+ color: "#374151",
853
+ outline: "none"
854
+ },
855
+ placeholder: "Leave a comment...",
856
+ value: text,
857
+ onChange: (e) => setText(e.target.value),
858
+ onKeyDown: handleKeyDown,
859
+ autoFocus: true
860
+ }
861
+ ),
862
+ /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 10 }, children: [
863
+ /* @__PURE__ */ jsx6(
864
+ "button",
865
+ {
866
+ style: {
867
+ padding: "6px 12px",
868
+ fontSize: "0.8125rem",
869
+ border: "1px solid #e5e7eb",
870
+ borderRadius: 6,
871
+ backgroundColor: "#fff",
872
+ cursor: "pointer",
873
+ color: "#6b7280"
874
+ },
875
+ onClick: handleCancel,
876
+ children: "Cancel"
877
+ }
878
+ ),
879
+ /* @__PURE__ */ jsx6(
880
+ "button",
881
+ {
882
+ style: {
883
+ padding: "6px 12px",
884
+ fontSize: "0.8125rem",
885
+ border: "none",
886
+ borderRadius: 6,
887
+ backgroundColor: accentColor,
888
+ color: "#fff",
889
+ cursor: text.trim() ? "pointer" : "not-allowed",
890
+ opacity: text.trim() ? 1 : 0.5
891
+ },
892
+ onClick: handleSubmit,
893
+ disabled: !text.trim(),
894
+ children: "Add"
895
+ }
896
+ )
897
+ ] })
898
+ ]
899
+ }
900
+ )
901
+ ] });
902
+ }
903
+
904
+ // src/WashiUI.tsx
905
+ import { useState as useState6 } from "react";
906
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
907
+ var SPINNER_ID = "__washi-ui-spinner__";
908
+ if (typeof document !== "undefined" && !document.getElementById(SPINNER_ID)) {
909
+ const style = document.createElement("style");
910
+ style.id = SPINNER_ID;
911
+ style.textContent = `@keyframes __washi-spin { to { transform: rotate(360deg); } }`;
912
+ document.head.appendChild(style);
913
+ }
914
+ function WashiUI({
915
+ position = "bottom-right",
916
+ accentColor = "#667eea",
917
+ showLoader = true
918
+ }) {
919
+ const { isReady } = useWashiContext();
920
+ const [sidebarOpen, setSidebarOpen] = useState6(false);
921
+ return /* @__PURE__ */ jsxs4(Fragment3, { children: [
922
+ showLoader && !isReady && /* @__PURE__ */ jsx7(
923
+ "div",
924
+ {
925
+ style: {
926
+ position: "fixed",
927
+ inset: 0,
928
+ zIndex: 100,
929
+ display: "flex",
930
+ alignItems: "center",
931
+ justifyContent: "center",
932
+ backgroundColor: "rgba(255,255,255,0.9)"
933
+ },
934
+ children: /* @__PURE__ */ jsx7(
935
+ "div",
936
+ {
937
+ style: {
938
+ width: 40,
939
+ height: 40,
940
+ borderRadius: "50%",
941
+ border: "3px solid #e5e7eb",
942
+ borderTopColor: accentColor,
943
+ animation: "__washi-spin 1s linear infinite"
944
+ }
945
+ }
946
+ )
947
+ }
948
+ ),
949
+ /* @__PURE__ */ jsx7(
950
+ WashiToolBubble,
951
+ {
952
+ position,
953
+ accentColor,
954
+ sidebarOpen,
955
+ onSidebarToggle: () => setSidebarOpen((o) => !o)
956
+ }
957
+ ),
958
+ /* @__PURE__ */ jsx7(
959
+ WashiCommentsSidebar,
960
+ {
961
+ open: sidebarOpen,
962
+ onClose: () => setSidebarOpen(false),
963
+ position,
964
+ accentColor
965
+ }
966
+ ),
967
+ /* @__PURE__ */ jsx7(WashiPinDialog, { accentColor })
968
+ ] });
969
+ }
970
+ export {
971
+ CommentList,
972
+ WashiCommentsSidebar,
973
+ WashiFrame,
974
+ WashiPinDialog,
975
+ WashiProvider,
976
+ WashiToolBubble,
977
+ WashiUI,
978
+ useWashi,
979
+ useWashiContext
980
+ };
981
+ //# sourceMappingURL=index.mjs.map