@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,657 +1,143 @@
1
- import {
2
- alpha,
3
- Avatar,
4
- Box,
5
- Card,
6
- CardContent,
7
- CircularProgress,
8
- keyframes,
9
- Popover,
10
- Typography,
11
- } from '@mui/material';
1
+ import { Box, Card, CardContent, Popover } from '@mui/material';
12
2
  import { styled } from '@mui/material/styles';
13
3
  import React from 'react';
14
4
 
15
- import type { HoverCardAnimation, HoverCardPlacement, HoverCardProps } from './HoverCard.types';
16
-
17
- // Animation definitions
18
- const pulseAnimation = keyframes`
19
- 0% {
20
- box-shadow: 0 0 0 0 currentColor;
21
- opacity: 1;
22
- }
23
- 70% {
24
- box-shadow: 0 0 0 10px currentColor;
25
- opacity: 0;
26
- }
27
- 100% {
28
- box-shadow: 0 0 0 0 currentColor;
29
- opacity: 0;
30
- }
31
- `;
32
-
33
- const slideInUp = keyframes`
34
- from {
35
- opacity: 0;
36
- transform: translateY(8px);
37
- }
38
- to {
39
- opacity: 1;
40
- transform: translateY(0);
41
- }
42
- `;
43
-
44
- const slideInDown = keyframes`
45
- from {
46
- opacity: 0;
47
- transform: translateY(-8px);
48
- }
49
- to {
50
- opacity: 1;
51
- transform: translateY(0);
52
- }
53
- `;
54
-
55
- const slideInLeft = keyframes`
56
- from {
57
- opacity: 0;
58
- transform: translateX(8px);
59
- }
60
- to {
61
- opacity: 1;
62
- transform: translateX(0);
63
- }
64
- `;
65
-
66
- const slideInRight = keyframes`
67
- from {
68
- opacity: 0;
69
- transform: translateX(-8px);
70
- }
71
- to {
72
- opacity: 1;
73
- transform: translateX(0);
74
- }
75
- `;
76
-
77
- const scaleIn = keyframes`
78
- from {
79
- opacity: 0;
80
- transform: scale(0.8);
81
- }
82
- to {
83
- opacity: 1;
84
- transform: scale(1);
85
- }
86
- `;
87
-
88
- // Arrow component
89
- const ArrowContainer = styled('div')<{ placement: HoverCardPlacement; offset: number }>(({
90
- theme,
91
- placement,
92
- }) => {
93
- const arrowSize = 8;
94
- const isTop = placement.startsWith('top');
95
- const isBottom = placement.startsWith('bottom');
96
- const isLeft = placement.startsWith('left');
97
- const isRight = placement.startsWith('right');
98
-
99
- let position: React.CSSProperties = {};
100
- let borderStyle: React.CSSProperties = {};
5
+ import { withDefaults } from '../../../utils/withDefaults';
101
6
 
102
- if (isTop) {
103
- position = { bottom: -arrowSize, left: '50%', transform: 'translateX(-50%)' };
104
- borderStyle = {
105
- borderLeft: `${arrowSize}px solid transparent`,
106
- borderRight: `${arrowSize}px solid transparent`,
107
- borderTop: `${arrowSize}px solid ${theme.palette.background.paper}`,
108
- };
109
- } else if (isBottom) {
110
- position = { top: -arrowSize, left: '50%', transform: 'translateX(-50%)' };
111
- borderStyle = {
112
- borderLeft: `${arrowSize}px solid transparent`,
113
- borderRight: `${arrowSize}px solid transparent`,
114
- borderBottom: `${arrowSize}px solid ${theme.palette.background.paper}`,
115
- };
116
- } else if (isLeft) {
117
- position = { right: -arrowSize, top: '50%', transform: 'translateY(-50%)' };
118
- borderStyle = {
119
- borderTop: `${arrowSize}px solid transparent`,
120
- borderBottom: `${arrowSize}px solid transparent`,
121
- borderLeft: `${arrowSize}px solid ${theme.palette.background.paper}`,
122
- };
123
- } else if (isRight) {
124
- position = { left: -arrowSize, top: '50%', transform: 'translateY(-50%)' };
125
- borderStyle = {
126
- borderTop: `${arrowSize}px solid transparent`,
127
- borderBottom: `${arrowSize}px solid transparent`,
128
- borderRight: `${arrowSize}px solid ${theme.palette.background.paper}`,
129
- };
130
- }
7
+ import { HoverCardContent } from './HoverCard.content';
8
+ import { useHoverCard } from './HoverCard.hooks';
9
+ import { arrowSx, cardSx, getAnchorOrigin, getTransformOrigin, sideOf } from './HoverCard.styles';
10
+ import type { CardStyleFlags } from './HoverCard.styles';
11
+ import type { HoverCardAnimation, HoverCardPlacement, HoverCardProps } from './HoverCard.types';
131
12
 
132
- return {
133
- position: 'absolute',
134
- width: 0,
135
- height: 0,
136
- ...position,
137
- ...borderStyle,
138
- zIndex: 1,
139
- };
140
- });
13
+ const ArrowContainer = styled('div')<{ placement: HoverCardPlacement; offset: number }>(
14
+ ({ theme, placement }) => ({ ...arrowSx(theme, placement) }),
15
+ );
141
16
 
142
17
  const StyledCard = styled(Card, {
143
18
  shouldForwardProp: (prop) =>
144
19
  !['customVariant', 'glow', 'pulse', 'animation'].includes(prop as string),
145
- })<{
146
- customVariant?: string;
147
- glow?: boolean;
148
- pulse?: boolean;
149
- animation?: HoverCardAnimation;
150
- }>(({ theme, customVariant, glow, pulse, animation }) => ({
151
- borderRadius: theme.spacing(1.5),
152
- transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
153
- position: 'relative',
154
- overflow: 'visible',
155
- minWidth: 200,
156
- maxWidth: 400,
157
-
158
- // Animation styles
159
- ...(animation === 'fade' && {
160
- animation: `${slideInUp} 0.2s ease-out`,
161
- }),
162
- ...(animation === 'scale' && {
163
- transformOrigin: 'center',
164
- animation: `${scaleIn} 0.2s cubic-bezier(0.4, 0, 0.2, 1)`,
165
- }),
166
- ...(animation === 'slide-up' && {
167
- animation: `${slideInUp} 0.3s cubic-bezier(0.4, 0, 0.2, 1)`,
168
- }),
169
- ...(animation === 'slide-down' && {
170
- animation: `${slideInDown} 0.3s cubic-bezier(0.4, 0, 0.2, 1)`,
171
- }),
172
- ...(animation === 'slide-left' && {
173
- animation: `${slideInLeft} 0.3s cubic-bezier(0.4, 0, 0.2, 1)`,
174
- }),
175
- ...(animation === 'slide-right' && {
176
- animation: `${slideInRight} 0.3s cubic-bezier(0.4, 0, 0.2, 1)`,
177
- }),
178
-
179
- '@keyframes scaleIn': {
180
- from: {
181
- opacity: 0,
182
- transform: 'scale(0.9)',
183
- },
184
- to: {
185
- opacity: 1,
186
- transform: 'scale(1)',
187
- },
188
- },
189
-
190
- // Variant styles
191
- ...(customVariant === 'default' && {
192
- backgroundColor: theme.palette.background.paper,
193
- border: `1px solid ${alpha(theme.palette.divider, 0.2)}`,
194
- boxShadow: theme.shadows[8],
195
- }),
196
-
197
- ...(customVariant === 'glass' && {
198
- backgroundColor: alpha(theme.palette.background.paper, 0.85),
199
- backdropFilter: 'blur(20px)',
200
- WebkitBackdropFilter: 'blur(20px)',
201
- border: `1px solid ${alpha(theme.palette.divider, 0.2)}`,
202
- boxShadow: `0 8px 32px ${alpha(theme.palette.common.black, 0.12)}`,
203
- }),
204
-
205
- ...(customVariant === 'detailed' && {
206
- backgroundColor: theme.palette.background.paper,
207
- border: `1px solid ${alpha(theme.palette.divider, 0.2)}`,
208
- boxShadow: theme.shadows[12],
209
- }),
210
-
211
- ...(customVariant === 'minimal' && {
212
- backgroundColor: theme.palette.background.paper,
213
- border: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
214
- boxShadow: theme.shadows[4],
215
- }),
216
-
217
- // Glow effect
218
- ...(glow &&
219
- !pulse && {
220
- boxShadow: `0 0 20px 5px ${alpha(theme.palette.primary.main, 0.3)} !important`,
221
- filter: 'brightness(1.05)',
222
- }),
223
-
224
- // Pulse animation
225
- ...(pulse &&
226
- !glow && {
227
- position: 'relative',
228
- '&::after': {
229
- content: '""',
230
- position: 'absolute',
231
- top: 0,
232
- left: 0,
233
- right: 0,
234
- bottom: 0,
235
- borderRadius: 'inherit',
236
- backgroundColor: theme.palette.primary.main,
237
- opacity: 0.1,
238
- animation: `${pulseAnimation} 2s infinite`,
239
- pointerEvents: 'none',
240
- zIndex: -1,
241
- },
242
- }),
243
-
244
- // Both glow and pulse
245
- ...(glow &&
246
- pulse && {
247
- position: 'relative',
248
- boxShadow: `0 0 20px 5px ${alpha(theme.palette.primary.main, 0.3)} !important`,
249
- filter: 'brightness(1.05)',
250
- '&::after': {
251
- content: '""',
252
- position: 'absolute',
253
- top: 0,
254
- left: 0,
255
- right: 0,
256
- bottom: 0,
257
- borderRadius: 'inherit',
258
- backgroundColor: theme.palette.primary.main,
259
- opacity: 0.1,
260
- animation: `${pulseAnimation} 2s infinite`,
261
- pointerEvents: 'none',
262
- zIndex: -1,
263
- },
264
- }),
265
- }));
20
+ })<CardStyleFlags>(({ theme, ...flags }) => ({ ...cardSx(theme, flags) }));
266
21
 
267
22
  const StyledPopover = styled(Popover, {
268
23
  shouldForwardProp: (prop) => prop !== 'customAnimation',
269
24
  })<{ customAnimation?: HoverCardAnimation }>(({ customAnimation }) => ({
25
+ // The popover only positions; StyledCard draws the surface.
270
26
  '& .MuiPopover-paper': {
271
27
  backgroundColor: 'transparent',
272
28
  boxShadow: 'none',
273
29
  overflow: 'visible',
30
+ ...(customAnimation === 'scale' && { transformOrigin: 'center' }),
274
31
  },
275
- ...(customAnimation === 'scale' && {
276
- '& .MuiPopover-paper': {
277
- transformOrigin: 'center',
278
- },
279
- }),
280
32
  }));
281
33
 
282
- const LoadingContainer = styled(Box)(({ theme }) => ({
283
- display: 'flex',
284
- alignItems: 'center',
285
- justifyContent: 'center',
286
- minHeight: 80,
287
- color: theme.palette.text.secondary,
288
- }));
289
-
290
- // Utility functions
291
- const getAnchorOrigin = (placement: HoverCardPlacement) => {
292
- if (placement.startsWith('top')) {
293
- return { vertical: 'top' as const, horizontal: 'center' as const };
294
- }
295
- if (placement.startsWith('bottom')) {
296
- return { vertical: 'bottom' as const, horizontal: 'center' as const };
297
- }
298
- if (placement.startsWith('left')) {
299
- return { vertical: 'center' as const, horizontal: 'left' as const };
300
- }
301
- if (placement.startsWith('right')) {
302
- return { vertical: 'center' as const, horizontal: 'right' as const };
303
- }
304
- return { vertical: 'bottom' as const, horizontal: 'center' as const };
305
- };
306
-
307
- const getTransformOrigin = (placement: HoverCardPlacement) => {
308
- if (placement.startsWith('top')) {
309
- return { vertical: 'bottom' as const, horizontal: 'center' as const };
310
- }
311
- if (placement.startsWith('bottom')) {
312
- return { vertical: 'top' as const, horizontal: 'center' as const };
313
- }
314
- if (placement.startsWith('left')) {
315
- return { vertical: 'center' as const, horizontal: 'right' as const };
316
- }
317
- if (placement.startsWith('right')) {
318
- return { vertical: 'center' as const, horizontal: 'left' as const };
319
- }
320
- return { vertical: 'top' as const, horizontal: 'center' as const };
34
+ const DEFAULTS = {
35
+ variant: 'default',
36
+ glow: false,
37
+ pulse: false,
38
+ placement: 'bottom',
39
+ showArrow: false,
40
+ animation: 'fade',
41
+ enterDelay: 700,
42
+ exitDelay: 0,
43
+ maxWidth: 400,
44
+ loading: false,
45
+ touchEnabled: true,
46
+ offset: 8,
47
+ disabled: false,
48
+ } satisfies Partial<HoverCardProps>;
49
+
50
+ type ResolvedProps = HoverCardProps & Required<Pick<HoverCardProps, keyof typeof DEFAULTS>>;
51
+
52
+ /** Padding is the variant's own: detailed roomier, minimal tighter. */
53
+ const CONTENT_PADDING: Record<string, number> = { detailed: 3, minimal: 1.5 };
54
+
55
+ /** The card is nudged off the anchor on whichever side it sits. */
56
+ const offsetMargin = (placement: HoverCardPlacement, offset: number) => {
57
+ const side = sideOf(placement);
58
+ const key = ({ top: 'mt', bottom: 'mb', left: 'ml', right: 'mr' } as const)[side];
59
+ return { [key]: `${offset}px` };
321
60
  };
322
61
 
323
- export const HoverCard = React.forwardRef<HTMLDivElement, HoverCardProps>(
324
- (
325
- {
326
- variant = 'default',
327
- glow = false,
328
- pulse = false,
329
- title,
330
- description,
331
- avatar,
332
- trigger,
333
- placement = 'bottom',
334
- showArrow = false,
335
- animation = 'fade',
336
- enterDelay = 700,
337
- exitDelay = 0,
338
- maxWidth = 400,
339
- loading = false,
340
- loadingComponent,
341
- touchEnabled = true,
342
- offset = 8,
343
- disabled = false,
344
- onOpen,
345
- onClose,
346
- children,
347
- dataTestId,
348
- ...props
349
- },
350
- ref,
351
- ) => {
352
- const [anchorEl, setAnchorEl] = React.useState<HTMLElement | null>(null);
353
- const [isOpen, setIsOpen] = React.useState(false);
354
- const [isTouchDevice, setIsTouchDevice] = React.useState(false);
355
- const [touchTimeout, setTouchTimeout] = React.useState<number>();
356
- const enterTimeoutRef = React.useRef<number | undefined>(undefined);
357
- const exitTimeoutRef = React.useRef<number | undefined>(undefined);
358
-
359
- // Detect touch device
360
- React.useEffect(() => {
361
- setIsTouchDevice('ontouchstart' in window);
362
- }, []);
363
-
364
- const openCard = (event: React.MouseEvent<HTMLElement> | React.TouchEvent<HTMLElement>) => {
365
- if (disabled) return;
366
-
367
- if (exitTimeoutRef.current) {
368
- window.clearTimeout(exitTimeoutRef.current);
369
- }
370
-
371
- const target = event.currentTarget;
372
- enterTimeoutRef.current = window.setTimeout(() => {
373
- setAnchorEl(target);
374
- setIsOpen(true);
375
- onOpen?.();
376
- }, enterDelay);
377
- };
378
-
379
- const closeCard = () => {
380
- if (enterTimeoutRef.current) {
381
- window.clearTimeout(enterTimeoutRef.current);
382
- }
383
-
384
- exitTimeoutRef.current = window.setTimeout(() => {
385
- setIsOpen(false);
386
- setAnchorEl(null);
387
- onClose?.();
388
- }, exitDelay);
389
- };
390
-
391
- const handleMouseEnter = (event: React.MouseEvent<HTMLElement>) => {
392
- if (isTouchDevice && !touchEnabled) return;
393
- openCard(event);
394
- };
395
-
396
- const handleMouseLeave = () => {
397
- if (isTouchDevice && !touchEnabled) return;
398
- closeCard();
399
- };
400
-
401
- const handleTouchStart = (event: React.TouchEvent<HTMLElement>) => {
402
- if (!touchEnabled) return;
403
-
404
- // Clear any existing timeout
405
- if (touchTimeout) {
406
- window.clearTimeout(touchTimeout);
407
- }
408
-
409
- // Set a timeout to show hover card on long press
410
- const timeout = window.setTimeout(() => {
411
- openCard(event);
412
- }, enterDelay);
413
-
414
- setTouchTimeout(timeout);
415
- };
416
-
417
- const handleTouchEnd = () => {
418
- if (touchTimeout) {
419
- window.clearTimeout(touchTimeout);
420
- setTouchTimeout(undefined);
421
- }
422
- };
423
-
424
- const handleTouchCancel = () => {
425
- if (touchTimeout) {
426
- window.clearTimeout(touchTimeout);
427
- setTouchTimeout(undefined);
428
- }
429
- };
430
-
431
- const handlePopoverMouseEnter = () => {
432
- if (exitTimeoutRef.current) {
433
- window.clearTimeout(exitTimeoutRef.current);
434
- }
435
- };
436
-
437
- const handlePopoverMouseLeave = () => {
438
- closeCard();
439
- };
440
-
441
- const handleClose = React.useCallback(() => {
442
- setIsOpen(false);
443
- setAnchorEl(null);
444
- onClose?.();
445
- }, [onClose]);
446
-
447
- // Handle escape key press
448
- React.useEffect(() => {
449
- const handleEscape = (event: globalThis.KeyboardEvent) => {
450
- if (event.key === 'Escape' && isOpen) {
451
- handleClose();
452
- }
453
- };
454
-
455
- if (isOpen) {
456
- document.addEventListener('keydown', handleEscape);
457
- }
458
-
459
- return () => {
460
- document.removeEventListener('keydown', handleEscape);
461
- };
462
- }, [isOpen, handleClose]);
463
-
464
- React.useEffect(() => () => {
465
- if (enterTimeoutRef.current) {
466
- window.clearTimeout(enterTimeoutRef.current);
467
- }
468
- if (exitTimeoutRef.current) {
469
- window.clearTimeout(exitTimeoutRef.current);
470
- }
471
- if (touchTimeout) {
472
- window.clearTimeout(touchTimeout);
473
- }
474
- }, [touchTimeout]);
475
-
476
- const triggerProps = {
477
- onMouseEnter: handleMouseEnter,
478
- onMouseLeave: handleMouseLeave,
479
- onTouchStart: handleTouchStart,
480
- onTouchEnd: handleTouchEnd,
481
- onTouchCancel: handleTouchCancel,
482
- style: { cursor: disabled ? 'default' : 'pointer' },
483
- 'data-testid': dataTestId ? `${dataTestId}-trigger` : 'hover-card-trigger',
484
- };
485
-
486
- const triggerElement = trigger ? (
487
- React.cloneElement(trigger, triggerProps)
488
- ) : (
489
- <span {...triggerProps} style={{ display: 'inline-block', ...triggerProps.style }}>
490
- {children}
491
- </span>
492
- );
493
-
494
- const renderAvatar = () => {
495
- if (!avatar) return null;
496
-
497
- if (typeof avatar === 'string') {
498
- return <Avatar src={avatar} sx={{ width: 48, height: 48, mr: 2 }} />;
499
- }
500
-
501
- const avatarElement = avatar as React.ReactElement<{ sx?: object }>;
502
- return React.cloneElement(avatarElement, {
503
- sx: { width: 48, height: 48, mr: 2, ...avatarElement.props.sx },
504
- });
505
- };
506
-
507
- const renderContent = () => {
508
- if (loading) {
509
- return (
510
- <LoadingContainer>
511
- {loadingComponent || (
512
- <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
513
- <CircularProgress size={20} />
514
- <Typography variant="body2">Loading...</Typography>
515
- </Box>
516
- )}
517
- </LoadingContainer>
518
- );
519
- }
520
-
521
- if (variant === 'detailed' && (avatar || title || description)) {
522
- return (
523
- <>
524
- {avatar && (
525
- <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
526
- {renderAvatar()}
527
- <Box>
528
- {title && (
529
- <Typography
530
- variant="h6"
531
- sx={{ fontWeight: 600, mb: 0.5 }}
532
- data-testid={dataTestId ? `${dataTestId}-title` : 'hover-card-title'}
533
- >
534
- {title}
535
- </Typography>
536
- )}
537
- {description && (
538
- <Typography
539
- variant="body2"
540
- color="text.secondary"
541
- data-testid={dataTestId ? `${dataTestId}-description` : 'hover-card-description'}
542
- >
543
- {description}
544
- </Typography>
545
- )}
546
- </Box>
547
- </Box>
548
- )}
549
- {!avatar && title && (
550
- <Typography
551
- variant="h6"
552
- sx={{ fontWeight: 600, mb: 1 }}
553
- data-testid={dataTestId ? `${dataTestId}-title` : 'hover-card-title'}
554
- >
555
- {title}
556
- </Typography>
557
- )}
558
- {!avatar && description && (
559
- <Typography
560
- variant="body2"
561
- color="text.secondary"
562
- sx={{ mb: 1 }}
563
- data-testid={dataTestId ? `${dataTestId}-description` : 'hover-card-description'}
564
- >
565
- {description}
566
- </Typography>
567
- )}
568
- {children && <Box>{children}</Box>}
569
- </>
570
- );
571
- }
572
-
573
- return (
574
- <>
575
- {title && (
576
- <Typography
577
- variant={variant === 'minimal' ? 'body2' : 'subtitle1'}
578
- sx={{ fontWeight: 600, mb: 0.5 }}
579
- data-testid={dataTestId ? `${dataTestId}-title` : 'hover-card-title'}
580
- >
581
- {title}
582
- </Typography>
583
- )}
584
- {description && (
585
- <Typography
586
- variant="body2"
587
- color="text.secondary"
588
- data-testid={dataTestId ? `${dataTestId}-description` : 'hover-card-description'}
589
- >
590
- {description}
591
- </Typography>
592
- )}
593
- {children}
594
- </>
595
- );
596
- };
62
+ export const HoverCard = React.forwardRef<HTMLDivElement, HoverCardProps>((props, ref) => {
63
+ const {
64
+ variant, glow, pulse, title, description, avatar, trigger, placement, showArrow,
65
+ animation, enterDelay, exitDelay, maxWidth, loading, loadingComponent, touchEnabled,
66
+ offset, disabled, onOpen, onClose, children, dataTestId,
67
+ ...rest
68
+ } = withDefaults(props, DEFAULTS) as ResolvedProps;
69
+
70
+ const { anchorEl, isOpen, handleClose, triggerHandlers, cardHandlers } = useHoverCard({
71
+ disabled,
72
+ touchEnabled,
73
+ enterDelay,
74
+ exitDelay,
75
+ onOpen,
76
+ onClose,
77
+ });
78
+
79
+ const triggerProps = {
80
+ ...triggerHandlers,
81
+ style: { cursor: disabled ? 'default' : 'pointer' },
82
+ 'data-testid': dataTestId ? `${dataTestId}-trigger` : 'hover-card-trigger',
83
+ };
597
84
 
598
- return (
599
- <>
600
- {triggerElement}
601
- <StyledPopover
602
- ref={ref}
603
- open={isOpen && !disabled}
604
- anchorEl={anchorEl}
605
- onClose={handleClose}
606
- anchorOrigin={getAnchorOrigin(placement)}
607
- transformOrigin={getTransformOrigin(placement)}
608
- onMouseEnter={handlePopoverMouseEnter}
609
- onMouseLeave={handlePopoverMouseLeave}
610
- disableAutoFocus={true}
611
- disableEnforceFocus={true}
612
- disableRestoreFocus={true}
613
- customAnimation={animation}
614
- slotProps={{
615
- paper: {
616
- style: {
617
- pointerEvents: 'auto',
618
- },
619
- },
620
- }}
621
- {...props}
622
- >
623
- <Box sx={{ position: 'relative' }}>
624
- {showArrow && <ArrowContainer placement={placement} offset={offset} />}
625
- <StyledCard
626
- customVariant={variant}
627
- glow={glow}
628
- pulse={pulse}
629
- animation={animation}
630
- data-testid={dataTestId || 'hover-card-content'}
631
- sx={{
632
- maxWidth,
633
- mt: placement.startsWith('top') ? `${offset}px` : 0,
634
- mb: placement.startsWith('bottom') ? `${offset}px` : 0,
635
- ml: placement.startsWith('left') ? `${offset}px` : 0,
636
- mr: placement.startsWith('right') ? `${offset}px` : 0,
637
- }}
638
- >
639
- <CardContent
640
- sx={{
641
- p: variant === 'detailed' ? 3 : variant === 'minimal' ? 1.5 : 2,
642
- '&:last-child': {
643
- pb: variant === 'detailed' ? 3 : variant === 'minimal' ? 1.5 : 2,
644
- },
645
- }}
85
+ const triggerElement = trigger ? (
86
+ React.cloneElement(trigger, triggerProps)
87
+ ) : (
88
+ <span {...triggerProps} style={{ display: 'inline-block', ...triggerProps.style }}>
89
+ {children}
90
+ </span>
91
+ );
92
+
93
+ const padding = CONTENT_PADDING[variant] ?? 2;
94
+
95
+ return (
96
+ <>
97
+ {triggerElement}
98
+ <StyledPopover
99
+ ref={ref}
100
+ open={isOpen && !disabled}
101
+ anchorEl={anchorEl}
102
+ onClose={handleClose}
103
+ anchorOrigin={getAnchorOrigin(placement)}
104
+ transformOrigin={getTransformOrigin(placement)}
105
+ {...cardHandlers}
106
+ disableAutoFocus
107
+ disableEnforceFocus
108
+ disableRestoreFocus
109
+ customAnimation={animation}
110
+ slotProps={{ paper: { style: { pointerEvents: 'auto' } } }}
111
+ {...rest}
112
+ >
113
+ <Box sx={{ position: 'relative' }}>
114
+ {showArrow && <ArrowContainer placement={placement} offset={offset} />}
115
+ <StyledCard
116
+ customVariant={variant}
117
+ glow={glow}
118
+ pulse={pulse}
119
+ animation={animation}
120
+ data-testid={dataTestId || 'hover-card-content'}
121
+ sx={{ maxWidth, ...offsetMargin(placement, offset) }}
122
+ >
123
+ <CardContent sx={{ p: padding, '&:last-child': { pb: padding } }}>
124
+ <HoverCardContent
125
+ variant={variant}
126
+ title={title}
127
+ description={description}
128
+ avatar={avatar}
129
+ loading={loading}
130
+ loadingComponent={loadingComponent}
131
+ dataTestId={dataTestId}
646
132
  >
647
- {renderContent()}
648
- </CardContent>
649
- </StyledCard>
650
- </Box>
651
- </StyledPopover>
652
- </>
653
- );
654
- },
655
- );
133
+ {children}
134
+ </HoverCardContent>
135
+ </CardContent>
136
+ </StyledCard>
137
+ </Box>
138
+ </StyledPopover>
139
+ </>
140
+ );
141
+ });
656
142
 
657
143
  HoverCard.displayName = 'HoverCard';