slidemind 0.1.2 → 0.1.4

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.
@@ -0,0 +1,821 @@
1
+ // Preview-review overlay — the CLI port of the slidemind add-in's spatial
2
+ // comment gate, styled as a branded slidemind "app". Dormant unless
3
+ // `window.__SLIDEMIND_REVIEW__` is set (only the `slidemind review` local
4
+ // server injects it), so a normal `slidemind build` preview is unaffected.
5
+ //
6
+ // Mechanics (mirrors the add-in, minus all the Office plumbing):
7
+ // - Every intrinsic element carries data-sm-loc="slides/<f>.jsx:line:col"
8
+ // (stamped by build.mjs's Babel plugin).
9
+ // - Click anywhere on the stage -> resolve the innermost element under the
10
+ // click via closest('[data-sm-loc]') and its ancestor chain, drop a pin,
11
+ // capture a note. Slide-level notes (empty-area clicks) carry no element.
12
+ // - Previous round's comments render as read-only ghost pins.
13
+ // - Approve, or Send comments -> POST to the local server, which unblocks the
14
+ // `slidemind review` command so the harness proceeds. No copy-paste.
15
+ //
16
+ // Branding (lifted from the add-in's product identity):
17
+ // - The embossed "raised slide" logo (public/logo.svg).
18
+ // - The "slide" / "mind" wordmark in Urbanist, drifting in opposite
19
+ // directions — the framer-motion splash animation, reimplemented in CSS.
20
+ // - Poppins UI type, Lucide icons, the neutral dark product palette.
21
+
22
+ (function () {
23
+ var CFG = window.__SLIDEMIND_REVIEW__;
24
+ if (!CFG) return;
25
+
26
+ var STAGE_SEL = "#stage";
27
+ var HEADER_H = 62;
28
+ var SIDEBAR_W = 316; // panel + gaps reserved on the right when comments exist
29
+ var commentsBySlide = {}; // stem -> [comment] (this round, editable)
30
+ var previousBySlide = {}; // stem -> [comment] (last round, read-only ghosts)
31
+ var currentStem = null;
32
+ var draftPopover = null;
33
+ var submitted = false;
34
+ var sidebarEl = null;
35
+ var sidebarOpen = false;
36
+ var hoverBox = null;
37
+ var locked = false; // hover box pinned to a popover's target
38
+
39
+ // UI accent for the hover box + focus rings (product chrome). The brand
40
+ // colour now lives entirely in the orb PNG (CFG.logoMark) served from the
41
+ // bundle — the CLI no longer re-derives an orb from the CD's colours.
42
+ var ACCENT = "#a9c2ff";
43
+
44
+ // Slide play order (stem by index) read from the inlined slide scripts — lets
45
+ // the sidebar jump to any slide by synthesising the deck's arrow-key nav.
46
+ var slideOrder = [];
47
+
48
+ (CFG.previousComments || []).forEach(function (c) {
49
+ var stem = stemOf(c && c.jsxPath);
50
+ if (!stem) return;
51
+ (previousBySlide[stem] = previousBySlide[stem] || []).push(c);
52
+ });
53
+
54
+ // ---------- brand assets ----------
55
+ // The add-in's embossed "raised slide" mark (src/ui/public/logo.svg), inlined.
56
+ var LOGO_SVG =
57
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="160 160 960 540" preserveAspectRatio="none">' +
58
+ '<defs>' +
59
+ '<linearGradient id="rimTop" gradientUnits="userSpaceOnUse" x1="160" y1="0" x2="1120" y2="0">' +
60
+ '<stop offset="0%" stop-color="#d0e4ff" stop-opacity="0.85"/>' +
61
+ '<stop offset="35%" stop-color="#fff" stop-opacity="0.5"/>' +
62
+ '<stop offset="100%" stop-color="#fff" stop-opacity="0"/></linearGradient>' +
63
+ '<linearGradient id="rimLeft" gradientUnits="userSpaceOnUse" x1="0" y1="160" x2="0" y2="700">' +
64
+ '<stop offset="0%" stop-color="#d0e4ff" stop-opacity="0.6"/>' +
65
+ '<stop offset="40%" stop-color="#fff" stop-opacity="0.28"/>' +
66
+ '<stop offset="100%" stop-color="#fff" stop-opacity="0"/></linearGradient>' +
67
+ '<linearGradient id="rimBottom" gradientUnits="userSpaceOnUse" x1="1120" y1="0" x2="160" y2="0">' +
68
+ '<stop offset="0%" stop-color="#000" stop-opacity="0.55"/>' +
69
+ '<stop offset="60%" stop-color="#000" stop-opacity="0.2"/>' +
70
+ '<stop offset="100%" stop-color="#000" stop-opacity="0"/></linearGradient>' +
71
+ '<linearGradient id="rimRight" gradientUnits="userSpaceOnUse" x1="0" y1="700" x2="0" y2="160">' +
72
+ '<stop offset="0%" stop-color="#000" stop-opacity="0.45"/>' +
73
+ '<stop offset="60%" stop-color="#000" stop-opacity="0.16"/>' +
74
+ '<stop offset="100%" stop-color="#000" stop-opacity="0"/></linearGradient>' +
75
+ '<linearGradient id="faceFill" gradientUnits="userSpaceOnUse" x1="160" y1="160" x2="1120" y2="700">' +
76
+ '<stop offset="0%" stop-color="#fff" stop-opacity="0.08"/>' +
77
+ '<stop offset="50%" stop-color="#808080" stop-opacity="0.02"/>' +
78
+ '<stop offset="100%" stop-color="#000" stop-opacity="0.10"/></linearGradient>' +
79
+ '</defs>' +
80
+ '<rect x="160" y="160" width="960" height="540" rx="40" ry="40" fill="url(#faceFill)"/>' +
81
+ '<path d="M 160,200 A 40,40 0 0 1 200,160 L 1080,160 A 40,40 0 0 1 1120,200" fill="none" stroke="url(#rimTop)" stroke-width="3" stroke-linecap="round"/>' +
82
+ '<path d="M 160,200 L 160,660 A 40,40 0 0 0 200,700" fill="none" stroke="url(#rimLeft)" stroke-width="2.4" stroke-linecap="round"/>' +
83
+ '<path d="M 1120,660 A 40,40 0 0 1 1080,700 L 200,700 A 40,40 0 0 1 160,660" fill="none" stroke="url(#rimBottom)" stroke-width="3" stroke-linecap="round"/>' +
84
+ '<path d="M 1120,660 L 1120,200 A 40,40 0 0 0 1080,160" fill="none" stroke="url(#rimRight)" stroke-width="2.4" stroke-linecap="round"/>' +
85
+ '<rect x="160" y="160" width="960" height="540" rx="40" ry="40" fill="none" stroke="#fff" stroke-opacity="0.14" stroke-width="1.4"/>' +
86
+ "</svg>";
87
+
88
+ // Give a copy of the logo unique filter/gradient ids so two instances (header
89
+ // + done veil) can coexist without id collisions.
90
+ function logoWithIds(suffix) {
91
+ return LOGO_SVG.replace(/id="([^"]+)"/g, 'id="$1' + suffix + '"').replace(
92
+ /url\(#([^)]+)\)/g,
93
+ "url(#$1" + suffix + ")",
94
+ );
95
+ }
96
+
97
+ // Lucide icons (MIT) — the same family the add-in's review UI uses.
98
+ var ICONS = {
99
+ check:
100
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>',
101
+ message:
102
+ '<svg 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"/><path d="M7 9h10"/><path d="M7 13h6"/></svg>',
103
+ trash:
104
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>',
105
+ close:
106
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="M6 6l12 12"/></svg>',
107
+ pin:
108
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 4.4-8 12-8 12s-8-7.6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>',
109
+ arrow:
110
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>',
111
+ };
112
+
113
+ // ---------- styles ----------
114
+ var CSS =
115
+ "@keyframes sm-drift-a{0%,100%{transform:translateX(-5px)}50%{transform:translateX(5px)}}" +
116
+ "@keyframes sm-drift-b{0%,100%{transform:translateX(5px)}50%{transform:translateX(-5px)}}" +
117
+ "@keyframes sm-bar-in{from{transform:translateY(-100%);opacity:0}to{transform:none;opacity:1}}" +
118
+ "@keyframes sm-pop-in{from{opacity:0;transform:translateY(5px) scale(.98)}to{opacity:1;transform:none}}" +
119
+ "@keyframes sm-pin-in{from{transform:translate(-50%,-50%) scale(0)}to{transform:translate(-50%,-50%) scale(1)}}" +
120
+ "@keyframes sm-veil-in{from{opacity:0}to{opacity:1}}" +
121
+ "@keyframes sm-rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}" +
122
+ ":root{--sm-bg:#0f0f0f;--sm-surface:#161618;--sm-border:#2b2b30;--sm-fg:#ededed;" +
123
+ "--sm-dim:#9a9aa0;--sm-accent:#a9c2ff;--sm-success:#2ecc71;--sm-danger:#f87171;" +
124
+ "--sm-font:'Poppins',ui-sans-serif,system-ui,sans-serif;--sm-word:'Urbanist',var(--sm-font);}" +
125
+ ".sm-o,.sm-o *{box-sizing:border-box;}" +
126
+ /* header */
127
+ ".sm-bar{position:fixed;top:0;left:0;right:0;z-index:2147483600;display:flex;align-items:center;" +
128
+ "gap:16px;height:62px;padding:0 18px;color:var(--sm-fg);font-family:var(--sm-font);font-size:13px;" +
129
+ "background:linear-gradient(180deg,rgba(13,13,15,.97),rgba(13,13,15,.86));" +
130
+ "-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-bottom:1px solid var(--sm-border);" +
131
+ "user-select:none;animation:sm-bar-in .5s cubic-bezier(.2,.7,.2,1);}" +
132
+ ".sm-brand{display:flex;align-items:center;gap:11px;}" +
133
+ ".sm-logo{position:relative;width:48px;height:27px;border-radius:5px;overflow:hidden;flex-shrink:0;background:#0a0c11;box-shadow:0 1px 3px rgba(0,0,0,.5);}" +
134
+ ".sm-logo-svg{position:absolute;inset:0;z-index:1;}.sm-logo-svg svg{width:100%;height:100%;display:block;}" +
135
+ ".sm-logo-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;}" +
136
+ ".sm-word{display:flex;flex-direction:column;line-height:.98;font-family:var(--sm-word);" +
137
+ "font-weight:300;font-size:13px;letter-spacing:.46em;color:var(--sm-fg);}" +
138
+ ".sm-word b{font-weight:300;display:block;will-change:transform;}" +
139
+ ".sm-word .top{animation:sm-drift-a 20s ease-in-out infinite;}" +
140
+ ".sm-word .bot{margin-left:.18em;animation:sm-drift-b 20s ease-in-out infinite;}" +
141
+ ".sm-sep{width:1px;height:28px;background:var(--sm-border);}" +
142
+ ".sm-meta{display:flex;flex-direction:column;gap:2px;}" +
143
+ ".sm-meta .t{font-weight:600;font-size:13px;}" +
144
+ ".sm-meta .h{font-size:11.5px;color:var(--sm-dim);}" +
145
+ ".sm-grow{flex:1;}" +
146
+ ".sm-count{font-size:12px;color:var(--sm-dim);font-variant-numeric:tabular-nums;}" +
147
+ /* buttons */
148
+ ".sm-btn{display:inline-flex;align-items:center;gap:7px;border:1px solid transparent;border-radius:9px;" +
149
+ "padding:8px 14px;font-family:var(--sm-font);font-weight:600;font-size:12.5px;cursor:pointer;" +
150
+ "transition:transform .14s ease,background .14s ease,box-shadow .14s ease,border-color .14s ease;white-space:nowrap;}" +
151
+ ".sm-btn svg{width:15px;height:15px;flex-shrink:0;}" +
152
+ ".sm-btn-primary{background:#f4f4f5;color:#0f0f0f;}" +
153
+ ".sm-btn-primary:hover{background:#fff;transform:translateY(-1px);box-shadow:0 6px 18px rgba(0,0,0,.45);}" +
154
+ ".sm-btn-primary svg{color:var(--sm-success);}" +
155
+ ".sm-btn-ghost{background:transparent;border-color:var(--sm-border);color:var(--sm-fg);}" +
156
+ ".sm-btn-ghost:hover{background:rgba(255,255,255,.06);border-color:#3a3a40;}" +
157
+ ".sm-btn-danger{background:transparent;border-color:rgba(248,113,113,.4);color:var(--sm-danger);}" +
158
+ ".sm-btn-danger:hover{background:rgba(248,113,113,.12);}" +
159
+ ".sm-btn.is-off{opacity:.4;cursor:not-allowed;pointer-events:none;}" +
160
+ /* pins */
161
+ ".sm-pinlayer{position:fixed;inset:0;z-index:2147483500;pointer-events:none;}" +
162
+ ".sm-pin{position:fixed;transform:translate(-50%,-50%);width:26px;height:26px;border-radius:50%;" +
163
+ "font:600 12px var(--sm-font);cursor:pointer;display:flex;align-items:center;justify-content:center;" +
164
+ "pointer-events:auto;animation:sm-pin-in .26s cubic-bezier(.2,1.5,.4,1);transition:filter .12s;}" +
165
+ ".sm-pin:hover{filter:brightness(1.12);}" +
166
+ ".sm-pin-active{background:#161618;color:#fff;border:2px solid rgba(255,255,255,.92);box-shadow:0 2px 9px rgba(0,0,0,.55);}" +
167
+ ".sm-pin-ghost{background:rgba(20,20,22,.72);color:#a1a1aa;border:1.5px dashed #71717a;}" +
168
+ /* popover */
169
+ ".sm-pop{position:fixed;z-index:2147483646;width:272px;padding:13px;background:var(--sm-surface);" +
170
+ "color:var(--sm-fg);border:1px solid var(--sm-border);border-radius:12px;" +
171
+ "box-shadow:0 20px 54px rgba(0,0,0,.6);font-family:var(--sm-font);font-size:13px;animation:sm-pop-in .14s ease;}" +
172
+ ".sm-pop-head{display:flex;align-items:center;gap:6px;font-weight:600;font-size:11.5px;" +
173
+ "color:var(--sm-accent);margin-bottom:9px;word-break:break-all;}" +
174
+ ".sm-pop-head svg{width:13px;height:13px;flex-shrink:0;}" +
175
+ ".sm-pop-head .mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;}" +
176
+ ".sm-ta{width:100%;min-height:74px;resize:vertical;background:#0c0c0e;color:var(--sm-fg);" +
177
+ "border:1px solid var(--sm-border);border-radius:8px;padding:9px;font:inherit;outline:none;transition:border-color .15s;}" +
178
+ ".sm-ta:focus{border-color:var(--sm-accent);}.sm-ta::placeholder{color:#6b6b72;}" +
179
+ ".sm-row{display:flex;gap:8px;margin-top:10px;}.sm-row.end{justify-content:flex-end;}.sm-row.split{justify-content:space-between;}" +
180
+ ".sm-pop .sm-btn{padding:6px 12px;font-size:12px;}" +
181
+ ".sm-ghost-body{white-space:pre-wrap;color:#d4d4d8;font-size:12.5px;line-height:1.5;}" +
182
+ /* done veil */
183
+ ".sm-veil{position:fixed;inset:0;z-index:2147483647;display:flex;flex-direction:column;align-items:center;" +
184
+ "justify-content:center;gap:30px;font-family:var(--sm-font);color:var(--sm-fg);" +
185
+ "background:radial-gradient(ellipse at center,rgba(16,16,18,.93),rgba(5,5,7,.98));" +
186
+ "-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);animation:sm-veil-in .4s ease;}" +
187
+ ".sm-veil .sm-logo{width:188px;height:106px;border-radius:14px;animation:sm-rise .6s cubic-bezier(.2,.7,.2,1) both;}" +
188
+ ".sm-veil .word{display:flex;flex-direction:column;gap:6px;font-family:var(--sm-word);font-weight:300;" +
189
+ "font-size:36px;letter-spacing:.6em;text-align:center;animation:sm-rise .6s .06s cubic-bezier(.2,.7,.2,1) both;}" +
190
+ ".sm-veil .word b{font-weight:300;}" +
191
+ ".sm-veil .word .top{animation:sm-drift-a 18s ease-in-out infinite;}" +
192
+ ".sm-veil .word .bot{margin-left:.3em;animation:sm-drift-b 18s ease-in-out infinite;}" +
193
+ ".sm-veil .status{display:flex;align-items:center;gap:10px;font-size:18px;font-weight:600;animation:sm-rise .6s .12s cubic-bezier(.2,.7,.2,1) both;}" +
194
+ ".sm-veil .status svg{width:24px;height:24px;color:var(--sm-success);}" +
195
+ ".sm-veil .sub{color:var(--sm-dim);font-size:13.5px;animation:sm-rise .6s .18s cubic-bezier(.2,.7,.2,1) both;}" +
196
+ /* hover box */
197
+ ".sm-hover{position:fixed;z-index:2147483450;pointer-events:none;border-radius:5px;border:2px solid var(--sm-accent);" +
198
+ "box-shadow:0 0 0 1px rgba(0,0,0,.3),0 6px 22px rgba(0,0,0,.28);opacity:0;" +
199
+ "transition:left .07s ease,top .07s ease,width .07s ease,height .07s ease,opacity .12s ease;}" +
200
+ /* sidebar */
201
+ "@keyframes sm-side-in{from{opacity:0;transform:translateX(16px)}to{opacity:1;transform:none}}" +
202
+ ".sm-side{position:fixed;top:74px;right:14px;bottom:14px;width:288px;z-index:2147483520;display:flex;flex-direction:column;" +
203
+ "background:rgba(18,18,20,.93);-webkit-backdrop-filter:blur(13px);backdrop-filter:blur(13px);border:1px solid var(--sm-border);" +
204
+ "border-radius:14px;box-shadow:0 18px 54px rgba(0,0,0,.5);font-family:var(--sm-font);color:var(--sm-fg);overflow:hidden;" +
205
+ "animation:sm-side-in .28s cubic-bezier(.2,.7,.2,1);}" +
206
+ ".sm-side-head{display:flex;align-items:center;gap:8px;padding:13px 15px;border-bottom:1px solid var(--sm-border);font-weight:600;font-size:13px;}" +
207
+ ".sm-side-head svg{width:15px;height:15px;color:var(--sm-accent);}" +
208
+ ".sm-side-head .n{margin-left:auto;color:var(--sm-dim);font-weight:500;font-variant-numeric:tabular-nums;}" +
209
+ ".sm-side-body{flex:1;overflow-y:auto;padding:8px 8px 12px;}" +
210
+ ".sm-side-grp{margin-top:9px;}.sm-side-grp:first-child{margin-top:2px;}" +
211
+ ".sm-side-grp-h{font-size:11px;font-weight:600;color:var(--sm-dim);padding:6px 8px 4px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;}" +
212
+ ".sm-side-sub{font-size:10px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--sm-dim);padding:7px 8px 2px;}" +
213
+ ".sm-side-row{display:flex;align-items:flex-start;gap:9px;padding:7px 8px;border-radius:9px;cursor:pointer;transition:background .12s;}" +
214
+ ".sm-side-row:hover{background:rgba(255,255,255,.05);}" +
215
+ ".sm-side-chip{flex-shrink:0;width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;" +
216
+ "font-size:10px;font-weight:600;margin-top:1px;background:#161618;color:#fff;border:1.5px solid rgba(255,255,255,.85);}" +
217
+ ".sm-side-chip.ghost{background:rgba(20,20,22,.6);color:#a1a1aa;border:1.5px dashed #71717a;}" +
218
+ ".sm-side-txt{font-size:12.5px;line-height:1.4;color:#e4e4e7;word-break:break-word;}" +
219
+ ".sm-side-loc{font-size:10.5px;color:var(--sm-dim);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;margin-top:2px;word-break:break-all;}";
220
+
221
+ // ---------- helpers ----------
222
+ function stemOf(jsxPath) {
223
+ if (!jsxPath) return null;
224
+ return String(jsxPath).split("/").pop().replace(/\.jsx$/, "");
225
+ }
226
+ function clamp01(n) {
227
+ return n < 0 ? 0 : n > 1 ? 1 : n;
228
+ }
229
+ function genId() {
230
+ return "c" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
231
+ }
232
+ function stage() {
233
+ return document.querySelector(STAGE_SEL);
234
+ }
235
+ function activeComments() {
236
+ return commentsBySlide[currentStem] || [];
237
+ }
238
+ function totalComments() {
239
+ var n = 0;
240
+ Object.keys(commentsBySlide).forEach(function (k) {
241
+ n += commentsBySlide[k].length;
242
+ });
243
+ return n;
244
+ }
245
+ function el(tag, cls, html) {
246
+ var e = document.createElement(tag);
247
+ if (cls) e.className = cls;
248
+ if (html != null) e.innerHTML = html;
249
+ return e;
250
+ }
251
+ function wordmarkHtml() {
252
+ return '<b class="top">slide</b><b class="bot">mind</b>';
253
+ }
254
+ // A logo mark. Genuine path: the add-in's per-brand orb PNG (CFG.logoMark),
255
+ // shipped on the bundle and shown verbatim. Fallback (no PNG materialized
256
+ // yet): a plain embossed slide glyph — no CLI-side orb re-creation.
257
+ function logoNode(cls, idSuffix) {
258
+ var box = el("div", cls);
259
+ if (CFG.logoMark) {
260
+ var img = el("img", "sm-logo-img");
261
+ img.src = CFG.logoMark;
262
+ img.alt = "slidemind";
263
+ box.appendChild(img);
264
+ return box;
265
+ }
266
+ box.appendChild(el("div", "sm-logo-svg", idSuffix ? logoWithIds(idSuffix) : LOGO_SVG));
267
+ return box;
268
+ }
269
+ function mkButton(label, variant, iconName) {
270
+ var b = el("button", "sm-btn sm-btn-" + variant);
271
+ b.innerHTML = (iconName ? ICONS[iconName] : "") + "<span>" + label + "</span>";
272
+ return b;
273
+ }
274
+ function mkButton(label, variant, iconName) {
275
+ var b = el("button", "sm-btn sm-btn-" + variant);
276
+ b.innerHTML = (iconName ? ICONS[iconName] : "") + "<span>" + label + "</span>";
277
+ return b;
278
+ }
279
+
280
+ // Build the resolvedTarget HitRegion + ancestor locs for an element, in the
281
+ // same shape the add-in's preview-review uses, so the harness-side formatter
282
+ // ports verbatim.
283
+ function describeTarget(elx) {
284
+ var st = stage();
285
+ if (!elx || !st) return { region: null, ancestors: [] };
286
+ var sr = st.getBoundingClientRect();
287
+ var r = elx.getBoundingClientRect();
288
+ var cls =
289
+ typeof elx.className === "string"
290
+ ? elx.className
291
+ : elx.getAttribute("class") || "";
292
+ var depth = 0;
293
+ for (var q = elx.parentElement; q && q !== st; q = q.parentElement) depth++;
294
+ var ancestors = [];
295
+ for (var p = elx.parentElement; p && p !== st; p = p.parentElement) {
296
+ var aloc = p.getAttribute && p.getAttribute("data-sm-loc");
297
+ if (aloc) ancestors.push(aloc);
298
+ }
299
+ return {
300
+ region: {
301
+ loc: elx.getAttribute("data-sm-loc"),
302
+ tag: elx.tagName.toLowerCase(),
303
+ className: cls,
304
+ textSnippet: (elx.textContent || "").replace(/\s+/g, " ").trim().slice(0, 80),
305
+ bbox: {
306
+ x: (r.left - sr.left) / sr.width,
307
+ y: (r.top - sr.top) / sr.height,
308
+ w: r.width / sr.width,
309
+ h: r.height / sr.height,
310
+ },
311
+ depth: depth,
312
+ },
313
+ ancestors: ancestors,
314
+ };
315
+ }
316
+
317
+ // ---------- chrome (top bar) ----------
318
+ var bar, sendBtn, countLabel;
319
+ function buildChrome() {
320
+ bar = el("div", "sm-o sm-bar");
321
+ bar.setAttribute("data-sm-overlay", "1");
322
+
323
+ var brand = el("div", "sm-brand");
324
+ brand.appendChild(logoNode("sm-logo"));
325
+ brand.appendChild(el("div", "sm-word", wordmarkHtml()));
326
+ bar.appendChild(brand);
327
+ bar.appendChild(el("div", "sm-sep"));
328
+
329
+ var meta = el("div", "sm-meta");
330
+ meta.appendChild(el("div", "t", "Review"));
331
+ meta.appendChild(el("div", "h", "Click any element to comment · ← → to switch slide"));
332
+ bar.appendChild(meta);
333
+
334
+ bar.appendChild(el("div", "sm-grow"));
335
+
336
+ countLabel = el("div", "sm-count");
337
+ bar.appendChild(countLabel);
338
+
339
+ sendBtn = mkButton("Send comments", "ghost", "message");
340
+ sendBtn.onclick = function () {
341
+ if (totalComments() === 0) return;
342
+ submit("comments");
343
+ };
344
+ bar.appendChild(sendBtn);
345
+
346
+ var approveBtn = mkButton("Approve & continue", "primary", "check");
347
+ approveBtn.onclick = function () {
348
+ submit("approve");
349
+ };
350
+ bar.appendChild(approveBtn);
351
+
352
+ document.body.appendChild(bar);
353
+ refreshChrome();
354
+ }
355
+ function refreshChrome() {
356
+ var n = totalComments();
357
+ countLabel.textContent = n === 0 ? "no comments yet" : n + (n === 1 ? " comment" : " comments");
358
+ sendBtn.querySelector("span").textContent =
359
+ n > 0 ? "Send " + n + " comment" + (n === 1 ? "" : "s") : "Send comments";
360
+ sendBtn.classList.toggle("is-off", n === 0);
361
+ }
362
+
363
+ // ---------- hover box (mirrors the add-in's hit-region-overlay) ----------
364
+ function buildHoverBox() {
365
+ hoverBox = el("div", "sm-o sm-hover");
366
+ hoverBox.setAttribute("data-sm-overlay", "1");
367
+ hoverBox.style.borderColor = ACCENT;
368
+ hoverBox.style.background = rgbaFrom(ACCENT, 0.1);
369
+ document.body.appendChild(hoverBox);
370
+ }
371
+ function placeHoverBox(rect) {
372
+ if (!hoverBox) return;
373
+ hoverBox.style.left = rect.left + "px";
374
+ hoverBox.style.top = rect.top + "px";
375
+ hoverBox.style.width = rect.width + "px";
376
+ hoverBox.style.height = rect.height + "px";
377
+ hoverBox.style.opacity = "1";
378
+ }
379
+ function hideHoverBox() {
380
+ if (hoverBox) hoverBox.style.opacity = "0";
381
+ }
382
+ // Pin the box to a normalized bbox (used while a popover is anchored).
383
+ function lockHoverBoxToBbox(bbox) {
384
+ locked = true;
385
+ if (!bbox) {
386
+ hideHoverBox();
387
+ return;
388
+ }
389
+ var sr = stage().getBoundingClientRect();
390
+ placeHoverBox({
391
+ left: sr.left + bbox.x * sr.width,
392
+ top: sr.top + bbox.y * sr.height,
393
+ width: bbox.w * sr.width,
394
+ height: bbox.h * sr.height,
395
+ });
396
+ }
397
+ // Translucent fill from a hex accent; falls back to a CSS var for non-hex.
398
+ function rgbaFrom(hex, a) {
399
+ var m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex || "");
400
+ if (!m) return "rgba(169,194,255," + a + ")";
401
+ return (
402
+ "rgba(" + parseInt(m[1], 16) + "," + parseInt(m[2], 16) + "," + parseInt(m[3], 16) + "," + a + ")"
403
+ );
404
+ }
405
+
406
+ // ---------- stage fit (take over build's fit so the sidebar + header don't
407
+ // cover the slide; pins/hover read live rects so they stay aligned) ----------
408
+ function applyStageFit() {
409
+ var st = stage();
410
+ if (!st) return;
411
+ var reserve = sidebarOpen ? SIDEBAR_W : 0;
412
+ var availW = window.innerWidth - reserve;
413
+ var availH = window.innerHeight - HEADER_H - 8;
414
+ var s = Math.min(availW / 1280, availH / 720) * 0.94;
415
+ var offX = -reserve / 2;
416
+ var offY = HEADER_H / 2;
417
+ st.style.transform = "translate(" + offX + "px," + offY + "px) scale(" + s + ")";
418
+ renderPins();
419
+ }
420
+
421
+ // ---------- sidebar (the add-in's comment list) ----------
422
+ function commentScreenPoint(c) {
423
+ var sr = stage().getBoundingClientRect();
424
+ return {
425
+ x: sr.left + clamp01(c.point.x) * sr.width,
426
+ y: sr.top + clamp01(c.point.y) * sr.height,
427
+ };
428
+ }
429
+ function openCommentAt(c, ghost) {
430
+ var p = commentScreenPoint(c);
431
+ if (ghost) openGhost(c, p.x, p.y);
432
+ else openEditor(c, p.x, p.y);
433
+ }
434
+ function sideRow(stem, c, num, ghost) {
435
+ var r = el("div", "sm-side-row");
436
+ var chip = el("div", "sm-side-chip" + (ghost ? " ghost" : ""));
437
+ chip.textContent = String(num);
438
+ var txt = el("div");
439
+ txt.appendChild(el("div", "sm-side-txt", "")).textContent = c.comment;
440
+ var locStr = c.resolvedTarget && c.resolvedTarget.loc ? c.resolvedTarget.loc : "slide-level note";
441
+ txt.appendChild(el("div", "sm-side-loc", "")).textContent = locStr;
442
+ r.appendChild(chip);
443
+ r.appendChild(txt);
444
+ r.onclick = function () {
445
+ goToStem(stem);
446
+ openCommentAt(c, ghost);
447
+ };
448
+ return r;
449
+ }
450
+ function updateSidebar() {
451
+ var show = totalComments() > 0;
452
+ if (!show) {
453
+ if (sidebarEl) {
454
+ sidebarEl.remove();
455
+ sidebarEl = null;
456
+ sidebarOpen = false;
457
+ applyStageFit();
458
+ }
459
+ return;
460
+ }
461
+ if (!sidebarEl) {
462
+ sidebarEl = el("div", "sm-o sm-side");
463
+ sidebarEl.setAttribute("data-sm-overlay", "1");
464
+ document.body.appendChild(sidebarEl);
465
+ sidebarOpen = true;
466
+ applyStageFit();
467
+ }
468
+ sidebarEl.innerHTML = "";
469
+ var n = totalComments();
470
+ var head = el(
471
+ "div",
472
+ "sm-side-head",
473
+ ICONS.message + "<span>Comments</span><span class=\"n\">" + n + "</span>",
474
+ );
475
+ var body = el("div", "sm-side-body");
476
+ var stems = slideOrder.length
477
+ ? slideOrder
478
+ : Object.keys(commentsBySlide); // fallback if order unknown
479
+ stems.forEach(function (stem) {
480
+ var act = commentsBySlide[stem] || [];
481
+ var gh = previousBySlide[stem] || [];
482
+ if (!act.length && !gh.length) return;
483
+ var grp = el("div", "sm-side-grp");
484
+ grp.appendChild(el("div", "sm-side-grp-h", "")).textContent = stem;
485
+ act.forEach(function (c, i) {
486
+ grp.appendChild(sideRow(stem, c, i + 1, false));
487
+ });
488
+ if (gh.length) {
489
+ grp.appendChild(el("div", "sm-side-sub", "Previous round"));
490
+ gh.forEach(function (c, i) {
491
+ grp.appendChild(sideRow(stem, c, i + 1, true));
492
+ });
493
+ }
494
+ body.appendChild(grp);
495
+ });
496
+ sidebarEl.appendChild(head);
497
+ sidebarEl.appendChild(body);
498
+ }
499
+
500
+ // Jump the deck to a slide by synthesising its arrow-key navigation.
501
+ function goToStem(stem) {
502
+ var target = slideOrder.indexOf(stem);
503
+ if (target < 0) return;
504
+ var nav = document.getElementById("nav");
505
+ var m = (nav && nav.textContent || "").match(/(\d+)\s*\/\s*\d+/);
506
+ var cur = m ? parseInt(m[1], 10) - 1 : 0;
507
+ var diff = target - cur;
508
+ var key = diff > 0 ? "ArrowRight" : "ArrowLeft";
509
+ for (var i = 0; i < Math.abs(diff); i++) {
510
+ document.dispatchEvent(new KeyboardEvent("keydown", { key: key, bubbles: true }));
511
+ }
512
+ syncSlideFromNav(); // deck render is synchronous; pick up the new stem now
513
+ }
514
+
515
+ // ---------- pins ----------
516
+ var pinLayer;
517
+ function buildPinLayer() {
518
+ pinLayer = el("div", "sm-o sm-pinlayer");
519
+ pinLayer.setAttribute("data-sm-overlay", "1");
520
+ document.body.appendChild(pinLayer);
521
+ }
522
+ function renderPins() {
523
+ if (!pinLayer) return;
524
+ pinLayer.innerHTML = "";
525
+ var st = stage();
526
+ if (!st) return;
527
+ var sr = st.getBoundingClientRect();
528
+ (previousBySlide[currentStem] || []).forEach(function (c, i) {
529
+ pinLayer.appendChild(makePin(c, sr, i + 1, true));
530
+ });
531
+ activeComments().forEach(function (c, i) {
532
+ pinLayer.appendChild(makePin(c, sr, i + 1, false));
533
+ });
534
+ }
535
+ function makePin(c, sr, num, ghost) {
536
+ var pin = el("button", "sm-pin " + (ghost ? "sm-pin-ghost" : "sm-pin-active"));
537
+ pin.setAttribute("data-sm-overlay", "1");
538
+ pin.textContent = String(num);
539
+ pin.title = c.comment || "";
540
+ pin.style.left = sr.left + clamp01(c.point.x) * sr.width + "px";
541
+ pin.style.top = sr.top + clamp01(c.point.y) * sr.height + "px";
542
+ pin.onclick = function (e) {
543
+ e.stopPropagation();
544
+ var px = sr.left + clamp01(c.point.x) * sr.width;
545
+ var py = sr.top + clamp01(c.point.y) * sr.height;
546
+ if (ghost) openGhost(c, px, py);
547
+ else openEditor(c, px, py);
548
+ };
549
+ return pin;
550
+ }
551
+
552
+ // ---------- popover ----------
553
+ function closePopover() {
554
+ if (draftPopover && draftPopover.parentNode) {
555
+ draftPopover.parentNode.removeChild(draftPopover);
556
+ }
557
+ draftPopover = null;
558
+ locked = false;
559
+ hideHoverBox();
560
+ }
561
+ function makePopover(px, py) {
562
+ closePopover();
563
+ var pop = el("div", "sm-o sm-pop");
564
+ pop.setAttribute("data-sm-overlay", "1");
565
+ var left = Math.min(px + 14, window.innerWidth - 288);
566
+ var top = Math.min(Math.max(py - 10, 72), window.innerHeight - 170);
567
+ pop.style.left = Math.max(8, left) + "px";
568
+ pop.style.top = top + "px";
569
+ pop.addEventListener("keydown", function (e) {
570
+ e.stopPropagation(); // don't let arrows/space drive slide nav while typing
571
+ });
572
+ draftPopover = pop;
573
+ document.body.appendChild(pop);
574
+ return pop;
575
+ }
576
+ function head(text, isElement) {
577
+ return el(
578
+ "div",
579
+ "sm-pop-head",
580
+ ICONS.pin + '<span class="' + (isElement ? "mono" : "") + '">' + text + "</span>",
581
+ );
582
+ }
583
+ function targetLabel(region) {
584
+ if (!region) return "Slide-level note";
585
+ var cls = region.className ? "." + region.className.split(/\s+/)[0] : "";
586
+ return "<" + region.tag + cls + ">";
587
+ }
588
+ function bindTextareaKeys(ta, onSave) {
589
+ ta.addEventListener("keydown", function (e) {
590
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") onSave();
591
+ if (e.key === "Escape") closePopover();
592
+ });
593
+ }
594
+
595
+ function openDraft(point, region, ancestors, px, py) {
596
+ var pop = makePopover(px, py);
597
+ pop.appendChild(head(targetLabel(region), !!region));
598
+ var ta = el("textarea", "sm-ta");
599
+ ta.placeholder = "What should change here?";
600
+ var row = el("div", "sm-row end");
601
+ var cancel = mkButton("Cancel", "ghost", "close");
602
+ cancel.onclick = closePopover;
603
+ var save = mkButton("Add comment", "primary", "check");
604
+ function doSave() {
605
+ var text = ta.value.trim();
606
+ if (!text) return;
607
+ (commentsBySlide[currentStem] = commentsBySlide[currentStem] || []).push({
608
+ id: genId(),
609
+ jsxPath: "slides/" + currentStem + ".jsx",
610
+ point: point,
611
+ resolvedTarget: region,
612
+ ancestors: ancestors,
613
+ comment: text,
614
+ });
615
+ closePopover();
616
+ renderPins();
617
+ refreshChrome();
618
+ updateSidebar();
619
+ }
620
+ save.onclick = doSave;
621
+ pop.appendChild(ta);
622
+ pop.appendChild(row);
623
+ row.appendChild(cancel);
624
+ row.appendChild(save);
625
+ lockHoverBoxToBbox(region && region.bbox);
626
+ ta.focus();
627
+ bindTextareaKeys(ta, doSave);
628
+ }
629
+
630
+ function openEditor(comment, px, py) {
631
+ var pop = makePopover(px, py);
632
+ pop.appendChild(head(targetLabel(comment.resolvedTarget), !!comment.resolvedTarget));
633
+ var ta = el("textarea", "sm-ta");
634
+ ta.value = comment.comment;
635
+ var row = el("div", "sm-row split");
636
+ var del = mkButton("Delete", "danger", "trash");
637
+ function doDelete() {
638
+ var list = commentsBySlide[currentStem] || [];
639
+ var idx = list.indexOf(comment);
640
+ if (idx >= 0) list.splice(idx, 1);
641
+ closePopover();
642
+ renderPins();
643
+ refreshChrome();
644
+ updateSidebar();
645
+ }
646
+ del.onclick = doDelete;
647
+ var save = mkButton("Save", "primary", "check");
648
+ function doSave() {
649
+ var text = ta.value.trim();
650
+ if (!text) return doDelete();
651
+ comment.comment = text;
652
+ closePopover();
653
+ renderPins();
654
+ updateSidebar();
655
+ }
656
+ save.onclick = doSave;
657
+ pop.appendChild(ta);
658
+ pop.appendChild(row);
659
+ row.appendChild(del);
660
+ row.appendChild(save);
661
+ lockHoverBoxToBbox(comment.resolvedTarget && comment.resolvedTarget.bbox);
662
+ ta.focus();
663
+ bindTextareaKeys(ta, doSave);
664
+ }
665
+
666
+ function openGhost(comment, px, py) {
667
+ var pop = makePopover(px, py);
668
+ pop.appendChild(
669
+ head("Previous round · " + targetLabel(comment.resolvedTarget), !!comment.resolvedTarget),
670
+ );
671
+ pop.appendChild(el("div", "sm-ghost-body", "")).textContent = comment.comment;
672
+ lockHoverBoxToBbox(comment.resolvedTarget && comment.resolvedTarget.bbox);
673
+ }
674
+
675
+ // ---------- submit ----------
676
+ function submit(decision) {
677
+ if (submitted) return;
678
+ submitted = true;
679
+ var comments = [];
680
+ Object.keys(commentsBySlide).forEach(function (stem) {
681
+ commentsBySlide[stem].forEach(function (c) {
682
+ comments.push(c);
683
+ });
684
+ });
685
+ fetch(CFG.endpoint, {
686
+ method: "POST",
687
+ headers: { "Content-Type": "application/json" },
688
+ body: JSON.stringify({
689
+ decision: decision,
690
+ comments: decision === "comments" ? comments : [],
691
+ }),
692
+ })
693
+ .then(function () {
694
+ showDone(decision);
695
+ })
696
+ .catch(function () {
697
+ submitted = false;
698
+ alert("Could not reach the slidemind review server — is the terminal command still running?");
699
+ });
700
+ }
701
+ function showDone(decision) {
702
+ closePopover();
703
+ if (pinLayer) pinLayer.style.display = "none";
704
+ if (bar) bar.style.display = "none";
705
+ var veil = el("div", "sm-o sm-veil");
706
+ veil.setAttribute("data-sm-overlay", "1");
707
+ veil.appendChild(logoNode("sm-logo", "-v"));
708
+ veil.appendChild(el("div", "word", wordmarkHtml()));
709
+ veil.appendChild(
710
+ el(
711
+ "div",
712
+ "status",
713
+ ICONS.check + "<span>" + (decision === "approve" ? "Approved" : "Comments sent") + "</span>",
714
+ ),
715
+ );
716
+ veil.appendChild(el("div", "sub", "Returned to your terminal — you can close this tab."));
717
+ document.body.appendChild(veil);
718
+ }
719
+
720
+ // ---------- slide tracking ----------
721
+ function syncSlideFromNav() {
722
+ var nav = document.getElementById("nav");
723
+ if (!nav) return;
724
+ var m = (nav.textContent || "").match(/\d+\s*\/\s*\d+\s+·\s+(\S+)/);
725
+ var stem = m ? m[1] : null;
726
+ if (stem && stem !== currentStem) {
727
+ currentStem = stem;
728
+ closePopover();
729
+ renderPins();
730
+ }
731
+ }
732
+
733
+ // ---------- wire up ----------
734
+ function injectAssets() {
735
+ var style = el("style");
736
+ style.id = "sm-review-style";
737
+ style.textContent = CSS;
738
+ document.head.appendChild(style);
739
+
740
+ var pre1 = el("link");
741
+ pre1.rel = "preconnect";
742
+ pre1.href = "https://fonts.googleapis.com";
743
+ var pre2 = el("link");
744
+ pre2.rel = "preconnect";
745
+ pre2.href = "https://fonts.gstatic.com";
746
+ pre2.crossOrigin = "anonymous";
747
+ var fonts = el("link");
748
+ fonts.rel = "stylesheet";
749
+ fonts.href =
750
+ "https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&family=Urbanist:wght@300;400&display=swap";
751
+ document.head.appendChild(pre1);
752
+ document.head.appendChild(pre2);
753
+ document.head.appendChild(fonts);
754
+ }
755
+
756
+ function init() {
757
+ var st = stage();
758
+ if (!st) {
759
+ setTimeout(init, 50);
760
+ return;
761
+ }
762
+ injectAssets();
763
+ st.style.cursor = "crosshair";
764
+ // Slide play order, for the sidebar's jump-to-slide.
765
+ document.querySelectorAll('script[data-kind="slide"]').forEach(function (s) {
766
+ slideOrder[parseInt(s.dataset.index, 10)] = s.dataset.name;
767
+ });
768
+ buildChrome();
769
+ buildPinLayer();
770
+ buildHoverBox();
771
+
772
+ st.addEventListener("click", function (e) {
773
+ if (submitted) return;
774
+ if (e.target.closest("[data-sm-overlay]")) return;
775
+ var sr = st.getBoundingClientRect();
776
+ var x = (e.clientX - sr.left) / sr.width;
777
+ var y = (e.clientY - sr.top) / sr.height;
778
+ if (x < 0 || x > 1 || y < 0 || y > 1) return;
779
+ var hit = e.target.closest("[data-sm-loc]");
780
+ var desc = describeTarget(hit);
781
+ openDraft({ x: clamp01(x), y: clamp01(y) }, desc.region, desc.ancestors, e.clientX, e.clientY);
782
+ });
783
+
784
+ // Hover-box: outline the element under the cursor (the add-in's affordance).
785
+ st.addEventListener("pointermove", function (e) {
786
+ if (submitted || locked) return;
787
+ if (e.target.closest("[data-sm-overlay]")) {
788
+ hideHoverBox();
789
+ return;
790
+ }
791
+ var hit = e.target.closest("[data-sm-loc]");
792
+ if (hit && hit !== st) placeHoverBox(hit.getBoundingClientRect());
793
+ else hideHoverBox();
794
+ });
795
+ st.addEventListener("pointerleave", function () {
796
+ if (!locked) hideHoverBox();
797
+ });
798
+
799
+ window.addEventListener("resize", function () {
800
+ closePopover();
801
+ applyStageFit();
802
+ });
803
+
804
+ var nav = document.getElementById("nav");
805
+ if (nav) {
806
+ new MutationObserver(syncSlideFromNav).observe(nav, {
807
+ childList: true,
808
+ characterData: true,
809
+ subtree: true,
810
+ });
811
+ }
812
+ syncSlideFromNav();
813
+ applyStageFit();
814
+ }
815
+
816
+ if (document.readyState === "loading") {
817
+ document.addEventListener("DOMContentLoaded", init);
818
+ } else {
819
+ init();
820
+ }
821
+ })();