@delete101020/aidlc 4.0.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.
Files changed (93) hide show
  1. package/AUTOMATION.md +148 -0
  2. package/README.md +684 -0
  3. package/dist/assets/annotate-artifact.skill.md +121 -0
  4. package/dist/assets/epic-context.skill.md +55 -0
  5. package/dist/bundle.js +49470 -0
  6. package/dist/templates/ainative/agents/native-engineer.md +66 -0
  7. package/dist/templates/ainative/agents/native-operator.md +71 -0
  8. package/dist/templates/ainative/agents/native-originator.md +56 -0
  9. package/dist/templates/ainative/agents/native-product-owner.md +68 -0
  10. package/dist/templates/ainative/agents/native-reviewer.md +65 -0
  11. package/dist/templates/ainative/agents/native-verifier.md +58 -0
  12. package/dist/templates/ainative/artifacts/build-plan.md +56 -0
  13. package/dist/templates/ainative/artifacts/implement.md +55 -0
  14. package/dist/templates/ainative/artifacts/intent.md +50 -0
  15. package/dist/templates/ainative/artifacts/maintain.md +85 -0
  16. package/dist/templates/ainative/artifacts/review.md +68 -0
  17. package/dist/templates/ainative/artifacts/spec.md +69 -0
  18. package/dist/templates/ainative/artifacts/verify.md +55 -0
  19. package/dist/templates/ainative/skills/native-build-plan.md +74 -0
  20. package/dist/templates/ainative/skills/native-implement.md +62 -0
  21. package/dist/templates/ainative/skills/native-intent.md +50 -0
  22. package/dist/templates/ainative/skills/native-maintain.md +94 -0
  23. package/dist/templates/ainative/skills/native-review.md +82 -0
  24. package/dist/templates/ainative/skills/native-spec.md +83 -0
  25. package/dist/templates/ainative/skills/native-verify.md +60 -0
  26. package/dist/templates/sdlc/agents/designer.md +87 -0
  27. package/dist/templates/sdlc/agents/developer.md +178 -0
  28. package/dist/templates/sdlc/agents/example-with-skills.md +59 -0
  29. package/dist/templates/sdlc/agents/orchestrator.md +186 -0
  30. package/dist/templates/sdlc/agents/po.md +115 -0
  31. package/dist/templates/sdlc/agents/qa.md +141 -0
  32. package/dist/templates/sdlc/agents/tech-lead.md +124 -0
  33. package/dist/templates/sdlc/artifacts/design.md +130 -0
  34. package/dist/templates/sdlc/artifacts/execute-test.md +64 -0
  35. package/dist/templates/sdlc/artifacts/implement.backend.md +96 -0
  36. package/dist/templates/sdlc/artifacts/implement.md +70 -0
  37. package/dist/templates/sdlc/artifacts/implement.web-react.md +109 -0
  38. package/dist/templates/sdlc/artifacts/implement.web.md +99 -0
  39. package/dist/templates/sdlc/artifacts/plan.md +97 -0
  40. package/dist/templates/sdlc/artifacts/prototype.md +141 -0
  41. package/dist/templates/sdlc/artifacts/test-cases.md +47 -0
  42. package/dist/templates/sdlc/artifacts/test-plan.md +100 -0
  43. package/dist/templates/sdlc/artifacts/unit-test.md +32 -0
  44. package/dist/templates/sdlc/profiles/agile-lite.yaml +29 -0
  45. package/dist/templates/sdlc/profiles/hybrid.yaml +43 -0
  46. package/dist/templates/sdlc/profiles/iso-ieee.yaml +67 -0
  47. package/dist/templates/sdlc/profiles/none.yaml +21 -0
  48. package/dist/templates/sdlc/skills/advance-epic.md +77 -0
  49. package/dist/templates/sdlc/skills/analyze-requirements.md +279 -0
  50. package/dist/templates/sdlc/skills/discovery-gate.md +118 -0
  51. package/dist/templates/sdlc/skills/execute-test.md +74 -0
  52. package/dist/templates/sdlc/skills/generate-test-cases.md +77 -0
  53. package/dist/templates/sdlc/skills/implement.md +74 -0
  54. package/dist/templates/sdlc/skills/prd.md +114 -0
  55. package/dist/templates/sdlc/skills/prototype.md +128 -0
  56. package/dist/templates/sdlc/skills/tech-design.md +134 -0
  57. package/dist/templates/sdlc/skills/test-plan.md +115 -0
  58. package/dist/templates/sdlc/skills/test-report.md +38 -0
  59. package/dist/templates/sdlc/skills/unit-test.md +43 -0
  60. package/dist/templates/sdlc/validators/ci.mjs +49 -0
  61. package/dist/templates/sdlc/validators/traceability.mjs +244 -0
  62. package/dist/templates/speckit/agents/analyst.md +42 -0
  63. package/dist/templates/speckit/agents/speckit-developer.md +36 -0
  64. package/dist/templates/speckit/agents/speckit-qa.md +38 -0
  65. package/dist/templates/speckit/agents/speckit-tech-lead.md +40 -0
  66. package/dist/templates/speckit/artifacts/analyze.md +48 -0
  67. package/dist/templates/speckit/artifacts/clarify.md +64 -0
  68. package/dist/templates/speckit/artifacts/plan.md +57 -0
  69. package/dist/templates/speckit/artifacts/specify.md +64 -0
  70. package/dist/templates/speckit/artifacts/tasks.md +33 -0
  71. package/dist/templates/speckit/skills/analyze.md +50 -0
  72. package/dist/templates/speckit/skills/clarify.md +31 -0
  73. package/dist/templates/speckit/skills/plan.md +56 -0
  74. package/dist/templates/speckit/skills/specify.md +52 -0
  75. package/dist/templates/speckit/skills/speckit-implement.md +33 -0
  76. package/dist/templates/speckit/skills/tasks.md +38 -0
  77. package/dist/tools/epic-memory-hook.mjs +78 -0
  78. package/dist/tools/epic-memory.mjs +130 -0
  79. package/dist/tools/md-to-html.mjs +478 -0
  80. package/dist/tools/vendor/marked.esm.mjs +2432 -0
  81. package/dist/vendor/annotron/LICENSE +21 -0
  82. package/dist/vendor/annotron/README.md +213 -0
  83. package/dist/vendor/annotron/bin/annotron +467 -0
  84. package/dist/vendor/annotron/commands/annotron.md +11 -0
  85. package/dist/vendor/annotron/hooks/annotron-hook.sh +38 -0
  86. package/dist/vendor/annotron/hooks/hooks.json +50 -0
  87. package/dist/vendor/annotron/package.json +72 -0
  88. package/dist/vendor/annotron/skills/annotron-review/SKILL.md +86 -0
  89. package/dist/vendor/annotron/src/chrome.html +2029 -0
  90. package/dist/vendor/annotron/src/mdRender.js +208 -0
  91. package/dist/vendor/annotron/src/sdk.js +1042 -0
  92. package/dist/vendor/annotron/src/server.js +780 -0
  93. package/package.json +60 -0
@@ -0,0 +1,1042 @@
1
+ /**
2
+ * annotron SDK — injected into the artifact iframe at serve time only.
3
+ * Never written to disk. Communicates with the chrome via postMessage.
4
+ */
5
+ (function () {
6
+ const TAG = 'annotron-sdk';
7
+ let annotating = false;
8
+ console.log('[annotron-sdk] loaded');
9
+ let hovered = null;
10
+ let cardEl = null;
11
+ let overlayEl = null;
12
+ let annotationsOverlayEl = null;
13
+ let annotations = [];
14
+ let selectedAnnotationId = null;
15
+ let annotationRenderFrame = null;
16
+ let ignoreNextClick = false;
17
+ let commentPopupEl = null;
18
+ let textContextMenuEl = null;
19
+ let quickCommentBtnEl = null;
20
+ // Cached .md source + this iframe's file path — used to gate the inline
21
+ // "Edit" affordance (only offered when a selection maps to a single exact
22
+ // run in the source) and to compute the edit index sent to the server.
23
+ let mdSource = null;
24
+ let artifactFile = null;
25
+
26
+ // ── CSS path ───────────────────────────────────────────────────────────────
27
+ function cssPath(el) {
28
+ if (el.id) return '#' + CSS.escape(el.id);
29
+ const parts = [];
30
+ let node = el;
31
+ while (node && node !== document.body && node.nodeType === 1) {
32
+ const tag = node.tagName.toLowerCase();
33
+ const siblings = Array.from(node.parentElement?.children || []).filter(c => c.tagName === node.tagName);
34
+ if (siblings.length > 1) {
35
+ parts.unshift(`${tag}:nth-of-type(${siblings.indexOf(node) + 1})`);
36
+ } else {
37
+ parts.unshift(tag);
38
+ }
39
+ node = node.parentElement;
40
+ }
41
+ parts.unshift('body');
42
+ return parts.join(' > ');
43
+ }
44
+
45
+ function labelFor(el) {
46
+ const tag = el.tagName.toLowerCase();
47
+ const id = el.id ? `#${el.id}` : '';
48
+ const cls = el.className && typeof el.className === 'string'
49
+ ? '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.') : '';
50
+ const text = (el.textContent || '').trim().slice(0, 40);
51
+ return `<${tag}${id}${cls}>${text ? ' "' + text + '"' : ''}`;
52
+ }
53
+
54
+ function isInteractive(el) {
55
+ return !!(el && el.closest && el.closest(
56
+ 'button,input,select,textarea,label,summary,[contenteditable]:not([contenteditable="false"])'
57
+ ));
58
+ }
59
+
60
+ function isAnnotronEl(el) {
61
+ return !!(el && el.closest && el.closest('[data-annotron-ui]'));
62
+ }
63
+
64
+ // ── Overlay root (highlight layer) ────────────────────────────────────────
65
+ function ensureOverlay() {
66
+ if (overlayEl) return overlayEl;
67
+ overlayEl = document.createElement('div');
68
+ overlayEl.setAttribute('data-annotron-ui', 'overlay');
69
+ overlayEl.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:2147483645;';
70
+ document.documentElement.appendChild(overlayEl);
71
+ return overlayEl;
72
+ }
73
+
74
+ function ensureAnnotationsOverlay() {
75
+ if (annotationsOverlayEl) return annotationsOverlayEl;
76
+ annotationsOverlayEl = document.createElement('div');
77
+ annotationsOverlayEl.setAttribute('data-annotron-ui', 'annotations-overlay');
78
+ annotationsOverlayEl.style.cssText = 'position:fixed;inset:0;pointer-events:none;z-index:2147483644;';
79
+ document.documentElement.appendChild(annotationsOverlayEl);
80
+ return annotationsOverlayEl;
81
+ }
82
+
83
+ function clearHighlights() {
84
+ if (overlayEl) overlayEl.innerHTML = '';
85
+ }
86
+
87
+ function clearCommentPopup() {
88
+ if (commentPopupEl) {
89
+ commentPopupEl.remove();
90
+ commentPopupEl = null;
91
+ }
92
+ }
93
+
94
+ function clearTextContextMenu(clearHighlight = false) {
95
+ if (textContextMenuEl) {
96
+ textContextMenuEl.remove();
97
+ textContextMenuEl = null;
98
+ }
99
+ if (clearHighlight) {
100
+ clearHighlights();
101
+ }
102
+ }
103
+
104
+ function clearQuickCommentButton(clearHighlight = false) {
105
+ if (quickCommentBtnEl) {
106
+ quickCommentBtnEl.remove();
107
+ quickCommentBtnEl = null;
108
+ }
109
+ if (clearHighlight) {
110
+ clearHighlights();
111
+ }
112
+ }
113
+
114
+ function highlightTextRange(range) {
115
+ const overlay = ensureOverlay();
116
+ clearHighlights();
117
+ for (const rect of range.getClientRects()) {
118
+ if (rect.width <= 0 || rect.height <= 0) continue;
119
+ const m = document.createElement('div');
120
+ m.style.cssText = `position:absolute;left:${rect.left}px;top:${rect.top}px;width:${rect.width}px;height:${rect.height}px;background:rgba(39,65,241,.11);border-bottom:1.5px solid rgba(39,65,241,.45);border-radius:2px;pointer-events:none;`;
121
+ overlay.appendChild(m);
122
+ }
123
+ }
124
+
125
+ function highlightElement(el) {
126
+ const overlay = ensureOverlay();
127
+ clearHighlights();
128
+ const r = el.getBoundingClientRect();
129
+ const m = document.createElement('div');
130
+ m.style.cssText = `position:absolute;left:${r.left}px;top:${r.top}px;width:${r.width}px;height:${r.height}px;outline:2px solid #2741F1;outline-offset:2px;border-radius:3px;pointer-events:none;`;
131
+ overlay.appendChild(m);
132
+ }
133
+
134
+ function textOffsetWithin(root, container, offset) {
135
+ const range = document.createRange();
136
+ range.selectNodeContents(root);
137
+ try {
138
+ range.setEnd(container, offset);
139
+ return range.toString().length;
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ function textPositionAt(root, offset) {
146
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
147
+ let remaining = Math.max(0, offset);
148
+ let node;
149
+ let last = null;
150
+ while ((node = walker.nextNode())) {
151
+ last = node;
152
+ if (remaining <= node.data.length) return { node, offset: remaining };
153
+ remaining -= node.data.length;
154
+ }
155
+ return last ? { node: last, offset: last.data.length } : null;
156
+ }
157
+
158
+ function rangeForAnnotation(annotation, root) {
159
+ const fullText = root.textContent || '';
160
+ let start = Number.isInteger(annotation.textStart) ? annotation.textStart : -1;
161
+ let end = Number.isInteger(annotation.textEnd) ? annotation.textEnd : -1;
162
+ const quote = annotation.text || '';
163
+ if (start < 0 || end < start || fullText.slice(start, end) !== quote) {
164
+ const expected = start >= 0 ? start : 0;
165
+ const matches = [];
166
+ let index = fullText.indexOf(quote);
167
+ while (quote && index !== -1) {
168
+ matches.push(index);
169
+ index = fullText.indexOf(quote, index + 1);
170
+ }
171
+ if (!matches.length) return null;
172
+ const prefix = annotation.textPrefix || '';
173
+ const suffix = annotation.textSuffix || '';
174
+ const contextual = matches.filter(i =>
175
+ (!prefix || fullText.slice(Math.max(0, i - prefix.length), i) === prefix) &&
176
+ (!suffix || fullText.slice(i + quote.length, i + quote.length + suffix.length) === suffix)
177
+ );
178
+ const candidates = contextual.length ? contextual : matches;
179
+ start = candidates.reduce((best, value) =>
180
+ Math.abs(value - expected) < Math.abs(best - expected) ? value : best
181
+ );
182
+ end = start + quote.length;
183
+ }
184
+ const from = textPositionAt(root, start);
185
+ const to = textPositionAt(root, end);
186
+ if (!from || !to) return null;
187
+ const range = document.createRange();
188
+ range.setStart(from.node, from.offset);
189
+ range.setEnd(to.node, to.offset);
190
+ return range;
191
+ }
192
+
193
+ function extractSelectedTextData() {
194
+ const sel = window.getSelection();
195
+ if (!sel || sel.isCollapsed) return null;
196
+ const rawText = sel.toString();
197
+ const text = rawText.trim();
198
+ if (!text) return null;
199
+ const range = sel.getRangeAt(0);
200
+ const anchorEl = range.commonAncestorContainer.nodeType === 3
201
+ ? range.commonAncestorContainer.parentElement
202
+ : range.commonAncestorContainer;
203
+ if (!anchorEl || isAnnotronEl(anchorEl)) return null;
204
+ const anchorRect = range.getBoundingClientRect();
205
+ const clonedRange = range.cloneRange();
206
+ const selector = cssPath(anchorEl);
207
+ const rawStart = textOffsetWithin(anchorEl, range.startContainer, range.startOffset);
208
+ const textStart = rawStart === null ? null : rawStart + rawText.indexOf(text);
209
+ const textEnd = textStart === null ? null : textStart + text.length;
210
+ const anchorText = anchorEl.textContent || '';
211
+ const textPrefix = textStart === null ? '' : anchorText.slice(Math.max(0, textStart - 24), textStart);
212
+ const textSuffix = textEnd === null ? '' : anchorText.slice(textEnd, textEnd + 24);
213
+ const label = `"${text.slice(0, 60)}"`;
214
+ return {
215
+ selector,
216
+ text,
217
+ label,
218
+ textStart,
219
+ textEnd,
220
+ textPrefix,
221
+ textSuffix,
222
+ range: clonedRange,
223
+ anchorRect,
224
+ };
225
+ }
226
+
227
+ // Open the inline "New comment" composer anchored right under the selection.
228
+ // On submit it hands the note to the chrome, which stores the annotation.
229
+ function openTextCommentCard(selected) {
230
+ if (!selected) return;
231
+ clearTextContextMenu();
232
+ clearQuickCommentButton();
233
+ showCard({
234
+ quote: selected.text,
235
+ placeholder: 'Add your comment…',
236
+ anchorRect: selected.anchorRect,
237
+ onAdd: note => {
238
+ window.parent.postMessage({
239
+ [TAG]: true,
240
+ type: 'text-selected',
241
+ selector: selected.selector,
242
+ text: selected.text,
243
+ label: selected.label,
244
+ note,
245
+ textStart: selected.textStart,
246
+ textEnd: selected.textEnd,
247
+ textPrefix: selected.textPrefix,
248
+ textSuffix: selected.textSuffix,
249
+ }, '*');
250
+ try { const s = window.getSelection(); s && s.removeAllRanges(); } catch {}
251
+ },
252
+ });
253
+ }
254
+
255
+ // One pill button (Comment / Edit) inside the floating selection toolbar.
256
+ function makeQuickPill(label, svg, title, onActivate) {
257
+ const btn = document.createElement('button');
258
+ btn.type = 'button';
259
+ btn.setAttribute('data-annotron-ui', 'quick-comment-button');
260
+ btn.title = title;
261
+ btn.innerHTML = svg + `<span>${label}</span>`;
262
+ btn.style.cssText = [
263
+ 'height:34px', 'padding:0 13px', 'border:none', 'border-radius:9px',
264
+ 'background:#1B1A3D', 'color:#fff',
265
+ "font:600 12.5px 'Inter',system-ui,-apple-system,sans-serif",
266
+ 'display:inline-flex', 'align-items:center', 'gap:6px', 'cursor:pointer',
267
+ 'box-shadow:0 8px 22px rgba(27,26,61,.34)', 'pointer-events:auto',
268
+ ].join(';');
269
+ const activate = e => { e.preventDefault(); e.stopPropagation(); ignoreNextClick = true; onActivate(); };
270
+ // pointerdown so selectionchange doesn't drop the button before click fires.
271
+ btn.addEventListener('pointerdown', activate);
272
+ btn.addEventListener('click', activate);
273
+ btn.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') activate(e); });
274
+ return btn;
275
+ }
276
+
277
+ // Fetch + cache the .md source so the client can decide whether a selection
278
+ // is safely editable. Called at load; the iframe fully reloads (and re-runs
279
+ // this SDK) whenever the file changes, so the cache stays fresh.
280
+ async function initMdSource() {
281
+ try {
282
+ artifactFile = new URLSearchParams(location.search).get('file');
283
+ if (!artifactFile || !/\.md$/i.test(artifactFile)) return; // markdown only
284
+ const res = await fetch('/source?file=' + encodeURIComponent(artifactFile));
285
+ if (!res.ok) return;
286
+ const d = await res.json();
287
+ mdSource = typeof d.markdown === 'string' ? d.markdown : null;
288
+ } catch { mdSource = null; }
289
+ }
290
+
291
+ // Locate the selected text in the .md source. Returns a single index when the
292
+ // selection maps to exactly one run (unique, or disambiguated by the
293
+ // surrounding rendered context) — otherwise -1, which hides the Edit button.
294
+ function findEditIndex(selected) {
295
+ if (!mdSource || !selected || !selected.text) return -1;
296
+ const t = selected.text;
297
+ const idxs = [];
298
+ for (let i = mdSource.indexOf(t); i !== -1 && idxs.length <= 50; i = mdSource.indexOf(t, i + 1)) idxs.push(i);
299
+ if (idxs.length === 0) return -1;
300
+ if (idxs.length === 1) return idxs[0];
301
+ // Multiple matches: disambiguate with the rendered prefix/suffix context.
302
+ const pre = (selected.textPrefix || '').trim().slice(-12);
303
+ const suf = (selected.textSuffix || '').trim().slice(0, 12);
304
+ if (!pre && !suf) return -1;
305
+ const hits = idxs.filter(idx => {
306
+ const before = mdSource.slice(Math.max(0, idx - 80), idx);
307
+ const after = mdSource.slice(idx + t.length, idx + t.length + 80);
308
+ return (!pre || before.includes(pre)) && (!suf || after.includes(suf));
309
+ });
310
+ return hits.length === 1 ? hits[0] : -1;
311
+ }
312
+
313
+ // Inline edit card: an input pre-filled with the selected text, anchored at
314
+ // the selection. Save writes the new text (or Delete = empty) straight to the
315
+ // .md via the chrome → /edit-text. No agent.
316
+ function openTextEditCard(selected, index) {
317
+ clearCard();
318
+ cardEl = document.createElement('div');
319
+ cardEl.setAttribute('data-annotron-ui', 'card');
320
+ cardEl.style.cssText = CARD_STYLE;
321
+
322
+ const hdg = document.createElement('div');
323
+ hdg.style.cssText = `display:flex;align-items:center;gap:6px;font:600 11px ${FONT};letter-spacing:.09em;text-transform:uppercase;color:#2741F1;margin-bottom:9px;`;
324
+ hdg.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg><span>Edit text</span>';
325
+
326
+ const ta = document.createElement('textarea');
327
+ ta.value = selected.text;
328
+ ta.style.cssText = `width:100%;min-height:66px;resize:vertical;border-radius:10px;border:1px solid rgba(27,26,61,.14);background:#fff;color:#1B1A3D;padding:10px 11px;font:400 13.5px/1.5 ${FONT};box-sizing:border-box;outline:none;`;
329
+ ta.addEventListener('focus', () => { ta.style.borderColor = '#2741F1'; ta.style.boxShadow = '0 0 0 3px rgba(39,65,241,.12)'; });
330
+ ta.addEventListener('blur', () => { ta.style.borderColor = 'rgba(27,26,61,.14)'; ta.style.boxShadow = 'none'; });
331
+
332
+ const row = document.createElement('div');
333
+ row.style.cssText = 'display:flex;gap:8px;align-items:center;margin-top:10px;';
334
+ const btnDelete = document.createElement('button');
335
+ btnDelete.textContent = 'Delete';
336
+ btnDelete.style.cssText = `height:32px;padding:0 13px;border-radius:8px;border:1px solid rgba(216,134,137,.5);background:#fff;color:#b4484c;font:600 12.5px ${FONT};cursor:pointer;margin-right:auto;`;
337
+ const btnCancel = document.createElement('button');
338
+ btnCancel.textContent = 'Cancel';
339
+ btnCancel.style.cssText = `height:32px;padding:0 13px;border-radius:8px;border:1px solid rgba(27,26,61,.14);background:#fff;color:rgba(27,26,61,.7);font:600 12.5px ${FONT};cursor:pointer;`;
340
+ const btnSave = document.createElement('button');
341
+ btnSave.textContent = 'Save';
342
+ btnSave.style.cssText = `height:32px;padding:0 15px;border-radius:8px;border:none;background:#2741F1;color:#fff;font:600 12.5px ${FONT};cursor:pointer;`;
343
+ row.append(btnDelete, btnCancel, btnSave);
344
+
345
+ cardEl.append(hdg, ta, row);
346
+ document.documentElement.appendChild(cardEl);
347
+ positionCard(selected.anchorRect);
348
+
349
+ const apply = (newText) => {
350
+ window.parent.postMessage({
351
+ [TAG]: true, type: 'text-edit',
352
+ file: artifactFile, oldText: selected.text, newText, index,
353
+ }, '*');
354
+ // Optimistically patch the local cache so a follow-up edit stays aligned
355
+ // (the iframe also reloads once the .md write lands, re-seeding it).
356
+ if (mdSource != null && index >= 0) {
357
+ mdSource = mdSource.slice(0, index) + newText + mdSource.slice(index + selected.text.length);
358
+ }
359
+ try { const s = window.getSelection(); s && s.removeAllRanges(); } catch {}
360
+ dismissCard();
361
+ };
362
+
363
+ btnCancel.addEventListener('click', e => { e.stopPropagation(); dismissCard(); });
364
+ btnSave.addEventListener('click', e => { e.stopPropagation(); apply(ta.value); });
365
+ btnDelete.addEventListener('click', e => { e.stopPropagation(); apply(''); });
366
+ ta.addEventListener('keydown', e => {
367
+ if (e.key === 'Escape') { e.stopPropagation(); dismissCard(); }
368
+ if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); apply(ta.value); }
369
+ });
370
+ setTimeout(() => { ta.focus(); ta.select(); }, 0);
371
+ }
372
+
373
+ function showQuickCommentButton(selected) {
374
+ clearQuickCommentButton();
375
+ const COMMENT_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
376
+ const EDIT_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>';
377
+
378
+ const wrap = document.createElement('div');
379
+ wrap.setAttribute('data-annotron-ui', 'quick-comment-button');
380
+ wrap.style.cssText = 'position:fixed;z-index:2147483647;display:inline-flex;gap:6px;pointer-events:auto;';
381
+
382
+ wrap.appendChild(makeQuickPill('Comment', COMMENT_SVG, 'Comment on selected text', () => {
383
+ clearTextContextMenu(); clearQuickCommentButton(); openTextCommentCard(selected);
384
+ }));
385
+
386
+ // "Edit" — direct .md edit. Shown ONLY when the selection maps to a single
387
+ // exact run in the Markdown source, so we never offer an edit we can't
388
+ // safely apply (no error message — the button just isn't there).
389
+ const editIndex = findEditIndex(selected);
390
+ if (editIndex >= 0) {
391
+ wrap.appendChild(makeQuickPill('Edit', EDIT_SVG, 'Edit selected text directly', () => {
392
+ clearTextContextMenu(); clearQuickCommentButton(); openTextEditCard(selected, editIndex);
393
+ }));
394
+ }
395
+
396
+ const rect = selected.anchorRect;
397
+ const width = editIndex >= 0 ? 168 : 96;
398
+ const left = Math.max(8, Math.min(window.innerWidth - width - 8, rect.left + rect.width / 2 - width / 2));
399
+ const top = Math.max(8, Math.min(window.innerHeight - 42, rect.top - 42));
400
+ wrap.style.left = `${left}px`;
401
+ wrap.style.top = `${top}px`;
402
+
403
+ document.documentElement.appendChild(wrap);
404
+ quickCommentBtnEl = wrap;
405
+ }
406
+
407
+ function showTextContextMenu(x, y, onComment) {
408
+ clearTextContextMenu();
409
+ const menu = document.createElement('div');
410
+ menu.setAttribute('data-annotron-ui', 'text-context-menu');
411
+ menu.setAttribute('role', 'menu');
412
+ menu.style.cssText = [
413
+ 'position:fixed',
414
+ 'z-index:2147483647',
415
+ 'min-width:168px',
416
+ 'background:#fff',
417
+ 'color:#1B1A3D',
418
+ 'border:1px solid rgba(27,26,61,.1)',
419
+ 'border-radius:12px',
420
+ 'padding:5px',
421
+ 'box-shadow:0 12px 34px rgba(27,26,61,.2)',
422
+ 'pointer-events:auto',
423
+ ].join(';');
424
+
425
+ const item = document.createElement('button');
426
+ item.type = 'button';
427
+ item.innerHTML = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#2741F1" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg><span>Comment</span>';
428
+ item.setAttribute('role', 'menuitem');
429
+ item.style.cssText = [
430
+ 'display:flex',
431
+ 'align-items:center',
432
+ 'gap:9px',
433
+ 'width:100%',
434
+ 'text-align:left',
435
+ 'border:0',
436
+ 'border-radius:8px',
437
+ 'padding:8px 12px',
438
+ 'background:#fff',
439
+ 'color:#1B1A3D',
440
+ 'cursor:pointer',
441
+ "font:500 13px 'Inter',system-ui,-apple-system,sans-serif",
442
+ ].join(';');
443
+ item.addEventListener('mouseenter', () => { item.style.background = '#F3F3F8'; });
444
+ item.addEventListener('mouseleave', () => { item.style.background = '#fff'; });
445
+ item.addEventListener('click', e => {
446
+ e.preventDefault();
447
+ e.stopPropagation();
448
+ clearTextContextMenu();
449
+ onComment();
450
+ });
451
+
452
+ menu.appendChild(item);
453
+ document.documentElement.appendChild(menu);
454
+ textContextMenuEl = menu;
455
+
456
+ const bounds = menu.getBoundingClientRect();
457
+ const left = Math.max(8, Math.min(window.innerWidth - bounds.width - 8, x));
458
+ const top = Math.max(8, Math.min(window.innerHeight - bounds.height - 8, y));
459
+ menu.style.left = `${left}px`;
460
+ menu.style.top = `${top}px`;
461
+ setTimeout(() => item.focus(), 0);
462
+ }
463
+
464
+ function makeSnippet(text, max = 140) {
465
+ if (!text) return '';
466
+ const t = String(text).trim().replace(/\s+/g, ' ');
467
+ if (t.length <= max) return t;
468
+ return t.slice(0, max - 1) + '…';
469
+ }
470
+
471
+ function latestMessage(annotation) {
472
+ const lastThread = annotation.thread?.length
473
+ ? annotation.thread[annotation.thread.length - 1].message
474
+ : '';
475
+ return lastThread || annotation.note || annotation.text || '';
476
+ }
477
+
478
+ // ── Hover preview popup (shows a comment's content on highlight hover) ──────
479
+ let hoverPopupEl = null;
480
+ let hoverHideTimer = null;
481
+ const HOVER_FONT = "'Inter',system-ui,-apple-system,sans-serif";
482
+
483
+ function clearHoverPopup() {
484
+ clearTimeout(hoverHideTimer);
485
+ if (hoverPopupEl) { hoverPopupEl.remove(); hoverPopupEl = null; }
486
+ }
487
+
488
+ function showHoverPopup(annotation, rect) {
489
+ clearTimeout(hoverHideTimer);
490
+ clearHoverPopup();
491
+ const msg = latestMessage(annotation);
492
+ if (!msg) return;
493
+ const resolved = annotation.status === 'resolved';
494
+ const accent = resolved ? '#1c8a4e' : '#2741F1';
495
+ const pop = document.createElement('div');
496
+ pop.setAttribute('data-annotron-ui', 'hover-popup');
497
+ pop.style.cssText = [
498
+ 'position:fixed', 'z-index:2147483647', 'pointer-events:none',
499
+ 'width:max-content', 'max-width:300px',
500
+ 'background:#fff', 'color:#1B1A3D',
501
+ 'border:1px solid rgba(27,26,61,.1)', 'border-radius:12px',
502
+ 'padding:10px 12px', 'box-shadow:0 10px 30px rgba(27,26,61,.18)',
503
+ `font:12px/1.5 ${HOVER_FONT}`, 'animation:none',
504
+ ].join(';');
505
+
506
+ const title = document.createElement('div');
507
+ title.style.cssText = `display:flex;align-items:center;gap:5px;font:600 10px ${HOVER_FONT};letter-spacing:.09em;text-transform:uppercase;color:${accent};margin-bottom:5px;`;
508
+ title.innerHTML = `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg><span>${resolved ? 'Resolved' : (annotation.kind === 'text' ? 'Text comment' : 'Element comment')}</span>`;
509
+
510
+ const body = document.createElement('div');
511
+ body.style.cssText = 'color:rgba(27,26,61,.82);white-space:pre-wrap;word-break:break-word;';
512
+ body.textContent = makeSnippet(msg, 240);
513
+
514
+ const foot = document.createElement('div');
515
+ const replies = annotation.thread?.length || 0;
516
+ foot.style.cssText = `margin-top:6px;font:500 11px ${HOVER_FONT};color:rgba(27,26,61,.45);`;
517
+ foot.textContent = replies > 1 ? `${replies} messages · click to open` : 'Click to open';
518
+
519
+ pop.append(title, body, foot);
520
+ document.documentElement.appendChild(pop);
521
+
522
+ const vw = window.innerWidth, vh = window.innerHeight;
523
+ const b = pop.getBoundingClientRect();
524
+ let left = Math.min(vw - b.width - 10, Math.max(10, rect.left));
525
+ let top = rect.bottom + 8;
526
+ if (top + b.height > vh - 10) top = Math.max(10, rect.top - b.height - 8);
527
+ pop.style.left = `${left}px`;
528
+ pop.style.top = `${top}px`;
529
+ hoverPopupEl = pop;
530
+ }
531
+
532
+ function showCommentPopup(annotation, anchorRect) {
533
+ clearCommentPopup();
534
+ const popup = document.createElement('div');
535
+ popup.setAttribute('data-annotron-ui', 'comment-popup');
536
+ popup.setAttribute('role', 'dialog');
537
+ popup.setAttribute('aria-label', 'Annotation comment');
538
+ const resolved = annotation.status === 'resolved';
539
+ popup.style.cssText = [
540
+ 'position:fixed',
541
+ 'z-index:2147483647',
542
+ 'width:min(320px,calc(100vw - 24px))',
543
+ 'background:#fff',
544
+ 'color:#1B1A3D',
545
+ 'border:1px solid rgba(27,26,61,.1)',
546
+ 'border-radius:14px',
547
+ 'padding:13px',
548
+ 'box-shadow:0 12px 34px rgba(27,26,61,.2)',
549
+ "font:12px/1.45 'Inter',system-ui,-apple-system,sans-serif",
550
+ 'display:flex',
551
+ 'flex-direction:column',
552
+ 'gap:9px',
553
+ 'pointer-events:auto',
554
+ ].join(';');
555
+
556
+ const title = document.createElement('div');
557
+ title.style.cssText = `font-size:11px;font-weight:600;color:${resolved ? '#1c8a4e' : '#2741F1'};text-transform:uppercase;letter-spacing:.09em;`;
558
+ title.textContent = resolved ? 'Resolved' : (annotation.kind === 'text' ? 'Text comment' : 'Element comment');
559
+
560
+ const label = document.createElement('div');
561
+ label.style.cssText = 'font-size:12px;font-weight:600;color:#1B1A3D;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
562
+ label.textContent = annotation.label || '(annotation)';
563
+
564
+ const body = document.createElement('div');
565
+ body.style.cssText = 'font-size:13px;color:rgba(27,26,61,.82);white-space:pre-wrap;word-break:break-word;';
566
+ body.textContent = makeSnippet(latestMessage(annotation));
567
+
568
+ const row = document.createElement('div');
569
+ row.style.cssText = 'display:flex;justify-content:flex-end;gap:8px;';
570
+
571
+ const close = document.createElement('button');
572
+ close.type = 'button';
573
+ close.textContent = 'Close';
574
+ close.style.cssText = "border:1px solid rgba(27,26,61,.14);border-radius:8px;padding:6px 12px;background:#fff;color:rgba(27,26,61,.7);cursor:pointer;font:600 11.5px 'Inter',system-ui,sans-serif;";
575
+
576
+ const open = document.createElement('button');
577
+ open.type = 'button';
578
+ open.textContent = 'Open thread';
579
+ open.style.cssText = "border:0;border-radius:8px;padding:6px 13px;background:#2741F1;color:#fff;cursor:pointer;font:600 11.5px 'Inter',system-ui,sans-serif;";
580
+
581
+ close.addEventListener('click', () => clearCommentPopup());
582
+ open.addEventListener('click', () => {
583
+ window.parent.postMessage({ [TAG]: true, type: 'annotation-selected', id: annotation.id }, '*');
584
+ clearCommentPopup();
585
+ });
586
+ popup.addEventListener('keydown', event => {
587
+ if (event.key === 'Escape') {
588
+ event.stopPropagation();
589
+ clearCommentPopup();
590
+ }
591
+ });
592
+
593
+ row.append(close, open);
594
+ popup.append(title, label, body, row);
595
+ document.documentElement.appendChild(popup);
596
+
597
+ const vw = window.innerWidth;
598
+ const vh = window.innerHeight;
599
+ const bounds = popup.getBoundingClientRect();
600
+ let left = Math.min(vw - bounds.width - 10, anchorRect.left);
601
+ left = Math.max(10, left);
602
+ let top = anchorRect.bottom + 10;
603
+ if (top + bounds.height > vh - 10) top = Math.max(10, anchorRect.top - bounds.height - 10);
604
+ popup.style.left = `${left}px`;
605
+ popup.style.top = `${top}px`;
606
+ commentPopupEl = popup;
607
+ setTimeout(() => open.focus(), 0);
608
+ }
609
+
610
+ function addAnnotationButton(overlay, annotation, rect) {
611
+ const button = document.createElement('button');
612
+ button.type = 'button';
613
+ button.setAttribute('data-annotron-ui', 'annotation-button');
614
+ button.setAttribute('aria-label', `Open comment for ${annotation.label || annotation.text || annotation.kind}`);
615
+ button.title = annotation.thread?.at(-1)?.message || annotation.note || 'Open comment';
616
+ const selected = annotation.id === selectedAnnotationId;
617
+ const resolved = annotation.status === 'resolved';
618
+ const bg = resolved ? '#2fbf71' : (selected ? '#1B1A3D' : '#2741F1');
619
+ button.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>';
620
+ button.style.cssText = [
621
+ 'position:absolute',
622
+ `left:${Math.max(2, Math.min(window.innerWidth - 24, rect.right - 10))}px`,
623
+ `top:${Math.max(2, rect.top - 10)}px`,
624
+ 'width:22px', 'height:22px', 'padding:0',
625
+ 'display:grid', 'place-items:center',
626
+ 'border-radius:8px 8px 8px 2px',
627
+ 'border:none',
628
+ `background:${bg}`,
629
+ 'color:#fff',
630
+ 'box-shadow:0 3px 10px rgba(27,26,61,.32)',
631
+ 'cursor:pointer', 'pointer-events:auto',
632
+ ].join(';');
633
+ button.addEventListener('click', e => {
634
+ e.preventDefault();
635
+ e.stopPropagation();
636
+ selectedAnnotationId = annotation.id;
637
+ renderAnnotationsOverlay();
638
+ showCommentPopup(annotation, rect);
639
+ });
640
+ overlay.appendChild(button);
641
+ }
642
+
643
+ function renderAnnotationsOverlay() {
644
+ annotationRenderFrame = null;
645
+ const overlay = ensureAnnotationsOverlay();
646
+ overlay.innerHTML = '';
647
+ clearCommentPopup();
648
+ clearHoverPopup();
649
+ for (const annotation of annotations) {
650
+ if (!annotation.selector) continue;
651
+ let target;
652
+ try { target = document.querySelector(annotation.selector); } catch { continue; }
653
+ if (!target) continue;
654
+ let rects = [];
655
+ if (annotation.kind === 'text' && annotation.text) {
656
+ const range = rangeForAnnotation(annotation, target);
657
+ if (range) rects = Array.from(range.getClientRects()).filter(r => r.width > 0 && r.height > 0);
658
+ }
659
+ if (!rects.length) {
660
+ const rect = target.getBoundingClientRect();
661
+ if (rect.width > 0 && rect.height > 0) rects = [rect];
662
+ }
663
+ if (!rects.length) continue;
664
+ const selected = annotation.id === selectedAnnotationId;
665
+ const resolved = annotation.status === 'resolved';
666
+ // Subtle, design-matched palette: faint tint + a thin underline for text,
667
+ // a soft outline for elements. Selected only deepens slightly.
668
+ const tint = resolved ? '.13' : (selected ? '.18' : '.11');
669
+ const line = resolved ? '.4' : (selected ? '.7' : '.45');
670
+ const rgb = resolved ? '47,191,113' : '39,65,241';
671
+ const fill = `rgba(${rgb},${tint})`;
672
+ const underline = `rgba(${rgb},${line})`;
673
+ rects.forEach((rect, i) => {
674
+ const mark = document.createElement('div');
675
+ mark.setAttribute('data-annotron-ui', 'annotation-mark');
676
+ mark.style.cssText = annotation.kind === 'text'
677
+ ? `position:absolute;left:${rect.left}px;top:${rect.top}px;width:${rect.width}px;height:${rect.height}px;background:${fill};border-bottom:1.5px solid ${underline};border-radius:2px;cursor:pointer;pointer-events:auto;transition:background .12s;`
678
+ : `position:absolute;left:${rect.left}px;top:${rect.top}px;width:${rect.width}px;height:${rect.height}px;outline:1.5px solid ${underline};outline-offset:2px;border-radius:3px;background:${fill};cursor:pointer;pointer-events:auto;transition:background .12s;`;
679
+ mark.addEventListener('mouseenter', () => {
680
+ mark.style.background = `rgba(${rgb},.2)`;
681
+ showHoverPopup(annotation, mark.getBoundingClientRect());
682
+ });
683
+ mark.addEventListener('mouseleave', () => {
684
+ mark.style.background = fill;
685
+ hoverHideTimer = setTimeout(clearHoverPopup, 120);
686
+ });
687
+ mark.addEventListener('click', e => {
688
+ e.preventDefault();
689
+ e.stopPropagation();
690
+ clearHoverPopup();
691
+ selectedAnnotationId = annotation.id;
692
+ renderAnnotationsOverlay();
693
+ window.parent.postMessage({ [TAG]: true, type: 'annotation-selected', id: annotation.id }, '*');
694
+ });
695
+ overlay.appendChild(mark);
696
+ });
697
+ }
698
+ }
699
+
700
+ function scheduleAnnotationsRender() {
701
+ if (annotationRenderFrame !== null) return;
702
+ annotationRenderFrame = requestAnimationFrame(renderAnnotationsOverlay);
703
+ }
704
+
705
+ window.addEventListener('resize', scheduleAnnotationsRender);
706
+ window.addEventListener('load', scheduleAnnotationsRender);
707
+ document.addEventListener('scroll', scheduleAnnotationsRender, true);
708
+ document.addEventListener('load', scheduleAnnotationsRender, true);
709
+
710
+ // ── Inline composer card ────────────────────────────────────────────────
711
+ const FONT = "'Inter',system-ui,-apple-system,sans-serif";
712
+ const CARD_STYLE = [
713
+ 'position:fixed',
714
+ 'z-index:2147483646',
715
+ 'width:min(340px,calc(100vw - 24px))',
716
+ 'padding:13px',
717
+ 'border-radius:14px',
718
+ 'background:#fff',
719
+ 'color:#1B1A3D',
720
+ 'border:1px solid rgba(39,65,241,.3)',
721
+ 'box-shadow:0 6px 20px rgba(27,26,61,.16)',
722
+ `font:13px/1.45 ${FONT}`,
723
+ 'display:flex',
724
+ 'flex-direction:column',
725
+ 'gap:0',
726
+ ].join(';');
727
+
728
+ function clearCard() {
729
+ if (cardEl) { cardEl.remove(); cardEl = null; }
730
+ }
731
+
732
+ function dismissCard() {
733
+ clearCard();
734
+ clearHighlights();
735
+ clearTextContextMenu();
736
+ clearQuickCommentButton();
737
+ clearCommentPopup();
738
+ if (hovered) {
739
+ hovered.style.outline = '';
740
+ hovered.style.outlineOffset = '';
741
+ hovered.style.cursor = '';
742
+ hovered = null;
743
+ }
744
+ }
745
+
746
+ function positionCard(anchorRect) {
747
+ const vw = window.innerWidth, vh = window.innerHeight;
748
+ const cw = cardEl.offsetWidth || 340, ch = cardEl.offsetHeight || 200;
749
+ let left = Math.max(12, Math.min(anchorRect.left, vw - cw - 12));
750
+ let top = anchorRect.bottom + 10;
751
+ if (top + ch > vh - 12) top = Math.max(12, anchorRect.top - ch - 10);
752
+ cardEl.style.left = left + 'px';
753
+ cardEl.style.top = top + 'px';
754
+ }
755
+
756
+ function el(tag, styles, text) {
757
+ const e = document.createElement(tag);
758
+ if (styles) e.style.cssText = styles;
759
+ if (text !== undefined) e.textContent = text;
760
+ return e;
761
+ }
762
+
763
+ function showCard({ quote, placeholder, anchorRect, onAdd }) {
764
+ clearCard();
765
+
766
+ cardEl = document.createElement('div');
767
+ cardEl.setAttribute('data-annotron-ui', 'card');
768
+ cardEl.style.cssText = CARD_STYLE;
769
+
770
+ const hdg = document.createElement('div');
771
+ hdg.style.cssText = `display:flex;align-items:center;gap:6px;font:600 11px ${FONT};letter-spacing:.09em;text-transform:uppercase;color:#2741F1;margin-bottom:9px;`;
772
+ hdg.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg><span>New comment</span>';
773
+
774
+ let quoteEl = null;
775
+ if (quote) {
776
+ quoteEl = document.createElement('div');
777
+ quoteEl.style.cssText = `border-left:3px solid #D88689;padding:2px 0 2px 10px;font:italic 400 12.5px/1.5 ${FONT};color:rgba(27,26,61,.6);margin-bottom:11px;max-height:66px;overflow:hidden;`;
778
+ quoteEl.textContent = '"' + quote + '"';
779
+ }
780
+
781
+ const ta = document.createElement('textarea');
782
+ ta.placeholder = placeholder || 'Add your comment…';
783
+ ta.style.cssText = `width:100%;min-height:66px;resize:vertical;border-radius:10px;border:1px solid rgba(27,26,61,.14);background:#fff;color:#1B1A3D;padding:10px 11px;font:400 13.5px/1.5 ${FONT};box-sizing:border-box;outline:none;`;
784
+ ta.addEventListener('focus', () => { ta.style.borderColor = '#2741F1'; ta.style.boxShadow = '0 0 0 3px rgba(39,65,241,.12)'; });
785
+ ta.addEventListener('blur', () => { ta.style.borderColor = 'rgba(27,26,61,.14)'; ta.style.boxShadow = 'none'; });
786
+
787
+ const row = document.createElement('div');
788
+ row.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;margin-top:10px;';
789
+
790
+ const btnCancel = document.createElement('button');
791
+ btnCancel.textContent = 'Cancel';
792
+ btnCancel.style.cssText = `height:32px;padding:0 13px;border-radius:8px;border:1px solid rgba(27,26,61,.14);background:#fff;color:rgba(27,26,61,.7);font:600 12.5px ${FONT};cursor:pointer;`;
793
+ const btnAdd = document.createElement('button');
794
+ btnAdd.textContent = 'Comment';
795
+ btnAdd.style.cssText = `height:32px;padding:0 15px;border-radius:8px;border:none;background:#2741F1;color:#fff;font:600 12.5px ${FONT};cursor:pointer;`;
796
+
797
+ row.append(btnCancel, btnAdd);
798
+ cardEl.append(hdg);
799
+ if (quoteEl) cardEl.append(quoteEl);
800
+ cardEl.append(ta, row);
801
+ document.documentElement.appendChild(cardEl);
802
+ positionCard(anchorRect);
803
+
804
+ const submit = () => { const v = ta.value.trim(); if (!v) { ta.focus(); return; } onAdd(v); dismissCard(); };
805
+
806
+ btnCancel.addEventListener('click', e => { e.stopPropagation(); dismissCard(); });
807
+ btnAdd.addEventListener('click', e => { e.stopPropagation(); submit(); });
808
+ ta.addEventListener('keydown', e => {
809
+ if (e.key === 'Escape') { e.stopPropagation(); dismissCard(); }
810
+ if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); submit(); }
811
+ });
812
+ setTimeout(() => ta.focus(), 0);
813
+ }
814
+
815
+ // ── Hover ──────────────────────────────────────────────────────────────────
816
+ document.addEventListener('mouseover', e => {
817
+ if (!annotating || isInteractive(e.target) || isAnnotronEl(e.target)) return;
818
+ if (hovered) { hovered.style.outline = ''; hovered.style.outlineOffset = ''; hovered.style.cursor = ''; }
819
+ hovered = e.target;
820
+ hovered.style.outline = '2px solid #2741F1';
821
+ hovered.style.outlineOffset = '2px';
822
+ hovered.style.cursor = 'crosshair';
823
+ }, true);
824
+
825
+ document.addEventListener('mouseout', e => {
826
+ if (!annotating || !hovered || e.target !== hovered) return;
827
+ hovered.style.outline = '';
828
+ hovered.style.outlineOffset = '';
829
+ hovered.style.cursor = '';
830
+ hovered = null;
831
+ }, true);
832
+
833
+ document.addEventListener('contextmenu', e => {
834
+ if (isAnnotronEl(e.target) || isInteractive(e.target)) return;
835
+ const selected = extractSelectedTextData();
836
+ if (!selected) {
837
+ clearTextContextMenu(true);
838
+ return;
839
+ }
840
+
841
+ e.preventDefault();
842
+ e.stopPropagation();
843
+ highlightTextRange(selected.range);
844
+ clearQuickCommentButton();
845
+ ignoreNextClick = true;
846
+
847
+ showTextContextMenu(e.clientX, e.clientY, () => {
848
+ openTextCommentCard(selected);
849
+ });
850
+ }, true);
851
+
852
+ function maybeShowQuickComment() {
853
+ if (annotating) return;
854
+ if (textContextMenuEl || cardEl || commentPopupEl) return;
855
+ const selected = extractSelectedTextData();
856
+ if (!selected) {
857
+ clearQuickCommentButton(true);
858
+ return;
859
+ }
860
+ highlightTextRange(selected.range);
861
+ showQuickCommentButton(selected);
862
+ }
863
+
864
+ document.addEventListener('mouseup', () => {
865
+ maybeShowQuickComment();
866
+ }, true);
867
+
868
+ document.addEventListener('selectionchange', () => {
869
+ maybeShowQuickComment();
870
+ }, true);
871
+
872
+ document.addEventListener('keyup', e => {
873
+ if (e.key === 'Shift' || e.key.startsWith('Arrow') || e.key === 'Home' || e.key === 'End' || e.key === 'PageUp' || e.key === 'PageDown') {
874
+ maybeShowQuickComment();
875
+ }
876
+ if ((e.ctrlKey || e.metaKey) && (e.key === 'a' || e.key === 'A')) {
877
+ maybeShowQuickComment();
878
+ }
879
+ }, true);
880
+
881
+ document.addEventListener('click', e => {
882
+ if (textContextMenuEl && !isAnnotronEl(e.target)) {
883
+ clearTextContextMenu(true);
884
+ }
885
+ if (quickCommentBtnEl && !isAnnotronEl(e.target)) {
886
+ const sel = window.getSelection();
887
+ if (!sel || sel.isCollapsed) {
888
+ clearQuickCommentButton(true);
889
+ }
890
+ }
891
+ }, true);
892
+
893
+ document.addEventListener('keydown', e => {
894
+ if (e.key === 'Escape') {
895
+ clearTextContextMenu(true);
896
+ clearQuickCommentButton(true);
897
+ }
898
+ }, true);
899
+
900
+ // ── Text selection ─────────────────────────────────────────────────────────
901
+ document.addEventListener('mouseup', e => {
902
+ console.log('[annotron-sdk] mouseup', 'annotating:', annotating, 'target:', e.target?.tagName);
903
+ if (!annotating || isAnnotronEl(e.target)) return;
904
+
905
+ const sel = window.getSelection();
906
+ console.log('[annotron-sdk] selection:', sel?.toString()?.slice(0, 30));
907
+ if (!sel || sel.isCollapsed) return;
908
+ const rawText = sel.toString();
909
+ const text = rawText.trim();
910
+ if (!text) return;
911
+
912
+ const range = sel.getRangeAt(0);
913
+ const anchorEl = range.commonAncestorContainer.nodeType === 3
914
+ ? range.commonAncestorContainer.parentElement
915
+ : range.commonAncestorContainer;
916
+ const anchorRect = range.getBoundingClientRect();
917
+ const clonedRange = range.cloneRange();
918
+ const selector = cssPath(anchorEl);
919
+ const rawStart = textOffsetWithin(anchorEl, range.startContainer, range.startOffset);
920
+ const textStart = rawStart === null ? null : rawStart + rawText.indexOf(text);
921
+ const textEnd = textStart === null ? null : textStart + text.length;
922
+ const anchorText = anchorEl.textContent || '';
923
+ const textPrefix = textStart === null ? '' : anchorText.slice(Math.max(0, textStart - 24), textStart);
924
+ const textSuffix = textEnd === null ? '' : anchorText.slice(textEnd, textEnd + 24);
925
+ const label = `"${text.slice(0, 60)}"`;
926
+
927
+ ignoreNextClick = true;
928
+ dismissCard();
929
+ clearQuickCommentButton();
930
+ highlightTextRange(clonedRange);
931
+ // Show the Comment + (conditional) Edit toolbar instead of jumping straight
932
+ // into the comment composer, so a plain-text selection can be edited in
933
+ // place. Keep the native selection alive (don't removeAllRanges) so the
934
+ // pill survives the trailing click handler — matching the non-annotate path.
935
+ showQuickCommentButton({ selector, text, label, anchorRect, textStart, textEnd, textPrefix, textSuffix });
936
+ }, true);
937
+
938
+ // ── Element click ──────────────────────────────────────────────────────────
939
+ document.addEventListener('click', e => {
940
+ console.log('[annotron-sdk] click', 'annotating:', annotating, 'target:', e.target?.tagName);
941
+ if (!annotating || isAnnotronEl(e.target)) return;
942
+ if (isInteractive(e.target)) return;
943
+ e.preventDefault();
944
+ e.stopPropagation();
945
+
946
+ if (ignoreNextClick) { ignoreNextClick = false; return; }
947
+
948
+ const el = e.target;
949
+ const selector = cssPath(el);
950
+ const label = labelFor(el);
951
+ const anchorRect = el.getBoundingClientRect();
952
+ const quote = (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 120);
953
+
954
+ dismissCard();
955
+ highlightElement(el);
956
+ showCard({
957
+ quote,
958
+ placeholder: 'What should change about this element?',
959
+ anchorRect,
960
+ onAdd: note => {
961
+ window.parent.postMessage({ [TAG]: true, type: 'element-selected', selector, label, note }, '*');
962
+ },
963
+ });
964
+ }, true);
965
+
966
+ // ── Serialize ──────────────────────────────────────────────────────────────
967
+ function serialize() {
968
+ dismissCard();
969
+ const clone = document.documentElement.cloneNode(true);
970
+ clone.querySelectorAll('script[data-annotron]').forEach(s => s.remove());
971
+ clone.querySelectorAll('[data-annotron-ui]').forEach(s => s.remove());
972
+ clone.querySelectorAll('[style]').forEach(el => {
973
+ el.style.outline = '';
974
+ el.style.outlineOffset = '';
975
+ el.style.cursor = '';
976
+ });
977
+ const dt = document.doctype ? `<!DOCTYPE ${document.doctype.name}>\n` : '';
978
+ return dt + clone.outerHTML;
979
+ }
980
+
981
+ // ── Message bus ────────────────────────────────────────────────────────────
982
+ window.addEventListener('message', e => {
983
+ const d = e.data;
984
+ if (!d || !d[TAG]) return;
985
+ if (d.type === 'set-annotate') {
986
+ annotating = d.value;
987
+ console.log('[annotron-sdk] set-annotate', annotating);
988
+ if (!annotating) dismissCard();
989
+ }
990
+ if (d.type === 'set-annotations') {
991
+ annotations = Array.isArray(d.annotations) ? d.annotations : [];
992
+ selectedAnnotationId = d.selectedId || null;
993
+ scheduleAnnotationsRender();
994
+ }
995
+ if (d.type === 'serialize') {
996
+ e.source.postMessage({ [TAG]: true, type: 'serialized', html: serialize(), reqId: d.reqId }, '*');
997
+ }
998
+ if (d.type === 'jump-to-element') {
999
+ const target = d.selector ? document.querySelector(d.selector) : null;
1000
+ if (target) {
1001
+ dismissCard();
1002
+ target.scrollIntoView({ behavior: 'smooth', block: 'center' });
1003
+ highlightElement(target);
1004
+ }
1005
+ }
1006
+ if (d.type === 'clear-highlight') {
1007
+ clearHighlights();
1008
+ if (hovered) {
1009
+ hovered.style.outline = '';
1010
+ hovered.style.outlineOffset = '';
1011
+ hovered.style.cursor = '';
1012
+ hovered = null;
1013
+ }
1014
+ }
1015
+ if (d.type === 'focus-annotation') {
1016
+ selectedAnnotationId = d.annotation?.id || null;
1017
+ const annotation = d.annotation;
1018
+ let target = null;
1019
+ try { target = annotation?.selector ? document.querySelector(annotation.selector) : null; } catch {}
1020
+ if (target) target.scrollIntoView({ behavior: 'smooth', block: 'center' });
1021
+ scheduleAnnotationsRender();
1022
+ }
1023
+ });
1024
+
1025
+ // Extract headings for outline
1026
+ function extractHeadings() {
1027
+ const headingsData = document.querySelector('meta[name="headings-data"]');
1028
+ if (headingsData && headingsData.content) {
1029
+ try {
1030
+ return JSON.parse(headingsData.content);
1031
+ } catch (e) {
1032
+ console.error('[annotron-sdk] failed to parse headings:', e);
1033
+ }
1034
+ }
1035
+ return [];
1036
+ }
1037
+
1038
+ const headings = extractHeadings();
1039
+ window.parent.postMessage({ [TAG]: true, type: 'sdk-ready', headings }, '*');
1040
+ // Load the .md source so the inline "Edit" affordance can gate itself.
1041
+ initMdSource();
1042
+ })();