punchlist-axi 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 +21 -0
- package/README.md +57 -0
- package/SKILL.md +34 -0
- package/package.json +36 -0
- package/scripts/client.js +564 -0
- package/scripts/punchlist-axi.mjs +25 -0
- package/scripts/punchlist-cli.mjs +3535 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
// punchlist picker — injected by the proxy into every HTML page.
|
|
2
|
+
// Notes are snapshotted when added (so they survive navigation) and sent to the agent as one batch.
|
|
3
|
+
(() => {
|
|
4
|
+
if (window.__pointEdit) return;
|
|
5
|
+
window.__pointEdit = true;
|
|
6
|
+
|
|
7
|
+
const API = '/__pe';
|
|
8
|
+
const STORE = '__pe_notes';
|
|
9
|
+
const MAX_ELS = 6;
|
|
10
|
+
const STYLE_PROPS = ['display', 'position', 'width', 'height', 'margin', 'padding', 'gap', 'flex-direction',
|
|
11
|
+
'justify-content', 'align-items', 'color', 'background-color', 'font-family', 'font-size', 'font-weight',
|
|
12
|
+
'line-height', 'border', 'border-radius', 'box-shadow', 'opacity'];
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------- UI (shadow DOM, outside <body> so Turbo keeps it)
|
|
15
|
+
const host = document.createElement('punchlist-root');
|
|
16
|
+
const root = host.attachShadow({ mode: 'open' });
|
|
17
|
+
root.innerHTML = `
|
|
18
|
+
<style>
|
|
19
|
+
:host { all: initial; }
|
|
20
|
+
* { box-sizing: border-box; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
21
|
+
.layer { position: fixed; inset: 0; pointer-events: none; z-index: 2147483645; }
|
|
22
|
+
.box { position: fixed; border: 2px solid #7c3aed; background: rgba(124,58,237,.12); border-radius: 2px; display: none; }
|
|
23
|
+
.box.draft { border-color: #f59e0b; background: rgba(245,158,11,.14); display: block; }
|
|
24
|
+
.box.noted { border: 1.5px dashed #7c3aed; background: transparent; display: block; }
|
|
25
|
+
.box.marquee { border-style: dashed; background: rgba(124,58,237,.08); }
|
|
26
|
+
.tag { position: absolute; left: -2px; top: -22px; background: #7c3aed; color: #fff; font: 11px/20px ui-monospace, monospace;
|
|
27
|
+
padding: 0 6px; border-radius: 3px; white-space: nowrap; max-width: 80vw; overflow: hidden; text-overflow: ellipsis; }
|
|
28
|
+
.draft .tag { background: #f59e0b; color: #000; }
|
|
29
|
+
.flip .tag { top: auto; bottom: -22px; }
|
|
30
|
+
.pin { position: fixed; width: 22px; height: 22px; margin: -11px 0 0 -11px; border-radius: 50%; background: #7c3aed; color: #fff;
|
|
31
|
+
font: 600 12px/22px ui-sans-serif, system-ui, sans-serif; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,.3); }
|
|
32
|
+
.tools { position: fixed; z-index: 2147483647; right: 12px; bottom: calc(12px + env(safe-area-inset-bottom, 0px)); display: flex; gap: 2px;
|
|
33
|
+
padding: 4px; border-radius: 999px; background: #18181b; box-shadow: 0 4px 16px rgba(0,0,0,.3); }
|
|
34
|
+
.tools button { height: 40px; min-width: 40px; border: 0; border-radius: 999px; background: transparent; color: #fafafa;
|
|
35
|
+
font: 600 15px ui-sans-serif, system-ui, sans-serif; cursor: pointer; padding: 0 10px; display: inline-flex; align-items: center; gap: 4px; }
|
|
36
|
+
.tools button[aria-pressed="true"] { background: #7c3aed; }
|
|
37
|
+
.tools .freeze[aria-pressed="true"] { background: #f59e0b; color: #000; }
|
|
38
|
+
.tools .count { font-size: 12px; background: #27272a; border-radius: 999px; padding: 1px 6px; }
|
|
39
|
+
.tools .busy { font-size: 12px; background: #f59e0b; color: #000; border-radius: 999px; padding: 1px 6px; display: none; }
|
|
40
|
+
.tools .go { background: #7c3aed; padding: 0 14px; }
|
|
41
|
+
.tools .go:disabled { background: #27272a; color: #71717a; cursor: default; }
|
|
42
|
+
.sheet { position: fixed; z-index: 2147483647; left: 50%; transform: translateX(-50%); bottom: calc(68px + env(safe-area-inset-bottom, 0px));
|
|
43
|
+
width: min(520px, calc(100vw - 24px)); max-height: calc(100vh - 120px); overflow: auto; background: #18181b; color: #fafafa;
|
|
44
|
+
border-radius: 12px; padding: 12px; box-shadow: 0 8px 32px rgba(0,0,0,.4); display: none; }
|
|
45
|
+
.panel.top { top: calc(12px + env(safe-area-inset-top, 0px)); bottom: auto; }
|
|
46
|
+
.label { font: 12px ui-monospace, monospace; color: #fbbf24; word-break: break-all; }
|
|
47
|
+
.hint { font: 11px ui-monospace, monospace; color: #a1a1aa; margin-top: 2px; word-break: break-all; }
|
|
48
|
+
.where { display: flex; gap: 8px; align-items: center; } .where .hint { flex: 1; min-width: 0; }
|
|
49
|
+
.sheet .open, .sheet .locate { padding: 2px 8px; font-size: 12px; flex: none; }
|
|
50
|
+
textarea { width: 100%; min-height: 64px; margin: 8px 0; padding: 8px; border-radius: 8px; border: 1px solid #3f3f46;
|
|
51
|
+
background: #09090b; color: #fafafa; font-size: 16px; resize: vertical; }
|
|
52
|
+
.row { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
|
|
53
|
+
.sheet button, .chip, .toast button { border: 1px solid #3f3f46; background: #27272a; color: #fafafa; border-radius: 6px; padding: 6px 10px; font-size: 13px; cursor: pointer; }
|
|
54
|
+
.primary { background: #7c3aed !important; border-color: #7c3aed !important; margin-left: auto; }
|
|
55
|
+
h3 { margin: 0 0 8px; font-size: 14px; }
|
|
56
|
+
ul { list-style: none; margin: 0 0 10px; padding: 0; }
|
|
57
|
+
li { display: grid; grid-template-columns: 22px 1fr auto; gap: 8px; align-items: start; padding: 6px 0; border-bottom: 1px solid #27272a; }
|
|
58
|
+
.num { width: 22px; height: 22px; border-radius: 50%; background: #7c3aed; font: 600 12px/22px ui-sans-serif, system-ui, sans-serif; text-align: center; }
|
|
59
|
+
.say { font-size: 14px; } .what { font: 11px ui-monospace, monospace; color: #a1a1aa; word-break: break-all; }
|
|
60
|
+
li button { padding: 2px 8px !important; }
|
|
61
|
+
.empty { color: #a1a1aa; font-size: 13px; margin: 0 0 10px; }
|
|
62
|
+
.chip { position: fixed; z-index: 2147483647; background: #18181b; box-shadow: 0 4px 16px rgba(0,0,0,.3); display: none; }
|
|
63
|
+
.toasts { position: fixed; z-index: 2147483647; left: 12px; right: 12px; top: calc(12px + env(safe-area-inset-top, 0px));
|
|
64
|
+
display: flex; flex-direction: column; align-items: flex-start; gap: 6px; pointer-events: none; }
|
|
65
|
+
.toast { background: #18181b; color: #fafafa; padding: 8px 10px; border-radius: 8px; font-size: 13px; max-width: 100%;
|
|
66
|
+
box-shadow: 0 4px 16px rgba(0,0,0,.3); display: flex; gap: 8px; align-items: center; pointer-events: auto; }
|
|
67
|
+
label.auto { font-size: 12px; color: #a1a1aa; display: flex; gap: 4px; align-items: center; }
|
|
68
|
+
</style>
|
|
69
|
+
<div class="layer"><div class="box hover"><span class="tag"></span></div><div class="box marquee"></div><div class="marks"></div></div>
|
|
70
|
+
<div class="toasts"></div>
|
|
71
|
+
<button class="chip" type="button">💬 Annotate</button>
|
|
72
|
+
<div class="sheet panel" role="dialog" aria-label="Add a note">
|
|
73
|
+
<div class="label"></div>
|
|
74
|
+
<div class="where"><div class="hint"></div><button class="open" type="button" title="Open this file in your editor">↗ Open</button><button class="locate" type="button" title="Ask the agent to find this file and open it in your editor">🔎 Locate</button></div>
|
|
75
|
+
<textarea placeholder="What should change? (⌘/Ctrl+Enter to add)"></textarea>
|
|
76
|
+
<div class="row">
|
|
77
|
+
<button class="up" type="button" title="Select parent">⬆ parent</button>
|
|
78
|
+
<button class="down" type="button" title="Select first child">⬇ child</button>
|
|
79
|
+
<button class="cancel" type="button">Cancel</button>
|
|
80
|
+
<button class="primary add" type="button">Add note</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="sheet tray" role="dialog" aria-label="Notes">
|
|
84
|
+
<h3>Notes to send</h3>
|
|
85
|
+
<ul class="notes"></ul>
|
|
86
|
+
<p class="empty">No notes yet. Press ◎, then tap or drag on the page.</p>
|
|
87
|
+
<div class="row">
|
|
88
|
+
<button class="copy" type="button">Copy markdown</button>
|
|
89
|
+
<button class="clear" type="button">Clear</button>
|
|
90
|
+
<button class="primary send" type="button">Send to agent</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="tools">
|
|
94
|
+
<button data-act="pick" aria-pressed="false" title="Annotate (Alt+Shift+E)" aria-label="Annotate">◎</button>
|
|
95
|
+
<button data-act="freeze" class="freeze" aria-pressed="false" title="Pause animations (Alt+Shift+P)" aria-label="Pause animations">⏸</button>
|
|
96
|
+
<button data-act="tray" aria-label="Notes">🗒 <span class="count">0</span><span class="busy"></span></button>
|
|
97
|
+
<button data-act="send" class="go" disabled>Send</button>
|
|
98
|
+
</div>`;
|
|
99
|
+
document.documentElement.appendChild(host);
|
|
100
|
+
|
|
101
|
+
const $ = s => root.querySelector(s);
|
|
102
|
+
const hoverBox = $('.box.hover'), marquee = $('.box.marquee'), marks = $('.marks');
|
|
103
|
+
const panel = $('.panel'), tray = $('.tray'), ta = $('textarea'), chip = $('.chip'), tools = $('.tools');
|
|
104
|
+
|
|
105
|
+
let picking = false, frozen = false, draft = null, drag = null, raf = 0;
|
|
106
|
+
let notes = []; // { ...snapshot, els: [live elements or null] }
|
|
107
|
+
const isOurs = el => el === host || host.contains(el);
|
|
108
|
+
const pageEl = el => el && el.nodeType === 1 && !isOurs(el) && el !== document.documentElement && el !== document.body;
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------- context
|
|
111
|
+
function label(el) {
|
|
112
|
+
let s = el.localName;
|
|
113
|
+
if (el.id) s += '#' + el.id;
|
|
114
|
+
const cls = [...el.classList].slice(0, 3);
|
|
115
|
+
if (cls.length) s += '.' + cls.join('.');
|
|
116
|
+
if (el.classList.length > 3) s += '…';
|
|
117
|
+
return s;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function cssPath(el) {
|
|
121
|
+
const uniq = id => { try { return document.querySelectorAll('#' + CSS.escape(id)).length === 1; } catch { return false; } };
|
|
122
|
+
const parts = [];
|
|
123
|
+
while (el && el.nodeType === 1 && el !== document.documentElement) {
|
|
124
|
+
if (el.id && uniq(el.id)) { parts.unshift('#' + CSS.escape(el.id)); break; }
|
|
125
|
+
let part = el.localName;
|
|
126
|
+
const cls = [...el.classList].filter(c => c.length < 40).slice(0, 2);
|
|
127
|
+
if (cls.length) part += '.' + cls.map(c => CSS.escape(c)).join('.');
|
|
128
|
+
const parent = el.parentElement;
|
|
129
|
+
if (parent) {
|
|
130
|
+
const sibs = [...parent.children].filter(s => s.localName === el.localName);
|
|
131
|
+
if (sibs.length > 1) part += `:nth-of-type(${sibs.indexOf(el) + 1})`;
|
|
132
|
+
}
|
|
133
|
+
parts.unshift(part);
|
|
134
|
+
el = parent;
|
|
135
|
+
}
|
|
136
|
+
return parts.join(' > ');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Rails: config.action_view.annotate_rendered_view_with_filenames = true → <!-- BEGIN app/views/... -->
|
|
140
|
+
function railsTemplates(el) {
|
|
141
|
+
const stack = [];
|
|
142
|
+
const w = document.createTreeWalker(document, NodeFilter.SHOW_COMMENT);
|
|
143
|
+
let c;
|
|
144
|
+
while ((c = w.nextNode())) {
|
|
145
|
+
if (!(c.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING)) break;
|
|
146
|
+
const m = c.data.trim().match(/^(BEGIN|END)\s+(\S+)/);
|
|
147
|
+
if (!m) continue;
|
|
148
|
+
if (m[1] === 'BEGIN') stack.push(m[2]);
|
|
149
|
+
else { const i = stack.lastIndexOf(m[2]); if (i >= 0) stack.splice(i); }
|
|
150
|
+
}
|
|
151
|
+
return stack;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function hints(el) {
|
|
155
|
+
const h = { rails: railsTemplates(el), svelte: [], react: [], vue: [] };
|
|
156
|
+
for (let n = el; n && h.svelte.length < 3; n = n.parentElement) {
|
|
157
|
+
const loc = n.__svelte_meta?.loc;
|
|
158
|
+
if (loc?.file) { const s = `${loc.file}:${loc.line}:${loc.column}`; if (!h.svelte.includes(s)) h.svelte.push(s); }
|
|
159
|
+
}
|
|
160
|
+
for (let n = el; n && !h.react.length; n = n.parentElement) {
|
|
161
|
+
const key = Object.keys(n).find(k => k.startsWith('__reactFiber$'));
|
|
162
|
+
if (!key) continue;
|
|
163
|
+
for (let f = n[key]; f && h.react.length < 6; f = f.return) {
|
|
164
|
+
if (typeof f.type !== 'function' && typeof f.type !== 'object') continue;
|
|
165
|
+
const name = f.type?.displayName || f.type?.name || f.type?.render?.name;
|
|
166
|
+
if (!name) continue;
|
|
167
|
+
const src = f._debugSource ? ` (${f._debugSource.fileName}:${f._debugSource.lineNumber})` : '';
|
|
168
|
+
h.react.push(name + src);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (let n = el; n && !h.vue.length; n = n.parentElement) {
|
|
172
|
+
for (let c = n.__vueParentComponent; c && h.vue.length < 6; c = c.parent) {
|
|
173
|
+
const t = c.type || {};
|
|
174
|
+
const s = t.__file || t.name || t.__name;
|
|
175
|
+
if (s) h.vue.push(s);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return h;
|
|
179
|
+
}
|
|
180
|
+
const bestHint = h => h.svelte[0] || h.rails.at(-1) || h.vue[0] || h.react[0] || '';
|
|
181
|
+
// { file, line?, column? } from the best hint that names a file, or null (a bare component name)
|
|
182
|
+
function fileHint(h) {
|
|
183
|
+
for (const s of [h.svelte[0], h.rails.at(-1), h.vue[0], h.react.find(r => r.endsWith(')'))]) {
|
|
184
|
+
const m = s && (s.match(/\((.+):(\d+)\)$/) || s.match(/^([^\s()]+\.\w+)(?::(\d+))?(?::(\d+))?$/));
|
|
185
|
+
if (m) return { file: m[1], ...(m[2] ? { line: +m[2] } : {}), ...(m[3] ? { column: +m[3] } : {}) };
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function elementContext(el) {
|
|
191
|
+
const cs = getComputedStyle(el);
|
|
192
|
+
const styles = {};
|
|
193
|
+
for (const p of STYLE_PROPS) { const v = cs.getPropertyValue(p); if (v && v !== 'none' && v !== 'normal' && v !== 'auto') styles[p] = v; }
|
|
194
|
+
let html = el.outerHTML;
|
|
195
|
+
if (html.length > 4000) html = html.slice(0, 4000) + '\n<!-- …truncated by punchlist -->';
|
|
196
|
+
const ancestors = [];
|
|
197
|
+
for (let n = el.parentElement; n && n !== document.body && ancestors.length < 4; n = n.parentElement) ancestors.unshift(label(n));
|
|
198
|
+
const r = el.getBoundingClientRect();
|
|
199
|
+
return {
|
|
200
|
+
label: label(el), selector: cssPath(el), ancestors,
|
|
201
|
+
text: (el.innerText || el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 300),
|
|
202
|
+
html, styles, rect: { x: r.x + scrollX, y: r.y + scrollY, width: r.width, height: r.height }, hints: hints(el),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function describe(n) {
|
|
207
|
+
if (n.kind === 'text') return `text “${n.text.length > 40 ? n.text.slice(0, 40) + '…' : n.text}” in ${n.elements[0].label}`;
|
|
208
|
+
if (n.kind === 'elements') return `${n.elements.length} elements: ${n.elements.map(e => e.label).join(', ')}`;
|
|
209
|
+
if (n.kind === 'area') return `area ${Math.round(n.area.width)}×${Math.round(n.area.height)} in ${n.elements[0].label}`;
|
|
210
|
+
return n.elements[0].label;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------- persistence (survives reloads / Turbo visits)
|
|
214
|
+
function save() {
|
|
215
|
+
try { sessionStorage.setItem(STORE, JSON.stringify(notes.map(({ els, ...n }) => n))); } catch {}
|
|
216
|
+
}
|
|
217
|
+
function restore() {
|
|
218
|
+
try {
|
|
219
|
+
const saved = JSON.parse(sessionStorage.getItem(STORE) || '[]');
|
|
220
|
+
notes = saved.map(n => ({ ...n, els: n.path === here() ? n.elements.map(e => { try { return document.querySelector(e.selector); } catch { return null; } }) : [] }));
|
|
221
|
+
} catch { notes = []; }
|
|
222
|
+
}
|
|
223
|
+
const here = () => location.pathname + location.search;
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------- overlay rendering
|
|
226
|
+
function placeBox(box, r, text) {
|
|
227
|
+
Object.assign(box.style, { left: r.left + 'px', top: r.top + 'px', width: r.width + 'px', height: r.height + 'px' });
|
|
228
|
+
box.classList.toggle('flip', r.top < 24);
|
|
229
|
+
const tag = box.querySelector('.tag'); if (tag && text != null) tag.textContent = text;
|
|
230
|
+
}
|
|
231
|
+
const areaRect = a => ({ left: a.x - scrollX, top: a.y - scrollY, width: a.width, height: a.height });
|
|
232
|
+
|
|
233
|
+
function renderMarks() {
|
|
234
|
+
marks.innerHTML = '';
|
|
235
|
+
const items = [];
|
|
236
|
+
const add = (cls, getRect, ok, text, pinNo) => {
|
|
237
|
+
const b = document.createElement('div'); b.className = 'box ' + cls;
|
|
238
|
+
if (text) b.innerHTML = '<span class="tag"></span>';
|
|
239
|
+
marks.append(b);
|
|
240
|
+
items.push({ node: b, getRect, ok, text });
|
|
241
|
+
if (pinNo) {
|
|
242
|
+
const p = document.createElement('div'); p.className = 'pin'; p.textContent = pinNo; marks.append(p);
|
|
243
|
+
items.push({ node: p, pin: true, getRect, ok });
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
notes.forEach((n, i) => {
|
|
247
|
+
if (n.path !== here()) return;
|
|
248
|
+
if (n.kind === 'area') return add('noted', () => areaRect(n.area), () => true, null, i + 1);
|
|
249
|
+
n.els.forEach((el, j) => el && add('noted', () => el.getBoundingClientRect(), () => el.isConnected, null, j === 0 ? i + 1 : null));
|
|
250
|
+
});
|
|
251
|
+
if (draft) {
|
|
252
|
+
if (draft.kind === 'area') add('draft', () => areaRect(draft.area), () => true, draft.desc);
|
|
253
|
+
else draft.els.forEach((el, j) => add('draft', () => el.getBoundingClientRect(), () => el.isConnected, j === 0 ? draft.desc : null));
|
|
254
|
+
}
|
|
255
|
+
cancelAnimationFrame(raf);
|
|
256
|
+
const tick = () => {
|
|
257
|
+
for (const it of items) {
|
|
258
|
+
if (!it.ok()) { it.node.style.display = 'none'; continue; }
|
|
259
|
+
const r = it.getRect();
|
|
260
|
+
if (it.pin) { it.node.style.left = r.left + 'px'; it.node.style.top = r.top + 'px'; }
|
|
261
|
+
else placeBox(it.node, r, it.text);
|
|
262
|
+
}
|
|
263
|
+
if (items.length) raf = requestAnimationFrame(tick);
|
|
264
|
+
};
|
|
265
|
+
tick();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function refresh() {
|
|
269
|
+
$('.count').textContent = notes.length;
|
|
270
|
+
const go = tools.querySelector('.go');
|
|
271
|
+
go.disabled = !notes.length;
|
|
272
|
+
go.textContent = notes.length ? `Send ${notes.length}` : 'Send';
|
|
273
|
+
tray.querySelector('.send').disabled = !notes.length;
|
|
274
|
+
$('.empty').style.display = notes.length ? 'none' : 'block';
|
|
275
|
+
const list = $('.notes'); list.innerHTML = '';
|
|
276
|
+
notes.forEach((n, i) => {
|
|
277
|
+
const li = document.createElement('li');
|
|
278
|
+
li.innerHTML = '<span class="num"></span><div><div class="say"></div><div class="what"></div></div><button type="button" aria-label="Remove note">×</button>';
|
|
279
|
+
li.querySelector('.num').textContent = i + 1;
|
|
280
|
+
li.querySelector('.say').textContent = n.comment;
|
|
281
|
+
li.querySelector('.what').textContent = `${describe(n)}${n.path !== here() ? ` (${n.path})` : ''}`;
|
|
282
|
+
li.querySelector('button').onclick = () => { notes.splice(i, 1); save(); refresh(); renderMarks(); };
|
|
283
|
+
list.append(li);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function openSheet(which) {
|
|
288
|
+
panel.style.display = which === 'panel' ? 'block' : 'none';
|
|
289
|
+
tray.style.display = which === 'tray' ? 'block' : 'none';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function toast(text, actions, ms = 5000) {
|
|
293
|
+
const t = document.createElement('div');
|
|
294
|
+
t.className = 'toast'; t.textContent = text;
|
|
295
|
+
if (actions) t.append(actions);
|
|
296
|
+
$('.toasts').append(t);
|
|
297
|
+
setTimeout(() => t.remove(), actions ? 15000 : ms);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------- modes
|
|
301
|
+
function setPicking(on) {
|
|
302
|
+
picking = on;
|
|
303
|
+
tools.querySelector('[data-act="pick"]').setAttribute('aria-pressed', on);
|
|
304
|
+
const ds = document.documentElement.style;
|
|
305
|
+
ds.cursor = on ? 'crosshair' : '';
|
|
306
|
+
ds.touchAction = on ? 'none' : '';
|
|
307
|
+
ds.userSelect = ds.webkitUserSelect = on ? 'none' : '';
|
|
308
|
+
if (!on) hoverBox.style.display = 'none';
|
|
309
|
+
if (on) { hideChip(); getSelection()?.removeAllRanges(); }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let freezeStyle = null;
|
|
313
|
+
function setFrozen(on) {
|
|
314
|
+
frozen = on;
|
|
315
|
+
tools.querySelector('[data-act="freeze"]').setAttribute('aria-pressed', on);
|
|
316
|
+
if (on) {
|
|
317
|
+
freezeStyle = document.createElement('style');
|
|
318
|
+
freezeStyle.textContent = '*, *::before, *::after { animation-play-state: paused !important; transition: none !important; }';
|
|
319
|
+
document.head.append(freezeStyle);
|
|
320
|
+
} else freezeStyle?.remove();
|
|
321
|
+
for (const a of document.getAnimations?.() || []) { try { on ? a.pause() : a.play(); } catch {} }
|
|
322
|
+
document.querySelectorAll('video, audio').forEach(m => { if (on) { m.dataset.peWasPlaying = !m.paused; m.pause(); } else if (m.dataset.peWasPlaying === 'true') m.play().catch(() => {}); });
|
|
323
|
+
toast(on ? '⏸ Animations paused' : '▶︎ Animations resumed', null, 2500);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
tools.addEventListener('click', e => {
|
|
327
|
+
const act = e.target.closest('button')?.dataset.act;
|
|
328
|
+
if (act === 'pick') { cancelDraft(); setPicking(!picking); }
|
|
329
|
+
if (act === 'freeze') setFrozen(!frozen);
|
|
330
|
+
if (act === 'tray') { cancelDraft(); openSheet(tray.style.display === 'block' ? null : 'tray'); }
|
|
331
|
+
if (act === 'send') send();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// ---------------------------------------------------------------- drafting
|
|
335
|
+
function startDraft(d) {
|
|
336
|
+
draft = d;
|
|
337
|
+
hoverBox.style.display = 'none';
|
|
338
|
+
const snap = snapshot(d);
|
|
339
|
+
d.desc = describe(snap);
|
|
340
|
+
$('.label').textContent = d.desc;
|
|
341
|
+
$('.hint').textContent = bestHint(snap.elements[0].hints) || snap.elements[0].selector;
|
|
342
|
+
d.file = fileHint(snap.elements[0].hints);
|
|
343
|
+
$('.open').style.display = d.file ? '' : 'none';
|
|
344
|
+
$('.locate').style.display = d.file ? 'none' : '';
|
|
345
|
+
const single = d.kind === 'element';
|
|
346
|
+
$('.up').style.display = $('.down').style.display = single ? '' : 'none';
|
|
347
|
+
// keep the target visible: a target in the lower half gets the sheet at the top
|
|
348
|
+
const r = d.area ? { top: d.area.y - scrollY, bottom: d.area.y - scrollY + d.area.height } : d.els[0].getBoundingClientRect();
|
|
349
|
+
panel.classList.toggle('top', (r.top + r.bottom) / 2 > innerHeight / 2);
|
|
350
|
+
openSheet('panel');
|
|
351
|
+
renderMarks();
|
|
352
|
+
// focus inside the gesture: early keys aren't lost and iOS opens the keyboard
|
|
353
|
+
ta.focus({ preventScroll: true });
|
|
354
|
+
}
|
|
355
|
+
function snapshot(d) {
|
|
356
|
+
return { kind: d.kind, path: here(), title: document.title, text: d.text, area: d.area, frozen, elements: d.els.map(elementContext) };
|
|
357
|
+
}
|
|
358
|
+
function cancelDraft() { if (!draft) return; draft = null; ta.value = ''; openSheet(null); renderMarks(); }
|
|
359
|
+
function addNote() {
|
|
360
|
+
const comment = ta.value.trim();
|
|
361
|
+
if (!draft || !comment) return ta.focus();
|
|
362
|
+
notes.push({ ...snapshot(draft), comment, els: draft.els });
|
|
363
|
+
ta.value = '';
|
|
364
|
+
draft = null;
|
|
365
|
+
openSheet(null);
|
|
366
|
+
save(); refresh(); renderMarks();
|
|
367
|
+
toast(`📝 Note ${notes.length} added${picking ? ' — tap the next spot' : ''}`, null, 2500);
|
|
368
|
+
}
|
|
369
|
+
$('.add').onclick = addNote;
|
|
370
|
+
// the proxy opens it on the dev machine, so this also works from a phone
|
|
371
|
+
$('.open').onclick = async () => {
|
|
372
|
+
if (!draft?.file) return;
|
|
373
|
+
try {
|
|
374
|
+
const r = await fetch(API + '/open', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(draft.file) });
|
|
375
|
+
const b = await r.json().catch(() => ({}));
|
|
376
|
+
if (!r.ok) throw new Error(b.error || r.status);
|
|
377
|
+
toast(`↗ Opened ${b.file} in your editor`, null, 2500);
|
|
378
|
+
} catch (e) { toast(`❌ Open failed: ${e.message}`); }
|
|
379
|
+
};
|
|
380
|
+
// no file in the hints (e.g. JSX built without source info): the agent finds it and runs `done --open`
|
|
381
|
+
$('.locate').onclick = async () => {
|
|
382
|
+
if (!draft) return;
|
|
383
|
+
const { elements: [el], ...n } = snapshot(draft);
|
|
384
|
+
try {
|
|
385
|
+
const r = await fetch(API + '/requests', { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
386
|
+
body: JSON.stringify({ kind: 'locate', notes: [{ ...n, comment: 'Find the source file of this element', elements: [el] }] }) });
|
|
387
|
+
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || r.status);
|
|
388
|
+
toast('🔎 Asked the agent to find this file', null, 2500);
|
|
389
|
+
} catch (e) { toast(`❌ Locate failed: ${e.message}`); }
|
|
390
|
+
};
|
|
391
|
+
$('.cancel').onclick = cancelDraft;
|
|
392
|
+
$('.up').onclick = () => { const p = draft?.els[0].parentElement; if (pageEl(p)) startDraft({ kind: 'element', els: [p] }); };
|
|
393
|
+
$('.down').onclick = () => { const c = draft?.els[0].firstElementChild; if (c) startDraft({ kind: 'element', els: [c] }); };
|
|
394
|
+
for (const t of ['keydown', 'keyup', 'keypress']) ta.addEventListener(t, e => e.stopPropagation());
|
|
395
|
+
ta.addEventListener('keydown', e => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) addNote(); });
|
|
396
|
+
|
|
397
|
+
// ---------------------------------------------------------------- pick: tap / drag
|
|
398
|
+
const at = (x, y) => { host.style.display = 'none'; const el = document.elementFromPoint(x, y); host.style.display = ''; return pageEl(el) ? el : null; };
|
|
399
|
+
const swallow = e => { if (picking && !isOurs(e.target)) { e.preventDefault(); e.stopImmediatePropagation(); } };
|
|
400
|
+
for (const t of ['mousedown', 'mouseup', 'touchstart', 'touchend', 'dblclick', 'contextmenu', 'click'])
|
|
401
|
+
document.addEventListener(t, swallow, { capture: true, passive: false });
|
|
402
|
+
|
|
403
|
+
document.addEventListener('pointerdown', e => {
|
|
404
|
+
if (!picking || isOurs(e.target)) return;
|
|
405
|
+
e.preventDefault(); e.stopImmediatePropagation();
|
|
406
|
+
drag = { x: e.clientX, y: e.clientY, moved: false };
|
|
407
|
+
}, true);
|
|
408
|
+
document.addEventListener('pointermove', e => {
|
|
409
|
+
if (!picking) return;
|
|
410
|
+
if (drag) {
|
|
411
|
+
if (Math.hypot(e.clientX - drag.x, e.clientY - drag.y) > 8) drag.moved = true;
|
|
412
|
+
if (drag.moved) {
|
|
413
|
+
hoverBox.style.display = 'none';
|
|
414
|
+
marquee.style.display = 'block';
|
|
415
|
+
placeBox(marquee, { left: Math.min(drag.x, e.clientX), top: Math.min(drag.y, e.clientY), width: Math.abs(e.clientX - drag.x), height: Math.abs(e.clientY - drag.y) });
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (e.pointerType !== 'mouse' || draft || isOurs(e.target)) return;
|
|
420
|
+
const el = at(e.clientX, e.clientY);
|
|
421
|
+
if (el) { hoverBox.style.display = 'block'; placeBox(hoverBox, el.getBoundingClientRect(), label(el)); }
|
|
422
|
+
}, true);
|
|
423
|
+
document.addEventListener('pointerup', e => {
|
|
424
|
+
if (!picking || !drag) return;
|
|
425
|
+
e.preventDefault(); e.stopImmediatePropagation();
|
|
426
|
+
const d = drag; drag = null;
|
|
427
|
+
marquee.style.display = 'none';
|
|
428
|
+
if (!d.moved) { const el = at(e.clientX, e.clientY); if (el) startDraft({ kind: 'element', els: [el] }); return; }
|
|
429
|
+
const sel = { left: Math.min(d.x, e.clientX), top: Math.min(d.y, e.clientY), right: Math.max(d.x, e.clientX), bottom: Math.max(d.y, e.clientY) };
|
|
430
|
+
const fits = r => r.width > 0 && r.height > 0 && r.left >= sel.left - 2 && r.right <= sel.right + 2 && r.top >= sel.top - 2 && r.bottom <= sel.bottom + 2;
|
|
431
|
+
// block elements are wider than their text, so also accept an element whose content fits
|
|
432
|
+
const contentRect = el => { const rg = document.createRange(); rg.selectNodeContents(el); return rg.getBoundingClientRect(); };
|
|
433
|
+
const inside = [...document.body.querySelectorAll('*')].filter(el =>
|
|
434
|
+
!isOurs(el) && (fits(el.getBoundingClientRect()) || (el.textContent.trim() && fits(contentRect(el)))));
|
|
435
|
+
const tops = inside.filter(el => !inside.some(o => o !== el && o.contains(el))).slice(0, MAX_ELS);
|
|
436
|
+
if (tops.length === 1) return startDraft({ kind: 'element', els: tops });
|
|
437
|
+
if (tops.length > 1) return startDraft({ kind: 'elements', els: tops });
|
|
438
|
+
// empty space → annotate the area, anchored to the smallest element containing it
|
|
439
|
+
let anchor = at((sel.left + sel.right) / 2, (sel.top + sel.bottom) / 2) || document.body;
|
|
440
|
+
while (anchor.parentElement && pageEl(anchor.parentElement)) {
|
|
441
|
+
const r = anchor.getBoundingClientRect();
|
|
442
|
+
if (r.left <= sel.left && r.right >= sel.right && r.top <= sel.top && r.bottom >= sel.bottom) break;
|
|
443
|
+
anchor = anchor.parentElement;
|
|
444
|
+
}
|
|
445
|
+
startDraft({ kind: 'area', els: [anchor], area: { x: sel.left + scrollX, y: sel.top + scrollY, width: sel.right - sel.left, height: sel.bottom - sel.top } });
|
|
446
|
+
}, true);
|
|
447
|
+
addEventListener('scroll', () => { hoverBox.style.display = 'none'; hideChip(); }, true);
|
|
448
|
+
|
|
449
|
+
document.addEventListener('keydown', e => {
|
|
450
|
+
if (e.altKey && e.shiftKey && e.code === 'KeyE') { e.preventDefault(); cancelDraft(); setPicking(!picking); }
|
|
451
|
+
else if (e.altKey && e.shiftKey && e.code === 'KeyP') { e.preventDefault(); setFrozen(!frozen); }
|
|
452
|
+
else if (e.key === 'Escape' && (picking || draft || tray.style.display === 'block')) {
|
|
453
|
+
if (draft) cancelDraft(); else if (picking) setPicking(false); else openSheet(null);
|
|
454
|
+
}
|
|
455
|
+
}, true);
|
|
456
|
+
|
|
457
|
+
// ---------------------------------------------------------------- text selection
|
|
458
|
+
let pendingText = null, selTimer = 0;
|
|
459
|
+
function hideChip() { chip.style.display = 'none'; pendingText = null; }
|
|
460
|
+
document.addEventListener('selectionchange', () => {
|
|
461
|
+
clearTimeout(selTimer);
|
|
462
|
+
selTimer = setTimeout(() => {
|
|
463
|
+
const s = getSelection();
|
|
464
|
+
if (picking || !s || s.isCollapsed || !s.rangeCount) return hideChip();
|
|
465
|
+
const range = s.getRangeAt(0);
|
|
466
|
+
let el = range.commonAncestorContainer;
|
|
467
|
+
if (el.nodeType !== 1) el = el.parentElement;
|
|
468
|
+
const text = s.toString().trim();
|
|
469
|
+
if (!pageEl(el) || !text || el.closest('input, textarea, [contenteditable]')) return hideChip();
|
|
470
|
+
pendingText = { el, text: text.slice(0, 300) };
|
|
471
|
+
const r = range.getBoundingClientRect();
|
|
472
|
+
chip.style.display = 'block';
|
|
473
|
+
chip.style.left = Math.max(8, Math.min(r.left, innerWidth - 130)) + 'px';
|
|
474
|
+
chip.style.top = Math.min(r.bottom + 8, innerHeight - 120) + 'px';
|
|
475
|
+
}, 120);
|
|
476
|
+
});
|
|
477
|
+
chip.addEventListener('pointerdown', e => e.preventDefault());
|
|
478
|
+
chip.addEventListener('click', () => {
|
|
479
|
+
if (!pendingText) return;
|
|
480
|
+
const d = { kind: 'text', els: [pendingText.el], text: pendingText.text };
|
|
481
|
+
hideChip();
|
|
482
|
+
getSelection()?.removeAllRanges();
|
|
483
|
+
startDraft(d);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// ---------------------------------------------------------------- markdown (copy) + send
|
|
487
|
+
function markdown(list) {
|
|
488
|
+
const out = [`## Page feedback (${list.length} note${list.length > 1 ? 's' : ''})`, `Viewport ${innerWidth}×${innerHeight}`, ''];
|
|
489
|
+
list.forEach((n, i) => {
|
|
490
|
+
out.push(`### ${i + 1}. ${describe(n)}${n.frozen ? ' (animations paused)' : ''}`);
|
|
491
|
+
out.push(`Page: ${n.path}`);
|
|
492
|
+
const hint = bestHint(n.elements[0].hints); if (hint) out.push(`Source: ${hint}`);
|
|
493
|
+
out.push(`Selector: ${n.elements.map(e => e.selector).join(' , ')}`);
|
|
494
|
+
if (n.kind === 'text') out.push(`Selected text: "${n.text}"`);
|
|
495
|
+
out.push(`Note: ${n.comment}`, '');
|
|
496
|
+
});
|
|
497
|
+
return out.join('\n');
|
|
498
|
+
}
|
|
499
|
+
tray.querySelector('.copy').onclick = async () => {
|
|
500
|
+
try { await navigator.clipboard.writeText(markdown(notes)); toast('📋 Copied', null, 2000); }
|
|
501
|
+
catch { toast('❌ Clipboard not available here'); }
|
|
502
|
+
};
|
|
503
|
+
tray.querySelector('.clear').onclick = () => { notes = []; save(); refresh(); renderMarks(); };
|
|
504
|
+
tray.querySelector('.send').onclick = send;
|
|
505
|
+
|
|
506
|
+
async function send() {
|
|
507
|
+
if (!notes.length) return;
|
|
508
|
+
const batch = notes.map(({ els, ...n }) => n);
|
|
509
|
+
try {
|
|
510
|
+
const r = await fetch(API + '/requests', { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
511
|
+
body: JSON.stringify({ notes: batch, viewport: { w: innerWidth, h: innerHeight } }) });
|
|
512
|
+
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || r.status);
|
|
513
|
+
notes = []; save();
|
|
514
|
+
cancelDraft(); openSheet(null); refresh(); renderMarks();
|
|
515
|
+
} catch (e) { toast(`❌ Send failed: ${e.message}`); }
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ---------------------------------------------------------------- status (SSE)
|
|
519
|
+
const state = new Map();
|
|
520
|
+
const autoKey = '__pe_autoreload';
|
|
521
|
+
const autoReload = () => { try { return localStorage.getItem(autoKey) === '1'; } catch { return false; } };
|
|
522
|
+
function renderBusy() {
|
|
523
|
+
const n = [...state.values()].filter(i => i.status === 'pending' || i.status === 'working').length;
|
|
524
|
+
const b = $('.busy'); b.textContent = `⏳${n}`; b.style.display = n ? 'inline' : 'none';
|
|
525
|
+
}
|
|
526
|
+
function onUpdate(it, fresh) {
|
|
527
|
+
const prev = state.get(it.id);
|
|
528
|
+
state.set(it.id, it);
|
|
529
|
+
renderBusy();
|
|
530
|
+
if (!fresh || prev?.status === it.status) return;
|
|
531
|
+
if (it.status === 'pending') toast(`📨 ${it.id} queued — ${it.label}`);
|
|
532
|
+
if (it.status === 'working') toast(`🛠 ${it.id} agent is on it`);
|
|
533
|
+
if (it.kind === 'locate') {
|
|
534
|
+
if (it.status === 'done' || it.status === 'failed') toast(`${it.status === 'done' ? '↗' : '❌'} ${it.message || (it.status === 'done' ? 'Opened' : 'Agent could not find the file')}`);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (it.status === 'done' || it.status === 'failed') {
|
|
538
|
+
if (it.status === 'done' && autoReload()) return location.reload();
|
|
539
|
+
const wrap = document.createElement('span');
|
|
540
|
+
wrap.style.cssText = 'display:flex;gap:6px;align-items:center;margin-left:auto';
|
|
541
|
+
const btn = document.createElement('button'); btn.textContent = 'Reload'; btn.onclick = () => location.reload();
|
|
542
|
+
const lab = document.createElement('label'); lab.className = 'auto';
|
|
543
|
+
const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = autoReload();
|
|
544
|
+
cb.onchange = () => { try { localStorage.setItem(autoKey, cb.checked ? '1' : '0'); } catch {} };
|
|
545
|
+
lab.append(cb, 'auto');
|
|
546
|
+
wrap.append(btn, lab);
|
|
547
|
+
toast(`${it.status === 'done' ? '✅' : '❌'} ${it.id}${it.message ? ': ' + it.message : ''}`, wrap);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
const es = new EventSource(API + '/events');
|
|
552
|
+
es.onmessage = e => {
|
|
553
|
+
const msg = JSON.parse(e.data);
|
|
554
|
+
if (msg.type === 'snapshot') for (const it of msg.items) onUpdate(it, false);
|
|
555
|
+
else if (msg.type === 'update') onUpdate(msg.item, true);
|
|
556
|
+
};
|
|
557
|
+
} catch {}
|
|
558
|
+
|
|
559
|
+
// ---------------------------------------------------------------- init (+ Turbo visits)
|
|
560
|
+
function init() { restore(); refresh(); renderMarks(); }
|
|
561
|
+
init();
|
|
562
|
+
document.addEventListener('turbo:load', init);
|
|
563
|
+
addEventListener('popstate', () => setTimeout(init, 0));
|
|
564
|
+
})();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Generated from src/ by `pnpm build` - do not edit.
|
|
3
|
+
|
|
4
|
+
// node_modules/.pnpm/axi-sdk-js@0.1.13/node_modules/axi-sdk-js/dist/fast-path.js
|
|
5
|
+
function tryFastPath(argv, options) {
|
|
6
|
+
if (argv.length !== 1) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const flag = argv[0];
|
|
10
|
+
if (flag !== "-v" && flag !== "-V" && flag !== "--version") {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
(options.stdout ?? process.stdout).write(`${options.version}
|
|
14
|
+
`);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// package.json
|
|
19
|
+
var version = "0.1.0";
|
|
20
|
+
|
|
21
|
+
// src/version.mjs
|
|
22
|
+
var VERSION = version;
|
|
23
|
+
|
|
24
|
+
// src/punchlist-axi.mjs
|
|
25
|
+
if (!tryFastPath(process.argv.slice(2), { version: VERSION })) await import("./punchlist-cli.mjs");
|