js-kompo-utils 1.0.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 (67) hide show
  1. package/README.md +1 -0
  2. package/app.js +22 -0
  3. package/components/VlApexChart.vue +89 -0
  4. package/components/VlCKEditorTemplate.vue +52 -0
  5. package/components/VlCKEditorWithVars.vue +80 -0
  6. package/components/VlCollapsible.vue +91 -0
  7. package/components/VlCopyBox.vue +61 -0
  8. package/components/VlCreditCardInput.vue +104 -0
  9. package/components/VlDateMonthDay.vue +327 -0
  10. package/components/VlDateTextInput.vue +151 -0
  11. package/components/VlInternationalPhoneInput.vue +902 -0
  12. package/components/VlPartialDate.vue +283 -0
  13. package/components/VlPasswordInput.vue +358 -0
  14. package/components/VlQrCodeScanner.vue +91 -0
  15. package/components/VlSwipeableTab.vue +35 -0
  16. package/components/VlSwipeableTabs.vue +325 -0
  17. package/components/VlText.vue +72 -0
  18. package/components/VlTutorialStepBuilder.vue +671 -0
  19. package/components/VlValidatedInput.vue +262 -0
  20. package/components/data/countries.js +253 -0
  21. package/components/getAllDefaultComponents.js +12 -0
  22. package/docs/architecture.md +71 -0
  23. package/docs/tutorials.md +247 -0
  24. package/docs/usage.md +128 -0
  25. package/functions/index.js +7 -0
  26. package/functions/inputs.js +64 -0
  27. package/functions/loading.js +20 -0
  28. package/kompo-ckeditor-1.0.17/index.js +5 -0
  29. package/kompo-ckeditor-1.0.17/komponents/CKEditor.vue +36 -0
  30. package/kompo-ckeditor-1.0.17/komponents/CKEditorExtended.vue +66 -0
  31. package/kompo-ckeditor-1.0.17/komponents/EditableCKEditor.vue +42 -0
  32. package/kompo-ckeditor-1.0.17/komponents/TranslatableEditor.vue +28 -0
  33. package/kompo-ckeditor-1.0.17/mixins/CKEditor.js +291 -0
  34. package/kompo-ckeditor-1.0.17/mixins/CKEditorUtilities.js +52 -0
  35. package/kompo-ckeditor-1.0.17/mixins/CkUploadAdapter.js +95 -0
  36. package/kompo-ckeditor-1.0.17/mixins/translations/en.js +1 -0
  37. package/kompo-ckeditor-1.0.17/mixins/translations/fr.js +1 -0
  38. package/kompo-ckeditor-1.0.17/plugins/JsonBlockCommand.js +100 -0
  39. package/kompo-ckeditor-1.0.17/plugins/JsonBlockEditing.js +225 -0
  40. package/kompo-ckeditor-1.0.17/plugins/JsonBlockUi.js +21 -0
  41. package/kompo-ckeditor-1.0.17/plugins/MentionPlugin.js +118 -0
  42. package/orchestrator/KompoJsBuilder.js +333 -0
  43. package/package.json +25 -0
  44. package/raw_animations/count-up-animation.js +220 -0
  45. package/raw_animations/magic-poof.js +210 -0
  46. package/tutorials/TutorialConfig.js +34 -0
  47. package/tutorials/TutorialEngine.js +448 -0
  48. package/tutorials/index.js +2 -0
  49. package/tutorials/modules/AutoNext.js +86 -0
  50. package/tutorials/modules/Avatar.js +94 -0
  51. package/tutorials/modules/Bubble.js +617 -0
  52. package/tutorials/modules/Cursor.js +315 -0
  53. package/tutorials/modules/Highlight.js +282 -0
  54. package/tutorials/modules/HoverForcer.js +250 -0
  55. package/tutorials/modules/Mobile.js +148 -0
  56. package/tutorials/modules/Options.js +98 -0
  57. package/tutorials/modules/Overlay.js +142 -0
  58. package/tutorials/modules/Scroll.js +37 -0
  59. package/tutorials/modules/Typewriter.js +132 -0
  60. package/tutorials/step-builder/step-builder-bridge.js +436 -0
  61. package/tutorials/step-builder/utils/bezier-editor.js +379 -0
  62. package/tutorials/step-builder/utils/code-generator.js +127 -0
  63. package/tutorials/step-builder/utils/element-picker.js +125 -0
  64. package/tutorials/tutorial.scss +816 -0
  65. package/tutorials/utils/selectors.js +225 -0
  66. package/tutorials/utils/stability.js +76 -0
  67. package/tutorials/utils/svg.js +46 -0
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Typewriter module: renders HTML content character-by-character.
3
+ *
4
+ * Walks DOM nodes, creates elements with their attributes, and types text
5
+ * nodes character-by-character. Supports click-to-skip.
6
+ */
7
+ export class Typewriter {
8
+ constructor(engine) {
9
+ this.engine = engine;
10
+ this._skipped = false;
11
+ this._skipHandler = null;
12
+ this._callbackFired = false;
13
+ }
14
+
15
+ /**
16
+ * Typewrite HTML content into a container, node by node.
17
+ * @param {HTMLElement} container
18
+ * @param {string} html
19
+ * @param {number} speed - Milliseconds per character
20
+ * @param {Function} [onComplete]
21
+ */
22
+ type(container, html, speed, onComplete) {
23
+ this.destroy();
24
+ this._skipped = false;
25
+ this._callbackFired = false;
26
+
27
+ const fireCallback = () => {
28
+ if (this._callbackFired) return;
29
+ this._callbackFired = true;
30
+ onComplete?.();
31
+ };
32
+
33
+ const skipAll = () => {
34
+ if (this._skipped) return;
35
+ this._skipped = true;
36
+ container.innerHTML = html;
37
+ fireCallback();
38
+ };
39
+
40
+ this._skipHandler = skipAll;
41
+ document.addEventListener('click', skipAll, { once: true, capture: true });
42
+ document.addEventListener('touchstart', skipAll, { once: true, capture: true });
43
+
44
+ this._typewriteHtml(container, html, speed, () => {
45
+ this._removeSkipListeners();
46
+ fireCallback();
47
+ }, null);
48
+ }
49
+
50
+ skip() {
51
+ if (this._skipHandler) {
52
+ this._skipHandler();
53
+ }
54
+ }
55
+
56
+ destroy() {
57
+ this._removeSkipListeners();
58
+ this._skipped = true;
59
+ this._callbackFired = true;
60
+ this._skipHandler = null;
61
+ }
62
+
63
+ /** @private */
64
+ _removeSkipListeners() {
65
+ if (this._skipHandler) {
66
+ document.removeEventListener('click', this._skipHandler, true);
67
+ document.removeEventListener('touchstart', this._skipHandler, true);
68
+ }
69
+ }
70
+
71
+ /** @private */
72
+ _typewriteHtml(container, html, speed, callback, parentIsSkipped) {
73
+ const isSkipped = () => {
74
+ return this._skipped || (parentIsSkipped && parentIsSkipped());
75
+ };
76
+
77
+ const temp = document.createElement('div');
78
+ temp.innerHTML = html;
79
+ const nodes = Array.from(temp.childNodes);
80
+ let nodeIndex = 0;
81
+
82
+ const processNode = () => {
83
+ if (isSkipped()) return;
84
+ if (nodeIndex >= nodes.length) {
85
+ callback();
86
+ return;
87
+ }
88
+
89
+ const node = nodes[nodeIndex];
90
+ nodeIndex++;
91
+
92
+ if (node.nodeType === Node.TEXT_NODE) {
93
+ this._typewriteText(container, node.textContent, speed, processNode, isSkipped);
94
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
95
+ const el = document.createElement(node.tagName);
96
+ for (let i = 0; i < node.attributes.length; i++) {
97
+ el.setAttribute(node.attributes[i].name, node.attributes[i].value);
98
+ }
99
+ container.appendChild(el);
100
+ if (node.childNodes.length > 0) {
101
+ this._typewriteHtml(el, node.innerHTML, speed, processNode, isSkipped);
102
+ } else {
103
+ processNode();
104
+ }
105
+ } else {
106
+ processNode();
107
+ }
108
+ };
109
+
110
+ processNode();
111
+ }
112
+
113
+ /** @private */
114
+ _typewriteText(container, text, speed, callback, isSkipped) {
115
+ let i = 0;
116
+ const textNode = document.createTextNode('');
117
+ container.appendChild(textNode);
118
+
119
+ const tick = () => {
120
+ if (isSkipped && isSkipped()) return;
121
+ if (i < text.length) {
122
+ textNode.textContent += text.charAt(i);
123
+ i++;
124
+ setTimeout(tick, speed);
125
+ } else {
126
+ callback();
127
+ }
128
+ };
129
+
130
+ tick();
131
+ }
132
+ }
@@ -0,0 +1,436 @@
1
+ /**
2
+ * StepBuilderBridge - Connects the TutorialStepBuilder Kompo Form to the
3
+ * TutorialEngine using Kompo's $k context (field, watchAll, panel, on).
4
+ *
5
+ * Initialized by TutorialStepBuilder.php onLoad handler which passes
6
+ * the real $k context. StepBuilderBridge class is pre-registered on
7
+ * window by KompoJsBuilder at boot time.
8
+ */
9
+
10
+ import { pickElement } from './utils/element-picker.js';
11
+ import { generateFullJson } from './utils/code-generator.js';
12
+
13
+ const WATCHED_FIELDS = [
14
+ 'sb-html', 'sb-overlay', 'sb-show-back',
15
+ 'sb-position', 'sb-align', 'sb-chat-mode', 'sb-position-target',
16
+ 'sb-advance', 'sb-auto-next-delay',
17
+ 'sb-silent-click', 'sb-redirect', 'sb-show-if',
18
+ 'sb-cursor-from', 'sb-cursor-to', 'sb-cursor-duration',
19
+ 'sb-cursor-click', 'sb-cursor-loop',
20
+ 'sb-highlight-elements', 'sb-highlight-mode',
21
+ 'sb-hover',
22
+ 'sb-scroll-to', 'sb-scroll-inside-selector', 'sb-scroll-inside-amount',
23
+ ];
24
+
25
+ export class StepBuilderBridge {
26
+ /**
27
+ * @param {object} ctx - Kompo $k context from onLoad (field, watchAll, panel, on, etc.)
28
+ * @param {TutorialEngine} engine
29
+ */
30
+ constructor(ctx, engine) {
31
+ this.ctx = ctx || {};
32
+ this.engine = engine || window.TutorialEngine;
33
+ this.steps = [{ html: '', overlay: true }];
34
+ this.selectedIndex = 0;
35
+
36
+ // Extract $k helpers with safe fallbacks
37
+ this._field = ctx?.field || (() => ({ value: '', set() {} }));
38
+ this._panel = ctx?.panel || (() => ({ fill() {} }));
39
+ this._watchAll = ctx?.watchAll || null;
40
+ this._on = ctx?.on || null;
41
+ this._el = ctx?.el || (() => null);
42
+
43
+ // Set data-sb for scoped styling
44
+ const panelEl = document.getElementById('tutorial-step-builder');
45
+ if (panelEl) panelEl.setAttribute('data-sb', '');
46
+
47
+ this._setupWatchers();
48
+ this._setupEventDelegation();
49
+
50
+ // Sync with engine when tutorial data loads
51
+ if (this.engine?.onReady) {
52
+ this.engine.onReady(() => this._initFromEngine());
53
+ }
54
+
55
+ // Fallback: catch tutorials started after bridge init
56
+ document.addEventListener('tutorial-step-change', () => {
57
+ if (!this.engine) this.engine = window.TutorialEngine;
58
+ if (this.engine?.steps?.length && this.steps.length <= 1 && !this.steps[0]?.html) {
59
+ this._initFromEngine();
60
+ }
61
+ });
62
+ }
63
+
64
+ // ─── Engine Sync ─────────────────────────────────────────────────
65
+
66
+ _initFromEngine() {
67
+ if (!this.engine?.steps?.length) return;
68
+ this.steps = this.engine.steps.map(s => ({ ...s }));
69
+ this.selectedIndex = Math.min(this.selectedIndex, this.steps.length - 1);
70
+ this._renderStepList();
71
+ this._syncStepToForm();
72
+ this._renderOptions();
73
+ this._updateOutput();
74
+ }
75
+
76
+ _previewCurrentStep() {
77
+ if (!this.engine) return;
78
+ this.engine.steps = this.steps;
79
+ this.engine.previewStep(this.selectedIndex);
80
+ }
81
+
82
+ // ─── Watchers (Kompo $k) ─────────────────────────────────────────
83
+
84
+ _setupWatchers() {
85
+ if (!this._watchAll) return;
86
+
87
+ this._watchAll(WATCHED_FIELDS, () => {
88
+ this._syncFormToStep();
89
+ this._updateOutput();
90
+ this._previewCurrentStep();
91
+ });
92
+ }
93
+
94
+ // ─── Event Delegation (Kompo $k or DOM fallback) ─────────────────
95
+
96
+ _setupEventDelegation() {
97
+ const listen = this._on || this._domOn.bind(this);
98
+
99
+ // Step list clicks
100
+ listen('click', '.sb-step-item', (event) => {
101
+ const el = (event.target || event).closest?.('.sb-step-item') || event.target;
102
+ const index = parseInt(el?.dataset?.index ?? el?.getAttribute?.('data-index'));
103
+ if (!isNaN(index)) this.selectStep(index);
104
+ });
105
+
106
+ // Option label changes
107
+ listen('input', '.sb-option-label', (event) => {
108
+ const el = event.target || event;
109
+ const idx = parseInt(el.dataset?.index);
110
+ const step = this.steps[this.selectedIndex];
111
+ if (step.options?.[idx]) {
112
+ step.options[idx].label = el.value;
113
+ this._updateOutput();
114
+ }
115
+ });
116
+
117
+ // Option action changes
118
+ listen('change', '.sb-option-action', (event) => {
119
+ const el = event.target || event;
120
+ const idx = parseInt(el.dataset?.index);
121
+ const step = this.steps[this.selectedIndex];
122
+ if (!step.options?.[idx]) return;
123
+ const opt = step.options[idx];
124
+ delete opt.done; delete opt.goToStep; delete opt.redirect;
125
+ switch (el.value) {
126
+ case 'done': opt.done = true; break;
127
+ case 'goToStep': opt.goToStep = this.selectedIndex + 1; break;
128
+ case 'redirect': opt.redirect = '/'; break;
129
+ }
130
+ this._updateOutput();
131
+ });
132
+
133
+ // Option removal
134
+ listen('click', '.sb-option-remove', (event) => {
135
+ const el = (event.target || event).closest?.('.sb-option-remove') || event.target;
136
+ const idx = parseInt(el?.dataset?.index);
137
+ const step = this.steps[this.selectedIndex];
138
+ if (step.options) {
139
+ step.options.splice(idx, 1);
140
+ if (!step.options.length) delete step.options;
141
+ this._renderOptions();
142
+ this._updateOutput();
143
+ }
144
+ });
145
+ }
146
+
147
+ /** DOM fallback for event delegation when $k.on is unavailable */
148
+ _domOn(event, selector, handler) {
149
+ const panel = document.getElementById('tutorial-step-builder');
150
+ if (!panel) return;
151
+ panel.addEventListener(event, (e) => {
152
+ const target = e.target.closest(selector);
153
+ if (target) handler(e);
154
+ });
155
+ }
156
+
157
+ // ─── Step Management ─────────────────────────────────────────────
158
+
159
+ selectStep(index) {
160
+ if (index < 0 || index >= this.steps.length) return;
161
+ this.selectedIndex = index;
162
+ this._syncStepToForm();
163
+ this._renderStepList();
164
+ this._renderOptions();
165
+ this._previewCurrentStep();
166
+ }
167
+
168
+ addStep() {
169
+ this.steps.push({ html: '', overlay: true, position: 'left', align: 'center' });
170
+ this.selectStep(this.steps.length - 1);
171
+ }
172
+
173
+ deleteStep() {
174
+ if (this.steps.length <= 1) return;
175
+ this.steps.splice(this.selectedIndex, 1);
176
+ this.selectStep(Math.min(this.selectedIndex, this.steps.length - 1));
177
+ }
178
+
179
+ duplicateStep() {
180
+ const clone = JSON.parse(JSON.stringify(this.steps[this.selectedIndex]));
181
+ this.steps.splice(this.selectedIndex + 1, 0, clone);
182
+ this.selectStep(this.selectedIndex + 1);
183
+ }
184
+
185
+ moveStepUp() {
186
+ if (this.selectedIndex <= 0) return;
187
+ const i = this.selectedIndex;
188
+ [this.steps[i - 1], this.steps[i]] = [this.steps[i], this.steps[i - 1]];
189
+ this.selectStep(i - 1);
190
+ }
191
+
192
+ moveStepDown() {
193
+ if (this.selectedIndex >= this.steps.length - 1) return;
194
+ const i = this.selectedIndex;
195
+ [this.steps[i], this.steps[i + 1]] = [this.steps[i + 1], this.steps[i]];
196
+ this.selectStep(i + 1);
197
+ }
198
+
199
+ // ─── Form ↔ Step Sync ────────────────────────────────────────────
200
+
201
+ _syncStepToForm() {
202
+ const step = this.steps[this.selectedIndex];
203
+ if (!step) return;
204
+ const f = this._field;
205
+
206
+ f('sb-html').set(step.html || '');
207
+ f('sb-overlay').set(step.overlay !== false);
208
+ f('sb-show-back').set(!!step.showBack);
209
+ f('sb-position').set(step.position || 'left');
210
+ f('sb-align').set(step.align || 'center');
211
+ f('sb-chat-mode').set(!!step.chatMode);
212
+ f('sb-position-target').set(step.positionTarget || '');
213
+
214
+ let advance = 'click';
215
+ if (step.autoNext) advance = 'auto';
216
+ else if (step.afterAnimation) advance = 'afterAnimation';
217
+ f('sb-advance').set(advance);
218
+ f('sb-auto-next-delay').set(typeof step.autoNext === 'number' ? step.autoNext : 3);
219
+
220
+ f('sb-silent-click').set(step.silentClick || '');
221
+ f('sb-redirect').set(step.redirect || '');
222
+ f('sb-show-if').set(step.showIf || '');
223
+
224
+ f('sb-cursor-from').set(step.cursor?.from || '');
225
+ f('sb-cursor-to').set(step.cursor?.to || '');
226
+ f('sb-cursor-duration').set(step.cursor?.duration || 1.5);
227
+ f('sb-cursor-click').set(!!step.cursor?.click);
228
+ f('sb-cursor-loop').set(!!step.cursor?.loop);
229
+
230
+ const hlElements = step.highlight?.groups?.flatMap(g => g.elements || g)?.join(', ') || '';
231
+ f('sb-highlight-elements').set(hlElements);
232
+ f('sb-highlight-mode').set(step.highlight?.mode || 'together');
233
+
234
+ f('sb-hover').set(Array.isArray(step.hover) ? step.hover.join(', ') : (step.hover || ''));
235
+ f('sb-scroll-to').set(step.scrollTo || '');
236
+ f('sb-scroll-inside-selector').set(step.scrollInside?.selector || '');
237
+ f('sb-scroll-inside-amount').set(step.scrollInside?.by || '');
238
+
239
+ this._updateOutput();
240
+ }
241
+
242
+ _syncFormToStep() {
243
+ const step = this.steps[this.selectedIndex];
244
+ if (!step) return;
245
+ const v = (name) => this._field(name).value;
246
+
247
+ step.html = v('sb-html');
248
+ step.overlay = v('sb-overlay');
249
+ if (v('sb-show-back')) step.showBack = true; else delete step.showBack;
250
+
251
+ step.position = v('sb-position') || 'left';
252
+ step.align = v('sb-align') || 'center';
253
+ if (v('sb-chat-mode')) step.chatMode = true; else delete step.chatMode;
254
+ const posTarget = v('sb-position-target');
255
+ if (posTarget) step.positionTarget = posTarget; else delete step.positionTarget;
256
+
257
+ const advance = v('sb-advance');
258
+ delete step.autoNext; delete step.afterAnimation;
259
+ if (advance === 'auto') step.autoNext = parseFloat(v('sb-auto-next-delay')) || 3;
260
+ else if (advance === 'afterAnimation') step.afterAnimation = true;
261
+
262
+ const sc = v('sb-silent-click');
263
+ if (sc) step.silentClick = sc; else delete step.silentClick;
264
+ const rd = v('sb-redirect');
265
+ if (rd) step.redirect = rd; else delete step.redirect;
266
+ const si = v('sb-show-if');
267
+ if (si) step.showIf = si; else delete step.showIf;
268
+
269
+ const cf = v('sb-cursor-from'), ct = v('sb-cursor-to');
270
+ if (cf || ct) {
271
+ step.cursor = step.cursor || {};
272
+ step.cursor.from = cf;
273
+ step.cursor.to = ct;
274
+ step.cursor.duration = parseFloat(v('sb-cursor-duration')) || 1.5;
275
+ step.cursor.click = !!v('sb-cursor-click');
276
+ step.cursor.loop = !!v('sb-cursor-loop');
277
+ } else { delete step.cursor; }
278
+
279
+ const hl = v('sb-highlight-elements');
280
+ if (hl) {
281
+ step.highlight = {
282
+ mode: v('sb-highlight-mode') || 'together',
283
+ groups: [{ elements: hl.split(',').map(s => s.trim()).filter(Boolean) }],
284
+ };
285
+ } else { delete step.highlight; }
286
+
287
+ const hv = v('sb-hover');
288
+ if (hv) {
289
+ const parts = hv.split(',').map(s => s.trim()).filter(Boolean);
290
+ step.hover = parts.length === 1 ? parts[0] : parts;
291
+ } else { delete step.hover; }
292
+
293
+ const st = v('sb-scroll-to');
294
+ if (st) step.scrollTo = st; else delete step.scrollTo;
295
+ const sis = v('sb-scroll-inside-selector');
296
+ if (sis) {
297
+ step.scrollInside = { selector: sis, by: parseFloat(v('sb-scroll-inside-amount')) || 0, duration: 1 };
298
+ } else { delete step.scrollInside; }
299
+ }
300
+
301
+ // ─── Dynamic Rendering ───────────────────────────────────────────
302
+
303
+ _renderStepList() {
304
+ const items = this.steps.map((step, i) => {
305
+ const active = i === this.selectedIndex;
306
+ const text = (step.html || step.silentClick || '').substring(0, 30);
307
+ const icons = [];
308
+ if (step.cursor) icons.push('\u2316');
309
+ if (step.highlight) icons.push('\u2606');
310
+ if (step.silentClick) icons.push('\u21B5');
311
+ if (step.options) icons.push('\u2026');
312
+
313
+ return `<div class="sb-step-item ${active ? 'sb-active' : ''}" data-index="${i}">
314
+ <span class="sb-step-num">${i}</span>
315
+ <span class="sb-step-text">${this._escapeHtml(text)}</span>
316
+ <span class="sb-step-icons">${icons.join('')}</span>
317
+ </div>`;
318
+ }).join('');
319
+
320
+ this._panel('step-builder-step-list').fill(`<div class="sb-step-list-inner">${items}</div>`);
321
+ }
322
+
323
+ _renderOptions() {
324
+ const step = this.steps[this.selectedIndex];
325
+ if (!step?.options?.length) {
326
+ this._panel('sb-options-editor').fill('');
327
+ return;
328
+ }
329
+
330
+ const html = step.options.map((opt, i) =>
331
+ `<div class="sb-option-row">
332
+ <input class="sb-option-input sb-option-label"
333
+ value="${this._escapeHtml(opt.label || '')}" data-index="${i}" placeholder="Label">
334
+ <select class="sb-option-select sb-option-action" data-index="${i}">
335
+ <option value="next" ${!opt.done && opt.goToStep === undefined && !opt.redirect ? 'selected' : ''}>Next</option>
336
+ <option value="done" ${opt.done ? 'selected' : ''}>Done</option>
337
+ <option value="goToStep" ${opt.goToStep !== undefined ? 'selected' : ''}>Go To Step</option>
338
+ <option value="redirect" ${opt.redirect ? 'selected' : ''}>Redirect</option>
339
+ </select>
340
+ <button class="sb-option-remove" data-index="${i}">&times;</button>
341
+ </div>`
342
+ ).join('');
343
+
344
+ this._panel('sb-options-editor').fill(html);
345
+ }
346
+
347
+ _updateOutput() {
348
+ const outputEl = document.getElementById('sb-output');
349
+ if (!outputEl) return;
350
+ try {
351
+ outputEl.textContent = generateFullJson(this.steps, this.engine?.config);
352
+ } catch (e) {
353
+ outputEl.textContent = JSON.stringify(this.steps, null, 2);
354
+ }
355
+ }
356
+
357
+ // ─── Element Picker ──────────────────────────────────────────────
358
+
359
+ async pickElementFor(targetField) {
360
+ const selector = await pickElement();
361
+ if (selector) {
362
+ this._field(targetField).set(selector);
363
+ this._syncFormToStep();
364
+ this._updateOutput();
365
+ this._previewCurrentStep();
366
+ }
367
+ }
368
+
369
+ // ─── Clipboard ───────────────────────────────────────────────────
370
+
371
+ copyAll() {
372
+ let text;
373
+ try { text = generateFullJson(this.steps, this.engine?.config); }
374
+ catch (e) { text = JSON.stringify(this.steps, null, 2); }
375
+ this._copyText(text);
376
+ }
377
+
378
+ copyStep() {
379
+ this._copyText(JSON.stringify(this.steps[this.selectedIndex], null, 2));
380
+ }
381
+
382
+ _copyText(text) {
383
+ navigator.clipboard.writeText(text).catch(() => {
384
+ const ta = document.createElement('textarea');
385
+ ta.value = text;
386
+ ta.style.cssText = 'position:fixed;left:-9999px';
387
+ document.body.appendChild(ta);
388
+ ta.select();
389
+ document.execCommand('copy');
390
+ ta.remove();
391
+ });
392
+ }
393
+
394
+ // ─── Spectator Mode ──────────────────────────────────────────────
395
+
396
+ spectatorMode() {
397
+ const panelEl = document.getElementById('tutorial-step-builder');
398
+ if (panelEl) panelEl.style.display = 'none';
399
+
400
+ const exitBtn = document.createElement('button');
401
+ exitBtn.textContent = 'Exit Spectator (Esc)';
402
+ exitBtn.className = 'sb-spectator-exit';
403
+ exitBtn.onclick = exit;
404
+ document.body.appendChild(exitBtn);
405
+
406
+ const onEsc = (e) => { if (e.key === 'Escape') exit(); };
407
+ document.addEventListener('keydown', onEsc);
408
+
409
+ if (this.engine) { this.engine.steps = this.steps; this.engine.previewStep(0); }
410
+
411
+ const exit = () => {
412
+ if (panelEl) panelEl.style.display = '';
413
+ exitBtn.remove();
414
+ document.removeEventListener('keydown', onEsc);
415
+ this._previewCurrentStep();
416
+ };
417
+ }
418
+
419
+ // ─── Options ─────────────────────────────────────────────────────
420
+
421
+ addOption() {
422
+ const step = this.steps[this.selectedIndex];
423
+ step.options = step.options || [];
424
+ step.options.push({ label: 'New Option' });
425
+ this._renderOptions();
426
+ this._updateOutput();
427
+ }
428
+
429
+ // ─── Utilities ───────────────────────────────────────────────────
430
+
431
+ _escapeHtml(str) {
432
+ const div = document.createElement('div');
433
+ div.textContent = str;
434
+ return div.innerHTML;
435
+ }
436
+ }