stagecraft 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/AGENT.md +792 -0
  2. package/LICENSE +21 -0
  3. package/README.md +210 -0
  4. package/bin/cli.js +51 -0
  5. package/bin/export.js +137 -0
  6. package/bin/init.js +52 -0
  7. package/bin/lib/edit-ops.js +405 -0
  8. package/bin/serve.js +278 -0
  9. package/dist/stagecraft.bundle.css +4443 -0
  10. package/dist/stagecraft.bundle.js +7621 -0
  11. package/dist/themes/brand.bundle.css +5262 -0
  12. package/dist/themes/neon.bundle.css +5289 -0
  13. package/dist/themes/paper.bundle.css +5276 -0
  14. package/dist/themes/phosphor.bundle.css +4443 -0
  15. package/dist/themes/shopware.bundle.css +5850 -0
  16. package/examples/closing-card.js +74 -0
  17. package/examples/orchestration-graph.js +156 -0
  18. package/examples/terminal-log.js +109 -0
  19. package/examples/token-stream.js +96 -0
  20. package/examples/whoami.js +90 -0
  21. package/package.json +41 -0
  22. package/src/components/activity-list.js +75 -0
  23. package/src/components/agenda.js +79 -0
  24. package/src/components/bar-chart.js +162 -0
  25. package/src/components/before-after.js +135 -0
  26. package/src/components/bento.js +73 -0
  27. package/src/components/big-number.js +87 -0
  28. package/src/components/callout.js +75 -0
  29. package/src/components/checklist.js +81 -0
  30. package/src/components/code-block.js +141 -0
  31. package/src/components/code-diff.js +98 -0
  32. package/src/components/compare.js +85 -0
  33. package/src/components/counter.js +80 -0
  34. package/src/components/cta.js +69 -0
  35. package/src/components/cycle.js +146 -0
  36. package/src/components/definition.js +96 -0
  37. package/src/components/donut-chart.js +179 -0
  38. package/src/components/full-image.js +82 -0
  39. package/src/components/funnel.js +111 -0
  40. package/src/components/gauge.js +147 -0
  41. package/src/components/heatmap.js +141 -0
  42. package/src/components/image-grid.js +80 -0
  43. package/src/components/image-text.js +96 -0
  44. package/src/components/kinetic-text.js +72 -0
  45. package/src/components/kpi.js +106 -0
  46. package/src/components/line-chart.js +215 -0
  47. package/src/components/manifesto.js +104 -0
  48. package/src/components/marquee.js +63 -0
  49. package/src/components/matrix2x2.js +151 -0
  50. package/src/components/pillars.js +80 -0
  51. package/src/components/pricing.js +90 -0
  52. package/src/components/process-flow.js +133 -0
  53. package/src/components/progress.js +136 -0
  54. package/src/components/punchline.js +82 -0
  55. package/src/components/pyramid.js +107 -0
  56. package/src/components/qanda.js +60 -0
  57. package/src/components/quote.js +70 -0
  58. package/src/components/roadmap.js +130 -0
  59. package/src/components/section-card.js +45 -0
  60. package/src/components/shift-arrow.js +41 -0
  61. package/src/components/spark-line.js +147 -0
  62. package/src/components/spotlight.js +85 -0
  63. package/src/components/statement.js +106 -0
  64. package/src/components/stats.js +91 -0
  65. package/src/components/steps.js +83 -0
  66. package/src/components/swot.js +110 -0
  67. package/src/components/team-grid.js +87 -0
  68. package/src/components/testimonial.js +99 -0
  69. package/src/components/timeline.js +91 -0
  70. package/src/components/tip.js +63 -0
  71. package/src/components/venn.js +198 -0
  72. package/src/edit-mode.js +1256 -0
  73. package/src/engine.js +823 -0
  74. package/src/helpers.js +169 -0
  75. package/src/transitions.js +101 -0
  76. package/starter/index.html +40 -0
  77. package/starter/slides/00-title.js +12 -0
  78. package/starter/stagecraft.config.js +8 -0
  79. package/themes/brand/base.css +4 -0
  80. package/themes/brand/components-business.css +173 -0
  81. package/themes/brand/components-chart.css +65 -0
  82. package/themes/brand/components-content.css +126 -0
  83. package/themes/brand/components-data.css +162 -0
  84. package/themes/brand/components-diagram.css +115 -0
  85. package/themes/brand/components-layout.css +112 -0
  86. package/themes/brand/components.css +46 -0
  87. package/themes/brand/manifest.json +20 -0
  88. package/themes/brand/tokens.css +20 -0
  89. package/themes/brand/transitions.css +4 -0
  90. package/themes/neon/base.css +10 -0
  91. package/themes/neon/components-business.css +189 -0
  92. package/themes/neon/components-chart.css +70 -0
  93. package/themes/neon/components-content.css +112 -0
  94. package/themes/neon/components-data.css +160 -0
  95. package/themes/neon/components-diagram.css +109 -0
  96. package/themes/neon/components-layout.css +87 -0
  97. package/themes/neon/components.css +87 -0
  98. package/themes/neon/manifest.json +21 -0
  99. package/themes/neon/tokens.css +17 -0
  100. package/themes/neon/transitions.css +13 -0
  101. package/themes/paper/base.css +9 -0
  102. package/themes/paper/components-business.css +196 -0
  103. package/themes/paper/components-chart.css +74 -0
  104. package/themes/paper/components-content.css +108 -0
  105. package/themes/paper/components-data.css +168 -0
  106. package/themes/paper/components-diagram.css +89 -0
  107. package/themes/paper/components-layout.css +105 -0
  108. package/themes/paper/components.css +60 -0
  109. package/themes/paper/manifest.json +10 -0
  110. package/themes/paper/tokens.css +21 -0
  111. package/themes/paper/transitions.css +11 -0
  112. package/themes/phosphor/base.css +511 -0
  113. package/themes/phosphor/components-business.css +818 -0
  114. package/themes/phosphor/components-chart.css +415 -0
  115. package/themes/phosphor/components-content.css +530 -0
  116. package/themes/phosphor/components-data.css +824 -0
  117. package/themes/phosphor/components-diagram.css +427 -0
  118. package/themes/phosphor/components-layout.css +450 -0
  119. package/themes/phosphor/components.css +223 -0
  120. package/themes/phosphor/manifest.json +11 -0
  121. package/themes/phosphor/tokens.css +17 -0
  122. package/themes/phosphor/transitions.css +213 -0
  123. package/themes/shopware/base.css +94 -0
  124. package/themes/shopware/components-business.css +344 -0
  125. package/themes/shopware/components-chart.css +121 -0
  126. package/themes/shopware/components-content.css +169 -0
  127. package/themes/shopware/components-data.css +219 -0
  128. package/themes/shopware/components-diagram.css +129 -0
  129. package/themes/shopware/components-layout.css +166 -0
  130. package/themes/shopware/components.css +83 -0
  131. package/themes/shopware/manifest.json +21 -0
  132. package/themes/shopware/tokens.css +68 -0
  133. package/themes/shopware/transitions.css +22 -0
@@ -0,0 +1,1256 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Stagecraft — Edit Mode UI (browser-side).
5
+ *
6
+ * Loaded after engine.js. When the engine connects to the dev server,
7
+ * it calls Stage._editUI.activate(ws). This module then attaches all
8
+ * the affordances:
9
+ *
10
+ * - Level 1: slide-level note via Storyboard tile or 'N' key in present mode
11
+ * - Level 2: element pin notes via click on hovered element
12
+ * - Level 3: inline text edit via single click on [data-stage-edit] elements
13
+ * - Drag-to-reorder in Storyboard
14
+ * - Transition picker between Storyboard tiles
15
+ */
16
+
17
+ (function (root) {
18
+ const Stage = root.Stage = root.Stage || {};
19
+
20
+ let dragJustEndedAt = 0;
21
+
22
+ const EditUI = {
23
+ ws: null,
24
+ active: false,
25
+
26
+ activate(ws) {
27
+ this.ws = ws;
28
+ this.active = true;
29
+ injectStyles();
30
+ bindPresentMode();
31
+ },
32
+
33
+ // Engine consults this before treating a tile click as a "jump-to-slide"
34
+ // intent. After a drag-drop, the browser synthesises a click — we want
35
+ // to swallow that so the overview doesn't close.
36
+ justFinishedDrag() {
37
+ return Date.now() - dragJustEndedAt < 300;
38
+ },
39
+
40
+ markDragEnded() { dragJustEndedAt = Date.now(); },
41
+
42
+ // Called by engine after a storyboard tile is built
43
+ decorateTile(tile, slide, idx) {
44
+ attachDragHandles(tile, idx);
45
+
46
+ // Action cluster (top-right) — note · speaker notes · delete
47
+ const cluster = document.createElement('div');
48
+ cluster.className = 'tile-edit-ui tile-actions';
49
+
50
+ const noteBtn = document.createElement('button');
51
+ noteBtn.className = 'tile-action';
52
+ noteBtn.textContent = '💬';
53
+ noteBtn.title = 'Feedback note for the agent';
54
+ noteBtn.addEventListener('click', (e) => {
55
+ e.stopPropagation();
56
+ openSlideNoteDialog(idx, slide);
57
+ });
58
+ cluster.appendChild(noteBtn);
59
+
60
+ const speakerBtn = document.createElement('button');
61
+ speakerBtn.className = 'tile-action';
62
+ speakerBtn.textContent = '🎙';
63
+ speakerBtn.title = 'Speaker notes (shown in presenter view)';
64
+ speakerBtn.addEventListener('click', (e) => {
65
+ e.stopPropagation();
66
+ openSpeakerNotesDialog(idx, slide);
67
+ });
68
+ cluster.appendChild(speakerBtn);
69
+
70
+ const deleteBtn = document.createElement('button');
71
+ deleteBtn.className = 'tile-action tile-action-danger';
72
+ deleteBtn.textContent = '×';
73
+ deleteBtn.title = 'Delete this slide';
74
+ deleteBtn.addEventListener('click', (e) => {
75
+ e.stopPropagation();
76
+ confirmDeleteSlide(idx, slide);
77
+ });
78
+ cluster.appendChild(deleteBtn);
79
+
80
+ tile.appendChild(cluster);
81
+ },
82
+
83
+ // Called after storyboard is fully built AND scaleTiles ran — add the
84
+ // inter-tile transition connectors + storyboard header toolbar.
85
+ afterOverviewBuilt(ov) {
86
+ attachDropZones(ov);
87
+ attachTransitionConnectors(ov);
88
+ attachStoryboardToolbar(ov);
89
+ attachAddSlideTile(ov);
90
+ },
91
+
92
+ // Called by engine after a slide renders + init. Fetches the file's
93
+ // @note[stage-key=...] pin comments and renders small markers.
94
+ onSlideRendered(slide, idx, el) {
95
+ const file = Stage._manifestSlides?.[idx]?.src;
96
+ if (!file) return;
97
+ apiPost('/api/notes/element', { file }).then(r => {
98
+ if (!r.ok || !r.pins?.length) return;
99
+ renderPinMarkers(el, r.pins);
100
+ }).catch(() => { /* offline, no pins */ });
101
+ }
102
+ };
103
+
104
+ Stage._editUI = EditUI;
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Present-mode bindings: element hover, click-to-edit, shift-click-to-pin
108
+ // ---------------------------------------------------------------------------
109
+ function bindPresentMode() {
110
+ let hoverEl = null;
111
+ let outline = null;
112
+
113
+ window.addEventListener('mousemove', (e) => {
114
+ if (!EditUI.active) return;
115
+ if (Stage._engine.isEditMode() === false) return;
116
+ if (document.getElementById('overview')) return; // skip in storyboard
117
+ const el = e.target;
118
+ if (!el || el === outline) return;
119
+ // Only target elements inside the current slide
120
+ const slideEl = el.closest('.slide.current');
121
+ if (!slideEl) {
122
+ if (hoverEl) clearHover();
123
+ return;
124
+ }
125
+ // Don't outline the slide itself
126
+ if (el === slideEl) {
127
+ if (hoverEl) clearHover();
128
+ return;
129
+ }
130
+ // Don't outline elements in edit-affordance overlays
131
+ if (el.closest('.edit-affordance, .note-overlay')) return;
132
+
133
+ if (hoverEl !== el) {
134
+ hoverEl = el;
135
+ showHover(el);
136
+ }
137
+ });
138
+
139
+ window.addEventListener('click', (e) => {
140
+ if (!EditUI.active) return;
141
+ if (Stage._engine.isEditMode() === false) return;
142
+ if (document.getElementById('overview')) return;
143
+ const el = e.target;
144
+ const slideEl = el.closest('.slide.current');
145
+ if (!slideEl) return;
146
+ if (el === slideEl) return;
147
+ if (el.closest('.edit-affordance, .note-overlay')) return;
148
+ // Already editing this element? Leave it alone (cursor placement).
149
+ if (el.contentEditable === 'true') return;
150
+
151
+ // Shift+click → pin note
152
+ if (e.shiftKey) {
153
+ e.preventDefault();
154
+ e.stopPropagation();
155
+ const stageKey = el.dataset.stageKey || el.closest('[data-stage-key]')?.dataset.stageKey;
156
+ if (!stageKey) {
157
+ toast('No stage-key on this element', 'warn');
158
+ return;
159
+ }
160
+ openElementNoteDialog(el, stageKey);
161
+ return;
162
+ }
163
+
164
+ // Plain click on an editable element → inline edit immediately
165
+ if (el.dataset?.stageEdit) {
166
+ e.preventDefault();
167
+ e.stopPropagation();
168
+ makeEditable(el);
169
+ }
170
+ }, true);
171
+
172
+ // Keyboard: 'N' opens slide-level note for current slide
173
+ window.addEventListener('keydown', (e) => {
174
+ if (!EditUI.active) return;
175
+ if (Stage._engine.isEditMode() === false) return;
176
+ if (e.target.isContentEditable) return;
177
+ if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') return;
178
+ if (e.key === 'n' || e.key === 'N') {
179
+ if (document.getElementById('overview')) return;
180
+ const idx = Stage._engine.currentIndex();
181
+ const slide = Stage.slides[idx];
182
+ if (slide) openSlideNoteDialog(idx, slide);
183
+ }
184
+ });
185
+
186
+ function showHover(el) {
187
+ clearHover();
188
+ const r = el.getBoundingClientRect();
189
+ outline = document.createElement('div');
190
+ outline.className = 'edit-affordance hover-outline';
191
+ Object.assign(outline.style, {
192
+ left: r.left + 'px',
193
+ top: r.top + 'px',
194
+ width: r.width + 'px',
195
+ height: r.height + 'px'
196
+ });
197
+ document.body.appendChild(outline);
198
+ hoverEl = el;
199
+ }
200
+ function clearHover() {
201
+ if (outline) outline.remove();
202
+ outline = null;
203
+ hoverEl = null;
204
+ }
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // Level 3: inline edit
209
+ // ---------------------------------------------------------------------------
210
+ function makeEditable(el) {
211
+ if (el.contentEditable === 'true') return; // already editing
212
+ const original = el.textContent;
213
+ el.contentEditable = 'true';
214
+ el.classList.add('inline-editing');
215
+ el.focus();
216
+ // Select all
217
+ const range = document.createRange();
218
+ range.selectNodeContents(el);
219
+ const sel = window.getSelection();
220
+ sel.removeAllRanges();
221
+ sel.addRange(range);
222
+
223
+ function commit() {
224
+ el.removeEventListener('blur', commit);
225
+ el.removeEventListener('keydown', onKey);
226
+ el.contentEditable = 'false';
227
+ el.classList.remove('inline-editing');
228
+ const newVal = el.textContent;
229
+ if (newVal === original) return;
230
+ const propPath = el.dataset.stageEdit;
231
+ const file = currentSlideFile();
232
+ if (!file) {
233
+ el.textContent = original;
234
+ toast('Cannot determine slide file', 'error');
235
+ return;
236
+ }
237
+ apiPost('/api/edit/inline', { file, propPath, value: newVal })
238
+ .then(r => {
239
+ if (!r.ok) {
240
+ el.textContent = original;
241
+ toast('Edit rejected: ' + r.error, 'error');
242
+ } else {
243
+ toast('saved', 'ok');
244
+ }
245
+ })
246
+ .catch(e => {
247
+ el.textContent = original;
248
+ toast('Edit failed: ' + e.message, 'error');
249
+ });
250
+ }
251
+
252
+ function onKey(e) {
253
+ if (e.key === 'Escape') {
254
+ e.preventDefault();
255
+ el.textContent = original;
256
+ commit();
257
+ } else if (e.key === 'Enter' && !e.shiftKey) {
258
+ e.preventDefault();
259
+ el.blur();
260
+ }
261
+ }
262
+
263
+ el.addEventListener('blur', commit);
264
+ el.addEventListener('keydown', onKey);
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Note dialogs
269
+ // ---------------------------------------------------------------------------
270
+ function openSlideNoteDialog(idx, slide) {
271
+ const file = slideFileForIdx(idx);
272
+ openNoteOverlay({
273
+ title: `Note on slide ${idx} — ${slide.title || ''}`,
274
+ onSubmit: (text) => {
275
+ return apiPost('/api/note/slide', { file, text });
276
+ }
277
+ });
278
+ }
279
+
280
+ function openElementNoteDialog(el, stageKey) {
281
+ const file = currentSlideFile();
282
+ if (!file) {
283
+ toast('Cannot determine slide file', 'error');
284
+ return;
285
+ }
286
+ const r = el.getBoundingClientRect();
287
+ openNoteOverlay({
288
+ title: `Pin note on ${stageKey}`,
289
+ anchor: { left: r.left + r.width + 12, top: r.top },
290
+ onSubmit: (text) => apiPost('/api/note/element', { file, stageKey, text })
291
+ });
292
+ }
293
+
294
+ function openNoteOverlay({ title, anchor, onSubmit }) {
295
+ // Close any existing
296
+ document.querySelectorAll('.note-overlay').forEach(n => n.remove());
297
+ const ov = document.createElement('div');
298
+ ov.className = 'note-overlay edit-affordance';
299
+ ov.innerHTML = `
300
+ <div class="note-title">${title}</div>
301
+ <textarea class="note-text" placeholder="Note for the agent..."></textarea>
302
+ <div class="note-actions">
303
+ <button class="note-cancel">Cancel</button>
304
+ <button class="note-save">Save (⌘↵)</button>
305
+ </div>
306
+ `;
307
+ if (anchor) {
308
+ ov.style.position = 'fixed';
309
+ ov.style.left = Math.min(anchor.left, window.innerWidth - 380) + 'px';
310
+ ov.style.top = Math.min(anchor.top, window.innerHeight - 200) + 'px';
311
+ }
312
+ document.body.appendChild(ov);
313
+ const ta = ov.querySelector('textarea');
314
+ ta.focus();
315
+
316
+ function close() { ov.remove(); }
317
+ function save() {
318
+ const text = ta.value.trim();
319
+ if (!text) { close(); return; }
320
+ onSubmit(text).then(r => {
321
+ if (r.ok) toast('note saved', 'ok');
322
+ else toast('save failed: ' + r.error, 'error');
323
+ close();
324
+ }).catch(e => { toast('save failed: ' + e.message, 'error'); close(); });
325
+ }
326
+ ov.querySelector('.note-cancel').addEventListener('click', close);
327
+ ov.querySelector('.note-save').addEventListener('click', save);
328
+ ta.addEventListener('keydown', (e) => {
329
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); save(); }
330
+ else if (e.key === 'Escape') { close(); }
331
+ });
332
+ }
333
+
334
+ // ---------------------------------------------------------------------------
335
+ // Pin markers — render small yellow dots on elements that have @note pins
336
+ // ---------------------------------------------------------------------------
337
+ function renderPinMarkers(slideEl, pins) {
338
+ // Clear any prior markers from a previous render
339
+ slideEl.querySelectorAll('.pin-marker').forEach(n => n.remove());
340
+ pins.forEach(({ stageKey, text }) => {
341
+ const target = slideEl.querySelector(`[data-stage-key="${escapeAttr(stageKey)}"]`);
342
+ if (!target) return;
343
+ const marker = document.createElement('div');
344
+ marker.className = 'pin-marker edit-affordance';
345
+ marker.title = text;
346
+ marker.textContent = '●';
347
+ target.style.position = target.style.position || 'relative';
348
+ target.appendChild(marker);
349
+ });
350
+ }
351
+
352
+ function escapeAttr(s) {
353
+ return String(s).replace(/"/g, '\\"');
354
+ }
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // Drag-to-reorder in Storyboard
358
+ // ---------------------------------------------------------------------------
359
+ function attachDragHandles(tile, idx) {
360
+ tile.addEventListener('dragstart', (e) => {
361
+ e.dataTransfer.setData('text/x-stagecraft-idx', String(idx));
362
+ e.dataTransfer.effectAllowed = 'move';
363
+ tile.classList.add('dragging');
364
+ });
365
+ tile.addEventListener('dragend', () => {
366
+ tile.classList.remove('dragging');
367
+ EditUI.markDragEnded();
368
+ });
369
+ }
370
+
371
+ // ---------------------------------------------------------------------------
372
+ // Storyboard toolbar — theme picker, process-notes, add-slide
373
+ // ---------------------------------------------------------------------------
374
+ const THEMES = ['phosphor', 'paper', 'neon', 'brand', 'shopware'];
375
+
376
+ function attachStoryboardToolbar(ov) {
377
+ if (ov.querySelector('.sb-toolbar')) return;
378
+ const currentTheme = document.documentElement.getAttribute('data-theme') || 'phosphor';
379
+ const toolbar = document.createElement('div');
380
+ toolbar.className = 'sb-toolbar tile-edit-ui';
381
+ toolbar.innerHTML = `
382
+ <div class="sb-toolbar-group">
383
+ <label class="sb-toolbar-label">Theme</label>
384
+ <div class="sb-theme-picker">
385
+ ${THEMES.map(t => `<button class="sb-theme-btn${t === currentTheme ? ' is-current' : ''}" data-theme="${t}">${t}</button>`).join('')}
386
+ </div>
387
+ </div>
388
+ <div class="sb-toolbar-spacer"></div>
389
+ <button class="sb-toolbar-btn" id="sbProcessNotes" title="Copy a ready-made agent prompt for processing notes">
390
+ <span class="sb-toolbar-icon">📋</span> Process notes
391
+ </button>
392
+ `;
393
+ ov.appendChild(toolbar);
394
+
395
+ toolbar.querySelectorAll('.sb-theme-btn').forEach(btn => {
396
+ btn.addEventListener('click', (e) => {
397
+ e.stopPropagation();
398
+ const theme = btn.dataset.theme;
399
+ switchTheme(theme);
400
+ });
401
+ });
402
+ toolbar.querySelector('#sbProcessNotes').addEventListener('click', (e) => {
403
+ e.stopPropagation();
404
+ copyProcessNotesPrompt();
405
+ });
406
+ }
407
+
408
+ function switchTheme(theme) {
409
+ // Update DOM immediately for instant feedback.
410
+ document.documentElement.setAttribute('data-theme', theme);
411
+ // Persist via server. The full reload also re-fetches the manifest with the new theme.
412
+ apiPost('/api/manifest/theme', { theme }).then(r => {
413
+ if (r.ok) toast(`Theme → ${theme}`, 'ok');
414
+ else toast('Theme switch failed: ' + r.error, 'error');
415
+ });
416
+ // Update active button
417
+ document.querySelectorAll('.sb-theme-btn').forEach(b =>
418
+ b.classList.toggle('is-current', b.dataset.theme === theme)
419
+ );
420
+ }
421
+
422
+ function copyProcessNotesPrompt() {
423
+ const prompt = `Process all notes in this Stagecraft deck.
424
+
425
+ 1. Run: \`grep -rn '@note' slides/\`
426
+ 2. For each match:
427
+ - Read the note + the surrounding slide code.
428
+ - Apply the requested change to the slide.
429
+ - DELETE the @note: line(s) from the source file.
430
+ 3. Absence of @note: comments means everything has been addressed.
431
+
432
+ The user has been working in the browser-based edit mode and left these notes for you. Inline text edits and reorderings have already been applied to disk via the dev server — no action needed for those.`;
433
+ navigator.clipboard.writeText(prompt).then(
434
+ () => toast('Prompt copied to clipboard', 'ok'),
435
+ () => toast('Clipboard access denied', 'error')
436
+ );
437
+ }
438
+
439
+ // ---------------------------------------------------------------------------
440
+ // Add-slide tile — appears as the last tile in the storyboard grid
441
+ // ---------------------------------------------------------------------------
442
+ function attachAddSlideTile(ov) {
443
+ const grid = ov.querySelector('.overview-grid');
444
+ if (!grid) return;
445
+ if (grid.querySelector('.tile-add')) return;
446
+ const tile = document.createElement('div');
447
+ tile.className = 'tile tile-add tile-edit-ui';
448
+ tile.innerHTML = `<div class="tile-add-glyph">+</div><div class="tile-add-label">add slide</div>`;
449
+ tile.addEventListener('click', (e) => {
450
+ e.stopPropagation();
451
+ openAddSlideDialog();
452
+ });
453
+ grid.appendChild(tile);
454
+
455
+ // Match dimensions of the other tiles
456
+ const sibling = grid.querySelector('.tile:not(.tile-add)');
457
+ if (sibling) {
458
+ tile.style.height = sibling.offsetHeight + 'px';
459
+ }
460
+ }
461
+
462
+ function openAddSlideDialog() {
463
+ document.querySelectorAll('.add-slide-dialog').forEach(n => n.remove());
464
+ const dlg = document.createElement('div');
465
+ dlg.className = 'add-slide-dialog edit-affordance';
466
+ dlg.innerHTML = `
467
+ <div class="asd-title">Add a new slide</div>
468
+ <label class="asd-row">
469
+ <span class="asd-label">File path</span>
470
+ <input class="asd-input" id="asdFile" value="slides/new-slide.js" />
471
+ </label>
472
+ <label class="asd-row">
473
+ <span class="asd-label">Template</span>
474
+ <select class="asd-input" id="asdTemplate">
475
+ <option value="kinetic-text">KineticText (default)</option>
476
+ <option value="section-card">SectionCard</option>
477
+ <option value="blank">Blank custom</option>
478
+ </select>
479
+ </label>
480
+ <label class="asd-row">
481
+ <span class="asd-label">Transition</span>
482
+ <select class="asd-input" id="asdTransition">
483
+ <option value="">(default: fade)</option>
484
+ ${Object.keys(TRANSITION_ICONS).map(t => `<option value="${t}">${t}</option>`).join('')}
485
+ </select>
486
+ </label>
487
+ <div class="asd-actions">
488
+ <button class="asd-cancel">Cancel</button>
489
+ <button class="asd-create">Create slide</button>
490
+ </div>
491
+ `;
492
+ document.body.appendChild(dlg);
493
+
494
+ const fileInput = dlg.querySelector('#asdFile');
495
+ const templateInput = dlg.querySelector('#asdTemplate');
496
+ const transitionInput = dlg.querySelector('#asdTransition');
497
+ fileInput.focus();
498
+ fileInput.select();
499
+
500
+ function close() { dlg.remove(); }
501
+ dlg.querySelector('.asd-cancel').addEventListener('click', close);
502
+ dlg.querySelector('.asd-create').addEventListener('click', () => {
503
+ const file = fileInput.value.trim();
504
+ if (!file) { toast('File path required', 'warn'); return; }
505
+ apiPost('/api/manifest/add-slide', {
506
+ file, template: templateInput.value,
507
+ transition: transitionInput.value || null,
508
+ atIdx: Stage.slides.length
509
+ }).then(r => {
510
+ if (r.ok) {
511
+ toast(`Slide created: ${file}`, 'ok');
512
+ close();
513
+ } else {
514
+ toast('Create failed: ' + r.error, 'error');
515
+ }
516
+ });
517
+ });
518
+ }
519
+
520
+ // ---------------------------------------------------------------------------
521
+ // Delete slide confirmation
522
+ // ---------------------------------------------------------------------------
523
+ function confirmDeleteSlide(idx, slide) {
524
+ const file = slideFileForIdx(idx);
525
+ const title = slide?.title || `slide ${idx}`;
526
+ if (!confirm(`Delete "${title}"?\n\nFile: ${file}\n\nThis removes the slide from the manifest. The file itself stays on disk.`)) return;
527
+ apiPost('/api/manifest/remove-slide', { idx, file, deleteFile: false }).then(r => {
528
+ if (r.ok) toast(`Removed ${title}`, 'ok');
529
+ else toast('Delete failed: ' + r.error, 'error');
530
+ });
531
+ }
532
+
533
+ // ---------------------------------------------------------------------------
534
+ // Speaker notes — open editor for slide.notes
535
+ // ---------------------------------------------------------------------------
536
+ function openSpeakerNotesDialog(idx, slide) {
537
+ const file = slideFileForIdx(idx);
538
+ if (!file) { toast('Cannot determine slide file', 'error'); return; }
539
+ document.querySelectorAll('.note-overlay').forEach(n => n.remove());
540
+ const ov = document.createElement('div');
541
+ ov.className = 'note-overlay edit-affordance';
542
+ ov.innerHTML = `
543
+ <div class="note-title">Speaker notes for ${slide?.title || `slide ${idx}`}</div>
544
+ <div class="note-hint">Shown in the presenter view (the laptop window). Not visible to the audience.</div>
545
+ <textarea class="note-text" placeholder="What you want to say. Bullet points, pauses, callouts..."></textarea>
546
+ <div class="note-actions">
547
+ <button class="note-cancel">Cancel</button>
548
+ <button class="note-save">Save (⌘↵)</button>
549
+ </div>
550
+ `;
551
+ document.body.appendChild(ov);
552
+ const ta = ov.querySelector('textarea');
553
+ if (slide?.notes) ta.value = slide.notes;
554
+ ta.focus();
555
+
556
+ function close() { ov.remove(); }
557
+ function save() {
558
+ const notes = ta.value;
559
+ apiPost('/api/edit/notes', { file, notes }).then(r => {
560
+ if (r.ok) { toast('Speaker notes saved', 'ok'); close(); }
561
+ else { toast('Save failed: ' + r.error, 'error'); }
562
+ });
563
+ }
564
+ ov.querySelector('.note-cancel').addEventListener('click', close);
565
+ ov.querySelector('.note-save').addEventListener('click', save);
566
+ ta.addEventListener('keydown', (e) => {
567
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault(); save(); }
568
+ else if (e.key === 'Escape') close();
569
+ });
570
+ }
571
+
572
+ function attachDropZones(ov) {
573
+ const tiles = ov.querySelectorAll('.tile');
574
+ tiles.forEach((tile, idx) => {
575
+ tile.addEventListener('dragover', (e) => {
576
+ e.preventDefault();
577
+ e.dataTransfer.dropEffect = 'move';
578
+ tile.classList.add('drop-target');
579
+ });
580
+ tile.addEventListener('dragleave', () => tile.classList.remove('drop-target'));
581
+ tile.addEventListener('drop', (e) => {
582
+ e.preventDefault();
583
+ e.stopPropagation();
584
+ tile.classList.remove('drop-target');
585
+ EditUI.markDragEnded(); // suppress the post-drop synthetic click
586
+ const from = parseInt(e.dataTransfer.getData('text/x-stagecraft-idx'), 10);
587
+ if (Number.isNaN(from) || from === idx) return;
588
+ const orig = Array.from({ length: Stage.slides.length }, (_, i) => i);
589
+ const moved = orig.splice(from, 1)[0];
590
+ orig.splice(idx, 0, moved);
591
+ apiPost('/api/manifest/reorder', { newOrder: orig })
592
+ .then(r => { if (r.ok) toast('Reordered — reloading', 'ok'); });
593
+ });
594
+ });
595
+ }
596
+
597
+ // ---------------------------------------------------------------------------
598
+ // Transition connectors — lines + icons drawn between adjacent storyboard tiles
599
+ // ---------------------------------------------------------------------------
600
+ const TRANSITION_ICONS = {
601
+ cut: '━',
602
+ fade: '◇',
603
+ slide: '▶',
604
+ dissolve: '◌',
605
+ glitch: '⚡',
606
+ wipe: '╱',
607
+ 'zoom-in': '⊙',
608
+ 'zoom-out': '⊚',
609
+ flip: '⟲',
610
+ iris: '◉',
611
+ shutter: '☰',
612
+ push: '⇉',
613
+ typewriter: '⎯',
614
+ shatter: '✦'
615
+ };
616
+
617
+ function attachTransitionConnectors(ov) {
618
+ // Clear any previous connectors (handles resize re-run)
619
+ ov.querySelectorAll('.tx-connector-line, .tx-connector-icon').forEach(n => n.remove());
620
+
621
+ const tiles = Array.from(ov.querySelectorAll('.tile'));
622
+ if (tiles.length < 2) return;
623
+
624
+ for (let i = 1; i < tiles.length; i++) {
625
+ const prev = tiles[i - 1];
626
+ const cur = tiles[i];
627
+ const slide = Stage.slides[i];
628
+ const trans = slide?.transition || 'fade';
629
+
630
+ const prevTop = prev.offsetTop;
631
+ const prevLeft = prev.offsetLeft;
632
+ const prevRight = prevLeft + prev.offsetWidth;
633
+ const prevH = prev.offsetHeight;
634
+ const curTop = cur.offsetTop;
635
+ const curLeft = cur.offsetLeft;
636
+
637
+ // Same row? Tolerance for sub-pixel drift.
638
+ const sameRow = Math.abs(prevTop - curTop) < 6;
639
+
640
+ const icon = document.createElement('div');
641
+ icon.className = 'tx-connector-icon tile-edit-ui';
642
+ icon.dataset.idx = String(i);
643
+ icon.title = `Transition into slide ${i}: ${trans} — click to change`;
644
+ icon.innerHTML = `
645
+ <span class="tx-connector-glyph">${TRANSITION_ICONS[trans] || '◇'}</span>
646
+ <span class="tx-connector-label">${trans}</span>
647
+ `;
648
+ icon.addEventListener('click', (e) => {
649
+ e.stopPropagation();
650
+ openTransitionPicker(i, slide);
651
+ });
652
+
653
+ if (sameRow) {
654
+ const midY = prevTop + prevH / 2;
655
+ const lineLeft = prevRight;
656
+ const lineRight = curLeft;
657
+ const lineWidth = lineRight - lineLeft;
658
+
659
+ const line = document.createElement('div');
660
+ line.className = 'tx-connector-line tile-edit-ui';
661
+ line.style.left = lineLeft + 'px';
662
+ line.style.top = (midY - 1) + 'px';
663
+ line.style.width = lineWidth + 'px';
664
+ ov.appendChild(line);
665
+
666
+ // Icon centered on the line
667
+ icon.style.left = (lineLeft + lineWidth / 2 - 16) + 'px';
668
+ icon.style.top = (midY - 16) + 'px';
669
+ } else {
670
+ // Row break: small icon on the top-left edge of the new-row tile,
671
+ // with a tiny arc-line hint above it.
672
+ icon.classList.add('row-break');
673
+ icon.style.left = (curLeft + 8) + 'px';
674
+ icon.style.top = (curTop - 16) + 'px';
675
+ }
676
+
677
+ ov.appendChild(icon);
678
+ }
679
+ }
680
+
681
+ function openTransitionPicker(idx, slide) {
682
+ document.querySelectorAll('.transition-picker').forEach(n => n.remove());
683
+ const currentTx = slide?.transition || 'fade';
684
+ const pick = document.createElement('div');
685
+ pick.className = 'transition-picker edit-affordance';
686
+ pick.innerHTML = `
687
+ <div class="tp-title">How does slide ${idx} enter? <span class="tp-current">currently: <strong>${currentTx}</strong></span></div>
688
+ <div class="tp-hint">Hover an option to preview · click to apply</div>
689
+ <div class="tp-grid">
690
+ ${Object.keys(TRANSITION_ICONS).map(n => `
691
+ <div class="tp-option${n === currentTx ? ' is-current' : ''}" data-tx="${n}">
692
+ <div class="tp-stage"><div class="tp-stage-content">${TRANSITION_ICONS[n]} ${n}</div></div>
693
+ <div class="tp-meta">
694
+ <span class="tp-glyph">${TRANSITION_ICONS[n]}</span>
695
+ <span class="tp-name">${n}</span>
696
+ </div>
697
+ </div>
698
+ `).join('')}
699
+ </div>
700
+ <button class="tp-close">Cancel · esc</button>
701
+ `;
702
+ document.body.appendChild(pick);
703
+
704
+ // Hover → play the transition once on the option's preview stage.
705
+ pick.querySelectorAll('.tp-option').forEach(opt => {
706
+ const tx = opt.dataset.tx;
707
+ const stageContent = opt.querySelector('.tp-stage-content');
708
+ opt.addEventListener('mouseenter', () => playPreviewOnce(stageContent, tx));
709
+ opt.addEventListener('mouseleave', () => resetPreview(stageContent, tx));
710
+ opt.addEventListener('click', () => {
711
+ apiPost('/api/manifest/transition', { idx, transition: tx })
712
+ .then(r => {
713
+ if (r.ok) {
714
+ toast(`Transition → ${tx}`, 'ok');
715
+ pick.remove();
716
+ } else {
717
+ toast('Failed: ' + r.error, 'error');
718
+ }
719
+ });
720
+ });
721
+ });
722
+
723
+ pick.querySelector('.tp-close').addEventListener('click', () => pick.remove());
724
+
725
+ // Esc to close
726
+ function onEsc(e) { if (e.key === 'Escape') { pick.remove(); window.removeEventListener('keydown', onEsc); } }
727
+ window.addEventListener('keydown', onEsc);
728
+ }
729
+
730
+ function playPreviewOnce(el, name) {
731
+ if (!el) return;
732
+ // Restart the animation by removing + forcing reflow + adding.
733
+ el.classList.remove(`tx-${name}-enter`);
734
+ if (name === 'glitch') {
735
+ // Extra: spawn the scanline overlay used in glitch
736
+ el.parentElement?.querySelectorAll('.tx-glitch-overlay').forEach(n => n.remove());
737
+ const ov = document.createElement('div');
738
+ ov.className = 'tx-glitch-overlay';
739
+ el.parentElement?.appendChild(ov);
740
+ setTimeout(() => ov.remove(), 700);
741
+ }
742
+ void el.offsetWidth;
743
+ el.classList.add(`tx-${name}-enter`);
744
+ }
745
+
746
+ function resetPreview(el, name) {
747
+ if (!el) return;
748
+ el.classList.remove(`tx-${name}-enter`);
749
+ el.parentElement?.querySelectorAll('.tx-glitch-overlay').forEach(n => n.remove());
750
+ }
751
+
752
+ // ---------------------------------------------------------------------------
753
+ // API + helpers
754
+ // ---------------------------------------------------------------------------
755
+ function apiPost(path, data) {
756
+ return fetch(path, {
757
+ method: 'POST',
758
+ headers: { 'Content-Type': 'application/json' },
759
+ body: JSON.stringify(data)
760
+ }).then(r => r.json());
761
+ }
762
+
763
+ function slideFileForIdx(idx) {
764
+ const m = Stage._manifestSlides?.[idx];
765
+ return m?.src || null;
766
+ }
767
+
768
+ function currentSlideFile() {
769
+ return slideFileForIdx(Stage._engine.currentIndex());
770
+ }
771
+
772
+ function toast(msg, kind = 'ok') {
773
+ const t = document.createElement('div');
774
+ t.className = `edit-toast edit-toast-${kind}`;
775
+ t.textContent = msg;
776
+ document.body.appendChild(t);
777
+ setTimeout(() => t.classList.add('in'), 10);
778
+ setTimeout(() => { t.classList.remove('in'); setTimeout(() => t.remove(), 300); }, 1800);
779
+ }
780
+
781
+ function injectStyles() {
782
+ if (document.getElementById('stagecraft-edit-styles')) return;
783
+ const s = document.createElement('style');
784
+ s.id = 'stagecraft-edit-styles';
785
+ s.textContent = `
786
+ body.edit-mode { cursor: default; }
787
+
788
+ .edit-affordance { font-family: var(--mono, monospace); font-size: 0.85rem; }
789
+
790
+ .hover-outline {
791
+ position: fixed;
792
+ border: 1px dashed var(--accent, #00FF9C);
793
+ background: var(--accent-soft, rgba(0, 255, 156, 0.05));
794
+ pointer-events: none;
795
+ z-index: 9000;
796
+ transition: all 80ms ease-out;
797
+ }
798
+
799
+ .inline-editing {
800
+ outline: 2px solid var(--accent, #00FF9C);
801
+ outline-offset: 2px;
802
+ background: var(--accent-soft, rgba(0, 255, 156, 0.06));
803
+ cursor: text;
804
+ }
805
+ /* Scope to body.edit-mode so the hover affordance disappears when
806
+ edit-mode is toggled off via 'E' (server may still be running). */
807
+ body.edit-mode [data-stage-edit]:hover {
808
+ text-decoration: underline dotted var(--accent, #00FF9C);
809
+ text-underline-offset: 4px;
810
+ cursor: text;
811
+ }
812
+
813
+ .note-overlay {
814
+ position: fixed;
815
+ top: 50%; left: 50%;
816
+ transform: translate(-50%, -50%);
817
+ width: 380px;
818
+ background: var(--bg-elevated, #121212);
819
+ border: 1px solid var(--accent, #00FF9C);
820
+ box-shadow: 0 20px 60px rgba(0,0,0,0.6);
821
+ padding: 1rem;
822
+ z-index: 9100;
823
+ }
824
+ .note-overlay .note-title {
825
+ color: var(--dim, #666);
826
+ font-size: 0.72rem;
827
+ letter-spacing: 0.15em;
828
+ text-transform: uppercase;
829
+ margin-bottom: 0.7rem;
830
+ }
831
+ .note-overlay textarea {
832
+ width: 100%;
833
+ min-height: 100px;
834
+ background: var(--bg, #0a0a0a);
835
+ color: var(--fg, #e6e6e6);
836
+ border: 1px solid var(--dim-2, #2a2a2a);
837
+ padding: 0.6rem;
838
+ font-family: inherit;
839
+ font-size: 0.95rem;
840
+ resize: vertical;
841
+ }
842
+ .note-overlay .note-actions {
843
+ margin-top: 0.7rem;
844
+ display: flex;
845
+ gap: 0.5rem;
846
+ justify-content: flex-end;
847
+ }
848
+ .note-overlay button {
849
+ background: transparent;
850
+ color: var(--fg, #e6e6e6);
851
+ border: 1px solid var(--dim-2, #2a2a2a);
852
+ padding: 0.4rem 0.9rem;
853
+ font-family: inherit;
854
+ font-size: 0.8rem;
855
+ cursor: pointer;
856
+ letter-spacing: 0.08em;
857
+ }
858
+ .note-overlay button.note-save {
859
+ border-color: var(--accent, #00FF9C);
860
+ color: var(--accent, #00FF9C);
861
+ }
862
+
863
+ .tile-actions {
864
+ position: absolute;
865
+ top: 0.5rem; right: 0.5rem;
866
+ z-index: 6;
867
+ display: flex;
868
+ gap: 0.25rem;
869
+ }
870
+ .tile-action {
871
+ background: var(--bg-elevated, rgba(10, 10, 10, 0.78));
872
+ border: 1px solid var(--dim-2, #2a2a2a);
873
+ color: var(--fg, #e6e6e6);
874
+ width: 26px; height: 26px;
875
+ padding: 0;
876
+ cursor: pointer;
877
+ font-size: 0.78rem;
878
+ line-height: 1;
879
+ backdrop-filter: blur(4px);
880
+ }
881
+ .tile-action:hover {
882
+ border-color: var(--accent, #00FF9C);
883
+ color: var(--accent, #00FF9C);
884
+ }
885
+ .tile-action-danger:hover {
886
+ border-color: var(--red, #FF5C5C);
887
+ color: var(--red, #FF5C5C);
888
+ }
889
+
890
+ /* Storyboard toolbar — theme picker + process notes */
891
+ .sb-toolbar {
892
+ position: fixed;
893
+ top: 4rem; left: 50%;
894
+ transform: translateX(-50%);
895
+ z-index: 320;
896
+ background: var(--bg-elevated, rgba(10, 10, 10, 0.92));
897
+ backdrop-filter: blur(8px);
898
+ border: 1px solid var(--dim-2, #2a2a2a);
899
+ padding: 0.5rem 0.7rem;
900
+ display: flex;
901
+ align-items: center;
902
+ gap: 0.8rem;
903
+ font-family: var(--mono, monospace);
904
+ font-size: 0.7rem;
905
+ letter-spacing: 0.15em;
906
+ }
907
+ .sb-toolbar-group {
908
+ display: flex;
909
+ align-items: center;
910
+ gap: 0.5rem;
911
+ }
912
+ .sb-toolbar-label {
913
+ color: var(--dim, #666);
914
+ text-transform: uppercase;
915
+ }
916
+ .sb-theme-picker {
917
+ display: flex;
918
+ gap: 0.2rem;
919
+ }
920
+ .sb-theme-btn {
921
+ background: transparent;
922
+ border: 1px solid var(--dim-2, #2a2a2a);
923
+ color: var(--fg, #e6e6e6);
924
+ font-family: inherit;
925
+ font-size: 0.68rem;
926
+ letter-spacing: 0.1em;
927
+ text-transform: uppercase;
928
+ padding: 0.3rem 0.6rem;
929
+ cursor: pointer;
930
+ }
931
+ .sb-theme-btn:hover { border-color: var(--accent, #00FF9C); }
932
+ .sb-theme-btn.is-current {
933
+ border-color: var(--accent, #00FF9C);
934
+ color: var(--accent, #00FF9C);
935
+ background: var(--accent-soft, rgba(0, 255, 156, 0.06));
936
+ }
937
+ .sb-toolbar-spacer { width: 1px; height: 1.4rem; background: var(--dim-2, #2a2a2a); }
938
+ .sb-toolbar-btn {
939
+ background: transparent;
940
+ border: 1px solid var(--dim-2, #2a2a2a);
941
+ color: var(--fg, #e6e6e6);
942
+ font-family: inherit;
943
+ font-size: 0.7rem;
944
+ letter-spacing: 0.12em;
945
+ text-transform: uppercase;
946
+ padding: 0.3rem 0.7rem;
947
+ cursor: pointer;
948
+ display: inline-flex;
949
+ align-items: center;
950
+ gap: 0.4rem;
951
+ }
952
+ .sb-toolbar-btn:hover {
953
+ border-color: var(--accent, #00FF9C);
954
+ color: var(--accent, #00FF9C);
955
+ }
956
+ .sb-toolbar-icon { font-size: 0.95rem; }
957
+
958
+ /* Add-slide tile */
959
+ .tile-add {
960
+ display: flex;
961
+ flex-direction: column;
962
+ align-items: center;
963
+ justify-content: center;
964
+ gap: 0.5rem;
965
+ border-style: dashed;
966
+ background: transparent;
967
+ }
968
+ .tile-add:hover {
969
+ border-color: var(--accent, #00FF9C);
970
+ background: var(--accent-soft, rgba(0, 255, 156, 0.04));
971
+ }
972
+ .tile-add-glyph {
973
+ font-size: 3rem;
974
+ color: var(--dim, #666);
975
+ font-weight: 300;
976
+ }
977
+ .tile-add:hover .tile-add-glyph { color: var(--accent, #00FF9C); }
978
+ .tile-add-label {
979
+ font-size: 0.7rem;
980
+ letter-spacing: 0.2em;
981
+ text-transform: uppercase;
982
+ color: var(--dim, #666);
983
+ font-family: var(--mono, monospace);
984
+ }
985
+
986
+ /* Add-slide dialog */
987
+ .add-slide-dialog {
988
+ position: fixed;
989
+ top: 50%; left: 50%;
990
+ transform: translate(-50%, -50%);
991
+ background: var(--bg-elevated, #121212);
992
+ border: 1px solid var(--accent, #00FF9C);
993
+ padding: 1.4rem;
994
+ z-index: 9300;
995
+ min-width: 460px;
996
+ box-shadow: 0 30px 80px rgba(0,0,0,0.7);
997
+ }
998
+ .add-slide-dialog .asd-title {
999
+ font-size: 0.78rem;
1000
+ letter-spacing: 0.18em;
1001
+ text-transform: uppercase;
1002
+ color: var(--dim, #666);
1003
+ margin-bottom: 1rem;
1004
+ }
1005
+ .add-slide-dialog .asd-row {
1006
+ display: grid;
1007
+ grid-template-columns: 110px 1fr;
1008
+ gap: 0.7rem;
1009
+ align-items: center;
1010
+ margin-bottom: 0.7rem;
1011
+ }
1012
+ .add-slide-dialog .asd-label {
1013
+ font-size: 0.7rem;
1014
+ letter-spacing: 0.15em;
1015
+ text-transform: uppercase;
1016
+ color: var(--dim, #666);
1017
+ }
1018
+ .add-slide-dialog .asd-input {
1019
+ background: var(--bg, #0a0a0a);
1020
+ color: var(--fg, #e6e6e6);
1021
+ border: 1px solid var(--dim-2, #2a2a2a);
1022
+ padding: 0.5rem 0.6rem;
1023
+ font-family: var(--mono, monospace);
1024
+ font-size: 0.9rem;
1025
+ }
1026
+ .add-slide-dialog .asd-actions {
1027
+ margin-top: 1rem;
1028
+ display: flex;
1029
+ gap: 0.5rem;
1030
+ justify-content: flex-end;
1031
+ }
1032
+ .add-slide-dialog button {
1033
+ background: transparent;
1034
+ color: var(--fg, #e6e6e6);
1035
+ border: 1px solid var(--dim-2, #2a2a2a);
1036
+ padding: 0.4rem 0.9rem;
1037
+ font-family: inherit;
1038
+ font-size: 0.78rem;
1039
+ letter-spacing: 0.1em;
1040
+ cursor: pointer;
1041
+ }
1042
+ .add-slide-dialog .asd-create {
1043
+ border-color: var(--accent, #00FF9C);
1044
+ color: var(--accent, #00FF9C);
1045
+ }
1046
+
1047
+ .note-overlay .note-hint {
1048
+ color: var(--dim, #666);
1049
+ font-size: 0.7rem;
1050
+ letter-spacing: 0.1em;
1051
+ margin-bottom: 0.5rem;
1052
+ }
1053
+
1054
+ /* Pin marker on an annotated element in present mode */
1055
+ .pin-marker {
1056
+ position: absolute;
1057
+ top: -8px; right: -8px;
1058
+ width: 16px; height: 16px;
1059
+ background: var(--amber, #FFB454);
1060
+ color: #000;
1061
+ font-size: 10px;
1062
+ line-height: 16px;
1063
+ text-align: center;
1064
+ border-radius: 50%;
1065
+ z-index: 50;
1066
+ cursor: help;
1067
+ animation: pin-pulse 2.4s ease-in-out infinite;
1068
+ box-shadow: 0 0 8px rgba(255, 180, 84, 0.6);
1069
+ }
1070
+ @keyframes pin-pulse {
1071
+ 0%, 100% { transform: scale(1); }
1072
+ 50% { transform: scale(1.18); }
1073
+ }
1074
+
1075
+ /* Connector line + icon between adjacent storyboard tiles */
1076
+ .tx-connector-line {
1077
+ position: absolute;
1078
+ height: 1px;
1079
+ background: linear-gradient(to right, transparent 0%, var(--dim, #666) 20%, var(--dim, #666) 80%, transparent 100%);
1080
+ pointer-events: none;
1081
+ z-index: 5;
1082
+ }
1083
+ .tx-connector-icon {
1084
+ position: absolute;
1085
+ width: 32px; height: 32px;
1086
+ background: var(--bg, #0a0a0a);
1087
+ border: 1px solid var(--dim-2, #2a2a2a);
1088
+ border-radius: 50%;
1089
+ display: flex;
1090
+ align-items: center;
1091
+ justify-content: center;
1092
+ cursor: pointer;
1093
+ z-index: 6;
1094
+ transition: border-color 180ms, transform 180ms, box-shadow 180ms;
1095
+ color: var(--dim, #666);
1096
+ }
1097
+ .tx-connector-icon:hover {
1098
+ border-color: var(--accent, #00FF9C);
1099
+ color: var(--accent, #00FF9C);
1100
+ transform: scale(1.15);
1101
+ box-shadow: 0 0 14px var(--accent-glow, rgba(0,255,156,0.45));
1102
+ }
1103
+ .tx-connector-icon.row-break {
1104
+ background: var(--bg-elevated, rgba(10, 10, 10, 0.92));
1105
+ backdrop-filter: blur(4px);
1106
+ }
1107
+ .tx-connector-glyph { font-size: 0.95rem; line-height: 1; }
1108
+ .tx-connector-label {
1109
+ position: absolute;
1110
+ top: 100%; left: 50%;
1111
+ transform: translateX(-50%);
1112
+ margin-top: 0.4rem;
1113
+ font-size: 0.6rem;
1114
+ letter-spacing: 0.2em;
1115
+ color: var(--dim, #666);
1116
+ text-transform: uppercase;
1117
+ white-space: nowrap;
1118
+ opacity: 0;
1119
+ transition: opacity 180ms;
1120
+ pointer-events: none;
1121
+ background: var(--bg-elevated, rgba(10, 10, 10, 0.95));
1122
+ padding: 0.2rem 0.5rem;
1123
+ }
1124
+ .tx-connector-icon:hover .tx-connector-label { opacity: 1; }
1125
+
1126
+ /* Transition picker */
1127
+ .transition-picker {
1128
+ position: fixed;
1129
+ top: 50%; left: 50%;
1130
+ transform: translate(-50%, -50%);
1131
+ background: var(--bg-elevated, #121212);
1132
+ border: 1px solid var(--accent, #00FF9C);
1133
+ padding: 1.4rem;
1134
+ z-index: 9200;
1135
+ min-width: 720px;
1136
+ max-width: min(900px, 92vw);
1137
+ max-height: 86vh;
1138
+ overflow-y: auto;
1139
+ box-shadow: 0 30px 80px rgba(0,0,0,0.7);
1140
+ }
1141
+ .transition-picker .tp-title {
1142
+ color: var(--fg, #e6e6e6);
1143
+ font-size: 0.85rem;
1144
+ letter-spacing: 0.15em;
1145
+ text-transform: uppercase;
1146
+ margin-bottom: 0.4rem;
1147
+ display: flex;
1148
+ justify-content: space-between;
1149
+ gap: 1rem;
1150
+ }
1151
+ .transition-picker .tp-current { color: var(--dim, #666); font-weight: 400; }
1152
+ .transition-picker .tp-current strong { color: var(--accent, #00FF9C); font-weight: 500; }
1153
+ .transition-picker .tp-hint {
1154
+ font-size: 0.7rem;
1155
+ letter-spacing: 0.15em;
1156
+ color: var(--dim, #666);
1157
+ margin-bottom: 1rem;
1158
+ text-transform: uppercase;
1159
+ }
1160
+ .transition-picker .tp-grid {
1161
+ display: grid;
1162
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
1163
+ gap: 0.8rem;
1164
+ }
1165
+ .transition-picker .tp-option {
1166
+ border: 1px solid var(--dim-2, #2a2a2a);
1167
+ padding: 0;
1168
+ cursor: pointer;
1169
+ background: var(--bg, #0a0a0a);
1170
+ transition: border-color 180ms;
1171
+ position: relative;
1172
+ overflow: hidden;
1173
+ }
1174
+ .transition-picker .tp-option:hover { border-color: var(--accent, #00FF9C); }
1175
+ .transition-picker .tp-option.is-current {
1176
+ border-color: var(--accent, #00FF9C);
1177
+ box-shadow: inset 0 0 0 1px var(--accent, #00FF9C);
1178
+ }
1179
+ .transition-picker .tp-stage {
1180
+ height: 90px;
1181
+ background: var(--bg-elevated, #121212);
1182
+ position: relative;
1183
+ overflow: hidden;
1184
+ display: flex;
1185
+ align-items: center;
1186
+ justify-content: center;
1187
+ }
1188
+ .transition-picker .tp-stage-content {
1189
+ font-size: 0.95rem;
1190
+ letter-spacing: 0.1em;
1191
+ color: var(--accent, #00FF9C);
1192
+ background: var(--bg-elevated, #121212);
1193
+ padding: 0.4rem 0.9rem;
1194
+ border: 1px solid var(--dim-2, #2a2a2a);
1195
+ /* Initially invisible — only the hover-triggered animation reveals it */
1196
+ opacity: 0;
1197
+ }
1198
+ .transition-picker .tp-meta {
1199
+ padding: 0.5rem 0.7rem;
1200
+ display: flex;
1201
+ align-items: center;
1202
+ gap: 0.5rem;
1203
+ border-top: 1px solid var(--dim-2, #2a2a2a);
1204
+ }
1205
+ .transition-picker .tp-glyph { font-size: 1rem; color: var(--accent, #00FF9C); }
1206
+ .transition-picker .tp-name {
1207
+ font-size: 0.7rem;
1208
+ letter-spacing: 0.18em;
1209
+ color: var(--fg, #e6e6e6);
1210
+ text-transform: uppercase;
1211
+ }
1212
+ .transition-picker .tp-close {
1213
+ margin-top: 1rem;
1214
+ background: transparent;
1215
+ border: 1px solid var(--dim-2, #2a2a2a);
1216
+ color: var(--dim, #666);
1217
+ padding: 0.4rem 0.9rem;
1218
+ cursor: pointer;
1219
+ font-family: inherit;
1220
+ font-size: 0.72rem;
1221
+ letter-spacing: 0.18em;
1222
+ text-transform: uppercase;
1223
+ }
1224
+ .transition-picker .tp-close:hover {
1225
+ border-color: var(--accent, #00FF9C);
1226
+ color: var(--accent, #00FF9C);
1227
+ }
1228
+
1229
+ .tile.dragging { opacity: 0.4; }
1230
+ .tile.drop-target { border-color: var(--accent, #00FF9C) !important; box-shadow: 0 0 0 2px var(--accent, #00FF9C); }
1231
+
1232
+ .edit-toast {
1233
+ position: fixed;
1234
+ top: 2rem;
1235
+ right: 2rem;
1236
+ background: var(--bg-elevated, #121212);
1237
+ color: var(--fg, #e6e6e6);
1238
+ border: 1px solid var(--dim-2, #2a2a2a);
1239
+ padding: 0.7rem 1.2rem;
1240
+ font-family: var(--mono, monospace);
1241
+ font-size: 0.78rem;
1242
+ letter-spacing: 0.1em;
1243
+ z-index: 9999;
1244
+ opacity: 0;
1245
+ transform: translateY(-10px);
1246
+ transition: all 250ms ease-out;
1247
+ }
1248
+ .edit-toast.in { opacity: 1; transform: translateY(0); }
1249
+ .edit-toast-ok { border-color: var(--accent, #00FF9C); }
1250
+ .edit-toast-warn { border-color: var(--amber, #FFB454); }
1251
+ .edit-toast-error { border-color: var(--red, #FF5C5C); color: var(--red, #FF5C5C); }
1252
+ `;
1253
+ document.head.appendChild(s);
1254
+ }
1255
+
1256
+ })(typeof window !== 'undefined' ? window : globalThis);