@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
@@ -42,8 +42,8 @@ export const BasicInteraction: Story = {
42
42
  await userEvent.click(textElement);
43
43
 
44
44
  // Test text element maintains focus capabilities
45
- textElement.focus();
46
- expect(textElement).toHaveFocus();
45
+ await userEvent.click(textElement);
46
+ await waitFor(() => expect(textElement).toHaveFocus());
47
47
 
48
48
  // Test element can be selected
49
49
  await userEvent.tripleClick(textElement);
@@ -154,31 +154,31 @@ export const KeyboardNavigation: Story = {
154
154
  ];
155
155
 
156
156
  // Test initial focus
157
- focusableTexts[0].focus();
158
- expect(focusableTexts[0]).toHaveFocus();
157
+ await userEvent.click(focusableTexts[0]);
158
+ await waitFor(() => expect(focusableTexts[0]).toHaveFocus());
159
159
 
160
160
  // Test Tab navigation between elements
161
161
  await userEvent.tab();
162
- expect(focusableTexts[1]).toHaveFocus();
162
+ await waitFor(() => expect(focusableTexts[1]).toHaveFocus());
163
163
 
164
164
  await userEvent.tab();
165
- expect(focusableTexts[2]).toHaveFocus();
165
+ await waitFor(() => expect(focusableTexts[2]).toHaveFocus());
166
166
 
167
167
  // Test Shift+Tab backward navigation
168
168
  await userEvent.tab({ shift: true });
169
- expect(focusableTexts[1]).toHaveFocus();
169
+ await waitFor(() => expect(focusableTexts[1]).toHaveFocus());
170
170
 
171
171
  // Test keyboard interaction doesn't lose focus
172
172
  await userEvent.keyboard('{Enter}');
173
- expect(focusableTexts[1]).toHaveFocus();
173
+ await waitFor(() => expect(focusableTexts[1]).toHaveFocus());
174
174
 
175
175
  // Test Space key interaction
176
176
  await userEvent.keyboard(' ');
177
- expect(focusableTexts[1]).toHaveFocus();
177
+ await waitFor(() => expect(focusableTexts[1]).toHaveFocus());
178
178
 
179
179
  // Test Escape key interaction
180
180
  await userEvent.keyboard('{Escape}');
181
- expect(focusableTexts[1]).toHaveFocus();
181
+ await waitFor(() => expect(focusableTexts[1]).toHaveFocus());
182
182
  },
183
183
  render: () => (
184
184
  <Stack spacing={2}>
@@ -259,31 +259,30 @@ export const FocusManagement: Story = {
259
259
  const triggerButton = canvas.getByTestId('focus-trigger');
260
260
 
261
261
  // Test initial focus state
262
- expect(focusableText).not.toHaveFocus();
262
+ await waitFor(() => expect(focusableText).not.toHaveFocus());
263
263
 
264
264
  // Test programmatic focus
265
265
  await userEvent.click(triggerButton);
266
266
  await waitFor(() => {
267
- expect(focusableText).toHaveFocus();
267
+ await waitFor(() => expect(focusableText).toHaveFocus());
268
268
  });
269
269
 
270
- // Test focus containment within container
271
- focusableText.focus();
272
- expect(document.activeElement).toBe(focusableText);
273
-
274
- // Test focus visibility
275
- expect(focusableText).toHaveFocus();
270
+ // Test focus containment within container. The activeElement read that used
271
+ // to sit here asserted exactly what the waitFor below does, one line earlier
272
+ // and without retrying.
273
+ await userEvent.click(focusableText);
274
+ await waitFor(() => expect(focusableText).toHaveFocus());
276
275
 
277
276
  // Test blur handling
278
277
  focusableText.blur();
279
- expect(focusableText).not.toHaveFocus();
278
+ await waitFor(() => expect(focusableText).not.toHaveFocus());
280
279
  },
281
280
  render: function FocusManagementRender() {
282
281
  const [focusTarget, setFocusTarget] = React.useState<HTMLElement | null>(null);
283
282
 
284
283
  const handleFocus = () => {
285
284
  if (focusTarget) {
286
- focusTarget.focus();
285
+ await userEvent.click(focusTarget);
287
286
  }
288
287
  };
289
288
 
@@ -325,9 +324,12 @@ export const ResponsiveDesign: Story = {
325
324
  expect(parseFloat(computedStyle.lineHeight)).toBeGreaterThan(1);
326
325
 
327
326
  // Test text wrapping behavior
327
+ // Wrapping is about the text surviving a narrow container, not about any
328
+ // particular pixel width: the whole string must still be rendered. The old
329
+ // `scrollWidth > 0` held for any element that exists at all.
328
330
  const longText = canvas.getByTestId('long-text');
329
331
  expect(longText).toBeInTheDocument();
330
- expect(longText.scrollWidth).toBeGreaterThan(0);
332
+ expect(longText.textContent?.trim().length).toBeGreaterThan(0);
331
333
  },
332
334
  render: () => (
333
335
  <Box sx={{ width: '100%', maxWidth: '320px' }}>
@@ -504,18 +506,13 @@ export const Performance: Story = {
504
506
  play: async ({ canvasElement }) => {
505
507
  const canvas = within(canvasElement);
506
508
 
507
- const startTime = globalThis.performance?.now() || Date.now();
508
-
509
- // Test rendering many text elements
509
+ // All fifty render — the assertion the wall-clock budget that used to
510
+ // bracket this query was standing in for. The query ran against elements
511
+ // that were already on the page, so the number it produced measured the
512
+ // machine rather than the component.
510
513
  const textElements = canvas.getAllByText(/Performance text/);
511
514
  expect(textElements).toHaveLength(50);
512
515
 
513
- const endTime = globalThis.performance?.now() || Date.now();
514
- const renderTime = endTime - startTime;
515
-
516
- // Performance should be reasonable (less than 100ms for 50 elements)
517
- expect(renderTime).toBeLessThan(100);
518
-
519
516
  // Test large text content rendering
520
517
  const largeTextElement = canvas.getByTestId('large-content');
521
518
  expect(largeTextElement).toBeInTheDocument();
@@ -0,0 +1,274 @@
1
+ import { keyframes } from '@mui/material';
2
+
3
+ export const rotateAnimation = keyframes`
4
+ from {
5
+ transform: rotate(0deg);
6
+ }
7
+ to {
8
+ transform: rotate(360deg);
9
+ }
10
+ `;
11
+
12
+ export const pulseAnimation = keyframes`
13
+ 0% {
14
+ transform: scale(1);
15
+ opacity: 1;
16
+ }
17
+ 50% {
18
+ transform: scale(1.1);
19
+ opacity: 0.8;
20
+ }
21
+ 100% {
22
+ transform: scale(1);
23
+ opacity: 1;
24
+ }
25
+ `;
26
+
27
+ export const translateAnimation = keyframes`
28
+ 0% {
29
+ transform: translateY(0px);
30
+ }
31
+ 50% {
32
+ transform: translateY(-8px);
33
+ }
34
+ 100% {
35
+ transform: translateY(0px);
36
+ }
37
+ `;
38
+
39
+ export const glowPulseAnimation = keyframes`
40
+ 0% {
41
+ box-shadow: 0 0 5px currentColor, 0 0 10px currentColor, 0 0 15px currentColor;
42
+ }
43
+ 50% {
44
+ box-shadow: 0 0 10px currentColor, 0 0 20px currentColor, 0 0 30px currentColor;
45
+ }
46
+ 100% {
47
+ box-shadow: 0 0 5px currentColor, 0 0 10px currentColor, 0 0 15px currentColor;
48
+ }
49
+ `;
50
+
51
+ // New animation keyframes
52
+ export const bounceAnimation = keyframes`
53
+ 0%, 20%, 50%, 80%, 100% {
54
+ transform: translateY(0);
55
+ }
56
+ 40% {
57
+ transform: translateY(-20px);
58
+ }
59
+ 60% {
60
+ transform: translateY(-10px);
61
+ }
62
+ `;
63
+
64
+ export const shakeAnimation = keyframes`
65
+ 0%, 100% {
66
+ transform: translateX(0);
67
+ }
68
+ 10%, 30%, 50%, 70%, 90% {
69
+ transform: translateX(-4px);
70
+ }
71
+ 20%, 40%, 60%, 80% {
72
+ transform: translateX(4px);
73
+ }
74
+ `;
75
+
76
+ export const flipAnimation = keyframes`
77
+ 0% {
78
+ transform: perspective(400px) rotateY(0);
79
+ }
80
+ 100% {
81
+ transform: perspective(400px) rotateY(360deg);
82
+ }
83
+ `;
84
+
85
+ export const spinAnimation = keyframes`
86
+ 0% {
87
+ transform: rotate(0deg) scale(1);
88
+ }
89
+ 50% {
90
+ transform: rotate(180deg) scale(1.2);
91
+ }
92
+ 100% {
93
+ transform: rotate(360deg) scale(1);
94
+ }
95
+ `;
96
+
97
+ export const fadeInOutAnimation = keyframes`
98
+ 0%, 100% {
99
+ opacity: 0.3;
100
+ }
101
+ 50% {
102
+ opacity: 1;
103
+ }
104
+ `;
105
+
106
+ export const heartbeatAnimation = keyframes`
107
+ 0%, 100% {
108
+ transform: scale(1);
109
+ }
110
+ 5% {
111
+ transform: scale(1.25);
112
+ }
113
+ 10% {
114
+ transform: scale(1);
115
+ }
116
+ 15% {
117
+ transform: scale(1.25);
118
+ }
119
+ 20% {
120
+ transform: scale(1);
121
+ }
122
+ `;
123
+
124
+ export const wobbleAnimation = keyframes`
125
+ 0%, 100% {
126
+ transform: translateX(0) rotate(0deg);
127
+ }
128
+ 15% {
129
+ transform: translateX(-10px) rotate(-5deg);
130
+ }
131
+ 30% {
132
+ transform: translateX(8px) rotate(3deg);
133
+ }
134
+ 45% {
135
+ transform: translateX(-6px) rotate(-3deg);
136
+ }
137
+ 60% {
138
+ transform: translateX(4px) rotate(2deg);
139
+ }
140
+ 75% {
141
+ transform: translateX(-2px) rotate(-1deg);
142
+ }
143
+ `;
144
+
145
+ export const morphAnimation = keyframes`
146
+ 0%, 100% {
147
+ border-radius: 50%;
148
+ transform: scale(1);
149
+ }
150
+ 25% {
151
+ border-radius: 30%;
152
+ transform: scale(1.1);
153
+ }
154
+ 50% {
155
+ border-radius: 20%;
156
+ transform: scale(0.95);
157
+ }
158
+ 75% {
159
+ border-radius: 40%;
160
+ transform: scale(1.05);
161
+ }
162
+ `;
163
+
164
+ export const swingAnimation = keyframes`
165
+ 20% {
166
+ transform: rotate(15deg);
167
+ }
168
+ 40% {
169
+ transform: rotate(-10deg);
170
+ }
171
+ 60% {
172
+ transform: rotate(5deg);
173
+ }
174
+ 80% {
175
+ transform: rotate(-5deg);
176
+ }
177
+ 100% {
178
+ transform: rotate(0deg);
179
+ }
180
+ `;
181
+
182
+ export const floatAnimation = keyframes`
183
+ 0%, 100% {
184
+ transform: translateY(0) translateX(0);
185
+ }
186
+ 33% {
187
+ transform: translateY(-10px) translateX(-5px);
188
+ }
189
+ 66% {
190
+ transform: translateY(5px) translateX(5px);
191
+ }
192
+ `;
193
+
194
+ export const jelloAnimation = keyframes`
195
+ 0%, 100% {
196
+ transform: scale(1, 1);
197
+ }
198
+ 25% {
199
+ transform: scale(0.9, 1.1);
200
+ }
201
+ 50% {
202
+ transform: scale(1.1, 0.9);
203
+ }
204
+ 75% {
205
+ transform: scale(0.95, 1.05);
206
+ }
207
+ `;
208
+
209
+ export const rippleAnimation = keyframes`
210
+ 0% {
211
+ transform: scale(0.8);
212
+ opacity: 1;
213
+ }
214
+ 100% {
215
+ transform: scale(2);
216
+ opacity: 0;
217
+ }
218
+ `;
219
+
220
+ export const neonFlickerAnimation = keyframes`
221
+ 0%, 100% {
222
+ opacity: 1;
223
+ filter: brightness(1) drop-shadow(0 0 10px currentColor);
224
+ }
225
+ 10% {
226
+ opacity: 0.8;
227
+ filter: brightness(0.8) drop-shadow(0 0 5px currentColor);
228
+ }
229
+ 20% {
230
+ opacity: 1;
231
+ filter: brightness(1.2) drop-shadow(0 0 15px currentColor);
232
+ }
233
+ 30% {
234
+ opacity: 0.9;
235
+ filter: brightness(0.9) drop-shadow(0 0 8px currentColor);
236
+ }
237
+ 40% {
238
+ opacity: 1;
239
+ filter: brightness(1.1) drop-shadow(0 0 12px currentColor);
240
+ }
241
+ 50% {
242
+ opacity: 0.95;
243
+ filter: brightness(1) drop-shadow(0 0 10px currentColor);
244
+ }
245
+ 60% {
246
+ opacity: 0.85;
247
+ filter: brightness(0.85) drop-shadow(0 0 6px currentColor);
248
+ }
249
+ 70% {
250
+ opacity: 1;
251
+ filter: brightness(1.15) drop-shadow(0 0 14px currentColor);
252
+ }
253
+ 80% {
254
+ opacity: 0.9;
255
+ filter: brightness(0.95) drop-shadow(0 0 9px currentColor);
256
+ }
257
+ 90% {
258
+ opacity: 1;
259
+ filter: brightness(1.05) drop-shadow(0 0 11px currentColor);
260
+ }
261
+ `;
262
+
263
+ export const breatheAnimation = keyframes`
264
+ 0%, 100% {
265
+ transform: scale(1);
266
+ filter: brightness(1);
267
+ }
268
+ 50% {
269
+ transform: scale(1.05);
270
+ filter: brightness(1.2);
271
+ }
272
+ `;
273
+
274
+ // Size configurations
@@ -0,0 +1,231 @@
1
+ import { alpha } from '@mui/material';
2
+ import type { CSSObject, Theme } from '@mui/material';
3
+
4
+ import {
5
+ bounceAnimation,
6
+ breatheAnimation,
7
+ fadeInOutAnimation,
8
+ flipAnimation,
9
+ floatAnimation,
10
+ glowPulseAnimation,
11
+ heartbeatAnimation,
12
+ jelloAnimation,
13
+ morphAnimation,
14
+ neonFlickerAnimation,
15
+ pulseAnimation,
16
+ rippleAnimation,
17
+ rotateAnimation,
18
+ shakeAnimation,
19
+ spinAnimation,
20
+ swingAnimation,
21
+ translateAnimation,
22
+ wobbleAnimation,
23
+ } from './AnimatedIcon.animations';
24
+ import type { AnimationSize, AnimationVariant } from './AnimatedIcon.types';
25
+
26
+ export const sizeConfigs: Record<AnimationSize, { size: number; fontSize: number }> = {
27
+ sm: { size: 24, fontSize: 20 },
28
+ md: { size: 32, fontSize: 28 },
29
+ lg: { size: 48, fontSize: 44 },
30
+ xl: { size: 64, fontSize: 60 },
31
+ };
32
+
33
+ /**
34
+ * Every variant produces the same shorthand — keyframes, duration, easing,
35
+ * iteration count, delay — so only the three things that actually differ are
36
+ * tabulated. `scale` multiplies the caller's duration for the variants that
37
+ * read as slower than the rest.
38
+ */
39
+ const ANIMATIONS: Record<string, { keyframes: string; easing: string; scale: number }> = {
40
+ rotate: { keyframes: `${rotateAnimation}`, easing: 'linear', scale: 1 },
41
+ pulse: { keyframes: `${pulseAnimation}`, easing: 'ease-in-out', scale: 1 },
42
+ translate: { keyframes: `${translateAnimation}`, easing: 'ease-in-out', scale: 1 },
43
+ bounce: { keyframes: `${bounceAnimation}`, easing: 'ease-in-out', scale: 1 },
44
+ shake: { keyframes: `${shakeAnimation}`, easing: 'ease-in-out', scale: 1 },
45
+ flip: { keyframes: `${flipAnimation}`, easing: 'ease-in-out', scale: 1 },
46
+ spin: { keyframes: `${spinAnimation}`, easing: 'ease-in-out', scale: 1 },
47
+ fadeInOut: { keyframes: `${fadeInOutAnimation}`, easing: 'ease-in-out', scale: 1 },
48
+ heartbeat: { keyframes: `${heartbeatAnimation}`, easing: 'ease-in-out', scale: 1 },
49
+ wobble: { keyframes: `${wobbleAnimation}`, easing: 'ease-in-out', scale: 1 },
50
+ morph: { keyframes: `${morphAnimation}`, easing: 'ease-in-out', scale: 1 },
51
+ swing: { keyframes: `${swingAnimation}`, easing: 'ease-in-out', scale: 1 },
52
+ jello: { keyframes: `${jelloAnimation}`, easing: 'ease-in-out', scale: 1 },
53
+ float: { keyframes: `${floatAnimation}`, easing: 'ease-in-out', scale: 2 },
54
+ neonFlicker: { keyframes: `${neonFlickerAnimation}`, easing: 'ease-in-out', scale: 2 },
55
+ breathe: { keyframes: `${breatheAnimation}`, easing: 'ease-in-out', scale: 1.5 },
56
+ };
57
+
58
+ const animationFor = (
59
+ variant: AnimationVariant,
60
+ duration: number,
61
+ delay: number,
62
+ loop: boolean,
63
+ ) => {
64
+ const spec = ANIMATIONS[variant];
65
+ if (!spec) return 'none';
66
+
67
+ const iterationCount = loop ? 'infinite' : '1';
68
+ return `${spec.keyframes} ${duration * spec.scale}s ${spec.easing} ${iterationCount} ${delay > 0 ? `${delay}s` : '0s'}`;
69
+ };
70
+
71
+ const SHADOWS: Record<string, (theme: Theme) => string> = {
72
+ soft: (theme) => `0 4px 20px ${alpha(theme.palette.common.black, 0.15)}`,
73
+ hard: (theme) => `4px 4px 0px ${alpha(theme.palette.common.black, 0.25)}`,
74
+ elevated: (theme) =>
75
+ `0 10px 40px ${alpha(theme.palette.common.black, 0.2)}, 0 2px 10px ${alpha(theme.palette.common.black, 0.1)}`,
76
+ };
77
+
78
+ export interface IconStyleFlags {
79
+ $size: number;
80
+ $fontSize: number;
81
+ $animationVariant: AnimationVariant;
82
+ $duration: number;
83
+ $delay: number;
84
+ $loop: boolean;
85
+ $glow: boolean;
86
+ $glass: boolean;
87
+ $glowColor?: string;
88
+ $customColor?: string;
89
+ $metallic?: boolean;
90
+ $gradient?: boolean;
91
+ $shadow?: 'soft' | 'hard' | 'elevated' | 'none';
92
+ $ripple?: boolean;
93
+ $neon?: boolean;
94
+ $holographic?: boolean;
95
+ }
96
+
97
+ /**
98
+ * The four surface treatments that paint the icon themselves — metallic,
99
+ * gradient, neon and holographic — each run a second animation alongside the
100
+ * chosen one, so they all need the base animation string.
101
+ */
102
+ const effectStyles = (
103
+ theme: Theme,
104
+ flags: IconStyleFlags,
105
+ animation: string,
106
+ color: string,
107
+ ): CSSObject => {
108
+ const { $glass, $metallic, $gradient, $neon, $holographic } = flags;
109
+
110
+ return {
111
+ ...($glass && {
112
+ background: `linear-gradient(135deg, ${alpha(theme.palette.background.paper, 0.2)}, ${alpha(theme.palette.background.paper, 0.1)})`,
113
+ backdropFilter: 'blur(10px)',
114
+ border: `1px solid ${alpha(theme.palette.divider, 0.2)}`,
115
+ boxShadow: `inset 0 1px 1px ${alpha(theme.palette.common.white, 0.3)}, 0 8px 32px ${alpha(theme.palette.common.black, 0.1)}`,
116
+ }),
117
+ ...($metallic && {
118
+ background: `linear-gradient(145deg,
119
+ ${theme.palette.grey[300]} 0%,
120
+ ${theme.palette.grey[500]} 25%,
121
+ ${theme.palette.grey[400]} 50%,
122
+ ${theme.palette.grey[600]} 75%,
123
+ ${theme.palette.grey[400]} 100%)`,
124
+ backgroundSize: '200% 200%',
125
+ animation: `${animation}, ${breatheAnimation} 4s ease-in-out infinite`,
126
+ boxShadow: `inset 0 2px 4px ${alpha(theme.palette.common.white, 0.5)},
127
+ inset 0 -2px 4px ${alpha(theme.palette.common.black, 0.3)},
128
+ 0 4px 8px ${alpha(theme.palette.common.black, 0.2)}`,
129
+ }),
130
+ ...($gradient && {
131
+ background: `linear-gradient(135deg,
132
+ ${theme.palette.primary.main} 0%,
133
+ ${theme.palette.secondary.main} 100%)`,
134
+ backgroundSize: '200% 200%',
135
+ animation: `${animation}, ${breatheAnimation} 3s ease-in-out infinite`,
136
+ }),
137
+ ...($neon && {
138
+ color,
139
+ textShadow: `0 0 10px ${color}, 0 0 20px ${color}, 0 0 30px ${color}`,
140
+ filter: `brightness(1.2) contrast(1.2)`,
141
+ animation: `${animation}, ${neonFlickerAnimation} 3s ease-in-out infinite`,
142
+ }),
143
+ ...($holographic && {
144
+ background: `linear-gradient(45deg,
145
+ ${theme.palette.primary.main} 0%,
146
+ ${theme.palette.secondary.main} 25%,
147
+ ${theme.palette.error.main} 50%,
148
+ ${theme.palette.warning.main} 75%,
149
+ ${theme.palette.primary.main} 100%)`,
150
+ backgroundSize: '400% 400%',
151
+ animation: `${animation}, ${floatAnimation} 6s ease-in-out infinite`,
152
+ backgroundClip: 'text',
153
+ WebkitBackgroundClip: 'text',
154
+ WebkitTextFillColor: 'transparent',
155
+ filter: `hue-rotate(0deg)`,
156
+ '@keyframes hueRotate': {
157
+ '0%': { filter: 'hue-rotate(0deg)' },
158
+ '100%': { filter: 'hue-rotate(360deg)' },
159
+ },
160
+ '&::before': {
161
+ content: '""',
162
+ position: 'absolute' as const,
163
+ inset: -2,
164
+ borderRadius: '50%',
165
+ background: 'inherit',
166
+ filter: 'blur(10px)',
167
+ opacity: 0.5,
168
+ zIndex: -1,
169
+ },
170
+ }),
171
+ };
172
+ };
173
+
174
+ export const iconSx = (theme: Theme, flags: IconStyleFlags): CSSObject => {
175
+ const {
176
+ $size, $fontSize, $animationVariant, $duration, $delay, $loop,
177
+ $glow, $glowColor, $customColor, $shadow, $ripple, $neon,
178
+ } = flags;
179
+
180
+ const color = $customColor || theme.palette.primary.main;
181
+ const glowColor = $glowColor || color;
182
+ const animation = animationFor($animationVariant, $duration, $delay, $loop);
183
+
184
+ // Both selectors size the same glyph — MUI icons render an MuiSvgIcon-root,
185
+ // a bare child renders an svg.
186
+ const glyphSize = { fontSize: $fontSize, width: $fontSize, height: $fontSize };
187
+
188
+ return {
189
+ display: 'inline-flex',
190
+ alignItems: 'center',
191
+ justifyContent: 'center',
192
+ width: $size,
193
+ height: $size,
194
+ color,
195
+ animation,
196
+ position: 'relative' as const,
197
+ cursor: 'default',
198
+ borderRadius: '50%',
199
+ transition: 'all 0.3s ease',
200
+ boxShadow: SHADOWS[$shadow ?? 'none']?.(theme) ?? 'none',
201
+ ...($glow && {
202
+ '&::before': {
203
+ content: '""',
204
+ position: 'absolute' as const,
205
+ inset: -8,
206
+ borderRadius: '50%',
207
+ background: `radial-gradient(circle, ${alpha(glowColor, 0.3)} 0%, transparent 70%)`,
208
+ animation: `${glowPulseAnimation} ${$duration * 1.2}s ease-in-out infinite`,
209
+ zIndex: -1,
210
+ },
211
+ filter: `drop-shadow(0 0 8px ${alpha(glowColor, 0.5)})`,
212
+ }),
213
+ ...effectStyles(theme, flags, animation, color),
214
+ ...($ripple && {
215
+ '&::after': {
216
+ content: '""',
217
+ position: 'absolute' as const,
218
+ inset: 0,
219
+ borderRadius: '50%',
220
+ border: `2px solid ${color}`,
221
+ animation: `${rippleAnimation} ${$duration * 1.5}s ease-out infinite`,
222
+ },
223
+ }),
224
+ '& .MuiSvgIcon-root': glyphSize,
225
+ '& svg': glyphSize,
226
+ '&:hover': {
227
+ transform: 'scale(1.05)',
228
+ filter: $neon ? `brightness(1.4) contrast(1.3)` : undefined,
229
+ },
230
+ };
231
+ };