@orangesk/orange-design-system 2.0.0-beta.3 → 2.0.0-beta.4

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 (89) hide show
  1. package/build/components/Accordion/tsconfig.tsbuildinfo +1 -1
  2. package/build/components/Alert/tsconfig.tsbuildinfo +1 -1
  3. package/build/components/AnchorNavigation/tsconfig.tsbuildinfo +1 -1
  4. package/build/components/Bar/tsconfig.tsbuildinfo +1 -1
  5. package/build/components/BlockAction/tsconfig.tsbuildinfo +1 -1
  6. package/build/components/BodyBanner/tsconfig.tsbuildinfo +1 -1
  7. package/build/components/Breadcrumbs/tsconfig.tsbuildinfo +1 -1
  8. package/build/components/Button/tsconfig.tsbuildinfo +1 -1
  9. package/build/components/Buttons/tsconfig.tsbuildinfo +1 -1
  10. package/build/components/Card/tsconfig.tsbuildinfo +1 -1
  11. package/build/components/Carousel/tsconfig.tsbuildinfo +1 -1
  12. package/build/components/CarouselHero/index.js +16 -0
  13. package/build/components/CarouselHero/index.js.map +1 -0
  14. package/build/components/CarouselHero/tsconfig.tsbuildinfo +1 -0
  15. package/build/components/CarouselPromotions/tsconfig.tsbuildinfo +1 -1
  16. package/build/components/CartTable/tsconfig.tsbuildinfo +1 -1
  17. package/build/components/Code/tsconfig.tsbuildinfo +1 -1
  18. package/build/components/Container/tsconfig.tsbuildinfo +1 -1
  19. package/build/components/Controls/tsconfig.tsbuildinfo +1 -1
  20. package/build/components/Cover/tsconfig.tsbuildinfo +1 -1
  21. package/build/components/Divider/tsconfig.tsbuildinfo +1 -1
  22. package/build/components/DocumentationSidebar/index.js +1 -1
  23. package/build/components/DocumentationSidebar/tsconfig.tsbuildinfo +1 -1
  24. package/build/components/Dropdown/tsconfig.tsbuildinfo +1 -1
  25. package/build/components/Expander/tsconfig.tsbuildinfo +1 -1
  26. package/build/components/FeatureAccordion/tsconfig.tsbuildinfo +1 -1
  27. package/build/components/Footer/tsconfig.tsbuildinfo +1 -1
  28. package/build/components/Forms/tsconfig.tsbuildinfo +1 -1
  29. package/build/components/Gauge/tsconfig.tsbuildinfo +1 -1
  30. package/build/components/Grid/tsconfig.tsbuildinfo +1 -1
  31. package/build/components/Hero/tsconfig.tsbuildinfo +1 -1
  32. package/build/components/Icon/tsconfig.tsbuildinfo +1 -1
  33. package/build/components/IconList/tsconfig.tsbuildinfo +1 -1
  34. package/build/components/Image/tsconfig.tsbuildinfo +1 -1
  35. package/build/components/Link/tsconfig.tsbuildinfo +1 -1
  36. package/build/components/List/tsconfig.tsbuildinfo +1 -1
  37. package/build/components/Loader/tsconfig.tsbuildinfo +1 -1
  38. package/build/components/Megamenu/tsconfig.tsbuildinfo +1 -1
  39. package/build/components/Modal/tsconfig.tsbuildinfo +1 -1
  40. package/build/components/Pagination/tsconfig.tsbuildinfo +1 -1
  41. package/build/components/Pill/tsconfig.tsbuildinfo +1 -1
  42. package/build/components/Preview/tsconfig.tsbuildinfo +1 -1
  43. package/build/components/Progress/tsconfig.tsbuildinfo +1 -1
  44. package/build/components/PromoBanner/tsconfig.tsbuildinfo +1 -1
  45. package/build/components/PromotionCard/tsconfig.tsbuildinfo +1 -1
  46. package/build/components/Section/tsconfig.tsbuildinfo +1 -1
  47. package/build/components/Skeleton/tsconfig.tsbuildinfo +1 -1
  48. package/build/components/SkipLink/tsconfig.tsbuildinfo +1 -1
  49. package/build/components/Stepbar/tsconfig.tsbuildinfo +1 -1
  50. package/build/components/Sticker/tsconfig.tsbuildinfo +1 -1
  51. package/build/components/Table/tsconfig.tsbuildinfo +1 -1
  52. package/build/components/Tabs/tsconfig.tsbuildinfo +1 -1
  53. package/build/components/Tag/tsconfig.tsbuildinfo +1 -1
  54. package/build/components/Testimonial/tsconfig.tsbuildinfo +1 -1
  55. package/build/components/Tile/tsconfig.tsbuildinfo +1 -1
  56. package/build/components/Tooltip/tsconfig.tsbuildinfo +1 -1
  57. package/build/components/index.js +6 -6
  58. package/build/components/index.js.map +1 -1
  59. package/build/components/static.js +4 -4
  60. package/build/components/static.js.map +1 -1
  61. package/build/components/tsconfig.tsbuildinfo +1 -1
  62. package/build/components/types/src/components/CarouselHero/CarouselHero.d.ts +18 -0
  63. package/build/components/types/src/components/CarouselHero/CarouselHero.static.d.ts +47 -0
  64. package/build/components/types/src/components/CarouselHero/CarouselHeroItem.d.ts +9 -0
  65. package/build/components/types/src/components/CarouselHero/constants.d.ts +34 -0
  66. package/build/components/types/src/components/CarouselHero/index.d.ts +2 -0
  67. package/build/components/types/src/components/index.d.ts +2 -1
  68. package/build/components/types/src/scripts/index.d.ts +5 -0
  69. package/build/lib/components.css +1 -1
  70. package/build/lib/components.css.map +1 -1
  71. package/build/lib/scripts.js +4 -4
  72. package/build/lib/scripts.js.map +1 -1
  73. package/build/lib/style.css +1 -1
  74. package/build/lib/style.css.map +1 -1
  75. package/package.json +1 -1
  76. package/src/components/CarouselHero/CarouselHero.static.ts +528 -0
  77. package/src/components/CarouselHero/CarouselHero.tsx +148 -0
  78. package/src/components/CarouselHero/CarouselHeroItem.tsx +41 -0
  79. package/src/components/CarouselHero/constants.ts +37 -0
  80. package/src/components/CarouselHero/index.ts +2 -0
  81. package/src/components/CarouselHero/styles/config.scss +54 -0
  82. package/src/components/CarouselHero/styles/mixins.scss +289 -0
  83. package/src/components/CarouselHero/styles/style.scss +67 -0
  84. package/src/components/CarouselHero/tests/CarouselHero.conformance.test.js +148 -0
  85. package/src/components/CarouselHero/tests/CarouselHero.unit.test.js +289 -0
  86. package/src/components/CarouselHero/tests/CarouselHeroItem.conformance.test.js +142 -0
  87. package/src/components/CarouselHero/tests/CarouselHeroItem.unit.test.js +210 -0
  88. package/src/components/Controls/styles/config.scss +2 -2
  89. package/src/components/index.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orangesk/orange-design-system",
3
- "version": "2.0.0-beta.3",
3
+ "version": "2.0.0-beta.4",
4
4
  "private": false,
5
5
  "engines": {
6
6
  "node": ">=18.x"
@@ -0,0 +1,528 @@
1
+ import type { SwiperOptions } from "swiper/types";
2
+ import {
3
+ Navigation,
4
+ Pagination,
5
+ A11y,
6
+ Keyboard,
7
+ Autoplay,
8
+ } from "swiper/modules";
9
+ import { Swiper } from "swiper";
10
+
11
+ import {
12
+ CLASS_SLIDE,
13
+ CLASS_TRACK,
14
+ CLASS_ACTIVE,
15
+ CLASS_PLAYING,
16
+ CLASS_PAUSED,
17
+ SELECTOR_VIEWPORT,
18
+ SELECTOR_PREV,
19
+ SELECTOR_NEXT,
20
+ SELECTOR_PLAY_PAUSE,
21
+ SELECTOR_TABS,
22
+ SELECTOR_TAB,
23
+ SELECTOR_PAGINATION,
24
+ CLASS_PAGINATION_ITEM,
25
+ CLASS_PAGINATION_SVG,
26
+ CLASS_PAGINATION_CIRCLE,
27
+ } from "./constants";
28
+
29
+ export const defaultConfig: SwiperOptions = {
30
+ pagination: {
31
+ clickable: true,
32
+ bulletClass: CLASS_PAGINATION_ITEM,
33
+ bulletActiveClass: "is-active",
34
+ renderBullet: function (index: number, className: string) {
35
+ return `<button type="button" class="${className}" aria-label="Prejsť na snímok ${index + 1}"></button>`;
36
+ },
37
+ },
38
+ slidesPerView: 1,
39
+ loop: false,
40
+ a11y: {
41
+ enabled: true,
42
+ prevSlideMessage: "Predchádzajúci snímok",
43
+ nextSlideMessage: "Nasledujúci snímok",
44
+ containerMessage: "Hero carousel so snímkami",
45
+ containerRoleDescriptionMessage: "carousel",
46
+ itemRoleDescriptionMessage: "snímok",
47
+ firstSlideMessage: "Prvý snímok",
48
+ lastSlideMessage: "Posledný snímok",
49
+ slideLabelMessage: "Snímok",
50
+ },
51
+ wrapperClass: CLASS_TRACK,
52
+ slideClass: CLASS_SLIDE,
53
+ slideActiveClass: CLASS_ACTIVE,
54
+ };
55
+
56
+ export default class CarouselHero {
57
+ element: HTMLElement;
58
+ config: SwiperOptions;
59
+ viewport!: HTMLElement;
60
+ instance!: Swiper;
61
+ tabs: HTMLElement[] = [];
62
+ _dotAnimationJustStarted: boolean = false;
63
+ _isDragging: boolean = false;
64
+
65
+ private _boundTabClick!: (e: Event) => void;
66
+ private _boundPrevClick!: () => void;
67
+ private _boundNextClick!: () => void;
68
+ private _boundPlayPauseClick!: () => void;
69
+
70
+ constructor(element: HTMLElement, config?: Partial<SwiperOptions>) {
71
+ this.element = element;
72
+ this.config = { ...defaultConfig, ...config };
73
+ (this.element as any).ODS_CarouselHero = this;
74
+ this._boundTabClick = this._onTabClick.bind(this);
75
+ this._boundPrevClick = this._onUserNavigation.bind(this);
76
+ this._boundNextClick = this._onUserNavigation.bind(this);
77
+ this._boundPlayPauseClick = this._onPlayPauseClick.bind(this);
78
+ this.init();
79
+ return this;
80
+ }
81
+
82
+ init() {
83
+ this.getElements();
84
+ this.setupConfig();
85
+ this.createSwiper();
86
+ this.setupEventListeners();
87
+ this.renderPaginationDots();
88
+ this.updateStates();
89
+ this.updatePlayPauseIcon();
90
+
91
+ if (this.hasAutoplay() && this.isAutoplayRunning()) {
92
+ this.startDotAnimation();
93
+ }
94
+ }
95
+ getElements() {
96
+ this.viewport = this.element.querySelector(SELECTOR_VIEWPORT)!;
97
+ const tabsContainer = this.element.querySelector(SELECTOR_TABS);
98
+ if (tabsContainer) {
99
+ this.tabs = Array.from(tabsContainer.querySelectorAll(SELECTOR_TAB));
100
+ }
101
+ }
102
+
103
+ setupConfig() {
104
+ const interval = this.element.hasAttribute("data-interval")
105
+ ? parseInt(this.element.getAttribute("data-interval")!) || 0
106
+ : 0;
107
+
108
+ if (this.element.hasAttribute("data-swiper-options")) {
109
+ try {
110
+ const customOptions = JSON.parse(
111
+ this.element.getAttribute("data-swiper-options")!,
112
+ );
113
+ this.config = { ...this.config, ...customOptions };
114
+ } catch (error) {
115
+ console.warn("Invalid swiper options:", error);
116
+ }
117
+ }
118
+
119
+ // Enable loop for smoother autoplay behavior
120
+ this.config.loop = true;
121
+
122
+ // Setup autoplay if interval is specified
123
+ if (interval >= 1000) {
124
+ this.config.autoplay = {
125
+ delay: interval,
126
+ disableOnInteraction: false,
127
+ pauseOnMouseEnter: false,
128
+ waitForTransition: true,
129
+ stopOnLastSlide: false,
130
+ } as NonNullable<SwiperOptions["autoplay"]>;
131
+ this.element.classList.add(CLASS_PLAYING);
132
+ }
133
+ }
134
+
135
+ createSwiper() {
136
+ const slides = this.viewport.querySelectorAll(`.${CLASS_SLIDE}`);
137
+ const hasPagination = slides.length > 1;
138
+
139
+ this.instance = new Swiper(this.viewport, {
140
+ ...this.config,
141
+ modules: [Navigation, Pagination, A11y, Keyboard, Autoplay],
142
+ navigation: {
143
+ nextEl: this.element.querySelector(SELECTOR_NEXT) as HTMLElement,
144
+ prevEl: this.element.querySelector(SELECTOR_PREV) as HTMLElement,
145
+ },
146
+ pagination: hasPagination
147
+ ? {
148
+ ...(this.config.pagination as object),
149
+ el: this.element.querySelector(SELECTOR_PAGINATION) as HTMLElement,
150
+ }
151
+ : false,
152
+ on: {
153
+ slideChange: () => {
154
+ this.updateStates();
155
+ if (this.isAutoplayRunning()) {
156
+ this.restartDotAnimation();
157
+ } else {
158
+ this.stopDotAnimation();
159
+ }
160
+ },
161
+ autoplayStart: () => {
162
+ this._onAutoplayStart();
163
+ },
164
+ autoplayStop: () => {
165
+ this._onAutoplayStop();
166
+ },
167
+ touchStart: () => {
168
+ this._isDragging = true;
169
+ this._stopAutoplayIfRunning();
170
+ },
171
+ touchEnd: () => {
172
+ this._isDragging = false;
173
+ },
174
+ },
175
+ });
176
+
177
+ this.updatePlayPauseIcon();
178
+ }
179
+
180
+ setupEventListeners() {
181
+ this.tabs.forEach((tab) => {
182
+ tab.removeEventListener("click", this._boundTabClick);
183
+ tab.addEventListener("click", this._boundTabClick);
184
+ });
185
+
186
+ const playPauseButton = this.element.querySelector(SELECTOR_PLAY_PAUSE);
187
+ if (playPauseButton) {
188
+ playPauseButton.removeEventListener("click", this._boundPlayPauseClick);
189
+ playPauseButton.addEventListener("click", this._boundPlayPauseClick);
190
+ }
191
+
192
+ const prevButton = this.element.querySelector(SELECTOR_PREV);
193
+ const nextButton = this.element.querySelector(SELECTOR_NEXT);
194
+ if (prevButton) {
195
+ prevButton.removeEventListener("click", this._boundPrevClick);
196
+ prevButton.addEventListener("click", this._boundPrevClick);
197
+ }
198
+ if (nextButton) {
199
+ nextButton.removeEventListener("click", this._boundNextClick);
200
+ nextButton.addEventListener("click", this._boundNextClick);
201
+ }
202
+ }
203
+
204
+ private _onUserNavigation() {
205
+ this._stopAutoplayIfRunning();
206
+ this.stopDotAnimation();
207
+ }
208
+
209
+ _onTabClick(e: Event) {
210
+ e.preventDefault();
211
+ const tab = e.currentTarget as HTMLElement;
212
+ const index = this.tabs.indexOf(tab);
213
+ if (index !== -1 && index !== this.instance?.activeIndex) {
214
+ this._onUserNavigation();
215
+ this.goToSlide(index);
216
+ }
217
+ }
218
+
219
+ private _onPlayPauseClick() {
220
+ this.toggleAutoplay();
221
+ }
222
+
223
+ // ====== Autoplay helpers ======
224
+ private isAutoplayRunning(): boolean {
225
+ return !!(this.instance?.autoplay && this.instance.autoplay.running);
226
+ }
227
+
228
+ private _stopAutoplayIfRunning() {
229
+ if (this.isAutoplayRunning()) {
230
+ this.instance.autoplay.stop();
231
+ }
232
+ }
233
+
234
+ private _onAutoplayStart() {
235
+ this.element.classList.add(CLASS_PLAYING);
236
+ this.element.classList.remove(CLASS_PAUSED);
237
+ this.updatePlayPauseIcon();
238
+ this.startDotAnimation();
239
+ }
240
+
241
+ private _onAutoplayStop() {
242
+ this.element.classList.remove(CLASS_PLAYING);
243
+ this.element.classList.add(CLASS_PAUSED);
244
+ this.updatePlayPauseIcon();
245
+ this.stopDotAnimation();
246
+ }
247
+
248
+ toggleAutoplay() {
249
+ if (!this.instance || !this.instance.autoplay) return;
250
+
251
+ if (this.isAutoplayRunning()) {
252
+ this.instance.autoplay.stop();
253
+ } else {
254
+ // Force Swiper to be on the correct slide before starting autoplay
255
+ const currentRealIndex = this.instance.realIndex;
256
+
257
+ // Ensure we're on the right slide (accounting for loop mode)
258
+ if (
259
+ this.instance.activeIndex !==
260
+ this.instance.slides.length + currentRealIndex
261
+ ) {
262
+ this.instance.slideTo(
263
+ this.instance.slides.length + currentRealIndex,
264
+ 0,
265
+ );
266
+ }
267
+
268
+ // Small delay to ensure slide position is stable before starting autoplay
269
+ setTimeout(() => {
270
+ if (this.instance && this.instance.autoplay) {
271
+ this.instance.autoplay.start();
272
+ }
273
+ }, 10);
274
+ }
275
+ }
276
+
277
+ goToSlide(index: number) {
278
+ if (!this.instance || this.instance.activeIndex === index) return;
279
+ this.instance.slideTo(index);
280
+ }
281
+
282
+ updateStates() {
283
+ if (!this.instance) return;
284
+
285
+ // Use realIndex for loop mode, fallback to activeIndex
286
+ const realIndex = this.instance.realIndex ?? this.instance.activeIndex;
287
+
288
+ // Update tab states
289
+ this.tabs.forEach((tab, index) => {
290
+ const isActive = index === realIndex;
291
+ tab.classList.toggle(CLASS_ACTIVE, isActive);
292
+ tab.setAttribute("aria-selected", String(isActive));
293
+ tab.setAttribute("tabindex", isActive ? "0" : "-1");
294
+ });
295
+
296
+ this.scrollActiveTabIntoView(realIndex);
297
+
298
+ // Manually update pagination bullets
299
+ this.updatePaginationBullets(realIndex);
300
+ }
301
+
302
+ private updatePaginationBullets(activeIndex: number) {
303
+ const paginationEl = this.element.querySelector(SELECTOR_PAGINATION);
304
+ if (!paginationEl) return;
305
+
306
+ const bullets = paginationEl.querySelectorAll(`.${CLASS_PAGINATION_ITEM}`);
307
+
308
+ // With loop enabled, we need to get the real slide index
309
+ const realIndex = this.instance?.realIndex ?? activeIndex;
310
+
311
+ bullets.forEach((bullet, index) => {
312
+ const isActive = index === realIndex;
313
+ bullet.classList.toggle("is-active", isActive);
314
+
315
+ // For carousels without autoplay, manually show/hide the SVG
316
+ if (!this.hasAutoplay()) {
317
+ const svg = bullet.querySelector(
318
+ `.${CLASS_PAGINATION_SVG}`,
319
+ ) as HTMLElement;
320
+ if (svg) {
321
+ svg.style.display = isActive ? "block" : "none";
322
+ }
323
+ }
324
+ });
325
+ }
326
+ scrollActiveTabIntoView(activeIndex: number) {
327
+ if (!this.tabs.length || activeIndex < 0 || activeIndex >= this.tabs.length)
328
+ return;
329
+
330
+ const activeTab = this.tabs[activeIndex];
331
+ const tabsContainer = this.element.querySelector(
332
+ SELECTOR_TABS,
333
+ ) as HTMLElement;
334
+ if (!tabsContainer || !activeTab) return;
335
+
336
+ const containerRect = tabsContainer.getBoundingClientRect();
337
+ const tabRect = activeTab.getBoundingClientRect();
338
+ const fadeWidth = 56;
339
+ const containerVisibleWidth = containerRect.width - fadeWidth;
340
+
341
+ if (
342
+ tabRect.right > containerRect.left + containerVisibleWidth ||
343
+ tabRect.left < containerRect.left
344
+ ) {
345
+ const scrollLeft =
346
+ tabRect.left -
347
+ containerRect.left -
348
+ containerVisibleWidth / 2 +
349
+ tabRect.width / 2;
350
+ tabsContainer.scrollBy({ left: scrollLeft, behavior: "smooth" });
351
+ }
352
+ }
353
+
354
+ updatePlayPauseIcon() {
355
+ const playPauseButton = this.element.querySelector(SELECTOR_PLAY_PAUSE);
356
+ if (!playPauseButton) return;
357
+
358
+ const useElement = playPauseButton.querySelector("use");
359
+ if (useElement) {
360
+ const isPlaying = this.isAutoplayRunning();
361
+ const newIcon = isPlaying ? "pause" : "play";
362
+ const currentHref = useElement.getAttribute("xlink:href") || "";
363
+ if (!currentHref.endsWith(`#${newIcon}`)) {
364
+ const newHref = currentHref.replace(/#[\w-]+$/, `#${newIcon}`);
365
+ useElement.setAttribute("xlink:href", newHref);
366
+ }
367
+ }
368
+ }
369
+
370
+ renderPaginationDots() {
371
+ const paginationButtons = this._getPaginationButtons();
372
+
373
+ paginationButtons.forEach((button) => {
374
+ const svg = this.createSvgDot();
375
+ button.innerHTML = "";
376
+ button.appendChild(svg);
377
+ });
378
+ }
379
+
380
+ startDotAnimation(force: boolean = false) {
381
+ if (!this.hasAutoplay()) return;
382
+ if (!force && !this.isAutoplayRunning()) return;
383
+ if (this._dotAnimationJustStarted) return;
384
+
385
+ this._dotAnimationJustStarted = true;
386
+
387
+ const activeButton = this._getActiveBulletButton();
388
+ if (!activeButton) {
389
+ this._dotAnimationJustStarted = false;
390
+ return;
391
+ }
392
+
393
+ const svg = activeButton.querySelector(
394
+ `.${CLASS_PAGINATION_SVG}`,
395
+ ) as SVGSVGElement | null;
396
+ if (!svg) {
397
+ this._dotAnimationJustStarted = false;
398
+ return;
399
+ }
400
+
401
+ const circle = svg.querySelector("circle") as SVGCircleElement | null;
402
+ if (!circle) {
403
+ this._dotAnimationJustStarted = false;
404
+ return;
405
+ }
406
+
407
+ const duration = this.getAnimationDuration();
408
+ circle.style.animation = "none";
409
+ // Force reflow
410
+ void activeButton.offsetHeight;
411
+ circle.style.animation = `countdown linear ${duration}ms 1 forwards`;
412
+ this._dotAnimationJustStarted = false;
413
+ }
414
+ restartDotAnimation() {
415
+ this.stopDotAnimation();
416
+ setTimeout(() => {
417
+ this.startDotAnimation();
418
+ }, 50);
419
+ }
420
+
421
+ stopDotAnimation() {
422
+ const paginationButtons = this._getPaginationButtons();
423
+ paginationButtons.forEach((button) => {
424
+ const svg = button.querySelector(
425
+ `.${CLASS_PAGINATION_SVG}`,
426
+ ) as SVGSVGElement | null;
427
+ if (!svg) return;
428
+ const circle = svg.querySelector("circle") as SVGCircleElement | null;
429
+ if (!circle) return;
430
+ circle.style.animation = "none";
431
+ });
432
+ this._dotAnimationJustStarted = false;
433
+ }
434
+
435
+ private _getPaginationButtons(): HTMLElement[] {
436
+ return Array.from(
437
+ this.element.querySelectorAll(`${SELECTOR_PAGINATION} > *`),
438
+ ) as HTMLElement[];
439
+ }
440
+
441
+ private _getActiveBulletClass(): string {
442
+ const pagination = (this.instance?.params as any)?.pagination;
443
+ if (
444
+ pagination &&
445
+ typeof pagination === "object" &&
446
+ pagination.bulletActiveClass
447
+ ) {
448
+ return String(pagination.bulletActiveClass);
449
+ }
450
+ const cfgPagination = this.config.pagination as any;
451
+ if (
452
+ cfgPagination &&
453
+ typeof cfgPagination === "object" &&
454
+ cfgPagination.bulletActiveClass
455
+ ) {
456
+ return String(cfgPagination.bulletActiveClass);
457
+ }
458
+ return CLASS_ACTIVE;
459
+ }
460
+
461
+ private _getActiveBulletButton(): HTMLElement | null {
462
+ const activeClass = this._getActiveBulletClass();
463
+ return this.element.querySelector(
464
+ `${SELECTOR_PAGINATION} .${activeClass}`,
465
+ ) as HTMLElement | null;
466
+ }
467
+
468
+ createSvgDot(): SVGSVGElement {
469
+ const svgNS = "http://www.w3.org/2000/svg";
470
+ const svg = document.createElementNS(svgNS, "svg");
471
+ svg.setAttribute("class", CLASS_PAGINATION_SVG);
472
+ svg.setAttribute("width", "12");
473
+ svg.setAttribute("height", "12");
474
+ svg.setAttribute("viewBox", "0 0 12 12");
475
+
476
+ const circle = document.createElementNS(svgNS, "circle");
477
+ circle.setAttribute("class", CLASS_PAGINATION_CIRCLE);
478
+ circle.setAttribute("r", "5");
479
+ circle.setAttribute("cx", "6");
480
+ circle.setAttribute("cy", "6");
481
+
482
+ svg.appendChild(circle);
483
+ return svg;
484
+ }
485
+
486
+ // ====== Autoplay config helpers ======
487
+ hasAutoplay(): boolean {
488
+ return !!(
489
+ this.config.autoplay &&
490
+ typeof this.config.autoplay === "object" &&
491
+ (this.config.autoplay as any).delay >= 1000
492
+ );
493
+ }
494
+
495
+ getAnimationDuration(): number {
496
+ return (this.config.autoplay as any)?.delay || 0;
497
+ }
498
+
499
+ destroy() {
500
+ this.tabs.forEach((tab) => {
501
+ tab.removeEventListener("click", this._boundTabClick);
502
+ });
503
+
504
+ const playPauseButton = this.element.querySelector(SELECTOR_PLAY_PAUSE);
505
+ if (playPauseButton) {
506
+ playPauseButton.removeEventListener("click", this._boundPlayPauseClick);
507
+ }
508
+
509
+ const prevButton = this.element.querySelector(SELECTOR_PREV);
510
+ const nextButton = this.element.querySelector(SELECTOR_NEXT);
511
+ if (prevButton) {
512
+ prevButton.removeEventListener("click", this._boundPrevClick);
513
+ }
514
+ if (nextButton) {
515
+ nextButton.removeEventListener("click", this._boundNextClick);
516
+ }
517
+
518
+ if (this.instance) {
519
+ this.instance.destroy(true, true);
520
+ }
521
+
522
+ delete (this.element as any).ODS_CarouselHero;
523
+ }
524
+
525
+ static getInstance(el: HTMLElement): CarouselHero | null {
526
+ return (el as any).ODS_CarouselHero || null;
527
+ }
528
+ }
@@ -0,0 +1,148 @@
1
+ "use client";
2
+
3
+ import React, { ReactNode } from "react";
4
+ import cx from "classnames";
5
+
6
+ import { Controls } from "../Controls";
7
+ import { CarouselHeroItem } from "./CarouselHeroItem";
8
+ import CarouselHeroStatic from "./CarouselHero.static";
9
+ import {
10
+ CLASS_ROOT,
11
+ CLASS_VIEWPORT_WRAPPER,
12
+ CLASS_VIEWPORT,
13
+ CLASS_PREV,
14
+ CLASS_NEXT,
15
+ CLASS_PLAY_PAUSE,
16
+ CLASS_TABS,
17
+ CLASS_TAB,
18
+ CLASS_PAGINATION,
19
+ CLASS_CONTROLS,
20
+ CLASS_TRACK,
21
+ CLASS_NAVIGATION,
22
+ } from "./constants";
23
+ import { useStatic } from "@/utils/hooks";
24
+
25
+ interface TabItem {
26
+ label: string;
27
+ [key: string]: any;
28
+ }
29
+
30
+ interface CarouselHeroProps {
31
+ className?: string;
32
+ swiperOptions?: Record<string, any>;
33
+ colorScheme?: "light" | "dark";
34
+ children?: ReactNode;
35
+ tabs?: TabItem[];
36
+ interval?: number;
37
+ [key: string]: any;
38
+ }
39
+
40
+ const CarouselHero: React.FC<CarouselHeroProps> = ({
41
+ className,
42
+ swiperOptions,
43
+ colorScheme,
44
+ children,
45
+ tabs = [],
46
+ interval,
47
+ ...other
48
+ }) => {
49
+ const [carouselRef] = useStatic(CarouselHeroStatic);
50
+
51
+ const classes = cx(CLASS_ROOT, className, {
52
+ "is-light": colorScheme === "light",
53
+ "is-dark": colorScheme === "dark",
54
+ });
55
+
56
+ const elementClasses = {
57
+ prev: cx(CLASS_PREV),
58
+ next: cx(CLASS_NEXT),
59
+ playPause: cx(CLASS_PLAY_PAUSE),
60
+ tabs: cx(CLASS_TABS),
61
+ tab: cx(CLASS_TAB),
62
+ pagination: cx(CLASS_PAGINATION),
63
+ controls: cx(CLASS_CONTROLS),
64
+ };
65
+
66
+ const hasAutoplay =
67
+ (swiperOptions?.autoplay &&
68
+ typeof swiperOptions.autoplay === "object" &&
69
+ (swiperOptions.autoplay as any).delay >= 1000) ||
70
+ (interval && interval >= 1000);
71
+
72
+ const playPauseIcon = "pause";
73
+
74
+ return (
75
+ <div
76
+ className={classes}
77
+ ref={carouselRef}
78
+ data-carousel-hero
79
+ {...(interval && interval >= 1000 ? { "data-interval": interval } : {})}
80
+ {...(swiperOptions
81
+ ? { "data-swiper-options": JSON.stringify(swiperOptions) }
82
+ : {})}
83
+ {...other}
84
+ >
85
+ <div className={CLASS_VIEWPORT_WRAPPER}>
86
+ <div className={CLASS_VIEWPORT}>
87
+ <div className={CLASS_TRACK}>{children}</div>
88
+ </div>
89
+ </div>
90
+
91
+ {/* Controls container */}
92
+ <div className={elementClasses.controls}>
93
+ {/* Tab navigation */}
94
+ {tabs.length > 0 && (
95
+ <div className={elementClasses.tabs} role="tablist">
96
+ {tabs.map((tab, index) => (
97
+ <button
98
+ key={index}
99
+ type="button"
100
+ className={elementClasses.tab}
101
+ role="tab"
102
+ aria-selected={index === 0 ? "true" : "false"}
103
+ tabIndex={index === 0 ? 0 : -1}
104
+ >
105
+ {tab.label}
106
+ </button>
107
+ ))}
108
+ </div>
109
+ )}
110
+
111
+ {/* Pagination dots for mobile */}
112
+ <div role="tablist" className={elementClasses.pagination} />
113
+
114
+ {/* Navigation controls */}
115
+ <div className={CLASS_NAVIGATION}>
116
+ <Controls
117
+ className={elementClasses.prev}
118
+ icon="chevron-left"
119
+ colorScheme={colorScheme}
120
+ aria-label="Predchádzajúci snímok"
121
+ />
122
+
123
+ <Controls
124
+ className={elementClasses.next}
125
+ icon="chevron-right"
126
+ colorScheme={colorScheme}
127
+ aria-label="Nasledujúci snímok"
128
+ />
129
+
130
+ {/* Only show play/pause button if autoplay is configured */}
131
+ {hasAutoplay && (
132
+ <Controls
133
+ className={elementClasses.playPause}
134
+ icon={playPauseIcon}
135
+ colorScheme={colorScheme}
136
+ aria-label="Pozastaviť/Spustiť automatické prehrávanie"
137
+ />
138
+ )}
139
+ </div>
140
+ </div>
141
+ </div>
142
+ );
143
+ };
144
+
145
+ CarouselHero.displayName = "CarouselHero";
146
+
147
+ export { CarouselHero, CarouselHeroItem };
148
+ export type { CarouselHeroProps, TabItem };