slidemind 0.1.7 → 0.1.9

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,1778 @@
1
+ // Plan sign-off overlay — the CLI's plan-review gate, styled as the same
2
+ // branded slidemind "app" as the preview-review overlay (review-overlay.js).
3
+ // Dormant unless `window.__SLIDEMIND_PLAN_REVIEW__` is set (only the
4
+ // `slidemind plan` local server injects it), and dormant inside an iframe /
5
+ // thumbnail render, so nothing runs on an ordinary page.
6
+ //
7
+ // The gate is a storyboard "dossier": the plan.md body is rendered into ONE
8
+ // scrollable document surface. A plan that carries ```storyboard-canvas```
9
+ // fences renders as the dossier — a red-thread tab strip + the selected
10
+ // slide's editable storyboard canvas + a details aside — instead of a raw
11
+ // editor. A plain-markdown plan renders its blocks inline-editable
12
+ // (contenteditable) directly. Both paths edit the SAME markdown string, mapped
13
+ // back through the plan modules exposed on window.__SLIDEMIND_PLAN__ (bundled
14
+ // alongside window.__SLIDEMIND_RENDER_MD__ by plan.mjs).
15
+ //
16
+ // The decision lives in ONE always-visible chat-style composer bar pinned at
17
+ // the bottom: a feedback input, a round send button (a "story iteration" pill
18
+ // while storyline edits are staged) and — fused to its right — the Approve
19
+ // button, whose sub-label says the agent builds the slides next. The previous
20
+ // round's feedback shows as a read-only note above the input.
21
+ // - send → decision "comments" with the overall comment (plus, when the
22
+ // red thread was restructured, a generated story-iteration
23
+ // instruction + the edited plan markdown). The tab does NOT
24
+ // veil over: the surface locks read-only and the composer bar
25
+ // becomes a spinner + wait note while heartbeat polling waits
26
+ // for the agent's next `slidemind plan` round, which reloads
27
+ // this same tab.
28
+ // - Approve → decision "approve" (unsent feedback arm-confirms first),
29
+ // then the full-screen brand animation — logo + drifting
30
+ // wordmark + "the agent is building your slides" — before the
31
+ // tab closes itself.
32
+ // Both POST {decision, comment, editedPlan} to the local server so the
33
+ // `slidemind plan` command unblocks.
34
+ //
35
+ // Branding is lifted verbatim from review-overlay.js: the embossed "raised
36
+ // slide" logo (inline-SVG fallback when no per-brand orb PNG is bundled), the
37
+ // drifting "slide"/"mind" wordmark, Poppins/Urbanist type, Lucide icons, and
38
+ // the neutral dark product palette.
39
+
40
+ (function () {
41
+ var CFG = window.__SLIDEMIND_PLAN_REVIEW__;
42
+ if (!CFG) return;
43
+ // Dormant inside an iframe / thumbnail render (mirrors review-overlay).
44
+ if (window.top !== window.self) return;
45
+ if (new URLSearchParams(location.search).get("thumb") === "1") return;
46
+
47
+ // The bundled plan modules (schema / storyboard-markdown / backbone / edit /
48
+ // renderer) hang off this namespace — see plan.mjs bundlePlanModules.
49
+ var PLAN = window.__SLIDEMIND_PLAN__ || {};
50
+
51
+ var APPROVE_CLOSE_DELAY_MS = 2400; // long enough to read the build animation
52
+ var ROUND_READY_FLAG = "slidemind-plan-round-ready"; // sessionStorage marker across the reload
53
+ var ATTENTION_TITLE = "● Plan ready — slidemind";
54
+ var WAITING_TITLE = "⏳ Agent revising — slidemind";
55
+ var BEAT_DRAG_THRESHOLD = 4; // px before a tab press becomes a drag
56
+ var BLOCK_DRAG_THRESHOLD = 3; // px before a block press becomes a drag
57
+
58
+ // ---------- state ----------
59
+ var submitted = false;
60
+ var approveArmed = false; // inline "discard unsent feedback?" confirm state
61
+ var waiting = false; // comments round sent → the surface locks read-only
62
+ var baseline = CFG.planBody || ""; // the served plan body; edits diff against it
63
+
64
+ // The single source of truth the whole overlay renders from. `value` is the
65
+ // frontmatter-less plan body (edits index into it); `selectedIndex` is the
66
+ // beat on stage; `storyDraft` is a staged red-thread restructure (null when
67
+ // none), which blocks Approve and turns "send" into a story-iteration round.
68
+ var S = { value: baseline, selectedIndex: 0, storyDraft: null };
69
+
70
+ var feedbackText = ""; // the composer input, kept across composer rebuilds
71
+ var pendingComment = null; // resolved comment/editedPlan for the next submit
72
+ var pendingEdited = null;
73
+ var editingKey = null; // the beat whose move text is in rename mode
74
+ var newSlideCounter = 0; // unique keys for inserted slides
75
+ var focusAfterRender = null; // an element to focus once a render settles
76
+
77
+ var bar, wsEl, surfaceEl, composerEl, commentEl;
78
+ var editedBadge, approveBtn, sendBtn;
79
+ var waitTextEl = null;
80
+ var notifyPermPromise = null; // resolved Notification permission, asked at the send gesture
81
+
82
+ function locked() {
83
+ return waiting;
84
+ }
85
+
86
+ // ---------- brand assets (identical to review-overlay.js) ----------
87
+ var LOGO_SVG =
88
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="160 160 960 540" preserveAspectRatio="none">' +
89
+ "<defs>" +
90
+ '<linearGradient id="rimTop" gradientUnits="userSpaceOnUse" x1="160" y1="0" x2="1120" y2="0">' +
91
+ '<stop offset="0%" stop-color="#d0e4ff" stop-opacity="0.85"/>' +
92
+ '<stop offset="35%" stop-color="#fff" stop-opacity="0.5"/>' +
93
+ '<stop offset="100%" stop-color="#fff" stop-opacity="0"/></linearGradient>' +
94
+ '<linearGradient id="rimLeft" gradientUnits="userSpaceOnUse" x1="0" y1="160" x2="0" y2="700">' +
95
+ '<stop offset="0%" stop-color="#d0e4ff" stop-opacity="0.6"/>' +
96
+ '<stop offset="40%" stop-color="#fff" stop-opacity="0.28"/>' +
97
+ '<stop offset="100%" stop-color="#fff" stop-opacity="0"/></linearGradient>' +
98
+ '<linearGradient id="rimBottom" gradientUnits="userSpaceOnUse" x1="1120" y1="0" x2="160" y2="0">' +
99
+ '<stop offset="0%" stop-color="#000" stop-opacity="0.55"/>' +
100
+ '<stop offset="60%" stop-color="#000" stop-opacity="0.2"/>' +
101
+ '<stop offset="100%" stop-color="#000" stop-opacity="0"/></linearGradient>' +
102
+ '<linearGradient id="rimRight" gradientUnits="userSpaceOnUse" x1="0" y1="700" x2="0" y2="160">' +
103
+ '<stop offset="0%" stop-color="#000" stop-opacity="0.45"/>' +
104
+ '<stop offset="60%" stop-color="#000" stop-opacity="0.16"/>' +
105
+ '<stop offset="100%" stop-color="#000" stop-opacity="0"/></linearGradient>' +
106
+ '<linearGradient id="faceFill" gradientUnits="userSpaceOnUse" x1="160" y1="160" x2="1120" y2="700">' +
107
+ '<stop offset="0%" stop-color="#fff" stop-opacity="0.08"/>' +
108
+ '<stop offset="50%" stop-color="#808080" stop-opacity="0.02"/>' +
109
+ '<stop offset="100%" stop-color="#000" stop-opacity="0.10"/></linearGradient>' +
110
+ "</defs>" +
111
+ '<rect x="160" y="160" width="960" height="540" rx="40" ry="40" fill="url(#faceFill)"/>' +
112
+ '<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"/>' +
113
+ '<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"/>' +
114
+ '<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"/>' +
115
+ '<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"/>' +
116
+ '<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"/>' +
117
+ "</svg>";
118
+
119
+ // Give a copy of the logo unique gradient ids so two instances (header + done
120
+ // veil) can coexist without id collisions.
121
+ function logoWithIds(suffix) {
122
+ return LOGO_SVG.replace(/id="([^"]+)"/g, 'id="$1' + suffix + '"').replace(
123
+ /url\(#([^)]+)\)/g,
124
+ "url(#$1" + suffix + ")",
125
+ );
126
+ }
127
+
128
+ // Lucide icons (MIT) — the same family review-overlay uses.
129
+ var ICONS = {
130
+ check:
131
+ '<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>',
132
+ ghost:
133
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11a9 9 0 0 1 18 0v10l-3-2-3 2-3-2-3 2-3-2-3 2Z"/><path d="M9 11h.01"/><path d="M15 11h.01"/></svg>',
134
+ send:
135
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>',
136
+ sparkles:
137
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .962 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.582a.5.5 0 0 1 0 .962L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.962 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>',
138
+ plus:
139
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>',
140
+ x:
141
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
142
+ ruler:
143
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg>',
144
+ };
145
+
146
+ // ---------- styles (chrome copied from review-overlay.js; the editor/preview
147
+ // split is replaced by the storyboard dossier — plan-markdown.css ported and
148
+ // remapped onto the overlay's dark --sm-* palette; the two structural hues
149
+ // #3565d4 (blueprint blue) and #b41830 (storyline red) are literals) ----------
150
+ var CSS =
151
+ "@keyframes sm-drift-a{0%,100%{transform:translateX(-5px)}50%{transform:translateX(5px)}}" +
152
+ "@keyframes sm-drift-b{0%,100%{transform:translateX(5px)}50%{transform:translateX(-5px)}}" +
153
+ "@keyframes sm-bar-in{from{transform:translateY(-100%);opacity:0}to{transform:none;opacity:1}}" +
154
+ "@keyframes sm-veil-in{from{opacity:0}to{opacity:1}}" +
155
+ "@keyframes sm-rise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}" +
156
+ "@keyframes sm-pulse{0%,100%{opacity:.45}50%{opacity:.85}}" +
157
+ ":root{--sm-bg:var(--deck-chrome-bg,#111113);--sm-surface:var(--deck-chrome-bg-2,#161618);" +
158
+ "--sm-border:var(--deck-chrome-border,#2b2b30);--sm-fg:var(--deck-chrome-fg,#ededed);" +
159
+ "--sm-dim:var(--deck-chrome-dim,#9a9aa0);--sm-muted:rgba(255,255,255,.05);--sm-accent:#a9c2ff;" +
160
+ "--sm-success:#2ecc71;--sm-danger:#f87171;--sm-warn:#f6c454;" +
161
+ "--sm-font:'Poppins',ui-sans-serif,system-ui,sans-serif;--sm-word:'Urbanist',var(--sm-font);" +
162
+ /* the two structural hues, brightened against the dark surface */
163
+ "--plan-annotation:color-mix(in srgb,#3565d4 60%,var(--sm-fg));" +
164
+ "--plan-annotation-line:color-mix(in srgb,#3565d4 52%,transparent);" +
165
+ "--plan-annotation-wash:color-mix(in srgb,#3565d4 12%,transparent);" +
166
+ "--plan-story:color-mix(in srgb,#b41830 72%,var(--sm-fg));" +
167
+ "--plan-story-line:color-mix(in srgb,#b41830 45%,transparent);}" +
168
+ ".sm-o,.sm-o *{box-sizing:border-box;}" +
169
+ /* header */
170
+ ".sm-bar{position:fixed;top:0;left:0;right:0;z-index:2147483600;display:flex;align-items:center;" +
171
+ "gap:16px;height:var(--deck-chrome-h,62px);padding:0 18px;color:var(--sm-fg);font-family:var(--sm-font);font-size:13px;" +
172
+ "background:var(--sm-bg);border-bottom:1px solid var(--sm-border);" +
173
+ "user-select:none;animation:sm-bar-in .5s cubic-bezier(.2,.7,.2,1);}" +
174
+ ".sm-brand{display:flex;align-items:center;gap:11px;}" +
175
+ ".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);}" +
176
+ ".sm-logo-svg{position:absolute;inset:0;z-index:1;}.sm-logo-svg svg{width:100%;height:100%;display:block;}" +
177
+ ".sm-logo-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;}" +
178
+ ".sm-word{display:flex;flex-direction:column;line-height:.98;font-family:var(--sm-word);" +
179
+ "font-weight:300;font-size:13px;letter-spacing:.46em;color:var(--sm-fg);}" +
180
+ ".sm-word b{font-weight:300;display:block;will-change:transform;}" +
181
+ ".sm-word .top{animation:sm-drift-a 20s ease-in-out infinite;}" +
182
+ ".sm-word .bot{margin-left:.18em;animation:sm-drift-b 20s ease-in-out infinite;}" +
183
+ ".sm-sep{width:1px;height:28px;background:var(--sm-border);}" +
184
+ ".sm-meta{display:flex;flex-direction:column;gap:2px;min-width:0;}" +
185
+ ".sm-meta .t{font-weight:600;font-size:13px;display:flex;align-items:center;gap:8px;}" +
186
+ ".sm-meta .h{font-size:11.5px;color:var(--sm-dim);}" +
187
+ ".sm-edited{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--sm-warn);" +
188
+ "border:1px solid rgba(246,196,84,.4);border-radius:999px;padding:1px 7px;}" +
189
+ ".sm-grow{flex:1;}" +
190
+ /* buttons */
191
+ ".sm-btn{display:inline-flex;align-items:center;gap:7px;border:1px solid transparent;border-radius:9px;" +
192
+ "padding:8px 14px;font-family:var(--sm-font);font-weight:600;font-size:12.5px;cursor:pointer;" +
193
+ "transition:transform .14s ease,background .14s ease,box-shadow .14s ease,border-color .14s ease;white-space:nowrap;}" +
194
+ ".sm-btn svg{width:15px;height:15px;flex-shrink:0;}" +
195
+ /* workspace: one scrollable document surface + composer dock */
196
+ ".sm-ws{position:fixed;left:0;right:0;top:var(--deck-chrome-h,62px);bottom:0;z-index:2147483540;" +
197
+ "display:flex;flex-direction:column;background:var(--sm-bg);}" +
198
+ ".sm-ws-surface{flex:1;min-height:0;overflow-y:auto;background:var(--sm-bg);padding:22px 22px 30px;}" +
199
+ ".sm-ws.is-waiting .sm-ws-surface{pointer-events:none;opacity:.62;}" +
200
+ /* markdown document (Mode A + the storyboard headings share this scale) */
201
+ ".plan-md-root{max-width:min(1120px,100%);margin:0 auto;line-height:1.65;font-size:15.5px;color:var(--sm-fg);}" +
202
+ ".plan-md-root--storyboards{max-width:100%;display:flex;flex-direction:column;gap:0;}" +
203
+ ".plan-md-root h1,.plan-md-root h2,.plan-md-root h3,.plan-md-root h4,.plan-md-root h5,.plan-md-root h6{" +
204
+ "color:var(--sm-fg);font-weight:600;line-height:1.28;margin:1.6em 0 .5em;}" +
205
+ ".plan-md-root h1{font-size:1.95em;margin-top:.2em;}" +
206
+ ".plan-md-root h2{font-size:1.5em;padding-bottom:.24em;border-bottom:1px solid var(--sm-border);}" +
207
+ ".plan-md-root h3{font-size:1.22em;}.plan-md-root h4{font-size:1.05em;}" +
208
+ ".plan-md-root h5,.plan-md-root h6{font-size:.95em;color:var(--sm-dim);}" +
209
+ ".plan-md-root p{margin:.7em 0;}" +
210
+ ".plan-md-root ul,.plan-md-root ol{margin:.6em 0;padding-left:1.5em;}" +
211
+ ".plan-md-root ul{list-style:disc;}.plan-md-root ol{list-style:decimal;}" +
212
+ ".plan-md-root li{margin:.3em 0;}" +
213
+ ".plan-md-root blockquote{margin:.95em 0;padding:.45em 1em;border-left:3px solid var(--sm-border);" +
214
+ "color:var(--sm-dim);background:var(--sm-muted);border-radius:0 6px 6px 0;}" +
215
+ ".plan-md-root code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:.88em;" +
216
+ "background:var(--sm-muted);padding:.12em .42em;border-radius:5px;}" +
217
+ ".plan-md-root pre{background:#0c0c0e;border:1px solid var(--sm-border);border-radius:10px;padding:14px 16px;overflow-x:auto;line-height:1.5;}" +
218
+ ".plan-md-root pre code{background:none;padding:0;font-size:.86em;}" +
219
+ ".plan-md-root strong{color:var(--sm-fg);font-weight:600;}.plan-md-root em{font-style:italic;}" +
220
+ ".plan-md-root a{color:var(--sm-accent);}" +
221
+ ".plan-md-root [contenteditable=\"true\"]{border:0;background:transparent;border-radius:0;box-shadow:none;caret-color:currentColor;outline:none;}" +
222
+ ".plan-md-root [contenteditable=\"true\"]:focus{outline:none;box-shadow:none;background:transparent;}" +
223
+ /* dossier card */
224
+ ".plan-dossier{max-width:1120px;margin:0 auto;border:1px solid var(--sm-border);border-radius:14px;" +
225
+ "background:var(--sm-surface);overflow:hidden;}" +
226
+ ".plan-dossier-body{display:flex;flex-direction:column;gap:16px;padding:18px 20px 20px;}" +
227
+ ".plan-dossier-new-slide{display:flex;align-items:flex-start;gap:10px;border:1.5px dashed var(--plan-story-line);" +
228
+ "border-radius:10px;background:color-mix(in srgb,#b41830 6%,transparent);color:var(--sm-dim);font-size:13px;line-height:1.5;padding:22px 18px;}" +
229
+ ".plan-dossier-new-slide svg{width:14px;height:14px;flex:none;margin-top:2px;color:var(--plan-story);}" +
230
+ ".plan-dossier-new-slide p{margin:0;}.plan-dossier-new-slide strong{color:var(--sm-fg);font-weight:600;}" +
231
+ /* hand-off note */
232
+ ".plan-storyboard-draft-note{display:flex;align-items:flex-start;gap:10px;margin:0;padding:9px 13px;" +
233
+ "border:1px dashed color-mix(in srgb,var(--sm-dim) 45%,transparent);border-radius:9px;background:var(--sm-muted);" +
234
+ "color:var(--sm-dim);font-size:12px;line-height:1.5;}" +
235
+ ".plan-storyboard-draft-note svg{width:14px;height:14px;flex:none;margin-top:2px;}" +
236
+ ".plan-storyboard-draft-note p{margin:0;}.plan-storyboard-draft-note strong{color:var(--sm-fg);font-weight:600;}" +
237
+ /* storyline tab strip */
238
+ ".plan-storyline{margin:0;}" +
239
+ ".plan-storyline-head{display:flex;flex-direction:column;align-items:flex-start;gap:3px;padding:16px 20px 12px;}" +
240
+ ".plan-storyline-label{color:var(--plan-story);font-size:9px;font-weight:700;letter-spacing:.18em;line-height:1.2;text-transform:uppercase;}" +
241
+ ".plan-storyline-track{display:flex;flex-wrap:wrap;align-items:stretch;margin:0;padding:4px 14px 10px;list-style:none;" +
242
+ "border-bottom:1px solid var(--sm-border);row-gap:2px;}" +
243
+ ".plan-storyline-beat{position:relative;display:flex;flex:1 1 150px;min-width:132px;max-width:260px;flex-direction:row;" +
244
+ "align-items:baseline;gap:9px;padding:8px 8px 9px;border:0;border-radius:8px;background:transparent;cursor:pointer;transition:background-color 130ms ease;}" +
245
+ ".plan-storyline-beat:hover{background:var(--sm-muted);}" +
246
+ ".plan-storyline-beat--active{background:color-mix(in srgb,var(--sm-fg) 8%,transparent);}" +
247
+ ".plan-storyline-beat--draggable{touch-action:none;}" +
248
+ ".plan-storyline-beat--dragging{z-index:20;cursor:grabbing;background:var(--sm-surface);box-shadow:0 16px 36px rgba(0,0,0,.5);transition:none;}" +
249
+ ".plan-storyline-beat--editing{cursor:text;}" +
250
+ ".plan-storyline-beat-slide{flex:none;color:color-mix(in srgb,var(--sm-dim) 55%,transparent);" +
251
+ "font-family:Georgia,'Times New Roman',serif;font-size:21px;font-weight:400;line-height:1;transition:color 130ms ease;}" +
252
+ ".plan-storyline-beat--active .plan-storyline-beat-slide{color:var(--plan-story);}" +
253
+ ".plan-storyline-beat-text{display:-webkit-box;min-width:0;overflow:hidden;color:var(--sm-dim);font-size:12px;" +
254
+ "font-weight:500;line-height:1.35;-webkit-box-orient:vertical;-webkit-line-clamp:2;}" +
255
+ ".plan-storyline-beat--active .plan-storyline-beat-text{color:var(--sm-fg);font-weight:600;}" +
256
+ ".plan-storyline-beat-text[contenteditable=\"true\"]{display:block;border-radius:4px;cursor:text;outline:none;-webkit-line-clamp:unset;}" +
257
+ ".plan-storyline-gap{position:relative;display:flex;flex:none;align-items:center;justify-content:center;width:22px;}" +
258
+ ".plan-storyline-gap[data-active=\"true\"]::before{position:absolute;top:18%;bottom:18%;left:50%;width:3px;border-radius:2px;background:var(--plan-story);content:\"\";transform:translateX(-50%);}" +
259
+ ".plan-storyline-insert{position:relative;display:flex;align-items:center;justify-content:center;width:18px;height:18px;" +
260
+ "border:1px solid var(--sm-border);border-radius:999px;background:var(--sm-surface);color:var(--sm-dim);cursor:pointer;" +
261
+ "opacity:0;padding:0;pointer-events:none;transform:scale(.85);transition:opacity 140ms ease,transform 140ms ease,background-color 140ms ease,border-color 140ms ease,color 140ms ease;}" +
262
+ ".plan-storyline-insert svg{width:12px;height:12px;}" +
263
+ ".plan-storyline-track:hover .plan-storyline-insert,.plan-storyline-insert:focus-visible{opacity:1;pointer-events:auto;transform:scale(1);}" +
264
+ ".plan-storyline-insert:hover{background:var(--plan-story);border-color:var(--plan-story);color:var(--sm-bg);transform:scale(1.1);}" +
265
+ ".plan-storyline-beat-remove{position:absolute;top:-7px;right:-7px;display:inline-flex;align-items:center;justify-content:center;" +
266
+ "width:19px;height:19px;border:1px solid var(--sm-border);border-radius:999px;background:var(--sm-surface);box-shadow:0 2px 6px rgba(0,0,0,.35);" +
267
+ "color:var(--sm-dim);cursor:pointer;opacity:0;padding:0;pointer-events:none;transition:opacity 120ms ease;}" +
268
+ ".plan-storyline-beat-remove svg{width:12px;height:12px;}" +
269
+ ".plan-storyline-beat:hover .plan-storyline-beat-remove{opacity:1;pointer-events:auto;}" +
270
+ ".plan-storyline-beat--dragging .plan-storyline-beat-remove,.plan-storyline-beat--editing .plan-storyline-beat-remove{opacity:0;pointer-events:none;}" +
271
+ ".plan-storyline-beat-remove:disabled{cursor:default;opacity:0;}" +
272
+ /* storyboard canvas */
273
+ ".plan-storyboard{margin:0;}" +
274
+ ".plan-storyboard-header{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:8px;" +
275
+ "font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--sm-dim);}" +
276
+ ".plan-storyboard-legend{display:inline-flex;flex:none;align-items:baseline;gap:10px;letter-spacing:0;text-transform:none;}" +
277
+ ".plan-storyboard-legend-item--element{border:1px dashed var(--plan-annotation-line);border-radius:4px;background:var(--plan-annotation-wash);" +
278
+ "color:var(--plan-annotation);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:8.5px;font-weight:650;letter-spacing:.08em;padding:2px 6px;text-transform:uppercase;}" +
279
+ ".plan-storyboard-legend-item--content{color:var(--sm-fg);font-size:10.5px;font-weight:500;}" +
280
+ ".plan-storyboard-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(200px,26%);gap:14px;align-items:start;}" +
281
+ ".plan-storyboard-frame{position:relative;aspect-ratio:16/9;width:100%;overflow:hidden;border:0;border-radius:8px;" +
282
+ "background:color-mix(in srgb,var(--sm-bg) 78%,white);}" +
283
+ ".slide-emboss{box-shadow:0 1px 2px rgba(0,0,0,.45),0 10px 26px rgba(0,0,0,.4),inset 0 1px 0 rgba(255,255,255,.06);}" +
284
+ ".plan-storyboard-reserved{position:absolute;left:0;right:0;display:flex;align-items:center;justify-content:center;" +
285
+ "background:repeating-linear-gradient(-45deg,var(--sm-muted),var(--sm-muted) 8px,transparent 8px,transparent 16px);" +
286
+ "color:var(--sm-dim);font-size:clamp(9px,1.6vw,11px);font-weight:500;}" +
287
+ ".plan-storyboard-reserved-top{top:0;height:12.2%;border-bottom:1px solid var(--sm-border);}" +
288
+ ".plan-storyboard-reserved-bottom{bottom:0;height:13.3%;border-top:1px solid var(--sm-border);}" +
289
+ ".plan-storyboard-body{position:absolute;inset:12.2% 5% 13.3%;" +
290
+ "background:linear-gradient(color-mix(in srgb,var(--plan-annotation) 13%,transparent) 1px,transparent 1px)," +
291
+ "linear-gradient(90deg,color-mix(in srgb,var(--plan-annotation) 13%,transparent) 1px,transparent 1px);" +
292
+ "background-size:calc(100%/12) calc(100%/6);background-color:color-mix(in srgb,#3565d4 3%,transparent);}" +
293
+ ".plan-storyboard-block{position:absolute;display:flex;min-width:0;min-height:0;flex-direction:column;gap:clamp(4px,.72vw,7px);" +
294
+ "overflow:hidden;border:1.5px dashed color-mix(in srgb,var(--plan-annotation) 44%,transparent);border-radius:6px;" +
295
+ "background:color-mix(in srgb,var(--sm-surface) 62%,transparent);padding:clamp(6px,.95vw,10px);}" +
296
+ ".plan-storyboard-block[data-emphasis=\"primary\"]{border-width:2px;border-color:color-mix(in srgb,var(--plan-annotation) 70%,transparent);}" +
297
+ ".plan-storyboard-block[data-emphasis=\"tertiary\"]{opacity:.78;}" +
298
+ ".plan-storyboard-block--movable{cursor:grab;touch-action:none;}" +
299
+ ".plan-storyboard-block--movable:hover{border-color:color-mix(in srgb,var(--plan-annotation) 66%,transparent);}" +
300
+ ".plan-storyboard-block--dragging{z-index:10;cursor:grabbing;border-style:solid;box-shadow:0 10px 26px rgba(0,0,0,.45);}" +
301
+ ".plan-storyboard-block-remove{position:absolute;top:3px;right:3px;display:inline-flex;align-items:center;justify-content:center;" +
302
+ "width:18px;height:18px;border:1px solid var(--sm-border);border-radius:999px;background:var(--sm-surface);box-shadow:0 2px 6px rgba(0,0,0,.35);" +
303
+ "color:var(--sm-dim);cursor:pointer;opacity:0;padding:0;pointer-events:none;transition:opacity 120ms ease;}" +
304
+ ".plan-storyboard-block-remove svg{width:12px;height:12px;}" +
305
+ ".plan-storyboard-block:hover .plan-storyboard-block-remove{opacity:1;pointer-events:auto;}" +
306
+ ".plan-storyboard-block--dragging .plan-storyboard-block-remove{opacity:0;pointer-events:none;}" +
307
+ ".plan-storyboard-block-resize{position:absolute;right:0;bottom:0;width:16px;height:16px;border:0;border-radius:0 0 5px 0;" +
308
+ "background:linear-gradient(135deg,transparent 0 52%,var(--plan-annotation-line) 52% 60%,transparent 60% 72%,var(--plan-annotation-line) 72% 80%,transparent 80%);" +
309
+ "cursor:nwse-resize;opacity:0;padding:0;touch-action:none;transition:opacity 120ms ease;}" +
310
+ ".plan-storyboard-block:hover .plan-storyboard-block-resize,.plan-storyboard-block--dragging .plan-storyboard-block-resize{opacity:1;}" +
311
+ ".plan-storyboard-reserved-top .plan-storyboard-add{position:absolute;top:50%;right:10px;transform:translateY(-50%);}" +
312
+ ".plan-storyboard-add{display:inline-flex;align-items:center;gap:4px;border:1px dashed var(--plan-annotation-line);border-radius:5px;" +
313
+ "background:var(--plan-annotation-wash);color:var(--plan-annotation);cursor:pointer;font-size:10px;font-weight:600;letter-spacing:.02em;padding:2px 7px;" +
314
+ "font-family:var(--sm-font);transition:background-color 130ms ease,border-color 130ms ease;}" +
315
+ ".plan-storyboard-add svg{width:11px;height:11px;}" +
316
+ ".plan-storyboard-add:hover:not(:disabled){border-color:var(--plan-annotation);background:color-mix(in srgb,#3565d4 18%,transparent);}" +
317
+ ".plan-storyboard-add:disabled{cursor:default;opacity:.45;}" +
318
+ ".plan-storyboard-field{min-width:0;}" +
319
+ ".plan-storyboard-field-control{display:block;width:100%;min-height:1.25em;overflow:hidden;border:0;background:transparent;padding:0;" +
320
+ "color:var(--sm-fg);font:inherit;line-height:1.16;outline:none;box-shadow:none;white-space:normal;word-break:break-word;}" +
321
+ ".plan-storyboard-field-control:focus{outline:none;box-shadow:none;background:transparent;}" +
322
+ ".plan-storyboard-field[data-field=\"element\"]{display:inline-flex;width:fit-content;max-width:100%;flex-direction:column;" +
323
+ "border:1px dashed var(--plan-annotation-line);border-radius:4px;background:var(--plan-annotation-wash);padding:3px 6px;}" +
324
+ ".plan-storyboard-field[data-field=\"element\"] .plan-storyboard-field-control{color:var(--plan-annotation);" +
325
+ "font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;letter-spacing:.07em;line-height:1.15;text-transform:uppercase;}" +
326
+ ".plan-storyboard-field[data-field=\"content\"] .plan-storyboard-field-control{color:var(--sm-fg);font-weight:500;line-height:1.18;}" +
327
+ ".plan-storyboard-detail{position:sticky;top:0;max-height:min(62vh,620px);overflow:auto;padding:30px 0 0;}" +
328
+ ".plan-storyboard-detail-label{margin:0 0 5px;color:var(--sm-dim);font-size:10px;font-weight:700;letter-spacing:.04em;line-height:1.2;text-transform:uppercase;}" +
329
+ ".plan-storyboard-key-message{margin:0 0 18px;color:var(--sm-fg);font-size:15px;font-weight:650;line-height:1.34;white-space:pre-wrap;word-break:break-word;outline:none;}" +
330
+ ".plan-storyboard-detail-text{min-height:9em;border:0;background:transparent;color:var(--sm-fg);font-size:13.5px;font-weight:400;line-height:1.42;outline:none;white-space:pre-wrap;word-break:break-word;}" +
331
+ ".plan-storyboard-warnings{margin:.75em 0 0;padding-left:1.2em;font-size:.86em;color:var(--sm-dim);}" +
332
+ ".plan-storyboard-error{margin:1em 0;border:1px solid var(--sm-danger);border-radius:8px;background:color-mix(in srgb,var(--sm-danger) 10%,transparent);padding:12px;}" +
333
+ ".plan-storyboard-error p{margin:0 0 8px;font-weight:600;}" +
334
+ ".plan-storyboard-error pre{margin:0;overflow-x:auto;font-size:.82em;color:var(--sm-dim);}" +
335
+ "@media (max-width:900px){.plan-storyline-beat{max-width:none;}.plan-storyboard-layout{grid-template-columns:1fr;}" +
336
+ ".plan-storyboard-detail{position:static;max-height:none;padding-top:8px;}}" +
337
+ /* chat composer bar */
338
+ ".sm-composer{flex-shrink:0;display:flex;flex-direction:column;gap:8px;padding:11px 16px;" +
339
+ "border-top:1px solid var(--sm-border);background:var(--sm-surface);font-family:var(--sm-font);}" +
340
+ ".sm-story-banner{display:flex;align-items:center;gap:8px;padding:7px 10px;font-size:12px;line-height:1.5;" +
341
+ "border:1px solid color-mix(in srgb,#b41830 30%,transparent);border-radius:9px;background:color-mix(in srgb,#b41830 8%,transparent);}" +
342
+ ".sm-story-banner svg{width:14px;height:14px;flex-shrink:0;color:var(--plan-story);}" +
343
+ ".sm-story-banner .txt{flex:1;min-width:0;color:var(--sm-dim);}" +
344
+ ".sm-story-banner .txt b{color:var(--sm-fg);font-weight:600;}" +
345
+ ".sm-story-discard{flex-shrink:0;border:0;background:transparent;color:var(--sm-dim);font-family:var(--sm-font);font-weight:500;" +
346
+ "font-size:12px;padding:4px 8px;border-radius:6px;cursor:pointer;transition:background .12s,color .12s;}" +
347
+ ".sm-story-discard:hover{background:var(--sm-muted);color:var(--sm-fg);}" +
348
+ ".sm-prev-note{display:flex;align-items:flex-start;gap:8px;font-size:12px;color:var(--sm-dim);line-height:1.5;" +
349
+ "padding:7px 10px;border:1px dashed #3a3a40;border-radius:9px;background:var(--sm-muted);}" +
350
+ ".sm-prev-note svg{width:14px;height:14px;flex-shrink:0;margin-top:2px;color:#71717a;}" +
351
+ ".sm-prev-note .lbl{font-weight:600;color:#a1a1aa;white-space:nowrap;}" +
352
+ ".sm-prev-note .txt{white-space:pre-wrap;word-break:break-word;color:#b9b9be;}" +
353
+ ".sm-composer-row{display:flex;align-items:flex-end;gap:10px;}" +
354
+ ".sm-chat-ta{flex:1;min-height:40px;max-height:150px;resize:none;background:#0c0c0e;color:var(--sm-fg);" +
355
+ "border:1px solid var(--sm-border);border-radius:14px;padding:10px 15px;font:13px/1.5 var(--sm-font);outline:none;transition:border-color .15s;}" +
356
+ ".sm-chat-ta:focus{border-color:var(--sm-accent);}.sm-chat-ta::placeholder{color:#6b6b72;}" +
357
+ ".sm-send{width:38px;height:38px;border-radius:50%;border:0;cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;" +
358
+ "background:var(--sm-accent);color:#0f1220;transition:transform .14s ease,box-shadow .14s ease,opacity .14s ease;}" +
359
+ ".sm-send svg{width:16px;height:16px;margin:1px 1px 0 0;}" +
360
+ ".sm-send:hover{transform:translateY(-1px) scale(1.05);box-shadow:0 8px 22px rgba(169,194,255,.35);}" +
361
+ ".sm-send.is-off{opacity:.35;cursor:not-allowed;pointer-events:none;}" +
362
+ /* story-iteration pill (send while a storyline draft is staged) */
363
+ ".sm-send-pill{display:inline-flex;align-items:center;gap:7px;height:38px;flex-shrink:0;border:0;border-radius:999px;" +
364
+ "padding:0 16px;background:var(--sm-accent);color:#0f1220;font-family:var(--sm-font);font-weight:600;font-size:12.5px;cursor:pointer;white-space:nowrap;" +
365
+ "transition:transform .14s ease,box-shadow .14s ease,opacity .14s ease;}" +
366
+ ".sm-send-pill svg{width:15px;height:15px;}" +
367
+ ".sm-send-pill:hover{transform:translateY(-1px);box-shadow:0 8px 22px rgba(169,194,255,.35);}" +
368
+ ".sm-send-pill.is-off{opacity:.35;cursor:not-allowed;pointer-events:none;}" +
369
+ ".sm-composer-sep{width:1px;align-self:stretch;background:var(--sm-border);}" +
370
+ ".sm-approve{display:inline-flex;flex-direction:column;align-items:center;gap:1px;border:0;border-radius:12px;padding:7px 16px;" +
371
+ "background:#f4f4f5;color:#0f0f0f;cursor:pointer;font-family:var(--sm-font);transition:transform .14s ease,background .14s ease,box-shadow .14s ease;white-space:nowrap;}" +
372
+ ".sm-approve:hover{background:#fff;transform:translateY(-1px);box-shadow:0 6px 18px rgba(0,0,0,.45);}" +
373
+ ".sm-approve .main{display:flex;align-items:center;gap:7px;font-weight:600;font-size:12.5px;}" +
374
+ ".sm-approve .main svg{width:15px;height:15px;color:var(--sm-success);}" +
375
+ ".sm-approve .sub{font-size:9.5px;font-weight:500;color:#61616a;}" +
376
+ ".sm-approve.is-armed{background:#f6c454;color:#241a00;}" +
377
+ ".sm-approve.is-armed .main svg{color:#241a00;}" +
378
+ ".sm-approve.is-off{opacity:.45;cursor:not-allowed;pointer-events:none;}" +
379
+ /* waiting state */
380
+ "@keyframes sm-rotate{to{transform:rotate(360deg)}}" +
381
+ ".sm-wait{display:flex;align-items:center;gap:12px;padding:7px 2px;font-size:12.5px;color:var(--sm-dim);line-height:1.5;}" +
382
+ ".sm-wait b{color:var(--sm-fg);font-weight:600;}" +
383
+ ".sm-spin{width:17px;height:17px;border-radius:50%;border:2px solid var(--sm-border);border-top-color:var(--sm-accent);animation:sm-rotate .9s linear infinite;flex-shrink:0;}" +
384
+ /* done veil */
385
+ ".sm-veil{position:fixed;inset:0;z-index:2147483647;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:30px;" +
386
+ "font-family:var(--sm-font);color:var(--sm-fg);background:radial-gradient(ellipse at center,rgba(16,16,18,.93),rgba(5,5,7,.98));" +
387
+ "-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);animation:sm-veil-in .4s ease;}" +
388
+ ".sm-veil .sm-logo{width:188px;height:106px;border-radius:14px;animation:sm-rise .6s cubic-bezier(.2,.7,.2,1) both;}" +
389
+ ".sm-veil .word{display:flex;flex-direction:column;gap:6px;font-family:var(--sm-word);font-weight:300;font-size:36px;letter-spacing:.6em;text-align:center;animation:sm-rise .6s .06s cubic-bezier(.2,.7,.2,1) both;}" +
390
+ ".sm-veil .word b{font-weight:300;}" +
391
+ ".sm-veil .word .top{animation:sm-drift-a 18s ease-in-out infinite;}" +
392
+ ".sm-veil .word .bot{margin-left:.3em;animation:sm-drift-b 18s ease-in-out infinite;}" +
393
+ ".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;}" +
394
+ ".sm-veil .status svg{width:24px;height:24px;color:var(--sm-success);}" +
395
+ ".sm-veil .sub{color:var(--sm-dim);font-size:13.5px;animation:sm-rise .6s .18s cubic-bezier(.2,.7,.2,1) both;}" +
396
+ ".sm-veil .sub.sm-pulse{animation:sm-rise .6s .18s cubic-bezier(.2,.7,.2,1) both,sm-pulse 2s .9s ease-in-out infinite;}";
397
+
398
+ // ---------- helpers ----------
399
+ function el(tag, cls, html) {
400
+ var e = document.createElement(tag);
401
+ if (cls) e.className = cls;
402
+ if (html != null) e.innerHTML = html;
403
+ return e;
404
+ }
405
+ function wordmarkHtml() {
406
+ return '<b class="top">slide</b><b class="bot">mind</b>';
407
+ }
408
+ // Genuine path: the add-in's per-brand orb PNG (CFG.logoMark). Fallback: the
409
+ // inline embossed slide glyph — no CLI-side orb re-creation.
410
+ function logoNode(cls, idSuffix) {
411
+ var box = el("div", cls);
412
+ if (CFG.logoMark) {
413
+ var img = el("img", "sm-logo-img");
414
+ img.src = CFG.logoMark;
415
+ img.alt = "slidemind";
416
+ box.appendChild(img);
417
+ return box;
418
+ }
419
+ box.appendChild(el("div", "sm-logo-svg", idSuffix ? logoWithIds(idSuffix) : LOGO_SVG));
420
+ return box;
421
+ }
422
+ function clamp(v, min, max) {
423
+ return Math.min(Math.max(v, min), max);
424
+ }
425
+ function planWasEdited() {
426
+ return S.value !== baseline;
427
+ }
428
+ function commentText() {
429
+ return (commentEl ? commentEl.value : feedbackText).trim();
430
+ }
431
+ function placeCaretAtEnd(node) {
432
+ try {
433
+ node.focus();
434
+ var sel = window.getSelection();
435
+ if (!sel) return;
436
+ var range = document.createRange();
437
+ range.selectNodeContents(node);
438
+ range.collapse(false);
439
+ sel.removeAllRanges();
440
+ sel.addRange(range);
441
+ } catch {
442
+ /* headless / detached — ignore */
443
+ }
444
+ }
445
+
446
+ // ---------- chrome (top bar) ----------
447
+ function buildChrome() {
448
+ bar = el("div", "sm-o sm-bar");
449
+ bar.setAttribute("data-sm-overlay", "1");
450
+
451
+ var brand = el("div", "sm-brand");
452
+ brand.appendChild(logoNode("sm-logo"));
453
+ brand.appendChild(el("div", "sm-word", wordmarkHtml()));
454
+ bar.appendChild(brand);
455
+ bar.appendChild(el("div", "sm-sep"));
456
+
457
+ var meta = el("div", "sm-meta");
458
+ var title = el("div", "t");
459
+ title.appendChild(document.createTextNode("Plan review"));
460
+ editedBadge = el("span", "sm-edited", "edited");
461
+ editedBadge.style.display = "none";
462
+ title.appendChild(editedBadge);
463
+ meta.appendChild(title);
464
+ meta.appendChild(
465
+ el(
466
+ "div",
467
+ "h",
468
+ "Shape the red thread and storyboards below · drag to reorder, rename moves, edit blocks · decide when it reads right",
469
+ ),
470
+ );
471
+ bar.appendChild(meta);
472
+ bar.appendChild(el("div", "sm-grow"));
473
+
474
+ document.body.appendChild(bar);
475
+ }
476
+ function refreshChrome() {
477
+ if (editedBadge) editedBadge.style.display = planWasEdited() ? "" : "none";
478
+
479
+ var hasFeedback = commentText().length > 0;
480
+ var canSend = !!S.storyDraft || hasFeedback;
481
+ if (sendBtn) sendBtn.classList.toggle("is-off", !canSend);
482
+
483
+ if (approveBtn) {
484
+ var main = approveBtn.querySelector(".main span");
485
+ var sub = approveBtn.querySelector(".sub");
486
+ if (S.storyDraft) {
487
+ // Approve is blocked until the staged storyline is sent or discarded.
488
+ approveArmed = false;
489
+ approveBtn.classList.remove("is-armed");
490
+ approveBtn.classList.add("is-off");
491
+ if (main) main.textContent = "Approve plan";
492
+ if (sub) {
493
+ sub.style.display = "";
494
+ sub.textContent = "Send or discard your storyline changes first";
495
+ }
496
+ } else {
497
+ approveBtn.classList.remove("is-off");
498
+ if (approveArmed && hasFeedback) {
499
+ approveBtn.classList.add("is-armed");
500
+ if (main) main.textContent = "Feedback not sent — approve anyway?";
501
+ if (sub) sub.style.display = "none";
502
+ } else {
503
+ approveArmed = false;
504
+ approveBtn.classList.remove("is-armed");
505
+ if (main) main.textContent = "Approve plan";
506
+ if (sub) {
507
+ sub.style.display = "";
508
+ sub.textContent = "the agent builds the slides next";
509
+ }
510
+ }
511
+ }
512
+ }
513
+ }
514
+
515
+ // ---------- workspace: one scrollable document surface + composer ----------
516
+ function buildWorkspace() {
517
+ wsEl = el("div", "sm-o sm-ws");
518
+ wsEl.setAttribute("data-sm-overlay", "1");
519
+
520
+ surfaceEl = el("div", "sm-ws-surface");
521
+ // Delegated inline-edit commit for Mode A rendered markdown blocks. Attached
522
+ // to the persistent surface so a Mode-B re-render (which replaces the
523
+ // surface's children) keeps these live. commitRenderedEdit is a no-op for
524
+ // anything that isn't a stamped, editable, non-storyboard block.
525
+ surfaceEl.addEventListener(
526
+ "blur",
527
+ function (e) {
528
+ commitRenderedEdit(e.target);
529
+ },
530
+ true,
531
+ );
532
+ surfaceEl.addEventListener(
533
+ "keydown",
534
+ function (e) {
535
+ if (e.key !== "Enter" || e.shiftKey) return;
536
+ var t = e.target;
537
+ if (!(t instanceof HTMLElement)) return;
538
+ if (t.closest(".plan-storyboard")) return;
539
+ if (!t.closest("[data-md-line][data-md-kind]")) return;
540
+ e.preventDefault();
541
+ commitRenderedEdit(t);
542
+ t.blur();
543
+ },
544
+ true,
545
+ );
546
+ wsEl.appendChild(surfaceEl);
547
+ document.body.appendChild(wsEl);
548
+
549
+ composerEl = el("div", "sm-composer");
550
+ composerEl.setAttribute("data-sm-overlay", "1");
551
+ wsEl.appendChild(composerEl);
552
+
553
+ renderSurface();
554
+ renderComposer();
555
+ }
556
+
557
+ // Map an edited Mode-A block's innerText back into S.value at its stamped
558
+ // source line WITHOUT re-rendering the node (caret preservation).
559
+ function commitRenderedEdit(target) {
560
+ if (locked()) return;
561
+ if (!(target instanceof HTMLElement)) return;
562
+ var editable = target.closest("[data-md-line][data-md-kind]");
563
+ if (!editable || editable.closest(".plan-storyboard")) return;
564
+ var next = PLAN.replaceRenderedMarkdownBlock({
565
+ markdown: S.value,
566
+ line: Number(editable.dataset.mdLine),
567
+ kind: editable.dataset.mdKind || "",
568
+ text: editable.innerText != null ? editable.innerText : editable.textContent || "",
569
+ });
570
+ if (next !== S.value) {
571
+ S.value = next;
572
+ approveArmed = false; // any plan edit disarms a pending approve-confirm
573
+ refreshChrome();
574
+ }
575
+ }
576
+
577
+ // ---------- surface render ----------
578
+ // A plan with storyboard fences renders as the dossier; otherwise the plain
579
+ // markdown renders inline-editable. Mode B re-renders in full on every
580
+ // committed model change (nothing is focused at commit — edits commit on
581
+ // blur / pointerup — so no caret is lost); Mode A never re-renders here.
582
+ function renderSurface() {
583
+ if (!surfaceEl) return;
584
+ surfaceEl.innerHTML = "";
585
+ focusAfterRender = null;
586
+ var segments = PLAN.splitPlanMarkdownIntoSegments(S.value);
587
+ var hasStoryboards = segments.some(function (s) {
588
+ return s.type === "storyboard";
589
+ });
590
+ if (hasStoryboards) renderDossier(segments);
591
+ else renderModeA(segments);
592
+ if (focusAfterRender) {
593
+ placeCaretAtEnd(focusAfterRender);
594
+ focusAfterRender = null;
595
+ }
596
+ }
597
+
598
+ function renderModeA(segments) {
599
+ var root = el("div", "plan-md-root");
600
+ segments.forEach(function (seg) {
601
+ if (seg.type !== "markdown") return;
602
+ var html = PLAN.renderPlanMarkdown(seg.text, { lineOffset: seg.lineOffset });
603
+ if (!locked()) html = PLAN.addEditableAttributes(html);
604
+ var holder = el("div");
605
+ holder.innerHTML = html;
606
+ while (holder.firstChild) root.appendChild(holder.firstChild);
607
+ });
608
+ surfaceEl.appendChild(root);
609
+ }
610
+
611
+ // ---------- dossier (storyboard mode) ----------
612
+ function renderDossier(segments) {
613
+ var storyboardSegments = segments.filter(function (s) {
614
+ return s.type === "storyboard";
615
+ });
616
+ var storyboardData = storyboardSegments
617
+ .map(function (s) {
618
+ return s.data;
619
+ })
620
+ .filter(Boolean);
621
+ var redThreadMessages = PLAN.extractRedThreadMessages(S.value);
622
+ // Tabs need a beat per board even without a `## Red thread` section — fall
623
+ // back to each board's key message (read-only tabs then).
624
+ var beatMessages = redThreadMessages.length
625
+ ? redThreadMessages
626
+ : storyboardSegments.map(function (seg, i) {
627
+ return (seg.data && seg.data.keyMessage) || "Slide " + ((seg.data && seg.data.slide) || i + 1);
628
+ });
629
+ var committed = PLAN.backboneFromMessages(beatMessages);
630
+ var slides = S.storyDraft || committed;
631
+ var backboneEditable = PLAN.parsePlanBackbone(S.value) !== null;
632
+ var canEdit = !locked() && backboneEditable;
633
+
634
+ if (S.selectedIndex >= slides.length) S.selectedIndex = slides.length - 1;
635
+ if (S.selectedIndex < 0) S.selectedIndex = 0;
636
+ var activeIndex = S.selectedIndex;
637
+
638
+ // A staged draft resolves a tab to the board of the slide it originally was;
639
+ // new slides (originalIndex null) have no board yet.
640
+ var activeSegment;
641
+ if (S.storyDraft) {
642
+ var oi = slides[activeIndex] ? slides[activeIndex].originalIndex : undefined;
643
+ activeSegment = oi === null || oi === undefined ? undefined : storyboardSegments[oi];
644
+ } else {
645
+ activeSegment = storyboardSegments[activeIndex];
646
+ }
647
+
648
+ var dossier = el("div", "plan-dossier");
649
+ var root = el("div", "plan-md-root plan-md-root--storyboards");
650
+ dossier.appendChild(root);
651
+
652
+ root.appendChild(
653
+ renderStrip(slides, committed, beatMessages, storyboardData, canEdit, activeIndex),
654
+ );
655
+
656
+ var body = el("div", "plan-dossier-body");
657
+ if (activeSegment) {
658
+ if (activeSegment.data) {
659
+ body.appendChild(renderCanvas(activeSegment));
660
+ } else {
661
+ var err = el("div", "plan-storyboard-error");
662
+ err.appendChild(el("p", null, "Storyboard block could not be rendered."));
663
+ var pre = el("pre");
664
+ var codeNode = document.createElement("code");
665
+ codeNode.textContent = activeSegment.raw || "";
666
+ pre.appendChild(codeNode);
667
+ err.appendChild(pre);
668
+ body.appendChild(err);
669
+ }
670
+ } else {
671
+ // A staged new slide (or a beat with no board yet): the agent builds its
672
+ // storyboard once the story round is sent.
673
+ var placeholder = el("div", "plan-dossier-new-slide");
674
+ placeholder.innerHTML =
675
+ ICONS.ruler +
676
+ "<p><strong>Storyline edited.</strong> Sending asks the agent to build storyboards and content for your new and renamed slides — add comments below if you like.</p>";
677
+ body.appendChild(placeholder);
678
+ }
679
+
680
+ var note = el("div", "plan-storyboard-draft-note");
681
+ note.innerHTML =
682
+ ICONS.ruler +
683
+ "<p><strong>Draft storyboard.</strong> A rough sketch of what goes where — it guides the design agent, and the finished slides will follow your corporate design.</p>";
684
+ body.appendChild(note);
685
+
686
+ root.appendChild(body);
687
+ surfaceEl.appendChild(dossier);
688
+ }
689
+
690
+ // ---------- storyline tab strip ----------
691
+ function renderStrip(slides, committed, committedMessages, storyboardData, canEdit, activeIndex) {
692
+ var section = el("section", "plan-storyline");
693
+ var head = el("div", "plan-storyline-head");
694
+ head.appendChild(el("span", "plan-storyline-label", "Red thread"));
695
+ section.appendChild(head);
696
+
697
+ var track = el("ol", "plan-storyline-track");
698
+ section.appendChild(track);
699
+
700
+ var cardEls = [];
701
+ var gapEls = [];
702
+ var stripDrag = null;
703
+ var dropGap = null;
704
+
705
+ function paintGaps() {
706
+ for (var i = 0; i < gapEls.length; i++) {
707
+ if (stripDrag && dropGap === i) gapEls[i].setAttribute("data-active", "true");
708
+ else gapEls[i].removeAttribute("data-active");
709
+ }
710
+ }
711
+ function nearestGap(cx, cy) {
712
+ var best = 0;
713
+ var bd = Infinity;
714
+ for (var i = 0; i < slides.length; i++) {
715
+ var card = cardEls[i];
716
+ if (!card) continue;
717
+ var r = card.getBoundingClientRect();
718
+ var dy = cy - (r.top + r.height / 2);
719
+ var before = (cx - r.left) * (cx - r.left) + dy * dy;
720
+ if (before < bd) {
721
+ bd = before;
722
+ best = i;
723
+ }
724
+ var after = (cx - r.right) * (cx - r.right) + dy * dy;
725
+ if (after < bd) {
726
+ bd = after;
727
+ best = i + 1;
728
+ }
729
+ }
730
+ return best;
731
+ }
732
+
733
+ // Sequence-only edits (reorder / remove) cascade to the storyboards now;
734
+ // renames / inserts stage a draft the agent has to build.
735
+ function beatCommit(next) {
736
+ var sequenceOnly = next.every(function (s) {
737
+ return s.originalIndex !== null && committedMessages[s.originalIndex] === s.message;
738
+ });
739
+ if (sequenceOnly) {
740
+ S.storyDraft = null;
741
+ var applied = PLAN.applyPlanBackbone(S.value, next);
742
+ if (applied) S.value = applied;
743
+ } else {
744
+ S.storyDraft = next;
745
+ }
746
+ approveArmed = false; // a storyline edit disarms a pending approve-confirm
747
+ refreshChrome();
748
+ renderComposer();
749
+ renderSurface();
750
+ }
751
+ function renameSlide(index, message) {
752
+ var cur = slides[index];
753
+ if (!cur || cur.message === message) {
754
+ editingKey = null;
755
+ renderSurface();
756
+ return;
757
+ }
758
+ editingKey = null;
759
+ beatCommit(
760
+ slides.map(function (s, i) {
761
+ return i === index ? { key: s.key, originalIndex: s.originalIndex, message: message } : s;
762
+ }),
763
+ );
764
+ }
765
+ function insertSlideAt(position) {
766
+ newSlideCounter += 1;
767
+ var next = slides.slice();
768
+ next.splice(position, 0, {
769
+ key: "new-" + newSlideCounter,
770
+ originalIndex: null,
771
+ message: "New slide",
772
+ });
773
+ S.selectedIndex = position;
774
+ beatCommit(next);
775
+ }
776
+ function removeSlide(index) {
777
+ if (slides.length <= 1) return;
778
+ var next = slides.filter(function (_, i) {
779
+ return i !== index;
780
+ });
781
+ if (index < S.selectedIndex) S.selectedIndex = S.selectedIndex - 1;
782
+ else if (index === S.selectedIndex) S.selectedIndex = Math.min(index, slides.length - 2);
783
+ beatCommit(next);
784
+ }
785
+ function reorderTo(from, gap) {
786
+ var next = slides.slice();
787
+ var moved = next.splice(from, 1)[0];
788
+ if (!moved) return;
789
+ var target = from < gap ? gap - 1 : gap;
790
+ if (target === from) return;
791
+ next.splice(target, 0, moved);
792
+ S.selectedIndex = target;
793
+ beatCommit(next);
794
+ }
795
+
796
+ function gapAt(position) {
797
+ var li = el("li", "plan-storyline-gap");
798
+ li.setAttribute("aria-hidden", canEdit ? "false" : "true");
799
+ if (canEdit) {
800
+ var btn = el("button", "plan-storyline-insert", ICONS.plus);
801
+ btn.type = "button";
802
+ btn.title = "Insert slide here";
803
+ btn.setAttribute("aria-label", "Insert slide here");
804
+ btn.onclick = function () {
805
+ insertSlideAt(position);
806
+ };
807
+ li.appendChild(btn);
808
+ }
809
+ gapEls[position] = li;
810
+ return li;
811
+ }
812
+
813
+ slides.forEach(function (item, index) {
814
+ track.appendChild(gapAt(index));
815
+
816
+ var li = el(
817
+ "li",
818
+ "plan-storyline-beat" +
819
+ (index === activeIndex ? " plan-storyline-beat--active" : "") +
820
+ (canEdit ? " plan-storyline-beat--draggable" : "") +
821
+ (editingKey === item.key ? " plan-storyline-beat--editing" : ""),
822
+ );
823
+ cardEls[index] = li;
824
+
825
+ var numeral = S.storyDraft ? index + 1 : (storyboardData[index] && storyboardData[index].slide) || index + 1;
826
+ var slideNo = el("div", "plan-storyline-beat-slide");
827
+ slideNo.setAttribute("aria-hidden", "true");
828
+ slideNo.textContent = numeral;
829
+ li.appendChild(slideNo);
830
+
831
+ var textEl = el("div", "plan-storyline-beat-text");
832
+ textEl.setAttribute("role", "textbox");
833
+ textEl.textContent = item.message;
834
+ var isEditing = canEdit && editingKey === item.key;
835
+ if (isEditing) {
836
+ textEl.contentEditable = "true";
837
+ textEl.spellcheck = true;
838
+ focusAfterRender = textEl;
839
+ // Commit-once guard: renameSlide re-renders and detaches this focused
840
+ // node, firing a second blur that must not commit again.
841
+ var renamed = false;
842
+ textEl.addEventListener("blur", function () {
843
+ if (renamed) return;
844
+ renamed = true;
845
+ var t = (textEl.textContent || "").replace(/\s+/g, " ").trim();
846
+ if (t) renameSlide(index, t);
847
+ else {
848
+ editingKey = null;
849
+ renderSurface();
850
+ }
851
+ });
852
+ textEl.addEventListener("keydown", function (e) {
853
+ if (e.key === "Enter") {
854
+ e.preventDefault();
855
+ textEl.blur();
856
+ } else if (e.key === "Escape") {
857
+ textEl.textContent = item.message;
858
+ editingKey = null;
859
+ textEl.blur();
860
+ }
861
+ });
862
+ }
863
+ li.appendChild(textEl);
864
+
865
+ if (canEdit) {
866
+ var rm = el("button", "plan-storyline-beat-remove", ICONS.x);
867
+ rm.type = "button";
868
+ rm.setAttribute("data-no-drag", "");
869
+ rm.title = "Remove slide";
870
+ rm.setAttribute("aria-label", "Remove slide");
871
+ rm.disabled = slides.length <= 1;
872
+ rm.addEventListener("pointerdown", function (e) {
873
+ e.stopPropagation();
874
+ });
875
+ rm.addEventListener("click", function () {
876
+ removeSlide(index);
877
+ });
878
+ li.appendChild(rm);
879
+ }
880
+
881
+ // Pointer: drag ≥ threshold reorders; a click selects; a click on the
882
+ // active tab (when editable) enters rename mode.
883
+ li.addEventListener("pointerdown", function (e) {
884
+ if (e.button !== 0) return;
885
+ if (editingKey === item.key) return;
886
+ if (e.target.closest && e.target.closest("[data-no-drag]")) return;
887
+ stripDrag = {
888
+ index: index,
889
+ key: item.key,
890
+ pointerId: e.pointerId,
891
+ ox: e.clientX,
892
+ oy: e.clientY,
893
+ moved: false,
894
+ };
895
+ try {
896
+ li.setPointerCapture(e.pointerId);
897
+ } catch {
898
+ /* headless */
899
+ }
900
+ });
901
+ li.addEventListener("pointermove", function (e) {
902
+ if (!stripDrag || stripDrag.pointerId !== e.pointerId) return;
903
+ if (!canEdit) return;
904
+ var dx = e.clientX - stripDrag.ox;
905
+ var dy = e.clientY - stripDrag.oy;
906
+ if (!stripDrag.moved && Math.hypot(dx, dy) < BEAT_DRAG_THRESHOLD) return;
907
+ stripDrag.moved = true;
908
+ li.classList.add("plan-storyline-beat--dragging");
909
+ li.style.transform = "translate(" + dx + "px," + dy + "px)";
910
+ dropGap = nearestGap(e.clientX, e.clientY);
911
+ paintGaps();
912
+ });
913
+ function endBeat(e) {
914
+ if (!stripDrag || stripDrag.pointerId !== e.pointerId) return;
915
+ try {
916
+ li.releasePointerCapture(e.pointerId);
917
+ } catch {
918
+ /* ignore */
919
+ }
920
+ var moved = stripDrag.moved;
921
+ var gap = dropGap;
922
+ var idx = stripDrag.index;
923
+ var key = stripDrag.key;
924
+ stripDrag = null;
925
+ dropGap = null;
926
+ li.style.transform = "";
927
+ li.classList.remove("plan-storyline-beat--dragging");
928
+ paintGaps();
929
+ if (moved) {
930
+ if (gap !== null) reorderTo(idx, gap);
931
+ } else if (idx !== activeIndex) {
932
+ S.selectedIndex = idx;
933
+ renderSurface();
934
+ } else if (canEdit) {
935
+ editingKey = key;
936
+ renderSurface();
937
+ }
938
+ }
939
+ li.addEventListener("pointerup", endBeat);
940
+ li.addEventListener("pointercancel", function (e) {
941
+ if (!stripDrag || stripDrag.pointerId !== e.pointerId) return;
942
+ stripDrag = null;
943
+ dropGap = null;
944
+ li.style.transform = "";
945
+ li.classList.remove("plan-storyline-beat--dragging");
946
+ paintGaps();
947
+ });
948
+
949
+ track.appendChild(li);
950
+ });
951
+ track.appendChild(gapAt(slides.length));
952
+
953
+ return section;
954
+ }
955
+
956
+ // ---------- storyboard canvas ----------
957
+ function fittedFontSize(value, variant) {
958
+ var len = value.trim().length;
959
+ if (variant === "element") return Math.max(8.5, 12.5 - Math.max(0, len - 14) * 0.22);
960
+ return Math.max(8, 13 - Math.max(0, len - 12) * 0.3);
961
+ }
962
+ function compactSingleLine(text, variant) {
963
+ var lim = variant === "element" ? PLAN.MAX_DESIGN_ELEMENT_LENGTH : PLAN.MAX_CONTENT_LENGTH;
964
+ return text
965
+ .replace(/\u00a0/g, " ")
966
+ .replace(/\s+/g, " ")
967
+ .trim()
968
+ .slice(0, lim);
969
+ }
970
+ function notesToText(notes) {
971
+ return notes && notes.length ? notes.join("\n") : "";
972
+ }
973
+ function textToNotes(text) {
974
+ var notes = text
975
+ .replace(/\u00a0/g, " ")
976
+ .split(/\r?\n/)
977
+ .map(function (l) {
978
+ return l.trim();
979
+ })
980
+ .filter(Boolean);
981
+ return notes.length ? notes : undefined;
982
+ }
983
+ function updateBlock(data, blockId, patch) {
984
+ var blocks = data.blocks.map(function (b) {
985
+ if (b.id !== blockId) return b;
986
+ var next = {};
987
+ for (var k in b) next[k] = b[k];
988
+ for (var p in patch) next[p] = patch[p];
989
+ return next;
990
+ });
991
+ return withBlocks(data, blocks);
992
+ }
993
+ function withBlocks(data, blocks) {
994
+ var next = {};
995
+ for (var k in data) next[k] = data[k];
996
+ next.blocks = blocks;
997
+ return next;
998
+ }
999
+
1000
+ // Commit a storyboard mutation into S.value and re-render the surface. Safe to
1001
+ // full-re-render because every canvas edit commits on blur / pointerup, so no
1002
+ // field is focused at commit time.
1003
+ function commitStoryboard(segment, nextData) {
1004
+ if (locked()) return;
1005
+ S.value = PLAN.replaceStoryboardSegment(S.value, segment, nextData);
1006
+ approveArmed = false; // a storyboard edit disarms a pending approve-confirm
1007
+ refreshChrome();
1008
+ renderSurface();
1009
+ }
1010
+
1011
+ function renderField(variant, value, onCommit) {
1012
+ var wrap = el("div", "plan-storyboard-field");
1013
+ wrap.setAttribute("data-field", variant);
1014
+ var ctrl = el("div", "plan-storyboard-field-control");
1015
+ ctrl.setAttribute("role", "textbox");
1016
+ ctrl.setAttribute("aria-label", variant === "element" ? "Design element" : "Content");
1017
+ ctrl.textContent = value;
1018
+ ctrl.style.fontSize = fittedFontSize(value, variant) + "px";
1019
+ if (!locked()) {
1020
+ ctrl.contentEditable = "true";
1021
+ ctrl.spellcheck = true;
1022
+ }
1023
+ // Commit exactly once per edit session. A commit re-renders the surface,
1024
+ // which detaches this (still-focused) node and fires a SECOND blur; the
1025
+ // `committed` guard makes that second blur a no-op so we never splice the
1026
+ // stale `segment` offsets back in (double-commit → corrupted fences).
1027
+ var committed = false;
1028
+ function commit() {
1029
+ if (committed) return;
1030
+ committed = true;
1031
+ var normalized = compactSingleLine(ctrl.textContent || "", variant);
1032
+ ctrl.textContent = normalized;
1033
+ ctrl.style.fontSize = fittedFontSize(normalized, variant) + "px";
1034
+ if (normalized !== value) onCommit(normalized);
1035
+ }
1036
+ ctrl.addEventListener("input", function () {
1037
+ ctrl.style.fontSize = fittedFontSize(ctrl.textContent || "", variant) + "px";
1038
+ });
1039
+ ctrl.addEventListener("blur", commit);
1040
+ // Enter blurs and lets the blur handler be the single committer (matches the
1041
+ // beat-rename pattern) — never commit directly here, or the follow-on blur
1042
+ // would commit a second time against the mutated markdown.
1043
+ ctrl.addEventListener("keydown", function (e) {
1044
+ if (e.key !== "Enter") return;
1045
+ e.preventDefault();
1046
+ ctrl.blur();
1047
+ });
1048
+ wrap.appendChild(ctrl);
1049
+ return wrap;
1050
+ }
1051
+
1052
+ function renderBlock(frameEl, block, data, segment, canRemove) {
1053
+ var node = el("div", "plan-storyboard-block" + (locked() ? "" : " plan-storyboard-block--movable"));
1054
+ node.setAttribute("data-emphasis", block.emphasis || "secondary");
1055
+ node.style.left = (block.x / PLAN.STORYBOARD_WIDTH) * 100 + "%";
1056
+ node.style.top = (block.y / PLAN.STORYBOARD_HEIGHT) * 100 + "%";
1057
+ node.style.width = (block.w / PLAN.STORYBOARD_WIDTH) * 100 + "%";
1058
+ node.style.height = (block.h / PLAN.STORYBOARD_HEIGHT) * 100 + "%";
1059
+
1060
+ function change(patch) {
1061
+ commitStoryboard(segment, updateBlock(data, block.id, patch));
1062
+ }
1063
+
1064
+ node.appendChild(
1065
+ renderField("element", block.designElement, function (v) {
1066
+ change({ designElement: v });
1067
+ }),
1068
+ );
1069
+ node.appendChild(
1070
+ renderField("content", block.content, function (v) {
1071
+ change({ content: v });
1072
+ }),
1073
+ );
1074
+
1075
+ if (!locked() && canRemove) {
1076
+ var rm = el("button", "plan-storyboard-block-remove", ICONS.x);
1077
+ rm.type = "button";
1078
+ rm.setAttribute("data-no-drag", "");
1079
+ rm.title = "Remove container";
1080
+ rm.setAttribute("aria-label", "Remove container");
1081
+ rm.addEventListener("pointerdown", function (e) {
1082
+ e.stopPropagation();
1083
+ });
1084
+ rm.addEventListener("click", function () {
1085
+ if (data.blocks.length <= 1) return;
1086
+ commitStoryboard(
1087
+ segment,
1088
+ withBlocks(
1089
+ data,
1090
+ data.blocks.filter(function (b) {
1091
+ return b.id !== block.id;
1092
+ }),
1093
+ ),
1094
+ );
1095
+ });
1096
+ node.appendChild(rm);
1097
+ }
1098
+
1099
+ if (!locked()) {
1100
+ node.title = "Drag to reposition";
1101
+ wireBlockDrag(node, frameEl, block, change);
1102
+ var rz = el("button", "plan-storyboard-block-resize");
1103
+ rz.type = "button";
1104
+ rz.setAttribute("data-no-drag", "");
1105
+ rz.title = "Drag to resize";
1106
+ rz.setAttribute("aria-label", "Drag to resize");
1107
+ wireBlockResize(rz, frameEl, block, change);
1108
+ node.appendChild(rz);
1109
+ }
1110
+ return node;
1111
+ }
1112
+
1113
+ function clampPos(block, x, y) {
1114
+ var A = PLAN.BODY_SAFE_AREA;
1115
+ return {
1116
+ x: clamp(x, A.x, A.x + A.w - block.w),
1117
+ y: clamp(y, A.y, A.y + A.h - block.h),
1118
+ };
1119
+ }
1120
+ function clampSize(block, w, h) {
1121
+ var A = PLAN.BODY_SAFE_AREA;
1122
+ return {
1123
+ w: clamp(w, PLAN.MIN_BLOCK_WIDTH, A.x + A.w - block.x),
1124
+ h: clamp(h, PLAN.MIN_BLOCK_HEIGHT, A.y + A.h - block.y),
1125
+ };
1126
+ }
1127
+ function wireBlockDrag(node, frameEl, block, change) {
1128
+ var st = null;
1129
+ node.addEventListener("pointerdown", function (e) {
1130
+ if (locked() || e.button !== 0) return;
1131
+ if (e.target.closest && e.target.closest(".plan-storyboard-field, [data-no-drag]")) return;
1132
+ var rect = frameEl.getBoundingClientRect();
1133
+ if (!rect || rect.width <= 0) return;
1134
+ st = {
1135
+ pointerId: e.pointerId,
1136
+ ox: e.clientX,
1137
+ oy: e.clientY,
1138
+ sx: block.x,
1139
+ sy: block.y,
1140
+ scale: PLAN.STORYBOARD_WIDTH / rect.width,
1141
+ moved: false,
1142
+ };
1143
+ try {
1144
+ node.setPointerCapture(e.pointerId);
1145
+ } catch {
1146
+ /* headless */
1147
+ }
1148
+ });
1149
+ node.addEventListener("pointermove", function (e) {
1150
+ if (!st || st.pointerId !== e.pointerId) return;
1151
+ var dx = e.clientX - st.ox;
1152
+ var dy = e.clientY - st.oy;
1153
+ if (!st.moved && Math.hypot(dx, dy) < BLOCK_DRAG_THRESHOLD) return;
1154
+ st.moved = true;
1155
+ node.classList.add("plan-storyboard-block--dragging");
1156
+ var p = clampPos(block, st.sx + dx * st.scale, st.sy + dy * st.scale);
1157
+ node.style.left = (p.x / PLAN.STORYBOARD_WIDTH) * 100 + "%";
1158
+ node.style.top = (p.y / PLAN.STORYBOARD_HEIGHT) * 100 + "%";
1159
+ });
1160
+ function end(e) {
1161
+ if (!st || st.pointerId !== e.pointerId) return;
1162
+ try {
1163
+ node.releasePointerCapture(e.pointerId);
1164
+ } catch {
1165
+ /* ignore */
1166
+ }
1167
+ var s = st;
1168
+ st = null;
1169
+ if (!s.moved) {
1170
+ node.classList.remove("plan-storyboard-block--dragging");
1171
+ return;
1172
+ }
1173
+ var p = clampPos(block, s.sx + (e.clientX - s.ox) * s.scale, s.sy + (e.clientY - s.oy) * s.scale);
1174
+ var x = Math.round(p.x);
1175
+ var y = Math.round(p.y);
1176
+ if (x !== block.x || y !== block.y) change({ x: x, y: y });
1177
+ else node.classList.remove("plan-storyboard-block--dragging");
1178
+ }
1179
+ node.addEventListener("pointerup", end);
1180
+ node.addEventListener("pointercancel", function (e) {
1181
+ if (!st || st.pointerId !== e.pointerId) return;
1182
+ st = null;
1183
+ node.classList.remove("plan-storyboard-block--dragging");
1184
+ });
1185
+ }
1186
+ function wireBlockResize(handle, frameEl, block, change) {
1187
+ var st = null;
1188
+ handle.addEventListener("pointerdown", function (e) {
1189
+ if (locked() || e.button !== 0) return;
1190
+ e.stopPropagation();
1191
+ var rect = frameEl.getBoundingClientRect();
1192
+ if (!rect || rect.width <= 0) return;
1193
+ st = {
1194
+ pointerId: e.pointerId,
1195
+ ox: e.clientX,
1196
+ oy: e.clientY,
1197
+ sw: block.w,
1198
+ sh: block.h,
1199
+ scale: PLAN.STORYBOARD_WIDTH / rect.width,
1200
+ moved: false,
1201
+ };
1202
+ try {
1203
+ handle.setPointerCapture(e.pointerId);
1204
+ } catch {
1205
+ /* headless */
1206
+ }
1207
+ });
1208
+ handle.addEventListener("pointermove", function (e) {
1209
+ if (!st || st.pointerId !== e.pointerId) return;
1210
+ var dx = e.clientX - st.ox;
1211
+ var dy = e.clientY - st.oy;
1212
+ if (!st.moved && Math.hypot(dx, dy) < BLOCK_DRAG_THRESHOLD) return;
1213
+ st.moved = true;
1214
+ var node = handle.parentNode;
1215
+ var p = clampSize(block, st.sw + dx * st.scale, st.sh + dy * st.scale);
1216
+ node.classList.add("plan-storyboard-block--dragging");
1217
+ node.style.width = (p.w / PLAN.STORYBOARD_WIDTH) * 100 + "%";
1218
+ node.style.height = (p.h / PLAN.STORYBOARD_HEIGHT) * 100 + "%";
1219
+ });
1220
+ function end(e) {
1221
+ if (!st || st.pointerId !== e.pointerId) return;
1222
+ try {
1223
+ handle.releasePointerCapture(e.pointerId);
1224
+ } catch {
1225
+ /* ignore */
1226
+ }
1227
+ var s = st;
1228
+ st = null;
1229
+ if (!s.moved) return;
1230
+ var p = clampSize(block, s.sw + (e.clientX - s.ox) * s.scale, s.sh + (e.clientY - s.oy) * s.scale);
1231
+ var w = Math.round(p.w);
1232
+ var h = Math.round(p.h);
1233
+ if (w !== block.w || h !== block.h) change({ w: w, h: h });
1234
+ }
1235
+ handle.addEventListener("pointerup", end);
1236
+ handle.addEventListener("pointercancel", function (e) {
1237
+ if (st && st.pointerId === e.pointerId) st = null;
1238
+ });
1239
+ }
1240
+
1241
+ function renderDetailField(className, ariaLabel, value, multiline, onCommit) {
1242
+ var field = el("div", className);
1243
+ field.setAttribute("role", "textbox");
1244
+ field.setAttribute("aria-label", ariaLabel);
1245
+ if (multiline) field.setAttribute("aria-multiline", "true");
1246
+ field.textContent = value;
1247
+ if (!locked()) {
1248
+ field.contentEditable = "true";
1249
+ field.spellcheck = true;
1250
+ }
1251
+ // Commit-once guard: a commit re-renders the surface and detaches this
1252
+ // focused field, firing a second blur \u2014 which must not re-splice the stale
1253
+ // segment (double-commit \u2192 corrupted fences on the single-line beat field).
1254
+ var committed = false;
1255
+ function commit() {
1256
+ if (committed) return;
1257
+ committed = true;
1258
+ var normalized = (field.textContent || "").replace(/\u00a0/g, " ").trim();
1259
+ field.textContent = normalized;
1260
+ if (normalized !== value) onCommit(normalized);
1261
+ }
1262
+ field.addEventListener("blur", commit);
1263
+ if (!multiline) {
1264
+ // Enter blurs; the blur handler is the single committer.
1265
+ field.addEventListener("keydown", function (e) {
1266
+ if (e.key !== "Enter") return;
1267
+ e.preventDefault();
1268
+ field.blur();
1269
+ });
1270
+ }
1271
+ return field;
1272
+ }
1273
+
1274
+ function renderDetails(data, segment) {
1275
+ var aside = el("aside", "plan-storyboard-detail");
1276
+ aside.appendChild(el("div", "plan-storyboard-detail-label", "Story beat"));
1277
+ aside.appendChild(
1278
+ renderDetailField(
1279
+ "plan-storyboard-key-message",
1280
+ "Story beat",
1281
+ data.keyMessage || "",
1282
+ false,
1283
+ function (next) {
1284
+ if (next) {
1285
+ var d = {};
1286
+ for (var k in data) d[k] = data[k];
1287
+ d.keyMessage = next;
1288
+ commitStoryboard(segment, d);
1289
+ } else {
1290
+ var d2 = {};
1291
+ for (var k2 in data) if (k2 !== "keyMessage") d2[k2] = data[k2];
1292
+ commitStoryboard(segment, d2);
1293
+ }
1294
+ },
1295
+ ),
1296
+ );
1297
+ aside.appendChild(el("div", "plan-storyboard-detail-label", "Content draft"));
1298
+ var currentContent = notesToText(data.contentNotes);
1299
+ aside.appendChild(
1300
+ renderDetailField(
1301
+ "plan-storyboard-detail-text",
1302
+ "Content draft",
1303
+ currentContent,
1304
+ true,
1305
+ function (next) {
1306
+ var notes = textToNotes(next);
1307
+ if (notesToText(notes) === currentContent) return;
1308
+ var d = {};
1309
+ for (var k in data) if (k !== "contentNotes" && k !== "depthNotes") d[k] = data[k];
1310
+ if (notes) d.contentNotes = notes;
1311
+ commitStoryboard(segment, d);
1312
+ },
1313
+ ),
1314
+ );
1315
+ return aside;
1316
+ }
1317
+
1318
+ function renderCanvas(segment) {
1319
+ var data = segment.data;
1320
+ var figure = el("figure", "plan-storyboard");
1321
+
1322
+ var header = el("div", "plan-storyboard-header");
1323
+ header.appendChild(el("span", null, "Slide " + data.slide + " body storyboard"));
1324
+ var legend = el("span", "plan-storyboard-legend");
1325
+ legend.appendChild(el("span", "plan-storyboard-legend-item plan-storyboard-legend-item--element", "Design element"));
1326
+ legend.appendChild(el("span", "plan-storyboard-legend-item plan-storyboard-legend-item--content", "Content"));
1327
+ header.appendChild(legend);
1328
+ figure.appendChild(header);
1329
+
1330
+ var layout = el("div", "plan-storyboard-layout");
1331
+ var frame = el("div", "plan-storyboard-frame slide-emboss");
1332
+
1333
+ var top = el("div", "plan-storyboard-reserved plan-storyboard-reserved-top");
1334
+ top.appendChild(document.createTextNode("Template header/title area not planned here"));
1335
+ if (!locked()) {
1336
+ var add = el("button", "plan-storyboard-add", ICONS.plus + "<span>Add container</span>");
1337
+ add.type = "button";
1338
+ add.title = "Add container";
1339
+ add.setAttribute("aria-label", "Add container");
1340
+ add.disabled = data.blocks.length >= PLAN.MAX_BLOCKS;
1341
+ add.addEventListener("click", function () {
1342
+ if (data.blocks.length >= PLAN.MAX_BLOCKS) return;
1343
+ var serial = data.blocks.length + 1;
1344
+ while (
1345
+ data.blocks.some(function (b) {
1346
+ return b.id === "block-" + serial;
1347
+ })
1348
+ )
1349
+ serial += 1;
1350
+ var A = PLAN.BODY_SAFE_AREA;
1351
+ var w = 360;
1352
+ var h = 150;
1353
+ var offset = (data.blocks.length % 4) * 48;
1354
+ var nextBlocks = data.blocks.concat([
1355
+ {
1356
+ id: "block-" + serial,
1357
+ x: clamp(A.x + 40 + offset, A.x, A.x + A.w - w),
1358
+ y: clamp(A.y + 40 + offset, A.y, A.y + A.h - h),
1359
+ w: w,
1360
+ h: h,
1361
+ designElement: "container",
1362
+ content: "New content",
1363
+ },
1364
+ ]);
1365
+ commitStoryboard(segment, withBlocks(data, nextBlocks));
1366
+ });
1367
+ top.appendChild(add);
1368
+ }
1369
+ frame.appendChild(top);
1370
+
1371
+ var grid = el("div", "plan-storyboard-body");
1372
+ grid.setAttribute("aria-hidden", "true");
1373
+ frame.appendChild(grid);
1374
+
1375
+ var canRemove = !locked() && data.blocks.length > 1;
1376
+ data.blocks.forEach(function (block) {
1377
+ frame.appendChild(renderBlock(frame, block, data, segment, canRemove));
1378
+ });
1379
+
1380
+ var bottom = el("div", "plan-storyboard-reserved plan-storyboard-reserved-bottom", "Footer/template area not planned here");
1381
+ frame.appendChild(bottom);
1382
+
1383
+ layout.appendChild(frame);
1384
+ layout.appendChild(renderDetails(data, segment));
1385
+ figure.appendChild(layout);
1386
+
1387
+ if (segment.warnings && segment.warnings.length) {
1388
+ var ul = el("ul", "plan-storyboard-warnings");
1389
+ segment.warnings.forEach(function (w) {
1390
+ var li = document.createElement("li");
1391
+ li.textContent = w;
1392
+ ul.appendChild(li);
1393
+ });
1394
+ figure.appendChild(ul);
1395
+ }
1396
+ return figure;
1397
+ }
1398
+
1399
+ // ---------- chat composer bar ----------
1400
+ // Rebuilt on structural state changes (a storyline draft staged / discarded),
1401
+ // NOT on every keystroke (keystrokes only call refreshChrome). The typed
1402
+ // feedback survives a rebuild via feedbackText.
1403
+ function renderComposer() {
1404
+ if (!composerEl || waiting) return;
1405
+ composerEl.innerHTML = "";
1406
+
1407
+ if (S.storyDraft) {
1408
+ var banner = el("div", "sm-story-banner");
1409
+ banner.innerHTML =
1410
+ ICONS.sparkles +
1411
+ '<span class="txt"><b>Storyline edited.</b> Sending asks the agent to build storyboards and content for your new and renamed slides — add comments below if you like.</span>';
1412
+ var discard = el("button", "sm-story-discard", "Discard changes");
1413
+ discard.type = "button";
1414
+ discard.onclick = function () {
1415
+ S.storyDraft = null;
1416
+ approveArmed = false; // discarding the storyline disarms approve-confirm
1417
+ refreshChrome();
1418
+ renderComposer();
1419
+ renderSurface();
1420
+ };
1421
+ banner.appendChild(discard);
1422
+ composerEl.appendChild(banner);
1423
+ }
1424
+
1425
+ if (CFG.previousComment) {
1426
+ var prev = el("div", "sm-prev-note");
1427
+ prev.innerHTML = ICONS.ghost + '<span class="lbl">Previous round</span>';
1428
+ var txt = el("span", "txt");
1429
+ txt.textContent = CFG.previousComment;
1430
+ prev.appendChild(txt);
1431
+ composerEl.appendChild(prev);
1432
+ }
1433
+
1434
+ var row = el("div", "sm-composer-row");
1435
+ commentEl = el("textarea", "sm-chat-ta");
1436
+ commentEl.rows = 1;
1437
+ commentEl.placeholder = "What should change? Leave a note, or edit the plan directly.";
1438
+ commentEl.value = feedbackText;
1439
+ commentEl.addEventListener("input", function () {
1440
+ feedbackText = commentEl.value;
1441
+ approveArmed = false; // typing more feedback disarms a pending approve-confirm
1442
+ commentEl.style.height = "auto";
1443
+ commentEl.style.height = Math.min(commentEl.scrollHeight, 150) + "px";
1444
+ refreshChrome();
1445
+ });
1446
+ commentEl.addEventListener("keydown", function (e) {
1447
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") requestChanges();
1448
+ });
1449
+ row.appendChild(commentEl);
1450
+
1451
+ if (S.storyDraft) {
1452
+ // The round is sendable as-is — "send" grows the story-iteration label.
1453
+ sendBtn = el("button", "sm-send-pill", ICONS.sparkles + "<span>Iterate on the story</span>");
1454
+ sendBtn.type = "button";
1455
+ sendBtn.title = "Iterate on the story";
1456
+ } else {
1457
+ sendBtn = el("button", "sm-send", ICONS.send);
1458
+ sendBtn.type = "button";
1459
+ sendBtn.title = "Send feedback";
1460
+ sendBtn.setAttribute("aria-label", "Send feedback");
1461
+ }
1462
+ sendBtn.onclick = requestChanges;
1463
+ row.appendChild(sendBtn);
1464
+
1465
+ row.appendChild(el("div", "sm-composer-sep"));
1466
+
1467
+ approveBtn = el("button", "sm-approve");
1468
+ approveBtn.type = "button";
1469
+ approveBtn.innerHTML =
1470
+ '<span class="main">' +
1471
+ ICONS.check +
1472
+ "<span>Approve plan</span></span>" +
1473
+ '<span class="sub">the agent builds the slides next</span>';
1474
+ approveBtn.onclick = function () {
1475
+ if (S.storyDraft) return; // blocked while a storyline draft is staged
1476
+ // Approving with unsent feedback in the input discards it — confirm inline.
1477
+ if (commentText() && !approveArmed) {
1478
+ approveArmed = true;
1479
+ refreshChrome();
1480
+ return;
1481
+ }
1482
+ submit("approve");
1483
+ };
1484
+ row.appendChild(approveBtn);
1485
+
1486
+ composerEl.appendChild(row);
1487
+ refreshChrome();
1488
+ }
1489
+
1490
+ // ---------- waiting state (comments sent) ----------
1491
+ // The tab does NOT veil over on a comments round: the plan stays readable, the
1492
+ // surface locks, and the composer becomes the wait signal until the agent's
1493
+ // next `slidemind plan` round reloads this tab (heartbeat polling).
1494
+ function enterWaiting() {
1495
+ waiting = true;
1496
+ if (wsEl) wsEl.classList.add("is-waiting");
1497
+ renderSurface(); // rebuild the surface read-only (no contenteditable attrs)
1498
+ document.title = WAITING_TITLE; // the tab label signals the state from the tab bar
1499
+ if (composerEl) {
1500
+ composerEl.innerHTML = "";
1501
+ var w = el("div", "sm-wait");
1502
+ w.appendChild(el("div", "sm-spin"));
1503
+ waitTextEl = el(
1504
+ "div",
1505
+ null,
1506
+ "<b>Feedback sent.</b> The agent is revising the plan — this tab refreshes " +
1507
+ "automatically when the next round is ready. Feel free to keep reading, " +
1508
+ "or come back later.",
1509
+ );
1510
+ w.appendChild(waitTextEl);
1511
+ composerEl.appendChild(w);
1512
+ }
1513
+ // The permission prompt was raised at the send click (a user gesture, so the
1514
+ // browser shows it); once granted, tell the user a notification will come.
1515
+ (notifyPermPromise || Promise.resolve(notifyPermission())).then(function (perm) {
1516
+ if (perm === "granted" && waitTextEl) {
1517
+ waitTextEl.innerHTML += " You'll get a notification the moment it's ready.";
1518
+ }
1519
+ });
1520
+ }
1521
+
1522
+ // ---------- next-round attention ----------
1523
+ function notifyPermission() {
1524
+ try {
1525
+ return "Notification" in window ? Notification.permission : "unsupported";
1526
+ } catch {
1527
+ return "unsupported";
1528
+ }
1529
+ }
1530
+ function requestNotifyPermission() {
1531
+ try {
1532
+ if (!("Notification" in window)) return Promise.resolve("unsupported");
1533
+ if (Notification.permission !== "default") return Promise.resolve(Notification.permission);
1534
+ return Promise.resolve(Notification.requestPermission());
1535
+ } catch {
1536
+ return Promise.resolve("unsupported");
1537
+ }
1538
+ }
1539
+ // The reload wipes this JS context, so the "announce the new round" wish is
1540
+ // parked in sessionStorage and honoured by the NEXT page load.
1541
+ function markRoundReadyAndReload() {
1542
+ try {
1543
+ sessionStorage.setItem(ROUND_READY_FLAG, "1");
1544
+ } catch {
1545
+ /* storage blocked — the reload still lands, just without the announce */
1546
+ }
1547
+ location.reload();
1548
+ }
1549
+ function announceNewRound() {
1550
+ var flagged = false;
1551
+ try {
1552
+ flagged = sessionStorage.getItem(ROUND_READY_FLAG) === "1";
1553
+ if (flagged) sessionStorage.removeItem(ROUND_READY_FLAG);
1554
+ } catch {
1555
+ /* no storage — nothing to announce */
1556
+ }
1557
+ if (!flagged) return;
1558
+ try {
1559
+ window.focus();
1560
+ } catch {
1561
+ /* ignore */
1562
+ }
1563
+ if (!document.hidden) return; // user is already looking at it
1564
+ var origTitle = document.title;
1565
+ var restore = function () {
1566
+ document.title = origTitle;
1567
+ document.removeEventListener("visibilitychange", onVis);
1568
+ window.removeEventListener("focus", restore);
1569
+ };
1570
+ var onVis = function () {
1571
+ if (!document.hidden) restore();
1572
+ };
1573
+ document.title = ATTENTION_TITLE;
1574
+ document.addEventListener("visibilitychange", onVis);
1575
+ window.addEventListener("focus", restore);
1576
+ try {
1577
+ if (notifyPermission() === "granted") {
1578
+ var n = new Notification("slidemind — plan ready", {
1579
+ body: "The revised plan is ready for your review. Click to jump back to the tab.",
1580
+ tag: "slidemind-plan-round",
1581
+ });
1582
+ n.onclick = function () {
1583
+ try {
1584
+ window.focus();
1585
+ } catch {
1586
+ /* ignore */
1587
+ }
1588
+ n.close();
1589
+ };
1590
+ }
1591
+ } catch {
1592
+ /* notification construction can throw on some platforms — title marker stands */
1593
+ }
1594
+ }
1595
+
1596
+ // ---------- submit ----------
1597
+ function requestChanges() {
1598
+ if (waiting) return;
1599
+ if (S.storyDraft) {
1600
+ var staged = PLAN.stageStoryIteration(S.value, S.storyDraft);
1601
+ if (!staged) return;
1602
+ var typed = commentText();
1603
+ pendingComment = typed ? staged.feedback + "\n\n" + typed : staged.feedback;
1604
+ pendingEdited = staged.editedPlanMarkdown;
1605
+ } else {
1606
+ if (!commentText()) return;
1607
+ pendingComment = commentText();
1608
+ pendingEdited = planWasEdited() ? S.value : null;
1609
+ }
1610
+ // Ask for notification permission HERE — the click is a user gesture, the
1611
+ // only context where the browser will actually show the prompt.
1612
+ notifyPermPromise = requestNotifyPermission();
1613
+ submit("comments");
1614
+ }
1615
+ function submit(decision) {
1616
+ if (submitted) return;
1617
+ submitted = true;
1618
+ var payload = {
1619
+ decision: decision,
1620
+ comment: decision === "comments" ? pendingComment : null,
1621
+ editedPlan: decision === "comments" ? pendingEdited : planWasEdited() ? S.value : null,
1622
+ };
1623
+ fetch(CFG.endpoint, {
1624
+ method: "POST",
1625
+ headers: { "Content-Type": "application/json" },
1626
+ body: JSON.stringify(payload),
1627
+ })
1628
+ .then(function () {
1629
+ if (decision === "approve") {
1630
+ showApproved();
1631
+ } else {
1632
+ enterWaiting();
1633
+ startNextRoundPolling();
1634
+ }
1635
+ })
1636
+ .catch(function () {
1637
+ submitted = false;
1638
+ alert("Could not reach the slidemind plan server — is the terminal command still running?");
1639
+ });
1640
+ }
1641
+ function requestTabClose() {
1642
+ setTimeout(function () {
1643
+ try {
1644
+ window.open("", "_self");
1645
+ window.close();
1646
+ } catch {
1647
+ // Some browsers only allow script-opened tabs to close themselves.
1648
+ }
1649
+ }, 120);
1650
+ }
1651
+ // After a comments round the tab stays open and polls the heartbeat so the
1652
+ // NEXT run (which binds the same deterministic port) reloads this tab instead
1653
+ // of opening a fresh browser window. The poll reports this tab's round nonce
1654
+ // and its visibility: only a VISIBLE tab may claim the next round for reuse —
1655
+ // a tab buried in a background window must not swallow the round invisibly, so
1656
+ // the server opens a fresh browser tab instead and answers `action: "close"`
1657
+ // here, which retires this tab with a notice rather than reloading it into a
1658
+ // hidden duplicate.
1659
+ function startNextRoundPolling() {
1660
+ var startedAt = Date.now();
1661
+ var pollMs = CFG.pollMs || 1000;
1662
+ var ttl = CFG.pollTtlMs || 35 * 60 * 1000;
1663
+ var hb = CFG.heartbeat || "/__plan/heartbeat";
1664
+ var timer = setInterval(function () {
1665
+ if (Date.now() - startedAt > ttl) {
1666
+ clearInterval(timer);
1667
+ showSessionEnded();
1668
+ return;
1669
+ }
1670
+ var visible = document.visibilityState === "visible" ? "1" : "0";
1671
+ var url =
1672
+ hb +
1673
+ (hb.indexOf("?") < 0 ? "?" : "&") +
1674
+ "round=" +
1675
+ encodeURIComponent(CFG.round || "") +
1676
+ "&visible=" +
1677
+ visible;
1678
+ fetch(url, { cache: "no-store" })
1679
+ .then(function (r) {
1680
+ return r.ok ? r.json() : null;
1681
+ })
1682
+ .then(function (data) {
1683
+ if (!data || data.service !== "slidemind-plan" || !data.round) return;
1684
+ if (data.round === CFG.round) return;
1685
+ if (data.action === "close") {
1686
+ clearInterval(timer);
1687
+ showSuperseded();
1688
+ return;
1689
+ }
1690
+ if (data.action === "wait") return; // grace window still deciding — keep polling
1691
+ clearInterval(timer);
1692
+ markRoundReadyAndReload();
1693
+ })
1694
+ .catch(function () {
1695
+ /* server down between rounds — keep polling */
1696
+ });
1697
+ }, pollMs);
1698
+ }
1699
+
1700
+ // ---------- done veil ----------
1701
+ function showVeil(statusHtml, subText, subPulse) {
1702
+ if (bar) bar.style.display = "none";
1703
+ if (wsEl) wsEl.style.display = "none";
1704
+ var existing = document.querySelector(".sm-veil[data-sm-overlay]");
1705
+ if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
1706
+ var veil = el("div", "sm-o sm-veil");
1707
+ veil.setAttribute("data-sm-overlay", "1");
1708
+ veil.appendChild(logoNode("sm-logo", "-v"));
1709
+ veil.appendChild(el("div", "word", wordmarkHtml()));
1710
+ veil.appendChild(el("div", "status", statusHtml));
1711
+ veil.appendChild(el("div", "sub" + (subPulse ? " sm-pulse" : ""), subText));
1712
+ document.body.appendChild(veil);
1713
+ }
1714
+ function showApproved() {
1715
+ showVeil(
1716
+ ICONS.check + "<span>Plan approved</span>",
1717
+ "The agent is building your slides now — you can close this tab.",
1718
+ true,
1719
+ );
1720
+ setTimeout(requestTabClose, APPROVE_CLOSE_DELAY_MS);
1721
+ }
1722
+ function showSessionEnded() {
1723
+ showVeil("<span>Session ended</span>", "You can close this tab.");
1724
+ }
1725
+ function showSuperseded() {
1726
+ document.title = "Plan review moved — slidemind";
1727
+ showVeil(
1728
+ "<span>Continued in a new tab</span>",
1729
+ "The next plan round opened in a fresh browser tab — you can close this one.",
1730
+ );
1731
+ }
1732
+
1733
+ // ---------- assets ----------
1734
+ function injectAssets() {
1735
+ var style = el("style");
1736
+ style.id = "sm-plan-style";
1737
+ style.textContent = CSS;
1738
+ document.head.appendChild(style);
1739
+
1740
+ var pre1 = el("link");
1741
+ pre1.rel = "preconnect";
1742
+ pre1.href = "https://fonts.googleapis.com";
1743
+ var pre2 = el("link");
1744
+ pre2.rel = "preconnect";
1745
+ pre2.href = "https://fonts.gstatic.com";
1746
+ pre2.crossOrigin = "anonymous";
1747
+ var fonts = el("link");
1748
+ fonts.rel = "stylesheet";
1749
+ fonts.href =
1750
+ "https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&family=Urbanist:wght@300;400&display=swap";
1751
+ document.head.appendChild(pre1);
1752
+ document.head.appendChild(pre2);
1753
+ document.head.appendChild(fonts);
1754
+ }
1755
+
1756
+ // ---------- wire up ----------
1757
+ function init() {
1758
+ if (!document.body) {
1759
+ setTimeout(init, 20);
1760
+ return;
1761
+ }
1762
+ // The server-rendered #plan-root is the no-JS fallback only; the overlay
1763
+ // renders the interactive surface from S.value, so drop the static node.
1764
+ var stale = document.getElementById("plan-root");
1765
+ if (stale && stale.parentNode) stale.parentNode.removeChild(stale);
1766
+
1767
+ injectAssets();
1768
+ buildChrome();
1769
+ buildWorkspace();
1770
+ announceNewRound(); // notification + title marker when this load IS the next round
1771
+ }
1772
+
1773
+ if (document.readyState === "loading") {
1774
+ document.addEventListener("DOMContentLoaded", init);
1775
+ } else {
1776
+ init();
1777
+ }
1778
+ })();