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