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.
- package/AGENT.md +792 -0
- package/LICENSE +21 -0
- package/README.md +210 -0
- package/bin/cli.js +51 -0
- package/bin/export.js +137 -0
- package/bin/init.js +52 -0
- package/bin/lib/edit-ops.js +405 -0
- package/bin/serve.js +278 -0
- package/dist/stagecraft.bundle.css +4443 -0
- package/dist/stagecraft.bundle.js +7621 -0
- package/dist/themes/brand.bundle.css +5262 -0
- package/dist/themes/neon.bundle.css +5289 -0
- package/dist/themes/paper.bundle.css +5276 -0
- package/dist/themes/phosphor.bundle.css +4443 -0
- package/dist/themes/shopware.bundle.css +5850 -0
- package/examples/closing-card.js +74 -0
- package/examples/orchestration-graph.js +156 -0
- package/examples/terminal-log.js +109 -0
- package/examples/token-stream.js +96 -0
- package/examples/whoami.js +90 -0
- package/package.json +41 -0
- package/src/components/activity-list.js +75 -0
- package/src/components/agenda.js +79 -0
- package/src/components/bar-chart.js +162 -0
- package/src/components/before-after.js +135 -0
- package/src/components/bento.js +73 -0
- package/src/components/big-number.js +87 -0
- package/src/components/callout.js +75 -0
- package/src/components/checklist.js +81 -0
- package/src/components/code-block.js +141 -0
- package/src/components/code-diff.js +98 -0
- package/src/components/compare.js +85 -0
- package/src/components/counter.js +80 -0
- package/src/components/cta.js +69 -0
- package/src/components/cycle.js +146 -0
- package/src/components/definition.js +96 -0
- package/src/components/donut-chart.js +179 -0
- package/src/components/full-image.js +82 -0
- package/src/components/funnel.js +111 -0
- package/src/components/gauge.js +147 -0
- package/src/components/heatmap.js +141 -0
- package/src/components/image-grid.js +80 -0
- package/src/components/image-text.js +96 -0
- package/src/components/kinetic-text.js +72 -0
- package/src/components/kpi.js +106 -0
- package/src/components/line-chart.js +215 -0
- package/src/components/manifesto.js +104 -0
- package/src/components/marquee.js +63 -0
- package/src/components/matrix2x2.js +151 -0
- package/src/components/pillars.js +80 -0
- package/src/components/pricing.js +90 -0
- package/src/components/process-flow.js +133 -0
- package/src/components/progress.js +136 -0
- package/src/components/punchline.js +82 -0
- package/src/components/pyramid.js +107 -0
- package/src/components/qanda.js +60 -0
- package/src/components/quote.js +70 -0
- package/src/components/roadmap.js +130 -0
- package/src/components/section-card.js +45 -0
- package/src/components/shift-arrow.js +41 -0
- package/src/components/spark-line.js +147 -0
- package/src/components/spotlight.js +85 -0
- package/src/components/statement.js +106 -0
- package/src/components/stats.js +91 -0
- package/src/components/steps.js +83 -0
- package/src/components/swot.js +110 -0
- package/src/components/team-grid.js +87 -0
- package/src/components/testimonial.js +99 -0
- package/src/components/timeline.js +91 -0
- package/src/components/tip.js +63 -0
- package/src/components/venn.js +198 -0
- package/src/edit-mode.js +1256 -0
- package/src/engine.js +823 -0
- package/src/helpers.js +169 -0
- package/src/transitions.js +101 -0
- package/starter/index.html +40 -0
- package/starter/slides/00-title.js +12 -0
- package/starter/stagecraft.config.js +8 -0
- package/themes/brand/base.css +4 -0
- package/themes/brand/components-business.css +173 -0
- package/themes/brand/components-chart.css +65 -0
- package/themes/brand/components-content.css +126 -0
- package/themes/brand/components-data.css +162 -0
- package/themes/brand/components-diagram.css +115 -0
- package/themes/brand/components-layout.css +112 -0
- package/themes/brand/components.css +46 -0
- package/themes/brand/manifest.json +20 -0
- package/themes/brand/tokens.css +20 -0
- package/themes/brand/transitions.css +4 -0
- package/themes/neon/base.css +10 -0
- package/themes/neon/components-business.css +189 -0
- package/themes/neon/components-chart.css +70 -0
- package/themes/neon/components-content.css +112 -0
- package/themes/neon/components-data.css +160 -0
- package/themes/neon/components-diagram.css +109 -0
- package/themes/neon/components-layout.css +87 -0
- package/themes/neon/components.css +87 -0
- package/themes/neon/manifest.json +21 -0
- package/themes/neon/tokens.css +17 -0
- package/themes/neon/transitions.css +13 -0
- package/themes/paper/base.css +9 -0
- package/themes/paper/components-business.css +196 -0
- package/themes/paper/components-chart.css +74 -0
- package/themes/paper/components-content.css +108 -0
- package/themes/paper/components-data.css +168 -0
- package/themes/paper/components-diagram.css +89 -0
- package/themes/paper/components-layout.css +105 -0
- package/themes/paper/components.css +60 -0
- package/themes/paper/manifest.json +10 -0
- package/themes/paper/tokens.css +21 -0
- package/themes/paper/transitions.css +11 -0
- package/themes/phosphor/base.css +511 -0
- package/themes/phosphor/components-business.css +818 -0
- package/themes/phosphor/components-chart.css +415 -0
- package/themes/phosphor/components-content.css +530 -0
- package/themes/phosphor/components-data.css +824 -0
- package/themes/phosphor/components-diagram.css +427 -0
- package/themes/phosphor/components-layout.css +450 -0
- package/themes/phosphor/components.css +223 -0
- package/themes/phosphor/manifest.json +11 -0
- package/themes/phosphor/tokens.css +17 -0
- package/themes/phosphor/transitions.css +213 -0
- package/themes/shopware/base.css +94 -0
- package/themes/shopware/components-business.css +344 -0
- package/themes/shopware/components-chart.css +121 -0
- package/themes/shopware/components-content.css +169 -0
- package/themes/shopware/components-data.css +219 -0
- package/themes/shopware/components-diagram.css +129 -0
- package/themes/shopware/components-layout.css +166 -0
- package/themes/shopware/components.css +83 -0
- package/themes/shopware/manifest.json +21 -0
- package/themes/shopware/tokens.css +68 -0
- package/themes/shopware/transitions.css +22 -0
package/src/edit-mode.js
ADDED
|
@@ -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);
|