@12-apps/ui 1.4.0 → 1.5.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 (66) hide show
  1. package/eslint.config.js +12 -0
  2. package/package.json +5 -4
  3. package/src/components/data-display/AlertDialog/AlertDialog.test.stories.tsx +16 -6
  4. package/src/components/data-display/Banner/Banner.test.stories.tsx +26 -18
  5. package/src/components/data-display/Carousel/Carousel.chrome.tsx +83 -0
  6. package/src/components/data-display/Carousel/Carousel.hooks.ts +88 -0
  7. package/src/components/data-display/Carousel/Carousel.parts.tsx +346 -0
  8. package/src/components/data-display/Carousel/Carousel.slide.tsx +190 -0
  9. package/src/components/data-display/Carousel/Carousel.styles.ts +86 -0
  10. package/src/components/data-display/Carousel/Carousel.test.stories.tsx +15 -5
  11. package/src/components/data-display/Carousel/Carousel.tsx +84 -732
  12. package/src/components/data-display/HoverCard/HoverCard.animations.ts +73 -0
  13. package/src/components/data-display/HoverCard/HoverCard.content.tsx +143 -0
  14. package/src/components/data-display/HoverCard/HoverCard.hooks.ts +180 -0
  15. package/src/components/data-display/HoverCard/HoverCard.styles.ts +178 -0
  16. package/src/components/data-display/HoverCard/HoverCard.tsx +117 -631
  17. package/src/components/data-display/Sheet/Sheet.animations.ts +44 -0
  18. package/src/components/data-display/Sheet/Sheet.body.tsx +124 -0
  19. package/src/components/data-display/Sheet/Sheet.drag.ts +203 -0
  20. package/src/components/data-display/Sheet/Sheet.hooks.ts +318 -0
  21. package/src/components/data-display/Sheet/Sheet.parts.tsx +224 -0
  22. package/src/components/data-display/Sheet/Sheet.styles.ts +257 -0
  23. package/src/components/data-display/Sheet/Sheet.test.stories.tsx +25 -21
  24. package/src/components/data-display/Sheet/Sheet.tsx +125 -948
  25. package/src/components/feedback/Dialog/Dialog.test.stories.tsx +37 -28
  26. package/src/components/feedback/Modal/Modal.styles.ts +126 -0
  27. package/src/components/feedback/Modal/Modal.test.stories.tsx +5 -5
  28. package/src/components/feedback/Modal/Modal.transitions.tsx +40 -0
  29. package/src/components/feedback/Modal/Modal.tsx +37 -242
  30. package/src/components/feedback/StackedModal/StackedModal.test.stories.tsx +9 -11
  31. package/src/components/form/InputOTP/InputOTP.hooks.ts +104 -0
  32. package/src/components/form/InputOTP/InputOTP.styles.ts +97 -0
  33. package/src/components/form/InputOTP/InputOTP.tsx +74 -221
  34. package/src/components/form/Label/Label.parts.tsx +75 -0
  35. package/src/components/form/Label/Label.styles.ts +206 -0
  36. package/src/components/form/Label/Label.test.stories.tsx +20 -14
  37. package/src/components/form/Label/Label.tsx +73 -325
  38. package/src/components/form/Menubar/Menubar.hooks.ts +41 -0
  39. package/src/components/form/Menubar/Menubar.item.tsx +121 -0
  40. package/src/components/form/Menubar/Menubar.separator.tsx +25 -0
  41. package/src/components/form/Menubar/Menubar.styles.ts +110 -0
  42. package/src/components/form/Menubar/Menubar.toolbar.tsx +103 -0
  43. package/src/components/form/Menubar/Menubar.tsx +64 -347
  44. package/src/components/form/RadioGroup/RadioGroup.animations.ts +48 -0
  45. package/src/components/form/RadioGroup/RadioGroup.styles.ts +305 -0
  46. package/src/components/form/RadioGroup/RadioGroup.tsx +81 -651
  47. package/src/components/form/RadioGroup/RadioGroup.variants.tsx +291 -0
  48. package/src/components/form/Slider/Slider.animations.ts +40 -0
  49. package/src/components/form/Slider/Slider.styles.ts +292 -0
  50. package/src/components/form/Slider/Slider.tsx +106 -357
  51. package/src/components/form/Switch/Switch.animations.ts +62 -0
  52. package/src/components/form/Switch/Switch.parts.tsx +213 -0
  53. package/src/components/form/Switch/Switch.styles.ts +355 -0
  54. package/src/components/form/Switch/Switch.tsx +87 -566
  55. package/src/components/form/ToggleGroup/ToggleGroup.test.stories.tsx +26 -29
  56. package/src/components/layout/Resizable/Resizable.test.stories.tsx +11 -7
  57. package/src/components/layout/Spacer/Spacer.test.stories.tsx +11 -9
  58. package/src/components/typography/Blockquote/Blockquote.test.stories.tsx +7 -7
  59. package/src/components/typography/Heading/Heading.styles.ts +101 -0
  60. package/src/components/typography/Heading/Heading.tsx +48 -164
  61. package/src/components/typography/Text/Text.test.stories.tsx +27 -30
  62. package/src/components/utility/AnimatedIcon/AnimatedIcon.animations.ts +274 -0
  63. package/src/components/utility/AnimatedIcon/AnimatedIcon.styles.ts +231 -0
  64. package/src/components/utility/AnimatedIcon/AnimatedIcon.tsx +39 -559
  65. package/src/utils/index.ts +1 -0
  66. 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
  };