@hortonstudio/main 1.9.11 → 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 (120) 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/{autoInit/accessibility → src/modules/structure}/functions/pagination/README.md +147 -72
  50. package/{autoInit/accessibility/functions/pagination/pagination.js → src/modules/structure/functions/pagination/pagination.ts} +98 -50
  51. package/{autoInit → src/modules/structure/functions}/site-settings/README.md +57 -27
  52. package/{autoInit/site-settings/site-settings.js → src/modules/structure/functions/site-settings/site-settings.ts} +36 -32
  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/text-synchronization/README.md +0 -62
  82. package/autoInit/accessibility/functions/text-synchronization/text-synchronization.js +0 -101
  83. package/autoInit/accessibility/functions/year-replacement/year-replacement.js +0 -43
  84. package/autoInit/button/README.md +0 -122
  85. package/autoInit/button/button.js +0 -51
  86. package/autoInit/counter/README.md +0 -274
  87. package/autoInit/counter/counter.js +0 -185
  88. package/autoInit/form/README.md +0 -338
  89. package/autoInit/form/form.js +0 -374
  90. package/autoInit/navbar/README.md +0 -366
  91. package/autoInit/navbar/navbar.js +0 -786
  92. package/autoInit/transition/transition.js +0 -116
  93. package/index.js +0 -305
  94. package/utils/before-after/README.md +0 -520
  95. package/utils/before-after/before-after.js +0 -653
  96. package/utils/css-animations/buttons/main/bgbasic/btn-main-bgbasic.html +0 -10
  97. package/utils/css-animations/buttons/main/bgfill/btn-main-bgfill.html +0 -29
  98. package/utils/css-animations/buttons/navbar/bgbasic/navbar-main-bgbasic.html +0 -17
  99. package/utils/css-animations/buttons/navbar/bgbasic/navbar-menu-bgbasic.html +0 -16
  100. package/utils/css-animations/buttons/navbar/bgfill/navbar-main-bgfill.html +0 -46
  101. package/utils/css-animations/buttons/navbar/bgfill/navbar-menu-bgfill.html +0 -39
  102. package/utils/css-animations/buttons/navbar/color/navbar-announce-color.html +0 -5
  103. package/utils/css-animations/buttons/navbar/color/navbar-main-color.html +0 -7
  104. package/utils/css-animations/buttons/navbar/color/navbar-menu-color.html +0 -7
  105. package/utils/css-animations/buttons/navbar/double-slide/navbar-announce-double-slide.html +0 -40
  106. package/utils/css-animations/buttons/navbar/double-slide/navbar-main-double-slide.html +0 -77
  107. package/utils/css-animations/buttons/navbar/scale/navbar-announce-scale.html +0 -6
  108. package/utils/css-animations/buttons/navbar/scale/navbar-main-scale.html +0 -9
  109. package/utils/css-animations/buttons/navbar/scale/navbar-menu-scale.html +0 -8
  110. package/utils/css-animations/buttons/navbar/underline/navbar-announce-underline.html +0 -32
  111. package/utils/css-animations/buttons/navbar/underline/navbar-main-underline.html +0 -56
  112. package/utils/css-animations/buttons/text/color/text-footer-color.html +0 -5
  113. package/utils/css-animations/buttons/text/color/text-main-color.html +0 -5
  114. package/utils/css-animations/buttons/text/double-slide/text-main-double-slide.html +0 -56
  115. package/utils/css-animations/buttons/text/scale/text-footer-scale.html +0 -6
  116. package/utils/css-animations/buttons/text/scale/text-main-scale.html +0 -6
  117. package/utils/css-animations/buttons/text/underline/text-footer-underline.html +0 -45
  118. package/utils/css-animations/buttons/text/underline/text-main-underline.html +0 -58
  119. package/utils/css-animations/cards/card-clickable.html +0 -11
  120. package/utils/css-animations/defaults.html +0 -69
@@ -5,35 +5,55 @@
5
5
  * Supports infinite looping, responsive layouts, and accessibility.
6
6
  */
7
7
 
8
- export function init() {
8
+ import { querySelectorAll, querySelector, getSelector, globalConfig, cssVariables } from '@utils';
9
+
10
+ // Module-scoped config (set during init)
11
+ let moduleConfig = null;
12
+
13
+ export function init(config) {
14
+ // Store config at module scope for helper functions
15
+ moduleConfig = config;
16
+
9
17
  const cleanup = {
10
18
  observers: [],
11
- handlers: []
19
+ handlers: [],
20
+ liveRegions: [],
12
21
  };
13
22
 
14
23
  // Initialize all pagination containers
15
24
  try {
16
- document.querySelectorAll('[data-hs-pagination="wrapper"]')
17
- .forEach(container => {
18
- try {
19
- const instance = initPaginationInstance(container, cleanup);
20
- if (!instance) {
21
- console.warn('[hs-pagination] Failed to initialize container', container);
22
- }
23
- } catch (error) {
24
- console.error('[hs-pagination] Error initializing container:', error, container);
25
+ querySelectorAll(config, 'wrapper').forEach((container) => {
26
+ try {
27
+ const instance = initPaginationInstance(container, cleanup);
28
+ if (!instance) {
29
+ console.warn('[hs-pagination] Failed to initialize container', container);
25
30
  }
26
- });
31
+ } catch (error) {
32
+ console.error('[hs-pagination] Error initializing container:', error, container);
33
+ }
34
+ });
27
35
  } catch (error) {
28
36
  console.error('[hs-pagination] Critical error during initialization:', error);
29
37
  }
30
38
 
31
39
  return {
32
- result: "pagination initialized",
40
+ result: 'pagination initialized',
33
41
  destroy: () => {
34
42
  try {
43
+ // Remove all live regions
44
+ cleanup.liveRegions.forEach((liveRegion) => {
45
+ try {
46
+ if (liveRegion.parentNode) {
47
+ liveRegion.parentNode.removeChild(liveRegion);
48
+ }
49
+ } catch (error) {
50
+ console.error('[hs-pagination] Error removing live region:', error);
51
+ }
52
+ });
53
+ cleanup.liveRegions.length = 0;
54
+
35
55
  // Disconnect all observers
36
- cleanup.observers.forEach(obs => {
56
+ cleanup.observers.forEach((obs) => {
37
57
  try {
38
58
  obs.disconnect();
39
59
  } catch (error) {
@@ -51,15 +71,18 @@ export function init() {
51
71
  }
52
72
  });
53
73
  cleanup.handlers.length = 0;
74
+
75
+ // Clear module config
76
+ moduleConfig = null;
54
77
  } catch (error) {
55
78
  console.error('[hs-pagination] Critical error during cleanup:', error);
56
79
  }
57
- }
80
+ },
58
81
  };
59
82
  }
60
83
 
61
84
  function initPaginationInstance(container, cleanup) {
62
- const list = container.querySelector('[data-hs-pagination="list"]');
85
+ const list = querySelector(moduleConfig, 'list', container);
63
86
  if (!list) {
64
87
  console.warn('[hs-pagination] Missing required element: data-hs-pagination="list"');
65
88
  return null;
@@ -69,9 +92,9 @@ function initPaginationInstance(container, cleanup) {
69
92
  if (!wrapper) return null;
70
93
 
71
94
  const elements = {
72
- controls: container.querySelector('[data-hs-pagination="controls"]'),
73
- counter: container.querySelector('[data-hs-pagination="counter"]'),
74
- dotsWrap: container.querySelector('[data-hs-pagination="dots"]')
95
+ controls: querySelector(moduleConfig, 'controls', container),
96
+ counter: querySelector(moduleConfig, 'counter', container),
97
+ dotsWrap: querySelector(moduleConfig, 'dots', container),
75
98
  };
76
99
 
77
100
  // Early exit for infinite mode - no controls means no pagination
@@ -79,11 +102,13 @@ function initPaginationInstance(container, cleanup) {
79
102
  return { initialized: false };
80
103
  }
81
104
 
82
- // Find next/previous buttons using data-site-clickable pattern
83
- const nextClickable = container.querySelector('[data-hs-pagination="next"]');
84
- const prevClickable = container.querySelector('[data-hs-pagination="previous"]');
85
- const nextBtn = nextClickable?.children[0];
86
- const prevBtn = prevClickable?.children[0];
105
+ // Find next/previous buttons using clickable pattern
106
+ const nextClickable = querySelector(moduleConfig, 'next', container);
107
+ const prevClickable = querySelector(moduleConfig, 'previous', container);
108
+
109
+ const clickableSelector = getSelector(globalConfig.clickable, 'button');
110
+ const nextBtn = nextClickable?.querySelector(clickableSelector) || nextClickable;
111
+ const prevBtn = prevClickable?.querySelector(clickableSelector) || prevClickable;
87
112
 
88
113
  if (!nextBtn || !prevBtn) {
89
114
  console.warn('[hs-pagination] Missing required navigation buttons');
@@ -103,11 +128,12 @@ function initPaginationInstance(container, cleanup) {
103
128
 
104
129
  // Parse configuration from new attributes
105
130
  const desktopItems = parseInt(elements.controls?.getAttribute('data-hs-pagination-show')) || 6;
106
- const mobileItems = parseInt(elements.controls?.getAttribute('data-hs-pagination-show-mobile')) || desktopItems;
131
+ const mobileItems =
132
+ parseInt(elements.controls?.getAttribute('data-hs-pagination-show-mobile')) || desktopItems;
107
133
 
108
134
  const isMobileLayout = () => {
109
- const breakpoint = getComputedStyle(list).getPropertyValue('--data-hs-break').trim().replace(/"/g, '');
110
- return breakpoint === 'mobile';
135
+ const stateValue = getComputedStyle(list).getPropertyValue(cssVariables.state).trim();
136
+ return stateValue === globalConfig.cssVars.state.values.active;
111
137
  };
112
138
 
113
139
  const allItems = Array.from(list.children);
@@ -120,7 +146,7 @@ function initPaginationInstance(container, cleanup) {
120
146
  currentPage: 1,
121
147
  isAnimating: false,
122
148
  itemsPerPage: desktopItems,
123
- dotTemplates: { active: null, inactive: null }
149
+ dotTemplates: { active: null, inactive: null },
124
150
  };
125
151
  let wrapperChildren = [];
126
152
 
@@ -129,8 +155,10 @@ function initPaginationInstance(container, cleanup) {
129
155
  liveRegion.className = 'sr-only';
130
156
  liveRegion.setAttribute('aria-live', 'assertive');
131
157
  liveRegion.setAttribute('aria-atomic', 'true');
132
- liveRegion.style.cssText = 'position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden;';
158
+ liveRegion.style.cssText =
159
+ 'position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden;';
133
160
  container.appendChild(liveRegion);
161
+ cleanup.liveRegions.push(liveRegion);
134
162
 
135
163
  const updateCounter = () => {
136
164
  if (elements.counter) {
@@ -140,7 +168,7 @@ function initPaginationInstance(container, cleanup) {
140
168
 
141
169
  const announcePageChange = () => {
142
170
  liveRegion.textContent = `Page ${state.currentPage} of ${state.totalPages}`;
143
- setTimeout(() => liveRegion.textContent = '', 1000);
171
+ setTimeout(() => (liveRegion.textContent = ''), 1000);
144
172
  };
145
173
 
146
174
  const manageFocus = () => {
@@ -164,12 +192,16 @@ function initPaginationInstance(container, cleanup) {
164
192
  if (!existingDots.length) return;
165
193
 
166
194
  // Identify active and inactive templates
167
- const activeDot = existingDots.find(dot => dot.classList.contains('is-active'));
168
- const inactiveDot = existingDots.find(dot => !dot.classList.contains('is-active'));
195
+ const activeDot = existingDots.find((dot) => dot.classList.contains('is-active'));
196
+ const inactiveDot = existingDots.find((dot) => !dot.classList.contains('is-active'));
169
197
 
170
198
  // Store templates (use same template for both if only one exists)
171
- state.dotTemplates.active = activeDot ? activeDot.cloneNode(true) : existingDots[0].cloneNode(true);
172
- state.dotTemplates.inactive = inactiveDot ? inactiveDot.cloneNode(true) : existingDots[0].cloneNode(true);
199
+ state.dotTemplates.active = activeDot
200
+ ? activeDot.cloneNode(true)
201
+ : existingDots[0].cloneNode(true);
202
+ state.dotTemplates.inactive = inactiveDot
203
+ ? inactiveDot.cloneNode(true)
204
+ : existingDots[0].cloneNode(true);
173
205
 
174
206
  // Clear existing dots
175
207
  elements.dotsWrap.innerHTML = '';
@@ -180,7 +212,9 @@ function initPaginationInstance(container, cleanup) {
180
212
 
181
213
  // Create dots for each page
182
214
  for (let i = 1; i <= state.totalPages; i++) {
183
- const dot = (i === 1 ? state.dotTemplates.active : state.dotTemplates.inactive).cloneNode(true);
215
+ const dot = (i === 1 ? state.dotTemplates.active : state.dotTemplates.inactive).cloneNode(
216
+ true
217
+ );
184
218
 
185
219
  // Add accessibility attributes
186
220
  dot.setAttribute('role', 'button');
@@ -244,11 +278,11 @@ function initPaginationInstance(container, cleanup) {
244
278
  }
245
279
 
246
280
  // Clean up previous page lists
247
- Array.from(wrapper.children).forEach(child => {
281
+ Array.from(wrapper.children).forEach((child) => {
248
282
  if (child !== list) wrapper.removeChild(child);
249
283
  });
250
284
  list.innerHTML = '';
251
- allItems.forEach(item => list.appendChild(item));
285
+ allItems.forEach((item) => list.appendChild(item));
252
286
 
253
287
  // Single page case
254
288
  if (state.totalPages <= 1) {
@@ -282,18 +316,20 @@ function initPaginationInstance(container, cleanup) {
282
316
  const pageList = list.cloneNode(false);
283
317
  const startIndex = page * state.itemsPerPage;
284
318
  const endIndex = Math.min(startIndex + state.itemsPerPage, totalItems);
285
- allItems.slice(startIndex, endIndex).forEach(item => pageList.appendChild(item.cloneNode(true)));
319
+ allItems
320
+ .slice(startIndex, endIndex)
321
+ .forEach((item) => pageList.appendChild(item.cloneNode(true)));
286
322
  return pageList;
287
323
  });
288
324
 
289
325
  // Insert cloned pages for infinite loop
290
326
  wrapper.insertBefore(pageLists[pageLists.length - 1].cloneNode(true), list);
291
- pageLists.slice(1).forEach(page => wrapper.appendChild(page));
327
+ pageLists.slice(1).forEach((page) => wrapper.appendChild(page));
292
328
  wrapper.appendChild(pageLists[0].cloneNode(true));
293
329
 
294
330
  // Populate original list with first page
295
331
  list.innerHTML = '';
296
- Array.from(pageLists[0].children).forEach(item => list.appendChild(item));
332
+ Array.from(pageLists[0].children).forEach((item) => list.appendChild(item));
297
333
 
298
334
  Object.assign(state, { currentIndex: 1, currentPage: 1, isAnimating: false });
299
335
  wrapperChildren = Array.from(wrapper.children);
@@ -341,14 +377,14 @@ function initPaginationInstance(container, cleanup) {
341
377
 
342
378
  if (isMobileLayout() && elements.controls) {
343
379
  setTimeout(() => {
344
- const controlsBottom = elements.controls.getBoundingClientRect().bottom + window.pageYOffset;
380
+ const controlsBottom =
381
+ elements.controls.getBoundingClientRect().bottom + window.pageYOffset;
345
382
  const clearance = 5 * 16; // 5rem in pixels
346
383
  const targetScrollPosition = controlsBottom - window.innerHeight + clearance;
347
384
  window.scrollTo({ top: targetScrollPosition, behavior: 'smooth' });
348
385
  }, 50);
349
386
  }
350
387
 
351
- wrapper.style.transition = 'transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
352
388
  wrapper.style.transform = `translateX(${-state.currentIndex * 100}%)`;
353
389
 
354
390
  let transitionTimeout = null;
@@ -356,7 +392,6 @@ function initPaginationInstance(container, cleanup) {
356
392
  const handleTransitionEnd = () => {
357
393
  clearTimeout(transitionTimeout);
358
394
  wrapper.removeEventListener('transitionend', handleTransitionEnd);
359
- wrapper.style.transition = '';
360
395
 
361
396
  updateCounter();
362
397
  announcePageChange();
@@ -377,8 +412,12 @@ function initPaginationInstance(container, cleanup) {
377
412
  state.isAnimating = true;
378
413
  state.currentIndex += direction;
379
414
 
380
- state.currentPage = state.currentIndex > state.totalPages ? 1 :
381
- state.currentIndex < 1 ? state.totalPages : state.currentIndex;
415
+ state.currentPage =
416
+ state.currentIndex > state.totalPages
417
+ ? 1
418
+ : state.currentIndex < 1
419
+ ? state.totalPages
420
+ : state.currentIndex;
382
421
 
383
422
  updateCounter();
384
423
  announcePageChange();
@@ -387,14 +426,14 @@ function initPaginationInstance(container, cleanup) {
387
426
 
388
427
  if (isMobileLayout() && elements.controls) {
389
428
  setTimeout(() => {
390
- const controlsBottom = elements.controls.getBoundingClientRect().bottom + window.pageYOffset;
429
+ const controlsBottom =
430
+ elements.controls.getBoundingClientRect().bottom + window.pageYOffset;
391
431
  const clearance = 5 * 16; // 5rem in pixels
392
432
  const targetScrollPosition = controlsBottom - window.innerHeight + clearance;
393
433
  window.scrollTo({ top: targetScrollPosition, behavior: 'smooth' });
394
434
  }, 50);
395
435
  }
396
436
 
397
- wrapper.style.transition = 'transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
398
437
  wrapper.style.transform = `translateX(${-state.currentIndex * 100}%)`;
399
438
 
400
439
  let transitionTimeout = null;
@@ -402,16 +441,27 @@ function initPaginationInstance(container, cleanup) {
402
441
  const handleTransitionEnd = () => {
403
442
  clearTimeout(transitionTimeout);
404
443
  wrapper.removeEventListener('transitionend', handleTransitionEnd);
405
- wrapper.style.transition = '';
406
444
 
407
445
  if (state.currentIndex > state.totalPages) {
408
446
  state.currentIndex = 1;
409
447
  state.currentPage = 1;
448
+ // Disable transition for instant jump
449
+ wrapper.style.transition = 'none';
410
450
  wrapper.style.transform = 'translateX(-100%)';
451
+ // Force reflow to apply the instant jump
452
+ wrapper.offsetHeight;
453
+ // Re-enable CSS transition
454
+ wrapper.style.transition = '';
411
455
  } else if (state.currentIndex < 1) {
412
456
  state.currentIndex = state.totalPages;
413
457
  state.currentPage = state.totalPages;
458
+ // Disable transition for instant jump
459
+ wrapper.style.transition = 'none';
414
460
  wrapper.style.transform = `translateX(${-state.totalPages * 100}%)`;
461
+ // Force reflow to apply the instant jump
462
+ wrapper.offsetHeight;
463
+ // Re-enable CSS transition
464
+ wrapper.style.transition = '';
415
465
  }
416
466
 
417
467
  updateCounter();
@@ -441,5 +491,3 @@ function initPaginationInstance(container, cleanup) {
441
491
 
442
492
  return { initialized: true };
443
493
  }
444
-
445
- export const version = "1.0.0";
@@ -10,20 +10,20 @@ The site settings system collects dynamic values from designated elements and re
10
10
 
11
11
  ### **Required Elements**
12
12
 
13
- **Settings Wrapper** *(container for all settings lists)*
13
+ **Settings Wrapper** _(container for all settings lists)_
14
14
 
15
- * data-site-settings-element="wrapper"
15
+ - data-site-settings-element="wrapper"
16
16
 
17
- **Settings List** *(can have multiple lists inside wrapper)*
17
+ **Settings List** _(can have multiple lists inside wrapper)_
18
18
 
19
- * data-site-settings-element="list"
19
+ - data-site-settings-element="list"
20
20
 
21
- **Setting Element** *(individual setting with a name and value)*
21
+ **Setting Element** _(individual setting with a name and value)_
22
22
 
23
- * data-site-settings="name"
24
- * The attribute value is the placeholder name (e.g., "company", "email", "phone")
25
- * The element's text content is used for text replacements
26
- * The element's href attribute (if present) is used for link replacements
23
+ - data-site-settings="name"
24
+ - The attribute value is the placeholder name (e.g., "company", "email", "phone")
25
+ - The element's text content is used for text replacements
26
+ - The element's href attribute (if present) is used for link replacements
27
27
 
28
28
  **Typical element layout:**
29
29
 
@@ -48,18 +48,20 @@ The site settings system collects dynamic values from designated elements and re
48
48
  ### **Text vs Href Behavior**
49
49
 
50
50
  **Text replacement** (in page content):
51
+
51
52
  - **First choice:** Use text value
52
53
  - **Fallback:** Use href value if no text
53
54
 
54
55
  **Href replacement** (in link attributes):
56
+
55
57
  - **With dedicated href:** Completely replace entire href (fixes Webflow prefix issue)
56
58
  - **Without dedicated href:** Normal placeholder replacement with text value
57
59
 
58
60
  ### **When It Runs**
59
61
 
60
- * Runs once on page load
61
- * Runs again on Barba.js page transitions (via `reinitialize()`)
62
- * No ongoing monitoring or event listeners
62
+ - Runs once on page load
63
+ - Runs again on Barba.js page transitions (via `reinitialize()`)
64
+ - No ongoing monitoring or event listeners
63
65
 
64
66
  ---
65
67
 
@@ -67,11 +69,11 @@ The site settings system collects dynamic values from designated elements and re
67
69
 
68
70
  ### **Optional Hide Attribute**
69
71
 
70
- **Hide Element** *(removes content conditionally before extracting text)*
72
+ **Hide Element** _(removes content conditionally before extracting text)_
71
73
 
72
- * data-site-settings-hide="class-name"
73
- * Can specify multiple classes separated by spaces
74
- * Element is removed if it has ANY of the specified classes
74
+ - data-site-settings-hide="class-name"
75
+ - Can specify multiple classes separated by spaces
76
+ - Element is removed if it has ANY of the specified classes
75
77
 
76
78
  ### **Example Use Case**
77
79
 
@@ -150,6 +152,7 @@ For dynamic content where certain parts should be hidden based on CMS conditions
150
152
  ```
151
153
 
152
154
  **Result:**
155
+
153
156
  ```html
154
157
  <h1>Welcome to Compass Facilities</h1>
155
158
  <p>Contact us at info@compass.com or call (555) 123-4567</p>
@@ -170,6 +173,7 @@ For dynamic content where certain parts should be hidden based on CMS conditions
170
173
  ```
171
174
 
172
175
  **Result:**
176
+
173
177
  ```html
174
178
  <!-- Entire href replaced with dedicated href value -->
175
179
  <a href="mailto:info@compass.com">Email Us</a>
@@ -178,6 +182,7 @@ For dynamic content where certain parts should be hidden based on CMS conditions
178
182
  ```
179
183
 
180
184
  **Why this matters:** Webflow often adds `https://` prefix by default. Without complete href replacement:
185
+
181
186
  - `https://tel:+15551234567` ❌ (broken)
182
187
  - `tel:+15551234567` ✅ (works)
183
188
 
@@ -196,6 +201,7 @@ When setting has only text (no href), normal placeholder replacement occurs:
196
201
  ```
197
202
 
198
203
  **Result:**
204
+
199
205
  ```html
200
206
  <a href="https://compassfacilities.com">Visit Site</a>
201
207
  ```
@@ -234,13 +240,13 @@ All settings are collected regardless of which list they're in.
234
240
 
235
241
  ## **Key Attributes Summary**
236
242
 
237
- | Attribute | Purpose | Required On |
238
- | ----- | ----- | ----- |
239
- | `data-site-settings-element="wrapper"` | Settings container | Wrapper div |
240
- | `data-site-settings-element="list"` | Settings list | List div (at least one) |
241
- | `data-site-settings="name"` | Setting with name | Individual setting elements |
242
- | `href` attribute | Link URL for href replacement | Optional (on link settings) |
243
- | `data-site-settings-hide="class"` | Conditional hide | Optional (inside setting elements) |
243
+ | Attribute | Purpose | Required On |
244
+ | -------------------------------------- | ----------------------------- | ---------------------------------- |
245
+ | `data-site-settings-element="wrapper"` | Settings container | Wrapper div |
246
+ | `data-site-settings-element="list"` | Settings list | List div (at least one) |
247
+ | `data-site-settings="name"` | Setting with name | Individual setting elements |
248
+ | `href` attribute | Link URL for href replacement | Optional (on link settings) |
249
+ | `data-site-settings-hide="class"` | Conditional hide | Optional (inside setting elements) |
244
250
 
245
251
  ---
246
252
 
@@ -248,21 +254,23 @@ All settings are collected regardless of which list they're in.
248
254
 
249
255
  **Format:** `{{name}}`
250
256
 
251
- * Double curly braces
252
- * Name must match the `data-site-settings` attribute value exactly
253
- * Case-sensitive
254
- * Works in text content and link hrefs
257
+ - Double curly braces
258
+ - Name must match the `data-site-settings` attribute value exactly
259
+ - Case-sensitive
260
+ - Works in text content and link hrefs
255
261
 
256
262
  ---
257
263
 
258
264
  ## **Replacement Logic**
259
265
 
260
266
  ### **For Text Content (`{{name}}` in page text):**
267
+
261
268
  1. If setting has text value → use text
262
269
  2. If no text but has href value → use href
263
270
  3. If neither → skip replacement
264
271
 
265
272
  ### **For Link Hrefs (`{{name}}` in href attribute):**
273
+
266
274
  1. If setting has href value → **completely replace entire href**
267
275
  2. If no href but has text value → normal placeholder replacement
268
276
  3. If neither → skip replacement
@@ -272,6 +280,7 @@ All settings are collected regardless of which list they're in.
272
280
  ## **Common Use Cases**
273
281
 
274
282
  ### **Phone Number**
283
+
275
284
  ```html
276
285
  <!-- Setting with both formatted display and tel: link -->
277
286
  <a data-site-settings="phone" href="tel:+15551234567">(555) 123-4567</a>
@@ -286,6 +295,7 @@ All settings are collected regardless of which list they're in.
286
295
  ```
287
296
 
288
297
  ### **Email Address**
298
+
289
299
  ```html
290
300
  <!-- Setting -->
291
301
  <a data-site-settings="email" href="mailto:contact@example.com">contact@example.com</a>
@@ -300,6 +310,7 @@ All settings are collected regardless of which list they're in.
300
310
  ```
301
311
 
302
312
  ### **Social Media**
313
+
303
314
  ```html
304
315
  <!-- Setting -->
305
316
  <a data-site-settings="facebook" href="https://facebook.com/mypage">@mypage</a>
@@ -326,6 +337,25 @@ All settings are collected regardless of which list they're in.
326
337
 
327
338
  ---
328
339
 
340
+ ## **v2.0.0 Improvements**
341
+
342
+ ### **JSON Config System**
343
+
344
+ - Added config.js with all attribute definitions
345
+ - Future-proof: Easy to add aliases or change attribute patterns
346
+ - Self-documenting: All supported attributes in one place
347
+
348
+ ### **Barba.js / SPA Compatibility**
349
+
350
+ The site-settings system is fully compatible with Barba.js and other SPA frameworks:
351
+
352
+ - Works on fresh page load and reinitialize
353
+ - No destroy needed (one-time DOM operation)
354
+ - Automatically replaces placeholders on new pages
355
+ - Works like fresh page load on new DOM
356
+
357
+ ---
358
+
329
359
  ## **Common Issues**
330
360
 
331
361
  **Placeholder not replacing:**
@@ -1,13 +1,15 @@
1
- export function init() {
1
+ import { querySelector, querySelectorAll, getSelector } from '@utils';
2
+
3
+ export function init(config) {
2
4
  function setupSiteSettings() {
3
- const wrapper = document.querySelector('[data-site-settings-element="wrapper"]');
5
+ const wrapper = querySelector(config, 'wrapper');
4
6
 
5
7
  if (!wrapper) {
6
8
  return;
7
9
  }
8
10
 
9
11
  // Find all lists inside the wrapper
10
- const lists = wrapper.querySelectorAll('[data-site-settings-element="list"]');
12
+ const lists = querySelectorAll(config, 'list', wrapper);
11
13
 
12
14
  if (lists.length === 0) {
13
15
  return;
@@ -16,26 +18,30 @@ export function init() {
16
18
  // Collect all site settings data
17
19
  const siteSettings = {};
18
20
 
19
- lists.forEach(list => {
21
+ lists.forEach((list) => {
20
22
  // Find all descendants with data-site-settings attribute
21
- const settingElements = list.querySelectorAll('[data-site-settings]');
23
+ const settingSelector = getSelector(config, 'setting');
24
+ const settingElements = list.querySelectorAll(settingSelector);
22
25
 
23
- settingElements.forEach(element => {
26
+ settingElements.forEach((element) => {
24
27
  // Clone the element to avoid modifying the original DOM
25
28
  const clonedElement = element.cloneNode(true);
26
29
 
27
30
  // Find and remove elements with data-site-settings-hide that match their classes
28
- const hideElements = clonedElement.querySelectorAll('[data-site-settings-hide]');
31
+ const hideSelector = getSelector(config, 'hide');
32
+ const hideElements = clonedElement.querySelectorAll(hideSelector);
29
33
 
30
- hideElements.forEach(hideElement => {
31
- const hideValue = hideElement.getAttribute('data-site-settings-hide');
34
+ hideElements.forEach((hideElement) => {
35
+ const hideAttr =
36
+ config.attributes.elements.hide.primary.match(/data-site-settings-hide/)[0];
37
+ const hideValue = hideElement.getAttribute(hideAttr);
32
38
 
33
39
  if (hideValue) {
34
40
  // Split by spaces to get all class names to check
35
- const classesToHide = hideValue.split(' ').filter(c => c.trim());
41
+ const classesToHide = hideValue.split(' ').filter((c) => c.trim());
36
42
 
37
43
  // Check if element has any of the classes
38
- const hasMatchingClass = classesToHide.some(className =>
44
+ const hasMatchingClass = classesToHide.some((className) =>
39
45
  hideElement.classList.contains(className)
40
46
  );
41
47
 
@@ -46,7 +52,9 @@ export function init() {
46
52
  }
47
53
  });
48
54
 
49
- const settingName = element.getAttribute('data-site-settings');
55
+ const settingAttr =
56
+ config.attributes.elements.setting.primary.match(/data-site-settings/)[0];
57
+ const settingName = element.getAttribute(settingAttr);
50
58
  const settingText = clonedElement.textContent.trim();
51
59
  const settingHref = element.getAttribute('href');
52
60
 
@@ -54,7 +62,7 @@ export function init() {
54
62
  if (settingName && (settingText || settingHref)) {
55
63
  siteSettings[settingName] = {
56
64
  text: settingText || null,
57
- href: settingHref || null
65
+ href: settingHref || null,
58
66
  };
59
67
  }
60
68
  });
@@ -66,31 +74,27 @@ export function init() {
66
74
  }
67
75
 
68
76
  // Replace text content efficiently using TreeWalker
69
- const walker = document.createTreeWalker(
70
- document.body,
71
- NodeFilter.SHOW_TEXT,
72
- {
73
- acceptNode: (node) => {
74
- // Check if any site setting names exist in the text content
75
- const text = node.textContent;
76
- for (const name in siteSettings) {
77
- if (text.includes(`{{${name}}}`)) {
78
- return NodeFilter.FILTER_ACCEPT;
79
- }
77
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
78
+ acceptNode: (node) => {
79
+ // Check if any site setting names exist in the text content
80
+ const text = node.textContent;
81
+ for (const name in siteSettings) {
82
+ if (text.includes(`{{${name}}}`)) {
83
+ return NodeFilter.FILTER_ACCEPT;
80
84
  }
81
- return NodeFilter.FILTER_SKIP;
82
85
  }
83
- }
84
- );
86
+ return NodeFilter.FILTER_SKIP;
87
+ },
88
+ });
85
89
 
86
90
  const textNodes = [];
87
91
  let node;
88
- while (node = walker.nextNode()) {
92
+ while ((node = walker.nextNode())) {
89
93
  textNodes.push(node);
90
94
  }
91
95
 
92
96
  // Replace text in collected nodes
93
- textNodes.forEach(textNode => {
97
+ textNodes.forEach((textNode) => {
94
98
  let newText = textNode.textContent;
95
99
  let hasChanges = false;
96
100
 
@@ -114,7 +118,7 @@ export function init() {
114
118
 
115
119
  // Replace link hrefs
116
120
  const links = document.querySelectorAll('a[href]');
117
- links.forEach(link => {
121
+ links.forEach((link) => {
118
122
  let href = link.getAttribute('href');
119
123
  let hasChanges = false;
120
124
 
@@ -146,9 +150,9 @@ export function init() {
146
150
  setupSiteSettings();
147
151
 
148
152
  return {
149
- result: "site-settings initialized",
153
+ result: 'site-settings initialized',
150
154
  destroy: () => {
151
155
  // No cleanup needed - this module only modifies DOM during init
152
- }
156
+ },
153
157
  };
154
158
  }