js-kompo-utils 1.0.13 → 1.0.15

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 (34) hide show
  1. package/components/VlDropdown.vue +8 -7
  2. package/components/VlTutorialStepBuilder.vue +137 -586
  3. package/components/step-builder/SbContextMenu.vue +41 -0
  4. package/components/step-builder/SbGraphView.vue +93 -0
  5. package/components/step-builder/SbHeader.vue +16 -0
  6. package/components/step-builder/SbHistoryDrawer.vue +43 -0
  7. package/components/step-builder/SbPathPreview.vue +57 -0
  8. package/components/step-builder/SbStepList.vue +87 -0
  9. package/components/step-builder/SbTimeline.vue +66 -0
  10. package/components/step-builder/cards/SbActionsCard.vue +26 -0
  11. package/components/step-builder/cards/SbAdvanceCard.vue +43 -0
  12. package/components/step-builder/cards/SbConditionsCard.vue +20 -0
  13. package/components/step-builder/cards/SbContentCard.vue +45 -0
  14. package/components/step-builder/cards/SbCursorCard.vue +102 -0
  15. package/components/step-builder/cards/SbGlobalOptsCard.vue +40 -0
  16. package/components/step-builder/cards/SbHighlightCard.vue +40 -0
  17. package/components/step-builder/cards/SbHoverCard.vue +32 -0
  18. package/components/step-builder/cards/SbOptionsCard.vue +50 -0
  19. package/components/step-builder/cards/SbOutputCard.vue +17 -0
  20. package/components/step-builder/cards/SbPositionCard.vue +87 -0
  21. package/components/step-builder/cards/SbScrollCard.vue +17 -0
  22. package/docs/extending-tutorial-system.md +376 -0
  23. package/package.json +10 -9
  24. package/tutorials/TutorialConfig.js +4 -0
  25. package/tutorials/TutorialEngine.js +49 -3
  26. package/tutorials/extensions/HideIfExtension.js +12 -0
  27. package/tutorials/extensions/LinkNextExtension.js +117 -0
  28. package/tutorials/modules/Cursor.js +24 -4
  29. package/tutorials/modules/Highlight.js +7 -5
  30. package/tutorials/modules/HoverForcer.js +1 -0
  31. package/tutorials/modules/Overlay.js +2 -0
  32. package/tutorials/step-builder/StepBuilderState.js +265 -0
  33. package/tutorials/tutorial.scss +186 -0
  34. package/tutorials/step-builder/step-builder-bridge.js +0 -436
@@ -1,436 +0,0 @@
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
- }