@pixelatlarge/peanut-gallery-widget 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +32 -2
- package/dist/chunk-O6C4PORF.js +539 -0
- package/dist/index.cjs +558 -0
- package/dist/index.d.cts +100 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.js +10 -0
- package/dist/peanut-gallery.global.js +75 -0
- package/dist/react.cjs +569 -0
- package/dist/react.d.cts +12 -0
- package/dist/react.d.ts +12 -0
- package/dist/react.js +20 -0
- package/package.json +68 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
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 store = {
|
|
105
|
+
get(key) {
|
|
106
|
+
try {
|
|
107
|
+
return localStorage.getItem(key);
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
set(key, value) {
|
|
113
|
+
try {
|
|
114
|
+
localStorage.setItem(key, value);
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var DEFAULT_KINDS = ["bug", "copy", "layout", "visual", "flow", "question", "praise"];
|
|
120
|
+
var PeanutGalleryWidget = class {
|
|
121
|
+
cfg;
|
|
122
|
+
host;
|
|
123
|
+
root;
|
|
124
|
+
session;
|
|
125
|
+
mode = "idle";
|
|
126
|
+
hoverEl = null;
|
|
127
|
+
highlight;
|
|
128
|
+
pinsLayer;
|
|
129
|
+
panel = null;
|
|
130
|
+
composer = null;
|
|
131
|
+
toolbar;
|
|
132
|
+
raf = 0;
|
|
133
|
+
destroyed = false;
|
|
134
|
+
constructor(cfg) {
|
|
135
|
+
if (!cfg.repo || !/^[\w.-]+\/[\w.-]+$/.test(cfg.repo)) throw new Error('PeanutGallery: config.repo must be "owner/name"');
|
|
136
|
+
this.cfg = {
|
|
137
|
+
mode: "auto",
|
|
138
|
+
labels: [import_peanut_gallery_core.LABELS.root, import_peanut_gallery_core.LABELS.feedback],
|
|
139
|
+
activation: "always",
|
|
140
|
+
kinds: DEFAULT_KINDS,
|
|
141
|
+
prototypeUrl: typeof location !== "undefined" ? location.origin : "",
|
|
142
|
+
...cfg
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
get storageKey() {
|
|
146
|
+
return `peanut-gallery:${this.cfg.repo}`;
|
|
147
|
+
}
|
|
148
|
+
/** Mount the widget. Safe to call once DOM is ready. */
|
|
149
|
+
mount() {
|
|
150
|
+
if (this.cfg.activation === "query") {
|
|
151
|
+
const want = new URLSearchParams(location.search).has("peanut");
|
|
152
|
+
const sticky = sessionStorage.getItem("peanut-gallery:active") === "1";
|
|
153
|
+
if (!want && !sticky) return this;
|
|
154
|
+
sessionStorage.setItem("peanut-gallery:active", "1");
|
|
155
|
+
}
|
|
156
|
+
this.session = this.loadSession();
|
|
157
|
+
this.host = document.createElement("peanut-gallery");
|
|
158
|
+
this.host.setAttribute("data-peanut-gallery", "");
|
|
159
|
+
this.root = this.host.attachShadow({ mode: "open" });
|
|
160
|
+
const style = document.createElement("style");
|
|
161
|
+
style.textContent = `:host { ${(0, import_peanut_gallery_core.themeToCssVars)((0, import_peanut_gallery_core.resolveTheme)(this.cfg.theme))} }
|
|
162
|
+
${css}`;
|
|
163
|
+
if (this.cfg.loadFonts && !document.querySelector(`link[href="${import_peanut_gallery_core.DEFAULT_FONTS_URL}"]`)) {
|
|
164
|
+
const link = document.createElement("link");
|
|
165
|
+
link.rel = "stylesheet";
|
|
166
|
+
link.href = import_peanut_gallery_core.DEFAULT_FONTS_URL;
|
|
167
|
+
document.head.append(link);
|
|
168
|
+
}
|
|
169
|
+
this.root.append(style);
|
|
170
|
+
this.highlight = el("div", "pg-highlight pg-hidden");
|
|
171
|
+
this.highlight.append(el("span", "pg-tag"));
|
|
172
|
+
this.pinsLayer = el("div");
|
|
173
|
+
this.toolbar = this.buildToolbar();
|
|
174
|
+
this.root.append(this.highlight, this.pinsLayer, this.toolbar);
|
|
175
|
+
document.body.append(this.host);
|
|
176
|
+
document.addEventListener("mousemove", this.onMouseMove, true);
|
|
177
|
+
document.addEventListener("click", this.onClick, true);
|
|
178
|
+
document.addEventListener("keydown", this.onKey, true);
|
|
179
|
+
window.addEventListener("scroll", this.schedulePins, true);
|
|
180
|
+
window.addEventListener("resize", this.schedulePins);
|
|
181
|
+
this.renderPins();
|
|
182
|
+
this.updateCount();
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
destroy() {
|
|
186
|
+
this.destroyed = true;
|
|
187
|
+
document.removeEventListener("mousemove", this.onMouseMove, true);
|
|
188
|
+
document.removeEventListener("click", this.onClick, true);
|
|
189
|
+
document.removeEventListener("keydown", this.onKey, true);
|
|
190
|
+
window.removeEventListener("scroll", this.schedulePins, true);
|
|
191
|
+
window.removeEventListener("resize", this.schedulePins);
|
|
192
|
+
this.host?.remove();
|
|
193
|
+
}
|
|
194
|
+
/** Current annotations (all pages). */
|
|
195
|
+
getAnnotations() {
|
|
196
|
+
return [...this.session.annotations];
|
|
197
|
+
}
|
|
198
|
+
// ---------- state ----------
|
|
199
|
+
loadSession() {
|
|
200
|
+
if (this.cfg.session && Array.isArray(this.cfg.session.annotations)) return structuredClone(this.cfg.session);
|
|
201
|
+
try {
|
|
202
|
+
const raw = store.get(this.storageKey);
|
|
203
|
+
if (raw) {
|
|
204
|
+
const s = JSON.parse(raw);
|
|
205
|
+
if (Array.isArray(s.annotations)) return s;
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
return this.freshSession();
|
|
210
|
+
}
|
|
211
|
+
freshSession() {
|
|
212
|
+
return {
|
|
213
|
+
id: (0, import_peanut_gallery_core.uid)("rs"),
|
|
214
|
+
prototypeUrl: this.cfg.prototypeUrl,
|
|
215
|
+
annotations: [],
|
|
216
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
217
|
+
author: this.cfg.author ?? store.get("peanut-gallery:author") ?? void 0,
|
|
218
|
+
userAgent: navigator.userAgent
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
save() {
|
|
222
|
+
store.set(this.storageKey, JSON.stringify(this.session));
|
|
223
|
+
this.updateCount();
|
|
224
|
+
this.renderPins();
|
|
225
|
+
if (this.panel) this.renderPanel();
|
|
226
|
+
}
|
|
227
|
+
pageUrl() {
|
|
228
|
+
return location.origin + location.pathname + location.search;
|
|
229
|
+
}
|
|
230
|
+
// ---------- toolbar ----------
|
|
231
|
+
buildToolbar() {
|
|
232
|
+
const bar = el("div", "pg-toolbar");
|
|
233
|
+
const brand = el("span", "pg-brand", "Notes");
|
|
234
|
+
const comment = el("button", "pg-btn", "Comment");
|
|
235
|
+
comment.dataset.role = "comment";
|
|
236
|
+
comment.onclick = () => this.setMode(this.mode === "commenting" ? "idle" : "commenting");
|
|
237
|
+
const review = el("button", "pg-btn", "Review");
|
|
238
|
+
review.dataset.role = "review";
|
|
239
|
+
review.onclick = () => this.togglePanel();
|
|
240
|
+
const count = el("span", "pg-count", "0");
|
|
241
|
+
count.dataset.role = "count";
|
|
242
|
+
review.append(count);
|
|
243
|
+
bar.append(brand, comment, review);
|
|
244
|
+
return bar;
|
|
245
|
+
}
|
|
246
|
+
updateCount() {
|
|
247
|
+
const c = this.toolbar.querySelector("[data-role=count]");
|
|
248
|
+
c.textContent = String(this.session.annotations.length);
|
|
249
|
+
}
|
|
250
|
+
setMode(m) {
|
|
251
|
+
this.mode = m;
|
|
252
|
+
const btn = this.toolbar.querySelector("[data-role=comment]");
|
|
253
|
+
btn.classList.toggle("pg-active", m === "commenting");
|
|
254
|
+
btn.textContent = m === "commenting" ? "Click an element\u2026 (Esc)" : "Comment";
|
|
255
|
+
document.body.style.cursor = m === "commenting" ? "crosshair" : "";
|
|
256
|
+
if (m === "idle") this.hideHighlight();
|
|
257
|
+
}
|
|
258
|
+
// ---------- events ----------
|
|
259
|
+
isOwn(target) {
|
|
260
|
+
return target instanceof Node && (this.host === target || this.host.contains(target));
|
|
261
|
+
}
|
|
262
|
+
onMouseMove = (e) => {
|
|
263
|
+
if (this.mode !== "commenting") return;
|
|
264
|
+
const path = e.composedPath();
|
|
265
|
+
if (path.includes(this.host)) return this.hideHighlight();
|
|
266
|
+
const target = path[0];
|
|
267
|
+
if (!(target instanceof Element) || target === document.documentElement || target === document.body) return this.hideHighlight();
|
|
268
|
+
this.hoverEl = target;
|
|
269
|
+
const r = target.getBoundingClientRect();
|
|
270
|
+
Object.assign(this.highlight.style, {
|
|
271
|
+
left: `${r.left + scrollX}px`,
|
|
272
|
+
top: `${r.top + scrollY}px`,
|
|
273
|
+
width: `${r.width}px`,
|
|
274
|
+
height: `${r.height}px`
|
|
275
|
+
});
|
|
276
|
+
this.highlight.querySelector(".pg-tag").textContent = describe(target);
|
|
277
|
+
this.highlight.classList.remove("pg-hidden");
|
|
278
|
+
};
|
|
279
|
+
hideHighlight() {
|
|
280
|
+
this.hoverEl = null;
|
|
281
|
+
this.highlight.classList.add("pg-hidden");
|
|
282
|
+
}
|
|
283
|
+
onClick = (e) => {
|
|
284
|
+
if (this.mode !== "commenting") return;
|
|
285
|
+
if (e.composedPath().includes(this.host)) return;
|
|
286
|
+
const target = this.hoverEl ?? e.target;
|
|
287
|
+
if (!(target instanceof Element)) return;
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
e.stopPropagation();
|
|
290
|
+
this.setMode("idle");
|
|
291
|
+
this.openComposer(target, e.clientX, e.clientY);
|
|
292
|
+
};
|
|
293
|
+
onKey = (e) => {
|
|
294
|
+
if (e.key === "Escape") {
|
|
295
|
+
if (this.composer) this.closeComposer();
|
|
296
|
+
else if (this.mode === "commenting") this.setMode("idle");
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
// ---------- composer ----------
|
|
300
|
+
openComposer(target, clientX, clientY, existing) {
|
|
301
|
+
this.closeComposer();
|
|
302
|
+
const anchor = existing?.anchor ?? (0, import_peanut_gallery_core.createAnchor)(target, clientX, clientY);
|
|
303
|
+
const box = el("div", "pg-composer");
|
|
304
|
+
const meta = el("div", "pg-meta", `${anchor.tag} \xB7 ${anchor.selector}`);
|
|
305
|
+
meta.title = anchor.selector;
|
|
306
|
+
const ta = document.createElement("textarea");
|
|
307
|
+
ta.placeholder = "What should change here? Be specific \u2014 this becomes a task.";
|
|
308
|
+
ta.value = existing?.text ?? "";
|
|
309
|
+
const kind = document.createElement("select");
|
|
310
|
+
kind.append(new Option("type\u2026", ""));
|
|
311
|
+
for (const k of this.cfg.kinds) kind.append(new Option(k, k, false, existing?.kind === k));
|
|
312
|
+
const row = el("div", "pg-row");
|
|
313
|
+
const save = el("button", "pg-btn pg-primary", existing ? "Update" : "Add note");
|
|
314
|
+
const cancel = el("button", "pg-btn pg-ghost", "Cancel");
|
|
315
|
+
cancel.onclick = () => this.closeComposer();
|
|
316
|
+
save.onclick = () => {
|
|
317
|
+
const text = ta.value.trim();
|
|
318
|
+
if (!text) return ta.focus();
|
|
319
|
+
if (existing) {
|
|
320
|
+
existing.text = text;
|
|
321
|
+
existing.kind = kind.value || void 0;
|
|
322
|
+
} else {
|
|
323
|
+
this.session.annotations.push({
|
|
324
|
+
id: (0, import_peanut_gallery_core.uid)("an"),
|
|
325
|
+
text,
|
|
326
|
+
kind: kind.value || void 0,
|
|
327
|
+
anchor,
|
|
328
|
+
pageUrl: this.pageUrl(),
|
|
329
|
+
viewport: { width: innerWidth, height: innerHeight },
|
|
330
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
331
|
+
author: this.session.author
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
this.save();
|
|
335
|
+
this.closeComposer();
|
|
336
|
+
};
|
|
337
|
+
row.append(kind, el("span", "pg-spacer"));
|
|
338
|
+
if (existing) {
|
|
339
|
+
const del = el("button", "pg-btn pg-danger", "Delete");
|
|
340
|
+
del.onclick = () => {
|
|
341
|
+
this.session.annotations = this.session.annotations.filter((a) => a.id !== existing.id);
|
|
342
|
+
this.save();
|
|
343
|
+
this.closeComposer();
|
|
344
|
+
};
|
|
345
|
+
row.append(del);
|
|
346
|
+
}
|
|
347
|
+
row.append(cancel, save);
|
|
348
|
+
box.append(meta, ta, row);
|
|
349
|
+
const x = Math.min(clientX + 12, innerWidth - 312) + scrollX;
|
|
350
|
+
const y = Math.min(clientY + 12, innerHeight - 190) + scrollY;
|
|
351
|
+
box.style.left = `${Math.max(8, x)}px`;
|
|
352
|
+
box.style.top = `${Math.max(8, y)}px`;
|
|
353
|
+
this.root.append(box);
|
|
354
|
+
this.composer = box;
|
|
355
|
+
ta.focus();
|
|
356
|
+
}
|
|
357
|
+
closeComposer() {
|
|
358
|
+
this.composer?.remove();
|
|
359
|
+
this.composer = null;
|
|
360
|
+
}
|
|
361
|
+
// ---------- pins ----------
|
|
362
|
+
schedulePins = () => {
|
|
363
|
+
if (this.raf || this.destroyed) return;
|
|
364
|
+
this.raf = requestAnimationFrame(() => {
|
|
365
|
+
this.raf = 0;
|
|
366
|
+
this.renderPins();
|
|
367
|
+
});
|
|
368
|
+
};
|
|
369
|
+
renderPins() {
|
|
370
|
+
this.pinsLayer.replaceChildren();
|
|
371
|
+
const here = this.pageUrl();
|
|
372
|
+
this.session.annotations.forEach((a, i) => {
|
|
373
|
+
if (a.pageUrl !== here) return;
|
|
374
|
+
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);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
// ---------- panel ----------
|
|
396
|
+
togglePanel() {
|
|
397
|
+
if (this.panel) {
|
|
398
|
+
this.panel.remove();
|
|
399
|
+
this.panel = null;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
this.panel = el("div", "pg-panel");
|
|
403
|
+
this.root.append(this.panel);
|
|
404
|
+
this.renderPanel();
|
|
405
|
+
}
|
|
406
|
+
renderPanel() {
|
|
407
|
+
const p = this.panel;
|
|
408
|
+
p.replaceChildren();
|
|
409
|
+
const h = el("h3", "", `Feedback (${this.session.annotations.length})`);
|
|
410
|
+
p.append(h);
|
|
411
|
+
if (!this.session.annotations.length) {
|
|
412
|
+
p.append(el("div", "pg-empty", "No notes yet. Click \u201CComment\u201D, then click anything on the page."));
|
|
413
|
+
}
|
|
414
|
+
const here = this.pageUrl();
|
|
415
|
+
this.session.annotations.forEach((a, i) => {
|
|
416
|
+
const item = el("div", "pg-item");
|
|
417
|
+
const text = el("div", "pg-text");
|
|
418
|
+
if (a.kind) text.append(el("span", "pg-kind", a.kind));
|
|
419
|
+
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` : ""}`);
|
|
421
|
+
item.append(text, meta);
|
|
422
|
+
p.append(item);
|
|
423
|
+
});
|
|
424
|
+
const row = el("div", "pg-row");
|
|
425
|
+
const clear = el("button", "pg-btn pg-ghost", "Clear");
|
|
426
|
+
clear.onclick = () => {
|
|
427
|
+
if (!this.session.annotations.length || confirm("Discard all notes?")) {
|
|
428
|
+
this.session = this.freshSession();
|
|
429
|
+
this.save();
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
const submit = el("button", "pg-btn pg-primary", this.effectiveMode() === "issue-url" ? "Submit to GitHub" : "Submit");
|
|
433
|
+
submit.disabled = !this.session.annotations.length;
|
|
434
|
+
submit.onclick = () => this.submit(submit);
|
|
435
|
+
row.append(clear, el("span", "pg-spacer"), submit);
|
|
436
|
+
p.append(row);
|
|
437
|
+
}
|
|
438
|
+
effectiveMode() {
|
|
439
|
+
if (this.cfg.onSubmit) return "callback";
|
|
440
|
+
if (this.cfg.mode === "relay") return "relay";
|
|
441
|
+
if (this.cfg.mode === "issue-url") return "issue-url";
|
|
442
|
+
return this.cfg.relayUrl ? "relay" : "issue-url";
|
|
443
|
+
}
|
|
444
|
+
ensureAuthor() {
|
|
445
|
+
if (this.session.author) return;
|
|
446
|
+
let name;
|
|
447
|
+
try {
|
|
448
|
+
name = prompt("Your name (shown on the feedback):")?.trim();
|
|
449
|
+
} catch {
|
|
450
|
+
}
|
|
451
|
+
if (name) {
|
|
452
|
+
this.session.author = name;
|
|
453
|
+
store.set("peanut-gallery:author", name);
|
|
454
|
+
this.session.annotations.forEach((a) => a.author ??= name);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async submit(btn) {
|
|
458
|
+
this.ensureAuthor();
|
|
459
|
+
const session = { ...this.session, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
460
|
+
if (this.effectiveMode() === "callback") {
|
|
461
|
+
btn.disabled = true;
|
|
462
|
+
btn.textContent = "Submitting\u2026";
|
|
463
|
+
try {
|
|
464
|
+
const out = await this.cfg.onSubmit(session) || {};
|
|
465
|
+
if (!out.keep) this.resetAfterSubmit();
|
|
466
|
+
this.toast(out.issueUrl ? `Submitted: ${out.issueUrl}` : "Submitted. Thank you!");
|
|
467
|
+
} catch (err) {
|
|
468
|
+
this.toast(`Could not submit: ${err.message}`);
|
|
469
|
+
} finally {
|
|
470
|
+
btn.disabled = false;
|
|
471
|
+
btn.textContent = "Submit";
|
|
472
|
+
}
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (this.effectiveMode() === "issue-url") {
|
|
476
|
+
const url = (0, import_peanut_gallery_core.newIssueUrl)(this.cfg.repo, session, this.cfg.labels);
|
|
477
|
+
const w = window.open(url, "_blank", "noopener");
|
|
478
|
+
if (!w) {
|
|
479
|
+
this.toast("Popup blocked \u2014 allow popups and try again.");
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
this.toast("Opened GitHub. Press \u201CSubmit new issue\u201D there; notes stay here until you Clear.");
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
btn.disabled = true;
|
|
486
|
+
btn.textContent = "Submitting\u2026";
|
|
487
|
+
try {
|
|
488
|
+
const res = await fetch(this.cfg.relayUrl, {
|
|
489
|
+
method: "POST",
|
|
490
|
+
headers: { "content-type": "application/json" },
|
|
491
|
+
body: JSON.stringify({ session, labels: this.cfg.labels })
|
|
492
|
+
});
|
|
493
|
+
if (!res.ok) throw new Error(`Relay responded ${res.status}`);
|
|
494
|
+
const data = await res.json();
|
|
495
|
+
this.resetAfterSubmit();
|
|
496
|
+
this.toast(data.issueUrl ? `Submitted: ${data.issueUrl}` : "Submitted. Thank you!");
|
|
497
|
+
if (data.issueUrl) window.open(data.issueUrl, "_blank", "noopener");
|
|
498
|
+
} catch (err) {
|
|
499
|
+
this.toast(`Could not submit: ${err.message}`);
|
|
500
|
+
btn.disabled = false;
|
|
501
|
+
btn.textContent = "Submit";
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/** Clear the submitted notes and redraw: pins, count and the panel. */
|
|
505
|
+
resetAfterSubmit() {
|
|
506
|
+
this.session = this.freshSession();
|
|
507
|
+
this.save();
|
|
508
|
+
if (this.panel) {
|
|
509
|
+
this.panel.remove();
|
|
510
|
+
this.panel = null;
|
|
511
|
+
}
|
|
512
|
+
this.renderPins();
|
|
513
|
+
this.updateCount();
|
|
514
|
+
}
|
|
515
|
+
toast(msg) {
|
|
516
|
+
const t = el("div", "pg-toast", msg);
|
|
517
|
+
this.root.append(t);
|
|
518
|
+
setTimeout(() => t.remove(), 5e3);
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
function el(tag, className = "", text) {
|
|
522
|
+
const e = document.createElement(tag);
|
|
523
|
+
if (className) e.className = className;
|
|
524
|
+
if (text !== void 0) e.textContent = text;
|
|
525
|
+
return e;
|
|
526
|
+
}
|
|
527
|
+
function describe(target) {
|
|
528
|
+
const id = target.getAttribute("data-testid") ?? target.id;
|
|
529
|
+
return target.tagName.toLowerCase() + (id ? `#${id}` : "");
|
|
530
|
+
}
|
|
531
|
+
function pathOf(url) {
|
|
532
|
+
try {
|
|
533
|
+
return new URL(url).pathname;
|
|
534
|
+
} catch {
|
|
535
|
+
return url;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/index.ts
|
|
540
|
+
var instance = null;
|
|
541
|
+
function init(config) {
|
|
542
|
+
if (instance) return instance;
|
|
543
|
+
instance = new PeanutGalleryWidget(config);
|
|
544
|
+
const mount = () => instance.mount();
|
|
545
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", mount, { once: true });
|
|
546
|
+
else mount();
|
|
547
|
+
return instance;
|
|
548
|
+
}
|
|
549
|
+
function destroy() {
|
|
550
|
+
instance?.destroy();
|
|
551
|
+
instance = null;
|
|
552
|
+
}
|
|
553
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
554
|
+
0 && (module.exports = {
|
|
555
|
+
PeanutGalleryWidget,
|
|
556
|
+
destroy,
|
|
557
|
+
init
|
|
558
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { AnnotationKind, Theme, ReviewSession, Annotation } from '@pixelatlarge/peanut-gallery-core';
|
|
2
|
+
export { Annotation, AnnotationKind, ReviewSession } from '@pixelatlarge/peanut-gallery-core';
|
|
3
|
+
|
|
4
|
+
interface PeanutGalleryConfig {
|
|
5
|
+
/** GitHub repo as "owner/name". Required. */
|
|
6
|
+
repo: string;
|
|
7
|
+
/**
|
|
8
|
+
* How to submit:
|
|
9
|
+
* - "issue-url": open a prefilled github.com/.../issues/new tab (zero infra, reviewer needs GitHub access)
|
|
10
|
+
* - "relay": POST the session to `relayUrl`, which creates the issue server-side
|
|
11
|
+
* - "auto" (default): relay if `relayUrl` is set, otherwise issue-url
|
|
12
|
+
*/
|
|
13
|
+
mode?: 'issue-url' | 'relay' | 'auto';
|
|
14
|
+
relayUrl?: string;
|
|
15
|
+
/** Labels to apply. Defaults to ["peanut-gallery", "peanut-gallery:feedback"]. */
|
|
16
|
+
labels?: string[];
|
|
17
|
+
/** Reviewer name; if omitted the widget asks once and remembers it. */
|
|
18
|
+
author?: string;
|
|
19
|
+
/** Base URL of the prototype, defaults to location.origin. */
|
|
20
|
+
prototypeUrl?: string;
|
|
21
|
+
/**
|
|
22
|
+
* When to show the widget:
|
|
23
|
+
* - "always" (default)
|
|
24
|
+
* - "query": only when the URL has ?peanut (sticky for the session)
|
|
25
|
+
*/
|
|
26
|
+
activation?: 'always' | 'query';
|
|
27
|
+
/** Which annotation kinds to offer. */
|
|
28
|
+
kinds?: AnnotationKind[];
|
|
29
|
+
/** Override any colour or font role. Defaults to the Conifer / Prep / Wasabi palette. */
|
|
30
|
+
theme?: Partial<Theme>;
|
|
31
|
+
/**
|
|
32
|
+
* Load the default fonts (Manrope, Young Serif, JetBrains Mono) from Google Fonts into the page.
|
|
33
|
+
* Off by default so the widget never adds third-party requests to a prototype unasked;
|
|
34
|
+
* without it the widget uses those fonts if the page already has them, else the system UI font.
|
|
35
|
+
*/
|
|
36
|
+
loadFonts?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Handle submission yourself instead of GitHub (e.g. send to your own backend, or a demo).
|
|
39
|
+
* Overrides `mode`. Resolve to clear the notes; return `{ issueUrl }` to link to the result,
|
|
40
|
+
* or `{ keep: true }` to leave the notes on the page. Throw to show an error and keep them.
|
|
41
|
+
*/
|
|
42
|
+
onSubmit?: (session: ReviewSession) => void | Promise<void | {
|
|
43
|
+
issueUrl?: string;
|
|
44
|
+
keep?: boolean;
|
|
45
|
+
}>;
|
|
46
|
+
/** Start from these notes instead of the ones saved in this browser (e.g. a demo or a test). */
|
|
47
|
+
session?: ReviewSession;
|
|
48
|
+
}
|
|
49
|
+
declare class PeanutGalleryWidget {
|
|
50
|
+
private cfg;
|
|
51
|
+
private host;
|
|
52
|
+
private root;
|
|
53
|
+
private session;
|
|
54
|
+
private mode;
|
|
55
|
+
private hoverEl;
|
|
56
|
+
private highlight;
|
|
57
|
+
private pinsLayer;
|
|
58
|
+
private panel;
|
|
59
|
+
private composer;
|
|
60
|
+
private toolbar;
|
|
61
|
+
private raf;
|
|
62
|
+
private destroyed;
|
|
63
|
+
constructor(cfg: PeanutGalleryConfig);
|
|
64
|
+
get storageKey(): string;
|
|
65
|
+
/** Mount the widget. Safe to call once DOM is ready. */
|
|
66
|
+
mount(): this;
|
|
67
|
+
destroy(): void;
|
|
68
|
+
/** Current annotations (all pages). */
|
|
69
|
+
getAnnotations(): Annotation[];
|
|
70
|
+
private loadSession;
|
|
71
|
+
private freshSession;
|
|
72
|
+
private save;
|
|
73
|
+
private pageUrl;
|
|
74
|
+
private buildToolbar;
|
|
75
|
+
private updateCount;
|
|
76
|
+
private setMode;
|
|
77
|
+
private isOwn;
|
|
78
|
+
private onMouseMove;
|
|
79
|
+
private hideHighlight;
|
|
80
|
+
private onClick;
|
|
81
|
+
private onKey;
|
|
82
|
+
private openComposer;
|
|
83
|
+
private closeComposer;
|
|
84
|
+
private schedulePins;
|
|
85
|
+
private renderPins;
|
|
86
|
+
private togglePanel;
|
|
87
|
+
private renderPanel;
|
|
88
|
+
private effectiveMode;
|
|
89
|
+
private ensureAuthor;
|
|
90
|
+
private submit;
|
|
91
|
+
/** Clear the submitted notes and redraw: pins, count and the panel. */
|
|
92
|
+
private resetAfterSubmit;
|
|
93
|
+
private toast;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Initialize (idempotent). Returns the widget instance. */
|
|
97
|
+
declare function init(config: PeanutGalleryConfig): PeanutGalleryWidget;
|
|
98
|
+
declare function destroy(): void;
|
|
99
|
+
|
|
100
|
+
export { type PeanutGalleryConfig, PeanutGalleryWidget, destroy, init };
|