js-kompo-utils 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +1 -0
  2. package/app.js +22 -0
  3. package/components/VlApexChart.vue +89 -0
  4. package/components/VlCKEditorTemplate.vue +52 -0
  5. package/components/VlCKEditorWithVars.vue +80 -0
  6. package/components/VlCollapsible.vue +91 -0
  7. package/components/VlCopyBox.vue +61 -0
  8. package/components/VlCreditCardInput.vue +104 -0
  9. package/components/VlDateMonthDay.vue +327 -0
  10. package/components/VlDateTextInput.vue +151 -0
  11. package/components/VlInternationalPhoneInput.vue +902 -0
  12. package/components/VlPartialDate.vue +283 -0
  13. package/components/VlPasswordInput.vue +358 -0
  14. package/components/VlQrCodeScanner.vue +91 -0
  15. package/components/VlSwipeableTab.vue +35 -0
  16. package/components/VlSwipeableTabs.vue +325 -0
  17. package/components/VlText.vue +72 -0
  18. package/components/VlTutorialStepBuilder.vue +671 -0
  19. package/components/VlValidatedInput.vue +262 -0
  20. package/components/data/countries.js +253 -0
  21. package/components/getAllDefaultComponents.js +12 -0
  22. package/docs/architecture.md +71 -0
  23. package/docs/tutorials.md +247 -0
  24. package/docs/usage.md +128 -0
  25. package/functions/index.js +7 -0
  26. package/functions/inputs.js +64 -0
  27. package/functions/loading.js +20 -0
  28. package/kompo-ckeditor-1.0.17/index.js +5 -0
  29. package/kompo-ckeditor-1.0.17/komponents/CKEditor.vue +36 -0
  30. package/kompo-ckeditor-1.0.17/komponents/CKEditorExtended.vue +66 -0
  31. package/kompo-ckeditor-1.0.17/komponents/EditableCKEditor.vue +42 -0
  32. package/kompo-ckeditor-1.0.17/komponents/TranslatableEditor.vue +28 -0
  33. package/kompo-ckeditor-1.0.17/mixins/CKEditor.js +291 -0
  34. package/kompo-ckeditor-1.0.17/mixins/CKEditorUtilities.js +52 -0
  35. package/kompo-ckeditor-1.0.17/mixins/CkUploadAdapter.js +95 -0
  36. package/kompo-ckeditor-1.0.17/mixins/translations/en.js +1 -0
  37. package/kompo-ckeditor-1.0.17/mixins/translations/fr.js +1 -0
  38. package/kompo-ckeditor-1.0.17/plugins/JsonBlockCommand.js +100 -0
  39. package/kompo-ckeditor-1.0.17/plugins/JsonBlockEditing.js +225 -0
  40. package/kompo-ckeditor-1.0.17/plugins/JsonBlockUi.js +21 -0
  41. package/kompo-ckeditor-1.0.17/plugins/MentionPlugin.js +118 -0
  42. package/orchestrator/KompoJsBuilder.js +333 -0
  43. package/package.json +25 -0
  44. package/raw_animations/count-up-animation.js +220 -0
  45. package/raw_animations/magic-poof.js +210 -0
  46. package/tutorials/TutorialConfig.js +34 -0
  47. package/tutorials/TutorialEngine.js +448 -0
  48. package/tutorials/index.js +2 -0
  49. package/tutorials/modules/AutoNext.js +86 -0
  50. package/tutorials/modules/Avatar.js +94 -0
  51. package/tutorials/modules/Bubble.js +617 -0
  52. package/tutorials/modules/Cursor.js +315 -0
  53. package/tutorials/modules/Highlight.js +282 -0
  54. package/tutorials/modules/HoverForcer.js +250 -0
  55. package/tutorials/modules/Mobile.js +148 -0
  56. package/tutorials/modules/Options.js +98 -0
  57. package/tutorials/modules/Overlay.js +142 -0
  58. package/tutorials/modules/Scroll.js +37 -0
  59. package/tutorials/modules/Typewriter.js +132 -0
  60. package/tutorials/step-builder/step-builder-bridge.js +436 -0
  61. package/tutorials/step-builder/utils/bezier-editor.js +379 -0
  62. package/tutorials/step-builder/utils/code-generator.js +127 -0
  63. package/tutorials/step-builder/utils/element-picker.js +125 -0
  64. package/tutorials/tutorial.scss +816 -0
  65. package/tutorials/utils/selectors.js +225 -0
  66. package/tutorials/utils/stability.js +76 -0
  67. package/tutorials/utils/svg.js +46 -0
@@ -0,0 +1,315 @@
1
+ import { gsap } from 'gsap';
2
+ import { MotionPathPlugin } from 'gsap/MotionPathPlugin';
3
+ import { resolveElementCenter, resolveRef } from '../utils/selectors.js';
4
+ import { denormalizeSvgPath } from '../utils/svg.js';
5
+ import { Z } from './Overlay.js';
6
+
7
+ gsap.registerPlugin(MotionPathPlugin);
8
+
9
+ const DEFAULT_CURSOR_SIZE = 'clamp(32px, 8vw, 48px)';
10
+ const DEFAULT_SVG_PATH = 'M 0,0 C 0.3,0.1 0.7,0.9 1,1';
11
+
12
+ /**
13
+ * Cursor module: animated cursor element using GSAP MotionPathPlugin.
14
+ */
15
+ export class Cursor {
16
+ constructor(engine) {
17
+ this.engine = engine;
18
+ this.el = null;
19
+ this.timeline = null;
20
+ }
21
+
22
+ /**
23
+ * Create a cursor DOM element (desktop image or mobile dot).
24
+ * @param {Object} cfg - Cursor configuration
25
+ * @param {boolean} [isMobile=false]
26
+ * @returns {HTMLElement}
27
+ */
28
+ create(cfg = {}, isMobile = false) {
29
+ let cursorEl;
30
+
31
+ if (isMobile) {
32
+ cursorEl = document.createElement('div');
33
+ cursorEl.className = 'tutorial-cursor tutorial-cursor--mobile';
34
+ } else {
35
+ cursorEl = document.createElement('img');
36
+ cursorEl.src = cfg.image || '/images/tutorial-cursor.svg';
37
+ cursorEl.className = 'tutorial-cursor';
38
+ cursorEl.style.width = DEFAULT_CURSOR_SIZE;
39
+ cursorEl.style.height = DEFAULT_CURSOR_SIZE;
40
+ }
41
+
42
+ document.body.appendChild(cursorEl);
43
+ return cursorEl;
44
+ }
45
+
46
+ /**
47
+ * Launch a cursor animation, choosing waypoints or simple based on step config.
48
+ * @param {Object} step
49
+ * @param {HTMLElement} overlay
50
+ * @param {Function} [onComplete]
51
+ * @returns {{ el, timeline, destroy } | null}
52
+ */
53
+ launch(step, overlay, onComplete) {
54
+ if (!step.cursor) {
55
+ onComplete?.();
56
+ return null;
57
+ }
58
+ return step.cursor.waypoints
59
+ ? this.animateWaypoints(step, overlay, onComplete)
60
+ : this.animate(step, overlay, onComplete);
61
+ }
62
+
63
+ /**
64
+ * Single from->to cursor animation with optional click pulse at end.
65
+ * @param {Object} step
66
+ * @param {HTMLElement} overlay
67
+ * @param {Function} [onComplete]
68
+ * @returns {{ el, timeline, destroy } | null}
69
+ */
70
+ animate(step, overlay, onComplete) {
71
+ const cfg = step.cursor;
72
+ if (!cfg) {
73
+ onComplete?.();
74
+ return null;
75
+ }
76
+
77
+ const fromPos = resolveElementCenter(cfg.from, cfg.fromAnchor, step);
78
+ const toPos = resolveElementCenter(cfg.to, cfg.toAnchor, step);
79
+ if (!fromPos || !toPos) {
80
+ console.warn('Cursor.animate: could not resolve from/to selectors', cfg.from, cfg.to);
81
+ onComplete?.();
82
+ return null;
83
+ }
84
+
85
+ const isMobile = this.engine.isMobile?.() ?? false;
86
+ const cursorEl = this.create(cfg, isMobile);
87
+ this.el = cursorEl;
88
+
89
+ const svgPathNorm = cfg.svgPath || DEFAULT_SVG_PATH;
90
+ const realPath = denormalizeSvgPath(svgPathNorm, fromPos, toPos);
91
+ const devMode = this.engine?.config?.get('devMode');
92
+
93
+ const timeline = gsap.timeline({
94
+ onComplete: () => {
95
+ if (cfg.click) {
96
+ const cursorRect = cursorEl.getBoundingClientRect();
97
+ const pulseX = cursorRect.left + cursorRect.width * 0.15;
98
+ const pulseY = cursorRect.top + cursorRect.height * 0.1;
99
+ this.createClickPulse(pulseX, pulseY);
100
+
101
+ gsap.to(cursorEl, {
102
+ scale: 0.7,
103
+ duration: 0.1,
104
+ yoyo: true,
105
+ repeat: 1,
106
+ onComplete: () => {
107
+ if (!devMode) {
108
+ const targetEl = document.querySelector(resolveRef(cfg.to, step));
109
+ if (targetEl && typeof targetEl.click === 'function') {
110
+ targetEl.click();
111
+ }
112
+ }
113
+ if (cfg.loop) {
114
+ gsap.set(cursorEl, { x: fromPos.x, y: fromPos.y, scale: 1 });
115
+ timeline.restart();
116
+ } else {
117
+ onComplete?.();
118
+ }
119
+ },
120
+ });
121
+ } else {
122
+ if (cfg.loop) {
123
+ gsap.set(cursorEl, { x: fromPos.x, y: fromPos.y });
124
+ timeline.restart();
125
+ } else {
126
+ onComplete?.();
127
+ }
128
+ }
129
+ },
130
+ });
131
+
132
+ this.timeline = timeline;
133
+
134
+ gsap.set(cursorEl, { x: fromPos.x, y: fromPos.y });
135
+ timeline.to(cursorEl, { opacity: 1, duration: 0.3 });
136
+ timeline.to(cursorEl, {
137
+ duration: cfg.duration || 1.5,
138
+ ease: cfg.ease || 'power2.inOut',
139
+ motionPath: { path: realPath },
140
+ });
141
+
142
+ if (cfg.delay) {
143
+ timeline.delay(cfg.delay);
144
+ }
145
+
146
+ return {
147
+ el: cursorEl,
148
+ timeline,
149
+ destroy: () => {
150
+ timeline.kill();
151
+ cursorEl.remove();
152
+ },
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Multi-point cursor animation with click/hover actions at each waypoint.
158
+ * @param {Object} step
159
+ * @param {HTMLElement} overlay
160
+ * @param {Function} [onComplete]
161
+ * @returns {{ el, timeline, destroy } | null}
162
+ */
163
+ animateWaypoints(step, overlay, onComplete) {
164
+ const cfg = step.cursor;
165
+ if (!cfg || !cfg.waypoints || !cfg.waypoints.length) {
166
+ return this.animate(step, overlay, onComplete);
167
+ }
168
+
169
+ const isMobile = this.engine.isMobile?.() ?? false;
170
+ const cursorEl = this.create(cfg, isMobile);
171
+ this.el = cursorEl;
172
+
173
+ const fromPos = resolveElementCenter(cfg.from, null, step);
174
+ if (!fromPos) {
175
+ cursorEl.remove();
176
+ this.el = null;
177
+ onComplete?.();
178
+ return null;
179
+ }
180
+
181
+ const devMode = this.engine?.config?.get('devMode');
182
+
183
+ const timeline = gsap.timeline({
184
+ delay: cfg.delay || 0,
185
+ onComplete: () => {
186
+ if (cfg.loop) {
187
+ gsap.set(cursorEl, { x: fromPos.x, y: fromPos.y });
188
+ timeline.restart();
189
+ } else {
190
+ onComplete?.();
191
+ }
192
+ },
193
+ });
194
+
195
+ this.timeline = timeline;
196
+
197
+ gsap.set(cursorEl, { x: fromPos.x, y: fromPos.y });
198
+ timeline.to(cursorEl, { opacity: 1, duration: 0.3 });
199
+
200
+ let currentFrom = fromPos;
201
+
202
+ cfg.waypoints.forEach((wp) => {
203
+ const wpPos = resolveElementCenter(wp.target, null, step);
204
+ if (!wpPos) return;
205
+
206
+ const wpPath = wp.svgPath || DEFAULT_SVG_PATH;
207
+ const realPath = denormalizeSvgPath(wpPath, currentFrom, wpPos);
208
+ const segDuration = (cfg.duration || 1.5) / cfg.waypoints.length;
209
+
210
+ timeline.to(cursorEl, {
211
+ duration: segDuration,
212
+ ease: cfg.ease || 'power2.inOut',
213
+ motionPath: { path: realPath },
214
+ });
215
+
216
+ const action = wp.action || 'path';
217
+
218
+ if (action === 'click') {
219
+ timeline.call(() => {
220
+ const cr = cursorEl.getBoundingClientRect();
221
+ this.createClickPulse(
222
+ cr.left + cr.width * 0.15,
223
+ cr.top + cr.height * 0.1
224
+ );
225
+ });
226
+ timeline.to(cursorEl, {
227
+ scale: 0.7,
228
+ duration: 0.1,
229
+ yoyo: true,
230
+ repeat: 1,
231
+ });
232
+ ((target) => {
233
+ timeline.call(() => {
234
+ if (!devMode) {
235
+ const el = document.querySelector(target);
236
+ if (el) el.click();
237
+ }
238
+ });
239
+ })(wp.target);
240
+ } else if (action === 'hover') {
241
+ ((target) => {
242
+ timeline.call(() => {
243
+ const el = document.querySelector(target);
244
+ if (el) {
245
+ this.engine.forceHover?.(el);
246
+ el.classList.add('tutorial-force-hover');
247
+ el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
248
+ el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
249
+ }
250
+ });
251
+ timeline.to(cursorEl, { duration: 0.5 });
252
+ })(wp.target);
253
+ }
254
+
255
+ if (wp.pause && wp.pause > 0) {
256
+ timeline.to(cursorEl, { duration: wp.pause });
257
+ }
258
+
259
+ currentFrom = wpPos;
260
+ });
261
+
262
+ return {
263
+ el: cursorEl,
264
+ timeline,
265
+ destroy: () => {
266
+ timeline.kill();
267
+ cursorEl.remove();
268
+ },
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Create a click-pulse visual effect at the given coordinates.
274
+ * @param {number} x
275
+ * @param {number} y
276
+ */
277
+ createClickPulse(x, y) {
278
+ this._createRing(x, y, 0.25, 3, 0.7, 0);
279
+ this._createRing(x, y, 0.15, 2, 0.5, 0.15);
280
+ }
281
+
282
+ /**
283
+ * @private
284
+ */
285
+ _createRing(x, y, bgOpacity, scale, duration, delay) {
286
+ const ring = document.createElement('div');
287
+ ring.className = 'tutorial-pulse-ring';
288
+ ring.style.left = x + 'px';
289
+ ring.style.top = y + 'px';
290
+ ring.style.backgroundColor = `rgba(255, 68, 68, ${bgOpacity})`;
291
+ ring.style.setProperty('--pulse-scale', String(scale));
292
+ ring.style.setProperty('--pulse-duration', duration + 's');
293
+ ring.style.setProperty('--pulse-delay', delay + 's');
294
+
295
+ document.body.appendChild(ring);
296
+
297
+ ring.addEventListener('animationend', () => {
298
+ ring.remove();
299
+ });
300
+
301
+ setTimeout(() => {
302
+ if (ring.parentNode) ring.remove();
303
+ }, (duration + delay) * 1000 + 100);
304
+ }
305
+
306
+ /**
307
+ * Kill the GSAP timeline and remove the cursor element.
308
+ */
309
+ destroy() {
310
+ this.timeline?.kill();
311
+ this.el?.remove();
312
+ this.el = null;
313
+ this.timeline = null;
314
+ }
315
+ }
@@ -0,0 +1,282 @@
1
+ /**
2
+ * SVG overlay with cutout windows around highlighted elements.
3
+ *
4
+ * Creates an SVG covering the viewport with a semi-transparent dark layer,
5
+ * using the even-odd fill rule to cut out rounded rectangular windows
6
+ * around specified element groups.
7
+ */
8
+
9
+ import { createSvgEl } from '../utils/svg.js';
10
+ import { waitForStableHighlight } from '../utils/stability.js';
11
+ import { Z } from './Overlay.js';
12
+
13
+ /**
14
+ * Build SVG path data for highlight cutouts from groups config.
15
+ * @param {Array} groups
16
+ * @param {number} padding
17
+ * @param {number} radius
18
+ * @returns {{ pathData: string, rects: Array }}
19
+ */
20
+ function buildHighlightPathData(groups, padding, radius) {
21
+ const W = window.innerWidth;
22
+ const H = window.innerHeight;
23
+
24
+ let d = `M 0,0 L ${W},0 L ${W},${H} L 0,${H} Z `;
25
+ const cutoutRects = [];
26
+
27
+ for (const group of groups) {
28
+ const selectors = group.elements || group;
29
+ if (!selectors.length) continue;
30
+
31
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
32
+
33
+ for (const sel of selectors) {
34
+ const el = document.querySelector(sel);
35
+ if (!el) continue;
36
+ const rect = el.getBoundingClientRect();
37
+ if (rect.left < minX) minX = rect.left;
38
+ if (rect.top < minY) minY = rect.top;
39
+ if (rect.right > maxX) maxX = rect.right;
40
+ if (rect.bottom > maxY) maxY = rect.bottom;
41
+ }
42
+
43
+ if (minX === Infinity) continue;
44
+
45
+ const x = minX - padding;
46
+ const y = minY - padding;
47
+ const w = (maxX - minX) + padding * 2;
48
+ const h = (maxY - minY) + padding * 2;
49
+ const r = Math.min(radius, w / 2, h / 2);
50
+
51
+ cutoutRects.push({ x, y, w, h, r });
52
+
53
+ d += `M ${x + r},${y}`
54
+ + ` L ${x + w - r},${y}`
55
+ + ` A ${r},${r} 0 0 1 ${x + w},${y + r}`
56
+ + ` L ${x + w},${y + h - r}`
57
+ + ` A ${r},${r} 0 0 1 ${x + w - r},${y + h}`
58
+ + ` L ${x + r},${y + h}`
59
+ + ` A ${r},${r} 0 0 1 ${x},${y + h - r}`
60
+ + ` L ${x},${y + r}`
61
+ + ` A ${r},${r} 0 0 1 ${x + r},${y} Z `;
62
+ }
63
+
64
+ return { pathData: d, rects: cutoutRects };
65
+ }
66
+
67
+ /**
68
+ * Create an SVG <rect> border element for a highlight cutout.
69
+ * @param {{ x, y, w, h, r }} cr
70
+ * @returns {SVGRectElement}
71
+ */
72
+ function createBorderRect(cr) {
73
+ return createSvgEl('rect', {
74
+ x: cr.x,
75
+ y: cr.y,
76
+ width: cr.w,
77
+ height: cr.h,
78
+ rx: cr.r,
79
+ fill: 'none',
80
+ stroke: '#ffd700',
81
+ 'stroke-width': '2',
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Debounced scroll/resize listener using requestAnimationFrame.
87
+ * @param {Function} callback
88
+ * @returns {Function} cleanup
89
+ */
90
+ function onScrollResize(callback) {
91
+ let rafId = null;
92
+
93
+ function handler() {
94
+ if (!rafId) {
95
+ rafId = requestAnimationFrame(() => {
96
+ rafId = null;
97
+ callback();
98
+ });
99
+ }
100
+ }
101
+
102
+ window.addEventListener('scroll', handler, true);
103
+ window.addEventListener('resize', handler);
104
+
105
+ return function cleanup() {
106
+ window.removeEventListener('scroll', handler, true);
107
+ window.removeEventListener('resize', handler);
108
+ if (rafId) {
109
+ cancelAnimationFrame(rafId);
110
+ rafId = null;
111
+ }
112
+ };
113
+ }
114
+
115
+ export class Highlight {
116
+ constructor(engine) {
117
+ this.engine = engine;
118
+ this.svgEl = null;
119
+ this.pathEl = null;
120
+ this.cleanupFn = null;
121
+ this._config = null;
122
+ this._generation = 0;
123
+ this._cancelStability = null;
124
+ }
125
+
126
+ /**
127
+ * Auto-scroll to the first highlighted element if off-screen.
128
+ * @param {object} step
129
+ * @param {boolean} isMobile
130
+ * @returns {{ highlightBottom: boolean }}
131
+ */
132
+ scrollToElements(step, isMobile) {
133
+ let highlightBottom = false;
134
+ const hlConfig = step.highlight;
135
+ if (!hlConfig) return { highlightBottom };
136
+
137
+ let firstSel = null;
138
+ if (hlConfig.selector) {
139
+ firstSel = hlConfig.selector;
140
+ } else if (hlConfig.groups && hlConfig.groups.length) {
141
+ const g = hlConfig.groups[0];
142
+ const elems = g.elements || g;
143
+ if (elems.length) firstSel = elems[0];
144
+ }
145
+
146
+ if (firstSel) {
147
+ const hlFirstEl = document.querySelector(firstSel);
148
+ if (hlFirstEl) {
149
+ let hlRect = hlFirstEl.getBoundingClientRect();
150
+ if (hlRect.top < 0 || hlRect.bottom > window.innerHeight) {
151
+ document.body.style.overflow = '';
152
+ hlFirstEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
153
+ document.body.style.overflow = 'hidden';
154
+ }
155
+ hlRect = hlFirstEl.getBoundingClientRect();
156
+ if (isMobile) {
157
+ highlightBottom = (hlRect.top + hlRect.height / 2) > window.innerHeight / 2;
158
+ }
159
+ }
160
+ }
161
+
162
+ return { highlightBottom };
163
+ }
164
+
165
+ /**
166
+ * Create the highlight overlay with cutout windows.
167
+ * @param {Object} config
168
+ * @returns {Highlight} this
169
+ */
170
+ create(config) {
171
+ if (!config) return this;
172
+
173
+ this.destroy();
174
+
175
+ if (config.selector && !config.groups) {
176
+ config = {
177
+ groups: [{ elements: [config.selector] }],
178
+ padding: config.padding || 8,
179
+ borderRadius: config.borderRadius || 8,
180
+ };
181
+ }
182
+
183
+ if (!config.groups || !config.groups.length) return this;
184
+
185
+ this._config = config;
186
+ this._generation++;
187
+ const gen = this._generation;
188
+ const padding = config.padding || 8;
189
+ const radius = config.borderRadius || 8;
190
+ const blockOutside = config.blockOutside || false;
191
+
192
+ this._cancelStability = waitForStableHighlight(config, () => {
193
+ if (gen !== this._generation) return;
194
+ this._build(padding, radius, blockOutside);
195
+ });
196
+
197
+ return this;
198
+ }
199
+
200
+ /** @private */
201
+ _build(padding, radius, blockOutside) {
202
+ const config = this._config;
203
+ if (!config) return;
204
+
205
+ const W = window.innerWidth;
206
+ const H = window.innerHeight;
207
+ this.svgEl = createSvgEl('svg', {
208
+ width: W,
209
+ height: H,
210
+ viewBox: `0 0 ${W} ${H}`,
211
+ });
212
+ this.svgEl.classList.add('tutorial-highlight-svg');
213
+ Object.assign(this.svgEl.style, {
214
+ position: 'fixed',
215
+ top: '0',
216
+ left: '0',
217
+ width: '100vw',
218
+ height: '100vh',
219
+ zIndex: String(Z.HIGHLIGHT),
220
+ pointerEvents: 'none',
221
+ });
222
+
223
+ const result = buildHighlightPathData(config.groups, padding, radius);
224
+
225
+ this.pathEl = createSvgEl('path', {
226
+ d: result.pathData,
227
+ fill: 'rgba(0,0,0,0.55)',
228
+ 'fill-rule': 'evenodd',
229
+ 'pointer-events': blockOutside ? 'fill' : 'none',
230
+ });
231
+ this.svgEl.appendChild(this.pathEl);
232
+
233
+ for (const cr of result.rects) {
234
+ this.svgEl.appendChild(createBorderRect(cr));
235
+ }
236
+
237
+ document.body.appendChild(this.svgEl);
238
+
239
+ // Highlight SVG is now in DOM — overlay can safely go transparent
240
+ this.engine.overlay.setMode('transparent');
241
+
242
+ const cleanupScrollResize = onScrollResize(() => {
243
+ this._refresh(padding, radius);
244
+ });
245
+
246
+ this.cleanupFn = cleanupScrollResize;
247
+ }
248
+
249
+ /** @private */
250
+ _refresh(padding, radius) {
251
+ if (!this.svgEl || !this.pathEl || !this._config) return;
252
+
253
+ const W = window.innerWidth;
254
+ const H = window.innerHeight;
255
+ this.svgEl.setAttribute('width', W);
256
+ this.svgEl.setAttribute('height', H);
257
+ this.svgEl.setAttribute('viewBox', `0 0 ${W} ${H}`);
258
+
259
+ const result = buildHighlightPathData(this._config.groups, padding, radius);
260
+ this.pathEl.setAttribute('d', result.pathData);
261
+
262
+ const oldBorders = this.svgEl.querySelectorAll('rect');
263
+ for (const border of oldBorders) {
264
+ border.remove();
265
+ }
266
+ for (const cr of result.rects) {
267
+ this.svgEl.appendChild(createBorderRect(cr));
268
+ }
269
+ }
270
+
271
+ destroy() {
272
+ this._generation++;
273
+ this._cancelStability?.();
274
+ this._cancelStability = null;
275
+ this.cleanupFn?.();
276
+ this.svgEl?.remove();
277
+ this.svgEl = null;
278
+ this.pathEl = null;
279
+ this.cleanupFn = null;
280
+ this._config = null;
281
+ }
282
+ }