@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.
- package/eslint.config.js +12 -0
- package/package.json +5 -4
- package/src/components/data-display/AlertDialog/AlertDialog.test.stories.tsx +16 -6
- package/src/components/data-display/Banner/Banner.test.stories.tsx +26 -18
- package/src/components/data-display/Carousel/Carousel.chrome.tsx +83 -0
- package/src/components/data-display/Carousel/Carousel.hooks.ts +88 -0
- package/src/components/data-display/Carousel/Carousel.parts.tsx +346 -0
- package/src/components/data-display/Carousel/Carousel.slide.tsx +190 -0
- package/src/components/data-display/Carousel/Carousel.styles.ts +86 -0
- package/src/components/data-display/Carousel/Carousel.test.stories.tsx +15 -5
- package/src/components/data-display/Carousel/Carousel.tsx +84 -732
- package/src/components/data-display/HoverCard/HoverCard.animations.ts +73 -0
- package/src/components/data-display/HoverCard/HoverCard.content.tsx +143 -0
- package/src/components/data-display/HoverCard/HoverCard.hooks.ts +180 -0
- package/src/components/data-display/HoverCard/HoverCard.styles.ts +178 -0
- package/src/components/data-display/HoverCard/HoverCard.tsx +117 -631
- package/src/components/data-display/Sheet/Sheet.animations.ts +44 -0
- package/src/components/data-display/Sheet/Sheet.body.tsx +124 -0
- package/src/components/data-display/Sheet/Sheet.drag.ts +203 -0
- package/src/components/data-display/Sheet/Sheet.hooks.ts +318 -0
- package/src/components/data-display/Sheet/Sheet.parts.tsx +224 -0
- package/src/components/data-display/Sheet/Sheet.styles.ts +257 -0
- package/src/components/data-display/Sheet/Sheet.test.stories.tsx +25 -21
- package/src/components/data-display/Sheet/Sheet.tsx +125 -948
- package/src/components/feedback/Dialog/Dialog.test.stories.tsx +37 -28
- package/src/components/feedback/Modal/Modal.styles.ts +126 -0
- package/src/components/feedback/Modal/Modal.test.stories.tsx +5 -5
- package/src/components/feedback/Modal/Modal.transitions.tsx +40 -0
- package/src/components/feedback/Modal/Modal.tsx +37 -242
- package/src/components/feedback/StackedModal/StackedModal.test.stories.tsx +9 -11
- package/src/components/form/InputOTP/InputOTP.hooks.ts +104 -0
- package/src/components/form/InputOTP/InputOTP.styles.ts +97 -0
- package/src/components/form/InputOTP/InputOTP.tsx +74 -221
- package/src/components/form/Label/Label.parts.tsx +75 -0
- package/src/components/form/Label/Label.styles.ts +206 -0
- package/src/components/form/Label/Label.test.stories.tsx +20 -14
- package/src/components/form/Label/Label.tsx +73 -325
- package/src/components/form/Menubar/Menubar.hooks.ts +41 -0
- package/src/components/form/Menubar/Menubar.item.tsx +121 -0
- package/src/components/form/Menubar/Menubar.separator.tsx +25 -0
- package/src/components/form/Menubar/Menubar.styles.ts +110 -0
- package/src/components/form/Menubar/Menubar.toolbar.tsx +103 -0
- package/src/components/form/Menubar/Menubar.tsx +64 -347
- package/src/components/form/RadioGroup/RadioGroup.animations.ts +48 -0
- package/src/components/form/RadioGroup/RadioGroup.styles.ts +305 -0
- package/src/components/form/RadioGroup/RadioGroup.tsx +81 -651
- package/src/components/form/RadioGroup/RadioGroup.variants.tsx +291 -0
- package/src/components/form/Slider/Slider.animations.ts +40 -0
- package/src/components/form/Slider/Slider.styles.ts +292 -0
- package/src/components/form/Slider/Slider.tsx +106 -357
- package/src/components/form/Switch/Switch.animations.ts +62 -0
- package/src/components/form/Switch/Switch.parts.tsx +213 -0
- package/src/components/form/Switch/Switch.styles.ts +355 -0
- package/src/components/form/Switch/Switch.tsx +87 -566
- package/src/components/form/ToggleGroup/ToggleGroup.test.stories.tsx +26 -29
- package/src/components/layout/Resizable/Resizable.test.stories.tsx +11 -7
- package/src/components/layout/Spacer/Spacer.test.stories.tsx +11 -9
- package/src/components/typography/Blockquote/Blockquote.test.stories.tsx +7 -7
- package/src/components/typography/Heading/Heading.styles.ts +101 -0
- package/src/components/typography/Heading/Heading.tsx +48 -164
- package/src/components/typography/Text/Text.test.stories.tsx +27 -30
- package/src/components/utility/AnimatedIcon/AnimatedIcon.animations.ts +274 -0
- package/src/components/utility/AnimatedIcon/AnimatedIcon.styles.ts +231 -0
- package/src/components/utility/AnimatedIcon/AnimatedIcon.tsx +39 -559
- package/src/utils/index.ts +1 -0
- 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
|
-
|
|
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]
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
//
|
|
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
|
+
};
|