slidemind 0.1.8 → 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.
- package/CLAUDE.md +7 -6
- package/README.md +19 -7
- package/bin/slidemind.mjs +377 -39
- package/package.json +2 -1
- package/skills/create-slides/SKILL.md +66 -15
- package/skills/search/SKILL.md +15 -2
- package/src/bridge/build-and-push.mjs +11 -0
- package/src/bridge/bundle.mjs +15 -4
- package/src/bridge/capture.mjs +20 -2
- package/src/bridge/client.mjs +99 -0
- package/src/builders/build.mjs +53 -23
- package/src/builders/gate-server.mjs +163 -22
- package/src/builders/plan-markdown-edit.mjs +120 -0
- package/src/builders/plan-overlay.js +1261 -230
- package/src/builders/plan.mjs +146 -25
- package/src/builders/red-thread-backbone.mjs +194 -0
- package/src/builders/review-overlay.js +44 -7
- package/src/builders/review.mjs +72 -10
- package/src/builders/storyboard-markdown.mjs +118 -0
- package/src/builders/storyboard-schema.mjs +180 -0
- package/src/builders/visual-families.mjs +32 -0
- package/src/pptx/app-props.mjs +222 -0
- package/src/pptx/hidden-ole.mjs +125 -0
- package/src/pptx/image-extensions.mjs +190 -0
- package/src/pptx/ooxml.mjs +169 -0
- package/src/pptx/read-local.mjs +206 -0
- package/src/pptx/slide-swap.mjs +429 -0
- package/src/pptx/zip.mjs +30 -0
|
@@ -4,23 +4,27 @@
|
|
|
4
4
|
// `slidemind plan` local server injects it), and dormant inside an iframe /
|
|
5
5
|
// thumbnail render, so nothing runs on an ordinary page.
|
|
6
6
|
//
|
|
7
|
-
// The gate is
|
|
8
|
-
//
|
|
9
|
-
// the
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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).
|
|
12
15
|
//
|
|
13
16
|
// The decision lives in ONE always-visible chat-style composer bar pinned at
|
|
14
|
-
// the bottom
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// read-only
|
|
22
|
-
// note while
|
|
23
|
-
// `slidemind plan` round, which reloads
|
|
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.
|
|
24
28
|
// - Approve → decision "approve" (unsent feedback arm-confirms first),
|
|
25
29
|
// then the full-screen brand animation — logo + drifting
|
|
26
30
|
// wordmark + "the agent is building your slides" — before the
|
|
@@ -40,25 +44,45 @@
|
|
|
40
44
|
if (window.top !== window.self) return;
|
|
41
45
|
if (new URLSearchParams(location.search).get("thumb") === "1") return;
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
|
|
45
51
|
var APPROVE_CLOSE_DELAY_MS = 2400; // long enough to read the build animation
|
|
46
52
|
var ROUND_READY_FLAG = "slidemind-plan-round-ready"; // sessionStorage marker across the reload
|
|
47
53
|
var ATTENTION_TITLE = "● Plan ready — slidemind";
|
|
48
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
|
|
49
57
|
|
|
50
58
|
// ---------- state ----------
|
|
51
59
|
var submitted = false;
|
|
52
60
|
var approveArmed = false; // inline "discard unsent feedback?" confirm state
|
|
53
|
-
var
|
|
61
|
+
var waiting = false; // comments round sent → the surface locks read-only
|
|
54
62
|
var baseline = CFG.planBody || ""; // the served plan body; edits diff against it
|
|
55
|
-
var renderTimer = null;
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
|
|
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;
|
|
59
79
|
var waitTextEl = null;
|
|
60
80
|
var notifyPermPromise = null; // resolved Notification permission, asked at the send gesture
|
|
61
81
|
|
|
82
|
+
function locked() {
|
|
83
|
+
return waiting;
|
|
84
|
+
}
|
|
85
|
+
|
|
62
86
|
// ---------- brand assets (identical to review-overlay.js) ----------
|
|
63
87
|
var LOGO_SVG =
|
|
64
88
|
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="160 160 960 540" preserveAspectRatio="none">' +
|
|
@@ -105,16 +129,24 @@
|
|
|
105
129
|
var ICONS = {
|
|
106
130
|
check:
|
|
107
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>',
|
|
108
|
-
panel:
|
|
109
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/></svg>',
|
|
110
132
|
ghost:
|
|
111
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>',
|
|
112
134
|
send:
|
|
113
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>',
|
|
114
144
|
};
|
|
115
145
|
|
|
116
|
-
// ---------- styles (chrome copied from review-overlay.js;
|
|
117
|
-
//
|
|
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) ----------
|
|
118
150
|
var CSS =
|
|
119
151
|
"@keyframes sm-drift-a{0%,100%{transform:translateX(-5px)}50%{transform:translateX(5px)}}" +
|
|
120
152
|
"@keyframes sm-drift-b{0%,100%{transform:translateX(5px)}50%{transform:translateX(-5px)}}" +
|
|
@@ -124,8 +156,15 @@
|
|
|
124
156
|
"@keyframes sm-pulse{0%,100%{opacity:.45}50%{opacity:.85}}" +
|
|
125
157
|
":root{--sm-bg:var(--deck-chrome-bg,#111113);--sm-surface:var(--deck-chrome-bg-2,#161618);" +
|
|
126
158
|
"--sm-border:var(--deck-chrome-border,#2b2b30);--sm-fg:var(--deck-chrome-fg,#ededed);" +
|
|
127
|
-
"--sm-dim:var(--deck-chrome-dim,#9a9aa0);--sm-
|
|
128
|
-
"--sm-
|
|
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);}" +
|
|
129
168
|
".sm-o,.sm-o *{box-sizing:border-box;}" +
|
|
130
169
|
/* header */
|
|
131
170
|
".sm-bar{position:fixed;top:0;left:0;right:0;z-index:2147483600;display:flex;align-items:center;" +
|
|
@@ -142,7 +181,7 @@
|
|
|
142
181
|
".sm-word .top{animation:sm-drift-a 20s ease-in-out infinite;}" +
|
|
143
182
|
".sm-word .bot{margin-left:.18em;animation:sm-drift-b 20s ease-in-out infinite;}" +
|
|
144
183
|
".sm-sep{width:1px;height:28px;background:var(--sm-border);}" +
|
|
145
|
-
".sm-meta{display:flex;flex-direction:column;gap:2px;}" +
|
|
184
|
+
".sm-meta{display:flex;flex-direction:column;gap:2px;min-width:0;}" +
|
|
146
185
|
".sm-meta .t{font-weight:600;font-size:13px;display:flex;align-items:center;gap:8px;}" +
|
|
147
186
|
".sm-meta .h{font-size:11.5px;color:var(--sm-dim);}" +
|
|
148
187
|
".sm-edited{font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--sm-warn);" +
|
|
@@ -153,72 +192,201 @@
|
|
|
153
192
|
"padding:8px 14px;font-family:var(--sm-font);font-weight:600;font-size:12.5px;cursor:pointer;" +
|
|
154
193
|
"transition:transform .14s ease,background .14s ease,box-shadow .14s ease,border-color .14s ease;white-space:nowrap;}" +
|
|
155
194
|
".sm-btn svg{width:15px;height:15px;flex-shrink:0;}" +
|
|
156
|
-
|
|
157
|
-
".sm-btn-primary:hover{background:#fff;transform:translateY(-1px);box-shadow:0 6px 18px rgba(0,0,0,.45);}" +
|
|
158
|
-
".sm-btn-primary svg{color:var(--sm-success);}" +
|
|
159
|
-
".sm-btn-primary.is-armed{background:#f6c454;color:#241a00;}" +
|
|
160
|
-
".sm-btn-primary.is-armed svg{color:#241a00;}" +
|
|
161
|
-
".sm-btn-ghost{background:transparent;border-color:var(--sm-border);color:var(--sm-fg);}" +
|
|
162
|
-
".sm-btn-ghost:hover{background:rgba(255,255,255,.06);border-color:#3a3a40;}" +
|
|
163
|
-
".sm-btn-ghost.is-on{background:rgba(169,194,255,.14);border-color:var(--sm-accent);color:#dbe6ff;}" +
|
|
164
|
-
".sm-btn.is-off{opacity:.4;cursor:not-allowed;pointer-events:none;}" +
|
|
165
|
-
/* workspace: editor + preview panes, comment dock */
|
|
195
|
+
/* workspace: one scrollable document surface + composer dock */
|
|
166
196
|
".sm-ws{position:fixed;left:0;right:0;top:var(--deck-chrome-h,62px);bottom:0;z-index:2147483540;" +
|
|
167
197
|
"display:flex;flex-direction:column;background:var(--sm-bg);}" +
|
|
168
|
-
".sm-
|
|
169
|
-
".sm-
|
|
170
|
-
|
|
171
|
-
".
|
|
172
|
-
"
|
|
173
|
-
".
|
|
174
|
-
"
|
|
175
|
-
".
|
|
176
|
-
|
|
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 */
|
|
177
338
|
".sm-composer{flex-shrink:0;display:flex;flex-direction:column;gap:8px;padding:11px 16px;" +
|
|
178
339
|
"border-top:1px solid var(--sm-border);background:var(--sm-surface);font-family:var(--sm-font);}" +
|
|
179
|
-
".sm-
|
|
180
|
-
"
|
|
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);}" +
|
|
181
350
|
".sm-prev-note svg{width:14px;height:14px;flex-shrink:0;margin-top:2px;color:#71717a;}" +
|
|
182
351
|
".sm-prev-note .lbl{font-weight:600;color:#a1a1aa;white-space:nowrap;}" +
|
|
183
352
|
".sm-prev-note .txt{white-space:pre-wrap;word-break:break-word;color:#b9b9be;}" +
|
|
184
353
|
".sm-composer-row{display:flex;align-items:flex-end;gap:10px;}" +
|
|
185
354
|
".sm-chat-ta{flex:1;min-height:40px;max-height:150px;resize:none;background:#0c0c0e;color:var(--sm-fg);" +
|
|
186
|
-
"border:1px solid var(--sm-border);border-radius:14px;padding:10px 15px;font:13px/1.5 var(--sm-font);" +
|
|
187
|
-
"outline:none;transition:border-color .15s;}" +
|
|
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;}" +
|
|
188
356
|
".sm-chat-ta:focus{border-color:var(--sm-accent);}.sm-chat-ta::placeholder{color:#6b6b72;}" +
|
|
189
|
-
".sm-send{width:38px;height:38px;border-radius:50%;border:0;cursor:pointer;flex-shrink:0;" +
|
|
190
|
-
"
|
|
191
|
-
"transition:transform .14s ease,box-shadow .14s ease,opacity .14s ease;}" +
|
|
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;}" +
|
|
192
359
|
".sm-send svg{width:16px;height:16px;margin:1px 1px 0 0;}" +
|
|
193
360
|
".sm-send:hover{transform:translateY(-1px) scale(1.05);box-shadow:0 8px 22px rgba(169,194,255,.35);}" +
|
|
194
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;}" +
|
|
195
369
|
".sm-composer-sep{width:1px;align-self:stretch;background:var(--sm-border);}" +
|
|
196
|
-
|
|
197
|
-
"
|
|
198
|
-
"border-radius:12px;padding:7px 16px;background:#f4f4f5;color:#0f0f0f;cursor:pointer;font-family:var(--sm-font);" +
|
|
199
|
-
"transition:transform .14s ease,background .14s ease,box-shadow .14s ease;white-space:nowrap;}" +
|
|
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;}" +
|
|
200
372
|
".sm-approve:hover{background:#fff;transform:translateY(-1px);box-shadow:0 6px 18px rgba(0,0,0,.45);}" +
|
|
201
373
|
".sm-approve .main{display:flex;align-items:center;gap:7px;font-weight:600;font-size:12.5px;}" +
|
|
202
374
|
".sm-approve .main svg{width:15px;height:15px;color:var(--sm-success);}" +
|
|
203
375
|
".sm-approve .sub{font-size:9.5px;font-weight:500;color:#61616a;}" +
|
|
204
376
|
".sm-approve.is-armed{background:#f6c454;color:#241a00;}" +
|
|
205
377
|
".sm-approve.is-armed .main svg{color:#241a00;}" +
|
|
206
|
-
|
|
378
|
+
".sm-approve.is-off{opacity:.45;cursor:not-allowed;pointer-events:none;}" +
|
|
379
|
+
/* waiting state */
|
|
207
380
|
"@keyframes sm-rotate{to{transform:rotate(360deg)}}" +
|
|
208
|
-
".sm-wait{display:flex;align-items:center;gap:12px;padding:7px 2px;font-size:12.5px;" +
|
|
209
|
-
"color:var(--sm-dim);line-height:1.5;}" +
|
|
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;}" +
|
|
210
382
|
".sm-wait b{color:var(--sm-fg);font-weight:600;}" +
|
|
211
|
-
".sm-spin{width:17px;height:17px;border-radius:50%;border:2px solid var(--sm-border);" +
|
|
212
|
-
"border-top-color:var(--sm-accent);animation:sm-rotate .9s linear infinite;flex-shrink:0;}" +
|
|
213
|
-
".sm-ws.is-waiting .sm-ed-ta{opacity:.55;}" +
|
|
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;}" +
|
|
214
384
|
/* done veil */
|
|
215
|
-
".sm-veil{position:fixed;inset:0;z-index:2147483647;display:flex;flex-direction:column;align-items:center;" +
|
|
216
|
-
"
|
|
217
|
-
"background:radial-gradient(ellipse at center,rgba(16,16,18,.93),rgba(5,5,7,.98));" +
|
|
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));" +
|
|
218
387
|
"-webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);animation:sm-veil-in .4s ease;}" +
|
|
219
388
|
".sm-veil .sm-logo{width:188px;height:106px;border-radius:14px;animation:sm-rise .6s cubic-bezier(.2,.7,.2,1) both;}" +
|
|
220
|
-
".sm-veil .word{display:flex;flex-direction:column;gap:6px;font-family:var(--sm-word);font-weight:300;" +
|
|
221
|
-
"font-size:36px;letter-spacing:.6em;text-align:center;animation:sm-rise .6s .06s 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;}" +
|
|
222
390
|
".sm-veil .word b{font-weight:300;}" +
|
|
223
391
|
".sm-veil .word .top{animation:sm-drift-a 18s ease-in-out infinite;}" +
|
|
224
392
|
".sm-veil .word .bot{margin-left:.3em;animation:sm-drift-b 18s ease-in-out infinite;}" +
|
|
@@ -234,9 +402,6 @@
|
|
|
234
402
|
if (html != null) e.innerHTML = html;
|
|
235
403
|
return e;
|
|
236
404
|
}
|
|
237
|
-
function planRoot() {
|
|
238
|
-
return document.getElementById("plan-root");
|
|
239
|
-
}
|
|
240
405
|
function wordmarkHtml() {
|
|
241
406
|
return '<b class="top">slide</b><b class="bot">mind</b>';
|
|
242
407
|
}
|
|
@@ -254,20 +419,28 @@
|
|
|
254
419
|
box.appendChild(el("div", "sm-logo-svg", idSuffix ? logoWithIds(idSuffix) : LOGO_SVG));
|
|
255
420
|
return box;
|
|
256
421
|
}
|
|
257
|
-
function
|
|
258
|
-
|
|
259
|
-
b.innerHTML = (iconName ? ICONS[iconName] : "") + "<span>" + label + "</span>";
|
|
260
|
-
return b;
|
|
261
|
-
}
|
|
262
|
-
function btnLabel(b, text) {
|
|
263
|
-
var s = b.querySelector("span");
|
|
264
|
-
if (s) s.textContent = text;
|
|
422
|
+
function clamp(v, min, max) {
|
|
423
|
+
return Math.min(Math.max(v, min), max);
|
|
265
424
|
}
|
|
266
425
|
function planWasEdited() {
|
|
267
|
-
return
|
|
426
|
+
return S.value !== baseline;
|
|
268
427
|
}
|
|
269
428
|
function commentText() {
|
|
270
|
-
return commentEl ? commentEl.value.trim()
|
|
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
|
+
}
|
|
271
444
|
}
|
|
272
445
|
|
|
273
446
|
// ---------- chrome (top bar) ----------
|
|
@@ -289,95 +462,965 @@
|
|
|
289
462
|
title.appendChild(editedBadge);
|
|
290
463
|
meta.appendChild(title);
|
|
291
464
|
meta.appendChild(
|
|
292
|
-
el(
|
|
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
|
+
),
|
|
293
470
|
);
|
|
294
471
|
bar.appendChild(meta);
|
|
295
|
-
|
|
296
472
|
bar.appendChild(el("div", "sm-grow"));
|
|
297
473
|
|
|
298
|
-
previewBtn = mkButton("Preview", "ghost", "panel");
|
|
299
|
-
previewBtn.onclick = togglePreview;
|
|
300
|
-
bar.appendChild(previewBtn);
|
|
301
|
-
|
|
302
474
|
document.body.appendChild(bar);
|
|
303
475
|
}
|
|
304
476
|
function refreshChrome() {
|
|
305
477
|
if (editedBadge) editedBadge.style.display = planWasEdited() ? "" : "none";
|
|
306
478
|
|
|
307
479
|
var hasFeedback = commentText().length > 0;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
480
|
+
var canSend = !!S.storyDraft || hasFeedback;
|
|
481
|
+
if (sendBtn) sendBtn.classList.toggle("is-off", !canSend);
|
|
482
|
+
|
|
311
483
|
if (approveBtn) {
|
|
312
484
|
var main = approveBtn.querySelector(".main span");
|
|
313
485
|
var sub = approveBtn.querySelector(".sub");
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
if (main) main.textContent = "Feedback not sent — approve anyway?";
|
|
317
|
-
if (sub) sub.style.display = "none";
|
|
318
|
-
} else {
|
|
486
|
+
if (S.storyDraft) {
|
|
487
|
+
// Approve is blocked until the staged storyline is sent or discarded.
|
|
319
488
|
approveArmed = false;
|
|
320
489
|
approveBtn.classList.remove("is-armed");
|
|
490
|
+
approveBtn.classList.add("is-off");
|
|
321
491
|
if (main) main.textContent = "Approve plan";
|
|
322
|
-
if (sub)
|
|
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
|
+
}
|
|
323
511
|
}
|
|
324
512
|
}
|
|
325
|
-
if (previewBtn) {
|
|
326
|
-
previewBtn.classList.toggle("is-on", previewOpen);
|
|
327
|
-
btnLabel(previewBtn, previewOpen ? "Hide preview" : "Show preview");
|
|
328
|
-
}
|
|
329
513
|
}
|
|
330
514
|
|
|
331
|
-
// ---------- workspace:
|
|
515
|
+
// ---------- workspace: one scrollable document surface + composer ----------
|
|
332
516
|
function buildWorkspace() {
|
|
333
517
|
wsEl = el("div", "sm-o sm-ws");
|
|
334
518
|
wsEl.setAttribute("data-sm-overlay", "1");
|
|
335
519
|
|
|
336
|
-
|
|
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
|
+
}
|
|
337
556
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
344
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";
|
|
345
589
|
});
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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);
|
|
356
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);
|
|
357
950
|
});
|
|
358
|
-
|
|
359
|
-
panes.appendChild(edPane);
|
|
951
|
+
track.appendChild(gapAt(slides.length));
|
|
360
952
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
previewPane = el("div", "sm-prev");
|
|
364
|
-
var root = planRoot();
|
|
365
|
-
if (root) previewPane.appendChild(root);
|
|
366
|
-
panes.appendChild(previewPane);
|
|
953
|
+
return section;
|
|
954
|
+
}
|
|
367
955
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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;
|
|
371
1397
|
}
|
|
372
1398
|
|
|
373
1399
|
// ---------- chat composer bar ----------
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
composerEl =
|
|
380
|
-
|
|
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
|
+
}
|
|
381
1424
|
|
|
382
1425
|
if (CFG.previousComment) {
|
|
383
1426
|
var prev = el("div", "sm-prev-note");
|
|
@@ -391,8 +1434,11 @@
|
|
|
391
1434
|
var row = el("div", "sm-composer-row");
|
|
392
1435
|
commentEl = el("textarea", "sm-chat-ta");
|
|
393
1436
|
commentEl.rows = 1;
|
|
394
|
-
commentEl.placeholder = "
|
|
1437
|
+
commentEl.placeholder = "What should change? Leave a note, or edit the plan directly.";
|
|
1438
|
+
commentEl.value = feedbackText;
|
|
395
1439
|
commentEl.addEventListener("input", function () {
|
|
1440
|
+
feedbackText = commentEl.value;
|
|
1441
|
+
approveArmed = false; // typing more feedback disarms a pending approve-confirm
|
|
396
1442
|
commentEl.style.height = "auto";
|
|
397
1443
|
commentEl.style.height = Math.min(commentEl.scrollHeight, 150) + "px";
|
|
398
1444
|
refreshChrome();
|
|
@@ -402,21 +1448,31 @@
|
|
|
402
1448
|
});
|
|
403
1449
|
row.appendChild(commentEl);
|
|
404
1450
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
+
}
|
|
408
1462
|
sendBtn.onclick = requestChanges;
|
|
409
1463
|
row.appendChild(sendBtn);
|
|
410
1464
|
|
|
411
1465
|
row.appendChild(el("div", "sm-composer-sep"));
|
|
412
1466
|
|
|
413
1467
|
approveBtn = el("button", "sm-approve");
|
|
1468
|
+
approveBtn.type = "button";
|
|
414
1469
|
approveBtn.innerHTML =
|
|
415
1470
|
'<span class="main">' +
|
|
416
1471
|
ICONS.check +
|
|
417
1472
|
"<span>Approve plan</span></span>" +
|
|
418
1473
|
'<span class="sub">the agent builds the slides next</span>';
|
|
419
1474
|
approveBtn.onclick = function () {
|
|
1475
|
+
if (S.storyDraft) return; // blocked while a storyline draft is staged
|
|
420
1476
|
// Approving with unsent feedback in the input discards it — confirm inline.
|
|
421
1477
|
if (commentText() && !approveArmed) {
|
|
422
1478
|
approveArmed = true;
|
|
@@ -428,16 +1484,17 @@
|
|
|
428
1484
|
row.appendChild(approveBtn);
|
|
429
1485
|
|
|
430
1486
|
composerEl.appendChild(row);
|
|
431
|
-
|
|
1487
|
+
refreshChrome();
|
|
432
1488
|
}
|
|
433
1489
|
|
|
434
1490
|
// ---------- waiting state (comments sent) ----------
|
|
435
|
-
// The tab does NOT veil over on a comments round: the plan stays readable,
|
|
436
|
-
//
|
|
437
|
-
//
|
|
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).
|
|
438
1494
|
function enterWaiting() {
|
|
439
|
-
|
|
1495
|
+
waiting = true;
|
|
440
1496
|
if (wsEl) wsEl.classList.add("is-waiting");
|
|
1497
|
+
renderSurface(); // rebuild the surface read-only (no contenteditable attrs)
|
|
441
1498
|
document.title = WAITING_TITLE; // the tab label signals the state from the tab bar
|
|
442
1499
|
if (composerEl) {
|
|
443
1500
|
composerEl.innerHTML = "";
|
|
@@ -463,9 +1520,6 @@
|
|
|
463
1520
|
}
|
|
464
1521
|
|
|
465
1522
|
// ---------- next-round attention ----------
|
|
466
|
-
// A background tab cannot steal focus (by browser design). What it CAN do:
|
|
467
|
-
// fire a system notification whose click is allowed to focus the tab, and
|
|
468
|
-
// flag itself in the tab bar with an attention title until it is focused.
|
|
469
1523
|
function notifyPermission() {
|
|
470
1524
|
try {
|
|
471
1525
|
return "Notification" in window ? Notification.permission : "unsupported";
|
|
@@ -502,7 +1556,7 @@
|
|
|
502
1556
|
}
|
|
503
1557
|
if (!flagged) return;
|
|
504
1558
|
try {
|
|
505
|
-
window.focus();
|
|
1559
|
+
window.focus();
|
|
506
1560
|
} catch {
|
|
507
1561
|
/* ignore */
|
|
508
1562
|
}
|
|
@@ -523,7 +1577,7 @@
|
|
|
523
1577
|
if (notifyPermission() === "granted") {
|
|
524
1578
|
var n = new Notification("slidemind — plan ready", {
|
|
525
1579
|
body: "The revised plan is ready for your review. Click to jump back to the tab.",
|
|
526
|
-
tag: "slidemind-plan-round",
|
|
1580
|
+
tag: "slidemind-plan-round",
|
|
527
1581
|
});
|
|
528
1582
|
n.onclick = function () {
|
|
529
1583
|
try {
|
|
@@ -539,58 +1593,20 @@
|
|
|
539
1593
|
}
|
|
540
1594
|
}
|
|
541
1595
|
|
|
542
|
-
// ---------- live preview ----------
|
|
543
|
-
// Debounced re-render of the preview from the editor's current text, via the
|
|
544
|
-
// renderer the page ships (plan-md.mjs run in-browser). Absent the renderer
|
|
545
|
-
// the preview simply stays at the served render.
|
|
546
|
-
function schedulePreview() {
|
|
547
|
-
if (!window.__SLIDEMIND_RENDER_MD__) return;
|
|
548
|
-
if (renderTimer) clearTimeout(renderTimer);
|
|
549
|
-
renderTimer = setTimeout(renderPreview, RENDER_DEBOUNCE_MS);
|
|
550
|
-
}
|
|
551
|
-
function renderPreview() {
|
|
552
|
-
renderTimer = null;
|
|
553
|
-
var root = planRoot();
|
|
554
|
-
var render = window.__SLIDEMIND_RENDER_MD__;
|
|
555
|
-
if (!root || !render) return;
|
|
556
|
-
var st = previewPane ? previewPane.scrollTop : 0;
|
|
557
|
-
try {
|
|
558
|
-
root.innerHTML = render(editorEl.value, {
|
|
559
|
-
lineOffset: (CFG.bodyStartLine || 1) - 1,
|
|
560
|
-
});
|
|
561
|
-
} catch {
|
|
562
|
-
return; // never let a renderer hiccup take down the editor
|
|
563
|
-
}
|
|
564
|
-
if (previewPane) previewPane.scrollTop = st;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// ---------- preview collapse ----------
|
|
568
|
-
function readPreviewPref() {
|
|
569
|
-
try {
|
|
570
|
-
return localStorage.getItem(PREVIEW_PREF_KEY) !== "hidden";
|
|
571
|
-
} catch {
|
|
572
|
-
return true;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
function applyPreviewState(open) {
|
|
576
|
-
previewOpen = open;
|
|
577
|
-
var panes = wsEl && wsEl.querySelector(".sm-panes");
|
|
578
|
-
if (panes) panes.classList.toggle("preview-hidden", !previewOpen);
|
|
579
|
-
refreshChrome();
|
|
580
|
-
}
|
|
581
|
-
function togglePreview() {
|
|
582
|
-
applyPreviewState(!previewOpen);
|
|
583
|
-
try {
|
|
584
|
-
localStorage.setItem(PREVIEW_PREF_KEY, previewOpen ? "shown" : "hidden");
|
|
585
|
-
} catch {
|
|
586
|
-
/* private mode etc. — the toggle still works for this tab */
|
|
587
|
-
}
|
|
588
|
-
if (previewOpen) renderPreview(); // catch up on edits made while hidden
|
|
589
|
-
}
|
|
590
|
-
|
|
591
1596
|
// ---------- submit ----------
|
|
592
1597
|
function requestChanges() {
|
|
593
|
-
if (
|
|
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
|
+
}
|
|
594
1610
|
// Ask for notification permission HERE — the click is a user gesture, the
|
|
595
1611
|
// only context where the browser will actually show the prompt.
|
|
596
1612
|
notifyPermPromise = requestNotifyPermission();
|
|
@@ -601,8 +1617,8 @@
|
|
|
601
1617
|
submitted = true;
|
|
602
1618
|
var payload = {
|
|
603
1619
|
decision: decision,
|
|
604
|
-
comment: decision === "comments" ?
|
|
605
|
-
editedPlan: planWasEdited() ?
|
|
1620
|
+
comment: decision === "comments" ? pendingComment : null,
|
|
1621
|
+
editedPlan: decision === "comments" ? pendingEdited : planWasEdited() ? S.value : null,
|
|
606
1622
|
};
|
|
607
1623
|
fetch(CFG.endpoint, {
|
|
608
1624
|
method: "POST",
|
|
@@ -610,10 +1626,6 @@
|
|
|
610
1626
|
body: JSON.stringify(payload),
|
|
611
1627
|
})
|
|
612
1628
|
.then(function () {
|
|
613
|
-
// Approve ends the session — the brand build animation plays, then the
|
|
614
|
-
// tab closes itself. A comments round keeps the tab OPEN and readable:
|
|
615
|
-
// the composer becomes a spinner + wait note while the heartbeat
|
|
616
|
-
// polling waits for the next run, which reloads this same tab.
|
|
617
1629
|
if (decision === "approve") {
|
|
618
1630
|
showApproved();
|
|
619
1631
|
} else {
|
|
@@ -638,7 +1650,12 @@
|
|
|
638
1650
|
}
|
|
639
1651
|
// After a comments round the tab stays open and polls the heartbeat so the
|
|
640
1652
|
// NEXT run (which binds the same deterministic port) reloads this tab instead
|
|
641
|
-
// of opening a fresh browser window.
|
|
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.
|
|
642
1659
|
function startNextRoundPolling() {
|
|
643
1660
|
var startedAt = Date.now();
|
|
644
1661
|
var pollMs = CFG.pollMs || 1000;
|
|
@@ -650,27 +1667,37 @@
|
|
|
650
1667
|
showSessionEnded();
|
|
651
1668
|
return;
|
|
652
1669
|
}
|
|
653
|
-
|
|
654
|
-
|
|
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
|
+
})
|
|
655
1682
|
.then(function (data) {
|
|
656
|
-
// Reload only for a genuine NEWER slidemind plan round: shape-guard the
|
|
657
|
-
// service marker (a foreign app / the preview gate may have taken the
|
|
658
|
-
// port) and require a different nonce (same nonce = this run's own
|
|
659
|
-
// orphan — never self-reload).
|
|
660
1683
|
if (!data || data.service !== "slidemind-plan" || !data.round) return;
|
|
661
|
-
if (data.round
|
|
1684
|
+
if (data.round === CFG.round) return;
|
|
1685
|
+
if (data.action === "close") {
|
|
662
1686
|
clearInterval(timer);
|
|
663
|
-
|
|
1687
|
+
showSuperseded();
|
|
1688
|
+
return;
|
|
664
1689
|
}
|
|
1690
|
+
if (data.action === "wait") return; // grace window still deciding — keep polling
|
|
1691
|
+
clearInterval(timer);
|
|
1692
|
+
markRoundReadyAndReload();
|
|
665
1693
|
})
|
|
666
|
-
.catch(function () {
|
|
1694
|
+
.catch(function () {
|
|
1695
|
+
/* server down between rounds — keep polling */
|
|
1696
|
+
});
|
|
667
1697
|
}, pollMs);
|
|
668
1698
|
}
|
|
669
1699
|
|
|
670
1700
|
// ---------- done veil ----------
|
|
671
|
-
// Shared full-screen veil (logo + wordmark + status + sub). Replaces any veil
|
|
672
|
-
// already shown rather than stacking a second one, so a comments round's
|
|
673
|
-
// "waiting" veil can be swapped for "session ended" in place.
|
|
674
1701
|
function showVeil(statusHtml, subText, subPulse) {
|
|
675
1702
|
if (bar) bar.style.display = "none";
|
|
676
1703
|
if (wsEl) wsEl.style.display = "none";
|
|
@@ -684,10 +1711,6 @@
|
|
|
684
1711
|
veil.appendChild(el("div", "sub" + (subPulse ? " sm-pulse" : ""), subText));
|
|
685
1712
|
document.body.appendChild(veil);
|
|
686
1713
|
}
|
|
687
|
-
// Approve's exit moment: the full-screen brand animation — rising logo,
|
|
688
|
-
// drifting wordmark — announcing that the agent now builds the slides. The
|
|
689
|
-
// tab closes itself after the animation has had time to land; if the browser
|
|
690
|
-
// refuses, the pulsing veil keeps telling the user what's happening.
|
|
691
1714
|
function showApproved() {
|
|
692
1715
|
showVeil(
|
|
693
1716
|
ICONS.check + "<span>Plan approved</span>",
|
|
@@ -699,6 +1722,13 @@
|
|
|
699
1722
|
function showSessionEnded() {
|
|
700
1723
|
showVeil("<span>Session ended</span>", "You can close this tab.");
|
|
701
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
|
+
}
|
|
702
1732
|
|
|
703
1733
|
// ---------- assets ----------
|
|
704
1734
|
function injectAssets() {
|
|
@@ -725,17 +1755,18 @@
|
|
|
725
1755
|
|
|
726
1756
|
// ---------- wire up ----------
|
|
727
1757
|
function init() {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
setTimeout(init, 50);
|
|
1758
|
+
if (!document.body) {
|
|
1759
|
+
setTimeout(init, 20);
|
|
731
1760
|
return;
|
|
732
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
|
+
|
|
733
1767
|
injectAssets();
|
|
734
1768
|
buildChrome();
|
|
735
1769
|
buildWorkspace();
|
|
736
|
-
buildComposerBar();
|
|
737
|
-
refreshChrome();
|
|
738
|
-
editorEl.focus();
|
|
739
1770
|
announceNewRound(); // notification + title marker when this load IS the next round
|
|
740
1771
|
}
|
|
741
1772
|
|