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