tekivex-ui 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/dist/index.cjs +70 -0
  4. package/dist/index.d.ts +89 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +11002 -0
  7. package/dist/src/a11y/index.d.ts +24 -0
  8. package/dist/src/a11y/index.d.ts.map +1 -0
  9. package/dist/src/components/TkxAccordion.d.ts +32 -0
  10. package/dist/src/components/TkxAccordion.d.ts.map +1 -0
  11. package/dist/src/components/TkxAlert.d.ts +14 -0
  12. package/dist/src/components/TkxAlert.d.ts.map +1 -0
  13. package/dist/src/components/TkxAvatar.d.ts +14 -0
  14. package/dist/src/components/TkxAvatar.d.ts.map +1 -0
  15. package/dist/src/components/TkxBadge.d.ts +12 -0
  16. package/dist/src/components/TkxBadge.d.ts.map +1 -0
  17. package/dist/src/components/TkxButton.d.ts +17 -0
  18. package/dist/src/components/TkxButton.d.ts.map +1 -0
  19. package/dist/src/components/TkxCard.d.ts +20 -0
  20. package/dist/src/components/TkxCard.d.ts.map +1 -0
  21. package/dist/src/components/TkxCarousel.d.ts +32 -0
  22. package/dist/src/components/TkxCarousel.d.ts.map +1 -0
  23. package/dist/src/components/TkxChat.d.ts +32 -0
  24. package/dist/src/components/TkxChat.d.ts.map +1 -0
  25. package/dist/src/components/TkxCheckbox.d.ts +13 -0
  26. package/dist/src/components/TkxCheckbox.d.ts.map +1 -0
  27. package/dist/src/components/TkxClock.d.ts +23 -0
  28. package/dist/src/components/TkxClock.d.ts.map +1 -0
  29. package/dist/src/components/TkxColorPicker.d.ts +18 -0
  30. package/dist/src/components/TkxColorPicker.d.ts.map +1 -0
  31. package/dist/src/components/TkxCommand.d.ts +34 -0
  32. package/dist/src/components/TkxCommand.d.ts.map +1 -0
  33. package/dist/src/components/TkxDatePicker.d.ts +47 -0
  34. package/dist/src/components/TkxDatePicker.d.ts.map +1 -0
  35. package/dist/src/components/TkxDivider.d.ts +8 -0
  36. package/dist/src/components/TkxDivider.d.ts.map +1 -0
  37. package/dist/src/components/TkxDrawer.d.ts +16 -0
  38. package/dist/src/components/TkxDrawer.d.ts.map +1 -0
  39. package/dist/src/components/TkxFileUpload.d.ts +16 -0
  40. package/dist/src/components/TkxFileUpload.d.ts.map +1 -0
  41. package/dist/src/components/TkxIcon.d.ts +16 -0
  42. package/dist/src/components/TkxIcon.d.ts.map +1 -0
  43. package/dist/src/components/TkxImage.d.ts +16 -0
  44. package/dist/src/components/TkxImage.d.ts.map +1 -0
  45. package/dist/src/components/TkxInput.d.ts +13 -0
  46. package/dist/src/components/TkxInput.d.ts.map +1 -0
  47. package/dist/src/components/TkxLogo.d.ts +21 -0
  48. package/dist/src/components/TkxLogo.d.ts.map +1 -0
  49. package/dist/src/components/TkxMenu.d.ts +64 -0
  50. package/dist/src/components/TkxMenu.d.ts.map +1 -0
  51. package/dist/src/components/TkxModal.d.ts +14 -0
  52. package/dist/src/components/TkxModal.d.ts.map +1 -0
  53. package/dist/src/components/TkxNumberInput.d.ts +32 -0
  54. package/dist/src/components/TkxNumberInput.d.ts.map +1 -0
  55. package/dist/src/components/TkxOTP.d.ts +24 -0
  56. package/dist/src/components/TkxOTP.d.ts.map +1 -0
  57. package/dist/src/components/TkxPagination.d.ts +19 -0
  58. package/dist/src/components/TkxPagination.d.ts.map +1 -0
  59. package/dist/src/components/TkxProgress.d.ts +13 -0
  60. package/dist/src/components/TkxProgress.d.ts.map +1 -0
  61. package/dist/src/components/TkxRadio.d.ts +22 -0
  62. package/dist/src/components/TkxRadio.d.ts.map +1 -0
  63. package/dist/src/components/TkxRating.d.ts +18 -0
  64. package/dist/src/components/TkxRating.d.ts.map +1 -0
  65. package/dist/src/components/TkxSelect.d.ts +37 -0
  66. package/dist/src/components/TkxSelect.d.ts.map +1 -0
  67. package/dist/src/components/TkxSkeleton.d.ts +12 -0
  68. package/dist/src/components/TkxSkeleton.d.ts.map +1 -0
  69. package/dist/src/components/TkxSlider.d.ts +33 -0
  70. package/dist/src/components/TkxSlider.d.ts.map +1 -0
  71. package/dist/src/components/TkxStepper.d.ts +31 -0
  72. package/dist/src/components/TkxStepper.d.ts.map +1 -0
  73. package/dist/src/components/TkxTable.d.ts +23 -0
  74. package/dist/src/components/TkxTable.d.ts.map +1 -0
  75. package/dist/src/components/TkxTabs.d.ts +32 -0
  76. package/dist/src/components/TkxTabs.d.ts.map +1 -0
  77. package/dist/src/components/TkxTag.d.ts +30 -0
  78. package/dist/src/components/TkxTag.d.ts.map +1 -0
  79. package/dist/src/components/TkxTimeline.d.ts +20 -0
  80. package/dist/src/components/TkxTimeline.d.ts.map +1 -0
  81. package/dist/src/components/TkxToast.d.ts +23 -0
  82. package/dist/src/components/TkxToast.d.ts.map +1 -0
  83. package/dist/src/components/TkxToggle.d.ts +11 -0
  84. package/dist/src/components/TkxToggle.d.ts.map +1 -0
  85. package/dist/src/components/TkxTooltip.d.ts +10 -0
  86. package/dist/src/components/TkxTooltip.d.ts.map +1 -0
  87. package/dist/src/components/TkxVideoPlayer.d.ts +43 -0
  88. package/dist/src/components/TkxVideoPlayer.d.ts.map +1 -0
  89. package/dist/src/components/index.d.ts +41 -0
  90. package/dist/src/components/index.d.ts.map +1 -0
  91. package/dist/src/engine/css.d.ts +24 -0
  92. package/dist/src/engine/css.d.ts.map +1 -0
  93. package/dist/src/engine/index.d.ts +10 -0
  94. package/dist/src/engine/index.d.ts.map +1 -0
  95. package/dist/src/engine/quantum.d.ts +47 -0
  96. package/dist/src/engine/quantum.d.ts.map +1 -0
  97. package/dist/src/engine/security.d.ts +54 -0
  98. package/dist/src/engine/security.d.ts.map +1 -0
  99. package/dist/src/engine/tkx.d.ts +29 -0
  100. package/dist/src/engine/tkx.d.ts.map +1 -0
  101. package/dist/src/engine/wcag.d.ts +34 -0
  102. package/dist/src/engine/wcag.d.ts.map +1 -0
  103. package/dist/src/hooks/index.d.ts +8 -0
  104. package/dist/src/hooks/index.d.ts.map +1 -0
  105. package/dist/src/themes/index.d.ts +26 -0
  106. package/dist/src/themes/index.d.ts.map +1 -0
  107. package/dist/style.css +1 -0
  108. package/dist/tests/TkxButton.test.d.ts +2 -0
  109. package/dist/tests/TkxButton.test.d.ts.map +1 -0
  110. package/dist/tests/a11y/components.test.d.ts +2 -0
  111. package/dist/tests/a11y/components.test.d.ts.map +1 -0
  112. package/dist/tests/quantum.test.d.ts +2 -0
  113. package/dist/tests/quantum.test.d.ts.map +1 -0
  114. package/dist/tests/security.test.d.ts +2 -0
  115. package/dist/tests/security.test.d.ts.map +1 -0
  116. package/dist/tests/setup.d.ts +2 -0
  117. package/dist/tests/setup.d.ts.map +1 -0
  118. package/dist/tests/tkx.test.d.ts +2 -0
  119. package/dist/tests/tkx.test.d.ts.map +1 -0
  120. package/dist/tests/wcag.test.d.ts +2 -0
  121. package/dist/tests/wcag.test.d.ts.map +1 -0
  122. package/package.json +78 -0
  123. package/src/a11y/index.tsx +87 -0
  124. package/src/components/TkxAccordion.tsx +588 -0
  125. package/src/components/TkxAlert.tsx +91 -0
  126. package/src/components/TkxAvatar.tsx +101 -0
  127. package/src/components/TkxBadge.tsx +73 -0
  128. package/src/components/TkxButton.tsx +153 -0
  129. package/src/components/TkxCard.tsx +92 -0
  130. package/src/components/TkxCarousel.tsx +552 -0
  131. package/src/components/TkxChat.tsx +354 -0
  132. package/src/components/TkxCheckbox.tsx +218 -0
  133. package/src/components/TkxClock.tsx +601 -0
  134. package/src/components/TkxColorPicker.tsx +633 -0
  135. package/src/components/TkxCommand.tsx +644 -0
  136. package/src/components/TkxDatePicker.tsx +1567 -0
  137. package/src/components/TkxDivider.tsx +49 -0
  138. package/src/components/TkxDrawer.tsx +234 -0
  139. package/src/components/TkxFileUpload.tsx +371 -0
  140. package/src/components/TkxIcon.tsx +683 -0
  141. package/src/components/TkxImage.tsx +277 -0
  142. package/src/components/TkxInput.tsx +101 -0
  143. package/src/components/TkxLogo.tsx +297 -0
  144. package/src/components/TkxMenu.tsx +1042 -0
  145. package/src/components/TkxModal.tsx +119 -0
  146. package/src/components/TkxNumberInput.tsx +408 -0
  147. package/src/components/TkxOTP.tsx +295 -0
  148. package/src/components/TkxPagination.tsx +388 -0
  149. package/src/components/TkxProgress.tsx +114 -0
  150. package/src/components/TkxRadio.tsx +284 -0
  151. package/src/components/TkxRating.tsx +286 -0
  152. package/src/components/TkxSelect.tsx +923 -0
  153. package/src/components/TkxSkeleton.tsx +105 -0
  154. package/src/components/TkxSlider.tsx +492 -0
  155. package/src/components/TkxStepper.tsx +485 -0
  156. package/src/components/TkxTable.tsx +148 -0
  157. package/src/components/TkxTabs.tsx +124 -0
  158. package/src/components/TkxTag.tsx +319 -0
  159. package/src/components/TkxTimeline.tsx +303 -0
  160. package/src/components/TkxToast.tsx +284 -0
  161. package/src/components/TkxToggle.tsx +93 -0
  162. package/src/components/TkxTooltip.tsx +69 -0
  163. package/src/components/TkxVideoPlayer.tsx +1013 -0
  164. package/src/components/index.ts +40 -0
  165. package/src/engine/css.ts +126 -0
  166. package/src/engine/index.ts +10 -0
  167. package/src/engine/quantum.ts +207 -0
  168. package/src/engine/security.ts +217 -0
  169. package/src/engine/tkx.ts +874 -0
  170. package/src/engine/wcag.ts +275 -0
  171. package/src/hooks/index.ts +120 -0
  172. package/src/styles/global.css +167 -0
  173. package/src/themes/index.ts +111 -0
@@ -0,0 +1,552 @@
1
+ import {
2
+ useState,
3
+ useRef,
4
+ useEffect,
5
+ useCallback,
6
+ useId,
7
+ type CSSProperties,
8
+ type ReactNode,
9
+ type TouchEvent,
10
+ type MouseEvent as ReactMouseEvent,
11
+ } from 'react';
12
+ import { useTheme } from '../themes';
13
+ import { tkx, cx } from '../engine/tkx';
14
+ import { useReducedMotion } from '../hooks';
15
+
16
+ // ── Types ─────────────────────────────────────────────────────────────────────
17
+
18
+ export interface CarouselSlide {
19
+ id: string;
20
+ content: ReactNode;
21
+ thumbnail?: string;
22
+ }
23
+
24
+ export interface TkxCarouselProps {
25
+ slides: CarouselSlide[];
26
+ autoPlay?: boolean;
27
+ autoPlayInterval?: number;
28
+ pauseOnHover?: boolean;
29
+ loop?: boolean;
30
+ showArrows?: boolean;
31
+ showDots?: boolean;
32
+ showThumbnails?: boolean;
33
+ orientation?: 'horizontal' | 'vertical';
34
+ slidesToShow?: number;
35
+ gap?: number;
36
+ transitionDuration?: number;
37
+ swipeable?: boolean;
38
+ initialIndex?: number;
39
+ onChange?: (index: number) => void;
40
+ arrowPosition?: 'inside' | 'outside';
41
+ height?: string | number;
42
+ className?: string;
43
+ style?: CSSProperties;
44
+ }
45
+
46
+ // ── Arrow button ──────────────────────────────────────────────────────────────
47
+
48
+ interface ArrowBtnProps {
49
+ direction: 'prev' | 'next';
50
+ onClick: () => void;
51
+ isDisabled: boolean;
52
+ primaryColor: string;
53
+ position: 'inside' | 'outside';
54
+ orientation: 'horizontal' | 'vertical';
55
+ }
56
+
57
+ function ArrowBtn({ direction, onClick, isDisabled, primaryColor, position, orientation }: ArrowBtnProps) {
58
+ const isVertical = orientation === 'vertical';
59
+ const isPrev = direction === 'prev';
60
+
61
+ const posStyle: CSSProperties = position === 'inside'
62
+ ? {
63
+ position: 'absolute',
64
+ zIndex: 3,
65
+ ...(isVertical
66
+ ? { [isPrev ? 'top' : 'bottom']: 10, left: '50%', transform: 'translateX(-50%)' }
67
+ : { [isPrev ? 'left' : 'right']: 10, top: '50%', transform: 'translateY(-50%)' }),
68
+ }
69
+ : {
70
+ flexShrink: 0,
71
+ position: 'relative',
72
+ };
73
+
74
+ return (
75
+ <button
76
+ type="button"
77
+ aria-label={isPrev ? 'Previous slide' : 'Next slide'}
78
+ disabled={isDisabled}
79
+ onClick={onClick}
80
+ style={{
81
+ ...posStyle,
82
+ width: 36,
83
+ height: 36,
84
+ borderRadius: '50%',
85
+ border: 'none',
86
+ backgroundColor: 'rgba(0,0,0,0.45)',
87
+ backdropFilter: 'blur(4px)',
88
+ color: '#fff',
89
+ display: 'flex',
90
+ alignItems: 'center',
91
+ justifyContent: 'center',
92
+ cursor: isDisabled ? 'not-allowed' : 'pointer',
93
+ opacity: isDisabled ? 0.35 : 1,
94
+ transition: 'background 150ms ease, opacity 150ms ease',
95
+ outline: 'none',
96
+ }}
97
+ onFocus={(e) => { (e.currentTarget as HTMLElement).style.boxShadow = `0 0 0 3px ${primaryColor}55`; }}
98
+ onBlur={(e) => { (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
99
+ onMouseEnter={(e) => { if (!isDisabled) (e.currentTarget as HTMLElement).style.backgroundColor = 'rgba(0,0,0,0.65)'; }}
100
+ onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.backgroundColor = 'rgba(0,0,0,0.45)'; }}
101
+ >
102
+ {isVertical ? (
103
+ isPrev ? (
104
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} aria-hidden="true">
105
+ <polyline points="18 15 12 9 6 15" />
106
+ </svg>
107
+ ) : (
108
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} aria-hidden="true">
109
+ <polyline points="6 9 12 15 18 9" />
110
+ </svg>
111
+ )
112
+ ) : isPrev ? (
113
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} aria-hidden="true">
114
+ <polyline points="15 18 9 12 15 6" />
115
+ </svg>
116
+ ) : (
117
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.5} aria-hidden="true">
118
+ <polyline points="9 18 15 12 9 6" />
119
+ </svg>
120
+ )}
121
+ </button>
122
+ );
123
+ }
124
+
125
+ // ── Main component ────────────────────────────────────────────────────────────
126
+
127
+ export function TkxCarousel({
128
+ slides,
129
+ autoPlay = false,
130
+ autoPlayInterval = 4000,
131
+ pauseOnHover = true,
132
+ loop = true,
133
+ showArrows = true,
134
+ showDots = true,
135
+ showThumbnails = false,
136
+ orientation = 'horizontal',
137
+ slidesToShow = 1,
138
+ gap = 0,
139
+ transitionDuration = 400,
140
+ swipeable = true,
141
+ initialIndex = 0,
142
+ onChange,
143
+ arrowPosition = 'inside',
144
+ height = 320,
145
+ className,
146
+ style,
147
+ }: TkxCarouselProps) {
148
+ const theme = useTheme();
149
+ const reducedMotion = useReducedMotion();
150
+ const carouselId = useId();
151
+
152
+ const count = slides.length;
153
+
154
+ // ── Index state ──────────────────────────────────────────────────────────
155
+ // When loop=true we clone first & last slides → real slides are at indices 1..count
156
+ // displayed index is the "real" index (0-based, 0..count-1)
157
+ const [currentIdx, setCurrentIdx] = useState(initialIndex);
158
+ // internal track index (for cloned-slide loop trick)
159
+ const [trackIdx, setTrackIdx] = useState(loop ? initialIndex + 1 : initialIndex);
160
+ const [isTransitioning, setIsTransitioning] = useState(false);
161
+ const [paused, setPaused] = useState(false);
162
+
163
+ const trackRef = useRef<HTMLDivElement>(null);
164
+ const autoPlayRef = useRef<ReturnType<typeof setInterval> | null>(null);
165
+
166
+ // Drag / swipe state
167
+ const dragStartRef = useRef<number | null>(null);
168
+ const dragDeltaRef = useRef<number>(0);
169
+ const isDraggingRef = useRef(false);
170
+
171
+ const isVertical = orientation === 'vertical';
172
+ const actualDuration = reducedMotion ? 0 : transitionDuration;
173
+
174
+ // Slides to render (with optional clones for seamless loop)
175
+ const renderSlides: CarouselSlide[] = loop && count > 1
176
+ ? [slides[count - 1], ...slides, slides[0]]
177
+ : slides;
178
+
179
+ const totalTracks = renderSlides.length;
180
+
181
+ // Compute transform for current trackIdx
182
+ const slideWidthPct = 100 / slidesToShow;
183
+
184
+ const getTransform = (idx: number, extra = 0) => {
185
+ const px = extra !== 0 ? ` - ${extra}px` : '';
186
+ if (isVertical) return `translateY(calc(-${idx * slideWidthPct}%${px}))`;
187
+ return `translateX(calc(-${idx * slideWidthPct}%${px}))`;
188
+ };
189
+
190
+ // Navigate to a real index
191
+ const goTo = useCallback((realIdx: number, skipTransition = false) => {
192
+ const clamped = loop
193
+ ? ((realIdx % count) + count) % count
194
+ : Math.max(0, Math.min(count - 1, realIdx));
195
+
196
+ const newTrackIdx = loop ? clamped + 1 : clamped;
197
+
198
+ if (skipTransition && trackRef.current) {
199
+ trackRef.current.style.transition = 'none';
200
+ setTrackIdx(newTrackIdx);
201
+ setCurrentIdx(clamped);
202
+ // Force reflow
203
+ void trackRef.current.offsetHeight;
204
+ trackRef.current.style.transition = '';
205
+ } else {
206
+ setIsTransitioning(true);
207
+ setTrackIdx(newTrackIdx);
208
+ setCurrentIdx(clamped);
209
+ }
210
+
211
+ onChange?.(clamped);
212
+ }, [count, loop, onChange]);
213
+
214
+ const prev = useCallback(() => goTo(currentIdx - 1), [currentIdx, goTo]);
215
+ const next = useCallback(() => goTo(currentIdx + 1), [currentIdx, goTo]);
216
+
217
+ // Handle seamless loop: after transition ends, snap without animation
218
+ const handleTransitionEnd = useCallback(() => {
219
+ setIsTransitioning(false);
220
+ if (!loop || count <= 1) return;
221
+
222
+ if (trackIdx === 0) {
223
+ // Was on cloned-last → snap to real last
224
+ goTo(count - 1, true);
225
+ } else if (trackIdx === totalTracks - 1) {
226
+ // Was on cloned-first → snap to real first
227
+ goTo(0, true);
228
+ }
229
+ }, [loop, count, trackIdx, totalTracks, goTo]);
230
+
231
+ // Auto-play
232
+ useEffect(() => {
233
+ if (!autoPlay || paused || count <= 1) return;
234
+ autoPlayRef.current = setInterval(next, autoPlayInterval);
235
+ return () => { if (autoPlayRef.current) clearInterval(autoPlayRef.current); };
236
+ }, [autoPlay, paused, autoPlayInterval, next, count]);
237
+
238
+ // Keyboard navigation
239
+ useEffect(() => {
240
+ const handler = (e: KeyboardEvent) => {
241
+ const focused = document.activeElement;
242
+ const container = document.getElementById(carouselId);
243
+ if (!container?.contains(focused as Node)) return;
244
+
245
+ if (e.key === (isVertical ? 'ArrowUp' : 'ArrowLeft')) { e.preventDefault(); prev(); }
246
+ if (e.key === (isVertical ? 'ArrowDown' : 'ArrowRight')) { e.preventDefault(); next(); }
247
+ };
248
+ document.addEventListener('keydown', handler);
249
+ return () => document.removeEventListener('keydown', handler);
250
+ }, [carouselId, isVertical, prev, next]);
251
+
252
+ // ── Swipe handling ────────────────────────────────────────────────────────
253
+
254
+ const getEventCoord = (e: TouchEvent | ReactMouseEvent): number => {
255
+ if ('touches' in e) {
256
+ return isVertical ? e.touches[0].clientY : e.touches[0].clientX;
257
+ }
258
+ return isVertical ? (e as ReactMouseEvent).clientY : (e as ReactMouseEvent).clientX;
259
+ };
260
+
261
+ const onDragStart = (e: TouchEvent | ReactMouseEvent) => {
262
+ if (!swipeable) return;
263
+ dragStartRef.current = getEventCoord(e);
264
+ dragDeltaRef.current = 0;
265
+ isDraggingRef.current = true;
266
+ };
267
+
268
+ const onDragMove = (e: TouchEvent | ReactMouseEvent) => {
269
+ if (!isDraggingRef.current || dragStartRef.current === null) return;
270
+ const delta = getEventCoord(e) - dragStartRef.current;
271
+ dragDeltaRef.current = delta;
272
+ // Live-drag offset
273
+ if (trackRef.current) {
274
+ trackRef.current.style.transition = 'none';
275
+ trackRef.current.style.transform = getTransform(trackIdx, -delta);
276
+ }
277
+ };
278
+
279
+ const onDragEnd = () => {
280
+ if (!isDraggingRef.current) return;
281
+ isDraggingRef.current = false;
282
+
283
+ const delta = dragDeltaRef.current;
284
+ const threshold = 50;
285
+
286
+ // Restore transition
287
+ if (trackRef.current) {
288
+ trackRef.current.style.transition = '';
289
+ trackRef.current.style.transform = '';
290
+ }
291
+
292
+ if (delta < -threshold) next();
293
+ else if (delta > threshold) prev();
294
+ else {
295
+ // Snap back
296
+ if (trackRef.current) {
297
+ trackRef.current.style.transform = getTransform(trackIdx);
298
+ }
299
+ }
300
+
301
+ dragStartRef.current = null;
302
+ dragDeltaRef.current = 0;
303
+ };
304
+
305
+ // ── Sizing ────────────────────────────────────────────────────────────────
306
+
307
+ const heightVal = typeof height === 'number' ? `${height}px` : height;
308
+
309
+ const trackStyle: CSSProperties = {
310
+ display: 'flex',
311
+ flexDirection: isVertical ? 'column' : 'row',
312
+ width: isVertical ? '100%' : `${(totalTracks / slidesToShow) * 100}%`,
313
+ height: isVertical ? `${(totalTracks / slidesToShow) * 100}%` : '100%',
314
+ transform: getTransform(trackIdx),
315
+ transition: isTransitioning || !reducedMotion
316
+ ? `transform ${actualDuration}ms cubic-bezier(0.4, 0, 0.2, 1)`
317
+ : 'none',
318
+ willChange: 'transform',
319
+ gap: gap > 0 ? gap : undefined,
320
+ };
321
+
322
+ const slideStyle: CSSProperties = {
323
+ flexShrink: 0,
324
+ width: isVertical ? '100%' : `${slideWidthPct / (totalTracks / slidesToShow)}%`,
325
+ height: isVertical ? `${slideWidthPct / (totalTracks / slidesToShow)}%` : '100%',
326
+ overflow: 'hidden',
327
+ position: 'relative',
328
+ };
329
+
330
+ const canPrev = loop || currentIdx > 0;
331
+ const canNext = loop || currentIdx < count - 1;
332
+
333
+ const outsideArrows = arrowPosition === 'outside';
334
+
335
+ return (
336
+ <div
337
+ id={carouselId}
338
+ className={cx(tkx('flex flex-col gap-2'), className)}
339
+ style={style}
340
+ aria-roledescription="carousel"
341
+ aria-label="Content carousel"
342
+ >
343
+ {/* Main carousel row */}
344
+ <div
345
+ className={tkx('flex items-center gap-2')}
346
+ style={{ gap: outsideArrows && showArrows ? 8 : 0 }}
347
+ >
348
+ {/* Outside prev arrow */}
349
+ {showArrows && outsideArrows && (
350
+ <ArrowBtn
351
+ direction="prev"
352
+ onClick={prev}
353
+ isDisabled={!canPrev}
354
+ primaryColor={theme.primary}
355
+ position="outside"
356
+ orientation={orientation}
357
+ />
358
+ )}
359
+
360
+ {/* Viewport */}
361
+ <div
362
+ style={{
363
+ position: 'relative',
364
+ flex: 1,
365
+ overflow: 'hidden',
366
+ height: isVertical ? heightVal : heightVal,
367
+ borderRadius: 12,
368
+ cursor: swipeable ? (isDraggingRef.current ? 'grabbing' : 'grab') : 'default',
369
+ userSelect: 'none',
370
+ }}
371
+ onMouseEnter={() => pauseOnHover && setPaused(true)}
372
+ onMouseLeave={() => { setPaused(false); onDragEnd(); }}
373
+ onTouchStart={onDragStart}
374
+ onTouchMove={onDragMove}
375
+ onTouchEnd={onDragEnd}
376
+ onMouseDown={onDragStart}
377
+ onMouseMove={onDragMove}
378
+ onMouseUp={onDragEnd}
379
+ aria-live="polite"
380
+ aria-atomic="false"
381
+ >
382
+ {/* Inside arrows */}
383
+ {showArrows && !outsideArrows && (
384
+ <>
385
+ <ArrowBtn
386
+ direction="prev"
387
+ onClick={prev}
388
+ isDisabled={!canPrev}
389
+ primaryColor={theme.primary}
390
+ position="inside"
391
+ orientation={orientation}
392
+ />
393
+ <ArrowBtn
394
+ direction="next"
395
+ onClick={next}
396
+ isDisabled={!canNext}
397
+ primaryColor={theme.primary}
398
+ position="inside"
399
+ orientation={orientation}
400
+ />
401
+ </>
402
+ )}
403
+
404
+ {/* Track */}
405
+ <div ref={trackRef} style={trackStyle} onTransitionEnd={handleTransitionEnd}>
406
+ {renderSlides.map((slide, idx) => (
407
+ <div
408
+ key={`${slide.id}-${idx}`}
409
+ style={slideStyle}
410
+ role="group"
411
+ aria-roledescription="slide"
412
+ aria-label={`Slide ${((idx - (loop ? 1 : 0) + count) % count) + 1} of ${count}`}
413
+ aria-hidden={((idx - (loop ? 1 : 0) + count) % count) !== currentIdx}
414
+ >
415
+ {slide.content}
416
+ </div>
417
+ ))}
418
+ </div>
419
+
420
+ {/* Dot indicators (inside) */}
421
+ {showDots && !showThumbnails && (
422
+ <div
423
+ aria-label="Slide indicators"
424
+ style={{
425
+ position: 'absolute',
426
+ bottom: 12,
427
+ left: '50%',
428
+ transform: 'translateX(-50%)',
429
+ display: 'flex',
430
+ gap: 6,
431
+ zIndex: 2,
432
+ }}
433
+ >
434
+ {slides.map((slide, idx) => (
435
+ <button
436
+ key={slide.id}
437
+ type="button"
438
+ aria-label={`Go to slide ${idx + 1}`}
439
+ aria-current={idx === currentIdx ? 'true' : undefined}
440
+ onClick={() => goTo(idx)}
441
+ style={{
442
+ width: idx === currentIdx ? 20 : 8,
443
+ height: 8,
444
+ borderRadius: 9999,
445
+ border: 'none',
446
+ backgroundColor: idx === currentIdx ? theme.primary : 'rgba(255,255,255,0.5)',
447
+ cursor: 'pointer',
448
+ padding: 0,
449
+ transition: 'width 250ms ease, background 250ms ease',
450
+ outline: 'none',
451
+ }}
452
+ onFocus={(e) => { (e.currentTarget as HTMLElement).style.boxShadow = `0 0 0 3px ${theme.primary}66`; }}
453
+ onBlur={(e) => { (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
454
+ />
455
+ ))}
456
+ </div>
457
+ )}
458
+ </div>
459
+
460
+ {/* Outside next arrow */}
461
+ {showArrows && outsideArrows && (
462
+ <ArrowBtn
463
+ direction="next"
464
+ onClick={next}
465
+ isDisabled={!canNext}
466
+ primaryColor={theme.primary}
467
+ position="outside"
468
+ orientation={orientation}
469
+ />
470
+ )}
471
+ </div>
472
+
473
+ {/* Thumbnail strip */}
474
+ {showThumbnails && (
475
+ <div
476
+ role="tablist"
477
+ aria-label="Slide thumbnails"
478
+ style={{
479
+ display: 'flex',
480
+ gap: 6,
481
+ overflowX: 'auto',
482
+ paddingBottom: 4,
483
+ scrollbarWidth: 'thin',
484
+ }}
485
+ >
486
+ {slides.map((slide, idx) => (
487
+ <button
488
+ key={slide.id}
489
+ type="button"
490
+ role="tab"
491
+ aria-selected={idx === currentIdx}
492
+ aria-label={`Thumbnail for slide ${idx + 1}`}
493
+ onClick={() => goTo(idx)}
494
+ style={{
495
+ flexShrink: 0,
496
+ width: 60,
497
+ height: 42,
498
+ border: `2px solid ${idx === currentIdx ? theme.primary : theme.border}`,
499
+ borderRadius: 6,
500
+ overflow: 'hidden',
501
+ cursor: 'pointer',
502
+ padding: 0,
503
+ background: theme.surfaceAlt,
504
+ transition: 'border-color 150ms ease',
505
+ outline: 'none',
506
+ position: 'relative',
507
+ }}
508
+ onFocus={(e) => { (e.currentTarget as HTMLElement).style.boxShadow = `0 0 0 3px ${theme.primary}55`; }}
509
+ onBlur={(e) => { (e.currentTarget as HTMLElement).style.boxShadow = 'none'; }}
510
+ >
511
+ {slide.thumbnail ? (
512
+ <img
513
+ src={slide.thumbnail}
514
+ alt=""
515
+ aria-hidden="true"
516
+ style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
517
+ />
518
+ ) : (
519
+ <div
520
+ style={{
521
+ width: '100%',
522
+ height: '100%',
523
+ display: 'flex',
524
+ alignItems: 'center',
525
+ justifyContent: 'center',
526
+ fontSize: '0.625rem',
527
+ color: theme.textMuted,
528
+ fontWeight: 600,
529
+ }}
530
+ >
531
+ {idx + 1}
532
+ </div>
533
+ )}
534
+ {idx === currentIdx && (
535
+ <div
536
+ aria-hidden="true"
537
+ style={{
538
+ position: 'absolute',
539
+ inset: 0,
540
+ backgroundColor: `${theme.primary}22`,
541
+ }}
542
+ />
543
+ )}
544
+ </button>
545
+ ))}
546
+ </div>
547
+ )}
548
+ </div>
549
+ );
550
+ }
551
+
552
+ TkxCarousel.displayName = 'TkxCarousel';