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,250 @@
1
+ /**
2
+ * Stylesheet scanning and inline :hover style injection.
3
+ *
4
+ * Forces :hover state on elements during tutorials by scanning all stylesheets
5
+ * for matching :hover rules and duplicating them with a special class selector.
6
+ */
7
+
8
+ const HOVER_PROPS = [
9
+ 'color', 'background-color', 'background', 'border-color',
10
+ 'box-shadow', 'text-decoration', 'opacity', 'transform',
11
+ 'outline', 'visibility', 'display',
12
+ ];
13
+
14
+ const HOVER_CLASS_PATTERN = /hover[:\\]|vlOpenOnHover|hover-/;
15
+ const FORCE_HOVER_CLASS = 'tutorial-force-hover';
16
+
17
+ export class HoverForcer {
18
+ constructor() {
19
+ this.forcedElements = new Set();
20
+ this._hoverSheet = null;
21
+ }
22
+
23
+ /** @private */
24
+ _getHoverSheet() {
25
+ if (!this._hoverSheet) {
26
+ const styleEl = document.createElement('style');
27
+ styleEl.setAttribute('data-tutorial-hover', '');
28
+ document.head.appendChild(styleEl);
29
+ this._hoverSheet = styleEl.sheet;
30
+ }
31
+ return this._hoverSheet;
32
+ }
33
+
34
+ /**
35
+ * Force :hover styles onto an element.
36
+ * @param {HTMLElement} el
37
+ */
38
+ forceHover(el) {
39
+ this.removeHover(el);
40
+
41
+ const sheet = this._getHoverSheet();
42
+ const injectedRules = [];
43
+
44
+ try {
45
+ const sheets = document.styleSheets;
46
+ for (let i = 0; i < sheets.length; i++) {
47
+ let rules;
48
+ try {
49
+ rules = sheets[i].cssRules || sheets[i].rules;
50
+ } catch {
51
+ continue;
52
+ }
53
+ if (!rules) continue;
54
+
55
+ for (let j = 0; j < rules.length; j++) {
56
+ const rule = rules[j];
57
+ if (!rule.selectorText || !rule.selectorText.includes(':hover')) continue;
58
+
59
+ const selectorParts = rule.selectorText.split(',');
60
+ let matches = false;
61
+
62
+ for (const rawPart of selectorParts) {
63
+ const part = rawPart.trim();
64
+ if (!part.includes(':hover')) continue;
65
+
66
+ const hoverSegment = part.split(':hover')[0].trim();
67
+ const lastSegment = hoverSegment.split(/[\s>+~]/).pop().trim();
68
+ try {
69
+ if (lastSegment && el.matches(lastSegment)) {
70
+ matches = true;
71
+ break;
72
+ }
73
+ } catch { /* invalid selector */ }
74
+
75
+ const baseSelector = part.replace(/:hover/g, '');
76
+ try {
77
+ if (el.matches(baseSelector)) {
78
+ matches = true;
79
+ break;
80
+ }
81
+ } catch { /* invalid selector */ }
82
+ }
83
+
84
+ if (!matches) continue;
85
+
86
+ const newSelector = rule.selectorText.replace(/:hover/g, '.' + FORCE_HOVER_CLASS);
87
+ try {
88
+ const idx = sheet.insertRule(
89
+ `${newSelector} { ${rule.style.cssText} }`,
90
+ sheet.cssRules.length,
91
+ );
92
+ injectedRules.push(idx);
93
+ } catch { /* skip invalid rules */ }
94
+ }
95
+ }
96
+ } catch (e) {
97
+ console.warn('HoverForcer: forceHover failed', e);
98
+ }
99
+
100
+ el._tutorialHoverRules = injectedRules;
101
+ el.classList.add(FORCE_HOVER_CLASS);
102
+ this.forcedElements.add(el);
103
+ }
104
+
105
+ /**
106
+ * Remove forced hover styles from an element.
107
+ * @param {HTMLElement} el
108
+ */
109
+ removeHover(el) {
110
+ if (!el._tutorialHoverRules) return;
111
+
112
+ const sheet = this._getHoverSheet();
113
+ const indices = el._tutorialHoverRules.slice().sort((a, b) => b - a);
114
+ for (const idx of indices) {
115
+ try {
116
+ sheet.deleteRule(idx);
117
+ } catch { /* already removed */ }
118
+ }
119
+
120
+ delete el._tutorialHoverRules;
121
+ el.classList.remove(FORCE_HOVER_CLASS);
122
+ this.forcedElements.delete(el);
123
+ }
124
+
125
+ /**
126
+ * Check if an element has visible hover effects.
127
+ * @param {HTMLElement} el
128
+ * @returns {boolean}
129
+ */
130
+ elementHasHoverEffect(el) {
131
+ const normalStyle = window.getComputedStyle(el);
132
+ const normalValues = {};
133
+ for (const prop of HOVER_PROPS) {
134
+ normalValues[prop] = normalStyle.getPropertyValue(prop);
135
+ }
136
+
137
+ const testClass = `_tut_hover_test_${Date.now()}`;
138
+ const styleEl = document.createElement('style');
139
+ document.head.appendChild(styleEl);
140
+
141
+ let hasChange = false;
142
+
143
+ try {
144
+ const sheets = document.styleSheets;
145
+ for (let i = 0; i < sheets.length; i++) {
146
+ let rules;
147
+ try {
148
+ rules = sheets[i].cssRules || sheets[i].rules;
149
+ } catch {
150
+ continue;
151
+ }
152
+ if (!rules) continue;
153
+
154
+ for (let j = 0; j < rules.length; j++) {
155
+ const rule = rules[j];
156
+ if (!rule.selectorText || !rule.selectorText.includes(':hover')) continue;
157
+
158
+ const baseSelector = rule.selectorText.replace(/:hover/g, '');
159
+ try {
160
+ if (!el.matches(baseSelector)) continue;
161
+ } catch {
162
+ continue;
163
+ }
164
+
165
+ const newSel = rule.selectorText.replace(/:hover/g, '.' + testClass);
166
+ try {
167
+ styleEl.sheet.insertRule(
168
+ `${newSel}{${rule.style.cssText}}`,
169
+ styleEl.sheet.cssRules.length,
170
+ );
171
+ } catch { /* skip invalid */ }
172
+ }
173
+ }
174
+
175
+ el.classList.add(testClass);
176
+ const hoverStyle = window.getComputedStyle(el);
177
+
178
+ for (const prop of HOVER_PROPS) {
179
+ if (hoverStyle.getPropertyValue(prop) !== normalValues[prop]) {
180
+ hasChange = true;
181
+ break;
182
+ }
183
+ }
184
+
185
+ el.classList.remove(testClass);
186
+ } catch { /* swallow errors */ }
187
+
188
+ document.head.removeChild(styleEl);
189
+
190
+ if (!hasChange && el.className && typeof el.className === 'string') {
191
+ if (HOVER_CLASS_PATTERN.test(el.className)) {
192
+ hasChange = true;
193
+ }
194
+ }
195
+
196
+ if (!hasChange && el.classList) {
197
+ for (const cls of el.classList) {
198
+ if (HOVER_CLASS_PATTERN.test(cls)) {
199
+ hasChange = true;
200
+ break;
201
+ }
202
+ }
203
+ }
204
+
205
+ return hasChange;
206
+ }
207
+
208
+ /**
209
+ * Find the closest ancestor (or self) with hover effects.
210
+ * @param {HTMLElement} el
211
+ * @returns {HTMLElement|null}
212
+ */
213
+ findHoverableParent(el) {
214
+ let current = el;
215
+ while (current && current !== document.body) {
216
+ if (this.elementHasHoverEffect(current)) return current;
217
+ current = current.parentElement;
218
+ }
219
+ return null;
220
+ }
221
+
222
+ /**
223
+ * Remove all forced hover styles and reset state.
224
+ */
225
+ clearAll() {
226
+ for (const el of this.forcedElements) {
227
+ if (el._tutorialHoverRules) {
228
+ const sheet = this._getHoverSheet();
229
+ const indices = el._tutorialHoverRules.slice().sort((a, b) => b - a);
230
+ for (const idx of indices) {
231
+ try {
232
+ sheet.deleteRule(idx);
233
+ } catch { /* already removed */ }
234
+ }
235
+ delete el._tutorialHoverRules;
236
+ }
237
+ el.classList.remove(FORCE_HOVER_CLASS);
238
+ el.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
239
+ }
240
+ this.forcedElements.clear();
241
+
242
+ if (this._hoverSheet) {
243
+ const ownerNode = this._hoverSheet.ownerNode;
244
+ if (ownerNode && ownerNode.parentNode) {
245
+ ownerNode.parentNode.removeChild(ownerNode);
246
+ }
247
+ this._hoverSheet = null;
248
+ }
249
+ }
250
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Mobile detection and tab-dropdown adaptation.
3
+ */
4
+
5
+ const MOBILE_BREAKPOINT = 600;
6
+
7
+ export class Mobile {
8
+ /** @returns {boolean} */
9
+ isMobile() {
10
+ return window.innerWidth < MOBILE_BREAKPOINT;
11
+ }
12
+
13
+ /**
14
+ * Check whether a selector targets a hidden desktop tab and resolve mobile alternative.
15
+ * @param {string} selector
16
+ * @returns {object|null}
17
+ */
18
+ resolveMobileTab(selector) {
19
+ if (!this.isMobile()) return null;
20
+
21
+ const el = document.querySelector(selector);
22
+ if (!el) return null;
23
+
24
+ const isHiddenTab =
25
+ el.tagName === 'A' &&
26
+ el.getAttribute('role') === 'tab' &&
27
+ getComputedStyle(el).display === 'none';
28
+ if (!isHiddenTab) return null;
29
+
30
+ const tabsContainer = el.closest('.vlTabs');
31
+ if (!tabsContainer) return null;
32
+
33
+ const tabsId = tabsContainer.id || '';
34
+ const hash = tabsId.replace('responsive-tabs-', '');
35
+
36
+ let tabsSelectInput = hash
37
+ ? document.querySelector('input#tabs-select-' + hash)
38
+ : null;
39
+
40
+ if (!tabsSelectInput) {
41
+ const parent = tabsContainer.parentElement;
42
+ if (parent) {
43
+ tabsSelectInput = parent.querySelector('input[id^="tabs-select"]');
44
+ }
45
+ }
46
+ if (!tabsSelectInput) return null;
47
+
48
+ const dropdownWrapper = tabsSelectInput.closest('.vlInputWrapper');
49
+ if (!dropdownWrapper) return null;
50
+
51
+ const taggable = dropdownWrapper.querySelector('.vlTaggableInput');
52
+ if (!taggable) return null;
53
+
54
+ if (!dropdownWrapper.id) {
55
+ dropdownWrapper.id = 'tutorial-mobile-tab-' + hash;
56
+ }
57
+
58
+ const tabText = el.textContent.trim();
59
+ const allTabLinks = Array.from(tabsContainer.querySelectorAll('a[role="tab"]'));
60
+ const tabIndex = allTabLinks.indexOf(el);
61
+
62
+ return {
63
+ isMobileTab: true,
64
+ dropdown: dropdownWrapper,
65
+ taggable,
66
+ selector: '#' + dropdownWrapper.id,
67
+ optionText: tabText,
68
+ tabIndex,
69
+ tabsContainer,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Resolve hidden desktop tabs to mobile dropdown equivalents for a step.
75
+ * @param {object} step
76
+ * @returns {object}
77
+ */
78
+ resolveTabsForStep(step) {
79
+ if (!this.isMobile()) return step;
80
+
81
+ if (step.highlight) {
82
+ const hlCfg = step.highlight;
83
+ let hlSel = hlCfg.selector || null;
84
+ if (!hlSel && hlCfg.groups && hlCfg.groups.length) {
85
+ const grp = hlCfg.groups[0];
86
+ const els = grp.elements || grp;
87
+ if (els.length) hlSel = els[0];
88
+ }
89
+ if (hlSel) {
90
+ const mTab = this.resolveMobileTab(hlSel);
91
+ if (mTab) {
92
+ step = JSON.parse(JSON.stringify(step));
93
+ step.highlight = {
94
+ groups: [{ elements: [mTab.selector] }],
95
+ padding: (hlCfg.padding || 8),
96
+ borderRadius: (hlCfg.borderRadius || 8),
97
+ };
98
+ this.selectMobileTab(mTab);
99
+ }
100
+ }
101
+ }
102
+
103
+ if (step.cursor && step.cursor.to) {
104
+ const cTab = this.resolveMobileTab(step.cursor.to);
105
+ if (cTab) {
106
+ step = typeof step._mobileCloned === 'undefined'
107
+ ? JSON.parse(JSON.stringify(step))
108
+ : step;
109
+ step._mobileCloned = true;
110
+ step.cursor.to = cTab.selector;
111
+ step.cursor.click = false;
112
+ step._mobileTabToSelect = cTab;
113
+ }
114
+ }
115
+
116
+ return step;
117
+ }
118
+
119
+ /**
120
+ * Click the mobile tab dropdown and select the matching option.
121
+ * @param {object} mobileTab
122
+ */
123
+ selectMobileTab(mobileTab) {
124
+ if (!mobileTab || !mobileTab.isMobileTab) return;
125
+
126
+ mobileTab.taggable.click();
127
+
128
+ setTimeout(() => {
129
+ const options = mobileTab.dropdown.querySelectorAll('.vlOption');
130
+ let clicked = false;
131
+
132
+ for (let i = 0; i < options.length; i++) {
133
+ if (options[i].textContent.trim() === mobileTab.optionText) {
134
+ options[i].click();
135
+ clicked = true;
136
+ break;
137
+ }
138
+ }
139
+
140
+ if (!clicked) {
141
+ const vue = mobileTab.tabsContainer.__vue__;
142
+ if (vue && typeof vue.selectTab === 'function' && mobileTab.tabIndex >= 0) {
143
+ vue.selectTab(mobileTab.tabIndex);
144
+ }
145
+ }
146
+ }, 200);
147
+ }
148
+ }
@@ -0,0 +1,98 @@
1
+ import { gsap } from 'gsap';
2
+
3
+ const STEP_TRANSITION_MS = 300;
4
+
5
+ /**
6
+ * Renders option buttons inside a tutorial step's options row.
7
+ */
8
+ export class Options {
9
+ constructor() {
10
+ /** @type {HTMLButtonElement[]} */
11
+ this.buttons = [];
12
+ }
13
+
14
+ /**
15
+ * Render option buttons into the container.
16
+ * @param {Array<object>} options
17
+ * @param {HTMLElement} container
18
+ * @param {object} callbacks - { goToStep, close, advanceClick, onTransition }
19
+ */
20
+ render(options, container, callbacks) {
21
+ this.destroy();
22
+
23
+ if (!container) return;
24
+
25
+ container.style.display = 'flex';
26
+ container.style.pointerEvents = 'none';
27
+
28
+ const totalOpts = options.length;
29
+ let renderedCount = 0;
30
+
31
+ options.forEach((opt, oi) => {
32
+ const btn = document.createElement('button');
33
+ btn.textContent = opt.label;
34
+ btn.className = 'tutorial-option-btn';
35
+
36
+ const hoverBg = opt.hoverColor || '#07499e';
37
+ const normalBg = opt.color || '#f0f4ff';
38
+ const normalColor = opt.textColor || '#07499e';
39
+
40
+ // Dynamic colors that depend on step config
41
+ btn.style.backgroundColor = normalBg;
42
+ btn.style.color = normalColor;
43
+ btn.style.border = '2px solid ' + (opt.borderColor || '#07499e');
44
+
45
+ const setColors = (bg, color) => {
46
+ btn.style.backgroundColor = bg;
47
+ btn.style.color = color;
48
+ };
49
+ btn.addEventListener('mouseenter', () => setColors(hoverBg, '#ffffff'));
50
+ btn.addEventListener('mouseleave', () => setColors(normalBg, normalColor));
51
+ btn.addEventListener('touchstart', () => setColors(hoverBg, '#ffffff'), { passive: true });
52
+ btn.addEventListener('touchend', () => setColors(normalBg, normalColor), { passive: true });
53
+
54
+ btn.addEventListener('click', () => {
55
+ if (opt.done) {
56
+ callbacks.close();
57
+ } else if (opt.redirect) {
58
+ let url = opt.redirect;
59
+ if (opt.startTutorial !== undefined && opt.startTutorial !== false) {
60
+ const fromStep = typeof opt.startTutorial === 'number' ? opt.startTutorial : 0;
61
+ const name = typeof opt.startTutorial === 'string' ? opt.startTutorial : '';
62
+ const sep = url.includes('?') ? '&' : '?';
63
+ url += `${sep}tutorial=${encodeURIComponent(name)}&fromStep=${fromStep}`;
64
+ }
65
+ window.location.href = url;
66
+ } else if (opt.goToStep !== undefined) {
67
+ callbacks.onTransition?.();
68
+ setTimeout(() => callbacks.goToStep(opt.goToStep), STEP_TRANSITION_MS);
69
+ } else {
70
+ callbacks.advanceClick();
71
+ }
72
+ });
73
+
74
+ container.appendChild(btn);
75
+ this.buttons.push(btn);
76
+
77
+ gsap.to(btn, {
78
+ opacity: 1,
79
+ y: 0,
80
+ duration: 0.3,
81
+ delay: 0.1 * (oi + 1),
82
+ ease: 'power2.out',
83
+ onComplete: () => {
84
+ btn.style.pointerEvents = 'auto';
85
+ renderedCount++;
86
+ if (renderedCount === totalOpts) {
87
+ container.style.pointerEvents = 'auto';
88
+ }
89
+ },
90
+ });
91
+ });
92
+ }
93
+
94
+ destroy() {
95
+ this.buttons.forEach((btn) => btn.remove());
96
+ this.buttons = [];
97
+ }
98
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Z-index constants shared across all tutorial modules.
3
+ */
4
+ const Z = {
5
+ OVERLAY: 9997,
6
+ HIGHLIGHT: 9998,
7
+ CURSOR: 10000,
8
+ PULSE: 10001,
9
+ CONTAINER: 10002,
10
+ };
11
+
12
+ export { Z };
13
+
14
+ /**
15
+ * Manages the full-screen overlay, click blocking, and mobile peek button.
16
+ */
17
+ export class Overlay {
18
+ constructor(engine) {
19
+ this.engine = engine;
20
+ this.el = null;
21
+ this.peekBtn = null;
22
+ this.peekHidden = false;
23
+ this._peekClickHandler = null;
24
+ this._peekTouchHandler = null;
25
+ this._savedOverflow = '';
26
+ }
27
+
28
+ /**
29
+ * Build overlay and peek button, append to document.body.
30
+ * Safe to call multiple times — cleans up previous DOM first.
31
+ */
32
+ create() {
33
+ if (this.el) this.destroy();
34
+
35
+ this._savedOverflow = document.body.style.overflow;
36
+
37
+ this.el = document.createElement('div');
38
+ this.el.id = 'tutorial-overlay';
39
+ this.el.className = 'tutorial-overlay';
40
+ this.el.setAttribute('role', 'dialog');
41
+ this.el.setAttribute('aria-modal', 'true');
42
+ this.el.setAttribute('aria-label', 'Tutorial');
43
+ this.el.style.backgroundColor = 'rgba(0,0,0,0.5)'; // Default dark, setMode() toggles
44
+
45
+ document.body.appendChild(this.el);
46
+ document.body.style.overflow = 'hidden';
47
+
48
+ // Mobile peek button -- hides avatar+bubble so user can see behind overlay
49
+ this.peekBtn = document.createElement('button');
50
+ this.peekBtn.textContent = '\uD83D\uDC41';
51
+ this.peekBtn.className = 'tutorial-peek-btn';
52
+
53
+ this._peekClickHandler = (e) => {
54
+ e.stopPropagation();
55
+ this.peekHidden = !this.peekHidden;
56
+
57
+ const container = this.engine.getContainer?.();
58
+ if (container) {
59
+ container.style.display = this.peekHidden ? 'none' : 'flex';
60
+ }
61
+
62
+ if (this.peekHidden) {
63
+ this.el.style.backgroundColor = 'transparent';
64
+ } else {
65
+ const step = this.engine.getCurrentStep?.();
66
+ const hasHighlight = this.engine.hasActiveHighlight?.();
67
+ const showOverlay = step ? (step.overlay !== false && !hasHighlight) : true;
68
+ this.el.style.backgroundColor = showOverlay
69
+ ? 'rgba(0,0,0,0.5)'
70
+ : 'transparent';
71
+ }
72
+
73
+ this.peekBtn.style.opacity = this.peekHidden ? '0.6' : '1';
74
+ };
75
+
76
+ this._peekTouchHandler = (e) => {
77
+ e.stopPropagation();
78
+ };
79
+
80
+ this.peekBtn.addEventListener('click', this._peekClickHandler);
81
+ this.peekBtn.addEventListener('touchstart', this._peekTouchHandler, { passive: true });
82
+ document.body.appendChild(this.peekBtn);
83
+ }
84
+
85
+ /**
86
+ * Toggle the overlay background between dimmed and transparent.
87
+ * @param {'dimmed'|'transparent'} mode
88
+ */
89
+ setMode(mode) {
90
+ if (!this.el) return;
91
+ this.el.style.backgroundColor = mode === 'dimmed'
92
+ ? 'rgba(0,0,0,0.5)'
93
+ : 'transparent';
94
+ }
95
+
96
+ /**
97
+ * Show or hide the mobile peek button.
98
+ * @param {boolean} show
99
+ */
100
+ showPeekButton(show) {
101
+ if (!this.peekBtn) return;
102
+ this.peekBtn.style.display = show ? 'flex' : 'none';
103
+ }
104
+
105
+ /**
106
+ * Reset peek state (un-hides the container if it was peeked).
107
+ */
108
+ resetPeek() {
109
+ if (!this.peekHidden) return;
110
+ this.peekHidden = false;
111
+ const container = this.engine.getContainer?.();
112
+ if (container) {
113
+ container.style.display = 'flex';
114
+ }
115
+ if (this.peekBtn) {
116
+ this.peekBtn.style.opacity = '1';
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Remove overlay and peek button, restore body overflow, clean up listeners.
122
+ */
123
+ destroy() {
124
+ if (this.peekBtn) {
125
+ this.peekBtn.removeEventListener('click', this._peekClickHandler);
126
+ this.peekBtn.removeEventListener('touchstart', this._peekTouchHandler);
127
+ this.peekBtn.remove();
128
+ this.peekBtn = null;
129
+ }
130
+
131
+ if (this.el) {
132
+ this.el.remove();
133
+ this.el = null;
134
+ }
135
+
136
+ document.body.style.overflow = this._savedOverflow;
137
+
138
+ this._peekClickHandler = null;
139
+ this._peekTouchHandler = null;
140
+ this.peekHidden = false;
141
+ }
142
+ }
@@ -0,0 +1,37 @@
1
+ import { gsap } from 'gsap';
2
+
3
+ /**
4
+ * Scroll helper: scrolls elements into view and scrolls content within containers.
5
+ */
6
+ export class Scroll {
7
+ /**
8
+ * @param {string} selector - CSS selector for the target element
9
+ */
10
+ scrollTo(selector) {
11
+ const el = document.querySelector(selector);
12
+ if (!el) return;
13
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
14
+ }
15
+
16
+ /**
17
+ * Animate the scroll position inside a scrollable container.
18
+ * @param {object} opts
19
+ * @param {string} opts.selector
20
+ * @param {number} [opts.to]
21
+ * @param {number} [opts.by]
22
+ * @param {number} [opts.duration=1]
23
+ */
24
+ scrollInside({ selector, to, by, duration = 1 }) {
25
+ const container = document.querySelector(selector);
26
+ if (!container) return;
27
+
28
+ const amount = to !== undefined ? to : (by || 300);
29
+ const target = to !== undefined ? amount : container.scrollTop + amount;
30
+
31
+ gsap.to(container, {
32
+ scrollTop: target,
33
+ duration,
34
+ ease: 'power2.inOut',
35
+ });
36
+ }
37
+ }