@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,493 @@
1
+ /**
2
+ * Pagination System
3
+ *
4
+ * Handles paginated lists with controls, counters, and dot navigation.
5
+ * Supports infinite looping, responsive layouts, and accessibility.
6
+ */
7
+
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
+
17
+ const cleanup = {
18
+ observers: [],
19
+ handlers: [],
20
+ liveRegions: [],
21
+ };
22
+
23
+ // Initialize all pagination containers
24
+ try {
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);
30
+ }
31
+ } catch (error) {
32
+ console.error('[hs-pagination] Error initializing container:', error, container);
33
+ }
34
+ });
35
+ } catch (error) {
36
+ console.error('[hs-pagination] Critical error during initialization:', error);
37
+ }
38
+
39
+ return {
40
+ result: 'pagination initialized',
41
+ destroy: () => {
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
+
55
+ // Disconnect all observers
56
+ cleanup.observers.forEach((obs) => {
57
+ try {
58
+ obs.disconnect();
59
+ } catch (error) {
60
+ console.error('[hs-pagination] Error disconnecting observer:', error);
61
+ }
62
+ });
63
+ cleanup.observers.length = 0;
64
+
65
+ // Remove all event listeners
66
+ cleanup.handlers.forEach(({ element, event, handler }) => {
67
+ try {
68
+ element.removeEventListener(event, handler);
69
+ } catch (error) {
70
+ console.error('[hs-pagination] Error removing event listener:', error);
71
+ }
72
+ });
73
+ cleanup.handlers.length = 0;
74
+
75
+ // Clear module config
76
+ moduleConfig = null;
77
+ } catch (error) {
78
+ console.error('[hs-pagination] Critical error during cleanup:', error);
79
+ }
80
+ },
81
+ };
82
+ }
83
+
84
+ function initPaginationInstance(container, cleanup) {
85
+ const list = querySelector(moduleConfig, 'list', container);
86
+ if (!list) {
87
+ console.warn('[hs-pagination] Missing required element: data-hs-pagination="list"');
88
+ return null;
89
+ }
90
+
91
+ const wrapper = list.parentElement;
92
+ if (!wrapper) return null;
93
+
94
+ const elements = {
95
+ controls: querySelector(moduleConfig, 'controls', container),
96
+ counter: querySelector(moduleConfig, 'counter', container),
97
+ dotsWrap: querySelector(moduleConfig, 'dots', container),
98
+ };
99
+
100
+ // Early exit for infinite mode - no controls means no pagination
101
+ if (!elements.controls) {
102
+ return { initialized: false };
103
+ }
104
+
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;
112
+
113
+ if (!nextBtn || !prevBtn) {
114
+ console.warn('[hs-pagination] Missing required navigation buttons');
115
+ return null;
116
+ }
117
+
118
+ elements.nextBtn = nextBtn;
119
+ elements.prevBtn = prevBtn;
120
+
121
+ // Add ARIA attributes to buttons
122
+ elements.nextBtn.setAttribute('aria-label', 'Go to next page');
123
+ elements.prevBtn.setAttribute('aria-label', 'Go to previous page');
124
+ if (elements.counter) {
125
+ elements.counter.setAttribute('aria-live', 'polite');
126
+ elements.counter.setAttribute('aria-label', 'Current page');
127
+ }
128
+
129
+ // Parse configuration from new attributes
130
+ const desktopItems = parseInt(elements.controls?.getAttribute('data-hs-pagination-show')) || 6;
131
+ const mobileItems =
132
+ parseInt(elements.controls?.getAttribute('data-hs-pagination-show-mobile')) || desktopItems;
133
+
134
+ const isMobileLayout = () => {
135
+ const stateValue = getComputedStyle(list).getPropertyValue(cssVariables.state).trim();
136
+ return stateValue === globalConfig.cssVars.state.values.active;
137
+ };
138
+
139
+ const allItems = Array.from(list.children);
140
+ const totalItems = allItems.length;
141
+ if (!totalItems) return null;
142
+
143
+ const state = {
144
+ totalPages: 1,
145
+ currentIndex: 1,
146
+ currentPage: 1,
147
+ isAnimating: false,
148
+ itemsPerPage: desktopItems,
149
+ dotTemplates: { active: null, inactive: null },
150
+ };
151
+ let wrapperChildren = [];
152
+
153
+ // Create live region for announcements
154
+ const liveRegion = document.createElement('div');
155
+ liveRegion.className = 'sr-only';
156
+ liveRegion.setAttribute('aria-live', 'assertive');
157
+ liveRegion.setAttribute('aria-atomic', 'true');
158
+ liveRegion.style.cssText =
159
+ 'position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden;';
160
+ container.appendChild(liveRegion);
161
+ cleanup.liveRegions.push(liveRegion);
162
+
163
+ const updateCounter = () => {
164
+ if (elements.counter) {
165
+ elements.counter.textContent = `${state.currentPage} / ${state.totalPages}`;
166
+ }
167
+ };
168
+
169
+ const announcePageChange = () => {
170
+ liveRegion.textContent = `Page ${state.currentPage} of ${state.totalPages}`;
171
+ setTimeout(() => (liveRegion.textContent = ''), 1000);
172
+ };
173
+
174
+ const manageFocus = () => {
175
+ wrapperChildren.forEach((page, index) => {
176
+ page[index === state.currentIndex ? 'removeAttribute' : 'setAttribute']('inert', '');
177
+ });
178
+ };
179
+
180
+ const updateHeight = () => {
181
+ const targetPage = wrapperChildren[state.currentIndex];
182
+ if (targetPage && targetPage.offsetHeight !== undefined) {
183
+ wrapper.style.height = targetPage.offsetHeight + 'px';
184
+ }
185
+ };
186
+
187
+ const initializeDots = () => {
188
+ if (!elements.dotsWrap) return;
189
+
190
+ // Find existing dots as templates
191
+ const existingDots = Array.from(elements.dotsWrap.children);
192
+ if (!existingDots.length) return;
193
+
194
+ // Identify active and inactive templates
195
+ const activeDot = existingDots.find((dot) => dot.classList.contains('is-active'));
196
+ const inactiveDot = existingDots.find((dot) => !dot.classList.contains('is-active'));
197
+
198
+ // Store templates (use same template for both if only one exists)
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);
205
+
206
+ // Clear existing dots
207
+ elements.dotsWrap.innerHTML = '';
208
+
209
+ // Add pagination accessibility attributes
210
+ elements.dotsWrap.setAttribute('role', 'group');
211
+ elements.dotsWrap.setAttribute('aria-label', 'Page navigation');
212
+
213
+ // Create dots for each page
214
+ for (let i = 1; i <= state.totalPages; i++) {
215
+ const dot = (i === 1 ? state.dotTemplates.active : state.dotTemplates.inactive).cloneNode(
216
+ true
217
+ );
218
+
219
+ // Add accessibility attributes
220
+ dot.setAttribute('role', 'button');
221
+ dot.setAttribute('tabindex', '0');
222
+ dot.setAttribute('aria-label', `Go to page ${i}`);
223
+ dot.setAttribute('aria-current', i === 1 ? 'page' : 'false');
224
+ dot.setAttribute('data-page', i);
225
+
226
+ // Set initial active state
227
+ if (i === 1) {
228
+ dot.classList.add('is-active');
229
+ } else {
230
+ dot.classList.remove('is-active');
231
+ }
232
+
233
+ // Click handler
234
+ const clickHandler = () => navigateToPage(i);
235
+ dot.addEventListener('click', clickHandler);
236
+ cleanup.handlers.push({ element: dot, event: 'click', handler: clickHandler });
237
+
238
+ // Keyboard handler
239
+ const keyHandler = (e) => {
240
+ if (e.key === 'Enter' || e.key === ' ') {
241
+ e.preventDefault();
242
+ navigateToPage(i);
243
+ }
244
+ };
245
+ dot.addEventListener('keydown', keyHandler);
246
+ cleanup.handlers.push({ element: dot, event: 'keydown', handler: keyHandler });
247
+
248
+ elements.dotsWrap.appendChild(dot);
249
+ }
250
+ };
251
+
252
+ const updateActiveDot = () => {
253
+ if (!elements.dotsWrap) return;
254
+
255
+ const dots = Array.from(elements.dotsWrap.children);
256
+ dots.forEach((dot, index) => {
257
+ const dotPage = index + 1;
258
+ if (dotPage === state.currentPage) {
259
+ dot.classList.add('is-active');
260
+ dot.setAttribute('aria-current', 'page');
261
+ } else {
262
+ dot.classList.remove('is-active');
263
+ dot.setAttribute('aria-current', 'false');
264
+ }
265
+ });
266
+ };
267
+
268
+ const initializePagination = (forceItemsPerPage = null) => {
269
+ const currentIsMobile = isMobileLayout();
270
+ state.itemsPerPage = forceItemsPerPage || (currentIsMobile ? mobileItems : desktopItems);
271
+ state.totalPages = Math.ceil(totalItems / state.itemsPerPage);
272
+
273
+ // Remove old dot event handlers to prevent memory leaks
274
+ if (elements.dotsWrap) {
275
+ cleanup.handlers = cleanup.handlers.filter(({ element }) => {
276
+ return !elements.dotsWrap.contains(element);
277
+ });
278
+ }
279
+
280
+ // Clean up previous page lists
281
+ Array.from(wrapper.children).forEach((child) => {
282
+ if (child !== list) wrapper.removeChild(child);
283
+ });
284
+ list.innerHTML = '';
285
+ allItems.forEach((item) => list.appendChild(item));
286
+
287
+ // Single page case
288
+ if (state.totalPages <= 1) {
289
+ if (elements.controls) {
290
+ if (elements.controls.contains(document.activeElement)) document.activeElement.blur();
291
+ elements.controls.style.display = 'none';
292
+ elements.controls.setAttribute('aria-hidden', 'true');
293
+ }
294
+ if (elements.dotsWrap) {
295
+ elements.dotsWrap.style.display = 'none';
296
+ elements.dotsWrap.setAttribute('aria-hidden', 'true');
297
+ }
298
+ Object.assign(state, { totalPages: 1, currentIndex: 1, currentPage: 1, isAnimating: false });
299
+ wrapper.style.cssText = `transform: translateX(0%); height: ${list.offsetHeight}px;`;
300
+ wrapperChildren = [list];
301
+ return 1;
302
+ }
303
+
304
+ // Show controls and dots
305
+ if (elements.controls) {
306
+ elements.controls.style.display = '';
307
+ elements.controls.removeAttribute('aria-hidden');
308
+ }
309
+ if (elements.dotsWrap) {
310
+ elements.dotsWrap.style.display = '';
311
+ elements.dotsWrap.removeAttribute('aria-hidden');
312
+ }
313
+
314
+ // Create page lists
315
+ const pageLists = Array.from({ length: state.totalPages }, (_, page) => {
316
+ const pageList = list.cloneNode(false);
317
+ const startIndex = page * state.itemsPerPage;
318
+ const endIndex = Math.min(startIndex + state.itemsPerPage, totalItems);
319
+ allItems
320
+ .slice(startIndex, endIndex)
321
+ .forEach((item) => pageList.appendChild(item.cloneNode(true)));
322
+ return pageList;
323
+ });
324
+
325
+ // Insert cloned pages for infinite loop
326
+ wrapper.insertBefore(pageLists[pageLists.length - 1].cloneNode(true), list);
327
+ pageLists.slice(1).forEach((page) => wrapper.appendChild(page));
328
+ wrapper.appendChild(pageLists[0].cloneNode(true));
329
+
330
+ // Populate original list with first page
331
+ list.innerHTML = '';
332
+ Array.from(pageLists[0].children).forEach((item) => list.appendChild(item));
333
+
334
+ Object.assign(state, { currentIndex: 1, currentPage: 1, isAnimating: false });
335
+ wrapperChildren = Array.from(wrapper.children);
336
+ wrapper.style.transform = 'translateX(-100%)';
337
+
338
+ updateCounter();
339
+ updateHeight();
340
+ manageFocus();
341
+ initializeDots();
342
+ return state.totalPages;
343
+ };
344
+
345
+ let currentLayoutIsMobile = isMobileLayout();
346
+
347
+ const checkLayoutChange = () => {
348
+ // Prevent resize during animation
349
+ if (state.isAnimating) return;
350
+
351
+ const newIsMobile = isMobileLayout();
352
+ if (newIsMobile !== currentLayoutIsMobile) {
353
+ currentLayoutIsMobile = newIsMobile;
354
+ initializePagination();
355
+ } else {
356
+ updateHeight();
357
+ }
358
+ };
359
+
360
+ const resizeObserver = new ResizeObserver(checkLayoutChange);
361
+ resizeObserver.observe(wrapper);
362
+ cleanup.observers.push(resizeObserver);
363
+
364
+ initializePagination();
365
+
366
+ const navigateToPage = (targetPage) => {
367
+ if (state.isAnimating || state.totalPages <= 1 || targetPage === state.currentPage) return;
368
+ state.isAnimating = true;
369
+
370
+ state.currentIndex = targetPage;
371
+ state.currentPage = targetPage;
372
+
373
+ updateCounter();
374
+ announcePageChange();
375
+ updateHeight();
376
+ updateActiveDot();
377
+
378
+ if (isMobileLayout() && elements.controls) {
379
+ setTimeout(() => {
380
+ const controlsBottom =
381
+ elements.controls.getBoundingClientRect().bottom + window.pageYOffset;
382
+ const clearance = 5 * 16; // 5rem in pixels
383
+ const targetScrollPosition = controlsBottom - window.innerHeight + clearance;
384
+ window.scrollTo({ top: targetScrollPosition, behavior: 'smooth' });
385
+ }, 50);
386
+ }
387
+
388
+ wrapper.style.transform = `translateX(${-state.currentIndex * 100}%)`;
389
+
390
+ let transitionTimeout = null;
391
+
392
+ const handleTransitionEnd = () => {
393
+ clearTimeout(transitionTimeout);
394
+ wrapper.removeEventListener('transitionend', handleTransitionEnd);
395
+
396
+ updateCounter();
397
+ announcePageChange();
398
+ updateHeight();
399
+ manageFocus();
400
+ updateActiveDot();
401
+ state.isAnimating = false;
402
+ };
403
+
404
+ // Safety timeout in case transitionend never fires
405
+ transitionTimeout = setTimeout(handleTransitionEnd, 1000);
406
+
407
+ wrapper.addEventListener('transitionend', handleTransitionEnd);
408
+ };
409
+
410
+ const navigate = (direction) => {
411
+ if (state.isAnimating || state.totalPages <= 1) return;
412
+ state.isAnimating = true;
413
+ state.currentIndex += direction;
414
+
415
+ state.currentPage =
416
+ state.currentIndex > state.totalPages
417
+ ? 1
418
+ : state.currentIndex < 1
419
+ ? state.totalPages
420
+ : state.currentIndex;
421
+
422
+ updateCounter();
423
+ announcePageChange();
424
+ updateHeight();
425
+ updateActiveDot();
426
+
427
+ if (isMobileLayout() && elements.controls) {
428
+ setTimeout(() => {
429
+ const controlsBottom =
430
+ elements.controls.getBoundingClientRect().bottom + window.pageYOffset;
431
+ const clearance = 5 * 16; // 5rem in pixels
432
+ const targetScrollPosition = controlsBottom - window.innerHeight + clearance;
433
+ window.scrollTo({ top: targetScrollPosition, behavior: 'smooth' });
434
+ }, 50);
435
+ }
436
+
437
+ wrapper.style.transform = `translateX(${-state.currentIndex * 100}%)`;
438
+
439
+ let transitionTimeout = null;
440
+
441
+ const handleTransitionEnd = () => {
442
+ clearTimeout(transitionTimeout);
443
+ wrapper.removeEventListener('transitionend', handleTransitionEnd);
444
+
445
+ if (state.currentIndex > state.totalPages) {
446
+ state.currentIndex = 1;
447
+ state.currentPage = 1;
448
+ // Disable transition for instant jump
449
+ wrapper.style.transition = 'none';
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 = '';
455
+ } else if (state.currentIndex < 1) {
456
+ state.currentIndex = state.totalPages;
457
+ state.currentPage = state.totalPages;
458
+ // Disable transition for instant jump
459
+ wrapper.style.transition = 'none';
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 = '';
465
+ }
466
+
467
+ updateCounter();
468
+ announcePageChange();
469
+ updateHeight();
470
+ manageFocus();
471
+ updateActiveDot();
472
+ state.isAnimating = false;
473
+ };
474
+
475
+ // Safety timeout in case transitionend never fires
476
+ transitionTimeout = setTimeout(handleTransitionEnd, 1000);
477
+
478
+ wrapper.addEventListener('transitionend', handleTransitionEnd);
479
+ };
480
+
481
+ const nextHandler = () => navigate(1);
482
+ const prevHandler = () => navigate(-1);
483
+
484
+ elements.nextBtn.addEventListener('click', nextHandler);
485
+ elements.prevBtn.addEventListener('click', prevHandler);
486
+
487
+ cleanup.handlers.push(
488
+ { element: elements.nextBtn, event: 'click', handler: nextHandler },
489
+ { element: elements.prevBtn, event: 'click', handler: prevHandler }
490
+ );
491
+
492
+ return { initialized: true };
493
+ }