@12-apps/ui 1.4.0 → 1.4.1

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 (53) hide show
  1. package/package.json +4 -4
  2. package/src/components/data-display/Carousel/Carousel.chrome.tsx +83 -0
  3. package/src/components/data-display/Carousel/Carousel.hooks.ts +88 -0
  4. package/src/components/data-display/Carousel/Carousel.parts.tsx +346 -0
  5. package/src/components/data-display/Carousel/Carousel.slide.tsx +190 -0
  6. package/src/components/data-display/Carousel/Carousel.styles.ts +86 -0
  7. package/src/components/data-display/Carousel/Carousel.tsx +84 -732
  8. package/src/components/data-display/HoverCard/HoverCard.animations.ts +73 -0
  9. package/src/components/data-display/HoverCard/HoverCard.content.tsx +143 -0
  10. package/src/components/data-display/HoverCard/HoverCard.hooks.ts +180 -0
  11. package/src/components/data-display/HoverCard/HoverCard.styles.ts +178 -0
  12. package/src/components/data-display/HoverCard/HoverCard.tsx +117 -631
  13. package/src/components/data-display/Sheet/Sheet.animations.ts +44 -0
  14. package/src/components/data-display/Sheet/Sheet.body.tsx +124 -0
  15. package/src/components/data-display/Sheet/Sheet.drag.ts +203 -0
  16. package/src/components/data-display/Sheet/Sheet.hooks.ts +318 -0
  17. package/src/components/data-display/Sheet/Sheet.parts.tsx +224 -0
  18. package/src/components/data-display/Sheet/Sheet.styles.ts +257 -0
  19. package/src/components/data-display/Sheet/Sheet.tsx +125 -948
  20. package/src/components/feedback/Modal/Modal.styles.ts +126 -0
  21. package/src/components/feedback/Modal/Modal.transitions.tsx +40 -0
  22. package/src/components/feedback/Modal/Modal.tsx +37 -242
  23. package/src/components/form/InputOTP/InputOTP.hooks.ts +104 -0
  24. package/src/components/form/InputOTP/InputOTP.styles.ts +97 -0
  25. package/src/components/form/InputOTP/InputOTP.tsx +74 -221
  26. package/src/components/form/Label/Label.parts.tsx +75 -0
  27. package/src/components/form/Label/Label.styles.ts +206 -0
  28. package/src/components/form/Label/Label.tsx +73 -325
  29. package/src/components/form/Menubar/Menubar.hooks.ts +41 -0
  30. package/src/components/form/Menubar/Menubar.item.tsx +121 -0
  31. package/src/components/form/Menubar/Menubar.separator.tsx +25 -0
  32. package/src/components/form/Menubar/Menubar.styles.ts +110 -0
  33. package/src/components/form/Menubar/Menubar.toolbar.tsx +103 -0
  34. package/src/components/form/Menubar/Menubar.tsx +64 -347
  35. package/src/components/form/RadioGroup/RadioGroup.animations.ts +48 -0
  36. package/src/components/form/RadioGroup/RadioGroup.styles.ts +305 -0
  37. package/src/components/form/RadioGroup/RadioGroup.tsx +81 -651
  38. package/src/components/form/RadioGroup/RadioGroup.variants.tsx +291 -0
  39. package/src/components/form/Slider/Slider.animations.ts +40 -0
  40. package/src/components/form/Slider/Slider.styles.ts +292 -0
  41. package/src/components/form/Slider/Slider.tsx +106 -357
  42. package/src/components/form/Switch/Switch.animations.ts +62 -0
  43. package/src/components/form/Switch/Switch.parts.tsx +213 -0
  44. package/src/components/form/Switch/Switch.styles.ts +355 -0
  45. package/src/components/form/Switch/Switch.tsx +87 -566
  46. package/src/components/typography/Blockquote/Blockquote.test.stories.tsx +7 -7
  47. package/src/components/typography/Heading/Heading.styles.ts +101 -0
  48. package/src/components/typography/Heading/Heading.tsx +48 -164
  49. package/src/components/utility/AnimatedIcon/AnimatedIcon.animations.ts +274 -0
  50. package/src/components/utility/AnimatedIcon/AnimatedIcon.styles.ts +231 -0
  51. package/src/components/utility/AnimatedIcon/AnimatedIcon.tsx +39 -559
  52. package/src/utils/index.ts +1 -0
  53. package/src/utils/withDefaults.ts +20 -0
@@ -1,115 +1,68 @@
1
- import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
2
- import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
3
- import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
4
- import {
5
- alpha,
6
- Box,
7
- CircularProgress,
8
- Fade,
9
- IconButton,
10
- Paper,
11
- Slide,
12
- Typography,
13
- useTheme,
14
- Zoom,
15
- } from '@mui/material';
16
- import React, { useCallback,useEffect, useRef, useState } from 'react';
1
+ import { Box, CircularProgress, Fade, Slide, useTheme, Zoom } from '@mui/material';
2
+ import React, { useCallback, useRef } from 'react';
3
+
4
+ import { withDefaults } from '../../../utils/withDefaults';
5
+
6
+ import { useCarouselNavigation } from './Carousel.hooks';
7
+ import { CarouselChrome } from './Carousel.chrome';
8
+ import { CarouselTrack } from './Carousel.slide';
9
+ import { containerStyles } from './Carousel.styles';
10
+ import type { CarouselItem, CarouselProps } from './Carousel.types';
11
+
12
+ export { CarouselArrows, CarouselIndicators, CarouselThumbnails } from './Carousel.parts';
13
+
14
+ const DEFAULTS = {
15
+ variant: 'default',
16
+ size: 'md',
17
+ color: 'primary',
18
+ autoPlay: false,
19
+ autoPlayInterval: 3000,
20
+ loop: true,
21
+ showIndicators: true,
22
+ showArrows: true,
23
+ showThumbnails: false,
24
+ glow: false,
25
+ pulse: false,
26
+ glass: false,
27
+ gradient: false,
28
+ loading: false,
29
+ disabled: false,
30
+ height: 400,
31
+ width: '100%',
32
+ pauseOnHover: true,
33
+ indicatorPosition: 'bottom',
34
+ arrowPosition: 'overlay',
35
+ animation: 'slide',
36
+ } satisfies Partial<CarouselProps>;
37
+
38
+ type ResolvedCarouselProps = CarouselProps & Required<Pick<CarouselProps, keyof typeof DEFAULTS>>;
39
+
40
+ /**
41
+ * `flip` reuses Slide with a fixed direction — MUI has no flip transition, and a
42
+ * slide from one side reads closer to a flip than a fade does.
43
+ */
44
+ const TRANSITIONS = { fade: Fade, zoom: Zoom, flip: Slide, slide: Slide } as const;
45
+
46
+ export const Carousel: React.FC<CarouselProps> = (props) => {
47
+ const resolved = withDefaults(props, DEFAULTS) as ResolvedCarouselProps;
48
+ const {
49
+ items, variant, size, color, autoPlay, autoPlayInterval, loop, loading, disabled,
50
+ className, style, height, width, pauseOnHover, onClick, onChange, onFocus, onBlur,
51
+ animation, glow, pulse, glass, gradient,
52
+ } = resolved;
17
53
 
18
- import type {
19
- CarouselArrowsProps,
20
- CarouselIndicatorsProps,
21
- CarouselItem,
22
- CarouselProps,
23
- CarouselThumbnailsProps,
24
- } from './Carousel.types';
25
-
26
- export const Carousel: React.FC<CarouselProps> = ({
27
- items,
28
- variant = 'default',
29
- size = 'md',
30
- color = 'primary',
31
- autoPlay = false,
32
- autoPlayInterval = 3000,
33
- loop = true,
34
- showIndicators = true,
35
- showArrows = true,
36
- showThumbnails = false,
37
- glow = false,
38
- pulse = false,
39
- glass = false,
40
- gradient = false,
41
- loading = false,
42
- disabled = false,
43
- className,
44
- style,
45
- height = 400,
46
- width = '100%',
47
- pauseOnHover = true,
48
- onClick,
49
- onChange,
50
- onFocus,
51
- onBlur,
52
- indicatorPosition = 'bottom',
53
- arrowPosition = 'overlay',
54
- animation = 'slide',
55
- }) => {
56
54
  const theme = useTheme();
57
- const [activeIndex, setActiveIndex] = useState(0);
58
- const [isHovered, setIsHovered] = useState(false);
59
- const intervalRef = useRef<number | null>(null);
60
55
  const containerRef = useRef<HTMLDivElement>(null);
61
56
 
62
- const handleNext = useCallback(() => {
63
- setActiveIndex((prev) => {
64
- const next = prev + 1;
65
- if (next >= items.length) {
66
- const newIndex = loop ? 0 : prev;
67
- if (newIndex !== prev) {
68
- onChange?.(newIndex);
69
- }
70
- return newIndex;
71
- }
72
- onChange?.(next);
73
- return next;
74
- });
75
- }, [items.length, loop, onChange]);
76
-
77
- const handlePrev = useCallback(() => {
78
- setActiveIndex((prev) => {
79
- const next = prev - 1;
80
- if (next < 0) {
81
- const newIndex = loop ? items.length - 1 : 0;
82
- if (newIndex !== prev) {
83
- onChange?.(newIndex);
84
- }
85
- return newIndex;
86
- }
87
- onChange?.(next);
88
- return next;
89
- });
90
- }, [items.length, loop, onChange]);
91
-
92
- useEffect(() => {
93
- if (autoPlay && !isHovered && !disabled) {
94
- intervalRef.current = window.setInterval(() => {
95
- handleNext();
96
- }, autoPlayInterval);
97
- }
98
-
99
- return () => {
100
- if (intervalRef.current) {
101
- window.clearInterval(intervalRef.current);
102
- }
103
- };
104
- }, [activeIndex, autoPlay, autoPlayInterval, isHovered, disabled, handleNext]);
105
-
106
- const handleSelect = useCallback(
107
- (index: number) => {
108
- setActiveIndex(index);
109
- onChange?.(index);
110
- },
111
- [onChange],
112
- );
57
+ const { activeIndex, handleNext, handlePrev, handleSelect, hoverProps } = useCarouselNavigation({
58
+ count: items.length,
59
+ loop,
60
+ autoPlay,
61
+ autoPlayInterval,
62
+ pauseOnHover,
63
+ disabled,
64
+ onChange,
65
+ });
113
66
 
114
67
  const handleItemClick = useCallback(
115
68
  (item: CarouselItem, index: number) => {
@@ -120,223 +73,13 @@ export const Carousel: React.FC<CarouselProps> = ({
120
73
  [disabled, onClick],
121
74
  );
122
75
 
123
- const getSizeStyles = () => {
124
- switch (size) {
125
- case 'xs':
126
- return { height: 200, fontSize: '0.75rem' };
127
- case 'sm':
128
- return { height: 300, fontSize: '0.875rem' };
129
- case 'md':
130
- return { height: 400, fontSize: '1rem' };
131
- case 'lg':
132
- return { height: 500, fontSize: '1.125rem' };
133
- case 'xl':
134
- return { height: 600, fontSize: '1.25rem' };
135
- default:
136
- return { height: 400, fontSize: '1rem' };
137
- }
138
- };
139
-
140
- const getVariantStyles = () => {
141
- const baseStyles = {
142
- position: 'relative' as const,
143
- width,
144
- height: typeof height === 'number' ? height : getSizeStyles().height,
145
- overflow: 'hidden',
146
- borderRadius: theme.spacing(1),
147
- transition: theme.transitions.create(['all'], {
148
- duration: theme.transitions.duration.standard,
149
- }),
150
- };
151
-
152
- const glowStyles = glow
153
- ? {
154
- boxShadow: `0 0 30px ${alpha(theme.palette[color].main, 0.4)}`,
155
- }
156
- : {};
157
-
158
- const pulseStyles = pulse
159
- ? {
160
- animation: 'pulse 2s infinite',
161
- '@keyframes pulse': {
162
- '0%': { boxShadow: `0 0 0 0 ${alpha(theme.palette[color].main, 0.4)}` },
163
- '70%': { boxShadow: `0 0 0 20px ${alpha(theme.palette[color].main, 0)}` },
164
- '100%': { boxShadow: `0 0 0 0 ${alpha(theme.palette[color].main, 0)}` },
165
- },
166
- }
167
- : {};
168
-
169
- switch (variant) {
170
- case 'glass':
171
- return {
172
- ...baseStyles,
173
- ...glowStyles,
174
- ...pulseStyles,
175
- backgroundColor: alpha(theme.palette.background.paper, glass ? 0.1 : 0.9),
176
- backdropFilter: 'blur(20px)',
177
- border: `1px solid ${alpha(theme.palette[color].main, 0.2)}`,
178
- };
179
-
180
- case 'gradient':
181
- return {
182
- ...baseStyles,
183
- ...glowStyles,
184
- ...pulseStyles,
185
- background: gradient
186
- ? `linear-gradient(135deg, ${alpha(theme.palette[color].light, 0.1)}, ${alpha(theme.palette[color].dark, 0.1)})`
187
- : 'transparent',
188
- };
189
-
190
- case 'elevated':
191
- return {
192
- ...baseStyles,
193
- ...glowStyles,
194
- ...pulseStyles,
195
- boxShadow: theme.shadows[8],
196
- };
197
-
198
- case 'minimal':
199
- return {
200
- ...baseStyles,
201
- ...glowStyles,
202
- ...pulseStyles,
203
- border: 'none',
204
- boxShadow: 'none',
205
- };
206
-
207
- case 'cards':
208
- return {
209
- ...baseStyles,
210
- ...glowStyles,
211
- ...pulseStyles,
212
- padding: theme.spacing(2),
213
- };
214
-
215
- default:
216
- return {
217
- ...baseStyles,
218
- ...glowStyles,
219
- ...pulseStyles,
220
- };
221
- }
222
- };
223
-
224
- const renderSlide = (item: CarouselItem, index: number) => {
225
- const isActive = index === activeIndex;
226
-
227
- const slideContent = (
228
- <Box
229
- sx={{
230
- width: '100%',
231
- height: '100%',
232
- display: 'flex',
233
- flexDirection: 'column',
234
- justifyContent: 'center',
235
- alignItems: 'center',
236
- cursor: onClick ? 'pointer' : 'default',
237
- position: 'relative',
238
- }}
239
- onClick={() => handleItemClick(item, index)}
240
- >
241
- {item.image && (
242
- <Box
243
- component="img"
244
- src={item.image}
245
- alt={item.alt || item.title}
246
- sx={{
247
- width: '100%',
248
- height: '100%',
249
- objectFit: 'cover',
250
- position: 'absolute',
251
- top: 0,
252
- left: 0,
253
- }}
254
- />
255
- )}
256
-
257
- <Box
258
- sx={{
259
- position: 'relative',
260
- zIndex: 1,
261
- textAlign: 'center',
262
- p: 3,
263
- }}
264
- >
265
- {item.title && (
266
- <Typography
267
- variant="h5"
268
- sx={{
269
- color: item.image ? 'white' : 'text.primary',
270
- textShadow: item.image ? '0 2px 4px rgba(0,0,0,0.5)' : 'none',
271
- mb: 1,
272
- }}
273
- >
274
- {item.title}
275
- </Typography>
276
- )}
277
-
278
- {item.description && (
279
- <Typography
280
- variant="body1"
281
- sx={{
282
- color: item.image ? 'white' : 'text.secondary',
283
- textShadow: item.image ? '0 1px 2px rgba(0,0,0,0.5)' : 'none',
284
- }}
285
- >
286
- {item.description}
287
- </Typography>
288
- )}
289
-
290
- {item.content}
291
- </Box>
292
- </Box>
293
- );
294
-
295
- if (variant === 'cards') {
296
- return (
297
- <Paper
298
- elevation={isActive ? 8 : 2}
299
- sx={{
300
- width: '100%',
301
- height: '100%',
302
- overflow: 'hidden',
303
- transform: isActive ? 'scale(1)' : 'scale(0.9)',
304
- transition: theme.transitions.create(['transform', 'box-shadow']),
305
- }}
306
- >
307
- {slideContent}
308
- </Paper>
309
- );
310
- }
311
-
312
- return slideContent;
313
- };
314
-
315
- const getTransitionComponent = () => {
316
- switch (animation) {
317
- case 'fade':
318
- return Fade;
319
- case 'zoom':
320
- return Zoom;
321
- case 'flip':
322
- return Slide;
323
- default:
324
- return Slide;
325
- }
326
- };
327
-
328
- const TransitionComponent = getTransitionComponent();
76
+ const frameSx = containerStyles({
77
+ theme, variant, size, color, height, width, glow, pulse, glass, gradient,
78
+ });
329
79
 
330
80
  if (loading) {
331
81
  return (
332
- <Box
333
- sx={{
334
- ...getVariantStyles(),
335
- display: 'flex',
336
- justifyContent: 'center',
337
- alignItems: 'center',
338
- }}
339
- >
82
+ <Box sx={{ ...frameSx, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
340
83
  <CircularProgress color={color} />
341
84
  </Box>
342
85
  );
@@ -347,419 +90,28 @@ export const Carousel: React.FC<CarouselProps> = ({
347
90
  ref={containerRef}
348
91
  className={className}
349
92
  data-testid="carousel-container"
350
- sx={{
351
- ...getVariantStyles(),
352
- ...style,
353
- }}
354
- onMouseEnter={() => pauseOnHover && setIsHovered(true)}
355
- onMouseLeave={() => pauseOnHover && setIsHovered(false)}
93
+ sx={{ ...frameSx, ...style }}
356
94
  onFocus={onFocus}
357
95
  onBlur={onBlur}
96
+ {...hoverProps}
358
97
  >
359
- <Box
360
- data-testid="carousel-track"
361
- sx={{
362
- width: '100%',
363
- height: '100%',
364
- position: 'relative',
365
- display: 'flex',
366
- alignItems: 'center',
367
- }}
368
- >
369
- {items.map((item, index) => (
370
- <TransitionComponent
371
- key={item.id}
372
- in={index === activeIndex}
373
- timeout={600}
374
- {...(animation === 'flip' && { direction: 'left' })}
375
- >
376
- <Box
377
- data-testid={`carousel-slide-${index}`}
378
- data-active={index === activeIndex}
379
- sx={{
380
- position: 'absolute',
381
- width: '100%',
382
- height: '100%',
383
- display: index === activeIndex ? 'block' : 'none',
384
- }}
385
- >
386
- {renderSlide(item, index)}
387
- </Box>
388
- </TransitionComponent>
389
- ))}
390
- </Box>
391
-
392
- {showArrows && !disabled && (
393
- <CarouselArrows
394
- onPrev={handlePrev}
395
- onNext={handleNext}
396
- position={arrowPosition}
397
- color={color}
398
- disablePrev={!loop && activeIndex === 0}
399
- disableNext={!loop && activeIndex === items.length - 1}
400
- />
401
- )}
402
-
403
- {showIndicators && (
404
- <CarouselIndicators
405
- count={items.length}
406
- activeIndex={activeIndex}
407
- onSelect={handleSelect}
408
- position={indicatorPosition}
409
- color={color}
410
- />
411
- )}
412
-
413
- {showThumbnails && (
414
- <CarouselThumbnails
415
- items={items}
416
- activeIndex={activeIndex}
417
- onSelect={handleSelect}
418
- size={size}
419
- />
420
- )}
421
- </Box>
422
- );
423
- };
424
-
425
- export const CarouselIndicators: React.FC<CarouselIndicatorsProps> = ({
426
- count,
427
- activeIndex,
428
- onSelect,
429
- position = 'bottom',
430
- color = 'primary',
431
- variant = 'dots',
432
- className,
433
- style,
434
- }) => {
435
- const theme = useTheme();
436
-
437
- const getPositionStyles = () => {
438
- switch (position) {
439
- case 'top':
440
- return { top: theme.spacing(2), left: '50%', transform: 'translateX(-50%)' };
441
- case 'bottom':
442
- return { bottom: theme.spacing(2), left: '50%', transform: 'translateX(-50%)' };
443
- case 'left':
444
- return {
445
- left: theme.spacing(2),
446
- top: '50%',
447
- transform: 'translateY(-50%)',
448
- flexDirection: 'column',
449
- };
450
- case 'right':
451
- return {
452
- right: theme.spacing(2),
453
- top: '50%',
454
- transform: 'translateY(-50%)',
455
- flexDirection: 'column',
456
- };
457
- default:
458
- return { bottom: theme.spacing(2), left: '50%', transform: 'translateX(-50%)' };
459
- }
460
- };
461
-
462
- const renderIndicator = (index: number) => {
463
- const isActive = index === activeIndex;
464
-
465
- switch (variant) {
466
- case 'lines':
467
- return (
468
- <Box
469
- role="button"
470
- tabIndex={0}
471
- aria-label={`Go to slide ${index + 1}`}
472
- data-testid={`carousel-indicator-${index}`}
473
- data-active={isActive}
474
- sx={{
475
- width: isActive ? 30 : 20,
476
- height: 3,
477
- backgroundColor: isActive
478
- ? theme.palette[color].main
479
- : alpha(theme.palette[color].main, 0.3),
480
- borderRadius: 1.5,
481
- transition: theme.transitions.create(['width', 'background-color']),
482
- cursor: 'pointer',
483
- }}
484
- onClick={() => onSelect(index)}
485
- onKeyDown={(e) => {
486
- if (e.key === 'Enter' || e.key === ' ') {
487
- e.preventDefault();
488
- onSelect(index);
489
- }
490
- }}
491
- />
492
- );
493
-
494
- case 'numbers':
495
- return (
496
- <Box
497
- role="button"
498
- tabIndex={0}
499
- aria-label={`Go to slide ${index + 1}`}
500
- data-testid={`carousel-indicator-${index}`}
501
- data-active={isActive}
502
- sx={{
503
- width: 24,
504
- height: 24,
505
- display: 'flex',
506
- alignItems: 'center',
507
- justifyContent: 'center',
508
- borderRadius: '50%',
509
- backgroundColor: isActive ? theme.palette[color].main : 'transparent',
510
- color: isActive ? theme.palette[color].contrastText : theme.palette[color].main,
511
- border: `1px solid ${theme.palette[color].main}`,
512
- fontSize: '0.75rem',
513
- fontWeight: isActive ? 'bold' : 'normal',
514
- cursor: 'pointer',
515
- transition: theme.transitions.create(['all']),
516
- }}
517
- onClick={() => onSelect(index)}
518
- onKeyDown={(e) => {
519
- if (e.key === 'Enter' || e.key === ' ') {
520
- e.preventDefault();
521
- onSelect(index);
522
- }
523
- }}
524
- >
525
- {index + 1}
526
- </Box>
527
- );
528
-
529
- default:
530
- return (
531
- <IconButton
532
- size="small"
533
- aria-label={`Go to slide ${index + 1}`}
534
- data-testid={`carousel-indicator-${index}`}
535
- data-active={isActive}
536
- onClick={() => onSelect(index)}
537
- sx={{
538
- p: 0.5,
539
- color: isActive ? theme.palette[color].main : alpha(theme.palette[color].main, 0.3),
540
- transform: isActive ? 'scale(1.2)' : 'scale(1)',
541
- transition: theme.transitions.create(['transform', 'color']),
542
- }}
543
- >
544
- <FiberManualRecordIcon fontSize="small" />
545
- </IconButton>
546
- );
547
- }
548
- };
549
-
550
- return (
551
- <Box
552
- className={className}
553
- data-testid="carousel-indicators"
554
- sx={{
555
- position: 'absolute',
556
- display: 'flex',
557
- gap: 1,
558
- zIndex: 2,
559
- ...getPositionStyles(),
560
- ...style,
561
- }}
562
- >
563
- {Array.from({ length: count }).map((_, index) => (
564
- <React.Fragment key={index}>{renderIndicator(index)}</React.Fragment>
565
- ))}
566
- </Box>
567
- );
568
- };
569
-
570
- export const CarouselArrows: React.FC<CarouselArrowsProps> = ({
571
- onPrev,
572
- onNext,
573
- position = 'overlay',
574
- disabled = false,
575
- disablePrev = false,
576
- disableNext = false,
577
- color = 'primary',
578
- size = 'medium',
579
- className,
580
- style,
581
- }) => {
582
- const theme = useTheme();
583
-
584
- const getArrowStyles = () => {
585
- const baseStyles = {
586
- backgroundColor: alpha(theme.palette.background.paper, 0.8),
587
- color: theme.palette[color].main,
588
- '&:hover': {
589
- backgroundColor: theme.palette.background.paper,
590
- },
591
- '&:disabled': {
592
- opacity: 0.3,
593
- },
594
- };
595
-
596
- switch (position) {
597
- case 'outside':
598
- return {
599
- ...baseStyles,
600
- position: 'absolute' as const,
601
- top: '50%',
602
- transform: 'translateY(-50%)',
603
- zIndex: 3,
604
- };
605
- case 'inside':
606
- return {
607
- ...baseStyles,
608
- position: 'absolute' as const,
609
- top: '50%',
610
- transform: 'translateY(-50%)',
611
- zIndex: 3,
612
- };
613
- case 'overlay':
614
- default:
615
- return {
616
- ...baseStyles,
617
- position: 'absolute' as const,
618
- top: '50%',
619
- transform: 'translateY(-50%)',
620
- zIndex: 3,
621
- opacity: 0.7,
622
- '&:hover': {
623
- ...baseStyles['&:hover'],
624
- opacity: 1,
625
- },
626
- };
627
- }
628
- };
629
-
630
- const leftPosition = position === 'outside' ? -40 : 8;
631
- const rightPosition = position === 'outside' ? -40 : 8;
632
-
633
- return (
634
- <Box data-testid="carousel-navigation">
635
- <IconButton
636
- onClick={onPrev}
637
- disabled={disabled || disablePrev}
638
- size={size}
639
- className={className}
640
- aria-label="Previous slide"
641
- data-testid="carousel-prev-button"
642
- sx={{
643
- ...getArrowStyles(),
644
- left: leftPosition,
645
- ...style,
646
- }}
647
- >
648
- <ArrowBackIosIcon />
649
- </IconButton>
650
-
651
- <IconButton
652
- onClick={onNext}
653
- disabled={disabled || disableNext}
654
- aria-label="Next slide"
655
- size={size}
656
- className={className}
657
- data-testid="carousel-next-button"
658
- sx={{
659
- ...getArrowStyles(),
660
- right: rightPosition,
661
- ...style,
662
- }}
663
- >
664
- <ArrowForwardIosIcon />
665
- </IconButton>
666
- </Box>
667
- );
668
- };
669
-
670
- export const CarouselThumbnails: React.FC<CarouselThumbnailsProps> = ({
671
- items,
672
- activeIndex,
673
- onSelect,
674
- size = 'sm',
675
- className,
676
- style,
677
- }) => {
678
- const theme = useTheme();
679
-
680
- const getThumbnailSize = () => {
681
- switch (size) {
682
- case 'xs':
683
- return 40;
684
- case 'sm':
685
- return 60;
686
- case 'md':
687
- return 80;
688
- case 'lg':
689
- return 100;
690
- case 'xl':
691
- return 120;
692
- default:
693
- return 60;
694
- }
695
- };
696
-
697
- return (
698
- <Box
699
- className={className}
700
- data-testid="carousel-thumbnails"
701
- sx={{
702
- position: 'absolute',
703
- bottom: -getThumbnailSize() - 16,
704
- left: '50%',
705
- transform: 'translateX(-50%)',
706
- display: 'flex',
707
- gap: 1,
708
- p: 1,
709
- backgroundColor: alpha(theme.palette.background.paper, 0.9),
710
- borderRadius: 1,
711
- ...style,
712
- }}
713
- >
714
- {items.map((item, index) => (
715
- <Box
716
- key={item.id}
717
- onClick={() => onSelect(index)}
718
- data-testid={`carousel-thumbnail-${index}`}
719
- data-active={index === activeIndex}
720
- sx={{
721
- width: getThumbnailSize(),
722
- height: getThumbnailSize(),
723
- border: `2px solid ${
724
- index === activeIndex ? theme.palette.primary.main : 'transparent'
725
- }`,
726
- borderRadius: 1,
727
- overflow: 'hidden',
728
- cursor: 'pointer',
729
- opacity: index === activeIndex ? 1 : 0.6,
730
- transition: theme.transitions.create(['opacity', 'border-color']),
731
- '&:hover': {
732
- opacity: 1,
733
- },
734
- }}
735
- >
736
- {item.image ? (
737
- <Box
738
- component="img"
739
- src={item.image}
740
- alt={item.alt || item.title}
741
- sx={{
742
- width: '100%',
743
- height: '100%',
744
- objectFit: 'cover',
745
- }}
746
- />
747
- ) : (
748
- <Box
749
- sx={{
750
- width: '100%',
751
- height: '100%',
752
- backgroundColor: theme.palette.grey[300],
753
- display: 'flex',
754
- alignItems: 'center',
755
- justifyContent: 'center',
756
- }}
757
- >
758
- <Typography variant="caption">{index + 1}</Typography>
759
- </Box>
760
- )}
761
- </Box>
762
- ))}
98
+ <CarouselTrack
99
+ items={items}
100
+ activeIndex={activeIndex}
101
+ variant={variant}
102
+ animation={animation}
103
+ transition={TRANSITIONS[animation] ?? Slide}
104
+ clickable={Boolean(onClick)}
105
+ onSelect={handleItemClick}
106
+ />
107
+
108
+ <CarouselChrome
109
+ {...resolved}
110
+ activeIndex={activeIndex}
111
+ onPrev={handlePrev}
112
+ onNext={handleNext}
113
+ onSelect={handleSelect}
114
+ />
763
115
  </Box>
764
116
  );
765
117
  };