@pixelatlarge/peanut-gallery-widget 0.1.0 → 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.
@@ -5,6 +5,7 @@ import {
5
5
  newIssueUrl,
6
6
  uid,
7
7
  LABELS,
8
+ MAX_STEPS,
8
9
  resolveTheme,
9
10
  themeToCssVars,
10
11
  DEFAULT_FONTS_URL
@@ -82,6 +83,37 @@ var css = `
82
83
  `;
83
84
 
84
85
  // src/widget.ts
86
+ var LOCATION_EVENT = "peanut-gallery:locationchange";
87
+ function normalizePageUrl(href) {
88
+ try {
89
+ const u = new URL(href);
90
+ for (const key of [...u.searchParams.keys()]) {
91
+ if (key === "peanut" || key.startsWith("utm_") || key === "fbclid" || key === "gclid") u.searchParams.delete(key);
92
+ }
93
+ const search = u.searchParams.toString();
94
+ const hash = /^#!?\//.test(u.hash) ? u.hash : "";
95
+ return u.origin + u.pathname + (search ? `?${search}` : "") + hash;
96
+ } catch {
97
+ return href;
98
+ }
99
+ }
100
+ function watchHistory() {
101
+ if (typeof history === "undefined") return;
102
+ const h = history;
103
+ if (h.__peanutGallery) return;
104
+ h.__peanutGallery = true;
105
+ for (const name of ["pushState", "replaceState"]) {
106
+ const original = h[name];
107
+ h[name] = function(...args) {
108
+ const result = original.apply(this, args);
109
+ try {
110
+ window.dispatchEvent(new Event(LOCATION_EVENT));
111
+ } catch {
112
+ }
113
+ return result;
114
+ };
115
+ }
116
+ }
85
117
  var store = {
86
118
  get(key) {
87
119
  try {
@@ -112,6 +144,10 @@ var PeanutGalleryWidget = class {
112
144
  toolbar;
113
145
  raf = 0;
114
146
  destroyed = false;
147
+ observer;
148
+ /** The reviewer's recent clicks on this page (see Annotation.steps). */
149
+ steps = [];
150
+ stepsPage = "";
115
151
  constructor(cfg) {
116
152
  if (!cfg.repo || !/^[\w.-]+\/[\w.-]+$/.test(cfg.repo)) throw new Error('PeanutGallery: config.repo must be "owner/name"');
117
153
  this.cfg = {
@@ -155,21 +191,44 @@ ${css}`;
155
191
  this.root.append(this.highlight, this.pinsLayer, this.toolbar);
156
192
  document.body.append(this.host);
157
193
  document.addEventListener("mousemove", this.onMouseMove, true);
194
+ document.addEventListener("click", this.recordStep, true);
158
195
  document.addEventListener("click", this.onClick, true);
159
196
  document.addEventListener("keydown", this.onKey, true);
160
197
  window.addEventListener("scroll", this.schedulePins, true);
161
198
  window.addEventListener("resize", this.schedulePins);
199
+ watchHistory();
200
+ window.addEventListener(LOCATION_EVENT, this.onLocationChange);
201
+ window.addEventListener("popstate", this.onLocationChange);
202
+ window.addEventListener("hashchange", this.onLocationChange);
203
+ if (typeof MutationObserver !== "undefined") {
204
+ this.observer = new MutationObserver((records) => {
205
+ if (records.some((r) => r.target !== this.host)) this.schedulePins();
206
+ });
207
+ this.observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style", "hidden", "open", "aria-hidden"], characterData: true });
208
+ }
162
209
  this.renderPins();
163
210
  this.updateCount();
164
211
  return this;
165
212
  }
213
+ onLocationChange = () => {
214
+ if (this.destroyed) return;
215
+ this.closeComposer();
216
+ this.schedulePins();
217
+ if (this.panel) this.renderPanel();
218
+ };
166
219
  destroy() {
167
220
  this.destroyed = true;
168
221
  document.removeEventListener("mousemove", this.onMouseMove, true);
169
222
  document.removeEventListener("click", this.onClick, true);
223
+ document.removeEventListener("click", this.recordStep, true);
170
224
  document.removeEventListener("keydown", this.onKey, true);
171
225
  window.removeEventListener("scroll", this.schedulePins, true);
172
226
  window.removeEventListener("resize", this.schedulePins);
227
+ window.removeEventListener(LOCATION_EVENT, this.onLocationChange);
228
+ window.removeEventListener("popstate", this.onLocationChange);
229
+ window.removeEventListener("hashchange", this.onLocationChange);
230
+ this.observer?.disconnect();
231
+ if (this.raf) cancelAnimationFrame(this.raf);
173
232
  this.host?.remove();
174
233
  }
175
234
  /** Current annotations (all pages). */
@@ -206,7 +265,7 @@ ${css}`;
206
265
  if (this.panel) this.renderPanel();
207
266
  }
208
267
  pageUrl() {
209
- return location.origin + location.pathname + location.search;
268
+ return normalizePageUrl(location.href);
210
269
  }
211
270
  // ---------- toolbar ----------
212
271
  buildToolbar() {
@@ -271,6 +330,30 @@ ${css}`;
271
330
  this.setMode("idle");
272
331
  this.openComposer(target, e.clientX, e.clientY);
273
332
  };
333
+ /**
334
+ * Remember what the reviewer clicks while using the app (not while placing a note), so a
335
+ * note inside a modal or a menu carries the clicks that opened it. Recorded in the capture
336
+ * phase, before the app re-renders. Only the element is kept, never what was typed.
337
+ */
338
+ recordStep = (e) => {
339
+ if (this.mode === "commenting" || this.destroyed) return;
340
+ if (this.host && e.composedPath().includes(this.host)) return;
341
+ const raw = e.target;
342
+ if (!(raw instanceof Element)) return;
343
+ const target = raw.closest('button, a, summary, label, select, [role="button"], [role="tab"], [role="menuitem"], [role="option"], [role="switch"], [role="checkbox"], [data-testid]') ?? raw;
344
+ const here = this.pageUrl();
345
+ if (here !== this.stepsPage) {
346
+ this.steps = [];
347
+ this.stepsPage = here;
348
+ }
349
+ const anchor = createAnchor(target, e.clientX, e.clientY);
350
+ this.steps.push({ ...anchor, context: void 0, textFingerprint: anchor.textFingerprint.slice(0, 60) });
351
+ if (this.steps.length > MAX_STEPS) this.steps.shift();
352
+ };
353
+ currentSteps() {
354
+ if (this.stepsPage !== this.pageUrl() || !this.steps.length) return void 0;
355
+ return this.steps.map((st) => ({ ...st }));
356
+ }
274
357
  onKey = (e) => {
275
358
  if (e.key === "Escape") {
276
359
  if (this.composer) this.closeComposer();
@@ -309,7 +392,8 @@ ${css}`;
309
392
  pageUrl: this.pageUrl(),
310
393
  viewport: { width: innerWidth, height: innerHeight },
311
394
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
312
- author: this.session.author
395
+ author: this.session.author,
396
+ ...this.currentSteps() ? { steps: this.currentSteps() } : {}
313
397
  });
314
398
  }
315
399
  this.save();
@@ -347,31 +431,38 @@ ${css}`;
347
431
  this.renderPins();
348
432
  });
349
433
  };
434
+ pinsSignature = "";
350
435
  renderPins() {
351
- this.pinsLayer.replaceChildren();
352
436
  const here = this.pageUrl();
437
+ const placed = [];
353
438
  this.session.annotations.forEach((a, i) => {
354
- if (a.pageUrl !== here) return;
439
+ if (normalizePageUrl(a.pageUrl) !== here) return;
355
440
  const { el: target, confidence } = resolveAnchor(a.anchor, document);
356
- let x, y;
357
- if (target) {
358
- const r = target.getBoundingClientRect();
359
- x = r.left + scrollX + r.width * a.anchor.offset.x;
360
- y = r.top + scrollY + r.height * a.anchor.offset.y;
361
- } else {
362
- x = a.anchor.rect.x + a.anchor.rect.width * a.anchor.offset.x;
363
- y = a.anchor.rect.y + a.anchor.rect.height * a.anchor.offset.y;
364
- }
365
- const pin = el("div", "pg-pin" + (confidence < 0.5 ? " pg-low" : ""), String(i + 1));
366
- pin.title = a.text;
367
- pin.style.left = `${x}px`;
368
- pin.style.top = `${y}px`;
369
- pin.onclick = (ev) => {
370
- ev.stopPropagation();
371
- this.openComposer(target ?? document.body, x - scrollX, y - scrollY, a);
372
- };
373
- this.pinsLayer.append(pin);
441
+ if (!target || confidence < 0.4) return;
442
+ const vis = target.checkVisibility;
443
+ if (typeof vis === "function" && !vis.call(target)) return;
444
+ const r = target.getBoundingClientRect();
445
+ const x = Math.round(r.left + scrollX + r.width * a.anchor.offset.x);
446
+ const y = Math.round(r.top + scrollY + r.height * a.anchor.offset.y);
447
+ placed.push({ a, n: i + 1, x, y, low: confidence < 0.5 });
374
448
  });
449
+ const sig = placed.map((p) => `${p.a.id}:${p.n}:${p.x}:${p.y}:${p.low}:${p.a.text}`).join("|");
450
+ if (sig === this.pinsSignature && this.pinsLayer.childElementCount === placed.length) return;
451
+ this.pinsSignature = sig;
452
+ this.pinsLayer.replaceChildren(
453
+ ...placed.map(({ a, n, x, y, low }) => {
454
+ const pin = el("div", "pg-pin" + (low ? " pg-low" : ""), String(n));
455
+ pin.title = a.text;
456
+ pin.style.left = `${x}px`;
457
+ pin.style.top = `${y}px`;
458
+ pin.onclick = (ev) => {
459
+ ev.stopPropagation();
460
+ const target = resolveAnchor(a.anchor, document).el ?? document.body;
461
+ this.openComposer(target, x - scrollX, y - scrollY, a);
462
+ };
463
+ return pin;
464
+ })
465
+ );
375
466
  }
376
467
  // ---------- panel ----------
377
468
  togglePanel() {
@@ -398,7 +489,7 @@ ${css}`;
398
489
  const text = el("div", "pg-text");
399
490
  if (a.kind) text.append(el("span", "pg-kind", a.kind));
400
491
  text.append(`${i + 1}. ${a.text}`);
401
- const meta = el("div", "pg-meta", `${a.pageUrl === here ? "this page" : pathOf(a.pageUrl)} \xB7 ${a.anchor.tag}${a.anchor.textFingerprint ? ` \u201C${a.anchor.textFingerprint.slice(0, 30)}\u201D` : ""}`);
492
+ 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` : ""}`);
402
493
  item.append(text, meta);
403
494
  p.append(item);
404
495
  });
package/dist/index.cjs CHANGED
@@ -101,6 +101,37 @@ var css = `
101
101
  `;
102
102
 
103
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
+ }
104
135
  var store = {
105
136
  get(key) {
106
137
  try {
@@ -131,6 +162,10 @@ var PeanutGalleryWidget = class {
131
162
  toolbar;
132
163
  raf = 0;
133
164
  destroyed = false;
165
+ observer;
166
+ /** The reviewer's recent clicks on this page (see Annotation.steps). */
167
+ steps = [];
168
+ stepsPage = "";
134
169
  constructor(cfg) {
135
170
  if (!cfg.repo || !/^[\w.-]+\/[\w.-]+$/.test(cfg.repo)) throw new Error('PeanutGallery: config.repo must be "owner/name"');
136
171
  this.cfg = {
@@ -174,21 +209,44 @@ ${css}`;
174
209
  this.root.append(this.highlight, this.pinsLayer, this.toolbar);
175
210
  document.body.append(this.host);
176
211
  document.addEventListener("mousemove", this.onMouseMove, true);
212
+ document.addEventListener("click", this.recordStep, true);
177
213
  document.addEventListener("click", this.onClick, true);
178
214
  document.addEventListener("keydown", this.onKey, true);
179
215
  window.addEventListener("scroll", this.schedulePins, true);
180
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
+ }
181
227
  this.renderPins();
182
228
  this.updateCount();
183
229
  return this;
184
230
  }
231
+ onLocationChange = () => {
232
+ if (this.destroyed) return;
233
+ this.closeComposer();
234
+ this.schedulePins();
235
+ if (this.panel) this.renderPanel();
236
+ };
185
237
  destroy() {
186
238
  this.destroyed = true;
187
239
  document.removeEventListener("mousemove", this.onMouseMove, true);
188
240
  document.removeEventListener("click", this.onClick, true);
241
+ document.removeEventListener("click", this.recordStep, true);
189
242
  document.removeEventListener("keydown", this.onKey, true);
190
243
  window.removeEventListener("scroll", this.schedulePins, true);
191
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);
192
250
  this.host?.remove();
193
251
  }
194
252
  /** Current annotations (all pages). */
@@ -225,7 +283,7 @@ ${css}`;
225
283
  if (this.panel) this.renderPanel();
226
284
  }
227
285
  pageUrl() {
228
- return location.origin + location.pathname + location.search;
286
+ return normalizePageUrl(location.href);
229
287
  }
230
288
  // ---------- toolbar ----------
231
289
  buildToolbar() {
@@ -290,6 +348,30 @@ ${css}`;
290
348
  this.setMode("idle");
291
349
  this.openComposer(target, e.clientX, e.clientY);
292
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
+ }
293
375
  onKey = (e) => {
294
376
  if (e.key === "Escape") {
295
377
  if (this.composer) this.closeComposer();
@@ -328,7 +410,8 @@ ${css}`;
328
410
  pageUrl: this.pageUrl(),
329
411
  viewport: { width: innerWidth, height: innerHeight },
330
412
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
331
- author: this.session.author
413
+ author: this.session.author,
414
+ ...this.currentSteps() ? { steps: this.currentSteps() } : {}
332
415
  });
333
416
  }
334
417
  this.save();
@@ -366,31 +449,38 @@ ${css}`;
366
449
  this.renderPins();
367
450
  });
368
451
  };
452
+ pinsSignature = "";
369
453
  renderPins() {
370
- this.pinsLayer.replaceChildren();
371
454
  const here = this.pageUrl();
455
+ const placed = [];
372
456
  this.session.annotations.forEach((a, i) => {
373
- if (a.pageUrl !== here) return;
457
+ if (normalizePageUrl(a.pageUrl) !== here) return;
374
458
  const { el: target, confidence } = (0, import_peanut_gallery_core.resolveAnchor)(a.anchor, document);
375
- let x, y;
376
- if (target) {
377
- const r = target.getBoundingClientRect();
378
- x = r.left + scrollX + r.width * a.anchor.offset.x;
379
- y = r.top + scrollY + r.height * a.anchor.offset.y;
380
- } else {
381
- x = a.anchor.rect.x + a.anchor.rect.width * a.anchor.offset.x;
382
- y = a.anchor.rect.y + a.anchor.rect.height * a.anchor.offset.y;
383
- }
384
- const pin = el("div", "pg-pin" + (confidence < 0.5 ? " pg-low" : ""), String(i + 1));
385
- pin.title = a.text;
386
- pin.style.left = `${x}px`;
387
- pin.style.top = `${y}px`;
388
- pin.onclick = (ev) => {
389
- ev.stopPropagation();
390
- this.openComposer(target ?? document.body, x - scrollX, y - scrollY, a);
391
- };
392
- this.pinsLayer.append(pin);
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 });
393
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
+ );
394
484
  }
395
485
  // ---------- panel ----------
396
486
  togglePanel() {
@@ -417,7 +507,7 @@ ${css}`;
417
507
  const text = el("div", "pg-text");
418
508
  if (a.kind) text.append(el("span", "pg-kind", a.kind));
419
509
  text.append(`${i + 1}. ${a.text}`);
420
- const meta = el("div", "pg-meta", `${a.pageUrl === here ? "this page" : pathOf(a.pageUrl)} \xB7 ${a.anchor.tag}${a.anchor.textFingerprint ? ` \u201C${a.anchor.textFingerprint.slice(0, 30)}\u201D` : ""}`);
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` : ""}`);
421
511
  item.append(text, meta);
422
512
  p.append(item);
423
513
  });
package/dist/index.d.cts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { AnnotationKind, Theme, ReviewSession, Annotation } from '@pixelatlarge/peanut-gallery-core';
2
2
  export { Annotation, AnnotationKind, ReviewSession } from '@pixelatlarge/peanut-gallery-core';
3
3
 
4
+ type SubmitResult = void | {
5
+ issueUrl?: string;
6
+ keep?: boolean;
7
+ };
4
8
  interface PeanutGalleryConfig {
5
9
  /** GitHub repo as "owner/name". Required. */
6
10
  repo: string;
@@ -39,10 +43,7 @@ interface PeanutGalleryConfig {
39
43
  * Overrides `mode`. Resolve to clear the notes; return `{ issueUrl }` to link to the result,
40
44
  * or `{ keep: true }` to leave the notes on the page. Throw to show an error and keep them.
41
45
  */
42
- onSubmit?: (session: ReviewSession) => void | Promise<void | {
43
- issueUrl?: string;
44
- keep?: boolean;
45
- }>;
46
+ onSubmit?: (session: ReviewSession) => SubmitResult | Promise<SubmitResult>;
46
47
  /** Start from these notes instead of the ones saved in this browser (e.g. a demo or a test). */
47
48
  session?: ReviewSession;
48
49
  }
@@ -60,10 +61,15 @@ declare class PeanutGalleryWidget {
60
61
  private toolbar;
61
62
  private raf;
62
63
  private destroyed;
64
+ private observer?;
65
+ /** The reviewer's recent clicks on this page (see Annotation.steps). */
66
+ private steps;
67
+ private stepsPage;
63
68
  constructor(cfg: PeanutGalleryConfig);
64
69
  get storageKey(): string;
65
70
  /** Mount the widget. Safe to call once DOM is ready. */
66
71
  mount(): this;
72
+ private onLocationChange;
67
73
  destroy(): void;
68
74
  /** Current annotations (all pages). */
69
75
  getAnnotations(): Annotation[];
@@ -78,10 +84,18 @@ declare class PeanutGalleryWidget {
78
84
  private onMouseMove;
79
85
  private hideHighlight;
80
86
  private onClick;
87
+ /**
88
+ * Remember what the reviewer clicks while using the app (not while placing a note), so a
89
+ * note inside a modal or a menu carries the clicks that opened it. Recorded in the capture
90
+ * phase, before the app re-renders. Only the element is kept, never what was typed.
91
+ */
92
+ private recordStep;
93
+ private currentSteps;
81
94
  private onKey;
82
95
  private openComposer;
83
96
  private closeComposer;
84
97
  private schedulePins;
98
+ private pinsSignature;
85
99
  private renderPins;
86
100
  private togglePanel;
87
101
  private renderPanel;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { AnnotationKind, Theme, ReviewSession, Annotation } from '@pixelatlarge/peanut-gallery-core';
2
2
  export { Annotation, AnnotationKind, ReviewSession } from '@pixelatlarge/peanut-gallery-core';
3
3
 
4
+ type SubmitResult = void | {
5
+ issueUrl?: string;
6
+ keep?: boolean;
7
+ };
4
8
  interface PeanutGalleryConfig {
5
9
  /** GitHub repo as "owner/name". Required. */
6
10
  repo: string;
@@ -39,10 +43,7 @@ interface PeanutGalleryConfig {
39
43
  * Overrides `mode`. Resolve to clear the notes; return `{ issueUrl }` to link to the result,
40
44
  * or `{ keep: true }` to leave the notes on the page. Throw to show an error and keep them.
41
45
  */
42
- onSubmit?: (session: ReviewSession) => void | Promise<void | {
43
- issueUrl?: string;
44
- keep?: boolean;
45
- }>;
46
+ onSubmit?: (session: ReviewSession) => SubmitResult | Promise<SubmitResult>;
46
47
  /** Start from these notes instead of the ones saved in this browser (e.g. a demo or a test). */
47
48
  session?: ReviewSession;
48
49
  }
@@ -60,10 +61,15 @@ declare class PeanutGalleryWidget {
60
61
  private toolbar;
61
62
  private raf;
62
63
  private destroyed;
64
+ private observer?;
65
+ /** The reviewer's recent clicks on this page (see Annotation.steps). */
66
+ private steps;
67
+ private stepsPage;
63
68
  constructor(cfg: PeanutGalleryConfig);
64
69
  get storageKey(): string;
65
70
  /** Mount the widget. Safe to call once DOM is ready. */
66
71
  mount(): this;
72
+ private onLocationChange;
67
73
  destroy(): void;
68
74
  /** Current annotations (all pages). */
69
75
  getAnnotations(): Annotation[];
@@ -78,10 +84,18 @@ declare class PeanutGalleryWidget {
78
84
  private onMouseMove;
79
85
  private hideHighlight;
80
86
  private onClick;
87
+ /**
88
+ * Remember what the reviewer clicks while using the app (not while placing a note), so a
89
+ * note inside a modal or a menu carries the clicks that opened it. Recorded in the capture
90
+ * phase, before the app re-renders. Only the element is kept, never what was typed.
91
+ */
92
+ private recordStep;
93
+ private currentSteps;
81
94
  private onKey;
82
95
  private openComposer;
83
96
  private closeComposer;
84
97
  private schedulePins;
98
+ private pinsSignature;
85
99
  private renderPins;
86
100
  private togglePanel;
87
101
  private renderPanel;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  PeanutGalleryWidget,
3
3
  destroy,
4
4
  init
5
- } from "./chunk-O6C4PORF.js";
5
+ } from "./chunk-X4WAQVYW.js";
6
6
  export {
7
7
  PeanutGalleryWidget,
8
8
  destroy,
@@ -1,9 +1,9 @@
1
- "use strict";var PeanutGallery=(()=>{var w=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var I=Object.getOwnPropertyNames;var z=Object.prototype.hasOwnProperty;var G=(e,t)=>{for(var n in t)w(e,n,{get:t[n],enumerable:!0})},q=(e,t,n,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of I(t))!z.call(e,o)&&o!==n&&w(e,o,{get:()=>t[o],enumerable:!(i=O(t,o))||i.enumerable});return e};var j=e=>q(w({},"__esModule",{value:!0}),e);var st={};G(st,{destroy:()=>D,init:()=>A});var k={root:"peanut-gallery",feedback:"peanut-gallery:feedback",proposal:"peanut-gallery:proposal",accepted:"peanut-gallery:accepted",rejected:"peanut-gallery:rejected"},K="json peanut-gallery",_=80;function y(e){return(e??"").replace(/\s+/g," ").trim().slice(0,_)}function L(e){return typeof CSS<"u"&&typeof CSS.escape=="function"?CSS.escape(e):e.replace(/([^a-zA-Z0-9_-])/g,"\\$1")}var Y=["data-testid","data-test","data-cy","data-proto-id","id","name","aria-label"];function $(e){return/[0-9a-f]{6,}/i.test(e)||/^(css|sc|emotion|svelte|jsx)-/.test(e)||/^_/.test(e)||e.length>40}function X(e){for(let r of Y){let s=e.getAttribute(r);if(s&&!$(s)){let p=r==="id"?`#${L(s)}`:`${e.tagName.toLowerCase()}[${r}="${s.replace(/"/g,'\\"')}"]`,c=e.ownerDocument;try{if(c.querySelectorAll(p).length===1)return{seg:p,unique:!0}}catch{}}}let t=e.tagName.toLowerCase(),n=Array.from(e.classList).filter(r=>!$(r)).slice(0,2),i=t+n.map(r=>`.${L(r)}`).join(""),o=e.parentElement;if(o){let r=Array.from(o.children).filter(s=>s.tagName===e.tagName);r.length>1&&(i+=`:nth-of-type(${r.indexOf(e)+1})`)}return{seg:i,unique:!1}}function J(e){let t=[],n=e;for(;n&&n!==n.ownerDocument.documentElement;){let{seg:i,unique:o}=X(n);if(t.unshift(i),o||(n=n.parentElement,t.length>8))break}return t.join(" > ")}function V(e){let t=e;for(;t;){let n=t.querySelector('h1, h2, h3, legend, [role="heading"]');if(n&&n!==e&&t!==e)return y(n.textContent);let i=t.getAttribute?.("aria-label")??t.getAttribute?.("aria-labelledby");if(i&&t!==e)return y(i);t=t.parentElement}}function M(e,t,n){let i=e.getBoundingClientRect(),o=e.ownerDocument.defaultView;return{selector:J(e),textFingerprint:y(e.textContent),tag:e.tagName.toLowerCase(),rect:{x:i.left+o.scrollX,y:i.top+o.scrollY,width:i.width,height:i.height},offset:{x:i.width?Math.min(1,Math.max(0,(t-i.left)/i.width)):.5,y:i.height?Math.min(1,Math.max(0,(n-i.top)/i.height)):.5},context:V(e)}}function E(e,t){if(!t)return .5;let n=y(e.textContent);return n===t?1:n.includes(t)||t.includes(n)?.75:0}function T(e,t){let n=[];try{n=Array.from(t.querySelectorAll(e.selector))}catch{n=[]}if(n.length===1&&E(n[0],e.textFingerprint)>0)return{el:n[0],confidence:.95};if(n.length>1){let o=n.map(r=>({el:r,s:E(r,e.textFingerprint)})).sort((r,s)=>s.s-r.s);if(o[0].s>0)return{el:o[0].el,confidence:.8}}if(e.textFingerprint){let r=Array.from(t.getElementsByTagName(e.tag)).map(s=>({el:s,s:E(s,e.textFingerprint)})).sort((s,p)=>p.s-s.s);if(r[0]?.s>=.75)return{el:r[0].el,confidence:.6}}if(n.length===1)return{el:n[0],confidence:.5};let i=t.defaultView;if(i&&typeof t.elementFromPoint=="function"){let o=e.rect.x+e.rect.width/2-i.scrollX,r=e.rect.y+e.rect.height/2-i.scrollY,s=t.elementFromPoint(o,r);if(s)return{el:s,confidence:.3}}return{el:null,confidence:0}}function W(e){let t=e.annotations.length,n=tt(e.annotations[0]?.pageUrl??e.prototypeUrl),i=`Prototype feedback: ${t} comment${t===1?"":"s"} on ${n}`,o=[];return o.push(`**Prototype:** ${e.prototypeUrl}`),e.author&&o.push(`**Reviewer:** ${e.author}`),o.push(`**Session:** \`${e.id}\` \xB7 ${e.createdAt}`),o.push(""),e.annotations.forEach((r,s)=>{o.push(`### ${s+1}. ${r.kind?`[${r.kind}] `:""}${Q(r.text)}`),r.text.includes(`
2
- `)&&o.push(r.text.split(`
1
+ "use strict";var PeanutGallery=(()=>{var w=Object.defineProperty;var z=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var q=Object.prototype.hasOwnProperty;var K=(e,t)=>{for(var o in t)w(e,o,{get:t[o],enumerable:!0})},X=(e,t,o,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of j(t))!q.call(e,n)&&n!==o&&w(e,n,{get:()=>t[n],enumerable:!(i=z(t,n))||i.enumerable});return e};var Y=e=>X(w({},"__esModule",{value:!0}),e);var ht={};K(ht,{destroy:()=>O,init:()=>P});var R=5;function V(e){let t=(e.textFingerprint??"").trim();return t?`\u201C${t.length>40?t.slice(0,39)+"\u2026":t}\u201D`:e.selector.length>60?e.tag:e.selector}var S={root:"peanut-gallery",feedback:"peanut-gallery:feedback",proposal:"peanut-gallery:proposal",accepted:"peanut-gallery:accepted",rejected:"peanut-gallery:rejected"},J="json peanut-gallery",W=80;function y(e){return(e??"").replace(/\s+/g," ").trim().slice(0,W)}function T(e){return typeof CSS<"u"&&typeof CSS.escape=="function"?CSS.escape(e):e.replace(/([^a-zA-Z0-9_-])/g,"\\$1")}var Z=["data-testid","data-test","data-cy","data-proto-id","id","name","aria-label"];function U(e){return/[0-9a-f]{6,}/i.test(e)||/^(css|sc|emotion|svelte|jsx)-/.test(e)||/^_/.test(e)||e.length>40}function Q(e){for(let r of Z){let s=e.getAttribute(r);if(s&&!U(s)){let l=r==="id"?`#${T(s)}`:`${e.tagName.toLowerCase()}[${r}="${s.replace(/"/g,'\\"')}"]`,h=e.ownerDocument;try{if(h.querySelectorAll(l).length===1)return{seg:l,unique:!0}}catch{}}}let t=e.tagName.toLowerCase(),o=Array.from(e.classList).filter(r=>!U(r)).slice(0,2),i=t+o.map(r=>`.${T(r)}`).join(""),n=e.parentElement;if(n){let r=Array.from(n.children).filter(s=>s.tagName===e.tagName);r.length>1&&(i+=`:nth-of-type(${r.indexOf(e)+1})`)}return{seg:i,unique:!1}}function tt(e){let t=[],o=e;for(;o&&o!==o.ownerDocument.documentElement;){let{seg:i,unique:n}=Q(o);if(t.unshift(i),n||(o=o.parentElement,t.length>8))break}return t.join(" > ")}function et(e){let t=e;for(;t;){let o=t.querySelector('h1, h2, h3, legend, [role="heading"]');if(o&&o!==e&&t!==e)return y(o.textContent);let i=t.getAttribute?.("aria-label")??t.getAttribute?.("aria-labelledby");if(i&&t!==e)return y(i);t=t.parentElement}}function k(e,t,o){let i=e.getBoundingClientRect(),n=e.ownerDocument.defaultView;return{selector:tt(e),textFingerprint:y(e.textContent),tag:e.tagName.toLowerCase(),rect:{x:i.left+n.scrollX,y:i.top+n.scrollY,width:i.width,height:i.height},offset:{x:i.width?Math.min(1,Math.max(0,(t-i.left)/i.width)):.5,y:i.height?Math.min(1,Math.max(0,(o-i.top)/i.height)):.5},context:et(e)}}function E(e,t){if(!t)return .5;let o=y(e.textContent);return o===t?1:o.includes(t)||t.includes(o)?.75:0}function C(e,t){let o=[];try{o=Array.from(t.querySelectorAll(e.selector))}catch{o=[]}if(o.length===1&&E(o[0],e.textFingerprint)>0)return{el:o[0],confidence:.95};if(o.length>1){let n=o.map(r=>({el:r,s:E(r,e.textFingerprint)})).sort((r,s)=>s.s-r.s);if(n[0].s>0)return{el:n[0].el,confidence:.8}}if(e.textFingerprint){let r=Array.from(t.getElementsByTagName(e.tag)).map(s=>({el:s,s:E(s,e.textFingerprint)})).sort((s,l)=>l.s-s.s);if(r[0]?.s>=.75)return{el:r[0].el,confidence:.6}}if(o.length===1)return{el:o[0],confidence:.5};let i=t.defaultView;if(i&&typeof t.elementFromPoint=="function"){let n=e.rect.x+e.rect.width/2-i.scrollX,r=e.rect.y+e.rect.height/2-i.scrollY,s=t.elementFromPoint(n,r);if(s)return{el:s,confidence:.3}}return{el:null,confidence:0}}function nt(e){let t=e.annotations.length,o=it(e.annotations[0]?.pageUrl??e.prototypeUrl),i=`Prototype feedback: ${t} comment${t===1?"":"s"} on ${o}`,n=[];return n.push(`**Prototype:** ${e.prototypeUrl}`),e.author&&n.push(`**Reviewer:** ${e.author}`),n.push(`**Session:** \`${e.id}\` \xB7 ${e.createdAt}`),n.push(""),e.annotations.forEach((r,s)=>{n.push(`### ${s+1}. ${r.kind?`[${r.kind}] `:""}${ot(r.text)}`),r.text.includes(`
2
+ `)&&n.push(r.text.split(`
3
3
  `).slice(1).join(`
4
- `)),o.push(""),o.push(`- **Page:** ${r.pageUrl}`),o.push(`- **Element:** \`${r.anchor.tag??"?"}\` \u2192 \`${r.anchor.selector}\``),r.anchor.textFingerprint&&o.push(`- **Text:** \u201C${r.anchor.textFingerprint}\u201D`),r.anchor.context&&o.push(`- **Near:** ${r.anchor.context}`),r.viewport&&Number.isFinite(r.viewport.width)&&Number.isFinite(r.viewport.height)&&o.push(`- **Viewport:** ${r.viewport.width}\xD7${r.viewport.height}`),o.push("")}),o.push("<details><summary>peanut-gallery data (do not edit)</summary>"),o.push(""),o.push("```"+K),o.push(JSON.stringify(e,null,2)),o.push("```"),o.push(""),o.push("</details>"),{title:i,body:o.join(`
5
- `)}}function U(e,t,n=[],i=7500){let o=`https://github.com/${e}/issues/new`,r=l=>{let{title:u,body:f}=W(l),v=new URLSearchParams({title:u,body:f});return n.length&&v.set("labels",n.join(",")),`${o}?${v.toString()}`},s=r(t);if(s.length<=i)return s;let p={...t,userAgent:void 0,annotations:t.annotations.map(l=>({...l,anchor:{...l.anchor,context:void 0,textFingerprint:l.anchor.textFingerprint.slice(0,40),rect:Z(l.anchor.rect)}}))};if(s=r(p),s.length<=i)return s;let c=[];for(let l of p.annotations){if(r({...p,annotations:[...c,l]}).length>i)break;c.push(l)}return r({...p,annotations:c})}function Z(e){return{x:Math.round(e.x),y:Math.round(e.y),width:Math.round(e.width),height:Math.round(e.height)}}function Q(e){return e.split(`
6
- `)[0].trim().slice(0,100)||"(no text)"}function tt(e){try{let t=new URL(e);return t.pathname==="/"?t.host:t.pathname}catch{return e}}function S(e=""){let t=typeof crypto<"u"&&"randomUUID"in crypto?crypto.randomUUID().slice(0,8):Math.random().toString(36).slice(2,10);return e?`${e}_${t}`:t}var h={conifer:"#1E4D36",emerald:"#0F7B52",wasabi:"#719872",matcha:"#9CC98A",pistachio:"#CFE1B4",seaGlass:"#CDE2F2",hydrangea:"#A6BFE5",harbor:"#40648A",chambray:"#6C8EBD",prep:"#152A4A",sunnyside:"#EAC26C",linen:"#F6F3E4",white:"#FFFFFF",gull:"#A5B0B6",carbon:"#22272B"},et={bg:h.linen,surface:h.white,text:h.carbon,muted:"#5A6268",line:"#DEDCCB",heading:h.conifer,primary:h.emerald,primaryHover:"#006340",onPrimary:h.white,toolbar:h.conifer,toolbarButton:"#325D46",toolbarButtonHover:"#4E7660",onToolbar:h.white,accent:h.sunnyside,onAccent:h.carbon,highlight:h.emerald,highlightTint:"rgba(15, 123, 82, 0.10)",pinLow:h.gull,chip:h.seaGlass,onChip:h.prep,count:h.pistachio,onCount:h.conifer,ghost:"#EDEBE3",ghostHover:"#DBD8CA",link:h.prep,toast:h.prep,onToast:h.linen,danger:"#F6E3DF",onDanger:"#9B3426",success:h.emerald,successTint:"#D4F4E2",fontBody:'"Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',fontDisplay:'"Young Serif", Georgia, "Times New Roman", serif',fontMono:'"JetBrains Mono", ui-monospace, Menlo, Consolas, monospace'},C="https://fonts.googleapis.com/css2?family=Young+Serif&family=Manrope:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap";function P(e){return{...et,...e??{}}}var nt=e=>e.replace(/[A-Z]/g,t=>"-"+t.toLowerCase());function F(e){return Object.keys(e).map(t=>`--pg-${nt(t)}: ${e[t]};`).join(" ")}var B=`
4
+ `)),n.push(""),n.push(`- **Page:** ${r.pageUrl}`),n.push(`- **Element:** \`${r.anchor.tag??"?"}\` \u2192 \`${r.anchor.selector}\``),r.anchor.textFingerprint&&n.push(`- **Text:** \u201C${r.anchor.textFingerprint}\u201D`),r.anchor.context&&n.push(`- **Near:** ${r.anchor.context}`),r.steps?.length&&n.push(`- **Reached by clicking:** ${r.steps.map(V).join(" \u2192 ")}`),r.viewport&&Number.isFinite(r.viewport.width)&&Number.isFinite(r.viewport.height)&&n.push(`- **Viewport:** ${r.viewport.width}\xD7${r.viewport.height}`),n.push("")}),n.push("<details><summary>peanut-gallery data (do not edit)</summary>"),n.push(""),n.push("```"+J),n.push(JSON.stringify(e,null,2)),n.push("```"),n.push(""),n.push("</details>"),{title:i,body:n.join(`
5
+ `)}}function D(e,t,o=[],i=7500){let n=`https://github.com/${e}/issues/new`,r=a=>{let{title:u,body:d}=nt(a),v=new URLSearchParams({title:u,body:d});return o.length&&v.set("labels",o.join(",")),`${n}?${v.toString()}`},s=r(t);if(s.length<=i)return s;let l={...t,userAgent:void 0,annotations:t.annotations.map(a=>({...a,anchor:{...a.anchor,context:void 0,textFingerprint:a.anchor.textFingerprint.slice(0,40),rect:F(a.anchor.rect)},steps:a.steps?.map(u=>({...u,context:void 0,textFingerprint:u.textFingerprint.slice(0,40),rect:F(u.rect)}))}))};if(s=r(l),s.length<=i)return s;let h=[];for(let a of l.annotations){if(r({...l,annotations:[...h,a]}).length>i)break;h.push(a)}return r({...l,annotations:h})}function F(e){return{x:Math.round(e.x),y:Math.round(e.y),width:Math.round(e.width),height:Math.round(e.height)}}function ot(e){return e.split(`
6
+ `)[0].trim().slice(0,100)||"(no text)"}function it(e){try{let t=new URL(e);return t.pathname==="/"?t.host:t.pathname}catch{return e}}function L(e=""){let t=typeof crypto<"u"&&"randomUUID"in crypto?crypto.randomUUID().slice(0,8):Math.random().toString(36).slice(2,10);return e?`${e}_${t}`:t}var c={conifer:"#1E4D36",emerald:"#0F7B52",wasabi:"#719872",matcha:"#9CC98A",pistachio:"#CFE1B4",seaGlass:"#CDE2F2",hydrangea:"#A6BFE5",harbor:"#40648A",chambray:"#6C8EBD",prep:"#152A4A",sunnyside:"#EAC26C",linen:"#F6F3E4",white:"#FFFFFF",gull:"#A5B0B6",carbon:"#22272B"},rt={bg:c.linen,surface:c.white,text:c.carbon,muted:"#5A6268",line:"#DEDCCB",heading:c.conifer,primary:c.emerald,primaryHover:"#006340",onPrimary:c.white,toolbar:c.conifer,toolbarButton:"#325D46",toolbarButtonHover:"#4E7660",onToolbar:c.white,accent:c.sunnyside,onAccent:c.carbon,highlight:c.emerald,highlightTint:"rgba(15, 123, 82, 0.10)",pinLow:c.gull,chip:c.seaGlass,onChip:c.prep,count:c.pistachio,onCount:c.conifer,ghost:"#EDEBE3",ghostHover:"#DBD8CA",link:c.prep,toast:c.prep,onToast:c.linen,danger:"#F6E3DF",onDanger:"#9B3426",success:c.emerald,successTint:"#D4F4E2",fontBody:'"Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',fontDisplay:'"Young Serif", Georgia, "Times New Roman", serif',fontMono:'"JetBrains Mono", ui-monospace, Menlo, Consolas, monospace'},A="https://fonts.googleapis.com/css2?family=Young+Serif&family=Manrope:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap";function H(e){return{...rt,...e??{}}}var st=e=>e.replace(/[A-Z]/g,t=>"-"+t.toLowerCase());function B(e){return Object.keys(e).map(t=>`--pg-${st(t)}: ${e[t]};`).join(" ")}var N=`
7
7
  :host { all: initial; }
8
8
  * { box-sizing: border-box; font-family: var(--pg-font-body); font-size: 13px; line-height: 1.4; color: var(--pg-text); }
9
9
  .pg-hidden { display: none !important; }
@@ -71,5 +71,5 @@
71
71
  .pg-empty { color: var(--pg-muted); padding: 12px 0; }
72
72
  .pg-link { color: var(--pg-link); }
73
73
  .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; }
74
- `;var b={get(e){try{return localStorage.getItem(e)}catch{return null}},set(e,t){try{localStorage.setItem(e,t)}catch{}}},ot=["bug","copy","layout","visual","flow","question","praise"],x=class{cfg;host;root;session;mode="idle";hoverEl=null;highlight;pinsLayer;panel=null;composer=null;toolbar;raf=0;destroyed=!1;constructor(t){if(!t.repo||!/^[\w.-]+\/[\w.-]+$/.test(t.repo))throw new Error('PeanutGallery: config.repo must be "owner/name"');this.cfg={mode:"auto",labels:[k.root,k.feedback],activation:"always",kinds:ot,prototypeUrl:typeof location<"u"?location.origin:"",...t}}get storageKey(){return`peanut-gallery:${this.cfg.repo}`}mount(){if(this.cfg.activation==="query"){let n=new URLSearchParams(location.search).has("peanut"),i=sessionStorage.getItem("peanut-gallery:active")==="1";if(!n&&!i)return this;sessionStorage.setItem("peanut-gallery:active","1")}this.session=this.loadSession(),this.host=document.createElement("peanut-gallery"),this.host.setAttribute("data-peanut-gallery",""),this.root=this.host.attachShadow({mode:"open"});let t=document.createElement("style");if(t.textContent=`:host { ${F(P(this.cfg.theme))} }
75
- ${B}`,this.cfg.loadFonts&&!document.querySelector(`link[href="${C}"]`)){let n=document.createElement("link");n.rel="stylesheet",n.href=C,document.head.append(n)}return this.root.append(t),this.highlight=a("div","pg-highlight pg-hidden"),this.highlight.append(a("span","pg-tag")),this.pinsLayer=a("div"),this.toolbar=this.buildToolbar(),this.root.append(this.highlight,this.pinsLayer,this.toolbar),document.body.append(this.host),document.addEventListener("mousemove",this.onMouseMove,!0),document.addEventListener("click",this.onClick,!0),document.addEventListener("keydown",this.onKey,!0),window.addEventListener("scroll",this.schedulePins,!0),window.addEventListener("resize",this.schedulePins),this.renderPins(),this.updateCount(),this}destroy(){this.destroyed=!0,document.removeEventListener("mousemove",this.onMouseMove,!0),document.removeEventListener("click",this.onClick,!0),document.removeEventListener("keydown",this.onKey,!0),window.removeEventListener("scroll",this.schedulePins,!0),window.removeEventListener("resize",this.schedulePins),this.host?.remove()}getAnnotations(){return[...this.session.annotations]}loadSession(){if(this.cfg.session&&Array.isArray(this.cfg.session.annotations))return structuredClone(this.cfg.session);try{let t=b.get(this.storageKey);if(t){let n=JSON.parse(t);if(Array.isArray(n.annotations))return n}}catch{}return this.freshSession()}freshSession(){return{id:S("rs"),prototypeUrl:this.cfg.prototypeUrl,annotations:[],createdAt:new Date().toISOString(),author:this.cfg.author??b.get("peanut-gallery:author")??void 0,userAgent:navigator.userAgent}}save(){b.set(this.storageKey,JSON.stringify(this.session)),this.updateCount(),this.renderPins(),this.panel&&this.renderPanel()}pageUrl(){return location.origin+location.pathname+location.search}buildToolbar(){let t=a("div","pg-toolbar"),n=a("span","pg-brand","Notes"),i=a("button","pg-btn","Comment");i.dataset.role="comment",i.onclick=()=>this.setMode(this.mode==="commenting"?"idle":"commenting");let o=a("button","pg-btn","Review");o.dataset.role="review",o.onclick=()=>this.togglePanel();let r=a("span","pg-count","0");return r.dataset.role="count",o.append(r),t.append(n,i,o),t}updateCount(){let t=this.toolbar.querySelector("[data-role=count]");t.textContent=String(this.session.annotations.length)}setMode(t){this.mode=t;let n=this.toolbar.querySelector("[data-role=comment]");n.classList.toggle("pg-active",t==="commenting"),n.textContent=t==="commenting"?"Click an element\u2026 (Esc)":"Comment",document.body.style.cursor=t==="commenting"?"crosshair":"",t==="idle"&&this.hideHighlight()}isOwn(t){return t instanceof Node&&(this.host===t||this.host.contains(t))}onMouseMove=t=>{if(this.mode!=="commenting")return;let n=t.composedPath();if(n.includes(this.host))return this.hideHighlight();let i=n[0];if(!(i instanceof Element)||i===document.documentElement||i===document.body)return this.hideHighlight();this.hoverEl=i;let o=i.getBoundingClientRect();Object.assign(this.highlight.style,{left:`${o.left+scrollX}px`,top:`${o.top+scrollY}px`,width:`${o.width}px`,height:`${o.height}px`}),this.highlight.querySelector(".pg-tag").textContent=it(i),this.highlight.classList.remove("pg-hidden")};hideHighlight(){this.hoverEl=null,this.highlight.classList.add("pg-hidden")}onClick=t=>{if(this.mode!=="commenting"||t.composedPath().includes(this.host))return;let n=this.hoverEl??t.target;n instanceof Element&&(t.preventDefault(),t.stopPropagation(),this.setMode("idle"),this.openComposer(n,t.clientX,t.clientY))};onKey=t=>{t.key==="Escape"&&(this.composer?this.closeComposer():this.mode==="commenting"&&this.setMode("idle"))};openComposer(t,n,i,o){this.closeComposer();let r=o?.anchor??M(t,n,i),s=a("div","pg-composer"),p=a("div","pg-meta",`${r.tag} \xB7 ${r.selector}`);p.title=r.selector;let c=document.createElement("textarea");c.placeholder="What should change here? Be specific \u2014 this becomes a task.",c.value=o?.text??"";let l=document.createElement("select");l.append(new Option("type\u2026",""));for(let d of this.cfg.kinds)l.append(new Option(d,d,!1,o?.kind===d));let u=a("div","pg-row"),f=a("button","pg-btn pg-primary",o?"Update":"Add note"),v=a("button","pg-btn pg-ghost","Cancel");if(v.onclick=()=>this.closeComposer(),f.onclick=()=>{let d=c.value.trim();if(!d)return c.focus();o?(o.text=d,o.kind=l.value||void 0):this.session.annotations.push({id:S("an"),text:d,kind:l.value||void 0,anchor:r,pageUrl:this.pageUrl(),viewport:{width:innerWidth,height:innerHeight},createdAt:new Date().toISOString(),author:this.session.author}),this.save(),this.closeComposer()},u.append(l,a("span","pg-spacer")),o){let d=a("button","pg-btn pg-danger","Delete");d.onclick=()=>{this.session.annotations=this.session.annotations.filter(N=>N.id!==o.id),this.save(),this.closeComposer()},u.append(d)}u.append(v,f),s.append(p,c,u);let R=Math.min(n+12,innerWidth-312)+scrollX,H=Math.min(i+12,innerHeight-190)+scrollY;s.style.left=`${Math.max(8,R)}px`,s.style.top=`${Math.max(8,H)}px`,this.root.append(s),this.composer=s,c.focus()}closeComposer(){this.composer?.remove(),this.composer=null}schedulePins=()=>{this.raf||this.destroyed||(this.raf=requestAnimationFrame(()=>{this.raf=0,this.renderPins()}))};renderPins(){this.pinsLayer.replaceChildren();let t=this.pageUrl();this.session.annotations.forEach((n,i)=>{if(n.pageUrl!==t)return;let{el:o,confidence:r}=T(n.anchor,document),s,p;if(o){let l=o.getBoundingClientRect();s=l.left+scrollX+l.width*n.anchor.offset.x,p=l.top+scrollY+l.height*n.anchor.offset.y}else s=n.anchor.rect.x+n.anchor.rect.width*n.anchor.offset.x,p=n.anchor.rect.y+n.anchor.rect.height*n.anchor.offset.y;let c=a("div","pg-pin"+(r<.5?" pg-low":""),String(i+1));c.title=n.text,c.style.left=`${s}px`,c.style.top=`${p}px`,c.onclick=l=>{l.stopPropagation(),this.openComposer(o??document.body,s-scrollX,p-scrollY,n)},this.pinsLayer.append(c)})}togglePanel(){if(this.panel){this.panel.remove(),this.panel=null;return}this.panel=a("div","pg-panel"),this.root.append(this.panel),this.renderPanel()}renderPanel(){let t=this.panel;t.replaceChildren();let n=a("h3","",`Feedback (${this.session.annotations.length})`);t.append(n),this.session.annotations.length||t.append(a("div","pg-empty","No notes yet. Click \u201CComment\u201D, then click anything on the page."));let i=this.pageUrl();this.session.annotations.forEach((p,c)=>{let l=a("div","pg-item"),u=a("div","pg-text");p.kind&&u.append(a("span","pg-kind",p.kind)),u.append(`${c+1}. ${p.text}`);let f=a("div","pg-meta",`${p.pageUrl===i?"this page":rt(p.pageUrl)} \xB7 ${p.anchor.tag}${p.anchor.textFingerprint?` \u201C${p.anchor.textFingerprint.slice(0,30)}\u201D`:""}`);l.append(u,f),t.append(l)});let o=a("div","pg-row"),r=a("button","pg-btn pg-ghost","Clear");r.onclick=()=>{(!this.session.annotations.length||confirm("Discard all notes?"))&&(this.session=this.freshSession(),this.save())};let s=a("button","pg-btn pg-primary",this.effectiveMode()==="issue-url"?"Submit to GitHub":"Submit");s.disabled=!this.session.annotations.length,s.onclick=()=>this.submit(s),o.append(r,a("span","pg-spacer"),s),t.append(o)}effectiveMode(){return this.cfg.onSubmit?"callback":this.cfg.mode==="relay"?"relay":this.cfg.mode==="issue-url"?"issue-url":this.cfg.relayUrl?"relay":"issue-url"}ensureAuthor(){if(this.session.author)return;let t;try{t=prompt("Your name (shown on the feedback):")?.trim()}catch{}t&&(this.session.author=t,b.set("peanut-gallery:author",t),this.session.annotations.forEach(n=>n.author??=t))}async submit(t){this.ensureAuthor();let n={...this.session,createdAt:new Date().toISOString()};if(this.effectiveMode()==="callback"){t.disabled=!0,t.textContent="Submitting\u2026";try{let i=await this.cfg.onSubmit(n)||{};i.keep||this.resetAfterSubmit(),this.toast(i.issueUrl?`Submitted: ${i.issueUrl}`:"Submitted. Thank you!")}catch(i){this.toast(`Could not submit: ${i.message}`)}finally{t.disabled=!1,t.textContent="Submit"}return}if(this.effectiveMode()==="issue-url"){let i=U(this.cfg.repo,n,this.cfg.labels);if(!window.open(i,"_blank","noopener")){this.toast("Popup blocked \u2014 allow popups and try again.");return}this.toast("Opened GitHub. Press \u201CSubmit new issue\u201D there; notes stay here until you Clear.");return}t.disabled=!0,t.textContent="Submitting\u2026";try{let i=await fetch(this.cfg.relayUrl,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({session:n,labels:this.cfg.labels})});if(!i.ok)throw new Error(`Relay responded ${i.status}`);let o=await i.json();this.resetAfterSubmit(),this.toast(o.issueUrl?`Submitted: ${o.issueUrl}`:"Submitted. Thank you!"),o.issueUrl&&window.open(o.issueUrl,"_blank","noopener")}catch(i){this.toast(`Could not submit: ${i.message}`),t.disabled=!1,t.textContent="Submit"}}resetAfterSubmit(){this.session=this.freshSession(),this.save(),this.panel&&(this.panel.remove(),this.panel=null),this.renderPins(),this.updateCount()}toast(t){let n=a("div","pg-toast",t);this.root.append(n),setTimeout(()=>n.remove(),5e3)}};function a(e,t="",n){let i=document.createElement(e);return t&&(i.className=t),n!==void 0&&(i.textContent=n),i}function it(e){let t=e.getAttribute("data-testid")??e.id;return e.tagName.toLowerCase()+(t?`#${t}`:"")}function rt(e){try{return new URL(e).pathname}catch{return e}}var m=null;function A(e){if(m)return m;m=new x(e);let t=()=>m.mount();return document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t,{once:!0}):t(),m}function D(){m?.destroy(),m=null}var g=typeof document<"u"?document.currentScript:null;g?.dataset.repo&&A({repo:g.dataset.repo,relayUrl:g.dataset.relayUrl,mode:g.dataset.mode,activation:g.dataset.activation,loadFonts:g.dataset.loadFonts==="true",author:g.dataset.author,labels:g.dataset.labels?.split(",").map(e=>e.trim())});return j(st);})();
74
+ `;var M="peanut-gallery:locationchange";function $(e){try{let t=new URL(e);for(let n of[...t.searchParams.keys()])(n==="peanut"||n.startsWith("utm_")||n==="fbclid"||n==="gclid")&&t.searchParams.delete(n);let o=t.searchParams.toString(),i=/^#!?\//.test(t.hash)?t.hash:"";return t.origin+t.pathname+(o?`?${o}`:"")+i}catch{return e}}function at(){if(typeof history>"u")return;let e=history;if(!e.__peanutGallery){e.__peanutGallery=!0;for(let t of["pushState","replaceState"]){let o=e[t];e[t]=function(...i){let n=o.apply(this,i);try{window.dispatchEvent(new Event(M))}catch{}return n}}}}var b={get(e){try{return localStorage.getItem(e)}catch{return null}},set(e,t){try{localStorage.setItem(e,t)}catch{}}},pt=["bug","copy","layout","visual","flow","question","praise"],x=class{cfg;host;root;session;mode="idle";hoverEl=null;highlight;pinsLayer;panel=null;composer=null;toolbar;raf=0;destroyed=!1;observer;steps=[];stepsPage="";constructor(t){if(!t.repo||!/^[\w.-]+\/[\w.-]+$/.test(t.repo))throw new Error('PeanutGallery: config.repo must be "owner/name"');this.cfg={mode:"auto",labels:[S.root,S.feedback],activation:"always",kinds:pt,prototypeUrl:typeof location<"u"?location.origin:"",...t}}get storageKey(){return`peanut-gallery:${this.cfg.repo}`}mount(){if(this.cfg.activation==="query"){let o=new URLSearchParams(location.search).has("peanut"),i=sessionStorage.getItem("peanut-gallery:active")==="1";if(!o&&!i)return this;sessionStorage.setItem("peanut-gallery:active","1")}this.session=this.loadSession(),this.host=document.createElement("peanut-gallery"),this.host.setAttribute("data-peanut-gallery",""),this.root=this.host.attachShadow({mode:"open"});let t=document.createElement("style");if(t.textContent=`:host { ${B(H(this.cfg.theme))} }
75
+ ${N}`,this.cfg.loadFonts&&!document.querySelector(`link[href="${A}"]`)){let o=document.createElement("link");o.rel="stylesheet",o.href=A,document.head.append(o)}return this.root.append(t),this.highlight=p("div","pg-highlight pg-hidden"),this.highlight.append(p("span","pg-tag")),this.pinsLayer=p("div"),this.toolbar=this.buildToolbar(),this.root.append(this.highlight,this.pinsLayer,this.toolbar),document.body.append(this.host),document.addEventListener("mousemove",this.onMouseMove,!0),document.addEventListener("click",this.recordStep,!0),document.addEventListener("click",this.onClick,!0),document.addEventListener("keydown",this.onKey,!0),window.addEventListener("scroll",this.schedulePins,!0),window.addEventListener("resize",this.schedulePins),at(),window.addEventListener(M,this.onLocationChange),window.addEventListener("popstate",this.onLocationChange),window.addEventListener("hashchange",this.onLocationChange),typeof MutationObserver<"u"&&(this.observer=new MutationObserver(o=>{o.some(i=>i.target!==this.host)&&this.schedulePins()}),this.observer.observe(document.body,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["class","style","hidden","open","aria-hidden"],characterData:!0})),this.renderPins(),this.updateCount(),this}onLocationChange=()=>{this.destroyed||(this.closeComposer(),this.schedulePins(),this.panel&&this.renderPanel())};destroy(){this.destroyed=!0,document.removeEventListener("mousemove",this.onMouseMove,!0),document.removeEventListener("click",this.onClick,!0),document.removeEventListener("click",this.recordStep,!0),document.removeEventListener("keydown",this.onKey,!0),window.removeEventListener("scroll",this.schedulePins,!0),window.removeEventListener("resize",this.schedulePins),window.removeEventListener(M,this.onLocationChange),window.removeEventListener("popstate",this.onLocationChange),window.removeEventListener("hashchange",this.onLocationChange),this.observer?.disconnect(),this.raf&&cancelAnimationFrame(this.raf),this.host?.remove()}getAnnotations(){return[...this.session.annotations]}loadSession(){if(this.cfg.session&&Array.isArray(this.cfg.session.annotations))return structuredClone(this.cfg.session);try{let t=b.get(this.storageKey);if(t){let o=JSON.parse(t);if(Array.isArray(o.annotations))return o}}catch{}return this.freshSession()}freshSession(){return{id:L("rs"),prototypeUrl:this.cfg.prototypeUrl,annotations:[],createdAt:new Date().toISOString(),author:this.cfg.author??b.get("peanut-gallery:author")??void 0,userAgent:navigator.userAgent}}save(){b.set(this.storageKey,JSON.stringify(this.session)),this.updateCount(),this.renderPins(),this.panel&&this.renderPanel()}pageUrl(){return $(location.href)}buildToolbar(){let t=p("div","pg-toolbar"),o=p("span","pg-brand","Notes"),i=p("button","pg-btn","Comment");i.dataset.role="comment",i.onclick=()=>this.setMode(this.mode==="commenting"?"idle":"commenting");let n=p("button","pg-btn","Review");n.dataset.role="review",n.onclick=()=>this.togglePanel();let r=p("span","pg-count","0");return r.dataset.role="count",n.append(r),t.append(o,i,n),t}updateCount(){let t=this.toolbar.querySelector("[data-role=count]");t.textContent=String(this.session.annotations.length)}setMode(t){this.mode=t;let o=this.toolbar.querySelector("[data-role=comment]");o.classList.toggle("pg-active",t==="commenting"),o.textContent=t==="commenting"?"Click an element\u2026 (Esc)":"Comment",document.body.style.cursor=t==="commenting"?"crosshair":"",t==="idle"&&this.hideHighlight()}isOwn(t){return t instanceof Node&&(this.host===t||this.host.contains(t))}onMouseMove=t=>{if(this.mode!=="commenting")return;let o=t.composedPath();if(o.includes(this.host))return this.hideHighlight();let i=o[0];if(!(i instanceof Element)||i===document.documentElement||i===document.body)return this.hideHighlight();this.hoverEl=i;let n=i.getBoundingClientRect();Object.assign(this.highlight.style,{left:`${n.left+scrollX}px`,top:`${n.top+scrollY}px`,width:`${n.width}px`,height:`${n.height}px`}),this.highlight.querySelector(".pg-tag").textContent=lt(i),this.highlight.classList.remove("pg-hidden")};hideHighlight(){this.hoverEl=null,this.highlight.classList.add("pg-hidden")}onClick=t=>{if(this.mode!=="commenting"||t.composedPath().includes(this.host))return;let o=this.hoverEl??t.target;o instanceof Element&&(t.preventDefault(),t.stopPropagation(),this.setMode("idle"),this.openComposer(o,t.clientX,t.clientY))};recordStep=t=>{if(this.mode==="commenting"||this.destroyed||this.host&&t.composedPath().includes(this.host))return;let o=t.target;if(!(o instanceof Element))return;let i=o.closest('button, a, summary, label, select, [role="button"], [role="tab"], [role="menuitem"], [role="option"], [role="switch"], [role="checkbox"], [data-testid]')??o,n=this.pageUrl();n!==this.stepsPage&&(this.steps=[],this.stepsPage=n);let r=k(i,t.clientX,t.clientY);this.steps.push({...r,context:void 0,textFingerprint:r.textFingerprint.slice(0,60)}),this.steps.length>R&&this.steps.shift()};currentSteps(){if(!(this.stepsPage!==this.pageUrl()||!this.steps.length))return this.steps.map(t=>({...t}))}onKey=t=>{t.key==="Escape"&&(this.composer?this.closeComposer():this.mode==="commenting"&&this.setMode("idle"))};openComposer(t,o,i,n){this.closeComposer();let r=n?.anchor??k(t,o,i),s=p("div","pg-composer"),l=p("div","pg-meta",`${r.tag} \xB7 ${r.selector}`);l.title=r.selector;let h=document.createElement("textarea");h.placeholder="What should change here? Be specific \u2014 this becomes a task.",h.value=n?.text??"";let a=document.createElement("select");a.append(new Option("type\u2026",""));for(let g of this.cfg.kinds)a.append(new Option(g,g,!1,n?.kind===g));let u=p("div","pg-row"),d=p("button","pg-btn pg-primary",n?"Update":"Add note"),v=p("button","pg-btn pg-ghost","Cancel");if(v.onclick=()=>this.closeComposer(),d.onclick=()=>{let g=h.value.trim();if(!g)return h.focus();n?(n.text=g,n.kind=a.value||void 0):this.session.annotations.push({id:L("an"),text:g,kind:a.value||void 0,anchor:r,pageUrl:this.pageUrl(),viewport:{width:innerWidth,height:innerHeight},createdAt:new Date().toISOString(),author:this.session.author,...this.currentSteps()?{steps:this.currentSteps()}:{}}),this.save(),this.closeComposer()},u.append(a,p("span","pg-spacer")),n){let g=p("button","pg-btn pg-danger","Delete");g.onclick=()=>{this.session.annotations=this.session.annotations.filter(I=>I.id!==n.id),this.save(),this.closeComposer()},u.append(g)}u.append(v,d),s.append(l,h,u);let _=Math.min(o+12,innerWidth-312)+scrollX,G=Math.min(i+12,innerHeight-190)+scrollY;s.style.left=`${Math.max(8,_)}px`,s.style.top=`${Math.max(8,G)}px`,this.root.append(s),this.composer=s,h.focus()}closeComposer(){this.composer?.remove(),this.composer=null}schedulePins=()=>{this.raf||this.destroyed||(this.raf=requestAnimationFrame(()=>{this.raf=0,this.renderPins()}))};pinsSignature="";renderPins(){let t=this.pageUrl(),o=[];this.session.annotations.forEach((n,r)=>{if($(n.pageUrl)!==t)return;let{el:s,confidence:l}=C(n.anchor,document);if(!s||l<.4)return;let h=s.checkVisibility;if(typeof h=="function"&&!h.call(s))return;let a=s.getBoundingClientRect(),u=Math.round(a.left+scrollX+a.width*n.anchor.offset.x),d=Math.round(a.top+scrollY+a.height*n.anchor.offset.y);o.push({a:n,n:r+1,x:u,y:d,low:l<.5})});let i=o.map(n=>`${n.a.id}:${n.n}:${n.x}:${n.y}:${n.low}:${n.a.text}`).join("|");i===this.pinsSignature&&this.pinsLayer.childElementCount===o.length||(this.pinsSignature=i,this.pinsLayer.replaceChildren(...o.map(({a:n,n:r,x:s,y:l,low:h})=>{let a=p("div","pg-pin"+(h?" pg-low":""),String(r));return a.title=n.text,a.style.left=`${s}px`,a.style.top=`${l}px`,a.onclick=u=>{u.stopPropagation();let d=C(n.anchor,document).el??document.body;this.openComposer(d,s-scrollX,l-scrollY,n)},a})))}togglePanel(){if(this.panel){this.panel.remove(),this.panel=null;return}this.panel=p("div","pg-panel"),this.root.append(this.panel),this.renderPanel()}renderPanel(){let t=this.panel;t.replaceChildren();let o=p("h3","",`Feedback (${this.session.annotations.length})`);t.append(o),this.session.annotations.length||t.append(p("div","pg-empty","No notes yet. Click \u201CComment\u201D, then click anything on the page."));let i=this.pageUrl();this.session.annotations.forEach((l,h)=>{let a=p("div","pg-item"),u=p("div","pg-text");l.kind&&u.append(p("span","pg-kind",l.kind)),u.append(`${h+1}. ${l.text}`);let d=p("div","pg-meta",`${$(l.pageUrl)===i?"this page":ct(l.pageUrl)} \xB7 ${l.anchor.tag}${l.anchor.textFingerprint?` \u201C${l.anchor.textFingerprint.slice(0,30)}\u201D`:""}`);a.append(u,d),t.append(a)});let n=p("div","pg-row"),r=p("button","pg-btn pg-ghost","Clear");r.onclick=()=>{(!this.session.annotations.length||confirm("Discard all notes?"))&&(this.session=this.freshSession(),this.save())};let s=p("button","pg-btn pg-primary",this.effectiveMode()==="issue-url"?"Submit to GitHub":"Submit");s.disabled=!this.session.annotations.length,s.onclick=()=>this.submit(s),n.append(r,p("span","pg-spacer"),s),t.append(n)}effectiveMode(){return this.cfg.onSubmit?"callback":this.cfg.mode==="relay"?"relay":this.cfg.mode==="issue-url"?"issue-url":this.cfg.relayUrl?"relay":"issue-url"}ensureAuthor(){if(this.session.author)return;let t;try{t=prompt("Your name (shown on the feedback):")?.trim()}catch{}t&&(this.session.author=t,b.set("peanut-gallery:author",t),this.session.annotations.forEach(o=>o.author??=t))}async submit(t){this.ensureAuthor();let o={...this.session,createdAt:new Date().toISOString()};if(this.effectiveMode()==="callback"){t.disabled=!0,t.textContent="Submitting\u2026";try{let i=await this.cfg.onSubmit(o)||{};i.keep||this.resetAfterSubmit(),this.toast(i.issueUrl?`Submitted: ${i.issueUrl}`:"Submitted. Thank you!")}catch(i){this.toast(`Could not submit: ${i.message}`)}finally{t.disabled=!1,t.textContent="Submit"}return}if(this.effectiveMode()==="issue-url"){let i=D(this.cfg.repo,o,this.cfg.labels);if(!window.open(i,"_blank","noopener")){this.toast("Popup blocked \u2014 allow popups and try again.");return}this.toast("Opened GitHub. Press \u201CSubmit new issue\u201D there; notes stay here until you Clear.");return}t.disabled=!0,t.textContent="Submitting\u2026";try{let i=await fetch(this.cfg.relayUrl,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({session:o,labels:this.cfg.labels})});if(!i.ok)throw new Error(`Relay responded ${i.status}`);let n=await i.json();this.resetAfterSubmit(),this.toast(n.issueUrl?`Submitted: ${n.issueUrl}`:"Submitted. Thank you!"),n.issueUrl&&window.open(n.issueUrl,"_blank","noopener")}catch(i){this.toast(`Could not submit: ${i.message}`),t.disabled=!1,t.textContent="Submit"}}resetAfterSubmit(){this.session=this.freshSession(),this.save(),this.panel&&(this.panel.remove(),this.panel=null),this.renderPins(),this.updateCount()}toast(t){let o=p("div","pg-toast",t);this.root.append(o),setTimeout(()=>o.remove(),5e3)}};function p(e,t="",o){let i=document.createElement(e);return t&&(i.className=t),o!==void 0&&(i.textContent=o),i}function lt(e){let t=e.getAttribute("data-testid")??e.id;return e.tagName.toLowerCase()+(t?`#${t}`:"")}function ct(e){try{return new URL(e).pathname}catch{return e}}var f=null;function P(e){if(f)return f;f=new x(e);let t=()=>f.mount();return document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t,{once:!0}):t(),f}function O(){f?.destroy(),f=null}var m=typeof document<"u"?document.currentScript:null;m?.dataset.repo&&P({repo:m.dataset.repo,relayUrl:m.dataset.relayUrl,mode:m.dataset.mode,activation:m.dataset.activation,loadFonts:m.dataset.loadFonts==="true",author:m.dataset.author,labels:m.dataset.labels?.split(",").map(e=>e.trim())});return Y(ht);})();
package/dist/react.cjs CHANGED
@@ -102,6 +102,37 @@ var css = `
102
102
  `;
103
103
 
104
104
  // src/widget.ts
105
+ var LOCATION_EVENT = "peanut-gallery:locationchange";
106
+ function normalizePageUrl(href) {
107
+ try {
108
+ const u = new URL(href);
109
+ for (const key of [...u.searchParams.keys()]) {
110
+ if (key === "peanut" || key.startsWith("utm_") || key === "fbclid" || key === "gclid") u.searchParams.delete(key);
111
+ }
112
+ const search = u.searchParams.toString();
113
+ const hash = /^#!?\//.test(u.hash) ? u.hash : "";
114
+ return u.origin + u.pathname + (search ? `?${search}` : "") + hash;
115
+ } catch {
116
+ return href;
117
+ }
118
+ }
119
+ function watchHistory() {
120
+ if (typeof history === "undefined") return;
121
+ const h = history;
122
+ if (h.__peanutGallery) return;
123
+ h.__peanutGallery = true;
124
+ for (const name of ["pushState", "replaceState"]) {
125
+ const original = h[name];
126
+ h[name] = function(...args) {
127
+ const result = original.apply(this, args);
128
+ try {
129
+ window.dispatchEvent(new Event(LOCATION_EVENT));
130
+ } catch {
131
+ }
132
+ return result;
133
+ };
134
+ }
135
+ }
105
136
  var store = {
106
137
  get(key) {
107
138
  try {
@@ -132,6 +163,10 @@ var PeanutGalleryWidget = class {
132
163
  toolbar;
133
164
  raf = 0;
134
165
  destroyed = false;
166
+ observer;
167
+ /** The reviewer's recent clicks on this page (see Annotation.steps). */
168
+ steps = [];
169
+ stepsPage = "";
135
170
  constructor(cfg) {
136
171
  if (!cfg.repo || !/^[\w.-]+\/[\w.-]+$/.test(cfg.repo)) throw new Error('PeanutGallery: config.repo must be "owner/name"');
137
172
  this.cfg = {
@@ -175,21 +210,44 @@ ${css}`;
175
210
  this.root.append(this.highlight, this.pinsLayer, this.toolbar);
176
211
  document.body.append(this.host);
177
212
  document.addEventListener("mousemove", this.onMouseMove, true);
213
+ document.addEventListener("click", this.recordStep, true);
178
214
  document.addEventListener("click", this.onClick, true);
179
215
  document.addEventListener("keydown", this.onKey, true);
180
216
  window.addEventListener("scroll", this.schedulePins, true);
181
217
  window.addEventListener("resize", this.schedulePins);
218
+ watchHistory();
219
+ window.addEventListener(LOCATION_EVENT, this.onLocationChange);
220
+ window.addEventListener("popstate", this.onLocationChange);
221
+ window.addEventListener("hashchange", this.onLocationChange);
222
+ if (typeof MutationObserver !== "undefined") {
223
+ this.observer = new MutationObserver((records) => {
224
+ if (records.some((r) => r.target !== this.host)) this.schedulePins();
225
+ });
226
+ this.observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style", "hidden", "open", "aria-hidden"], characterData: true });
227
+ }
182
228
  this.renderPins();
183
229
  this.updateCount();
184
230
  return this;
185
231
  }
232
+ onLocationChange = () => {
233
+ if (this.destroyed) return;
234
+ this.closeComposer();
235
+ this.schedulePins();
236
+ if (this.panel) this.renderPanel();
237
+ };
186
238
  destroy() {
187
239
  this.destroyed = true;
188
240
  document.removeEventListener("mousemove", this.onMouseMove, true);
189
241
  document.removeEventListener("click", this.onClick, true);
242
+ document.removeEventListener("click", this.recordStep, true);
190
243
  document.removeEventListener("keydown", this.onKey, true);
191
244
  window.removeEventListener("scroll", this.schedulePins, true);
192
245
  window.removeEventListener("resize", this.schedulePins);
246
+ window.removeEventListener(LOCATION_EVENT, this.onLocationChange);
247
+ window.removeEventListener("popstate", this.onLocationChange);
248
+ window.removeEventListener("hashchange", this.onLocationChange);
249
+ this.observer?.disconnect();
250
+ if (this.raf) cancelAnimationFrame(this.raf);
193
251
  this.host?.remove();
194
252
  }
195
253
  /** Current annotations (all pages). */
@@ -226,7 +284,7 @@ ${css}`;
226
284
  if (this.panel) this.renderPanel();
227
285
  }
228
286
  pageUrl() {
229
- return location.origin + location.pathname + location.search;
287
+ return normalizePageUrl(location.href);
230
288
  }
231
289
  // ---------- toolbar ----------
232
290
  buildToolbar() {
@@ -291,6 +349,30 @@ ${css}`;
291
349
  this.setMode("idle");
292
350
  this.openComposer(target, e.clientX, e.clientY);
293
351
  };
352
+ /**
353
+ * Remember what the reviewer clicks while using the app (not while placing a note), so a
354
+ * note inside a modal or a menu carries the clicks that opened it. Recorded in the capture
355
+ * phase, before the app re-renders. Only the element is kept, never what was typed.
356
+ */
357
+ recordStep = (e) => {
358
+ if (this.mode === "commenting" || this.destroyed) return;
359
+ if (this.host && e.composedPath().includes(this.host)) return;
360
+ const raw = e.target;
361
+ if (!(raw instanceof Element)) return;
362
+ const target = raw.closest('button, a, summary, label, select, [role="button"], [role="tab"], [role="menuitem"], [role="option"], [role="switch"], [role="checkbox"], [data-testid]') ?? raw;
363
+ const here = this.pageUrl();
364
+ if (here !== this.stepsPage) {
365
+ this.steps = [];
366
+ this.stepsPage = here;
367
+ }
368
+ const anchor = (0, import_peanut_gallery_core.createAnchor)(target, e.clientX, e.clientY);
369
+ this.steps.push({ ...anchor, context: void 0, textFingerprint: anchor.textFingerprint.slice(0, 60) });
370
+ if (this.steps.length > import_peanut_gallery_core.MAX_STEPS) this.steps.shift();
371
+ };
372
+ currentSteps() {
373
+ if (this.stepsPage !== this.pageUrl() || !this.steps.length) return void 0;
374
+ return this.steps.map((st) => ({ ...st }));
375
+ }
294
376
  onKey = (e) => {
295
377
  if (e.key === "Escape") {
296
378
  if (this.composer) this.closeComposer();
@@ -329,7 +411,8 @@ ${css}`;
329
411
  pageUrl: this.pageUrl(),
330
412
  viewport: { width: innerWidth, height: innerHeight },
331
413
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
332
- author: this.session.author
414
+ author: this.session.author,
415
+ ...this.currentSteps() ? { steps: this.currentSteps() } : {}
333
416
  });
334
417
  }
335
418
  this.save();
@@ -367,31 +450,38 @@ ${css}`;
367
450
  this.renderPins();
368
451
  });
369
452
  };
453
+ pinsSignature = "";
370
454
  renderPins() {
371
- this.pinsLayer.replaceChildren();
372
455
  const here = this.pageUrl();
456
+ const placed = [];
373
457
  this.session.annotations.forEach((a, i) => {
374
- if (a.pageUrl !== here) return;
458
+ if (normalizePageUrl(a.pageUrl) !== here) return;
375
459
  const { el: target, confidence } = (0, import_peanut_gallery_core.resolveAnchor)(a.anchor, document);
376
- let x, y;
377
- if (target) {
378
- const r = target.getBoundingClientRect();
379
- x = r.left + scrollX + r.width * a.anchor.offset.x;
380
- y = r.top + scrollY + r.height * a.anchor.offset.y;
381
- } else {
382
- x = a.anchor.rect.x + a.anchor.rect.width * a.anchor.offset.x;
383
- y = a.anchor.rect.y + a.anchor.rect.height * a.anchor.offset.y;
384
- }
385
- const pin = el("div", "pg-pin" + (confidence < 0.5 ? " pg-low" : ""), String(i + 1));
386
- pin.title = a.text;
387
- pin.style.left = `${x}px`;
388
- pin.style.top = `${y}px`;
389
- pin.onclick = (ev) => {
390
- ev.stopPropagation();
391
- this.openComposer(target ?? document.body, x - scrollX, y - scrollY, a);
392
- };
393
- this.pinsLayer.append(pin);
460
+ if (!target || confidence < 0.4) return;
461
+ const vis = target.checkVisibility;
462
+ if (typeof vis === "function" && !vis.call(target)) return;
463
+ const r = target.getBoundingClientRect();
464
+ const x = Math.round(r.left + scrollX + r.width * a.anchor.offset.x);
465
+ const y = Math.round(r.top + scrollY + r.height * a.anchor.offset.y);
466
+ placed.push({ a, n: i + 1, x, y, low: confidence < 0.5 });
394
467
  });
468
+ const sig = placed.map((p) => `${p.a.id}:${p.n}:${p.x}:${p.y}:${p.low}:${p.a.text}`).join("|");
469
+ if (sig === this.pinsSignature && this.pinsLayer.childElementCount === placed.length) return;
470
+ this.pinsSignature = sig;
471
+ this.pinsLayer.replaceChildren(
472
+ ...placed.map(({ a, n, x, y, low }) => {
473
+ const pin = el("div", "pg-pin" + (low ? " pg-low" : ""), String(n));
474
+ pin.title = a.text;
475
+ pin.style.left = `${x}px`;
476
+ pin.style.top = `${y}px`;
477
+ pin.onclick = (ev) => {
478
+ ev.stopPropagation();
479
+ const target = (0, import_peanut_gallery_core.resolveAnchor)(a.anchor, document).el ?? document.body;
480
+ this.openComposer(target, x - scrollX, y - scrollY, a);
481
+ };
482
+ return pin;
483
+ })
484
+ );
395
485
  }
396
486
  // ---------- panel ----------
397
487
  togglePanel() {
@@ -418,7 +508,7 @@ ${css}`;
418
508
  const text = el("div", "pg-text");
419
509
  if (a.kind) text.append(el("span", "pg-kind", a.kind));
420
510
  text.append(`${i + 1}. ${a.text}`);
421
- const meta = el("div", "pg-meta", `${a.pageUrl === here ? "this page" : pathOf(a.pageUrl)} \xB7 ${a.anchor.tag}${a.anchor.textFingerprint ? ` \u201C${a.anchor.textFingerprint.slice(0, 30)}\u201D` : ""}`);
511
+ 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` : ""}`);
422
512
  item.append(text, meta);
423
513
  p.append(item);
424
514
  });
package/dist/react.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  destroy,
3
3
  init
4
- } from "./chunk-O6C4PORF.js";
4
+ } from "./chunk-X4WAQVYW.js";
5
5
 
6
6
  // src/react.ts
7
7
  import { useEffect } from "react";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixelatlarge/peanut-gallery-widget",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Drop-in annotation layer for hosted prototypes. Comments attach to DOM elements and submit to GitHub Issues.",
5
5
  "keywords": [
6
6
  "prototype",
@@ -49,7 +49,7 @@
49
49
  "typecheck": "tsc --noEmit"
50
50
  },
51
51
  "dependencies": {
52
- "@pixelatlarge/peanut-gallery-core": "0.1.0"
52
+ "@pixelatlarge/peanut-gallery-core": "0.1.1"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "react": ">=17"