@hortonstudio/main 1.9.10 → 1.9.20

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 (124) hide show
  1. package/.prettierrc +8 -0
  2. package/README.md +146 -0
  3. package/eslint.config.js +32 -0
  4. package/index.ts +275 -0
  5. package/package.json +19 -2
  6. package/public/bootstrap.js +16 -0
  7. package/src/animations/animations.ts +93 -0
  8. package/src/animations/functions/counter/counter.ts +137 -0
  9. package/src/config.json +570 -0
  10. package/src/config.ts +105 -0
  11. package/src/modules/default/README.md +167 -0
  12. package/src/modules/default/default.ts +71 -0
  13. package/{autoInit → src/modules/default/functions}/accessibility/README.md +44 -12
  14. package/src/modules/default/functions/accessibility/accessibility.ts +54 -0
  15. package/src/modules/default/functions/accordion/README.md +451 -0
  16. package/src/modules/default/functions/accordion/accordion.ts +189 -0
  17. package/src/modules/default/functions/comparison/comparison.ts +424 -0
  18. package/src/modules/default/functions/marquee/marquee.ts +206 -0
  19. package/src/modules/default/functions/navbar/README.md +393 -0
  20. package/src/modules/default/functions/navbar/functions/arrow-navigation/arrow-navigation.ts +183 -0
  21. package/src/modules/default/functions/navbar/functions/dropdown/dropdown.ts +313 -0
  22. package/src/modules/default/functions/navbar/functions/menu/menu.ts +315 -0
  23. package/src/modules/default/functions/navbar/navbar.ts +51 -0
  24. package/{autoInit → src/modules/default/functions}/smooth-scroll/README.md +45 -14
  25. package/{autoInit/smooth-scroll/smooth-scroll.js → src/modules/default/functions/smooth-scroll/smooth-scroll.ts} +33 -38
  26. package/{autoInit → src/modules/default/functions}/transition/README.md +59 -32
  27. package/src/modules/default/functions/transition/transition.ts +290 -0
  28. package/src/modules/normalize/README.md +172 -0
  29. package/src/modules/normalize/functions/clickable/README.md +84 -0
  30. package/src/modules/normalize/functions/clickable/clickable.ts +43 -0
  31. package/src/modules/normalize/functions/clickable/functions/normalize/README.md +213 -0
  32. package/src/modules/normalize/functions/clickable/functions/normalize/normalize.ts +68 -0
  33. package/src/modules/normalize/functions/dupe/README.md +405 -0
  34. package/src/modules/normalize/functions/dupe/dupe.ts +197 -0
  35. package/src/modules/normalize/functions/sync/sync.ts +378 -0
  36. package/src/modules/normalize/normalize.ts +58 -0
  37. package/src/modules/structure/README.md +190 -0
  38. package/src/modules/structure/functions/form/README.md +94 -0
  39. package/src/modules/structure/functions/form/form.ts +54 -0
  40. package/src/modules/structure/functions/form/functions/honeypot/README.md +77 -0
  41. package/src/modules/structure/functions/form/functions/honeypot/honeypot.ts +37 -0
  42. package/src/modules/structure/functions/form/functions/range/README.md +410 -0
  43. package/src/modules/structure/functions/form/functions/range/range.ts +92 -0
  44. package/src/modules/structure/functions/form/functions/select/README.md +393 -0
  45. package/src/modules/structure/functions/form/functions/select/functions/custom-select/custom-select.ts +637 -0
  46. package/src/modules/structure/functions/form/functions/select/functions/states/states.ts +118 -0
  47. package/src/modules/structure/functions/form/functions/select/select.ts +48 -0
  48. package/src/modules/structure/functions/form/functions/test/test.ts +132 -0
  49. package/src/modules/structure/functions/pagination/README.md +527 -0
  50. package/src/modules/structure/functions/pagination/pagination.ts +493 -0
  51. package/src/modules/structure/functions/site-settings/README.md +395 -0
  52. package/src/modules/structure/functions/site-settings/site-settings.ts +158 -0
  53. package/{autoInit/accessibility → src/modules/structure}/functions/toc/README.md +18 -15
  54. package/{autoInit/accessibility/functions/toc/toc.js → src/modules/structure/functions/toc/functions/heading-links/heading-links.ts} +43 -63
  55. package/src/modules/structure/functions/toc/functions/progress-bar/progress-bar.ts +101 -0
  56. package/src/modules/structure/functions/toc/toc.ts +35 -0
  57. package/{autoInit/accessibility → src/modules/structure}/functions/year-replacement/README.md +7 -6
  58. package/src/modules/structure/functions/year-replacement/year-replacement.ts +59 -0
  59. package/src/modules/structure/structure.ts +59 -0
  60. package/src/utils/attributeSelector.ts +78 -0
  61. package/src/utils/cssVariables.ts +24 -0
  62. package/src/utils/gsap.ts +198 -0
  63. package/src/utils/heightAnimator.ts +130 -0
  64. package/src/utils/modalManager.ts +150 -0
  65. package/src/utils.ts +54 -0
  66. package/tsconfig.json +24 -0
  67. package/vite.config.js +45 -0
  68. package/.claude/settings.local.json +0 -70
  69. package/archive/hero.js +0 -794
  70. package/archive/modal.js +0 -80
  71. package/archive/text.js +0 -628
  72. package/autoInit/accessibility/accessibility.js +0 -53
  73. package/autoInit/accessibility/functions/blog-remover/README.md +0 -61
  74. package/autoInit/accessibility/functions/blog-remover/blog-remover.js +0 -31
  75. package/autoInit/accessibility/functions/click-forwarding/README.md +0 -60
  76. package/autoInit/accessibility/functions/click-forwarding/click-forwarding.js +0 -82
  77. package/autoInit/accessibility/functions/dropdown/README.md +0 -212
  78. package/autoInit/accessibility/functions/dropdown/dropdown.js +0 -167
  79. package/autoInit/accessibility/functions/list-accessibility/README.md +0 -56
  80. package/autoInit/accessibility/functions/list-accessibility/list-accessibility.js +0 -23
  81. package/autoInit/accessibility/functions/pagination/README.md +0 -428
  82. package/autoInit/accessibility/functions/pagination/pagination.js +0 -359
  83. package/autoInit/accessibility/functions/text-synchronization/README.md +0 -62
  84. package/autoInit/accessibility/functions/text-synchronization/text-synchronization.js +0 -101
  85. package/autoInit/accessibility/functions/year-replacement/year-replacement.js +0 -43
  86. package/autoInit/button/README.md +0 -122
  87. package/autoInit/button/button.js +0 -51
  88. package/autoInit/counter/README.md +0 -274
  89. package/autoInit/counter/counter.js +0 -185
  90. package/autoInit/form/README.md +0 -338
  91. package/autoInit/form/form.js +0 -374
  92. package/autoInit/navbar/README.md +0 -366
  93. package/autoInit/navbar/navbar.js +0 -786
  94. package/autoInit/site-settings/README.md +0 -218
  95. package/autoInit/site-settings/site-settings.js +0 -134
  96. package/autoInit/transition/transition.js +0 -116
  97. package/index.js +0 -305
  98. package/utils/before-after/README.md +0 -520
  99. package/utils/before-after/before-after.js +0 -653
  100. package/utils/css-animations/buttons/main/bgbasic/btn-main-bgbasic.html +0 -10
  101. package/utils/css-animations/buttons/main/bgfill/btn-main-bgfill.html +0 -29
  102. package/utils/css-animations/buttons/navbar/bgbasic/navbar-main-bgbasic.html +0 -17
  103. package/utils/css-animations/buttons/navbar/bgbasic/navbar-menu-bgbasic.html +0 -16
  104. package/utils/css-animations/buttons/navbar/bgfill/navbar-main-bgfill.html +0 -46
  105. package/utils/css-animations/buttons/navbar/bgfill/navbar-menu-bgfill.html +0 -39
  106. package/utils/css-animations/buttons/navbar/color/navbar-announce-color.html +0 -5
  107. package/utils/css-animations/buttons/navbar/color/navbar-main-color.html +0 -7
  108. package/utils/css-animations/buttons/navbar/color/navbar-menu-color.html +0 -7
  109. package/utils/css-animations/buttons/navbar/double-slide/navbar-announce-double-slide.html +0 -40
  110. package/utils/css-animations/buttons/navbar/double-slide/navbar-main-double-slide.html +0 -77
  111. package/utils/css-animations/buttons/navbar/scale/navbar-announce-scale.html +0 -6
  112. package/utils/css-animations/buttons/navbar/scale/navbar-main-scale.html +0 -9
  113. package/utils/css-animations/buttons/navbar/scale/navbar-menu-scale.html +0 -8
  114. package/utils/css-animations/buttons/navbar/underline/navbar-announce-underline.html +0 -32
  115. package/utils/css-animations/buttons/navbar/underline/navbar-main-underline.html +0 -56
  116. package/utils/css-animations/buttons/text/color/text-footer-color.html +0 -5
  117. package/utils/css-animations/buttons/text/color/text-main-color.html +0 -5
  118. package/utils/css-animations/buttons/text/double-slide/text-main-double-slide.html +0 -56
  119. package/utils/css-animations/buttons/text/scale/text-footer-scale.html +0 -6
  120. package/utils/css-animations/buttons/text/scale/text-main-scale.html +0 -6
  121. package/utils/css-animations/buttons/text/underline/text-footer-underline.html +0 -45
  122. package/utils/css-animations/buttons/text/underline/text-main-underline.html +0 -58
  123. package/utils/css-animations/cards/card-clickable.html +0 -11
  124. package/utils/css-animations/defaults.html +0 -69
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Page Transition Module
3
+ * Enhanced with namespace support and SPA/MPA compatibility
4
+ *
5
+ * Features:
6
+ * - Backward compatible with old single-trigger system
7
+ * - Namespace-specific transitions (data-hs-transition-namespace)
8
+ * - Separate exit/enter triggers with custom timing (optional)
9
+ * - MPA: Auto link interception (default)
10
+ * - SPA: Event-based integration (set data-hs-spa="true" on script tag)
11
+ *
12
+ * @version 2.0.0
13
+ */
14
+ import fullConfig from '@config';
15
+ import { querySelector, querySelectorAll, globalConfig } from '@utils';
16
+
17
+ const API_NAME = globalConfig.apiName;
18
+
19
+ export async function init(transitionConfig) {
20
+ const config = transitionConfig || fullConfig.default?.transition;
21
+
22
+ // Check if transition element exists - skip if not found
23
+ const transitionElement = querySelector(config, 'element');
24
+ if (!transitionElement) {
25
+ console.log('[transition] No transition element found, skipping initialization');
26
+ return {
27
+ result: 'transition skipped - no element found',
28
+ destroy: () => {},
29
+ };
30
+ }
31
+
32
+ // Get current namespace (if any)
33
+ const namespaceAttr = config.attributes.elements.namespace.primary;
34
+ const currentNamespace = transitionElement.getAttribute(namespaceAttr) || null;
35
+
36
+ // Check if running in SPA mode
37
+ const isSPA = window[API_NAME]?.settings?.isSPA || false;
38
+
39
+ // Store event handlers and timeouts for cleanup
40
+ const eventHandlers = new Map();
41
+ let delayTimeout = null;
42
+ let navTimeout = null;
43
+
44
+ const cleanup = () => {
45
+ // Clear any pending timeouts
46
+ if (delayTimeout) clearTimeout(delayTimeout);
47
+ if (navTimeout) clearTimeout(navTimeout);
48
+
49
+ // Remove all event listeners
50
+ eventHandlers.forEach((handler, event) => {
51
+ if (event === 'click' || event === 'pageshow' || event === 'resize') {
52
+ const target = event === 'click' ? document : window;
53
+ target.removeEventListener(event, handler);
54
+ } else {
55
+ window.removeEventListener(event, handler);
56
+ }
57
+ });
58
+ eventHandlers.clear();
59
+ };
60
+
61
+ /**
62
+ * Find trigger element for specific namespace and type (exit or enter)
63
+ * Falls back to default trigger (no namespace) if namespace-specific not found
64
+ * Also supports legacy single-trigger system for backward compatibility
65
+ */
66
+ const findTrigger = (type: 'exit' | 'enter', namespace: string | null) => {
67
+ // Try new system: separate exit/enter triggers
68
+ const allTriggers = querySelectorAll(config, type === 'exit' ? 'exitTrigger' : 'enterTrigger');
69
+
70
+ if (allTriggers.length > 0) {
71
+ // If namespace specified, try to find namespace-specific trigger first
72
+ if (namespace) {
73
+ const namespaceTrigger = Array.from(allTriggers).find(
74
+ (trigger) => trigger.getAttribute(namespaceAttr) === namespace
75
+ );
76
+ if (namespaceTrigger) return namespaceTrigger;
77
+ }
78
+
79
+ // Fall back to default trigger (no namespace attribute)
80
+ const defaultTrigger = Array.from(allTriggers).find(
81
+ (trigger) => !trigger.hasAttribute(namespaceAttr)
82
+ );
83
+
84
+ if (defaultTrigger) return defaultTrigger;
85
+ return allTriggers[0];
86
+ }
87
+
88
+ // Legacy fallback: look for old single trigger system
89
+ // This maintains backward compatibility with existing sites
90
+ const legacyTriggers = document.querySelectorAll('[data-hs-transition="trigger"]');
91
+ if (legacyTriggers.length > 0) {
92
+ // If namespace specified, try to find namespace-specific trigger
93
+ if (namespace) {
94
+ const namespaceTrigger = Array.from(legacyTriggers).find(
95
+ (trigger) => trigger.getAttribute(namespaceAttr) === namespace
96
+ );
97
+ if (namespaceTrigger) return namespaceTrigger;
98
+ }
99
+
100
+ // Fall back to first legacy trigger
101
+ return legacyTriggers[0];
102
+ }
103
+
104
+ return null;
105
+ };
106
+
107
+ /**
108
+ * Get timing value from trigger element or transition element (legacy)
109
+ */
110
+ const getTiming = (
111
+ trigger: Element | null,
112
+ type: 'time' | 'delay',
113
+ direction: 'exit' | 'enter'
114
+ ): number => {
115
+ if (!trigger) return 0;
116
+
117
+ // Try new system: timing attributes on trigger element
118
+ const attrName =
119
+ direction === 'exit'
120
+ ? type === 'time'
121
+ ? config.attributes.elements.exitTime.primary
122
+ : config.attributes.elements.exitDelay.primary
123
+ : type === 'time'
124
+ ? config.attributes.elements.enterTime.primary
125
+ : config.attributes.elements.enterDelay.primary;
126
+
127
+ let value = trigger.getAttribute(attrName);
128
+ if (value) return parseFloat(value) * 1000;
129
+
130
+ // Legacy fallback: timing on transition element
131
+ if (direction === 'exit' && type === 'time') {
132
+ value = transitionElement.getAttribute('data-hs-exit-time');
133
+ if (value) return parseFloat(value) * 1000;
134
+ }
135
+
136
+ if (direction === 'enter' && type === 'delay') {
137
+ value = transitionElement.getAttribute('data-hs-delay');
138
+ if (value) return parseFloat(value) * 1000;
139
+ }
140
+
141
+ return 0;
142
+ };
143
+
144
+ /**
145
+ * Play exit animation (when leaving page)
146
+ */
147
+ const playExitAnimation = (namespace: string | null = currentNamespace): Promise<void> => {
148
+ return new Promise((resolve) => {
149
+ const exitTrigger = findTrigger('exit', namespace);
150
+ if (!exitTrigger) {
151
+ resolve();
152
+ return;
153
+ }
154
+
155
+ const exitTime = getTiming(exitTrigger, 'time', 'exit');
156
+ const exitDelay = getTiming(exitTrigger, 'delay', 'exit');
157
+
158
+ const triggerAnimation = () => {
159
+ if (typeof Webflow !== 'undefined') {
160
+ Webflow.push(() => {
161
+ (exitTrigger as HTMLElement).click();
162
+ });
163
+ } else {
164
+ (exitTrigger as HTMLElement).click();
165
+ }
166
+
167
+ // Resolve after animation completes
168
+ setTimeout(resolve, exitTime);
169
+ };
170
+
171
+ if (exitDelay > 0) {
172
+ setTimeout(triggerAnimation, exitDelay);
173
+ } else {
174
+ triggerAnimation();
175
+ }
176
+ });
177
+ };
178
+
179
+ /**
180
+ * Play enter animation (when arriving on page)
181
+ */
182
+ const playEnterAnimation = (namespace: string | null = currentNamespace): Promise<void> => {
183
+ return new Promise((resolve) => {
184
+ const enterTrigger = findTrigger('enter', namespace);
185
+ if (!enterTrigger) {
186
+ resolve();
187
+ return;
188
+ }
189
+
190
+ const enterTime = getTiming(enterTrigger, 'time', 'enter');
191
+ const enterDelay = getTiming(enterTrigger, 'delay', 'enter');
192
+
193
+ // Check if first load (legacy behavior)
194
+ const isFirstLoad = !sessionStorage.getItem('transition-loaded');
195
+
196
+ const triggerAnimation = () => {
197
+ if (typeof Webflow !== 'undefined') {
198
+ Webflow.push(() => {
199
+ (enterTrigger as HTMLElement).click();
200
+ });
201
+ } else {
202
+ (enterTrigger as HTMLElement).click();
203
+ }
204
+
205
+ // Mark as loaded for session
206
+ if (isFirstLoad) {
207
+ sessionStorage.setItem('transition-loaded', 'true');
208
+ }
209
+
210
+ // Resolve after animation completes
211
+ setTimeout(resolve, enterTime);
212
+ };
213
+
214
+ // Only apply delay on first load (legacy behavior)
215
+ if (isFirstLoad && enterDelay > 0) {
216
+ delayTimeout = setTimeout(triggerAnimation, enterDelay);
217
+ } else {
218
+ triggerAnimation();
219
+ }
220
+ });
221
+ };
222
+
223
+ // Always listen for custom events (works for both MPA and SPA)
224
+ const handleExitEvent = () => playExitAnimation();
225
+ const handleEnterEvent = () => playEnterAnimation();
226
+
227
+ eventHandlers.set('hsmain:transition-exit', handleExitEvent);
228
+ eventHandlers.set('hsmain:transition-enter', handleEnterEvent);
229
+ window.addEventListener('hsmain:transition-exit', handleExitEvent);
230
+ window.addEventListener('hsmain:transition-enter', handleEnterEvent);
231
+
232
+ // MPA Mode: Intercept link clicks
233
+ if (!isSPA) {
234
+ const linkClickHandler = (e: MouseEvent) => {
235
+ const link = (e.target as Element).closest('a[href]') as HTMLAnchorElement;
236
+
237
+ if (
238
+ link &&
239
+ link.hostname === window.location.hostname &&
240
+ link.getAttribute('href') &&
241
+ link.getAttribute('href')!.indexOf('#') === -1 &&
242
+ link.getAttribute('target') !== '_blank'
243
+ ) {
244
+ e.preventDefault();
245
+ const href = link.href;
246
+
247
+ // Play exit animation, then navigate
248
+ playExitAnimation().then(() => {
249
+ window.location.href = href;
250
+ });
251
+ }
252
+ };
253
+
254
+ eventHandlers.set('click', linkClickHandler);
255
+ document.addEventListener('click', linkClickHandler);
256
+
257
+ // Handle back button navigation
258
+ const pageShowHandler = (event: PageTransitionEvent) => {
259
+ if (event.persisted) {
260
+ window.location.reload();
261
+ }
262
+ };
263
+
264
+ eventHandlers.set('pageshow', pageShowHandler);
265
+ window.addEventListener('pageshow', pageShowHandler);
266
+ }
267
+
268
+ // Hide transition on window resize (prevents visual glitches)
269
+ const resizeHandler = () => {
270
+ if (transitionElement) {
271
+ (transitionElement as HTMLElement).style.display = 'none';
272
+ }
273
+ };
274
+
275
+ eventHandlers.set('resize', resizeHandler);
276
+ window.addEventListener('resize', resizeHandler);
277
+
278
+ // On DOM ready, play enter animation
279
+ const domReadyHandler = () => {
280
+ playEnterAnimation();
281
+ };
282
+
283
+ eventHandlers.set(`${API_NAME}:dom-ready`, domReadyHandler);
284
+ window.addEventListener(`${API_NAME}:dom-ready`, domReadyHandler, { once: true });
285
+
286
+ return {
287
+ result: 'transition initialized',
288
+ destroy: cleanup,
289
+ };
290
+ }
@@ -0,0 +1,172 @@
1
+ # **Normalize System Documentation**
2
+
3
+ ## **Overview**
4
+
5
+ The normalize module provides functions that modify DOM structure before any visual changes occur. These are critical functions that run in Phase 1 (normalize) of the phased initialization system.
6
+
7
+ **Purpose**: Ensure DOM structure is normalized and consistent before visual-dom modules run, preventing layout shifts and ensuring reliable selectors.
8
+
9
+ **Phase**: Normalize (Phase 1) - runs first, before all other modules
10
+
11
+ ---
12
+
13
+ ## **Functions**
14
+
15
+ ### **1. Clickable**
16
+
17
+ Normalizes clickable wrappers by keeping either `<button>` or `<a>` element based on href validity.
18
+
19
+ **Use case:** Designer includes both button and link in wrapper. Script automatically keeps the semantically correct one based on whether there's a valid link destination.
20
+
21
+ **Sub-functions:**
22
+
23
+ - **normalize**: Removes duplicate button/link elements, keeps the correct one
24
+
25
+ **Performance:** ~30-50ms for 100 wrappers (with 1s timeout safety)
26
+
27
+ ---
28
+
29
+ ## **Documentation**
30
+
31
+ Each function has detailed documentation in its respective folder:
32
+
33
+ - `functions/clickable/README.md`
34
+ - `functions/clickable/functions/normalize/README.md`
35
+
36
+ ---
37
+
38
+ ## **How It Works**
39
+
40
+ The normalize system uses a dynamic loader pattern:
41
+
42
+ 1. Main `normalize.js` imports all function modules
43
+ 2. Each function is loaded in parallel via `Promise.allSettled()`
44
+ 3. All destroy functions are collected for cleanup
45
+ 4. On destroy, all functions are cleaned up properly
46
+
47
+ **Initialization Flow**:
48
+
49
+ ```
50
+ Page Load
51
+
52
+ Phase 1: Normalize (normalize module) ← WE ARE HERE
53
+
54
+ Phase 2: Visual-DOM (structure + form)
55
+
56
+ Phase 3: Transition reveals page
57
+
58
+ Phase 4: Default modules (accessibility, navbar, etc.)
59
+ ```
60
+
61
+ ---
62
+
63
+ ## **Phased Initialization**
64
+
65
+ ### **Why Phase 1?**
66
+
67
+ These functions modify the DOM structure in fundamental ways:
68
+
69
+ - Clickable removes elements from DOM
70
+ - Other normalize functions might add/remove/restructure elements
71
+ - Must happen BEFORE visual modules select elements
72
+
73
+ **Without phasing**: Visual modules might select wrong elements or fail to find elements
74
+
75
+ **With phasing**: DOM structure is normalized → visual modules always find correct elements
76
+
77
+ ### **Phase Characteristics**
78
+
79
+ - **Timing**: Runs first, before all other modules
80
+ - **Parallelization**: All functions load/execute in parallel within Phase 1
81
+ - **Resilience**: Uses `Promise.allSettled` - individual failures don't block phase
82
+ - **Timeout**: 1s timeout per module (safety net for stuck DOM operations)
83
+
84
+ ---
85
+
86
+ ## **Barba.js / SPA Compatibility**
87
+
88
+ The normalize system is fully compatible with Barba.js and other SPA frameworks:
89
+
90
+ ### **v2.0.0 Improvements:**
91
+
92
+ - **Resilient loading with Promise.allSettled** - Individual function failures won't break other functions
93
+ - **Graceful error handling** - Failed functions are logged but don't prevent successful ones from working
94
+ - **Complete cleanup on destroy** - All functions properly cleaned up for page transitions
95
+ - **Phased initialization** - Same clean flow on page load and SPA transitions
96
+
97
+ ### **Resilient Loading:**
98
+
99
+ - If one function fails (missing element, error, etc.), others continue working
100
+ - Logs helpful warnings showing which functions succeeded/failed
101
+ - System remains functional even with partial failures
102
+
103
+ ### **On Destroy:**
104
+
105
+ 1. Calls destroy() on all successfully loaded functions
106
+ 2. Errors during cleanup are caught and logged
107
+ 3. Cleanup tracking arrays are reset
108
+ 4. Safe for Barba.js page transitions
109
+
110
+ ### **On Reinitialize:**
111
+
112
+ 1. Attempts to load all functions again
113
+ 2. Works like fresh page load on new DOM
114
+ 3. Independent function loading ensures maximum resilience
115
+ 4. Runs in same Phase 1 timing for consistent UX
116
+
117
+ ---
118
+
119
+ ## **Performance**
120
+
121
+ **Expected load time**: ~20-50ms total
122
+
123
+ Individual timing varies by function:
124
+
125
+ - Clickable normalization: ~30-50ms (DOM manipulation for 100 elements)
126
+
127
+ **Total Phase 1 timing**: All normalize functions in parallel, typically completes in 30-50ms
128
+
129
+ **1s timeout**: Safety net for stuck operations, ensures Phase 2 starts even if normalize hangs
130
+
131
+ ---
132
+
133
+ ## **Adding New Normalize Functions**
134
+
135
+ To add a new normalize function:
136
+
137
+ 1. **Create function folder:**
138
+
139
+ ```
140
+ autoInit/normalize/functions/my-function/
141
+ my-function.js
142
+ config.js
143
+ README.md
144
+ ```
145
+
146
+ 2. **Update normalize.js functionMap:**
147
+
148
+ ```javascript
149
+ const functionMap = {
150
+ clickable: () => import('./functions/clickable/clickable.js'),
151
+ 'my-function': () => import('./functions/my-function/my-function.js'),
152
+ };
153
+ ```
154
+
155
+ 3. **Follow the pattern:**
156
+ - Export `init()` function that returns `{ result, destroy }`
157
+ - Export `version` constant
158
+ - Use Promise.allSettled for resilient loading
159
+ - Clean up properly in destroy()
160
+
161
+ ---
162
+
163
+ ## **Notes**
164
+
165
+ - All functions auto-initialize on page load
166
+ - Each function operates independently
167
+ - Barba.js compatible with proper cleanup
168
+ - No configuration required at normalize level - each function uses its own config
169
+ - Runs in Phase 1 (normalize) of initialization system
170
+ - Uses Promise.allSettled for maximum reliability
171
+ - Part of v2.0.0 phased initialization architecture
172
+ - 1s timeout per module ensures Phase 2 starts even if normalize hangs
@@ -0,0 +1,84 @@
1
+ # **Clickable System Documentation**
2
+
3
+ ## **Overview**
4
+
5
+ The clickable system provides modular functions to manage clickable elements and their behaviors. Each function operates independently and can be customized through data attributes.
6
+
7
+ **Note:** This module auto-initializes and loads all functions on page load.
8
+
9
+ ---
10
+
11
+ ## **Functions**
12
+
13
+ ### **1. Normalize**
14
+
15
+ Normalizes clickable wrappers by keeping either `<button>` or `<a>` element based on href validity.
16
+
17
+ **Use case:** Designer includes both button and link in wrapper. Script automatically keeps the semantically correct one based on whether there's a valid link destination.
18
+
19
+ **How it works:**
20
+
21
+ - If `<a>` has valid href (not empty, not "#") → keep link, remove button
22
+ - Otherwise → keep button, remove link
23
+ - Kept element gets `data-site-clickable="button"` for reliable selection by other modules
24
+
25
+ **Performance:** ~30-50ms for 100 wrappers
26
+
27
+ ---
28
+
29
+ ## **Documentation**
30
+
31
+ Each function has detailed documentation in its respective folder:
32
+
33
+ - `functions/normalize/README.md`
34
+
35
+ ---
36
+
37
+ ## **How It Works**
38
+
39
+ The clickable system uses a dynamic loader pattern:
40
+
41
+ 1. Main `clickable.js` imports all function modules
42
+ 2. Each function is loaded in parallel via `Promise.allSettled()`
43
+ 3. All destroy functions are collected for cleanup
44
+ 4. On destroy, all functions are cleaned up properly
45
+
46
+ ---
47
+
48
+ ## **Barba.js / SPA Compatibility**
49
+
50
+ The clickable system is fully compatible with Barba.js and other SPA frameworks:
51
+
52
+ ### **v2.0.0 Improvements:**
53
+
54
+ - **Resilient loading with Promise.allSettled** - Individual function failures won't break other functions
55
+ - **Graceful error handling** - Failed functions are logged but don't prevent successful ones from working
56
+ - **Complete cleanup on destroy** - All functions properly cleaned up for page transitions
57
+
58
+ ### **Resilient Loading:**
59
+
60
+ - If one clickable function fails (missing element, error, etc.), others continue working
61
+ - Logs helpful warnings showing which functions succeeded/failed
62
+ - System remains functional even with partial failures
63
+
64
+ ### **On Destroy:**
65
+
66
+ 1. Calls destroy() on all successfully loaded functions
67
+ 2. Errors during cleanup are caught and logged
68
+ 3. Cleanup tracking arrays are reset
69
+ 4. Safe for Barba.js page transitions
70
+
71
+ ### **On Reinitialize:**
72
+
73
+ 1. Attempts to load all functions again
74
+ 2. Works like fresh page load on new DOM
75
+ 3. Independent function loading ensures maximum resilience
76
+
77
+ ---
78
+
79
+ ## **Notes**
80
+
81
+ - All functions auto-initialize on page load
82
+ - Each function operates independently
83
+ - Barba.js compatible with proper cleanup
84
+ - No configuration required - works with data attributes
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Clickable Orchestrator
3
+ * Manages clickable wrapper normalization
4
+ *
5
+ * Uses static imports and passes config down to functions
6
+ * @version 2.0.0
7
+ */
8
+ import { init as normalizeInit } from './functions/normalize/normalize.ts';
9
+
10
+ export async function init(clickableConfig) {
11
+ const cleanup = { destroyFunctions: [] };
12
+
13
+ try {
14
+ // Load normalize function
15
+ const normalizeResult = await normalizeInit(clickableConfig.normalize);
16
+ if (normalizeResult?.destroy) cleanup.destroyFunctions.push(normalizeResult.destroy);
17
+
18
+ return {
19
+ result: 'clickable initialized',
20
+ destroy: () => {
21
+ cleanup.destroyFunctions.forEach((destroyFn) => {
22
+ try {
23
+ destroyFn();
24
+ } catch (error) {
25
+ console.error('[clickable] Error during cleanup:', error);
26
+ }
27
+ });
28
+ cleanup.destroyFunctions.length = 0;
29
+ },
30
+ };
31
+ } catch (error) {
32
+ console.error('[clickable] Initialization failed:', error);
33
+ // Cleanup any partial initialization
34
+ cleanup.destroyFunctions.forEach((fn) => {
35
+ try {
36
+ fn();
37
+ } catch (cleanupError) {
38
+ console.error('[clickable] Error during error cleanup:', cleanupError);
39
+ }
40
+ });
41
+ throw error;
42
+ }
43
+ }