@pixelatlarge/peanut-gallery-core 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +8 -2
- package/dist/index.cjs +353 -0
- package/dist/index.d.cts +233 -0
- package/dist/index.d.ts +233 -0
- package/dist/index.js +310 -0
- package/package.json +42 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anna Poirier
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @pixelatlarge/peanut-gallery-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared pieces of [Peanut Gallery](https://github.com/PixelAtLarge/peanut-gallery): types, DOM element anchoring (selector, text fingerprint and position fallbacks), the GitHub issue format for review sessions, and the theme. You usually get this as a dependency of `@pixelatlarge/peanut-gallery-widget`, `@pixelatlarge/peanut-gallery-relay` or the `peanut-gallery` CLI.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { createAnchor, resolveAnchor, sessionToIssue, issueToSession } from '@pixelatlarge/peanut-gallery-core';
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
MIT licensed.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DEFAULT_FONTS_URL: () => DEFAULT_FONTS_URL,
|
|
24
|
+
DEFAULT_THEME: () => DEFAULT_THEME,
|
|
25
|
+
ISSUE_BLOCK_LANG: () => ISSUE_BLOCK_LANG,
|
|
26
|
+
LABELS: () => LABELS,
|
|
27
|
+
MAX_STEPS: () => MAX_STEPS,
|
|
28
|
+
PALETTE: () => PALETTE,
|
|
29
|
+
buildSelector: () => buildSelector,
|
|
30
|
+
createAnchor: () => createAnchor,
|
|
31
|
+
issueToSession: () => issueToSession,
|
|
32
|
+
newIssueUrl: () => newIssueUrl,
|
|
33
|
+
normalizeText: () => normalizeText,
|
|
34
|
+
resolveAnchor: () => resolveAnchor,
|
|
35
|
+
resolveTheme: () => resolveTheme,
|
|
36
|
+
sessionToIssue: () => sessionToIssue,
|
|
37
|
+
stepLabel: () => stepLabel,
|
|
38
|
+
themeToCssVars: () => themeToCssVars,
|
|
39
|
+
uid: () => uid
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(index_exports);
|
|
42
|
+
|
|
43
|
+
// src/types.ts
|
|
44
|
+
var MAX_STEPS = 5;
|
|
45
|
+
function stepLabel(step) {
|
|
46
|
+
const t = (step.textFingerprint ?? "").trim();
|
|
47
|
+
if (t) return `\u201C${t.length > 40 ? t.slice(0, 39) + "\u2026" : t}\u201D`;
|
|
48
|
+
return step.selector.length > 60 ? step.tag : step.selector;
|
|
49
|
+
}
|
|
50
|
+
var LABELS = {
|
|
51
|
+
root: "peanut-gallery",
|
|
52
|
+
feedback: "peanut-gallery:feedback",
|
|
53
|
+
proposal: "peanut-gallery:proposal",
|
|
54
|
+
accepted: "peanut-gallery:accepted",
|
|
55
|
+
rejected: "peanut-gallery:rejected"
|
|
56
|
+
};
|
|
57
|
+
var ISSUE_BLOCK_LANG = "json peanut-gallery";
|
|
58
|
+
|
|
59
|
+
// src/anchor.ts
|
|
60
|
+
var MAX_FINGERPRINT = 80;
|
|
61
|
+
function normalizeText(s) {
|
|
62
|
+
return (s ?? "").replace(/\s+/g, " ").trim().slice(0, MAX_FINGERPRINT);
|
|
63
|
+
}
|
|
64
|
+
function cssEscape(s) {
|
|
65
|
+
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(s);
|
|
66
|
+
return s.replace(/([^a-zA-Z0-9_-])/g, "\\$1");
|
|
67
|
+
}
|
|
68
|
+
var ATTR_PRIORITY = ["data-testid", "data-test", "data-cy", "data-proto-id", "id", "name", "aria-label"];
|
|
69
|
+
function isGeneratedToken(v) {
|
|
70
|
+
return /[0-9a-f]{6,}/i.test(v) || /^(css|sc|emotion|svelte|jsx)-/.test(v) || /^_/.test(v) || v.length > 40;
|
|
71
|
+
}
|
|
72
|
+
function segmentFor(el) {
|
|
73
|
+
for (const attr of ATTR_PRIORITY) {
|
|
74
|
+
const v = el.getAttribute(attr);
|
|
75
|
+
if (v && !isGeneratedToken(v)) {
|
|
76
|
+
const seg2 = attr === "id" ? `#${cssEscape(v)}` : `${el.tagName.toLowerCase()}[${attr}="${v.replace(/"/g, '\\"')}"]`;
|
|
77
|
+
const doc = el.ownerDocument;
|
|
78
|
+
try {
|
|
79
|
+
if (doc.querySelectorAll(seg2).length === 1) return { seg: seg2, unique: true };
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const tag = el.tagName.toLowerCase();
|
|
85
|
+
const stableClasses = Array.from(el.classList).filter((c) => !isGeneratedToken(c)).slice(0, 2);
|
|
86
|
+
let seg = tag + stableClasses.map((c) => `.${cssEscape(c)}`).join("");
|
|
87
|
+
const parent = el.parentElement;
|
|
88
|
+
if (parent) {
|
|
89
|
+
const siblings = Array.from(parent.children).filter((c) => c.tagName === el.tagName);
|
|
90
|
+
if (siblings.length > 1) seg += `:nth-of-type(${siblings.indexOf(el) + 1})`;
|
|
91
|
+
}
|
|
92
|
+
return { seg, unique: false };
|
|
93
|
+
}
|
|
94
|
+
function buildSelector(el) {
|
|
95
|
+
const parts = [];
|
|
96
|
+
let cur = el;
|
|
97
|
+
while (cur && cur !== cur.ownerDocument.documentElement) {
|
|
98
|
+
const { seg, unique } = segmentFor(cur);
|
|
99
|
+
parts.unshift(seg);
|
|
100
|
+
if (unique) break;
|
|
101
|
+
cur = cur.parentElement;
|
|
102
|
+
if (parts.length > 8) break;
|
|
103
|
+
}
|
|
104
|
+
return parts.join(" > ");
|
|
105
|
+
}
|
|
106
|
+
function nearestContext(el) {
|
|
107
|
+
let cur = el;
|
|
108
|
+
while (cur) {
|
|
109
|
+
const heading = cur.querySelector('h1, h2, h3, legend, [role="heading"]');
|
|
110
|
+
if (heading && heading !== el && cur !== el) return normalizeText(heading.textContent);
|
|
111
|
+
const label = cur.getAttribute?.("aria-label") ?? cur.getAttribute?.("aria-labelledby");
|
|
112
|
+
if (label && cur !== el) return normalizeText(label);
|
|
113
|
+
cur = cur.parentElement;
|
|
114
|
+
}
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
function createAnchor(el, clientX, clientY) {
|
|
118
|
+
const r = el.getBoundingClientRect();
|
|
119
|
+
const win = el.ownerDocument.defaultView;
|
|
120
|
+
return {
|
|
121
|
+
selector: buildSelector(el),
|
|
122
|
+
textFingerprint: normalizeText(el.textContent),
|
|
123
|
+
tag: el.tagName.toLowerCase(),
|
|
124
|
+
rect: { x: r.left + win.scrollX, y: r.top + win.scrollY, width: r.width, height: r.height },
|
|
125
|
+
offset: {
|
|
126
|
+
x: r.width ? Math.min(1, Math.max(0, (clientX - r.left) / r.width)) : 0.5,
|
|
127
|
+
y: r.height ? Math.min(1, Math.max(0, (clientY - r.top) / r.height)) : 0.5
|
|
128
|
+
},
|
|
129
|
+
context: nearestContext(el)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function textScore(el, fp) {
|
|
133
|
+
if (!fp) return 0.5;
|
|
134
|
+
const t = normalizeText(el.textContent);
|
|
135
|
+
if (t === fp) return 1;
|
|
136
|
+
if (t.includes(fp) || fp.includes(t)) return 0.75;
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
function resolveAnchor(anchor, doc) {
|
|
140
|
+
let candidates = [];
|
|
141
|
+
try {
|
|
142
|
+
candidates = Array.from(doc.querySelectorAll(anchor.selector));
|
|
143
|
+
} catch {
|
|
144
|
+
candidates = [];
|
|
145
|
+
}
|
|
146
|
+
if (candidates.length === 1) {
|
|
147
|
+
const s = textScore(candidates[0], anchor.textFingerprint);
|
|
148
|
+
if (s > 0) return { el: candidates[0], confidence: 0.95 };
|
|
149
|
+
}
|
|
150
|
+
if (candidates.length > 1) {
|
|
151
|
+
const ranked = candidates.map((el) => ({ el, s: textScore(el, anchor.textFingerprint) })).sort((a, b) => b.s - a.s);
|
|
152
|
+
if (ranked[0].s > 0) return { el: ranked[0].el, confidence: 0.8 };
|
|
153
|
+
}
|
|
154
|
+
if (anchor.textFingerprint) {
|
|
155
|
+
const sameTag = Array.from(doc.getElementsByTagName(anchor.tag));
|
|
156
|
+
const ranked = sameTag.map((el) => ({ el, s: textScore(el, anchor.textFingerprint) })).sort((a, b) => b.s - a.s);
|
|
157
|
+
if (ranked[0]?.s >= 0.75) return { el: ranked[0].el, confidence: 0.6 };
|
|
158
|
+
}
|
|
159
|
+
if (candidates.length === 1) return { el: candidates[0], confidence: 0.5 };
|
|
160
|
+
const win = doc.defaultView;
|
|
161
|
+
if (win && typeof doc.elementFromPoint === "function") {
|
|
162
|
+
const cx = anchor.rect.x + anchor.rect.width / 2 - win.scrollX;
|
|
163
|
+
const cy = anchor.rect.y + anchor.rect.height / 2 - win.scrollY;
|
|
164
|
+
const el = doc.elementFromPoint(cx, cy);
|
|
165
|
+
if (el) return { el, confidence: 0.3 };
|
|
166
|
+
}
|
|
167
|
+
return { el: null, confidence: 0 };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/issue-format.ts
|
|
171
|
+
function sessionToIssue(session) {
|
|
172
|
+
const n = session.annotations.length;
|
|
173
|
+
const page = safePath(session.annotations[0]?.pageUrl ?? session.prototypeUrl);
|
|
174
|
+
const title = `Prototype feedback: ${n} comment${n === 1 ? "" : "s"} on ${page}`;
|
|
175
|
+
const lines = [];
|
|
176
|
+
lines.push(`**Prototype:** ${session.prototypeUrl}`);
|
|
177
|
+
if (session.author) lines.push(`**Reviewer:** ${session.author}`);
|
|
178
|
+
lines.push(`**Session:** \`${session.id}\` \xB7 ${session.createdAt}`);
|
|
179
|
+
lines.push("");
|
|
180
|
+
session.annotations.forEach((a, i) => {
|
|
181
|
+
lines.push(`### ${i + 1}. ${a.kind ? `[${a.kind}] ` : ""}${firstLine(a.text)}`);
|
|
182
|
+
if (a.text.includes("\n")) lines.push(a.text.split("\n").slice(1).join("\n"));
|
|
183
|
+
lines.push("");
|
|
184
|
+
lines.push(`- **Page:** ${a.pageUrl}`);
|
|
185
|
+
lines.push(`- **Element:** \`${a.anchor.tag ?? "?"}\` \u2192 \`${a.anchor.selector}\``);
|
|
186
|
+
if (a.anchor.textFingerprint) lines.push(`- **Text:** \u201C${a.anchor.textFingerprint}\u201D`);
|
|
187
|
+
if (a.anchor.context) lines.push(`- **Near:** ${a.anchor.context}`);
|
|
188
|
+
if (a.steps?.length) lines.push(`- **Reached by clicking:** ${a.steps.map(stepLabel).join(" \u2192 ")}`);
|
|
189
|
+
if (a.viewport && Number.isFinite(a.viewport.width) && Number.isFinite(a.viewport.height)) lines.push(`- **Viewport:** ${a.viewport.width}\xD7${a.viewport.height}`);
|
|
190
|
+
lines.push("");
|
|
191
|
+
});
|
|
192
|
+
lines.push("<details><summary>peanut-gallery data (do not edit)</summary>");
|
|
193
|
+
lines.push("");
|
|
194
|
+
lines.push("```" + ISSUE_BLOCK_LANG);
|
|
195
|
+
lines.push(JSON.stringify(session, null, 2));
|
|
196
|
+
lines.push("```");
|
|
197
|
+
lines.push("");
|
|
198
|
+
lines.push("</details>");
|
|
199
|
+
return { title, body: lines.join("\n") };
|
|
200
|
+
}
|
|
201
|
+
function issueToSession(body) {
|
|
202
|
+
const re = new RegExp("```" + ISSUE_BLOCK_LANG.replace(/ /g, "\\s+") + "\\s*\\n([\\s\\S]*?)\\n```");
|
|
203
|
+
const m = body.match(re);
|
|
204
|
+
if (!m) return null;
|
|
205
|
+
try {
|
|
206
|
+
const parsed = JSON.parse(m[1]);
|
|
207
|
+
if (!Array.isArray(parsed.annotations)) return null;
|
|
208
|
+
return parsed;
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function newIssueUrl(repo, session, labels = [], maxLen = 7500) {
|
|
214
|
+
const base = `https://github.com/${repo}/issues/new`;
|
|
215
|
+
const build = (s) => {
|
|
216
|
+
const { title, body } = sessionToIssue(s);
|
|
217
|
+
const params = new URLSearchParams({ title, body });
|
|
218
|
+
if (labels.length) params.set("labels", labels.join(","));
|
|
219
|
+
return `${base}?${params.toString()}`;
|
|
220
|
+
};
|
|
221
|
+
let url = build(session);
|
|
222
|
+
if (url.length <= maxLen) return url;
|
|
223
|
+
const slim = {
|
|
224
|
+
...session,
|
|
225
|
+
userAgent: void 0,
|
|
226
|
+
annotations: session.annotations.map((a) => ({
|
|
227
|
+
...a,
|
|
228
|
+
anchor: {
|
|
229
|
+
...a.anchor,
|
|
230
|
+
context: void 0,
|
|
231
|
+
textFingerprint: a.anchor.textFingerprint.slice(0, 40),
|
|
232
|
+
rect: roundRect(a.anchor.rect)
|
|
233
|
+
},
|
|
234
|
+
steps: a.steps?.map((st) => ({ ...st, context: void 0, textFingerprint: st.textFingerprint.slice(0, 40), rect: roundRect(st.rect) }))
|
|
235
|
+
}))
|
|
236
|
+
};
|
|
237
|
+
url = build(slim);
|
|
238
|
+
if (url.length <= maxLen) return url;
|
|
239
|
+
const kept = [];
|
|
240
|
+
for (const a of slim.annotations) {
|
|
241
|
+
const trial = build({ ...slim, annotations: [...kept, a] });
|
|
242
|
+
if (trial.length > maxLen) break;
|
|
243
|
+
kept.push(a);
|
|
244
|
+
}
|
|
245
|
+
return build({ ...slim, annotations: kept });
|
|
246
|
+
}
|
|
247
|
+
function roundRect(r) {
|
|
248
|
+
return { x: Math.round(r.x), y: Math.round(r.y), width: Math.round(r.width), height: Math.round(r.height) };
|
|
249
|
+
}
|
|
250
|
+
function firstLine(s) {
|
|
251
|
+
return s.split("\n")[0].trim().slice(0, 100) || "(no text)";
|
|
252
|
+
}
|
|
253
|
+
function safePath(url) {
|
|
254
|
+
try {
|
|
255
|
+
const u = new URL(url);
|
|
256
|
+
return u.pathname === "/" ? u.host : u.pathname;
|
|
257
|
+
} catch {
|
|
258
|
+
return url;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function uid(prefix = "") {
|
|
262
|
+
const rand = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID().slice(0, 8) : Math.random().toString(36).slice(2, 10);
|
|
263
|
+
return prefix ? `${prefix}_${rand}` : rand;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/theme.ts
|
|
267
|
+
var PALETTE = {
|
|
268
|
+
conifer: "#1E4D36",
|
|
269
|
+
emerald: "#0F7B52",
|
|
270
|
+
wasabi: "#719872",
|
|
271
|
+
matcha: "#9CC98A",
|
|
272
|
+
pistachio: "#CFE1B4",
|
|
273
|
+
seaGlass: "#CDE2F2",
|
|
274
|
+
hydrangea: "#A6BFE5",
|
|
275
|
+
harbor: "#40648A",
|
|
276
|
+
chambray: "#6C8EBD",
|
|
277
|
+
prep: "#152A4A",
|
|
278
|
+
sunnyside: "#EAC26C",
|
|
279
|
+
linen: "#F6F3E4",
|
|
280
|
+
white: "#FFFFFF",
|
|
281
|
+
gull: "#A5B0B6",
|
|
282
|
+
carbon: "#22272B"
|
|
283
|
+
};
|
|
284
|
+
var DEFAULT_THEME = {
|
|
285
|
+
bg: PALETTE.linen,
|
|
286
|
+
surface: PALETTE.white,
|
|
287
|
+
text: PALETTE.carbon,
|
|
288
|
+
muted: "#5A6268",
|
|
289
|
+
line: "#DEDCCB",
|
|
290
|
+
heading: PALETTE.conifer,
|
|
291
|
+
primary: PALETTE.emerald,
|
|
292
|
+
primaryHover: "#006340",
|
|
293
|
+
// emerald 700
|
|
294
|
+
onPrimary: PALETTE.white,
|
|
295
|
+
toolbar: PALETTE.conifer,
|
|
296
|
+
toolbarButton: "#325D46",
|
|
297
|
+
// conifer 700
|
|
298
|
+
toolbarButtonHover: "#4E7660",
|
|
299
|
+
// conifer 600
|
|
300
|
+
onToolbar: PALETTE.white,
|
|
301
|
+
accent: PALETTE.sunnyside,
|
|
302
|
+
onAccent: PALETTE.carbon,
|
|
303
|
+
highlight: PALETTE.emerald,
|
|
304
|
+
highlightTint: "rgba(15, 123, 82, 0.10)",
|
|
305
|
+
pinLow: PALETTE.gull,
|
|
306
|
+
chip: PALETTE.seaGlass,
|
|
307
|
+
onChip: PALETTE.prep,
|
|
308
|
+
count: PALETTE.pistachio,
|
|
309
|
+
onCount: PALETTE.conifer,
|
|
310
|
+
ghost: "#EDEBE3",
|
|
311
|
+
// linen 100
|
|
312
|
+
ghostHover: "#DBD8CA",
|
|
313
|
+
// linen 200
|
|
314
|
+
link: PALETTE.prep,
|
|
315
|
+
toast: PALETTE.prep,
|
|
316
|
+
onToast: PALETTE.linen,
|
|
317
|
+
danger: "#F6E3DF",
|
|
318
|
+
onDanger: "#9B3426",
|
|
319
|
+
success: PALETTE.emerald,
|
|
320
|
+
successTint: "#D4F4E2",
|
|
321
|
+
// emerald 100
|
|
322
|
+
fontBody: '"Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
|
|
323
|
+
fontDisplay: '"Young Serif", Georgia, "Times New Roman", serif',
|
|
324
|
+
fontMono: '"JetBrains Mono", ui-monospace, Menlo, Consolas, monospace'
|
|
325
|
+
};
|
|
326
|
+
var DEFAULT_FONTS_URL = "https://fonts.googleapis.com/css2?family=Young+Serif&family=Manrope:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap";
|
|
327
|
+
function resolveTheme(overrides) {
|
|
328
|
+
return { ...DEFAULT_THEME, ...overrides ?? {} };
|
|
329
|
+
}
|
|
330
|
+
var kebab = (s) => s.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
|
331
|
+
function themeToCssVars(theme) {
|
|
332
|
+
return Object.keys(theme).map((k) => `--pg-${kebab(k)}: ${theme[k]};`).join(" ");
|
|
333
|
+
}
|
|
334
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
335
|
+
0 && (module.exports = {
|
|
336
|
+
DEFAULT_FONTS_URL,
|
|
337
|
+
DEFAULT_THEME,
|
|
338
|
+
ISSUE_BLOCK_LANG,
|
|
339
|
+
LABELS,
|
|
340
|
+
MAX_STEPS,
|
|
341
|
+
PALETTE,
|
|
342
|
+
buildSelector,
|
|
343
|
+
createAnchor,
|
|
344
|
+
issueToSession,
|
|
345
|
+
newIssueUrl,
|
|
346
|
+
normalizeText,
|
|
347
|
+
resolveAnchor,
|
|
348
|
+
resolveTheme,
|
|
349
|
+
sessionToIssue,
|
|
350
|
+
stepLabel,
|
|
351
|
+
themeToCssVars,
|
|
352
|
+
uid
|
|
353
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An anchor ties a comment to a DOM element robustly enough to survive
|
|
3
|
+
* minor markup changes. We store several independent signals and resolve
|
|
4
|
+
* using the first that matches confidently.
|
|
5
|
+
*/
|
|
6
|
+
interface ElementAnchor {
|
|
7
|
+
/** Stable-ish CSS selector (prefers data-testid / id / semantic path). */
|
|
8
|
+
selector: string;
|
|
9
|
+
/** Normalized, truncated textContent of the element at annotation time. */
|
|
10
|
+
textFingerprint: string;
|
|
11
|
+
/** Tag name, lowercase. */
|
|
12
|
+
tag: string;
|
|
13
|
+
/** Bounding rect relative to the document at annotation time (CSS px). */
|
|
14
|
+
rect: {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
};
|
|
20
|
+
/** Position of the click inside the element, 0..1 on each axis. */
|
|
21
|
+
offset: {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
};
|
|
25
|
+
/** Nearest landmark/heading text to help humans + AI orient. */
|
|
26
|
+
context?: string;
|
|
27
|
+
}
|
|
28
|
+
interface Annotation {
|
|
29
|
+
id: string;
|
|
30
|
+
text: string;
|
|
31
|
+
anchor: ElementAnchor;
|
|
32
|
+
/** Page URL (origin + path + search) where the comment was left. */
|
|
33
|
+
pageUrl: string;
|
|
34
|
+
viewport: {
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
};
|
|
38
|
+
createdAt: string;
|
|
39
|
+
author?: string;
|
|
40
|
+
/** Optional free-form tag chosen by the reviewer, e.g. "bug", "copy", "layout". */
|
|
41
|
+
kind?: AnnotationKind;
|
|
42
|
+
/**
|
|
43
|
+
* Clicks the reviewer made on this page before leaving the note, oldest first (at most 5,
|
|
44
|
+
* reset on navigation). Screenshots replay them when the element isn't on the freshly
|
|
45
|
+
* loaded page, e.g. a note inside a modal. Element identities only, never typed values.
|
|
46
|
+
*/
|
|
47
|
+
steps?: ElementAnchor[];
|
|
48
|
+
}
|
|
49
|
+
/** Most clicks kept per note. */
|
|
50
|
+
declare const MAX_STEPS = 5;
|
|
51
|
+
/** Short human label for a recorded click, e.g. `“Remove”` or `button#save`. */
|
|
52
|
+
declare function stepLabel(step: Pick<ElementAnchor, 'tag' | 'textFingerprint' | 'selector'>): string;
|
|
53
|
+
type AnnotationKind = 'bug' | 'copy' | 'layout' | 'visual' | 'flow' | 'question' | 'praise';
|
|
54
|
+
interface ReviewSession {
|
|
55
|
+
id: string;
|
|
56
|
+
/** Base URL of the hosted prototype. */
|
|
57
|
+
prototypeUrl: string;
|
|
58
|
+
annotations: Annotation[];
|
|
59
|
+
createdAt: string;
|
|
60
|
+
author?: string;
|
|
61
|
+
userAgent?: string;
|
|
62
|
+
}
|
|
63
|
+
/** A change proposal derived (by Claude) from one or more annotations. */
|
|
64
|
+
interface Proposal {
|
|
65
|
+
id: string;
|
|
66
|
+
title: string;
|
|
67
|
+
/** One-paragraph rationale written for humans. */
|
|
68
|
+
summary: string;
|
|
69
|
+
/** Concrete, implementable instructions for the coding agent. */
|
|
70
|
+
instructions: string;
|
|
71
|
+
/** Annotation ids this proposal addresses. */
|
|
72
|
+
annotationIds: string[];
|
|
73
|
+
/** Issue numbers the annotations came from. */
|
|
74
|
+
issueNumbers: number[];
|
|
75
|
+
/** Pages (URLs) affected; used to decide what to screenshot. */
|
|
76
|
+
pages: string[];
|
|
77
|
+
/** Anchors to screenshot close-up, before and after. */
|
|
78
|
+
focusAnchors: ElementAnchor[];
|
|
79
|
+
effort: 'small' | 'medium' | 'large';
|
|
80
|
+
confidence: number;
|
|
81
|
+
/** Who asked for this (distinct reviewers) — computed from the annotations, not by the model. */
|
|
82
|
+
support: {
|
|
83
|
+
reviewers: string[];
|
|
84
|
+
count: number;
|
|
85
|
+
annotationCount: number;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Present when reviewers disagree about the same element/change. `apply` skips contested
|
|
89
|
+
* proposals until one position is chosen (`peanut-gallery decide <id> <n>`), or applies the
|
|
90
|
+
* model's recommendation with --include-contested.
|
|
91
|
+
*/
|
|
92
|
+
contested?: {
|
|
93
|
+
/** Why this is a conflict, one sentence. */
|
|
94
|
+
reason: string;
|
|
95
|
+
positions: ProposalPosition[];
|
|
96
|
+
/** Index into positions the model recommends; `instructions` on the proposal already reflect it. */
|
|
97
|
+
recommended: number;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
interface ProposalPosition {
|
|
101
|
+
summary: string;
|
|
102
|
+
instructions: string;
|
|
103
|
+
reviewers: string[];
|
|
104
|
+
annotationIds: string[];
|
|
105
|
+
}
|
|
106
|
+
interface ProposalRun {
|
|
107
|
+
proposalId: string;
|
|
108
|
+
branch: string;
|
|
109
|
+
prUrl?: string;
|
|
110
|
+
prNumber?: number;
|
|
111
|
+
comparePath?: string;
|
|
112
|
+
screenshots: {
|
|
113
|
+
label: string;
|
|
114
|
+
before: string;
|
|
115
|
+
after: string;
|
|
116
|
+
}[];
|
|
117
|
+
status: 'proposed' | 'decision-needed' | 'applied' | 'pr-opened' | 'accepted' | 'rejected';
|
|
118
|
+
}
|
|
119
|
+
declare const LABELS: {
|
|
120
|
+
readonly root: "peanut-gallery";
|
|
121
|
+
readonly feedback: "peanut-gallery:feedback";
|
|
122
|
+
readonly proposal: "peanut-gallery:proposal";
|
|
123
|
+
readonly accepted: "peanut-gallery:accepted";
|
|
124
|
+
readonly rejected: "peanut-gallery:rejected";
|
|
125
|
+
};
|
|
126
|
+
declare const ISSUE_BLOCK_LANG = "json peanut-gallery";
|
|
127
|
+
|
|
128
|
+
declare function normalizeText(s: string | null | undefined): string;
|
|
129
|
+
/** Build a CSS selector that prefers stable, human-meaningful hooks. */
|
|
130
|
+
declare function buildSelector(el: Element): string;
|
|
131
|
+
declare function createAnchor(el: Element, clientX: number, clientY: number): ElementAnchor;
|
|
132
|
+
/**
|
|
133
|
+
* Resolve an anchor to an element in `doc`. Strategy:
|
|
134
|
+
* 1. selector matches exactly one element -> use it (verify text loosely)
|
|
135
|
+
* 2. selector matches several -> pick best text match
|
|
136
|
+
* 3. selector fails -> search same tag by text fingerprint
|
|
137
|
+
* 4. fall back to element at the stored rect center
|
|
138
|
+
*/
|
|
139
|
+
declare function resolveAnchor(anchor: ElementAnchor, doc: Document): {
|
|
140
|
+
el: Element | null;
|
|
141
|
+
confidence: number;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** Human-readable + machine-parseable GitHub issue body for a review session. */
|
|
145
|
+
declare function sessionToIssue(session: ReviewSession): {
|
|
146
|
+
title: string;
|
|
147
|
+
body: string;
|
|
148
|
+
};
|
|
149
|
+
/** Parse a session back out of an issue body. Returns null if no block found. */
|
|
150
|
+
declare function issueToSession(body: string): ReviewSession | null;
|
|
151
|
+
/**
|
|
152
|
+
* Build a `/issues/new` URL. GitHub caps URLs around 8k chars; if the body
|
|
153
|
+
* is too long we trim the machine block to the essentials and note it.
|
|
154
|
+
*/
|
|
155
|
+
declare function newIssueUrl(repo: string, session: ReviewSession, labels?: string[], maxLen?: number): string;
|
|
156
|
+
declare function uid(prefix?: string): string;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Default look: the "Conifer, Prep & Wasabi" palette.
|
|
160
|
+
* Deep forest and navy grounds, creamy greens, coastal blues, one warm yellow accent, carbon text.
|
|
161
|
+
*
|
|
162
|
+
* Every role can be overridden (widget: `theme` option; CLI: `theme` in peanut-gallery.config.json),
|
|
163
|
+
* so teams can match their own design system.
|
|
164
|
+
*/
|
|
165
|
+
declare const PALETTE: {
|
|
166
|
+
readonly conifer: "#1E4D36";
|
|
167
|
+
readonly emerald: "#0F7B52";
|
|
168
|
+
readonly wasabi: "#719872";
|
|
169
|
+
readonly matcha: "#9CC98A";
|
|
170
|
+
readonly pistachio: "#CFE1B4";
|
|
171
|
+
readonly seaGlass: "#CDE2F2";
|
|
172
|
+
readonly hydrangea: "#A6BFE5";
|
|
173
|
+
readonly harbor: "#40648A";
|
|
174
|
+
readonly chambray: "#6C8EBD";
|
|
175
|
+
readonly prep: "#152A4A";
|
|
176
|
+
readonly sunnyside: "#EAC26C";
|
|
177
|
+
readonly linen: "#F6F3E4";
|
|
178
|
+
readonly white: "#FFFFFF";
|
|
179
|
+
readonly gull: "#A5B0B6";
|
|
180
|
+
readonly carbon: "#22272B";
|
|
181
|
+
};
|
|
182
|
+
interface Theme {
|
|
183
|
+
/** Page / report background */
|
|
184
|
+
bg: string;
|
|
185
|
+
/** Cards, popovers */
|
|
186
|
+
surface: string;
|
|
187
|
+
text: string;
|
|
188
|
+
muted: string;
|
|
189
|
+
line: string;
|
|
190
|
+
heading: string;
|
|
191
|
+
/** Primary actions (Add note, Submit) */
|
|
192
|
+
primary: string;
|
|
193
|
+
primaryHover: string;
|
|
194
|
+
onPrimary: string;
|
|
195
|
+
/** Floating toolbar */
|
|
196
|
+
toolbar: string;
|
|
197
|
+
toolbarButton: string;
|
|
198
|
+
toolbarButtonHover: string;
|
|
199
|
+
onToolbar: string;
|
|
200
|
+
/** "Commenting" state, pins, element outlines in screenshots */
|
|
201
|
+
accent: string;
|
|
202
|
+
onAccent: string;
|
|
203
|
+
/** Hover outline on the element being picked */
|
|
204
|
+
highlight: string;
|
|
205
|
+
highlightTint: string;
|
|
206
|
+
pinLow: string;
|
|
207
|
+
/** Kind tags (bug, copy, layout…) */
|
|
208
|
+
chip: string;
|
|
209
|
+
onChip: string;
|
|
210
|
+
/** Count badge on the Review button */
|
|
211
|
+
count: string;
|
|
212
|
+
onCount: string;
|
|
213
|
+
ghost: string;
|
|
214
|
+
ghostHover: string;
|
|
215
|
+
link: string;
|
|
216
|
+
toast: string;
|
|
217
|
+
onToast: string;
|
|
218
|
+
danger: string;
|
|
219
|
+
onDanger: string;
|
|
220
|
+
success: string;
|
|
221
|
+
successTint: string;
|
|
222
|
+
fontBody: string;
|
|
223
|
+
fontDisplay: string;
|
|
224
|
+
fontMono: string;
|
|
225
|
+
}
|
|
226
|
+
declare const DEFAULT_THEME: Theme;
|
|
227
|
+
/** Google Fonts stylesheet for the default type (Manrope, Young Serif, JetBrains Mono). */
|
|
228
|
+
declare const DEFAULT_FONTS_URL = "https://fonts.googleapis.com/css2?family=Young+Serif&family=Manrope:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap";
|
|
229
|
+
declare function resolveTheme(overrides?: Partial<Theme>): Theme;
|
|
230
|
+
/** `--pg-primary: #0F7B52; …` for every role. */
|
|
231
|
+
declare function themeToCssVars(theme: Theme): string;
|
|
232
|
+
|
|
233
|
+
export { type Annotation, type AnnotationKind, DEFAULT_FONTS_URL, DEFAULT_THEME, type ElementAnchor, ISSUE_BLOCK_LANG, LABELS, MAX_STEPS, PALETTE, type Proposal, type ProposalPosition, type ProposalRun, type ReviewSession, type Theme, buildSelector, createAnchor, issueToSession, newIssueUrl, normalizeText, resolveAnchor, resolveTheme, sessionToIssue, stepLabel, themeToCssVars, uid };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An anchor ties a comment to a DOM element robustly enough to survive
|
|
3
|
+
* minor markup changes. We store several independent signals and resolve
|
|
4
|
+
* using the first that matches confidently.
|
|
5
|
+
*/
|
|
6
|
+
interface ElementAnchor {
|
|
7
|
+
/** Stable-ish CSS selector (prefers data-testid / id / semantic path). */
|
|
8
|
+
selector: string;
|
|
9
|
+
/** Normalized, truncated textContent of the element at annotation time. */
|
|
10
|
+
textFingerprint: string;
|
|
11
|
+
/** Tag name, lowercase. */
|
|
12
|
+
tag: string;
|
|
13
|
+
/** Bounding rect relative to the document at annotation time (CSS px). */
|
|
14
|
+
rect: {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
};
|
|
20
|
+
/** Position of the click inside the element, 0..1 on each axis. */
|
|
21
|
+
offset: {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
};
|
|
25
|
+
/** Nearest landmark/heading text to help humans + AI orient. */
|
|
26
|
+
context?: string;
|
|
27
|
+
}
|
|
28
|
+
interface Annotation {
|
|
29
|
+
id: string;
|
|
30
|
+
text: string;
|
|
31
|
+
anchor: ElementAnchor;
|
|
32
|
+
/** Page URL (origin + path + search) where the comment was left. */
|
|
33
|
+
pageUrl: string;
|
|
34
|
+
viewport: {
|
|
35
|
+
width: number;
|
|
36
|
+
height: number;
|
|
37
|
+
};
|
|
38
|
+
createdAt: string;
|
|
39
|
+
author?: string;
|
|
40
|
+
/** Optional free-form tag chosen by the reviewer, e.g. "bug", "copy", "layout". */
|
|
41
|
+
kind?: AnnotationKind;
|
|
42
|
+
/**
|
|
43
|
+
* Clicks the reviewer made on this page before leaving the note, oldest first (at most 5,
|
|
44
|
+
* reset on navigation). Screenshots replay them when the element isn't on the freshly
|
|
45
|
+
* loaded page, e.g. a note inside a modal. Element identities only, never typed values.
|
|
46
|
+
*/
|
|
47
|
+
steps?: ElementAnchor[];
|
|
48
|
+
}
|
|
49
|
+
/** Most clicks kept per note. */
|
|
50
|
+
declare const MAX_STEPS = 5;
|
|
51
|
+
/** Short human label for a recorded click, e.g. `“Remove”` or `button#save`. */
|
|
52
|
+
declare function stepLabel(step: Pick<ElementAnchor, 'tag' | 'textFingerprint' | 'selector'>): string;
|
|
53
|
+
type AnnotationKind = 'bug' | 'copy' | 'layout' | 'visual' | 'flow' | 'question' | 'praise';
|
|
54
|
+
interface ReviewSession {
|
|
55
|
+
id: string;
|
|
56
|
+
/** Base URL of the hosted prototype. */
|
|
57
|
+
prototypeUrl: string;
|
|
58
|
+
annotations: Annotation[];
|
|
59
|
+
createdAt: string;
|
|
60
|
+
author?: string;
|
|
61
|
+
userAgent?: string;
|
|
62
|
+
}
|
|
63
|
+
/** A change proposal derived (by Claude) from one or more annotations. */
|
|
64
|
+
interface Proposal {
|
|
65
|
+
id: string;
|
|
66
|
+
title: string;
|
|
67
|
+
/** One-paragraph rationale written for humans. */
|
|
68
|
+
summary: string;
|
|
69
|
+
/** Concrete, implementable instructions for the coding agent. */
|
|
70
|
+
instructions: string;
|
|
71
|
+
/** Annotation ids this proposal addresses. */
|
|
72
|
+
annotationIds: string[];
|
|
73
|
+
/** Issue numbers the annotations came from. */
|
|
74
|
+
issueNumbers: number[];
|
|
75
|
+
/** Pages (URLs) affected; used to decide what to screenshot. */
|
|
76
|
+
pages: string[];
|
|
77
|
+
/** Anchors to screenshot close-up, before and after. */
|
|
78
|
+
focusAnchors: ElementAnchor[];
|
|
79
|
+
effort: 'small' | 'medium' | 'large';
|
|
80
|
+
confidence: number;
|
|
81
|
+
/** Who asked for this (distinct reviewers) — computed from the annotations, not by the model. */
|
|
82
|
+
support: {
|
|
83
|
+
reviewers: string[];
|
|
84
|
+
count: number;
|
|
85
|
+
annotationCount: number;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Present when reviewers disagree about the same element/change. `apply` skips contested
|
|
89
|
+
* proposals until one position is chosen (`peanut-gallery decide <id> <n>`), or applies the
|
|
90
|
+
* model's recommendation with --include-contested.
|
|
91
|
+
*/
|
|
92
|
+
contested?: {
|
|
93
|
+
/** Why this is a conflict, one sentence. */
|
|
94
|
+
reason: string;
|
|
95
|
+
positions: ProposalPosition[];
|
|
96
|
+
/** Index into positions the model recommends; `instructions` on the proposal already reflect it. */
|
|
97
|
+
recommended: number;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
interface ProposalPosition {
|
|
101
|
+
summary: string;
|
|
102
|
+
instructions: string;
|
|
103
|
+
reviewers: string[];
|
|
104
|
+
annotationIds: string[];
|
|
105
|
+
}
|
|
106
|
+
interface ProposalRun {
|
|
107
|
+
proposalId: string;
|
|
108
|
+
branch: string;
|
|
109
|
+
prUrl?: string;
|
|
110
|
+
prNumber?: number;
|
|
111
|
+
comparePath?: string;
|
|
112
|
+
screenshots: {
|
|
113
|
+
label: string;
|
|
114
|
+
before: string;
|
|
115
|
+
after: string;
|
|
116
|
+
}[];
|
|
117
|
+
status: 'proposed' | 'decision-needed' | 'applied' | 'pr-opened' | 'accepted' | 'rejected';
|
|
118
|
+
}
|
|
119
|
+
declare const LABELS: {
|
|
120
|
+
readonly root: "peanut-gallery";
|
|
121
|
+
readonly feedback: "peanut-gallery:feedback";
|
|
122
|
+
readonly proposal: "peanut-gallery:proposal";
|
|
123
|
+
readonly accepted: "peanut-gallery:accepted";
|
|
124
|
+
readonly rejected: "peanut-gallery:rejected";
|
|
125
|
+
};
|
|
126
|
+
declare const ISSUE_BLOCK_LANG = "json peanut-gallery";
|
|
127
|
+
|
|
128
|
+
declare function normalizeText(s: string | null | undefined): string;
|
|
129
|
+
/** Build a CSS selector that prefers stable, human-meaningful hooks. */
|
|
130
|
+
declare function buildSelector(el: Element): string;
|
|
131
|
+
declare function createAnchor(el: Element, clientX: number, clientY: number): ElementAnchor;
|
|
132
|
+
/**
|
|
133
|
+
* Resolve an anchor to an element in `doc`. Strategy:
|
|
134
|
+
* 1. selector matches exactly one element -> use it (verify text loosely)
|
|
135
|
+
* 2. selector matches several -> pick best text match
|
|
136
|
+
* 3. selector fails -> search same tag by text fingerprint
|
|
137
|
+
* 4. fall back to element at the stored rect center
|
|
138
|
+
*/
|
|
139
|
+
declare function resolveAnchor(anchor: ElementAnchor, doc: Document): {
|
|
140
|
+
el: Element | null;
|
|
141
|
+
confidence: number;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** Human-readable + machine-parseable GitHub issue body for a review session. */
|
|
145
|
+
declare function sessionToIssue(session: ReviewSession): {
|
|
146
|
+
title: string;
|
|
147
|
+
body: string;
|
|
148
|
+
};
|
|
149
|
+
/** Parse a session back out of an issue body. Returns null if no block found. */
|
|
150
|
+
declare function issueToSession(body: string): ReviewSession | null;
|
|
151
|
+
/**
|
|
152
|
+
* Build a `/issues/new` URL. GitHub caps URLs around 8k chars; if the body
|
|
153
|
+
* is too long we trim the machine block to the essentials and note it.
|
|
154
|
+
*/
|
|
155
|
+
declare function newIssueUrl(repo: string, session: ReviewSession, labels?: string[], maxLen?: number): string;
|
|
156
|
+
declare function uid(prefix?: string): string;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Default look: the "Conifer, Prep & Wasabi" palette.
|
|
160
|
+
* Deep forest and navy grounds, creamy greens, coastal blues, one warm yellow accent, carbon text.
|
|
161
|
+
*
|
|
162
|
+
* Every role can be overridden (widget: `theme` option; CLI: `theme` in peanut-gallery.config.json),
|
|
163
|
+
* so teams can match their own design system.
|
|
164
|
+
*/
|
|
165
|
+
declare const PALETTE: {
|
|
166
|
+
readonly conifer: "#1E4D36";
|
|
167
|
+
readonly emerald: "#0F7B52";
|
|
168
|
+
readonly wasabi: "#719872";
|
|
169
|
+
readonly matcha: "#9CC98A";
|
|
170
|
+
readonly pistachio: "#CFE1B4";
|
|
171
|
+
readonly seaGlass: "#CDE2F2";
|
|
172
|
+
readonly hydrangea: "#A6BFE5";
|
|
173
|
+
readonly harbor: "#40648A";
|
|
174
|
+
readonly chambray: "#6C8EBD";
|
|
175
|
+
readonly prep: "#152A4A";
|
|
176
|
+
readonly sunnyside: "#EAC26C";
|
|
177
|
+
readonly linen: "#F6F3E4";
|
|
178
|
+
readonly white: "#FFFFFF";
|
|
179
|
+
readonly gull: "#A5B0B6";
|
|
180
|
+
readonly carbon: "#22272B";
|
|
181
|
+
};
|
|
182
|
+
interface Theme {
|
|
183
|
+
/** Page / report background */
|
|
184
|
+
bg: string;
|
|
185
|
+
/** Cards, popovers */
|
|
186
|
+
surface: string;
|
|
187
|
+
text: string;
|
|
188
|
+
muted: string;
|
|
189
|
+
line: string;
|
|
190
|
+
heading: string;
|
|
191
|
+
/** Primary actions (Add note, Submit) */
|
|
192
|
+
primary: string;
|
|
193
|
+
primaryHover: string;
|
|
194
|
+
onPrimary: string;
|
|
195
|
+
/** Floating toolbar */
|
|
196
|
+
toolbar: string;
|
|
197
|
+
toolbarButton: string;
|
|
198
|
+
toolbarButtonHover: string;
|
|
199
|
+
onToolbar: string;
|
|
200
|
+
/** "Commenting" state, pins, element outlines in screenshots */
|
|
201
|
+
accent: string;
|
|
202
|
+
onAccent: string;
|
|
203
|
+
/** Hover outline on the element being picked */
|
|
204
|
+
highlight: string;
|
|
205
|
+
highlightTint: string;
|
|
206
|
+
pinLow: string;
|
|
207
|
+
/** Kind tags (bug, copy, layout…) */
|
|
208
|
+
chip: string;
|
|
209
|
+
onChip: string;
|
|
210
|
+
/** Count badge on the Review button */
|
|
211
|
+
count: string;
|
|
212
|
+
onCount: string;
|
|
213
|
+
ghost: string;
|
|
214
|
+
ghostHover: string;
|
|
215
|
+
link: string;
|
|
216
|
+
toast: string;
|
|
217
|
+
onToast: string;
|
|
218
|
+
danger: string;
|
|
219
|
+
onDanger: string;
|
|
220
|
+
success: string;
|
|
221
|
+
successTint: string;
|
|
222
|
+
fontBody: string;
|
|
223
|
+
fontDisplay: string;
|
|
224
|
+
fontMono: string;
|
|
225
|
+
}
|
|
226
|
+
declare const DEFAULT_THEME: Theme;
|
|
227
|
+
/** Google Fonts stylesheet for the default type (Manrope, Young Serif, JetBrains Mono). */
|
|
228
|
+
declare const DEFAULT_FONTS_URL = "https://fonts.googleapis.com/css2?family=Young+Serif&family=Manrope:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap";
|
|
229
|
+
declare function resolveTheme(overrides?: Partial<Theme>): Theme;
|
|
230
|
+
/** `--pg-primary: #0F7B52; …` for every role. */
|
|
231
|
+
declare function themeToCssVars(theme: Theme): string;
|
|
232
|
+
|
|
233
|
+
export { type Annotation, type AnnotationKind, DEFAULT_FONTS_URL, DEFAULT_THEME, type ElementAnchor, ISSUE_BLOCK_LANG, LABELS, MAX_STEPS, PALETTE, type Proposal, type ProposalPosition, type ProposalRun, type ReviewSession, type Theme, buildSelector, createAnchor, issueToSession, newIssueUrl, normalizeText, resolveAnchor, resolveTheme, sessionToIssue, stepLabel, themeToCssVars, uid };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var MAX_STEPS = 5;
|
|
3
|
+
function stepLabel(step) {
|
|
4
|
+
const t = (step.textFingerprint ?? "").trim();
|
|
5
|
+
if (t) return `\u201C${t.length > 40 ? t.slice(0, 39) + "\u2026" : t}\u201D`;
|
|
6
|
+
return step.selector.length > 60 ? step.tag : step.selector;
|
|
7
|
+
}
|
|
8
|
+
var LABELS = {
|
|
9
|
+
root: "peanut-gallery",
|
|
10
|
+
feedback: "peanut-gallery:feedback",
|
|
11
|
+
proposal: "peanut-gallery:proposal",
|
|
12
|
+
accepted: "peanut-gallery:accepted",
|
|
13
|
+
rejected: "peanut-gallery:rejected"
|
|
14
|
+
};
|
|
15
|
+
var ISSUE_BLOCK_LANG = "json peanut-gallery";
|
|
16
|
+
|
|
17
|
+
// src/anchor.ts
|
|
18
|
+
var MAX_FINGERPRINT = 80;
|
|
19
|
+
function normalizeText(s) {
|
|
20
|
+
return (s ?? "").replace(/\s+/g, " ").trim().slice(0, MAX_FINGERPRINT);
|
|
21
|
+
}
|
|
22
|
+
function cssEscape(s) {
|
|
23
|
+
if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(s);
|
|
24
|
+
return s.replace(/([^a-zA-Z0-9_-])/g, "\\$1");
|
|
25
|
+
}
|
|
26
|
+
var ATTR_PRIORITY = ["data-testid", "data-test", "data-cy", "data-proto-id", "id", "name", "aria-label"];
|
|
27
|
+
function isGeneratedToken(v) {
|
|
28
|
+
return /[0-9a-f]{6,}/i.test(v) || /^(css|sc|emotion|svelte|jsx)-/.test(v) || /^_/.test(v) || v.length > 40;
|
|
29
|
+
}
|
|
30
|
+
function segmentFor(el) {
|
|
31
|
+
for (const attr of ATTR_PRIORITY) {
|
|
32
|
+
const v = el.getAttribute(attr);
|
|
33
|
+
if (v && !isGeneratedToken(v)) {
|
|
34
|
+
const seg2 = attr === "id" ? `#${cssEscape(v)}` : `${el.tagName.toLowerCase()}[${attr}="${v.replace(/"/g, '\\"')}"]`;
|
|
35
|
+
const doc = el.ownerDocument;
|
|
36
|
+
try {
|
|
37
|
+
if (doc.querySelectorAll(seg2).length === 1) return { seg: seg2, unique: true };
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const tag = el.tagName.toLowerCase();
|
|
43
|
+
const stableClasses = Array.from(el.classList).filter((c) => !isGeneratedToken(c)).slice(0, 2);
|
|
44
|
+
let seg = tag + stableClasses.map((c) => `.${cssEscape(c)}`).join("");
|
|
45
|
+
const parent = el.parentElement;
|
|
46
|
+
if (parent) {
|
|
47
|
+
const siblings = Array.from(parent.children).filter((c) => c.tagName === el.tagName);
|
|
48
|
+
if (siblings.length > 1) seg += `:nth-of-type(${siblings.indexOf(el) + 1})`;
|
|
49
|
+
}
|
|
50
|
+
return { seg, unique: false };
|
|
51
|
+
}
|
|
52
|
+
function buildSelector(el) {
|
|
53
|
+
const parts = [];
|
|
54
|
+
let cur = el;
|
|
55
|
+
while (cur && cur !== cur.ownerDocument.documentElement) {
|
|
56
|
+
const { seg, unique } = segmentFor(cur);
|
|
57
|
+
parts.unshift(seg);
|
|
58
|
+
if (unique) break;
|
|
59
|
+
cur = cur.parentElement;
|
|
60
|
+
if (parts.length > 8) break;
|
|
61
|
+
}
|
|
62
|
+
return parts.join(" > ");
|
|
63
|
+
}
|
|
64
|
+
function nearestContext(el) {
|
|
65
|
+
let cur = el;
|
|
66
|
+
while (cur) {
|
|
67
|
+
const heading = cur.querySelector('h1, h2, h3, legend, [role="heading"]');
|
|
68
|
+
if (heading && heading !== el && cur !== el) return normalizeText(heading.textContent);
|
|
69
|
+
const label = cur.getAttribute?.("aria-label") ?? cur.getAttribute?.("aria-labelledby");
|
|
70
|
+
if (label && cur !== el) return normalizeText(label);
|
|
71
|
+
cur = cur.parentElement;
|
|
72
|
+
}
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
function createAnchor(el, clientX, clientY) {
|
|
76
|
+
const r = el.getBoundingClientRect();
|
|
77
|
+
const win = el.ownerDocument.defaultView;
|
|
78
|
+
return {
|
|
79
|
+
selector: buildSelector(el),
|
|
80
|
+
textFingerprint: normalizeText(el.textContent),
|
|
81
|
+
tag: el.tagName.toLowerCase(),
|
|
82
|
+
rect: { x: r.left + win.scrollX, y: r.top + win.scrollY, width: r.width, height: r.height },
|
|
83
|
+
offset: {
|
|
84
|
+
x: r.width ? Math.min(1, Math.max(0, (clientX - r.left) / r.width)) : 0.5,
|
|
85
|
+
y: r.height ? Math.min(1, Math.max(0, (clientY - r.top) / r.height)) : 0.5
|
|
86
|
+
},
|
|
87
|
+
context: nearestContext(el)
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function textScore(el, fp) {
|
|
91
|
+
if (!fp) return 0.5;
|
|
92
|
+
const t = normalizeText(el.textContent);
|
|
93
|
+
if (t === fp) return 1;
|
|
94
|
+
if (t.includes(fp) || fp.includes(t)) return 0.75;
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
function resolveAnchor(anchor, doc) {
|
|
98
|
+
let candidates = [];
|
|
99
|
+
try {
|
|
100
|
+
candidates = Array.from(doc.querySelectorAll(anchor.selector));
|
|
101
|
+
} catch {
|
|
102
|
+
candidates = [];
|
|
103
|
+
}
|
|
104
|
+
if (candidates.length === 1) {
|
|
105
|
+
const s = textScore(candidates[0], anchor.textFingerprint);
|
|
106
|
+
if (s > 0) return { el: candidates[0], confidence: 0.95 };
|
|
107
|
+
}
|
|
108
|
+
if (candidates.length > 1) {
|
|
109
|
+
const ranked = candidates.map((el) => ({ el, s: textScore(el, anchor.textFingerprint) })).sort((a, b) => b.s - a.s);
|
|
110
|
+
if (ranked[0].s > 0) return { el: ranked[0].el, confidence: 0.8 };
|
|
111
|
+
}
|
|
112
|
+
if (anchor.textFingerprint) {
|
|
113
|
+
const sameTag = Array.from(doc.getElementsByTagName(anchor.tag));
|
|
114
|
+
const ranked = sameTag.map((el) => ({ el, s: textScore(el, anchor.textFingerprint) })).sort((a, b) => b.s - a.s);
|
|
115
|
+
if (ranked[0]?.s >= 0.75) return { el: ranked[0].el, confidence: 0.6 };
|
|
116
|
+
}
|
|
117
|
+
if (candidates.length === 1) return { el: candidates[0], confidence: 0.5 };
|
|
118
|
+
const win = doc.defaultView;
|
|
119
|
+
if (win && typeof doc.elementFromPoint === "function") {
|
|
120
|
+
const cx = anchor.rect.x + anchor.rect.width / 2 - win.scrollX;
|
|
121
|
+
const cy = anchor.rect.y + anchor.rect.height / 2 - win.scrollY;
|
|
122
|
+
const el = doc.elementFromPoint(cx, cy);
|
|
123
|
+
if (el) return { el, confidence: 0.3 };
|
|
124
|
+
}
|
|
125
|
+
return { el: null, confidence: 0 };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/issue-format.ts
|
|
129
|
+
function sessionToIssue(session) {
|
|
130
|
+
const n = session.annotations.length;
|
|
131
|
+
const page = safePath(session.annotations[0]?.pageUrl ?? session.prototypeUrl);
|
|
132
|
+
const title = `Prototype feedback: ${n} comment${n === 1 ? "" : "s"} on ${page}`;
|
|
133
|
+
const lines = [];
|
|
134
|
+
lines.push(`**Prototype:** ${session.prototypeUrl}`);
|
|
135
|
+
if (session.author) lines.push(`**Reviewer:** ${session.author}`);
|
|
136
|
+
lines.push(`**Session:** \`${session.id}\` \xB7 ${session.createdAt}`);
|
|
137
|
+
lines.push("");
|
|
138
|
+
session.annotations.forEach((a, i) => {
|
|
139
|
+
lines.push(`### ${i + 1}. ${a.kind ? `[${a.kind}] ` : ""}${firstLine(a.text)}`);
|
|
140
|
+
if (a.text.includes("\n")) lines.push(a.text.split("\n").slice(1).join("\n"));
|
|
141
|
+
lines.push("");
|
|
142
|
+
lines.push(`- **Page:** ${a.pageUrl}`);
|
|
143
|
+
lines.push(`- **Element:** \`${a.anchor.tag ?? "?"}\` \u2192 \`${a.anchor.selector}\``);
|
|
144
|
+
if (a.anchor.textFingerprint) lines.push(`- **Text:** \u201C${a.anchor.textFingerprint}\u201D`);
|
|
145
|
+
if (a.anchor.context) lines.push(`- **Near:** ${a.anchor.context}`);
|
|
146
|
+
if (a.steps?.length) lines.push(`- **Reached by clicking:** ${a.steps.map(stepLabel).join(" \u2192 ")}`);
|
|
147
|
+
if (a.viewport && Number.isFinite(a.viewport.width) && Number.isFinite(a.viewport.height)) lines.push(`- **Viewport:** ${a.viewport.width}\xD7${a.viewport.height}`);
|
|
148
|
+
lines.push("");
|
|
149
|
+
});
|
|
150
|
+
lines.push("<details><summary>peanut-gallery data (do not edit)</summary>");
|
|
151
|
+
lines.push("");
|
|
152
|
+
lines.push("```" + ISSUE_BLOCK_LANG);
|
|
153
|
+
lines.push(JSON.stringify(session, null, 2));
|
|
154
|
+
lines.push("```");
|
|
155
|
+
lines.push("");
|
|
156
|
+
lines.push("</details>");
|
|
157
|
+
return { title, body: lines.join("\n") };
|
|
158
|
+
}
|
|
159
|
+
function issueToSession(body) {
|
|
160
|
+
const re = new RegExp("```" + ISSUE_BLOCK_LANG.replace(/ /g, "\\s+") + "\\s*\\n([\\s\\S]*?)\\n```");
|
|
161
|
+
const m = body.match(re);
|
|
162
|
+
if (!m) return null;
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(m[1]);
|
|
165
|
+
if (!Array.isArray(parsed.annotations)) return null;
|
|
166
|
+
return parsed;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function newIssueUrl(repo, session, labels = [], maxLen = 7500) {
|
|
172
|
+
const base = `https://github.com/${repo}/issues/new`;
|
|
173
|
+
const build = (s) => {
|
|
174
|
+
const { title, body } = sessionToIssue(s);
|
|
175
|
+
const params = new URLSearchParams({ title, body });
|
|
176
|
+
if (labels.length) params.set("labels", labels.join(","));
|
|
177
|
+
return `${base}?${params.toString()}`;
|
|
178
|
+
};
|
|
179
|
+
let url = build(session);
|
|
180
|
+
if (url.length <= maxLen) return url;
|
|
181
|
+
const slim = {
|
|
182
|
+
...session,
|
|
183
|
+
userAgent: void 0,
|
|
184
|
+
annotations: session.annotations.map((a) => ({
|
|
185
|
+
...a,
|
|
186
|
+
anchor: {
|
|
187
|
+
...a.anchor,
|
|
188
|
+
context: void 0,
|
|
189
|
+
textFingerprint: a.anchor.textFingerprint.slice(0, 40),
|
|
190
|
+
rect: roundRect(a.anchor.rect)
|
|
191
|
+
},
|
|
192
|
+
steps: a.steps?.map((st) => ({ ...st, context: void 0, textFingerprint: st.textFingerprint.slice(0, 40), rect: roundRect(st.rect) }))
|
|
193
|
+
}))
|
|
194
|
+
};
|
|
195
|
+
url = build(slim);
|
|
196
|
+
if (url.length <= maxLen) return url;
|
|
197
|
+
const kept = [];
|
|
198
|
+
for (const a of slim.annotations) {
|
|
199
|
+
const trial = build({ ...slim, annotations: [...kept, a] });
|
|
200
|
+
if (trial.length > maxLen) break;
|
|
201
|
+
kept.push(a);
|
|
202
|
+
}
|
|
203
|
+
return build({ ...slim, annotations: kept });
|
|
204
|
+
}
|
|
205
|
+
function roundRect(r) {
|
|
206
|
+
return { x: Math.round(r.x), y: Math.round(r.y), width: Math.round(r.width), height: Math.round(r.height) };
|
|
207
|
+
}
|
|
208
|
+
function firstLine(s) {
|
|
209
|
+
return s.split("\n")[0].trim().slice(0, 100) || "(no text)";
|
|
210
|
+
}
|
|
211
|
+
function safePath(url) {
|
|
212
|
+
try {
|
|
213
|
+
const u = new URL(url);
|
|
214
|
+
return u.pathname === "/" ? u.host : u.pathname;
|
|
215
|
+
} catch {
|
|
216
|
+
return url;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function uid(prefix = "") {
|
|
220
|
+
const rand = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID().slice(0, 8) : Math.random().toString(36).slice(2, 10);
|
|
221
|
+
return prefix ? `${prefix}_${rand}` : rand;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/theme.ts
|
|
225
|
+
var PALETTE = {
|
|
226
|
+
conifer: "#1E4D36",
|
|
227
|
+
emerald: "#0F7B52",
|
|
228
|
+
wasabi: "#719872",
|
|
229
|
+
matcha: "#9CC98A",
|
|
230
|
+
pistachio: "#CFE1B4",
|
|
231
|
+
seaGlass: "#CDE2F2",
|
|
232
|
+
hydrangea: "#A6BFE5",
|
|
233
|
+
harbor: "#40648A",
|
|
234
|
+
chambray: "#6C8EBD",
|
|
235
|
+
prep: "#152A4A",
|
|
236
|
+
sunnyside: "#EAC26C",
|
|
237
|
+
linen: "#F6F3E4",
|
|
238
|
+
white: "#FFFFFF",
|
|
239
|
+
gull: "#A5B0B6",
|
|
240
|
+
carbon: "#22272B"
|
|
241
|
+
};
|
|
242
|
+
var DEFAULT_THEME = {
|
|
243
|
+
bg: PALETTE.linen,
|
|
244
|
+
surface: PALETTE.white,
|
|
245
|
+
text: PALETTE.carbon,
|
|
246
|
+
muted: "#5A6268",
|
|
247
|
+
line: "#DEDCCB",
|
|
248
|
+
heading: PALETTE.conifer,
|
|
249
|
+
primary: PALETTE.emerald,
|
|
250
|
+
primaryHover: "#006340",
|
|
251
|
+
// emerald 700
|
|
252
|
+
onPrimary: PALETTE.white,
|
|
253
|
+
toolbar: PALETTE.conifer,
|
|
254
|
+
toolbarButton: "#325D46",
|
|
255
|
+
// conifer 700
|
|
256
|
+
toolbarButtonHover: "#4E7660",
|
|
257
|
+
// conifer 600
|
|
258
|
+
onToolbar: PALETTE.white,
|
|
259
|
+
accent: PALETTE.sunnyside,
|
|
260
|
+
onAccent: PALETTE.carbon,
|
|
261
|
+
highlight: PALETTE.emerald,
|
|
262
|
+
highlightTint: "rgba(15, 123, 82, 0.10)",
|
|
263
|
+
pinLow: PALETTE.gull,
|
|
264
|
+
chip: PALETTE.seaGlass,
|
|
265
|
+
onChip: PALETTE.prep,
|
|
266
|
+
count: PALETTE.pistachio,
|
|
267
|
+
onCount: PALETTE.conifer,
|
|
268
|
+
ghost: "#EDEBE3",
|
|
269
|
+
// linen 100
|
|
270
|
+
ghostHover: "#DBD8CA",
|
|
271
|
+
// linen 200
|
|
272
|
+
link: PALETTE.prep,
|
|
273
|
+
toast: PALETTE.prep,
|
|
274
|
+
onToast: PALETTE.linen,
|
|
275
|
+
danger: "#F6E3DF",
|
|
276
|
+
onDanger: "#9B3426",
|
|
277
|
+
success: PALETTE.emerald,
|
|
278
|
+
successTint: "#D4F4E2",
|
|
279
|
+
// emerald 100
|
|
280
|
+
fontBody: '"Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
|
|
281
|
+
fontDisplay: '"Young Serif", Georgia, "Times New Roman", serif',
|
|
282
|
+
fontMono: '"JetBrains Mono", ui-monospace, Menlo, Consolas, monospace'
|
|
283
|
+
};
|
|
284
|
+
var DEFAULT_FONTS_URL = "https://fonts.googleapis.com/css2?family=Young+Serif&family=Manrope:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap";
|
|
285
|
+
function resolveTheme(overrides) {
|
|
286
|
+
return { ...DEFAULT_THEME, ...overrides ?? {} };
|
|
287
|
+
}
|
|
288
|
+
var kebab = (s) => s.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
|
289
|
+
function themeToCssVars(theme) {
|
|
290
|
+
return Object.keys(theme).map((k) => `--pg-${kebab(k)}: ${theme[k]};`).join(" ");
|
|
291
|
+
}
|
|
292
|
+
export {
|
|
293
|
+
DEFAULT_FONTS_URL,
|
|
294
|
+
DEFAULT_THEME,
|
|
295
|
+
ISSUE_BLOCK_LANG,
|
|
296
|
+
LABELS,
|
|
297
|
+
MAX_STEPS,
|
|
298
|
+
PALETTE,
|
|
299
|
+
buildSelector,
|
|
300
|
+
createAnchor,
|
|
301
|
+
issueToSession,
|
|
302
|
+
newIssueUrl,
|
|
303
|
+
normalizeText,
|
|
304
|
+
resolveAnchor,
|
|
305
|
+
resolveTheme,
|
|
306
|
+
sessionToIssue,
|
|
307
|
+
stepLabel,
|
|
308
|
+
themeToCssVars,
|
|
309
|
+
uid
|
|
310
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixelatlarge/peanut-gallery-core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Shared types, DOM anchoring, and GitHub issue serialization for peanut-gallery.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"prototype",
|
|
7
|
+
"feedback",
|
|
8
|
+
"annotation"
|
|
9
|
+
],
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/PixelAtLarge/peanut-gallery.git",
|
|
13
|
+
"directory": "packages/core"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/PixelAtLarge/peanut-gallery#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/PixelAtLarge/peanut-gallery/issues"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "./dist/index.cjs",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"require": "./dist/index.cjs"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"typecheck": "tsc --noEmit"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|