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.
- package/README.md +1 -0
- package/app.js +22 -0
- package/components/VlApexChart.vue +89 -0
- package/components/VlCKEditorTemplate.vue +52 -0
- package/components/VlCKEditorWithVars.vue +80 -0
- package/components/VlCollapsible.vue +91 -0
- package/components/VlCopyBox.vue +61 -0
- package/components/VlCreditCardInput.vue +104 -0
- package/components/VlDateMonthDay.vue +327 -0
- package/components/VlDateTextInput.vue +151 -0
- package/components/VlInternationalPhoneInput.vue +902 -0
- package/components/VlPartialDate.vue +283 -0
- package/components/VlPasswordInput.vue +358 -0
- package/components/VlQrCodeScanner.vue +91 -0
- package/components/VlSwipeableTab.vue +35 -0
- package/components/VlSwipeableTabs.vue +325 -0
- package/components/VlText.vue +72 -0
- package/components/VlTutorialStepBuilder.vue +671 -0
- package/components/VlValidatedInput.vue +262 -0
- package/components/data/countries.js +253 -0
- package/components/getAllDefaultComponents.js +12 -0
- package/docs/architecture.md +71 -0
- package/docs/tutorials.md +247 -0
- package/docs/usage.md +128 -0
- package/functions/index.js +7 -0
- package/functions/inputs.js +64 -0
- package/functions/loading.js +20 -0
- package/kompo-ckeditor-1.0.17/index.js +5 -0
- package/kompo-ckeditor-1.0.17/komponents/CKEditor.vue +36 -0
- package/kompo-ckeditor-1.0.17/komponents/CKEditorExtended.vue +66 -0
- package/kompo-ckeditor-1.0.17/komponents/EditableCKEditor.vue +42 -0
- package/kompo-ckeditor-1.0.17/komponents/TranslatableEditor.vue +28 -0
- package/kompo-ckeditor-1.0.17/mixins/CKEditor.js +291 -0
- package/kompo-ckeditor-1.0.17/mixins/CKEditorUtilities.js +52 -0
- package/kompo-ckeditor-1.0.17/mixins/CkUploadAdapter.js +95 -0
- package/kompo-ckeditor-1.0.17/mixins/translations/en.js +1 -0
- package/kompo-ckeditor-1.0.17/mixins/translations/fr.js +1 -0
- package/kompo-ckeditor-1.0.17/plugins/JsonBlockCommand.js +100 -0
- package/kompo-ckeditor-1.0.17/plugins/JsonBlockEditing.js +225 -0
- package/kompo-ckeditor-1.0.17/plugins/JsonBlockUi.js +21 -0
- package/kompo-ckeditor-1.0.17/plugins/MentionPlugin.js +118 -0
- package/orchestrator/KompoJsBuilder.js +333 -0
- package/package.json +25 -0
- package/raw_animations/count-up-animation.js +220 -0
- package/raw_animations/magic-poof.js +210 -0
- package/tutorials/TutorialConfig.js +34 -0
- package/tutorials/TutorialEngine.js +448 -0
- package/tutorials/index.js +2 -0
- package/tutorials/modules/AutoNext.js +86 -0
- package/tutorials/modules/Avatar.js +94 -0
- package/tutorials/modules/Bubble.js +617 -0
- package/tutorials/modules/Cursor.js +315 -0
- package/tutorials/modules/Highlight.js +282 -0
- package/tutorials/modules/HoverForcer.js +250 -0
- package/tutorials/modules/Mobile.js +148 -0
- package/tutorials/modules/Options.js +98 -0
- package/tutorials/modules/Overlay.js +142 -0
- package/tutorials/modules/Scroll.js +37 -0
- package/tutorials/modules/Typewriter.js +132 -0
- package/tutorials/step-builder/step-builder-bridge.js +436 -0
- package/tutorials/step-builder/utils/bezier-editor.js +379 -0
- package/tutorials/step-builder/utils/code-generator.js +127 -0
- package/tutorials/step-builder/utils/element-picker.js +125 -0
- package/tutorials/tutorial.scss +816 -0
- package/tutorials/utils/selectors.js +225 -0
- package/tutorials/utils/stability.js +76 -0
- 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">▶</button>
|
|
9
|
+
<button class="sb-hbtn" title="Copy All" @click="copyAll">📋</button>
|
|
10
|
+
<button class="sb-hbtn" title="Close" @click="visible = false">×</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">⌖</span>
|
|
27
|
+
<span v-if="s.highlight" title="highlight">☆</span>
|
|
28
|
+
<span v-if="s.options" title="options">…</span>
|
|
29
|
+
</span>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="sb-step-actions">
|
|
33
|
+
<button class="sb-hbtn" @click="moveStepUp" title="Move up">↑</button>
|
|
34
|
+
<button class="sb-hbtn" @click="moveStepDown" title="Move down">↓</button>
|
|
35
|
+
<button class="sb-hbtn" @click="duplicateStep" title="Duplicate">⋔</button>
|
|
36
|
+
<button class="sb-hbtn sb-hbtn--danger" @click="deleteStep" title="Delete">✕</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 ? '▼' : '▶' }}</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 ? '▼' : '▶' }}</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 ? '▼' : '▶' }}</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 ? '▼' : '▶' }}</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 ? '▼' : '▶' }}</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 ? '▼' : '▶' }}</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 ? '▼' : '▶' }}</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)">×</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 ? '▼' : '▶' }}</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 ? '▼' : '▶' }}</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 ? '▼' : '▶' }}</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 ? '▼' : '▶' }}</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>
|