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,671 @@
1
+ <template>
2
+ <div v-show="visible" class="sb" @keydown.left.prevent="prevStep" @keydown.right.prevent="nextStep" tabindex="0">
3
+ <div class="sb-resize" @mousedown="startResize"></div>
4
+
5
+ <!-- Header -->
6
+ <div class="sb-header">
7
+ <span class="sb-header-title">Step Builder</span>
8
+ <button class="sb-hbtn" title="Spectator" @click="spectatorMode">&#9654;</button>
9
+ <button class="sb-hbtn" title="Copy All" @click="copyAll">&#128203;</button>
10
+ <button class="sb-hbtn" title="Close" @click="visible = false">&times;</button>
11
+ </div>
12
+
13
+ <!-- Step List -->
14
+ <div class="sb-steps">
15
+ <div class="sb-steps-bar">
16
+ <span class="sb-steps-count">STEPS ({{ steps.length }})</span>
17
+ <button class="sb-btn sb-btn-sm sb-btn-green" @click="addStep">+</button>
18
+ </div>
19
+ <div class="sb-steps-list">
20
+ <div v-for="(s, i) in steps" :key="i"
21
+ class="sb-step" :class="{ 'sb-step--active': i === idx }"
22
+ @click="selectStep(i)">
23
+ <span class="sb-step-num">{{ i }}</span>
24
+ <span class="sb-step-text">{{ stepPreview(s) }}</span>
25
+ <span class="sb-step-icons">
26
+ <span v-if="s.cursor" title="cursor">&#8982;</span>
27
+ <span v-if="s.highlight" title="highlight">&#9734;</span>
28
+ <span v-if="s.options" title="options">&#8230;</span>
29
+ </span>
30
+ </div>
31
+ </div>
32
+ <div class="sb-step-actions">
33
+ <button class="sb-hbtn" @click="moveStepUp" title="Move up">&#8593;</button>
34
+ <button class="sb-hbtn" @click="moveStepDown" title="Move down">&#8595;</button>
35
+ <button class="sb-hbtn" @click="duplicateStep" title="Duplicate">&#8916;</button>
36
+ <button class="sb-hbtn sb-hbtn--danger" @click="deleteStep" title="Delete">&#10005;</button>
37
+ </div>
38
+ </div>
39
+
40
+ <!-- Detail Zone -->
41
+ <div class="sb-detail">
42
+
43
+ <!-- Content -->
44
+ <div class="sb-card" :class="{ 'sb-card--open': cards.content }">
45
+ <div class="sb-card-head" @click="toggle('content')">
46
+ <span>CONTENT</span>
47
+ <span class="sb-chevron">{{ cards.content ? '&#9660;' : '&#9654;' }}</span>
48
+ </div>
49
+ <div v-show="cards.content" class="sb-card-body">
50
+ <label class="sb-lbl">HTML / Key</label>
51
+ <textarea v-model="step.html" class="sb-input sb-textarea" rows="3"></textarea>
52
+ <div class="sb-row">
53
+ <label class="sb-check"><input type="checkbox" v-model="step.overlay"> overlay</label>
54
+ <label class="sb-check"><input type="checkbox" v-model="showBack"> showBack</label>
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ <!-- Position -->
60
+ <div class="sb-card" :class="{ 'sb-card--open': cards.position }">
61
+ <div class="sb-card-head" @click="toggle('position')">
62
+ <span>POSITION</span>
63
+ <span class="sb-chevron">{{ cards.position ? '&#9660;' : '&#9654;' }}</span>
64
+ </div>
65
+ <div v-show="cards.position" class="sb-card-body">
66
+ <div class="sb-row">
67
+ <label class="sb-lbl">Position</label>
68
+ <select v-model="step.position" @change="previewNow()" class="sb-input">
69
+ <option value="left">Left of avatar</option>
70
+ <option value="right">Right of avatar</option>
71
+ <option value="top">Above avatar</option>
72
+ <option value="bottom">Below avatar</option>
73
+ <option value="custom">Custom (manual)</option>
74
+ </select>
75
+ </div>
76
+ <!-- Custom position: free left/top values -->
77
+ <template v-if="step.position === 'custom'">
78
+ <div class="sb-row">
79
+ <label class="sb-lbl">Left</label>
80
+ <input v-model="step.bubbleLeft" @input="previewNow()" class="sb-input" placeholder="e.g. 50%, 300px, 20vw">
81
+ </div>
82
+ <div class="sb-row">
83
+ <label class="sb-lbl">Top</label>
84
+ <input v-model="step.bubbleTop" @input="previewNow()" class="sb-input" placeholder="e.g. 50%, 200px, 30vh">
85
+ </div>
86
+ </template>
87
+ <div class="sb-row">
88
+ <label class="sb-lbl">Align</label>
89
+ <select v-model="step.align" @change="previewNow()" class="sb-input">
90
+ <option value="left">Left</option>
91
+ <option value="center">Center</option>
92
+ <option value="right">Right</option>
93
+ </select>
94
+ </div>
95
+ <!-- Avatar size override -->
96
+ <div class="sb-row">
97
+ <label class="sb-lbl">Avatar Size</label>
98
+ <input v-model="step.avatarSize" @input="applyAvatarSize()" class="sb-input" placeholder="e.g. 200px, 30vh (empty = default)">
99
+ </div>
100
+ <label class="sb-check"><input type="checkbox" v-model="chatMode" @change="previewNow()"> Chat Mode</label>
101
+ <div class="sb-row sb-row--pick">
102
+ <label class="sb-lbl">Target</label>
103
+ <input v-model="step.positionTarget" class="sb-input" placeholder="CSS selector">
104
+ <button class="sb-pick" @click="pick('positionTarget')">Pick</button>
105
+ </div>
106
+ </div>
107
+ </div>
108
+
109
+ <!-- Cursor -->
110
+ <div class="sb-card" :class="{ 'sb-card--open': cards.cursor }">
111
+ <div class="sb-card-head" @click="toggle('cursor')">
112
+ <span>CURSOR</span>
113
+ <span class="sb-chevron">{{ cards.cursor ? '&#9660;' : '&#9654;' }}</span>
114
+ </div>
115
+ <div v-show="cards.cursor" class="sb-card-body">
116
+ <div class="sb-row sb-row--pick">
117
+ <label class="sb-lbl">From</label>
118
+ <input :value="cursorFrom" @input="setCursor('from', $event.target.value)" class="sb-input">
119
+ <button class="sb-pick" @click="pickCursor('from')">Pick</button>
120
+ </div>
121
+ <div class="sb-row sb-row--pick">
122
+ <label class="sb-lbl">To</label>
123
+ <input :value="cursorTo" @input="setCursor('to', $event.target.value)" class="sb-input">
124
+ <button class="sb-pick" @click="pickCursor('to')">Pick</button>
125
+ </div>
126
+ <div class="sb-row">
127
+ <label class="sb-lbl">Duration</label>
128
+ <input type="number" :value="cursorDuration" @input="setCursor('duration', parseFloat($event.target.value) || 1.5)" class="sb-input sb-input--num" step="0.1">
129
+ </div>
130
+ <div class="sb-row">
131
+ <label class="sb-check"><input type="checkbox" :checked="cursorClick" @change="setCursor('click', $event.target.checked)"> Click</label>
132
+ <label class="sb-check"><input type="checkbox" :checked="cursorLoop" @change="setCursor('loop', $event.target.checked)"> Loop</label>
133
+ </div>
134
+ </div>
135
+ </div>
136
+
137
+ <!-- Highlight -->
138
+ <div class="sb-card" :class="{ 'sb-card--open': cards.highlight }">
139
+ <div class="sb-card-head" @click="toggle('highlight')">
140
+ <span>HIGHLIGHT</span>
141
+ <span class="sb-chevron">{{ cards.highlight ? '&#9660;' : '&#9654;' }}</span>
142
+ </div>
143
+ <div v-show="cards.highlight" class="sb-card-body">
144
+ <div class="sb-row sb-row--pick">
145
+ <label class="sb-lbl">Elements</label>
146
+ <input :value="highlightElements" @input="setHighlightElements($event.target.value)" class="sb-input" placeholder="sel1, sel2">
147
+ <button class="sb-pick" @click="pickHighlight">Pick</button>
148
+ </div>
149
+ </div>
150
+ </div>
151
+
152
+ <!-- Hover -->
153
+ <div class="sb-card" :class="{ 'sb-card--open': cards.hover }">
154
+ <div class="sb-card-head" @click="toggle('hover')">
155
+ <span>HOVER</span>
156
+ <span class="sb-chevron">{{ cards.hover ? '&#9660;' : '&#9654;' }}</span>
157
+ </div>
158
+ <div v-show="cards.hover" class="sb-card-body">
159
+ <div class="sb-row sb-row--pick">
160
+ <input v-model="hoverValue" class="sb-input" placeholder="CSS selector">
161
+ <button class="sb-pick" @click="pickField('hover')">Pick</button>
162
+ </div>
163
+ </div>
164
+ </div>
165
+
166
+ <!-- Scroll -->
167
+ <div class="sb-card" :class="{ 'sb-card--open': cards.scroll }">
168
+ <div class="sb-card-head" @click="toggle('scroll')">
169
+ <span>SCROLL</span>
170
+ <span class="sb-chevron">{{ cards.scroll ? '&#9660;' : '&#9654;' }}</span>
171
+ </div>
172
+ <div v-show="cards.scroll" class="sb-card-body">
173
+ <div class="sb-row sb-row--pick">
174
+ <label class="sb-lbl">Scroll To</label>
175
+ <input v-model="step.scrollTo" class="sb-input">
176
+ <button class="sb-pick" @click="pickField('scrollTo')">Pick</button>
177
+ </div>
178
+ </div>
179
+ </div>
180
+
181
+ <!-- Options -->
182
+ <div class="sb-card" :class="{ 'sb-card--open': cards.options }">
183
+ <div class="sb-card-head" @click="toggle('options')">
184
+ <span>OPTIONS</span>
185
+ <span class="sb-chevron">{{ cards.options ? '&#9660;' : '&#9654;' }}</span>
186
+ </div>
187
+ <div v-show="cards.options" class="sb-card-body">
188
+ <div v-for="(opt, oi) in (step.options || [])" :key="oi" class="sb-opt-row">
189
+ <input v-model="opt.label" class="sb-input" placeholder="Label">
190
+ <select v-model="opt._action" @change="setOptionAction(oi, opt._action)" class="sb-input sb-input--sm">
191
+ <option value="next">Next</option>
192
+ <option value="done">Done</option>
193
+ <option value="goToStep">Go To</option>
194
+ <option value="redirect">Redirect</option>
195
+ </select>
196
+ <button class="sb-pick sb-pick--danger" @click="removeOption(oi)">&times;</button>
197
+ </div>
198
+ <button class="sb-btn sb-btn-sm sb-btn-green" @click="addOption">+ Option</button>
199
+ </div>
200
+ </div>
201
+
202
+ <!-- Actions -->
203
+ <div class="sb-card" :class="{ 'sb-card--open': cards.actions }">
204
+ <div class="sb-card-head" @click="toggle('actions')">
205
+ <span>ACTIONS</span>
206
+ <span class="sb-chevron">{{ cards.actions ? '&#9660;' : '&#9654;' }}</span>
207
+ </div>
208
+ <div v-show="cards.actions" class="sb-card-body">
209
+ <div class="sb-row sb-row--pick">
210
+ <label class="sb-lbl">Silent Click</label>
211
+ <input v-model="step.silentClick" class="sb-input">
212
+ <button class="sb-pick" @click="pickField('silentClick')">Pick</button>
213
+ </div>
214
+ <div class="sb-row">
215
+ <label class="sb-lbl">Redirect</label>
216
+ <input v-model="step.redirect" class="sb-input" placeholder="/url">
217
+ </div>
218
+ </div>
219
+ </div>
220
+
221
+ <!-- Advance -->
222
+ <div class="sb-card" :class="{ 'sb-card--open': cards.advance }">
223
+ <div class="sb-card-head" @click="toggle('advance')">
224
+ <span>ADVANCE</span>
225
+ <span class="sb-chevron">{{ cards.advance ? '&#9660;' : '&#9654;' }}</span>
226
+ </div>
227
+ <div v-show="cards.advance" class="sb-card-body">
228
+ <select v-model="advanceMode" @change="previewNow()" class="sb-input">
229
+ <option value="click">Click</option>
230
+ <option value="auto">Auto Next</option>
231
+ <option value="afterAnimation">After Animation</option>
232
+ </select>
233
+ <div v-if="advanceMode === 'auto'" class="sb-row" style="margin-top:8px">
234
+ <label class="sb-lbl">Delay (s)</label>
235
+ <input type="number" v-model.number="autoNextDelay" class="sb-input sb-input--num" step="1" min="1">
236
+ </div>
237
+ </div>
238
+ </div>
239
+
240
+ <!-- Conditions -->
241
+ <div class="sb-card" :class="{ 'sb-card--open': cards.conditions }">
242
+ <div class="sb-card-head" @click="toggle('conditions')">
243
+ <span>CONDITIONS</span>
244
+ <span class="sb-chevron">{{ cards.conditions ? '&#9660;' : '&#9654;' }}</span>
245
+ </div>
246
+ <div v-show="cards.conditions" class="sb-card-body">
247
+ <div class="sb-row">
248
+ <label class="sb-lbl">Show If</label>
249
+ <input v-model="step.showIf" class="sb-input" placeholder="CSS selector">
250
+ </div>
251
+ </div>
252
+ </div>
253
+
254
+ <!-- Output -->
255
+ <div class="sb-card" :class="{ 'sb-card--open': cards.output }">
256
+ <div class="sb-card-head" @click="toggle('output')">
257
+ <span>OUTPUT (JSON)</span>
258
+ <span class="sb-chevron">{{ cards.output ? '&#9660;' : '&#9654;' }}</span>
259
+ </div>
260
+ <div v-show="cards.output" class="sb-card-body">
261
+ <pre class="sb-code">{{ outputJson }}</pre>
262
+ <div class="sb-row" style="margin-top:8px;justify-content:flex-end">
263
+ <button class="sb-btn sb-btn-sm sb-btn-ghost" @click="copyStep">Copy Step</button>
264
+ <button class="sb-btn sb-btn-sm sb-btn-ghost" @click="copyAll">Copy All</button>
265
+ </div>
266
+ </div>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ </template>
271
+
272
+ <script>
273
+ import { pickElement } from '../tutorials/step-builder/utils/element-picker.js';
274
+ import { generateFullJson } from '../tutorials/step-builder/utils/code-generator.js';
275
+
276
+ export default {
277
+ name: 'VlTutorialStepBuilder',
278
+
279
+ data() {
280
+ return {
281
+ visible: false,
282
+ steps: [{ html: '', overlay: true, position: 'left', align: 'center' }],
283
+ idx: 0,
284
+ engine: null,
285
+ cards: {
286
+ content: true, position: false, cursor: false, highlight: false,
287
+ hover: false, scroll: false, options: false, actions: false,
288
+ advance: false, conditions: false, output: false,
289
+ },
290
+ _previewTimer: null,
291
+ _syncing: false,
292
+ };
293
+ },
294
+
295
+ computed: {
296
+ step() {
297
+ return this.steps[this.idx] || {};
298
+ },
299
+
300
+ // --- Cursor helpers (nested object needs explicit reactivity) ---
301
+ cursorFrom() { return this.step.cursor?.from || ''; },
302
+ cursorTo() { return this.step.cursor?.to || ''; },
303
+ cursorDuration() { return this.step.cursor?.duration || 1.5; },
304
+ cursorClick() { return !!this.step.cursor?.click; },
305
+ cursorLoop() { return !!this.step.cursor?.loop; },
306
+
307
+ // --- Highlight helper ---
308
+ highlightElements() {
309
+ return this.step.highlight?.groups?.flatMap(g => g.elements || g)?.join(', ') || '';
310
+ },
311
+
312
+ // --- Hover helper ---
313
+ hoverValue: {
314
+ get() {
315
+ const h = this.step.hover;
316
+ return Array.isArray(h) ? h.join(', ') : (h || '');
317
+ },
318
+ set(val) {
319
+ if (!val) { this.$delete(this.step, 'hover'); return; }
320
+ const parts = val.split(',').map(s => s.trim()).filter(Boolean);
321
+ this.$set(this.step, 'hover', parts.length === 1 ? parts[0] : parts);
322
+ }
323
+ },
324
+
325
+ // --- Chat mode helper ---
326
+ chatMode: {
327
+ get() { return !!this.step.chatMode; },
328
+ set(v) {
329
+ if (v) this.$set(this.step, 'chatMode', true);
330
+ else this.$delete(this.step, 'chatMode');
331
+ }
332
+ },
333
+
334
+ // --- showBack helper ---
335
+ showBack: {
336
+ get() { return !!this.step.showBack; },
337
+ set(v) {
338
+ if (v) this.$set(this.step, 'showBack', true);
339
+ else this.$delete(this.step, 'showBack');
340
+ }
341
+ },
342
+
343
+ // --- Advance helpers ---
344
+ advanceMode: {
345
+ get() {
346
+ if (this.step.autoNext) return 'auto';
347
+ if (this.step.afterAnimation) return 'afterAnimation';
348
+ return 'click';
349
+ },
350
+ set(mode) {
351
+ this.$delete(this.step, 'autoNext');
352
+ this.$delete(this.step, 'afterAnimation');
353
+ if (mode === 'auto') this.$set(this.step, 'autoNext', 3);
354
+ else if (mode === 'afterAnimation') this.$set(this.step, 'afterAnimation', true);
355
+ }
356
+ },
357
+ autoNextDelay: {
358
+ get() { return typeof this.step.autoNext === 'number' ? this.step.autoNext : 3; },
359
+ set(v) { this.$set(this.step, 'autoNext', v || 3); }
360
+ },
361
+
362
+ outputJson() {
363
+ try { return generateFullJson(this.steps, this.engine?.config); }
364
+ catch (e) { return JSON.stringify(this.steps, null, 2); }
365
+ },
366
+ },
367
+
368
+ watch: {
369
+ steps: {
370
+ deep: true,
371
+ handler() { if (!this._syncing) this.schedulePreview(); }
372
+ }
373
+ },
374
+
375
+ mounted() {
376
+ this.engine = window.TutorialEngine;
377
+ if (!this.engine) return;
378
+
379
+ // When engine starts and has steps, sync and show
380
+ this.engine.onReady(() => {
381
+ if (this.engine.steps?.length) {
382
+ this._syncing = true;
383
+ this.steps = this.engine.steps.map(s => JSON.parse(JSON.stringify(s)));
384
+ this.idx = 0;
385
+ this.visible = true;
386
+ this.$nextTick(() => {
387
+ this._syncing = false;
388
+ this.$el.focus();
389
+ });
390
+ }
391
+ });
392
+
393
+ // Listen for step changes from the running tutorial
394
+ document.addEventListener('tutorial-step-change', (e) => {
395
+ const i = e.detail?.index;
396
+ if (typeof i === 'number' && i !== this.idx && i < this.steps.length) {
397
+ this.idx = i;
398
+ }
399
+ });
400
+ },
401
+
402
+ methods: {
403
+ // --- Step management ---
404
+ selectStep(i) {
405
+ this.idx = Math.max(0, Math.min(i, this.steps.length - 1));
406
+ this.previewNow();
407
+ },
408
+ prevStep() { if (this.idx > 0) this.selectStep(this.idx - 1); },
409
+ nextStep() { if (this.idx < this.steps.length - 1) this.selectStep(this.idx + 1); },
410
+
411
+ addStep() {
412
+ this.steps.splice(this.idx + 1, 0, { html: '', overlay: true, position: 'left', align: 'center' });
413
+ this.selectStep(this.idx + 1);
414
+ },
415
+ deleteStep() {
416
+ if (this.steps.length <= 1) return;
417
+ this.steps.splice(this.idx, 1);
418
+ this.selectStep(Math.min(this.idx, this.steps.length - 1));
419
+ },
420
+ duplicateStep() {
421
+ const clone = JSON.parse(JSON.stringify(this.steps[this.idx]));
422
+ this.steps.splice(this.idx + 1, 0, clone);
423
+ this.selectStep(this.idx + 1);
424
+ },
425
+ moveStepUp() {
426
+ if (this.idx <= 0) return;
427
+ const i = this.idx;
428
+ this.steps.splice(i - 1, 2, this.steps[i], this.steps[i - 1]);
429
+ this.selectStep(i - 1);
430
+ },
431
+ moveStepDown() {
432
+ if (this.idx >= this.steps.length - 1) return;
433
+ const i = this.idx;
434
+ this.steps.splice(i, 2, this.steps[i + 1], this.steps[i]);
435
+ this.selectStep(i + 1);
436
+ },
437
+
438
+ stepPreview(s) {
439
+ return (s.html || s.silentClick || '').substring(0, 30) || '(empty)';
440
+ },
441
+
442
+ // --- Card toggles ---
443
+ toggle(card) { this.cards[card] = !this.cards[card]; },
444
+
445
+ // --- Cursor (nested reactivity) ---
446
+ ensureCursor() {
447
+ if (!this.step.cursor) this.$set(this.step, 'cursor', {});
448
+ },
449
+ setCursor(key, val) {
450
+ this.ensureCursor();
451
+ this.$set(this.step.cursor, key, val);
452
+ if (!this.step.cursor.from && !this.step.cursor.to) {
453
+ this.$delete(this.step, 'cursor');
454
+ }
455
+ },
456
+ async pickCursor(key) {
457
+ const sel = await pickElement();
458
+ if (sel) this.setCursor(key, sel);
459
+ },
460
+
461
+ // --- Highlight ---
462
+ setHighlightElements(val) {
463
+ if (!val) { this.$delete(this.step, 'highlight'); return; }
464
+ const elements = val.split(',').map(s => s.trim()).filter(Boolean);
465
+ this.$set(this.step, 'highlight', { groups: [{ elements }] });
466
+ },
467
+ async pickHighlight() {
468
+ const sel = await pickElement();
469
+ if (sel) {
470
+ const current = this.highlightElements;
471
+ this.setHighlightElements(current ? current + ', ' + sel : sel);
472
+ }
473
+ },
474
+
475
+ // --- Avatar size ---
476
+ applyAvatarSize() {
477
+ const size = this.step.avatarSize;
478
+ if (this.engine?.avatar?.img) {
479
+ this.engine.avatar.img.style.height = size || this.engine.config?.get('avatarHeight') || '';
480
+ this.engine.avatar.img.style.maxHeight = size || this.engine.config?.get('avatarMaxHeight') || '';
481
+ }
482
+ this.previewNow();
483
+ },
484
+
485
+ // --- Generic field pick ---
486
+ async pickField(prop) {
487
+ const sel = await pickElement();
488
+ if (sel) this.$set(this.step, prop, sel);
489
+ },
490
+ async pick(prop) {
491
+ const sel = await pickElement();
492
+ if (sel) this.$set(this.step, prop, sel);
493
+ },
494
+
495
+ // --- Options ---
496
+ addOption() {
497
+ if (!this.step.options) this.$set(this.step, 'options', []);
498
+ this.step.options.push({ label: 'New Option', _action: 'next' });
499
+ },
500
+ removeOption(i) {
501
+ this.step.options.splice(i, 1);
502
+ if (!this.step.options.length) this.$delete(this.step, 'options');
503
+ },
504
+ setOptionAction(i, action) {
505
+ const opt = this.step.options[i];
506
+ delete opt.done; delete opt.goToStep; delete opt.redirect;
507
+ if (action === 'done') opt.done = true;
508
+ else if (action === 'goToStep') opt.goToStep = this.idx + 1;
509
+ else if (action === 'redirect') opt.redirect = '/';
510
+ },
511
+
512
+ // --- Preview ---
513
+ schedulePreview() {
514
+ clearTimeout(this._previewTimer);
515
+ this._previewTimer = setTimeout(() => this.previewNow(), 300);
516
+ },
517
+ previewNow() {
518
+ if (!this.engine) return;
519
+ this.engine.steps = this.steps;
520
+ this.engine.previewStep(this.idx);
521
+ },
522
+
523
+ // --- Clipboard ---
524
+ copyAll() { this._copy(this.outputJson); },
525
+ copyStep() { this._copy(JSON.stringify(this.steps[this.idx], null, 2)); },
526
+ _copy(text) {
527
+ navigator.clipboard.writeText(text).catch(() => {
528
+ const ta = document.createElement('textarea');
529
+ ta.value = text;
530
+ ta.style.cssText = 'position:fixed;left:-9999px';
531
+ document.body.appendChild(ta);
532
+ ta.select();
533
+ document.execCommand('copy');
534
+ ta.remove();
535
+ });
536
+ },
537
+
538
+ // --- Spectator ---
539
+ spectatorMode() {
540
+ this.visible = false;
541
+ if (this.engine) { this.engine.steps = this.steps; this.engine.previewStep(0); }
542
+
543
+ const btn = document.createElement('button');
544
+ btn.textContent = 'Exit Spectator (Esc)';
545
+ Object.assign(btn.style, {
546
+ position: 'fixed', top: '16px', right: '16px', zIndex: '100001',
547
+ background: 'rgba(0,0,0,0.7)', color: '#fff', border: '1px solid rgba(255,255,255,0.2)',
548
+ borderRadius: '8px', padding: '8px 16px', fontSize: '13px', fontWeight: '600',
549
+ cursor: 'pointer', backdropFilter: 'blur(8px)',
550
+ });
551
+ document.body.appendChild(btn);
552
+
553
+ const exit = () => { this.visible = true; btn.remove(); document.removeEventListener('keydown', esc); this.previewNow(); };
554
+ const esc = (e) => { if (e.key === 'Escape') exit(); };
555
+ btn.onclick = exit;
556
+ document.addEventListener('keydown', esc);
557
+ },
558
+
559
+ // --- Resize ---
560
+ startResize(e) {
561
+ e.preventDefault();
562
+ const startX = e.clientX;
563
+ const startW = this.$el.offsetWidth;
564
+ const onMove = (ev) => { this.$el.style.width = Math.max(300, startW - (ev.clientX - startX)) + 'px'; };
565
+ const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); };
566
+ document.addEventListener('mousemove', onMove);
567
+ document.addEventListener('mouseup', onUp);
568
+ },
569
+ },
570
+ };
571
+ </script>
572
+
573
+ <style scoped>
574
+ .sb {
575
+ position: fixed;
576
+ top: 0;
577
+ right: 0;
578
+ width: 420px;
579
+ height: 100vh;
580
+ z-index: 99997;
581
+ background: linear-gradient(170deg, #1e1e2e 0%, #161625 100%);
582
+ color: #e0e4ec;
583
+ font-family: system-ui, -apple-system, sans-serif;
584
+ font-size: 13px;
585
+ line-height: 1.5;
586
+ display: flex;
587
+ flex-direction: column;
588
+ border-left: 1px solid rgba(255,255,255,0.06);
589
+ box-shadow: -4px 0 24px rgba(0,0,0,0.4);
590
+ outline: none;
591
+ }
592
+ .sb * { box-sizing: border-box; }
593
+
594
+ /* Resize handle */
595
+ .sb-resize { position: absolute; width: 6px; top: 0; bottom: 0; left: 0; cursor: ew-resize; z-index: 2; }
596
+ .sb-resize:hover { background: rgba(108,138,255,0.2); }
597
+
598
+ /* Header */
599
+ .sb-header { height: 44px; min-height: 44px; padding: 0 14px; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid rgba(255,255,255,0.06); background: rgba(255,255,255,0.02); }
600
+ .sb-header-title { font-size: 14px; font-weight: 700; flex: 1; }
601
+ .sb-hbtn { background: none; border: 1px solid rgba(255,255,255,0.1); color: #aaa; border-radius: 6px; width: 28px; height: 28px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; transition: all 0.15s; }
602
+ .sb-hbtn:hover { background: rgba(255,255,255,0.08); color: #fff; }
603
+ .sb-hbtn--danger:hover { color: #e74c3c; border-color: #e74c3c; }
604
+
605
+ /* Steps */
606
+ .sb-steps { border-bottom: 1px solid rgba(255,255,255,0.06); padding: 8px 14px; flex-shrink: 0; }
607
+ .sb-steps-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
608
+ .sb-steps-count { font-size: 11px; font-weight: 600; color: #7a7f8e; text-transform: uppercase; letter-spacing: 0.3px; }
609
+ .sb-steps-list { max-height: 200px; overflow-y: auto; }
610
+ .sb-step { display: flex; align-items: center; gap: 6px; padding: 5px 8px; border-radius: 6px; cursor: pointer; transition: background 0.15s; border: 1px solid transparent; }
611
+ .sb-step:hover { background: rgba(108,138,255,0.1); }
612
+ .sb-step--active { background: rgba(108,138,255,0.2); border-color: rgba(108,138,255,0.3); }
613
+ .sb-step-num { width: 22px; height: 22px; border-radius: 50%; background: rgba(255,255,255,0.08); display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 700; flex-shrink: 0; }
614
+ .sb-step--active .sb-step-num { background: #6c8aff; color: #fff; }
615
+ .sb-step-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #b0b4c0; font-size: 12px; }
616
+ .sb-step-icons { display: flex; gap: 3px; font-size: 11px; color: #7a7f8e; flex-shrink: 0; }
617
+ .sb-step-actions { display: flex; gap: 4px; margin-top: 6px; }
618
+
619
+ /* Detail zone */
620
+ .sb-detail { flex: 1; overflow-y: auto; padding: 8px 14px; }
621
+ .sb-detail::-webkit-scrollbar { width: 5px; }
622
+ .sb-detail::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
623
+
624
+ /* Cards */
625
+ .sb-card { background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.06); border-radius: 8px; margin-bottom: 6px; overflow: hidden; }
626
+ .sb-card--open { background: rgba(255,255,255,0.04); }
627
+ .sb-card-head { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; cursor: pointer; user-select: none; font-size: 11px; font-weight: 600; color: #6c8aff; text-transform: uppercase; letter-spacing: 0.3px; }
628
+ .sb-card-head:hover { background: rgba(255,255,255,0.03); }
629
+ .sb-chevron { font-size: 10px; color: #555; }
630
+ .sb-card-body { padding: 8px 12px 12px; }
631
+
632
+ /* Inputs */
633
+ .sb-input { background: rgba(255,255,255,0.06); color: #e0e4ec; border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; padding: 7px 10px; font-size: 13px; font-family: inherit; outline: none; width: 100%; transition: border-color 0.2s; }
634
+ .sb-input:focus { border-color: #6c8aff; box-shadow: 0 0 0 2px rgba(108,138,255,0.15); }
635
+ .sb-input--num { width: 72px; }
636
+ .sb-input--sm { width: auto; min-width: 80px; }
637
+ .sb-textarea { resize: vertical; min-height: 60px; font-family: "Fira Code", monospace; font-size: 12px; }
638
+ select.sb-input { cursor: pointer; appearance: none; padding-right: 24px; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%23888'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; }
639
+
640
+ /* Labels, rows */
641
+ .sb-lbl { font-size: 11px; color: #7a7f8e; min-width: 50px; flex-shrink: 0; text-transform: uppercase; letter-spacing: 0.3px; margin-bottom: 4px; display: block; }
642
+ .sb-row { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; }
643
+ .sb-row--pick .sb-input { flex: 1; }
644
+ .sb-check { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: #b0b4c0; cursor: pointer; margin-right: 12px; }
645
+ .sb-check input[type="checkbox"] { width: 16px; height: 16px; accent-color: #6c8aff; cursor: pointer; }
646
+
647
+ /* Pick button */
648
+ .sb-pick { background: rgba(108,138,255,0.15); color: #6c8aff; border: 1px solid rgba(108,138,255,0.3); border-radius: 6px; padding: 6px 10px; font-size: 11px; font-weight: 600; cursor: pointer; white-space: nowrap; transition: all 0.15s; }
649
+ .sb-pick:hover { background: rgba(108,138,255,0.25); }
650
+ .sb-pick--danger { background: rgba(231,76,60,0.15); color: #e74c3c; border-color: rgba(231,76,60,0.3); }
651
+ .sb-pick--danger:hover { background: rgba(231,76,60,0.25); }
652
+
653
+ /* Buttons */
654
+ .sb-btn { border: none; border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 600; padding: 6px 12px; transition: all 0.15s; font-family: inherit; white-space: nowrap; }
655
+ .sb-btn:hover { filter: brightness(1.15); }
656
+ .sb-btn-sm { padding: 4px 8px; font-size: 11px; }
657
+ .sb-btn-green { background: #2ecc71; color: #fff; }
658
+ .sb-btn-ghost { background: rgba(255,255,255,0.06); color: #ccc; border: 1px solid rgba(255,255,255,0.1); }
659
+ .sb-btn-ghost:hover { background: rgba(255,255,255,0.12); }
660
+
661
+ /* Options row */
662
+ .sb-opt-row { display: flex; gap: 6px; align-items: center; padding: 6px 0; border-bottom: 1px solid rgba(255,255,255,0.04); }
663
+ .sb-opt-row .sb-input { flex: 1; }
664
+
665
+ /* Code output */
666
+ .sb-code { background: rgba(0,0,0,0.3); color: #8bc78b; padding: 10px 12px; border-radius: 6px; font-family: "Fira Code", monospace; font-size: 10px; line-height: 1.6; overflow: auto; max-height: 200px; white-space: pre-wrap; border: 1px solid rgba(255,255,255,0.04); }
667
+
668
+ @media (max-width: 600px) {
669
+ .sb { width: 100vw; }
670
+ }
671
+ </style>