js-kompo-utils 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/components/VlDropdown.vue +8 -7
  2. package/components/VlTutorialStepBuilder.vue +137 -586
  3. package/components/step-builder/SbContextMenu.vue +41 -0
  4. package/components/step-builder/SbGraphView.vue +93 -0
  5. package/components/step-builder/SbHeader.vue +16 -0
  6. package/components/step-builder/SbHistoryDrawer.vue +43 -0
  7. package/components/step-builder/SbPathPreview.vue +57 -0
  8. package/components/step-builder/SbStepList.vue +87 -0
  9. package/components/step-builder/SbTimeline.vue +66 -0
  10. package/components/step-builder/cards/SbActionsCard.vue +26 -0
  11. package/components/step-builder/cards/SbAdvanceCard.vue +43 -0
  12. package/components/step-builder/cards/SbConditionsCard.vue +20 -0
  13. package/components/step-builder/cards/SbContentCard.vue +45 -0
  14. package/components/step-builder/cards/SbCursorCard.vue +102 -0
  15. package/components/step-builder/cards/SbGlobalOptsCard.vue +40 -0
  16. package/components/step-builder/cards/SbHighlightCard.vue +40 -0
  17. package/components/step-builder/cards/SbHoverCard.vue +32 -0
  18. package/components/step-builder/cards/SbOptionsCard.vue +50 -0
  19. package/components/step-builder/cards/SbOutputCard.vue +17 -0
  20. package/components/step-builder/cards/SbPositionCard.vue +87 -0
  21. package/components/step-builder/cards/SbScrollCard.vue +17 -0
  22. package/docs/extending-tutorial-system.md +376 -0
  23. package/package.json +10 -9
  24. package/tutorials/TutorialConfig.js +4 -0
  25. package/tutorials/TutorialEngine.js +49 -3
  26. package/tutorials/extensions/HideIfExtension.js +12 -0
  27. package/tutorials/extensions/LinkNextExtension.js +117 -0
  28. package/tutorials/modules/Cursor.js +24 -4
  29. package/tutorials/modules/Highlight.js +7 -5
  30. package/tutorials/modules/HoverForcer.js +1 -0
  31. package/tutorials/modules/Overlay.js +2 -0
  32. package/tutorials/step-builder/StepBuilderState.js +265 -0
  33. package/tutorials/tutorial.scss +186 -0
  34. package/tutorials/step-builder/step-builder-bridge.js +0 -436
@@ -0,0 +1,376 @@
1
+ # Extending the Tutorial System
2
+
3
+ Guide for adding new features to the Tutorial Engine and Step Builder.
4
+
5
+ ---
6
+
7
+ ## Part 1: Adding a New Engine Extension
8
+
9
+ Engine extensions are self-contained plugins that hook into the tutorial lifecycle without modifying `TutorialEngine.js`.
10
+
11
+ ### Step 1: Create the Extension File
12
+
13
+ Create a file in `tutorials/extensions/YourExtension.js`:
14
+
15
+ ```js
16
+ export const YourExtension = {
17
+ name: 'yourFeature',
18
+
19
+ // Called once when engine.use() registers the plugin
20
+ install(engine) {
21
+ // Store engine ref if needed
22
+ this.engine = engine;
23
+ },
24
+
25
+ // Called BEFORE each step renders. Return false to skip the step.
26
+ beforeStep(step, index, engine) {
27
+ if (!step.yourProperty) return; // no-op if feature not used
28
+ // Your logic here
29
+ // return false; // to skip step
30
+ },
31
+
32
+ // Called AFTER the typewriter finishes typing
33
+ afterTyping(step, isLast, engine) {
34
+ // Good place to add interactive elements, start observers, etc.
35
+ },
36
+
37
+ // Called when the tutorial DOM is first built
38
+ onBuildDOM(engine) {},
39
+
40
+ // Called when animations/state are cleaned up between steps
41
+ onCleanup(engine) {
42
+ // IMPORTANT: clean up any DOM, listeners, or observers here
43
+ },
44
+
45
+ // Called when the tutorial closes
46
+ onClose(engine) {
47
+ // Final cleanup
48
+ },
49
+ };
50
+ ```
51
+
52
+ ### Step 2: Register the Extension
53
+
54
+ In `tutorials/TutorialEngine.js`, add the import at the top and register in the constructor:
55
+
56
+ ```js
57
+ import { YourExtension } from './extensions/YourExtension.js';
58
+
59
+ // In constructor, after the existing .use() calls:
60
+ this.use(YourExtension);
61
+ ```
62
+
63
+ ### Step 3: Add Config Defaults (if needed)
64
+
65
+ If your feature needs configurable options, add defaults to `tutorials/TutorialConfig.js`:
66
+
67
+ ```js
68
+ static DEFAULTS = {
69
+ // ... existing defaults ...
70
+ yourFeatureColor: '#ff0000',
71
+ yourFeatureEnabled: true,
72
+ };
73
+ ```
74
+
75
+ Read them in your extension via `engine.config.get('yourFeatureColor')`.
76
+
77
+ ### Available Hooks
78
+
79
+ | Hook | Arguments | Return | When |
80
+ |------|-----------|--------|------|
81
+ | `install` | `(engine)` | — | Once on `engine.use()` |
82
+ | `beforeStep` | `(step, index, engine)` | `false` to skip | Before step renders |
83
+ | `afterTyping` | `(step, isLast, engine)` | — | After typewriter finishes |
84
+ | `onBuildDOM` | `(engine)` | — | After initial DOM construction |
85
+ | `onCleanup` | `(engine)` | — | Between step transitions |
86
+ | `onClose` | `(engine)` | — | Tutorial closes |
87
+
88
+ ### Example: The HideIf Extension
89
+
90
+ ```js
91
+ // tutorials/extensions/HideIfExtension.js
92
+ export const HideIfExtension = {
93
+ name: 'hideIf',
94
+ beforeStep(step, index, engine) {
95
+ if (!step.hideIf) return;
96
+ if (engine.config.get('devMode')) return;
97
+ if (document.querySelector(step.hideIf)) return false;
98
+ },
99
+ };
100
+ ```
101
+
102
+ Registered in TutorialEngine constructor: `this.use(HideIfExtension);`
103
+
104
+ ---
105
+
106
+ ## Part 2: Adding a New Step Builder Section (Card)
107
+
108
+ Step builder cards are child Vue components that edit step properties.
109
+
110
+ ### Step 1: Create the Card Component
111
+
112
+ Create a file in `components/step-builder/cards/SbYourCard.vue`:
113
+
114
+ ```vue
115
+ <template>
116
+ <div class="sb-card" :class="{ 'sb-card--open': sb.cards.yourCard }">
117
+ <div class="sb-card-head" @click="sb.toggle('yourCard')">
118
+ <span>YOUR CARD</span>
119
+ <span class="sb-chevron">{{ sb.cards.yourCard ? '&#9660;' : '&#9654;' }}</span>
120
+ </div>
121
+ <div v-show="sb.cards.yourCard" class="sb-card-body">
122
+ <!-- Your card content here -->
123
+ <div class="sb-row">
124
+ <label class="sb-lbl">Your Field</label>
125
+ <input :value="sb.step.yourField || ''"
126
+ @input="sb.setStepProp('yourField', $event.target.value)"
127
+ class="sb-input"
128
+ placeholder="value">
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </template>
133
+ <script>
134
+ export default {
135
+ inject: ['sb'],
136
+ methods: {
137
+ // Card-specific methods go here — NOT in the parent
138
+ // Access parent: this.sb.state, this.sb.step, this.sb.engine
139
+ // Trigger preview: this.sb.previewNow()
140
+ // Set step property: this.sb.setStepProp(key, value)
141
+ // Pick element: this.sb.pickField('yourField')
142
+ // Vue reactivity: this.$set(this.sb.step, 'key', value)
143
+ // this.$delete(this.sb.step, 'key')
144
+ },
145
+ };
146
+ </script>
147
+ ```
148
+
149
+ ### Step 2: Register in the Parent
150
+
151
+ In `components/VlTutorialStepBuilder.vue`:
152
+
153
+ 1. **Import:**
154
+ ```js
155
+ import SbYourCard from './step-builder/cards/SbYourCard.vue';
156
+ ```
157
+
158
+ 2. **Register:**
159
+ ```js
160
+ components: {
161
+ // ... existing components ...
162
+ SbYourCard,
163
+ },
164
+ ```
165
+
166
+ 3. **Add to cards data:**
167
+ ```js
168
+ cards: {
169
+ // ... existing cards ...
170
+ yourCard: false, // false = collapsed by default
171
+ },
172
+ ```
173
+
174
+ 4. **Add to template** (inside the `.sb-detail` div):
175
+ ```html
176
+ <sb-your-card />
177
+ ```
178
+
179
+ ### Step 3: Add Styles (if needed)
180
+
181
+ Append any new CSS classes to `tutorials/tutorial.scss`. All styles are global with `sb-` prefix.
182
+
183
+ ```scss
184
+ // ─── Your Card ───
185
+ .sb-your-special-input { /* ... */ }
186
+ ```
187
+
188
+ Do NOT add `<style>` sections inside Vue components.
189
+
190
+ ### Communication Pattern
191
+
192
+ All children access the parent via `inject: ['sb']`:
193
+
194
+ | Access | Code |
195
+ |--------|------|
196
+ | Current step object | `this.sb.step` |
197
+ | All steps array | `this.sb.steps` |
198
+ | Current index | `this.sb.idx` |
199
+ | State manager (undo/redo) | `this.sb.state` |
200
+ | Engine instance | `this.sb.engine` |
201
+ | Engine config | `this.sb.engine.config.get('key')` |
202
+ | Trigger preview | `this.sb.previewNow()` |
203
+ | Set step property | `this.sb.setStepProp('key', 'value')` |
204
+ | Pick element | `this.sb.pickField('propName')` |
205
+ | Copy to clipboard | `this.sb._copy(text)` |
206
+ | Toggle card | `this.sb.toggle('cardName')` |
207
+ | Card open state | `this.sb.cards.cardName` |
208
+
209
+ ### Vue 2 Reactivity Rules
210
+
211
+ When adding new properties to the step object, you MUST use `$set`/`$delete`:
212
+
213
+ ```js
214
+ // Adding a new property to a step
215
+ this.$set(this.sb.step, 'newProperty', value);
216
+
217
+ // Removing a property
218
+ this.$delete(this.sb.step, 'newProperty');
219
+
220
+ // Modifying existing properties is fine
221
+ this.sb.step.html = 'new value';
222
+
223
+ // Nested objects need $set too
224
+ this.$set(this.sb.step, 'nested', {});
225
+ this.$set(this.sb.step.nested, 'child', value);
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Part 3: Adding a Structural Component (Non-Card)
231
+
232
+ For components that aren't cards (like timeline, graph view, history drawer):
233
+
234
+ ### Step 1: Create the Component
235
+
236
+ Create in `components/step-builder/SbYourComponent.vue`:
237
+
238
+ ```vue
239
+ <template>
240
+ <div class="sb-your-component">
241
+ <!-- your template -->
242
+ </div>
243
+ </template>
244
+ <script>
245
+ export default {
246
+ inject: ['sb'],
247
+ // Your data, computed, methods
248
+ };
249
+ </script>
250
+ ```
251
+
252
+ ### Step 2: Register and Place in Parent
253
+
254
+ Same import/register process as cards, but place the tag outside `.sb-detail`:
255
+
256
+ ```html
257
+ <template>
258
+ <div v-show="visible" class="sb" ...>
259
+ <div class="sb-resize" ...></div>
260
+ <sb-history-drawer /> <!-- structural: before header -->
261
+ <sb-header />
262
+ <sb-step-list />
263
+ <sb-context-menu />
264
+ <div class="sb-detail">
265
+ <!-- cards go here -->
266
+ </div>
267
+ <sb-timeline /> <!-- structural: after detail -->
268
+ <sb-your-component /> <!-- structural: place where it makes sense -->
269
+ <sb-path-preview />
270
+ <sb-graph-view />
271
+ </div>
272
+ </template>
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Part 4: Adding a New Step Property (End-to-End)
278
+
279
+ When a new feature needs both engine handling and builder editing:
280
+
281
+ ### 1. Engine side
282
+
283
+ If the property needs runtime behavior, create an extension (Part 1).
284
+ If it's just a config value, add to TutorialConfig defaults (Part 1, Step 3).
285
+
286
+ ### 2. Step Builder side
287
+
288
+ Add a field in the appropriate existing card, or create a new card (Part 2).
289
+
290
+ ### 3. Code generator
291
+
292
+ If the new property should appear in JSON export, check `tutorials/step-builder/utils/code-generator.js`. The `generateStepJson()` function strips default values — add your property to the clean-up logic if needed.
293
+
294
+ ### 4. Update step schema
295
+
296
+ Document the new property in the step object schema (see `docs/plans/` for the current schema).
297
+
298
+ ---
299
+
300
+ ## File Structure Reference
301
+
302
+ ```
303
+ tutorials/
304
+ ├── TutorialEngine.js # Core orchestrator + plugin system
305
+ ├── TutorialConfig.js # Config defaults
306
+ ├── tutorial.scss # ALL styles (engine + builder)
307
+ ├── index.js # Exports
308
+ │
309
+ ├── modules/ # Engine modules (one per feature)
310
+ │ ├── Overlay.js
311
+ │ ├── Highlight.js
312
+ │ ├── Bubble.js
313
+ │ ├── Avatar.js
314
+ │ ├── Cursor.js
315
+ │ ├── Typewriter.js
316
+ │ ├── HoverForcer.js
317
+ │ ├── Scroll.js
318
+ │ ├── AutoNext.js
319
+ │ ├── Options.js
320
+ │ └── Mobile.js
321
+ │
322
+ ├── extensions/ # Engine plugins (one per feature)
323
+ │ ├── HideIfExtension.js
324
+ │ └── LinkNextExtension.js
325
+ │
326
+ ├── step-builder/
327
+ │ ├── StepBuilderState.js # State management (undo/redo, CRUD)
328
+ │ ├── step-builder-bridge.js # Kompo form bridge (legacy)
329
+ │ └── utils/
330
+ │ ├── code-generator.js
331
+ │ ├── element-picker.js
332
+ │ └── bezier-editor.js
333
+ │
334
+ └── utils/
335
+ ├── selectors.js
336
+ ├── svg.js
337
+ └── stability.js
338
+
339
+ components/
340
+ ├── VlTutorialStepBuilder.vue # Parent shell (state, shortcuts, provide)
341
+ └── step-builder/
342
+ ├── SbHeader.vue
343
+ ├── SbStepList.vue
344
+ ├── SbHistoryDrawer.vue
345
+ ├── SbContextMenu.vue
346
+ ├── SbTimeline.vue
347
+ ├── SbPathPreview.vue
348
+ ├── SbGraphView.vue
349
+ └── cards/
350
+ ├── SbContentCard.vue
351
+ ├── SbPositionCard.vue
352
+ ├── SbCursorCard.vue
353
+ ├── SbHighlightCard.vue
354
+ ├── SbHoverCard.vue
355
+ ├── SbScrollCard.vue
356
+ ├── SbOptionsCard.vue
357
+ ├── SbActionsCard.vue
358
+ ├── SbAdvanceCard.vue
359
+ ├── SbConditionsCard.vue
360
+ ├── SbOutputCard.vue
361
+ └── SbGlobalOptsCard.vue
362
+ ```
363
+
364
+ ---
365
+
366
+ ## Checklist: New Feature
367
+
368
+ - [ ] Engine extension created in `tutorials/extensions/`
369
+ - [ ] Extension registered in `TutorialEngine.js` constructor
370
+ - [ ] Config defaults added to `TutorialConfig.js` (if needed)
371
+ - [ ] Builder card/component created in `components/step-builder/`
372
+ - [ ] Component imported and registered in parent `VlTutorialStepBuilder.vue`
373
+ - [ ] Card key added to `cards` data in parent
374
+ - [ ] Component tag added to parent template
375
+ - [ ] Styles appended to `tutorials/tutorial.scss`
376
+ - [ ] Code generator updated (if new step property)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-kompo-utils",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "",
5
5
  "main": "app.js",
6
6
  "scripts": {
@@ -16,6 +16,7 @@
16
16
  "ckeditor5": "^44.3.0",
17
17
  "gsap": "^3.14.2",
18
18
  "highlight.js": "^11.11.1",
19
+ "html2canvas": "^1.4.1",
19
20
  "jquery": "^3.6.0",
20
21
  "kompo-googlemaps": "^1.0.14",
21
22
  "laravel-echo": "^2.3.1",
@@ -26,13 +27,13 @@
26
27
  "vue-qrcode-reader": "^3.1.0"
27
28
  },
28
29
  "devDependencies": {
29
- "autoprefixer": "^10.4.7",
30
- "axios": "^0.21",
31
- "laravel-mix": "^6.0.49",
32
- "lodash": "^4.17.19",
33
- "postcss": "^8.4.14",
34
- "sass": "^1.83.0",
35
- "tailwindcss": "^3.1.6",
36
- "vue-kompo": "^4.0.3"
30
+ "autoprefixer": "^10.4.7",
31
+ "axios": "^0.21",
32
+ "laravel-mix": "^6.0.49",
33
+ "lodash": "^4.17.19",
34
+ "postcss": "^8.4.14",
35
+ "sass": "^1.83.0",
36
+ "tailwindcss": "^3.1.6",
37
+ "vue-kompo": "^4.0.3"
37
38
  }
38
39
  }
@@ -16,6 +16,10 @@ export class TutorialConfig {
16
16
  bubbleMaxWidth: 'clamp(260px, 85vw, 550px)',
17
17
  bubbleMinWidth: 'clamp(200px, 70vw, 350px)',
18
18
  devMode: false,
19
+ highlightColor: '#ffd700',
20
+ highlightBorderRadius: 8,
21
+ mouthPosition: 0.39,
22
+ pulseColor: null, // null = auto (red in normal mode, blue in devMode)
19
23
  };
20
24
 
21
25
  constructor(userOptions = {}) {
@@ -10,6 +10,8 @@ import { Scroll } from './modules/Scroll.js';
10
10
  import { Options } from './modules/Options.js';
11
11
  import { AutoNext } from './modules/AutoNext.js';
12
12
  import { Mobile } from './modules/Mobile.js';
13
+ import { HideIfExtension } from './extensions/HideIfExtension.js';
14
+ import { LinkNextExtension } from './extensions/LinkNextExtension.js';
13
15
 
14
16
 
15
17
  const STEP_TRANSITION_MS = 300;
@@ -46,7 +48,27 @@ export class TutorialEngine {
46
48
  this._running = false;
47
49
  this._startAborted = false;
48
50
  this._startGeneration = 0;
51
+ this._plugins = [];
52
+ this.use(HideIfExtension);
53
+ this.use(LinkNextExtension);
49
54
  this._hasActiveHighlight = false;
55
+ this._stepTimer = null;
56
+ }
57
+
58
+ use(plugin) {
59
+ this._plugins.push(plugin);
60
+ if (plugin.install) plugin.install(this);
61
+ return this;
62
+ }
63
+
64
+ _hook(name, ...args) {
65
+ for (const p of this._plugins) {
66
+ if (typeof p[name] === 'function') {
67
+ const result = p[name](...args);
68
+ if (result === false) return false;
69
+ }
70
+ }
71
+ return true;
50
72
  }
51
73
 
52
74
  static async preload(tutorialName) {
@@ -164,6 +186,8 @@ export class TutorialEngine {
164
186
 
165
187
  this._wireButtons();
166
188
 
189
+ this._hook('onBuildDOM', this);
190
+
167
191
  this._orientationHandler = () => {
168
192
  setTimeout(() => {
169
193
  const step = this.steps[this.currentStep];
@@ -228,6 +252,12 @@ export class TutorialEngine {
228
252
  if (!step) return;
229
253
  this.currentStep = index;
230
254
 
255
+ if (this._hook('beforeStep', step, index, this) === false) {
256
+ const nextIdx = index + this._lastDirection;
257
+ if (nextIdx >= 0 && nextIdx < this.steps.length) this._showStep(nextIdx);
258
+ return;
259
+ }
260
+
231
261
  const isLast = index === this.steps.length - 1;
232
262
  const devMode = this.config.get('devMode');
233
263
 
@@ -259,9 +289,16 @@ export class TutorialEngine {
259
289
  if (!devMode) {
260
290
  const silentEl = document.querySelector(step.silentClick);
261
291
  if (silentEl && typeof silentEl.click === 'function') silentEl.click();
292
+ if (!isLast) this._showStep(this.currentStep + 1);
293
+ return;
262
294
  }
263
- if (!isLast) this._showStep(this.currentStep + 1);
264
- return;
295
+ // devMode: inject a visible label so the bubble isn't empty,
296
+ // and neutralise the silentClick for this render pass so the
297
+ // rest of _showStep treats it as a normal step.
298
+ step = Object.assign({}, step, {
299
+ html: step.html || '<em style="color:#f1c40f">[silentClick: ' + step.silentClick + ']</em>',
300
+ silentClick: null,
301
+ });
265
302
  }
266
303
  this._isAutoAdvancing = false;
267
304
 
@@ -327,7 +364,7 @@ export class TutorialEngine {
327
364
  this.highlight.create(step.highlight);
328
365
  }
329
366
 
330
- setTimeout(() => {
367
+ this._stepTimer = setTimeout(() => {
331
368
  const twSpeed = devMode ? 0 : this.config.get('typewriteSpeed');
332
369
  this.typewriter.type(this.bubble.getTextElement(), step.html || '', twSpeed, () => {
333
370
  this.bubble.revealButtons();
@@ -389,6 +426,8 @@ export class TutorialEngine {
389
426
  advanceFn();
390
427
  }
391
428
  }
429
+
430
+ this._hook('afterTyping', step, isLast, this);
392
431
  }
393
432
 
394
433
  async _positionForStep(step, highlightBottom = false) {
@@ -424,6 +463,7 @@ export class TutorialEngine {
424
463
  }
425
464
 
426
465
  _close() {
466
+ this._hook('onClose', this);
427
467
  this._running = false;
428
468
  this._readyContext = null;
429
469
  this.autoNext.clear();
@@ -445,6 +485,11 @@ export class TutorialEngine {
445
485
  }
446
486
 
447
487
  _cleanupAnimations() {
488
+ // Cancel any pending typewriter setTimeout from _showStep
489
+ clearTimeout(this._stepTimer);
490
+ this._stepTimer = null;
491
+ this.typewriter.destroy();
492
+
448
493
  // If highlight was active, overlay was transparent — restore dimmed during transition
449
494
  if (this._hasActiveHighlight) {
450
495
  this._hasActiveHighlight = false;
@@ -454,5 +499,6 @@ export class TutorialEngine {
454
499
  this.highlight.destroy();
455
500
  this.hoverForcer.clearAll();
456
501
  this.options.destroy();
502
+ this._hook('onCleanup', this);
457
503
  }
458
504
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Skip a step if the hideIf element IS found in DOM (inverse of showIf).
3
+ * In devMode, shows step anyway for debugging.
4
+ */
5
+ export const HideIfExtension = {
6
+ name: 'hideIf',
7
+ beforeStep(step, index, engine) {
8
+ if (!step.hideIf) return;
9
+ if (engine.config.get('devMode')) return;
10
+ if (document.querySelector(step.hideIf)) return false;
11
+ },
12
+ };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Watches a DOM element via linkNext selector. When the element is clicked
3
+ * and the page content changes (detected via heading fingerprint), auto-advances.
4
+ * Uses MutationObserver to wait for the element if not yet in DOM.
5
+ */
6
+ export const LinkNextExtension = {
7
+ name: 'linkNext',
8
+ _cleanup: null,
9
+
10
+ afterTyping(step, isLast, engine) {
11
+ if (!step.linkNext) return;
12
+
13
+ // Hide Next button — linkNext replaces manual advance
14
+ if (engine.bubble.actionBtn) {
15
+ engine.bubble.actionBtn.style.display = 'none';
16
+ }
17
+
18
+ // Create a highlight cutout so the user can see and click the target element.
19
+ if (!step.highlight) {
20
+ engine.highlight.create({
21
+ groups: [{ elements: [step.linkNext] }],
22
+ padding: 6,
23
+ borderRadius: engine.config.get('highlightBorderRadius') || 8,
24
+ blockOutside: true,
25
+ });
26
+ engine._hasActiveHighlight = true;
27
+ }
28
+
29
+ // Overlay must not block clicks — the SVG blockOutside handles that
30
+ if (engine.overlay.el) {
31
+ engine.overlay.el.style.pointerEvents = 'none';
32
+ }
33
+
34
+ // Avatar container (z-index 10002) also blocks clicks — make it passable
35
+ // but keep the bubble itself interactive
36
+ const container = engine.getContainer?.();
37
+ if (container) {
38
+ container.style.pointerEvents = 'none';
39
+ }
40
+ const bubbleEl = engine.bubble.getElement?.();
41
+ if (bubbleEl) {
42
+ bubbleEl.style.pointerEvents = 'auto';
43
+ }
44
+
45
+ this._attach(step, engine);
46
+ },
47
+
48
+ _attach(step, engine) {
49
+ this._detach();
50
+
51
+ const selector = step.linkNext;
52
+ const targetEl = document.querySelector(selector);
53
+
54
+ if (targetEl) {
55
+ const onLinkedClick = () => {
56
+ const initial = this._fingerprint(selector);
57
+
58
+ // No fingerprint to track (no headings in container) — advance after short delay
59
+ if (!initial) {
60
+ setTimeout(() => engine._advance(), 500);
61
+ return;
62
+ }
63
+
64
+ // Has fingerprint — poll until content changes
65
+ let attempts = 0;
66
+ const poll = () => {
67
+ attempts++;
68
+ const current = this._fingerprint(selector);
69
+ if (current !== initial || attempts >= 30) {
70
+ engine._advance();
71
+ } else {
72
+ setTimeout(poll, 200);
73
+ }
74
+ };
75
+ setTimeout(poll, 300);
76
+ };
77
+
78
+ targetEl.addEventListener('click', onLinkedClick);
79
+ this._cleanup = () => targetEl.removeEventListener('click', onLinkedClick);
80
+ } else {
81
+ // Wait for element to appear
82
+ const obs = new MutationObserver(() => {
83
+ const el = document.querySelector(selector);
84
+ if (el) {
85
+ obs.disconnect();
86
+ this._attach(step, engine);
87
+ }
88
+ });
89
+ obs.observe(document.body, { childList: true, subtree: true });
90
+ this._cleanup = () => obs.disconnect();
91
+ }
92
+ },
93
+
94
+ _fingerprint(selector) {
95
+ const el = document.querySelector(selector);
96
+ if (!el) return '';
97
+ const container = el.closest('.kompoModal, .vlModal, .vlPanel, form') || el.parentElement;
98
+ if (!container) return '';
99
+ const headings = container.querySelectorAll('h1,h2,h3,h4,.text-xl,.text-2xl,.font-semibold,.miniTitle');
100
+ return Array.from(headings).map(h => h.textContent.trim()).join('|');
101
+ },
102
+
103
+ _detach() {
104
+ if (this._cleanup) { this._cleanup(); this._cleanup = null; }
105
+ },
106
+
107
+ onCleanup(engine) {
108
+ this._detach();
109
+ const container = engine.getContainer?.();
110
+ if (container) container.style.pointerEvents = 'auto';
111
+ },
112
+ onClose(engine) {
113
+ this._detach();
114
+ const container = engine.getContainer?.();
115
+ if (container) container.style.pointerEvents = 'auto';
116
+ },
117
+ };