@papernote/ui 1.3.1 → 1.6.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 (108) hide show
  1. package/dist/components/ActionBar.d.ts +112 -0
  2. package/dist/components/ActionBar.d.ts.map +1 -0
  3. package/dist/components/BottomNavigation.d.ts +98 -0
  4. package/dist/components/BottomNavigation.d.ts.map +1 -0
  5. package/dist/components/Checkbox.d.ts +2 -0
  6. package/dist/components/Checkbox.d.ts.map +1 -1
  7. package/dist/components/CheckboxList.d.ts +81 -0
  8. package/dist/components/CheckboxList.d.ts.map +1 -0
  9. package/dist/components/Chip.d.ts +92 -1
  10. package/dist/components/Chip.d.ts.map +1 -1
  11. package/dist/components/ConfirmDialog.d.ts +43 -1
  12. package/dist/components/ConfirmDialog.d.ts.map +1 -1
  13. package/dist/components/DataTable.d.ts +10 -1
  14. package/dist/components/DataTable.d.ts.map +1 -1
  15. package/dist/components/DataTableCardView.d.ts +99 -0
  16. package/dist/components/DataTableCardView.d.ts.map +1 -0
  17. package/dist/components/ExpandablePanel.d.ts +142 -0
  18. package/dist/components/ExpandablePanel.d.ts.map +1 -0
  19. package/dist/components/FloatingActionButton.d.ts +98 -0
  20. package/dist/components/FloatingActionButton.d.ts.map +1 -0
  21. package/dist/components/Input.d.ts +45 -1
  22. package/dist/components/Input.d.ts.map +1 -1
  23. package/dist/components/MobileHeader.d.ts +98 -0
  24. package/dist/components/MobileHeader.d.ts.map +1 -0
  25. package/dist/components/MobileLayout.d.ts +121 -0
  26. package/dist/components/MobileLayout.d.ts.map +1 -0
  27. package/dist/components/Modal.d.ts +78 -1
  28. package/dist/components/Modal.d.ts.map +1 -1
  29. package/dist/components/PageHeader.d.ts +86 -0
  30. package/dist/components/PageHeader.d.ts.map +1 -0
  31. package/dist/components/PullToRefresh.d.ts +87 -0
  32. package/dist/components/PullToRefresh.d.ts.map +1 -0
  33. package/dist/components/QueryTransparency.d.ts +1 -1
  34. package/dist/components/QueryTransparency.d.ts.map +1 -1
  35. package/dist/components/SearchableList.d.ts +83 -0
  36. package/dist/components/SearchableList.d.ts.map +1 -0
  37. package/dist/components/Select.d.ts +16 -2
  38. package/dist/components/Select.d.ts.map +1 -1
  39. package/dist/components/Sidebar.d.ts +40 -1
  40. package/dist/components/Sidebar.d.ts.map +1 -1
  41. package/dist/components/SwipeActions.d.ts +93 -0
  42. package/dist/components/SwipeActions.d.ts.map +1 -0
  43. package/dist/components/Switch.d.ts +1 -0
  44. package/dist/components/Switch.d.ts.map +1 -1
  45. package/dist/components/Textarea.d.ts +13 -0
  46. package/dist/components/Textarea.d.ts.map +1 -1
  47. package/dist/components/index.d.ts +31 -3
  48. package/dist/components/index.d.ts.map +1 -1
  49. package/dist/context/MobileContext.d.ts +168 -0
  50. package/dist/context/MobileContext.d.ts.map +1 -0
  51. package/dist/hooks/useResponsive.d.ts +158 -0
  52. package/dist/hooks/useResponsive.d.ts.map +1 -0
  53. package/dist/index.d.ts +1871 -51
  54. package/dist/index.esm.js +3025 -196
  55. package/dist/index.esm.js.map +1 -1
  56. package/dist/index.js +3063 -194
  57. package/dist/index.js.map +1 -1
  58. package/dist/styles.css +434 -1
  59. package/dist/types/index.d.ts +2 -0
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/package.json +1 -1
  62. package/src/components/ActionBar.stories.tsx +246 -0
  63. package/src/components/ActionBar.tsx +242 -0
  64. package/src/components/BottomNavigation.stories.tsx +142 -0
  65. package/src/components/BottomNavigation.tsx +225 -0
  66. package/src/components/Checkbox.stories.tsx +162 -0
  67. package/src/components/Checkbox.tsx +22 -6
  68. package/src/components/CheckboxList.stories.tsx +311 -0
  69. package/src/components/CheckboxList.tsx +433 -0
  70. package/src/components/Chip.stories.tsx +389 -0
  71. package/src/components/Chip.tsx +182 -3
  72. package/src/components/ConfirmDialog.tsx +56 -4
  73. package/src/components/DataTable.tsx +60 -1
  74. package/src/components/DataTableCardView.stories.tsx +307 -0
  75. package/src/components/DataTableCardView.tsx +419 -0
  76. package/src/components/ExpandablePanel.stories.tsx +620 -0
  77. package/src/components/ExpandablePanel.tsx +383 -0
  78. package/src/components/FloatingActionButton.stories.tsx +197 -0
  79. package/src/components/FloatingActionButton.tsx +301 -0
  80. package/src/components/Grid.stories.tsx +16 -16
  81. package/src/components/Input.stories.tsx +214 -0
  82. package/src/components/Input.tsx +81 -4
  83. package/src/components/MobileHeader.stories.tsx +205 -0
  84. package/src/components/MobileHeader.tsx +233 -0
  85. package/src/components/MobileLayout.stories.tsx +338 -0
  86. package/src/components/MobileLayout.tsx +313 -0
  87. package/src/components/Modal.stories.tsx +388 -0
  88. package/src/components/Modal.tsx +122 -4
  89. package/src/components/PageHeader.stories.tsx +198 -0
  90. package/src/components/PageHeader.tsx +217 -0
  91. package/src/components/PullToRefresh.stories.tsx +321 -0
  92. package/src/components/PullToRefresh.tsx +294 -0
  93. package/src/components/QueryTransparency.tsx +1 -1
  94. package/src/components/SearchableList.stories.tsx +437 -0
  95. package/src/components/SearchableList.tsx +326 -0
  96. package/src/components/Select.stories.tsx +190 -0
  97. package/src/components/Select.tsx +353 -137
  98. package/src/components/Sidebar.tsx +193 -10
  99. package/src/components/SwipeActions.stories.tsx +327 -0
  100. package/src/components/SwipeActions.tsx +387 -0
  101. package/src/components/Switch.stories.tsx +158 -0
  102. package/src/components/Switch.tsx +12 -3
  103. package/src/components/Textarea.tsx +31 -1
  104. package/src/components/index.ts +69 -3
  105. package/src/context/MobileContext.tsx +296 -0
  106. package/src/hooks/useResponsive.ts +360 -0
  107. package/src/types/index.ts +4 -0
  108. package/tailwind.config.js +56 -1
@@ -0,0 +1,360 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+
3
+ /**
4
+ * Tailwind breakpoint values in pixels
5
+ */
6
+ export const BREAKPOINTS = {
7
+ xs: 0,
8
+ sm: 640,
9
+ md: 768,
10
+ lg: 1024,
11
+ xl: 1280,
12
+ '2xl': 1536,
13
+ } as const;
14
+
15
+ export type Breakpoint = keyof typeof BREAKPOINTS;
16
+
17
+ /**
18
+ * Viewport size state
19
+ */
20
+ export interface ViewportSize {
21
+ width: number;
22
+ height: number;
23
+ }
24
+
25
+ /**
26
+ * Orientation type
27
+ */
28
+ export type Orientation = 'portrait' | 'landscape';
29
+
30
+ /**
31
+ * SSR-safe check for window availability
32
+ */
33
+ const isBrowser = typeof window !== 'undefined';
34
+
35
+ /**
36
+ * Get initial viewport size (SSR-safe)
37
+ */
38
+ const getInitialViewportSize = (): ViewportSize => {
39
+ if (!isBrowser) {
40
+ return { width: 1024, height: 768 }; // Default to desktop for SSR
41
+ }
42
+ return {
43
+ width: window.innerWidth,
44
+ height: window.innerHeight,
45
+ };
46
+ };
47
+
48
+ /**
49
+ * useViewportSize - Returns current viewport dimensions
50
+ *
51
+ * Updates on window resize with debouncing for performance.
52
+ * SSR-safe with sensible defaults.
53
+ *
54
+ * @example
55
+ * const { width, height } = useViewportSize();
56
+ * console.log(`Viewport: ${width}x${height}`);
57
+ */
58
+ export function useViewportSize(): ViewportSize {
59
+ const [size, setSize] = useState<ViewportSize>(getInitialViewportSize);
60
+
61
+ useEffect(() => {
62
+ if (!isBrowser) return;
63
+
64
+ let timeoutId: ReturnType<typeof setTimeout>;
65
+
66
+ const handleResize = () => {
67
+ clearTimeout(timeoutId);
68
+ timeoutId = setTimeout(() => {
69
+ setSize({
70
+ width: window.innerWidth,
71
+ height: window.innerHeight,
72
+ });
73
+ }, 100); // Debounce 100ms
74
+ };
75
+
76
+ window.addEventListener('resize', handleResize);
77
+
78
+ // Set initial size on mount (in case SSR default differs)
79
+ handleResize();
80
+
81
+ return () => {
82
+ clearTimeout(timeoutId);
83
+ window.removeEventListener('resize', handleResize);
84
+ };
85
+ }, []);
86
+
87
+ return size;
88
+ }
89
+
90
+ /**
91
+ * useBreakpoint - Returns the current Tailwind breakpoint
92
+ *
93
+ * Automatically updates when viewport crosses breakpoint thresholds.
94
+ *
95
+ * @example
96
+ * const breakpoint = useBreakpoint();
97
+ * // Returns: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
98
+ */
99
+ export function useBreakpoint(): Breakpoint {
100
+ const { width } = useViewportSize();
101
+
102
+ if (width >= BREAKPOINTS['2xl']) return '2xl';
103
+ if (width >= BREAKPOINTS.xl) return 'xl';
104
+ if (width >= BREAKPOINTS.lg) return 'lg';
105
+ if (width >= BREAKPOINTS.md) return 'md';
106
+ if (width >= BREAKPOINTS.sm) return 'sm';
107
+ return 'xs';
108
+ }
109
+
110
+ /**
111
+ * useMediaQuery - React hook for CSS media queries
112
+ *
113
+ * SSR-safe implementation that returns false during SSR and
114
+ * updates reactively when media query match state changes.
115
+ *
116
+ * @param query - CSS media query string (e.g., '(max-width: 768px)')
117
+ * @returns boolean indicating if the media query matches
118
+ *
119
+ * @example
120
+ * const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
121
+ * const isReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
122
+ * const isPortrait = useMediaQuery('(orientation: portrait)');
123
+ */
124
+ export function useMediaQuery(query: string): boolean {
125
+ const [matches, setMatches] = useState(() => {
126
+ if (!isBrowser) return false;
127
+ return window.matchMedia(query).matches;
128
+ });
129
+
130
+ useEffect(() => {
131
+ if (!isBrowser) return;
132
+
133
+ const media = window.matchMedia(query);
134
+
135
+ if (media.matches !== matches) {
136
+ setMatches(media.matches);
137
+ }
138
+
139
+ const listener = (event: MediaQueryListEvent) => {
140
+ setMatches(event.matches);
141
+ };
142
+
143
+ media.addEventListener('change', listener);
144
+ return () => media.removeEventListener('change', listener);
145
+ }, [query, matches]);
146
+
147
+ return matches;
148
+ }
149
+
150
+ /**
151
+ * useIsMobile - Returns true when viewport is mobile-sized (< 768px)
152
+ *
153
+ * @example
154
+ * const isMobile = useIsMobile();
155
+ * return isMobile ? <MobileNav /> : <DesktopNav />;
156
+ */
157
+ export function useIsMobile(): boolean {
158
+ return useMediaQuery(`(max-width: ${BREAKPOINTS.md - 1}px)`);
159
+ }
160
+
161
+ /**
162
+ * useIsTablet - Returns true when viewport is tablet-sized (768px - 1023px)
163
+ *
164
+ * @example
165
+ * const isTablet = useIsTablet();
166
+ */
167
+ export function useIsTablet(): boolean {
168
+ return useMediaQuery(`(min-width: ${BREAKPOINTS.md}px) and (max-width: ${BREAKPOINTS.lg - 1}px)`);
169
+ }
170
+
171
+ /**
172
+ * useIsDesktop - Returns true when viewport is desktop-sized (>= 1024px)
173
+ *
174
+ * @example
175
+ * const isDesktop = useIsDesktop();
176
+ */
177
+ export function useIsDesktop(): boolean {
178
+ return useMediaQuery(`(min-width: ${BREAKPOINTS.lg}px)`);
179
+ }
180
+
181
+ /**
182
+ * useIsTouchDevice - Detects if the device supports touch input
183
+ *
184
+ * Uses multiple detection methods for reliability:
185
+ * - Touch event support
186
+ * - Pointer coarse media query
187
+ * - Max touch points
188
+ *
189
+ * @example
190
+ * const isTouchDevice = useIsTouchDevice();
191
+ * // Show swipe hints on touch devices
192
+ */
193
+ export function useIsTouchDevice(): boolean {
194
+ const [isTouch, setIsTouch] = useState(() => {
195
+ if (!isBrowser) return false;
196
+ return (
197
+ 'ontouchstart' in window ||
198
+ navigator.maxTouchPoints > 0 ||
199
+ window.matchMedia('(pointer: coarse)').matches
200
+ );
201
+ });
202
+
203
+ useEffect(() => {
204
+ if (!isBrowser) return;
205
+
206
+ // Re-check on mount for accuracy
207
+ const touchSupported =
208
+ 'ontouchstart' in window ||
209
+ navigator.maxTouchPoints > 0 ||
210
+ window.matchMedia('(pointer: coarse)').matches;
211
+
212
+ setIsTouch(touchSupported);
213
+ }, []);
214
+
215
+ return isTouch;
216
+ }
217
+
218
+ /**
219
+ * useOrientation - Returns current screen orientation
220
+ *
221
+ * @returns 'portrait' | 'landscape'
222
+ *
223
+ * @example
224
+ * const orientation = useOrientation();
225
+ * // Adjust layout based on orientation
226
+ */
227
+ export function useOrientation(): Orientation {
228
+ const { width, height } = useViewportSize();
229
+ return height > width ? 'portrait' : 'landscape';
230
+ }
231
+
232
+ /**
233
+ * useBreakpointValue - Returns different values based on breakpoint
234
+ *
235
+ * Mobile-first: Returns the value for the current breakpoint or the
236
+ * closest smaller breakpoint that has a value defined.
237
+ *
238
+ * @param values - Object mapping breakpoints to values
239
+ * @param defaultValue - Fallback value if no breakpoint matches
240
+ *
241
+ * @example
242
+ * const columns = useBreakpointValue({ xs: 1, sm: 2, lg: 4 }, 1);
243
+ * // Returns 1 on xs, 2 on sm/md, 4 on lg/xl/2xl
244
+ *
245
+ * const padding = useBreakpointValue({ xs: 'p-2', md: 'p-4', xl: 'p-8' });
246
+ */
247
+ export function useBreakpointValue<T>(
248
+ values: Partial<Record<Breakpoint, T>>,
249
+ defaultValue?: T
250
+ ): T | undefined {
251
+ const breakpoint = useBreakpoint();
252
+
253
+ // Breakpoints in order from largest to smallest
254
+ const breakpointOrder: Breakpoint[] = ['2xl', 'xl', 'lg', 'md', 'sm', 'xs'];
255
+
256
+ // Find the current breakpoint index
257
+ const currentIndex = breakpointOrder.indexOf(breakpoint);
258
+
259
+ // Look for value at current breakpoint or smaller (mobile-first)
260
+ for (let i = currentIndex; i < breakpointOrder.length; i++) {
261
+ const bp = breakpointOrder[i];
262
+ if (bp in values && values[bp] !== undefined) {
263
+ return values[bp];
264
+ }
265
+ }
266
+
267
+ return defaultValue;
268
+ }
269
+
270
+ /**
271
+ * useResponsiveCallback - Returns a memoized callback that receives responsive info
272
+ *
273
+ * Useful for callbacks that need to behave differently based on viewport.
274
+ *
275
+ * @example
276
+ * const handleClick = useResponsiveCallback((isMobile) => {
277
+ * if (isMobile) {
278
+ * openBottomSheet();
279
+ * } else {
280
+ * openModal();
281
+ * }
282
+ * });
283
+ */
284
+ export function useResponsiveCallback<T extends (...args: any[]) => any>(
285
+ callback: (isMobile: boolean, isTablet: boolean, isDesktop: boolean) => T
286
+ ): T {
287
+ const isMobile = useIsMobile();
288
+ const isTablet = useIsTablet();
289
+ const isDesktop = useIsDesktop();
290
+
291
+ return useCallback(
292
+ (...args: Parameters<T>) => callback(isMobile, isTablet, isDesktop)(...args),
293
+ [callback, isMobile, isTablet, isDesktop]
294
+ ) as T;
295
+ }
296
+
297
+ /**
298
+ * useSafeAreaInsets - Returns safe area insets for notched devices
299
+ *
300
+ * Uses CSS environment variables (env(safe-area-inset-*)) to get
301
+ * safe area dimensions for devices with notches or home indicators.
302
+ *
303
+ * @example
304
+ * const { top, bottom } = useSafeAreaInsets();
305
+ * // Add padding-bottom for home indicator
306
+ */
307
+ export function useSafeAreaInsets(): {
308
+ top: number;
309
+ right: number;
310
+ bottom: number;
311
+ left: number;
312
+ } {
313
+ const [insets, setInsets] = useState({
314
+ top: 0,
315
+ right: 0,
316
+ bottom: 0,
317
+ left: 0,
318
+ });
319
+
320
+ useEffect(() => {
321
+ if (!isBrowser) return;
322
+
323
+ // Create a temporary element to read CSS env() values
324
+ const el = document.createElement('div');
325
+ el.style.position = 'fixed';
326
+ el.style.top = 'env(safe-area-inset-top, 0px)';
327
+ el.style.right = 'env(safe-area-inset-right, 0px)';
328
+ el.style.bottom = 'env(safe-area-inset-bottom, 0px)';
329
+ el.style.left = 'env(safe-area-inset-left, 0px)';
330
+ el.style.visibility = 'hidden';
331
+ el.style.pointerEvents = 'none';
332
+ document.body.appendChild(el);
333
+
334
+ const computed = getComputedStyle(el);
335
+ setInsets({
336
+ top: parseInt(computed.top, 10) || 0,
337
+ right: parseInt(computed.right, 10) || 0,
338
+ bottom: parseInt(computed.bottom, 10) || 0,
339
+ left: parseInt(computed.left, 10) || 0,
340
+ });
341
+
342
+ document.body.removeChild(el);
343
+ }, []);
344
+
345
+ return insets;
346
+ }
347
+
348
+ /**
349
+ * usePrefersMobile - Checks if user prefers reduced data/animations (mobile-friendly)
350
+ *
351
+ * Combines multiple preferences that might indicate mobile/low-power usage.
352
+ */
353
+ export function usePrefersMobile(): boolean {
354
+ const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
355
+ const prefersReducedData = useMediaQuery('(prefers-reduced-data: reduce)');
356
+ const isTouchDevice = useIsTouchDevice();
357
+ const isMobile = useIsMobile();
358
+
359
+ return isMobile || isTouchDevice || prefersReducedMotion || prefersReducedData;
360
+ }
@@ -2,6 +2,8 @@
2
2
  * Core types for notebook-ui library
3
3
  */
4
4
 
5
+ import type { QueryTransparencyInfo } from '../components/QueryTransparency';
6
+
5
7
  /**
6
8
  * Base interface for all data items that can be displayed in tables
7
9
  */
@@ -22,6 +24,8 @@ export interface PaginationResponse<T = BaseDataItem> {
22
24
  // Query transparency fields (optional)
23
25
  executedQuery?: string;
24
26
  executionTimeMs?: number;
27
+ // Full query transparency info for QueryTransparency component
28
+ queryInfo?: QueryTransparencyInfo;
25
29
  }
26
30
 
27
31
  /**
@@ -175,6 +175,21 @@ export default {
175
175
  '33': '8.25rem',
176
176
  '34': '8.5rem',
177
177
  '35': '8.75rem',
178
+ // Touch target sizes (Apple HIG / Material Design)
179
+ 'touch-sm': '2.25rem', // 36px - minimum for secondary actions
180
+ 'touch': '2.75rem', // 44px - standard touch target (Apple HIG)
181
+ 'touch-lg': '3rem', // 48px - large touch target (Material Design)
182
+ },
183
+ // Minimum sizes for touch targets
184
+ minWidth: {
185
+ 'touch-sm': '2.25rem',
186
+ 'touch': '2.75rem',
187
+ 'touch-lg': '3rem',
188
+ },
189
+ minHeight: {
190
+ 'touch-sm': '2.25rem',
191
+ 'touch': '2.75rem',
192
+ 'touch-lg': '3rem',
178
193
  },
179
194
  borderRadius: {
180
195
  '4xl': '2rem',
@@ -196,11 +211,19 @@ export default {
196
211
  'fade-in-up': 'fadeInUp 0.5s ease-out',
197
212
  'slide-in': 'slideIn 0.3s ease-out',
198
213
  'slide-in-right': 'slideInRight 0.3s ease-out',
214
+ 'slide-in-left': 'slideInLeft 0.3s ease-out',
215
+ 'slide-in-bottom': 'slideInBottom 0.3s ease-out',
216
+ 'slide-in-top': 'slideInTop 0.3s ease-out',
217
+ 'slide-out-right': 'slideOutRight 0.3s ease-in',
218
+ 'slide-out-left': 'slideOutLeft 0.3s ease-in',
219
+ 'slide-out-bottom': 'slideOutBottom 0.3s ease-in',
220
+ 'slide-out-top': 'slideOutTop 0.3s ease-in',
199
221
  'bounce-subtle': 'bounceSubtle 2s infinite',
200
222
  'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
201
223
  'shimmer': 'shimmer 2s linear infinite',
202
224
  'float': 'float 6s ease-in-out infinite',
203
225
  'scale-in': 'scaleIn 0.2s ease-out',
226
+ 'scale-out': 'scaleOut 0.2s ease-in',
204
227
  },
205
228
  keyframes: {
206
229
  fadeIn: {
@@ -216,9 +239,37 @@ export default {
216
239
  '100%': { transform: 'translateX(0)', opacity: '1' },
217
240
  },
218
241
  slideInRight: {
219
- '0%': { transform: 'translateX(10px)', opacity: '0' },
242
+ '0%': { transform: 'translateX(100%)', opacity: '0' },
243
+ '100%': { transform: 'translateX(0)', opacity: '1' },
244
+ },
245
+ slideInLeft: {
246
+ '0%': { transform: 'translateX(-100%)', opacity: '0' },
220
247
  '100%': { transform: 'translateX(0)', opacity: '1' },
221
248
  },
249
+ slideInBottom: {
250
+ '0%': { transform: 'translateY(100%)', opacity: '0' },
251
+ '100%': { transform: 'translateY(0)', opacity: '1' },
252
+ },
253
+ slideInTop: {
254
+ '0%': { transform: 'translateY(-100%)', opacity: '0' },
255
+ '100%': { transform: 'translateY(0)', opacity: '1' },
256
+ },
257
+ slideOutRight: {
258
+ '0%': { transform: 'translateX(0)', opacity: '1' },
259
+ '100%': { transform: 'translateX(100%)', opacity: '0' },
260
+ },
261
+ slideOutLeft: {
262
+ '0%': { transform: 'translateX(0)', opacity: '1' },
263
+ '100%': { transform: 'translateX(-100%)', opacity: '0' },
264
+ },
265
+ slideOutBottom: {
266
+ '0%': { transform: 'translateY(0)', opacity: '1' },
267
+ '100%': { transform: 'translateY(100%)', opacity: '0' },
268
+ },
269
+ slideOutTop: {
270
+ '0%': { transform: 'translateY(0)', opacity: '1' },
271
+ '100%': { transform: 'translateY(-100%)', opacity: '0' },
272
+ },
222
273
  bounceSubtle: {
223
274
  '0%, 100%': { transform: 'translateY(0)' },
224
275
  '50%': { transform: 'translateY(-2px)' },
@@ -235,6 +286,10 @@ export default {
235
286
  '0%': { transform: 'scale(0.9)', opacity: '0' },
236
287
  '100%': { transform: 'scale(1)', opacity: '1' },
237
288
  },
289
+ scaleOut: {
290
+ '0%': { transform: 'scale(1)', opacity: '1' },
291
+ '100%': { transform: 'scale(0.9)', opacity: '0' },
292
+ },
238
293
  },
239
294
  backdropBlur: {
240
295
  'xs': '2px',