@tonytangdev/pin-point 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,941 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ FeedbackOverlay: () => FeedbackOverlay
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/FeedbackOverlay.tsx
28
+ var import_react6 = require("react");
29
+
30
+ // src/components/AdminKeyModal.tsx
31
+ var import_react = require("react");
32
+ var import_jsx_runtime = require("react/jsx-runtime");
33
+ function AdminKeyModal({ onValidate, onSuccess, onClose }) {
34
+ const [value, setValue] = (0, import_react.useState)("");
35
+ const [error, setError] = (0, import_react.useState)(null);
36
+ const [busy, setBusy] = (0, import_react.useState)(false);
37
+ const handleSubmit = async () => {
38
+ setBusy(true);
39
+ setError(null);
40
+ try {
41
+ const ok = await onValidate(value);
42
+ if (ok) {
43
+ onSuccess(value);
44
+ } else {
45
+ setError("Invalid admin key");
46
+ }
47
+ } catch {
48
+ setError("Could not reach server");
49
+ } finally {
50
+ setBusy(false);
51
+ }
52
+ };
53
+ return (
54
+ // biome-ignore lint/a11y/useKeyWithClickEvents: backdrop click dismissal, Cancel button and Escape are primary dismissal paths
55
+ // biome-ignore lint/a11y/noStaticElementInteractions: backdrop is a visual scrim, inner dialog has role
56
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pp-modal-backdrop", onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
57
+ "div",
58
+ {
59
+ className: "pp-modal",
60
+ onClick: (e) => e.stopPropagation(),
61
+ role: "dialog",
62
+ "aria-modal": "true",
63
+ children: [
64
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { children: "Enter admin key" }),
65
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
66
+ "input",
67
+ {
68
+ type: "password",
69
+ placeholder: "Admin key",
70
+ value,
71
+ onChange: (e) => setValue(e.target.value),
72
+ disabled: busy
73
+ }
74
+ ),
75
+ error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pp-modal-error", children: error }),
76
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "pp-modal-actions", children: [
77
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: onClose, disabled: busy, children: "Cancel" }),
78
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
79
+ "button",
80
+ {
81
+ type: "button",
82
+ onClick: handleSubmit,
83
+ disabled: busy || !value,
84
+ children: "Save"
85
+ }
86
+ )
87
+ ] })
88
+ ]
89
+ }
90
+ ) })
91
+ );
92
+ }
93
+
94
+ // src/components/ClickInterceptLayer.tsx
95
+ var import_react2 = require("react");
96
+ var import_jsx_runtime2 = (
97
+ // biome-ignore lint/a11y/noStaticElementInteractions: click-only overlay for pin placement
98
+ // biome-ignore lint/a11y/useKeyWithClickEvents: click-only overlay for pin placement
99
+ require("react/jsx-runtime")
100
+ );
101
+ function ClickInterceptLayer({ onClick }) {
102
+ const lastClickTime = (0, import_react2.useRef)(0);
103
+ const handleClick = (0, import_react2.useCallback)(
104
+ (e) => {
105
+ const now = Date.now();
106
+ if (now - lastClickTime.current < 300) {
107
+ return;
108
+ }
109
+ lastClickTime.current = now;
110
+ onClick(e.clientX, e.clientY);
111
+ },
112
+ [onClick]
113
+ );
114
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "pp-intercept", onClick: handleClick });
115
+ }
116
+
117
+ // src/components/CommentPopover.tsx
118
+ var import_react3 = require("react");
119
+ var import_jsx_runtime3 = (
120
+ // biome-ignore lint/a11y/useKeyWithClickEvents: stopPropagation only, not interactive
121
+ require("react/jsx-runtime")
122
+ );
123
+ var POPOVER_OFFSET = 14;
124
+ var VIEWPORT_MARGIN = 8;
125
+ function CommentPopover(props) {
126
+ const { mode, top, left } = props;
127
+ const popoverRef = (0, import_react3.useRef)(null);
128
+ const [placement, setPlacement] = (0, import_react3.useState)({
129
+ x: "right",
130
+ y: "bottom"
131
+ });
132
+ const [size, setSize] = (0, import_react3.useState)(
133
+ null
134
+ );
135
+ (0, import_react3.useLayoutEffect)(() => {
136
+ const el = popoverRef.current;
137
+ if (!el) return;
138
+ const rect = el.getBoundingClientRect();
139
+ const w = rect.width;
140
+ const h = rect.height;
141
+ const pinViewX = left - window.scrollX;
142
+ const pinViewY = top - window.scrollY;
143
+ const fitsRight = pinViewX - POPOVER_OFFSET + w + VIEWPORT_MARGIN <= window.innerWidth;
144
+ const fitsLeft = pinViewX + POPOVER_OFFSET - w - VIEWPORT_MARGIN >= 0;
145
+ const fitsBottom = pinViewY + POPOVER_OFFSET + h + VIEWPORT_MARGIN <= window.innerHeight;
146
+ const fitsTop = pinViewY - POPOVER_OFFSET - h - VIEWPORT_MARGIN >= 0;
147
+ setPlacement({
148
+ x: fitsRight ? "right" : fitsLeft ? "left" : "right",
149
+ y: fitsBottom ? "bottom" : fitsTop ? "top" : "bottom"
150
+ });
151
+ setSize({ width: w, height: h });
152
+ }, [top, left]);
153
+ const style = size ? {
154
+ top: `${placement.y === "bottom" ? top + POPOVER_OFFSET : top - POPOVER_OFFSET - size.height}px`,
155
+ left: `${placement.x === "right" ? left - POPOVER_OFFSET : left + POPOVER_OFFSET - size.width}px`
156
+ } : {
157
+ top: `${top + POPOVER_OFFSET}px`,
158
+ left: `${left - POPOVER_OFFSET}px`,
159
+ visibility: "hidden"
160
+ };
161
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
162
+ "div",
163
+ {
164
+ ref: popoverRef,
165
+ className: "pp-popover",
166
+ "data-placement-x": placement.x,
167
+ "data-placement-y": placement.y,
168
+ style,
169
+ onClick: (e) => e.stopPropagation(),
170
+ role: "dialog",
171
+ children: [
172
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "pp-popover-arrow" }),
173
+ mode === "read" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
174
+ ReadContent,
175
+ {
176
+ content: props.content,
177
+ createdAt: props.createdAt,
178
+ viewportWidth: props.viewportWidth,
179
+ onDelete: props.onDelete,
180
+ onUpdate: props.onUpdate
181
+ }
182
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CreateContent, { onSubmit: props.onSubmit, onCancel: props.onCancel })
183
+ ]
184
+ }
185
+ );
186
+ }
187
+ function ReadContent({
188
+ content,
189
+ createdAt,
190
+ viewportWidth,
191
+ onDelete,
192
+ onUpdate
193
+ }) {
194
+ const [isConfirmingDelete, setIsConfirmingDelete] = (0, import_react3.useState)(false);
195
+ const [isEditing, setIsEditing] = (0, import_react3.useState)(false);
196
+ const [editContent, setEditContent] = (0, import_react3.useState)(content);
197
+ const [saving, setSaving] = (0, import_react3.useState)(false);
198
+ const [deleting, setDeleting] = (0, import_react3.useState)(false);
199
+ const [deleteError, setDeleteError] = (0, import_react3.useState)(false);
200
+ const [editError, setEditError] = (0, import_react3.useState)(false);
201
+ (0, import_react3.useEffect)(() => setEditContent(content), [content]);
202
+ const date = new Date(createdAt).toLocaleDateString(void 0, {
203
+ month: "short",
204
+ day: "numeric",
205
+ year: "numeric"
206
+ });
207
+ if (isEditing) {
208
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
209
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
210
+ "textarea",
211
+ {
212
+ className: "pp-popover-textarea",
213
+ value: editContent,
214
+ onChange: (e) => setEditContent(e.target.value)
215
+ }
216
+ ),
217
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "pp-popover-actions", children: [
218
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
219
+ "button",
220
+ {
221
+ type: "button",
222
+ className: "pp-btn pp-btn--cancel",
223
+ onClick: () => {
224
+ setIsEditing(false);
225
+ setEditContent(content);
226
+ setEditError(false);
227
+ },
228
+ children: "Cancel"
229
+ }
230
+ ),
231
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
232
+ "button",
233
+ {
234
+ type: "button",
235
+ className: "pp-btn pp-btn--submit",
236
+ disabled: editContent.trim().length === 0 || saving,
237
+ onClick: async () => {
238
+ setSaving(true);
239
+ setEditError(false);
240
+ try {
241
+ await onUpdate?.(editContent);
242
+ setIsEditing(false);
243
+ } catch {
244
+ setEditError(true);
245
+ } finally {
246
+ setSaving(false);
247
+ }
248
+ },
249
+ children: "Save"
250
+ }
251
+ )
252
+ ] }),
253
+ editError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "pp-popover-error", children: "Couldn't save. Try again." })
254
+ ] });
255
+ }
256
+ if (isConfirmingDelete) {
257
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "pp-delete-confirm", children: [
258
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "pp-delete-confirm-icon", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
259
+ "svg",
260
+ {
261
+ "aria-hidden": "true",
262
+ width: "20",
263
+ height: "20",
264
+ viewBox: "0 0 24 24",
265
+ fill: "none",
266
+ stroke: "currentColor",
267
+ strokeWidth: "2",
268
+ strokeLinecap: "round",
269
+ strokeLinejoin: "round",
270
+ children: [
271
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
272
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
273
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })
274
+ ]
275
+ }
276
+ ) }),
277
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "pp-delete-confirm-text", children: "Delete this comment?" }),
278
+ deleteError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "pp-popover-error", children: "Couldn't delete. Try again." }),
279
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "pp-delete-confirm-actions", children: [
280
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
281
+ "button",
282
+ {
283
+ type: "button",
284
+ className: "pp-btn pp-btn--cancel",
285
+ disabled: deleting,
286
+ onClick: () => {
287
+ setIsConfirmingDelete(false);
288
+ setDeleteError(false);
289
+ },
290
+ children: "Cancel"
291
+ }
292
+ ),
293
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
294
+ "button",
295
+ {
296
+ type: "button",
297
+ className: "pp-btn pp-btn--danger",
298
+ disabled: deleting,
299
+ onClick: async () => {
300
+ setDeleting(true);
301
+ setDeleteError(false);
302
+ try {
303
+ await onDelete?.();
304
+ } catch {
305
+ setDeleteError(true);
306
+ } finally {
307
+ setDeleting(false);
308
+ }
309
+ },
310
+ children: deleting ? "Deleting..." : "Delete"
311
+ }
312
+ )
313
+ ] })
314
+ ] });
315
+ }
316
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
317
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "pp-popover-content", children: content }),
318
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "pp-popover-footer", children: [
319
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "pp-popover-meta", children: [
320
+ date,
321
+ " \xB7 ",
322
+ viewportWidth,
323
+ "px viewport"
324
+ ] }),
325
+ (onDelete || onUpdate) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "pp-popover-actions-row", children: [
326
+ onUpdate && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
327
+ "button",
328
+ {
329
+ type: "button",
330
+ className: "pp-action-btn",
331
+ "aria-label": "Edit",
332
+ onClick: () => setIsEditing(true),
333
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
334
+ "svg",
335
+ {
336
+ "aria-hidden": "true",
337
+ width: "13",
338
+ height: "13",
339
+ viewBox: "0 0 24 24",
340
+ fill: "none",
341
+ stroke: "currentColor",
342
+ strokeWidth: "2",
343
+ strokeLinecap: "round",
344
+ strokeLinejoin: "round",
345
+ children: [
346
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" }),
347
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "m15 5 4 4" })
348
+ ]
349
+ }
350
+ )
351
+ }
352
+ ),
353
+ onDelete && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
354
+ "button",
355
+ {
356
+ type: "button",
357
+ className: "pp-action-btn pp-action-btn--danger",
358
+ "aria-label": "Delete",
359
+ onClick: () => setIsConfirmingDelete(true),
360
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
361
+ "svg",
362
+ {
363
+ "aria-hidden": "true",
364
+ width: "13",
365
+ height: "13",
366
+ viewBox: "0 0 24 24",
367
+ fill: "none",
368
+ stroke: "currentColor",
369
+ strokeWidth: "2",
370
+ strokeLinecap: "round",
371
+ strokeLinejoin: "round",
372
+ children: [
373
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M3 6h18" }),
374
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }),
375
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" })
376
+ ]
377
+ }
378
+ )
379
+ }
380
+ )
381
+ ] })
382
+ ] })
383
+ ] });
384
+ }
385
+ function CreateContent({
386
+ onSubmit,
387
+ onCancel
388
+ }) {
389
+ const [text, setText] = (0, import_react3.useState)("");
390
+ const [error, setError] = (0, import_react3.useState)(null);
391
+ const [submitting, setSubmitting] = (0, import_react3.useState)(false);
392
+ const handleSubmit = async () => {
393
+ setError(null);
394
+ setSubmitting(true);
395
+ try {
396
+ await onSubmit(text);
397
+ } catch {
398
+ setError("Couldn't save. Try again.");
399
+ } finally {
400
+ setSubmitting(false);
401
+ }
402
+ };
403
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
404
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
405
+ "textarea",
406
+ {
407
+ className: "pp-popover-textarea",
408
+ placeholder: "Leave your feedback...",
409
+ value: text,
410
+ onChange: (e) => setText(e.target.value)
411
+ }
412
+ ),
413
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "pp-popover-actions", children: [
414
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
415
+ "button",
416
+ {
417
+ type: "button",
418
+ className: "pp-btn pp-btn--cancel",
419
+ onClick: onCancel,
420
+ children: "Cancel"
421
+ }
422
+ ),
423
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
424
+ "button",
425
+ {
426
+ type: "button",
427
+ className: "pp-btn pp-btn--submit",
428
+ onClick: handleSubmit,
429
+ disabled: text.trim().length === 0 || submitting,
430
+ children: "Submit"
431
+ }
432
+ )
433
+ ] }),
434
+ error && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "pp-popover-error", children: error })
435
+ ] });
436
+ }
437
+
438
+ // src/components/FeedbackToolbar.tsx
439
+ var import_jsx_runtime4 = require("react/jsx-runtime");
440
+ var CommentIcon = () => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
441
+ "svg",
442
+ {
443
+ "aria-hidden": "true",
444
+ width: "16",
445
+ height: "16",
446
+ viewBox: "0 0 24 24",
447
+ fill: "none",
448
+ stroke: "currentColor",
449
+ strokeWidth: "2",
450
+ strokeLinecap: "round",
451
+ strokeLinejoin: "round",
452
+ 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" })
453
+ }
454
+ );
455
+ var KeyIcon = () => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
456
+ "svg",
457
+ {
458
+ "aria-hidden": "true",
459
+ width: "16",
460
+ height: "16",
461
+ viewBox: "0 0 24 24",
462
+ fill: "none",
463
+ stroke: "currentColor",
464
+ strokeWidth: "2",
465
+ strokeLinecap: "round",
466
+ strokeLinejoin: "round",
467
+ children: [
468
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "m21 2-9.6 9.6" }),
469
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("circle", { cx: "7.5", cy: "15.5", r: "5.5" }),
470
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "m15.5 7.5 3 3" })
471
+ ]
472
+ }
473
+ );
474
+ function FeedbackToolbar({
475
+ auth,
476
+ commentCount,
477
+ pinModeActive,
478
+ onPinModeToggle,
479
+ onAdminKeyOpen,
480
+ shareButton,
481
+ error
482
+ }) {
483
+ const commentDisabled = auth.role === "anonymous";
484
+ const commentTitle = commentDisabled ? "Sign in with a share link to leave feedback" : pinModeActive ? "Exit pin mode" : "Leave feedback";
485
+ const showAdminKey = auth.role !== "tokenHolder";
486
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "pp-toolbar", children: [
487
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "pp-toolbar-dot" }),
488
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
489
+ "button",
490
+ {
491
+ type: "button",
492
+ className: "pp-toolbar-btn",
493
+ onClick: onPinModeToggle,
494
+ disabled: commentDisabled,
495
+ "aria-pressed": pinModeActive,
496
+ "aria-label": "Leave feedback",
497
+ title: commentTitle,
498
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CommentIcon, {})
499
+ }
500
+ ),
501
+ showAdminKey && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
502
+ "button",
503
+ {
504
+ type: "button",
505
+ className: "pp-toolbar-btn",
506
+ onClick: onAdminKeyOpen,
507
+ "aria-label": "Enter admin key",
508
+ title: "Enter admin key",
509
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(KeyIcon, {})
510
+ }
511
+ ),
512
+ shareButton,
513
+ error ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "pp-toolbar-error", children: error }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "pp-toolbar-badge", children: [
514
+ commentCount,
515
+ " ",
516
+ commentCount === 1 ? "comment" : "comments"
517
+ ] })
518
+ ] });
519
+ }
520
+
521
+ // src/components/PinMarker.tsx
522
+ var import_jsx_runtime5 = require("react/jsx-runtime");
523
+ function PinMarker({ number, top, left, onClick }) {
524
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
525
+ "button",
526
+ {
527
+ type: "button",
528
+ className: "pp-pin",
529
+ style: { top: `${top}px`, left: `${left}px` },
530
+ onClick: (e) => {
531
+ e.stopPropagation();
532
+ onClick();
533
+ },
534
+ children: number
535
+ }
536
+ );
537
+ }
538
+
539
+ // src/components/ShareLinkButton.tsx
540
+ var import_react4 = require("react");
541
+ var import_jsx_runtime6 = require("react/jsx-runtime");
542
+ var ShareIcon = () => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
543
+ "svg",
544
+ {
545
+ "aria-hidden": "true",
546
+ width: "14",
547
+ height: "14",
548
+ viewBox: "0 0 24 24",
549
+ fill: "none",
550
+ stroke: "currentColor",
551
+ strokeWidth: "2.2",
552
+ strokeLinecap: "round",
553
+ strokeLinejoin: "round",
554
+ children: [
555
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" }),
556
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("polyline", { points: "16 6 12 2 8 6" }),
557
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "12", y1: "2", x2: "12", y2: "15" })
558
+ ]
559
+ }
560
+ );
561
+ var CheckIcon = () => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
562
+ "svg",
563
+ {
564
+ "aria-hidden": "true",
565
+ width: "14",
566
+ height: "14",
567
+ viewBox: "0 0 24 24",
568
+ fill: "none",
569
+ stroke: "currentColor",
570
+ strokeWidth: "2.5",
571
+ strokeLinecap: "round",
572
+ strokeLinejoin: "round",
573
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("polyline", { points: "20 6 9 17 4 12" })
574
+ }
575
+ );
576
+ function ShareLinkButton({ onCreate }) {
577
+ const [busy, setBusy] = (0, import_react4.useState)(false);
578
+ const [copied, setCopied] = (0, import_react4.useState)(false);
579
+ const handleClick = async () => {
580
+ setBusy(true);
581
+ try {
582
+ const { tokenId } = await onCreate();
583
+ const url = new URL(window.location.href);
584
+ url.searchParams.set("pin-token", tokenId);
585
+ await navigator.clipboard.writeText(url.toString());
586
+ setCopied(true);
587
+ setTimeout(() => setCopied(false), 2e3);
588
+ } finally {
589
+ setBusy(false);
590
+ }
591
+ };
592
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "pp-share-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
593
+ "button",
594
+ {
595
+ type: "button",
596
+ className: "pp-share-btn",
597
+ onClick: handleClick,
598
+ disabled: busy,
599
+ "data-state": copied ? "copied" : busy ? "busy" : "idle",
600
+ children: [
601
+ copied ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CheckIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ShareIcon, {}),
602
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: copied ? "Link copied" : "Share for feedback" })
603
+ ]
604
+ }
605
+ ) });
606
+ }
607
+
608
+ // src/hooks/useAuth.ts
609
+ var import_react5 = require("react");
610
+ var ADMIN_KEY_STORAGE = "pin-admin-key";
611
+ var TOKEN_QUERY_PARAM = "pin-token";
612
+ var resolveAuth = () => {
613
+ if (typeof window === "undefined") return { role: "anonymous" };
614
+ const params = new URLSearchParams(window.location.search);
615
+ const token = params.get(TOKEN_QUERY_PARAM);
616
+ if (token) return { role: "tokenHolder", token };
617
+ const adminKey = localStorage.getItem(ADMIN_KEY_STORAGE);
618
+ if (adminKey) return { role: "admin", secret: adminKey };
619
+ return { role: "anonymous" };
620
+ };
621
+ var computeHeaders = (auth) => {
622
+ switch (auth.role) {
623
+ case "tokenHolder":
624
+ return { "X-Pin-Token": auth.token };
625
+ case "admin":
626
+ return { "X-Pin-Admin": auth.secret };
627
+ case "anonymous":
628
+ return {};
629
+ }
630
+ };
631
+ var useAuth = () => {
632
+ const [auth, setAuth] = (0, import_react5.useState)(resolveAuth);
633
+ (0, import_react5.useEffect)(() => {
634
+ const onPopState = () => setAuth(resolveAuth());
635
+ window.addEventListener("popstate", onPopState);
636
+ return () => window.removeEventListener("popstate", onPopState);
637
+ }, []);
638
+ const setAdminKey = (0, import_react5.useCallback)((secret) => {
639
+ localStorage.setItem(ADMIN_KEY_STORAGE, secret);
640
+ setAuth(resolveAuth());
641
+ }, []);
642
+ const clearAdminKey = (0, import_react5.useCallback)(() => {
643
+ localStorage.removeItem(ADMIN_KEY_STORAGE);
644
+ setAuth(resolveAuth());
645
+ }, []);
646
+ return {
647
+ auth,
648
+ authHeaders: computeHeaders(auth),
649
+ setAdminKey,
650
+ clearAdminKey
651
+ };
652
+ };
653
+
654
+ // src/utils/resolveAnchor.ts
655
+ var DATA_ATTR_PRIORITY = ["data-testid", "data-cy"];
656
+ function findAnchorElement(element) {
657
+ if (element.id) {
658
+ return { el: element, selector: `#${CSS.escape(element.id)}` };
659
+ }
660
+ let current = element.parentElement;
661
+ while (current && current !== document.body) {
662
+ if (current.id) {
663
+ return { el: current, selector: `#${CSS.escape(current.id)}` };
664
+ }
665
+ current = current.parentElement;
666
+ }
667
+ current = element;
668
+ while (current && current !== document.body) {
669
+ for (const attr of DATA_ATTR_PRIORITY) {
670
+ const value = current.getAttribute(attr);
671
+ if (value) {
672
+ return { el: current, selector: `[${attr}="${CSS.escape(value)}"]` };
673
+ }
674
+ }
675
+ for (const attr of current.getAttributeNames()) {
676
+ if (attr.startsWith("data-") && !DATA_ATTR_PRIORITY.includes(attr)) {
677
+ const value = current.getAttribute(attr);
678
+ return {
679
+ el: current,
680
+ selector: `[${attr}="${CSS.escape(value ?? "")}"]`
681
+ };
682
+ }
683
+ }
684
+ current = current.parentElement;
685
+ }
686
+ return { el: element, selector: buildStructuralSelector(element) };
687
+ }
688
+ function buildStructuralSelector(element) {
689
+ const parts = [];
690
+ let current = element;
691
+ let depth = 0;
692
+ while (current && current !== document.body && depth < 3) {
693
+ const tag = current.tagName.toLowerCase();
694
+ const parent = current.parentElement;
695
+ if (parent) {
696
+ const siblings = Array.from(parent.children).filter(
697
+ (c) => c.tagName === current?.tagName
698
+ );
699
+ if (siblings.length > 1) {
700
+ const index = siblings.indexOf(current) + 1;
701
+ parts.unshift(`${tag}:nth-of-type(${index})`);
702
+ } else {
703
+ parts.unshift(tag);
704
+ }
705
+ } else {
706
+ parts.unshift(tag);
707
+ }
708
+ current = parent;
709
+ depth++;
710
+ }
711
+ return parts.join(" > ");
712
+ }
713
+ function computePercentages(anchorEl, clickX, clickY) {
714
+ const rect = anchorEl.getBoundingClientRect();
715
+ const width = rect.width || 1;
716
+ const height = rect.height || 1;
717
+ const xPercent = (clickX - rect.left) / width * 100;
718
+ const yPercent = (clickY - rect.top) / height * 100;
719
+ return { xPercent, yPercent };
720
+ }
721
+ function resolveAnchor(element, clickX, clickY) {
722
+ const { el, selector } = findAnchorElement(element);
723
+ const { xPercent, yPercent } = computePercentages(el, clickX, clickY);
724
+ return { selector, xPercent, yPercent };
725
+ }
726
+
727
+ // src/utils/restorePosition.ts
728
+ function restorePosition(anchor) {
729
+ if (!anchor.selector) {
730
+ return null;
731
+ }
732
+ let el;
733
+ try {
734
+ el = document.querySelector(anchor.selector);
735
+ } catch {
736
+ return null;
737
+ }
738
+ if (!el) {
739
+ return null;
740
+ }
741
+ const rect = el.getBoundingClientRect();
742
+ const scrollX = window.scrollX;
743
+ const scrollY = window.scrollY;
744
+ const left = rect.left + scrollX + rect.width * anchor.xPercent / 100;
745
+ const top = rect.top + scrollY + rect.height * anchor.yPercent / 100;
746
+ return { top, left };
747
+ }
748
+
749
+ // src/FeedbackOverlay.tsx
750
+ var import_jsx_runtime7 = require("react/jsx-runtime");
751
+ function FeedbackOverlay({
752
+ onCommentCreate,
753
+ onCommentsFetch,
754
+ onCommentDelete,
755
+ onCommentUpdate,
756
+ onAdminValidate,
757
+ onShareLinkCreate,
758
+ children
759
+ }) {
760
+ const { auth, authHeaders, setAdminKey } = useAuth();
761
+ const [comments, setComments] = (0, import_react6.useState)([]);
762
+ const [pendingPin, setPendingPin] = (0, import_react6.useState)(null);
763
+ const [expandedPinId, setExpandedPinId] = (0, import_react6.useState)(null);
764
+ const [fetchError, setFetchError] = (0, import_react6.useState)(null);
765
+ const [pinMode, setPinMode] = (0, import_react6.useState)(false);
766
+ const [adminModalOpen, setAdminModalOpen] = (0, import_react6.useState)(false);
767
+ const hasFetched = (0, import_react6.useRef)(false);
768
+ (0, import_react6.useEffect)(() => {
769
+ if (hasFetched.current) return;
770
+ hasFetched.current = true;
771
+ onCommentsFetch(authHeaders).then((data) => setComments([...data])).catch(() => setFetchError("Couldn't load comments."));
772
+ }, [onCommentsFetch, authHeaders]);
773
+ const handleClick = (0, import_react6.useCallback)((clientX, clientY) => {
774
+ setExpandedPinId(null);
775
+ const interceptLayer = document.querySelector(
776
+ ".pp-intercept"
777
+ );
778
+ if (interceptLayer) interceptLayer.style.pointerEvents = "none";
779
+ const elementBelow = document.elementFromPoint(clientX, clientY);
780
+ if (interceptLayer) interceptLayer.style.pointerEvents = "";
781
+ if (!elementBelow) return;
782
+ const anchor = resolveAnchor(elementBelow, clientX, clientY);
783
+ setPendingPin({
784
+ x: clientX + window.scrollX,
785
+ y: clientY + window.scrollY,
786
+ anchor
787
+ });
788
+ }, []);
789
+ const handleSubmit = (0, import_react6.useCallback)(
790
+ async (content) => {
791
+ if (!pendingPin) return;
792
+ const comment = {
793
+ id: crypto.randomUUID(),
794
+ url: window.location.pathname,
795
+ content,
796
+ anchor: pendingPin.anchor,
797
+ viewport: { width: window.innerWidth },
798
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
799
+ };
800
+ await onCommentCreate(comment, authHeaders);
801
+ setComments((prev) => [...prev, comment]);
802
+ setPendingPin(null);
803
+ setPinMode(false);
804
+ },
805
+ [pendingPin, onCommentCreate, authHeaders]
806
+ );
807
+ const handleCancel = (0, import_react6.useCallback)(() => {
808
+ setPendingPin(null);
809
+ }, []);
810
+ const handleDelete = (0, import_react6.useCallback)(
811
+ async (id) => {
812
+ if (!onCommentDelete) return;
813
+ await onCommentDelete(id, authHeaders);
814
+ setComments((prev) => prev.filter((c) => c.id !== id));
815
+ setExpandedPinId(null);
816
+ },
817
+ [onCommentDelete, authHeaders]
818
+ );
819
+ const handleUpdate = (0, import_react6.useCallback)(
820
+ async (id, content) => {
821
+ if (!onCommentUpdate) return;
822
+ const updated = await onCommentUpdate(id, content, authHeaders);
823
+ setComments((prev) => prev.map((c) => c.id === id ? updated : c));
824
+ },
825
+ [onCommentUpdate, authHeaders]
826
+ );
827
+ const handlePinModeToggle = (0, import_react6.useCallback)(() => {
828
+ setPinMode((prev) => {
829
+ const next = !prev;
830
+ if (!next) setPendingPin(null);
831
+ return next;
832
+ });
833
+ }, []);
834
+ const handleAdminValidate = (0, import_react6.useCallback)(
835
+ async (secret) => {
836
+ if (!onAdminValidate) return false;
837
+ return onAdminValidate(secret);
838
+ },
839
+ [onAdminValidate]
840
+ );
841
+ const handleAdminSuccess = (0, import_react6.useCallback)(
842
+ (secret) => {
843
+ setAdminKey(secret);
844
+ setAdminModalOpen(false);
845
+ },
846
+ [setAdminKey]
847
+ );
848
+ const handleShareCreate = (0, import_react6.useCallback)(
849
+ async (label, ttl) => {
850
+ if (!onShareLinkCreate) throw new Error("onShareLinkCreate not provided");
851
+ return onShareLinkCreate(label, ttl, authHeaders);
852
+ },
853
+ [onShareLinkCreate, authHeaders]
854
+ );
855
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { "data-pin-point": "", children: [
856
+ children,
857
+ pinMode && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ClickInterceptLayer, { onClick: handleClick }),
858
+ comments.map((comment, index) => {
859
+ const pos = restorePosition(comment.anchor);
860
+ if (!pos) return null;
861
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
862
+ PinMarker,
863
+ {
864
+ number: index + 1,
865
+ top: pos.top,
866
+ left: pos.left,
867
+ onClick: () => setExpandedPinId(expandedPinId === comment.id ? null : comment.id)
868
+ },
869
+ comment.id
870
+ );
871
+ }),
872
+ expandedPinId && (() => {
873
+ const comment = comments.find((c) => c.id === expandedPinId);
874
+ if (!comment) return null;
875
+ const pos = restorePosition(comment.anchor);
876
+ if (!pos) return null;
877
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
878
+ CommentPopover,
879
+ {
880
+ mode: "read",
881
+ content: comment.content,
882
+ createdAt: comment.createdAt,
883
+ viewportWidth: comment.viewport.width,
884
+ top: pos.top,
885
+ left: pos.left,
886
+ onDelete: onCommentDelete ? async () => handleDelete(comment.id) : void 0,
887
+ onUpdate: onCommentUpdate ? async (content) => {
888
+ await handleUpdate(comment.id, content);
889
+ } : void 0
890
+ }
891
+ );
892
+ })(),
893
+ pendingPin && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
894
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
895
+ PinMarker,
896
+ {
897
+ number: comments.length + 1,
898
+ top: pendingPin.y,
899
+ left: pendingPin.x,
900
+ onClick: () => {
901
+ }
902
+ }
903
+ ),
904
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
905
+ CommentPopover,
906
+ {
907
+ mode: "create",
908
+ top: pendingPin.y,
909
+ left: pendingPin.x,
910
+ onSubmit: handleSubmit,
911
+ onCancel: handleCancel
912
+ }
913
+ )
914
+ ] }),
915
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
916
+ FeedbackToolbar,
917
+ {
918
+ auth,
919
+ commentCount: comments.length,
920
+ pinModeActive: pinMode,
921
+ onPinModeToggle: handlePinModeToggle,
922
+ onAdminKeyOpen: () => setAdminModalOpen(true),
923
+ shareButton: auth.role === "admin" && onShareLinkCreate ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ShareLinkButton, { onCreate: handleShareCreate }) : void 0,
924
+ error: fetchError ?? void 0
925
+ }
926
+ ),
927
+ adminModalOpen && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
928
+ AdminKeyModal,
929
+ {
930
+ onValidate: handleAdminValidate,
931
+ onSuccess: handleAdminSuccess,
932
+ onClose: () => setAdminModalOpen(false)
933
+ }
934
+ )
935
+ ] });
936
+ }
937
+ // Annotate the CommonJS export names for ESM import in node:
938
+ 0 && (module.exports = {
939
+ FeedbackOverlay
940
+ });
941
+ //# sourceMappingURL=index.js.map