@pixelatlarge/peanut-gallery-widget 0.0.0-stage → 0.1.1

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.cjs ADDED
@@ -0,0 +1,648 @@
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 src_exports = {};
22
+ __export(src_exports, {
23
+ PeanutGalleryWidget: () => PeanutGalleryWidget,
24
+ destroy: () => destroy,
25
+ init: () => init
26
+ });
27
+ module.exports = __toCommonJS(src_exports);
28
+
29
+ // src/widget.ts
30
+ var import_peanut_gallery_core = require("@pixelatlarge/peanut-gallery-core");
31
+
32
+ // src/styles.ts
33
+ var css = `
34
+ :host { all: initial; }
35
+ * { box-sizing: border-box; font-family: var(--pg-font-body); font-size: 13px; line-height: 1.4; color: var(--pg-text); }
36
+ .pg-hidden { display: none !important; }
37
+
38
+ .pg-toolbar {
39
+ position: fixed; right: 16px; bottom: 16px; z-index: 2147483000;
40
+ display: flex; align-items: center; gap: 6px;
41
+ background: var(--pg-toolbar); border-radius: 999px; padding: 6px 8px 6px 14px;
42
+ box-shadow: 0 10px 28px rgba(21, 42, 74, .28); user-select: none;
43
+ }
44
+ .pg-toolbar .pg-brand { font-weight: 700; color: var(--pg-on-toolbar); margin-right: 4px; letter-spacing: .01em; }
45
+ .pg-btn {
46
+ appearance: none; border: 0; cursor: pointer; border-radius: 999px; padding: 6px 12px;
47
+ background: var(--pg-toolbar-button); color: var(--pg-on-toolbar); font-weight: 600;
48
+ }
49
+ .pg-btn:hover { background: var(--pg-toolbar-button-hover); }
50
+ .pg-btn.pg-primary { background: var(--pg-primary); color: var(--pg-on-primary); }
51
+ .pg-btn.pg-primary:hover { background: var(--pg-primary-hover); }
52
+ .pg-btn.pg-active, .pg-btn.pg-active:hover { background: var(--pg-accent); color: var(--pg-on-accent); }
53
+ .pg-btn:disabled { opacity: .5; cursor: default; }
54
+ .pg-btn:focus-visible, .pg-pin:focus-visible { outline: 2px solid var(--pg-accent); outline-offset: 2px; }
55
+ .pg-count { background: var(--pg-count); color: var(--pg-on-count); border-radius: 999px; padding: 1px 7px; font-weight: 700; margin-left: 6px; }
56
+
57
+ .pg-highlight {
58
+ position: absolute; z-index: 2147482000; pointer-events: none;
59
+ outline: 2px solid var(--pg-highlight); outline-offset: 2px; background: var(--pg-highlight-tint); border-radius: 3px;
60
+ }
61
+ .pg-tag {
62
+ position: absolute; top: -23px; left: -2px; background: var(--pg-highlight); color: var(--pg-on-primary); font-size: 11px;
63
+ padding: 2px 6px; border-radius: 4px; white-space: nowrap; font-family: var(--pg-font-mono);
64
+ }
65
+
66
+ .pg-pin {
67
+ position: absolute; z-index: 2147482500; width: 24px; height: 24px; margin: -12px 0 0 -12px;
68
+ border-radius: 999px 999px 999px 2px; background: var(--pg-accent); color: var(--pg-on-accent); font-weight: 700; font-size: 12px;
69
+ display: flex; align-items: center; justify-content: center; cursor: pointer;
70
+ box-shadow: 0 2px 8px rgba(21, 42, 74, .35); border: 2px solid var(--pg-surface);
71
+ }
72
+ .pg-pin.pg-low { background: var(--pg-pin-low); }
73
+ .pg-pin:hover { transform: scale(1.1); }
74
+
75
+ .pg-composer, .pg-panel {
76
+ position: absolute; z-index: 2147483100; background: var(--pg-surface); border-radius: 12px;
77
+ border: 1px solid var(--pg-line); box-shadow: 0 14px 40px rgba(21, 42, 74, .22); padding: 12px; width: 300px;
78
+ }
79
+ .pg-composer textarea {
80
+ width: 100%; min-height: 80px; resize: vertical; border: 1px solid var(--pg-line); border-radius: 8px; padding: 8px;
81
+ font-size: 13px; background: var(--pg-surface);
82
+ }
83
+ .pg-composer textarea:focus { outline: 2px solid var(--pg-primary); border-color: transparent; }
84
+ .pg-row { display: flex; gap: 6px; align-items: center; margin-top: 8px; }
85
+ .pg-row .pg-spacer { flex: 1; }
86
+ .pg-composer select { border: 1px solid var(--pg-line); border-radius: 8px; padding: 5px 6px; background: var(--pg-surface); }
87
+ .pg-meta { color: var(--pg-muted); font-size: 11px; margin-bottom: 6px; font-family: var(--pg-font-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
88
+ .pg-btn.pg-ghost { background: var(--pg-ghost); color: var(--pg-text); }
89
+ .pg-btn.pg-ghost:hover { background: var(--pg-ghost-hover); }
90
+ .pg-btn.pg-danger { background: var(--pg-danger); color: var(--pg-on-danger); }
91
+
92
+ .pg-panel { position: fixed; right: 16px; bottom: 64px; width: 340px; max-height: 70vh; overflow: auto; }
93
+ .pg-panel h3 { margin: 0 0 8px; font-family: var(--pg-font-display); font-weight: 400; font-size: 18px; color: var(--pg-heading); }
94
+ .pg-item { border-top: 1px solid var(--pg-line); padding: 8px 0; }
95
+ .pg-item .pg-text { white-space: pre-wrap; }
96
+ .pg-item .pg-meta { margin: 2px 0 0; }
97
+ .pg-item .pg-kind { display: inline-block; background: var(--pg-chip); color: var(--pg-on-chip); border-radius: 4px; padding: 0 5px; font-size: 11px; font-weight: 600; margin-right: 4px; }
98
+ .pg-empty { color: var(--pg-muted); padding: 12px 0; }
99
+ .pg-link { color: var(--pg-link); }
100
+ .pg-toast { position: fixed; left: 50%; bottom: 72px; transform: translateX(-50%); background: var(--pg-toast); color: var(--pg-on-toast); padding: 8px 14px; border-radius: 999px; z-index: 2147483200; font-weight: 500; }
101
+ `;
102
+
103
+ // src/widget.ts
104
+ var LOCATION_EVENT = "peanut-gallery:locationchange";
105
+ function normalizePageUrl(href) {
106
+ try {
107
+ const u = new URL(href);
108
+ for (const key of [...u.searchParams.keys()]) {
109
+ if (key === "peanut" || key.startsWith("utm_") || key === "fbclid" || key === "gclid") u.searchParams.delete(key);
110
+ }
111
+ const search = u.searchParams.toString();
112
+ const hash = /^#!?\//.test(u.hash) ? u.hash : "";
113
+ return u.origin + u.pathname + (search ? `?${search}` : "") + hash;
114
+ } catch {
115
+ return href;
116
+ }
117
+ }
118
+ function watchHistory() {
119
+ if (typeof history === "undefined") return;
120
+ const h = history;
121
+ if (h.__peanutGallery) return;
122
+ h.__peanutGallery = true;
123
+ for (const name of ["pushState", "replaceState"]) {
124
+ const original = h[name];
125
+ h[name] = function(...args) {
126
+ const result = original.apply(this, args);
127
+ try {
128
+ window.dispatchEvent(new Event(LOCATION_EVENT));
129
+ } catch {
130
+ }
131
+ return result;
132
+ };
133
+ }
134
+ }
135
+ var store = {
136
+ get(key) {
137
+ try {
138
+ return localStorage.getItem(key);
139
+ } catch {
140
+ return null;
141
+ }
142
+ },
143
+ set(key, value) {
144
+ try {
145
+ localStorage.setItem(key, value);
146
+ } catch {
147
+ }
148
+ }
149
+ };
150
+ var DEFAULT_KINDS = ["bug", "copy", "layout", "visual", "flow", "question", "praise"];
151
+ var PeanutGalleryWidget = class {
152
+ cfg;
153
+ host;
154
+ root;
155
+ session;
156
+ mode = "idle";
157
+ hoverEl = null;
158
+ highlight;
159
+ pinsLayer;
160
+ panel = null;
161
+ composer = null;
162
+ toolbar;
163
+ raf = 0;
164
+ destroyed = false;
165
+ observer;
166
+ /** The reviewer's recent clicks on this page (see Annotation.steps). */
167
+ steps = [];
168
+ stepsPage = "";
169
+ constructor(cfg) {
170
+ if (!cfg.repo || !/^[\w.-]+\/[\w.-]+$/.test(cfg.repo)) throw new Error('PeanutGallery: config.repo must be "owner/name"');
171
+ this.cfg = {
172
+ mode: "auto",
173
+ labels: [import_peanut_gallery_core.LABELS.root, import_peanut_gallery_core.LABELS.feedback],
174
+ activation: "always",
175
+ kinds: DEFAULT_KINDS,
176
+ prototypeUrl: typeof location !== "undefined" ? location.origin : "",
177
+ ...cfg
178
+ };
179
+ }
180
+ get storageKey() {
181
+ return `peanut-gallery:${this.cfg.repo}`;
182
+ }
183
+ /** Mount the widget. Safe to call once DOM is ready. */
184
+ mount() {
185
+ if (this.cfg.activation === "query") {
186
+ const want = new URLSearchParams(location.search).has("peanut");
187
+ const sticky = sessionStorage.getItem("peanut-gallery:active") === "1";
188
+ if (!want && !sticky) return this;
189
+ sessionStorage.setItem("peanut-gallery:active", "1");
190
+ }
191
+ this.session = this.loadSession();
192
+ this.host = document.createElement("peanut-gallery");
193
+ this.host.setAttribute("data-peanut-gallery", "");
194
+ this.root = this.host.attachShadow({ mode: "open" });
195
+ const style = document.createElement("style");
196
+ style.textContent = `:host { ${(0, import_peanut_gallery_core.themeToCssVars)((0, import_peanut_gallery_core.resolveTheme)(this.cfg.theme))} }
197
+ ${css}`;
198
+ if (this.cfg.loadFonts && !document.querySelector(`link[href="${import_peanut_gallery_core.DEFAULT_FONTS_URL}"]`)) {
199
+ const link = document.createElement("link");
200
+ link.rel = "stylesheet";
201
+ link.href = import_peanut_gallery_core.DEFAULT_FONTS_URL;
202
+ document.head.append(link);
203
+ }
204
+ this.root.append(style);
205
+ this.highlight = el("div", "pg-highlight pg-hidden");
206
+ this.highlight.append(el("span", "pg-tag"));
207
+ this.pinsLayer = el("div");
208
+ this.toolbar = this.buildToolbar();
209
+ this.root.append(this.highlight, this.pinsLayer, this.toolbar);
210
+ document.body.append(this.host);
211
+ document.addEventListener("mousemove", this.onMouseMove, true);
212
+ document.addEventListener("click", this.recordStep, true);
213
+ document.addEventListener("click", this.onClick, true);
214
+ document.addEventListener("keydown", this.onKey, true);
215
+ window.addEventListener("scroll", this.schedulePins, true);
216
+ window.addEventListener("resize", this.schedulePins);
217
+ watchHistory();
218
+ window.addEventListener(LOCATION_EVENT, this.onLocationChange);
219
+ window.addEventListener("popstate", this.onLocationChange);
220
+ window.addEventListener("hashchange", this.onLocationChange);
221
+ if (typeof MutationObserver !== "undefined") {
222
+ this.observer = new MutationObserver((records) => {
223
+ if (records.some((r) => r.target !== this.host)) this.schedulePins();
224
+ });
225
+ this.observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style", "hidden", "open", "aria-hidden"], characterData: true });
226
+ }
227
+ this.renderPins();
228
+ this.updateCount();
229
+ return this;
230
+ }
231
+ onLocationChange = () => {
232
+ if (this.destroyed) return;
233
+ this.closeComposer();
234
+ this.schedulePins();
235
+ if (this.panel) this.renderPanel();
236
+ };
237
+ destroy() {
238
+ this.destroyed = true;
239
+ document.removeEventListener("mousemove", this.onMouseMove, true);
240
+ document.removeEventListener("click", this.onClick, true);
241
+ document.removeEventListener("click", this.recordStep, true);
242
+ document.removeEventListener("keydown", this.onKey, true);
243
+ window.removeEventListener("scroll", this.schedulePins, true);
244
+ window.removeEventListener("resize", this.schedulePins);
245
+ window.removeEventListener(LOCATION_EVENT, this.onLocationChange);
246
+ window.removeEventListener("popstate", this.onLocationChange);
247
+ window.removeEventListener("hashchange", this.onLocationChange);
248
+ this.observer?.disconnect();
249
+ if (this.raf) cancelAnimationFrame(this.raf);
250
+ this.host?.remove();
251
+ }
252
+ /** Current annotations (all pages). */
253
+ getAnnotations() {
254
+ return [...this.session.annotations];
255
+ }
256
+ // ---------- state ----------
257
+ loadSession() {
258
+ if (this.cfg.session && Array.isArray(this.cfg.session.annotations)) return structuredClone(this.cfg.session);
259
+ try {
260
+ const raw = store.get(this.storageKey);
261
+ if (raw) {
262
+ const s = JSON.parse(raw);
263
+ if (Array.isArray(s.annotations)) return s;
264
+ }
265
+ } catch {
266
+ }
267
+ return this.freshSession();
268
+ }
269
+ freshSession() {
270
+ return {
271
+ id: (0, import_peanut_gallery_core.uid)("rs"),
272
+ prototypeUrl: this.cfg.prototypeUrl,
273
+ annotations: [],
274
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
275
+ author: this.cfg.author ?? store.get("peanut-gallery:author") ?? void 0,
276
+ userAgent: navigator.userAgent
277
+ };
278
+ }
279
+ save() {
280
+ store.set(this.storageKey, JSON.stringify(this.session));
281
+ this.updateCount();
282
+ this.renderPins();
283
+ if (this.panel) this.renderPanel();
284
+ }
285
+ pageUrl() {
286
+ return normalizePageUrl(location.href);
287
+ }
288
+ // ---------- toolbar ----------
289
+ buildToolbar() {
290
+ const bar = el("div", "pg-toolbar");
291
+ const brand = el("span", "pg-brand", "Notes");
292
+ const comment = el("button", "pg-btn", "Comment");
293
+ comment.dataset.role = "comment";
294
+ comment.onclick = () => this.setMode(this.mode === "commenting" ? "idle" : "commenting");
295
+ const review = el("button", "pg-btn", "Review");
296
+ review.dataset.role = "review";
297
+ review.onclick = () => this.togglePanel();
298
+ const count = el("span", "pg-count", "0");
299
+ count.dataset.role = "count";
300
+ review.append(count);
301
+ bar.append(brand, comment, review);
302
+ return bar;
303
+ }
304
+ updateCount() {
305
+ const c = this.toolbar.querySelector("[data-role=count]");
306
+ c.textContent = String(this.session.annotations.length);
307
+ }
308
+ setMode(m) {
309
+ this.mode = m;
310
+ const btn = this.toolbar.querySelector("[data-role=comment]");
311
+ btn.classList.toggle("pg-active", m === "commenting");
312
+ btn.textContent = m === "commenting" ? "Click an element\u2026 (Esc)" : "Comment";
313
+ document.body.style.cursor = m === "commenting" ? "crosshair" : "";
314
+ if (m === "idle") this.hideHighlight();
315
+ }
316
+ // ---------- events ----------
317
+ isOwn(target) {
318
+ return target instanceof Node && (this.host === target || this.host.contains(target));
319
+ }
320
+ onMouseMove = (e) => {
321
+ if (this.mode !== "commenting") return;
322
+ const path = e.composedPath();
323
+ if (path.includes(this.host)) return this.hideHighlight();
324
+ const target = path[0];
325
+ if (!(target instanceof Element) || target === document.documentElement || target === document.body) return this.hideHighlight();
326
+ this.hoverEl = target;
327
+ const r = target.getBoundingClientRect();
328
+ Object.assign(this.highlight.style, {
329
+ left: `${r.left + scrollX}px`,
330
+ top: `${r.top + scrollY}px`,
331
+ width: `${r.width}px`,
332
+ height: `${r.height}px`
333
+ });
334
+ this.highlight.querySelector(".pg-tag").textContent = describe(target);
335
+ this.highlight.classList.remove("pg-hidden");
336
+ };
337
+ hideHighlight() {
338
+ this.hoverEl = null;
339
+ this.highlight.classList.add("pg-hidden");
340
+ }
341
+ onClick = (e) => {
342
+ if (this.mode !== "commenting") return;
343
+ if (e.composedPath().includes(this.host)) return;
344
+ const target = this.hoverEl ?? e.target;
345
+ if (!(target instanceof Element)) return;
346
+ e.preventDefault();
347
+ e.stopPropagation();
348
+ this.setMode("idle");
349
+ this.openComposer(target, e.clientX, e.clientY);
350
+ };
351
+ /**
352
+ * Remember what the reviewer clicks while using the app (not while placing a note), so a
353
+ * note inside a modal or a menu carries the clicks that opened it. Recorded in the capture
354
+ * phase, before the app re-renders. Only the element is kept, never what was typed.
355
+ */
356
+ recordStep = (e) => {
357
+ if (this.mode === "commenting" || this.destroyed) return;
358
+ if (this.host && e.composedPath().includes(this.host)) return;
359
+ const raw = e.target;
360
+ if (!(raw instanceof Element)) return;
361
+ const target = raw.closest('button, a, summary, label, select, [role="button"], [role="tab"], [role="menuitem"], [role="option"], [role="switch"], [role="checkbox"], [data-testid]') ?? raw;
362
+ const here = this.pageUrl();
363
+ if (here !== this.stepsPage) {
364
+ this.steps = [];
365
+ this.stepsPage = here;
366
+ }
367
+ const anchor = (0, import_peanut_gallery_core.createAnchor)(target, e.clientX, e.clientY);
368
+ this.steps.push({ ...anchor, context: void 0, textFingerprint: anchor.textFingerprint.slice(0, 60) });
369
+ if (this.steps.length > import_peanut_gallery_core.MAX_STEPS) this.steps.shift();
370
+ };
371
+ currentSteps() {
372
+ if (this.stepsPage !== this.pageUrl() || !this.steps.length) return void 0;
373
+ return this.steps.map((st) => ({ ...st }));
374
+ }
375
+ onKey = (e) => {
376
+ if (e.key === "Escape") {
377
+ if (this.composer) this.closeComposer();
378
+ else if (this.mode === "commenting") this.setMode("idle");
379
+ }
380
+ };
381
+ // ---------- composer ----------
382
+ openComposer(target, clientX, clientY, existing) {
383
+ this.closeComposer();
384
+ const anchor = existing?.anchor ?? (0, import_peanut_gallery_core.createAnchor)(target, clientX, clientY);
385
+ const box = el("div", "pg-composer");
386
+ const meta = el("div", "pg-meta", `${anchor.tag} \xB7 ${anchor.selector}`);
387
+ meta.title = anchor.selector;
388
+ const ta = document.createElement("textarea");
389
+ ta.placeholder = "What should change here? Be specific \u2014 this becomes a task.";
390
+ ta.value = existing?.text ?? "";
391
+ const kind = document.createElement("select");
392
+ kind.append(new Option("type\u2026", ""));
393
+ for (const k of this.cfg.kinds) kind.append(new Option(k, k, false, existing?.kind === k));
394
+ const row = el("div", "pg-row");
395
+ const save = el("button", "pg-btn pg-primary", existing ? "Update" : "Add note");
396
+ const cancel = el("button", "pg-btn pg-ghost", "Cancel");
397
+ cancel.onclick = () => this.closeComposer();
398
+ save.onclick = () => {
399
+ const text = ta.value.trim();
400
+ if (!text) return ta.focus();
401
+ if (existing) {
402
+ existing.text = text;
403
+ existing.kind = kind.value || void 0;
404
+ } else {
405
+ this.session.annotations.push({
406
+ id: (0, import_peanut_gallery_core.uid)("an"),
407
+ text,
408
+ kind: kind.value || void 0,
409
+ anchor,
410
+ pageUrl: this.pageUrl(),
411
+ viewport: { width: innerWidth, height: innerHeight },
412
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
413
+ author: this.session.author,
414
+ ...this.currentSteps() ? { steps: this.currentSteps() } : {}
415
+ });
416
+ }
417
+ this.save();
418
+ this.closeComposer();
419
+ };
420
+ row.append(kind, el("span", "pg-spacer"));
421
+ if (existing) {
422
+ const del = el("button", "pg-btn pg-danger", "Delete");
423
+ del.onclick = () => {
424
+ this.session.annotations = this.session.annotations.filter((a) => a.id !== existing.id);
425
+ this.save();
426
+ this.closeComposer();
427
+ };
428
+ row.append(del);
429
+ }
430
+ row.append(cancel, save);
431
+ box.append(meta, ta, row);
432
+ const x = Math.min(clientX + 12, innerWidth - 312) + scrollX;
433
+ const y = Math.min(clientY + 12, innerHeight - 190) + scrollY;
434
+ box.style.left = `${Math.max(8, x)}px`;
435
+ box.style.top = `${Math.max(8, y)}px`;
436
+ this.root.append(box);
437
+ this.composer = box;
438
+ ta.focus();
439
+ }
440
+ closeComposer() {
441
+ this.composer?.remove();
442
+ this.composer = null;
443
+ }
444
+ // ---------- pins ----------
445
+ schedulePins = () => {
446
+ if (this.raf || this.destroyed) return;
447
+ this.raf = requestAnimationFrame(() => {
448
+ this.raf = 0;
449
+ this.renderPins();
450
+ });
451
+ };
452
+ pinsSignature = "";
453
+ renderPins() {
454
+ const here = this.pageUrl();
455
+ const placed = [];
456
+ this.session.annotations.forEach((a, i) => {
457
+ if (normalizePageUrl(a.pageUrl) !== here) return;
458
+ const { el: target, confidence } = (0, import_peanut_gallery_core.resolveAnchor)(a.anchor, document);
459
+ if (!target || confidence < 0.4) return;
460
+ const vis = target.checkVisibility;
461
+ if (typeof vis === "function" && !vis.call(target)) return;
462
+ const r = target.getBoundingClientRect();
463
+ const x = Math.round(r.left + scrollX + r.width * a.anchor.offset.x);
464
+ const y = Math.round(r.top + scrollY + r.height * a.anchor.offset.y);
465
+ placed.push({ a, n: i + 1, x, y, low: confidence < 0.5 });
466
+ });
467
+ const sig = placed.map((p) => `${p.a.id}:${p.n}:${p.x}:${p.y}:${p.low}:${p.a.text}`).join("|");
468
+ if (sig === this.pinsSignature && this.pinsLayer.childElementCount === placed.length) return;
469
+ this.pinsSignature = sig;
470
+ this.pinsLayer.replaceChildren(
471
+ ...placed.map(({ a, n, x, y, low }) => {
472
+ const pin = el("div", "pg-pin" + (low ? " pg-low" : ""), String(n));
473
+ pin.title = a.text;
474
+ pin.style.left = `${x}px`;
475
+ pin.style.top = `${y}px`;
476
+ pin.onclick = (ev) => {
477
+ ev.stopPropagation();
478
+ const target = (0, import_peanut_gallery_core.resolveAnchor)(a.anchor, document).el ?? document.body;
479
+ this.openComposer(target, x - scrollX, y - scrollY, a);
480
+ };
481
+ return pin;
482
+ })
483
+ );
484
+ }
485
+ // ---------- panel ----------
486
+ togglePanel() {
487
+ if (this.panel) {
488
+ this.panel.remove();
489
+ this.panel = null;
490
+ return;
491
+ }
492
+ this.panel = el("div", "pg-panel");
493
+ this.root.append(this.panel);
494
+ this.renderPanel();
495
+ }
496
+ renderPanel() {
497
+ const p = this.panel;
498
+ p.replaceChildren();
499
+ const h = el("h3", "", `Feedback (${this.session.annotations.length})`);
500
+ p.append(h);
501
+ if (!this.session.annotations.length) {
502
+ p.append(el("div", "pg-empty", "No notes yet. Click \u201CComment\u201D, then click anything on the page."));
503
+ }
504
+ const here = this.pageUrl();
505
+ this.session.annotations.forEach((a, i) => {
506
+ const item = el("div", "pg-item");
507
+ const text = el("div", "pg-text");
508
+ if (a.kind) text.append(el("span", "pg-kind", a.kind));
509
+ text.append(`${i + 1}. ${a.text}`);
510
+ const meta = el("div", "pg-meta", `${normalizePageUrl(a.pageUrl) === here ? "this page" : pathOf(a.pageUrl)} \xB7 ${a.anchor.tag}${a.anchor.textFingerprint ? ` \u201C${a.anchor.textFingerprint.slice(0, 30)}\u201D` : ""}`);
511
+ item.append(text, meta);
512
+ p.append(item);
513
+ });
514
+ const row = el("div", "pg-row");
515
+ const clear = el("button", "pg-btn pg-ghost", "Clear");
516
+ clear.onclick = () => {
517
+ if (!this.session.annotations.length || confirm("Discard all notes?")) {
518
+ this.session = this.freshSession();
519
+ this.save();
520
+ }
521
+ };
522
+ const submit = el("button", "pg-btn pg-primary", this.effectiveMode() === "issue-url" ? "Submit to GitHub" : "Submit");
523
+ submit.disabled = !this.session.annotations.length;
524
+ submit.onclick = () => this.submit(submit);
525
+ row.append(clear, el("span", "pg-spacer"), submit);
526
+ p.append(row);
527
+ }
528
+ effectiveMode() {
529
+ if (this.cfg.onSubmit) return "callback";
530
+ if (this.cfg.mode === "relay") return "relay";
531
+ if (this.cfg.mode === "issue-url") return "issue-url";
532
+ return this.cfg.relayUrl ? "relay" : "issue-url";
533
+ }
534
+ ensureAuthor() {
535
+ if (this.session.author) return;
536
+ let name;
537
+ try {
538
+ name = prompt("Your name (shown on the feedback):")?.trim();
539
+ } catch {
540
+ }
541
+ if (name) {
542
+ this.session.author = name;
543
+ store.set("peanut-gallery:author", name);
544
+ this.session.annotations.forEach((a) => a.author ??= name);
545
+ }
546
+ }
547
+ async submit(btn) {
548
+ this.ensureAuthor();
549
+ const session = { ...this.session, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
550
+ if (this.effectiveMode() === "callback") {
551
+ btn.disabled = true;
552
+ btn.textContent = "Submitting\u2026";
553
+ try {
554
+ const out = await this.cfg.onSubmit(session) || {};
555
+ if (!out.keep) this.resetAfterSubmit();
556
+ this.toast(out.issueUrl ? `Submitted: ${out.issueUrl}` : "Submitted. Thank you!");
557
+ } catch (err) {
558
+ this.toast(`Could not submit: ${err.message}`);
559
+ } finally {
560
+ btn.disabled = false;
561
+ btn.textContent = "Submit";
562
+ }
563
+ return;
564
+ }
565
+ if (this.effectiveMode() === "issue-url") {
566
+ const url = (0, import_peanut_gallery_core.newIssueUrl)(this.cfg.repo, session, this.cfg.labels);
567
+ const w = window.open(url, "_blank", "noopener");
568
+ if (!w) {
569
+ this.toast("Popup blocked \u2014 allow popups and try again.");
570
+ return;
571
+ }
572
+ this.toast("Opened GitHub. Press \u201CSubmit new issue\u201D there; notes stay here until you Clear.");
573
+ return;
574
+ }
575
+ btn.disabled = true;
576
+ btn.textContent = "Submitting\u2026";
577
+ try {
578
+ const res = await fetch(this.cfg.relayUrl, {
579
+ method: "POST",
580
+ headers: { "content-type": "application/json" },
581
+ body: JSON.stringify({ session, labels: this.cfg.labels })
582
+ });
583
+ if (!res.ok) throw new Error(`Relay responded ${res.status}`);
584
+ const data = await res.json();
585
+ this.resetAfterSubmit();
586
+ this.toast(data.issueUrl ? `Submitted: ${data.issueUrl}` : "Submitted. Thank you!");
587
+ if (data.issueUrl) window.open(data.issueUrl, "_blank", "noopener");
588
+ } catch (err) {
589
+ this.toast(`Could not submit: ${err.message}`);
590
+ btn.disabled = false;
591
+ btn.textContent = "Submit";
592
+ }
593
+ }
594
+ /** Clear the submitted notes and redraw: pins, count and the panel. */
595
+ resetAfterSubmit() {
596
+ this.session = this.freshSession();
597
+ this.save();
598
+ if (this.panel) {
599
+ this.panel.remove();
600
+ this.panel = null;
601
+ }
602
+ this.renderPins();
603
+ this.updateCount();
604
+ }
605
+ toast(msg) {
606
+ const t = el("div", "pg-toast", msg);
607
+ this.root.append(t);
608
+ setTimeout(() => t.remove(), 5e3);
609
+ }
610
+ };
611
+ function el(tag, className = "", text) {
612
+ const e = document.createElement(tag);
613
+ if (className) e.className = className;
614
+ if (text !== void 0) e.textContent = text;
615
+ return e;
616
+ }
617
+ function describe(target) {
618
+ const id = target.getAttribute("data-testid") ?? target.id;
619
+ return target.tagName.toLowerCase() + (id ? `#${id}` : "");
620
+ }
621
+ function pathOf(url) {
622
+ try {
623
+ return new URL(url).pathname;
624
+ } catch {
625
+ return url;
626
+ }
627
+ }
628
+
629
+ // src/index.ts
630
+ var instance = null;
631
+ function init(config) {
632
+ if (instance) return instance;
633
+ instance = new PeanutGalleryWidget(config);
634
+ const mount = () => instance.mount();
635
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", mount, { once: true });
636
+ else mount();
637
+ return instance;
638
+ }
639
+ function destroy() {
640
+ instance?.destroy();
641
+ instance = null;
642
+ }
643
+ // Annotate the CommonJS export names for ESM import in node:
644
+ 0 && (module.exports = {
645
+ PeanutGalleryWidget,
646
+ destroy,
647
+ init
648
+ });