@cdx-ui/components 0.0.1-beta.112 → 0.0.1-beta.113

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 (57) hide show
  1. package/lib/commonjs/animation/hooks/index.js +33 -0
  2. package/lib/commonjs/animation/hooks/index.js.map +1 -1
  3. package/lib/commonjs/animation/hooks/usePopupDialogContentAnimation.js +173 -0
  4. package/lib/commonjs/animation/hooks/usePopupDialogContentAnimation.js.map +1 -0
  5. package/lib/commonjs/animation/hooks/usePopupOverlayAnimation.js +67 -0
  6. package/lib/commonjs/animation/hooks/usePopupOverlayAnimation.js.map +1 -0
  7. package/lib/commonjs/animation/hooks/usePopupRootAnimation.js +26 -0
  8. package/lib/commonjs/animation/hooks/usePopupRootAnimation.js.map +1 -0
  9. package/lib/commonjs/components/AlertDialog/index.js +15 -1
  10. package/lib/commonjs/components/AlertDialog/index.js.map +1 -1
  11. package/lib/commonjs/components/Dialog/animation.js +13 -0
  12. package/lib/commonjs/components/Dialog/animation.js.map +1 -0
  13. package/lib/commonjs/components/Dialog/index.js +267 -24
  14. package/lib/commonjs/components/Dialog/index.js.map +1 -1
  15. package/lib/commonjs/components/Dialog/styles.js +7 -3
  16. package/lib/commonjs/components/Dialog/styles.js.map +1 -1
  17. package/lib/module/animation/hooks/index.js +3 -0
  18. package/lib/module/animation/hooks/index.js.map +1 -1
  19. package/lib/module/animation/hooks/usePopupDialogContentAnimation.js +169 -0
  20. package/lib/module/animation/hooks/usePopupDialogContentAnimation.js.map +1 -0
  21. package/lib/module/animation/hooks/usePopupOverlayAnimation.js +63 -0
  22. package/lib/module/animation/hooks/usePopupOverlayAnimation.js.map +1 -0
  23. package/lib/module/animation/hooks/usePopupRootAnimation.js +22 -0
  24. package/lib/module/animation/hooks/usePopupRootAnimation.js.map +1 -0
  25. package/lib/module/components/AlertDialog/index.js +15 -1
  26. package/lib/module/components/AlertDialog/index.js.map +1 -1
  27. package/lib/module/components/Dialog/animation.js +7 -0
  28. package/lib/module/components/Dialog/animation.js.map +1 -0
  29. package/lib/module/components/Dialog/index.js +271 -30
  30. package/lib/module/components/Dialog/index.js.map +1 -1
  31. package/lib/module/components/Dialog/styles.js +6 -2
  32. package/lib/module/components/Dialog/styles.js.map +1 -1
  33. package/lib/typescript/animation/hooks/index.d.ts +3 -0
  34. package/lib/typescript/animation/hooks/index.d.ts.map +1 -1
  35. package/lib/typescript/animation/hooks/usePopupDialogContentAnimation.d.ts +47 -0
  36. package/lib/typescript/animation/hooks/usePopupDialogContentAnimation.d.ts.map +1 -0
  37. package/lib/typescript/animation/hooks/usePopupOverlayAnimation.d.ts +29 -0
  38. package/lib/typescript/animation/hooks/usePopupOverlayAnimation.d.ts.map +1 -0
  39. package/lib/typescript/animation/hooks/usePopupRootAnimation.d.ts +16 -0
  40. package/lib/typescript/animation/hooks/usePopupRootAnimation.d.ts.map +1 -0
  41. package/lib/typescript/components/AlertDialog/index.d.ts +5 -2
  42. package/lib/typescript/components/AlertDialog/index.d.ts.map +1 -1
  43. package/lib/typescript/components/Dialog/animation.d.ts +11 -0
  44. package/lib/typescript/components/Dialog/animation.d.ts.map +1 -0
  45. package/lib/typescript/components/Dialog/index.d.ts +6 -2
  46. package/lib/typescript/components/Dialog/index.d.ts.map +1 -1
  47. package/lib/typescript/components/Dialog/styles.d.ts +1 -0
  48. package/lib/typescript/components/Dialog/styles.d.ts.map +1 -1
  49. package/package.json +13 -5
  50. package/src/animation/hooks/index.ts +3 -0
  51. package/src/animation/hooks/usePopupDialogContentAnimation.ts +256 -0
  52. package/src/animation/hooks/usePopupOverlayAnimation.ts +84 -0
  53. package/src/animation/hooks/usePopupRootAnimation.ts +27 -0
  54. package/src/components/AlertDialog/index.tsx +16 -3
  55. package/src/components/Dialog/animation.ts +13 -0
  56. package/src/components/Dialog/index.tsx +331 -31
  57. package/src/components/Dialog/styles.ts +22 -1
@@ -0,0 +1,27 @@
1
+ import { useSharedValue } from 'react-native-reanimated';
2
+ import type { AnimationRootDisableAll } from '../types';
3
+ import { useCombinedAnimationDisabledState } from './useCombinedAnimationDisabledState';
4
+
5
+ export interface UsePopupRootAnimationOptions {
6
+ animation?: AnimationRootDisableAll;
7
+ }
8
+
9
+ /**
10
+ * Root animation hook for popup-like components (Dialog, Select, BottomSheet, Popover, etc.)
11
+ * Manages component state transitions and animation coordination via a shared
12
+ * `progress` value: 0 = idle/closed, 1 = fully open, 2 = dismissed.
13
+ */
14
+ export function usePopupRootAnimation(options: UsePopupRootAnimationOptions) {
15
+ const isAllAnimationsDisabled = useCombinedAnimationDisabledState(options.animation);
16
+
17
+ const progress = useSharedValue(0);
18
+ const isDragging = useSharedValue(false);
19
+ const isGestureReleaseAnimationRunning = useSharedValue(false);
20
+
21
+ return {
22
+ isAllAnimationsDisabled,
23
+ progress,
24
+ isDragging,
25
+ isGestureReleaseAnimationRunning,
26
+ };
27
+ }
@@ -3,7 +3,7 @@ import { View } from 'react-native';
3
3
  import { Slot, useDialog } from '@cdx-ui/primitives';
4
4
  import { composeEventHandlers } from '@cdx-ui/utils';
5
5
  import { Button, type ButtonProps } from '../Button';
6
- import { Dialog, type DialogRootProps } from '../Dialog';
6
+ import { Dialog, type DialogRootProps, type DialogPopupProps } from '../Dialog';
7
7
 
8
8
  // =============================================================================
9
9
  // ALERT DIALOG ROOT (Dialog with non-dismissible defaults + alertdialog role)
@@ -35,6 +35,19 @@ const AlertDialogRoot = forwardRef<View, AlertDialogRootProps>(
35
35
  );
36
36
  AlertDialogRoot.displayName = 'AlertDialog';
37
37
 
38
+ // =============================================================================
39
+ // ALERT DIALOG POPUP (swipe disabled by default)
40
+ // =============================================================================
41
+
42
+ export interface AlertDialogPopupProps extends DialogPopupProps {}
43
+
44
+ const AlertDialogPopup = forwardRef<View, AlertDialogPopupProps>(
45
+ ({ isSwipeable = false, ...props }, ref) => (
46
+ <Dialog.Popup ref={ref} isSwipeable={isSwipeable} {...props} />
47
+ ),
48
+ );
49
+ AlertDialogPopup.displayName = 'AlertDialog.Popup';
50
+
38
51
  // =============================================================================
39
52
  // ALERT DIALOG ACTION
40
53
  // =============================================================================
@@ -98,7 +111,7 @@ AlertDialogCancel.displayName = 'AlertDialog.Cancel';
98
111
 
99
112
  type AlertDialogCompoundComponent = typeof AlertDialogRoot & {
100
113
  Trigger: typeof Dialog.Trigger;
101
- Popup: typeof Dialog.Popup;
114
+ Popup: typeof AlertDialogPopup;
102
115
  Header: typeof Dialog.Header;
103
116
  Title: typeof Dialog.Title;
104
117
  Description: typeof Dialog.Description;
@@ -112,7 +125,7 @@ type AlertDialogCompoundComponent = typeof AlertDialogRoot & {
112
125
 
113
126
  export const AlertDialog = Object.assign(AlertDialogRoot, {
114
127
  Trigger: Dialog.Trigger,
115
- Popup: Dialog.Popup,
128
+ Popup: AlertDialogPopup,
116
129
  Header: Dialog.Header,
117
130
  Title: Dialog.Title,
118
131
  Description: Dialog.Description,
@@ -0,0 +1,13 @@
1
+ import type { SharedValue } from 'react-native-reanimated';
2
+ import { createContext } from '@cdx-ui/utils';
3
+
4
+ export interface DialogAnimationContextValue {
5
+ progress: SharedValue<number>;
6
+ isDragging: SharedValue<boolean>;
7
+ isGestureReleaseAnimationRunning: SharedValue<boolean>;
8
+ }
9
+
10
+ export const [DialogAnimationProvider, useDialogAnimation] =
11
+ createContext<DialogAnimationContextValue>('DialogAnimationContext', {
12
+ strict: false,
13
+ });
@@ -1,22 +1,54 @@
1
- import { forwardRef, useMemo, type ReactNode } from 'react';
2
- import { Text, View, type TextProps, type ViewProps } from 'react-native';
1
+ import {
2
+ forwardRef,
3
+ useEffect,
4
+ useLayoutEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ type ReactNode,
9
+ } from 'react';
10
+ import {
11
+ KeyboardAvoidingView,
12
+ Platform,
13
+ StyleSheet,
14
+ Text,
15
+ View,
16
+ type TextProps,
17
+ type ViewProps,
18
+ } from 'react-native';
19
+ import { GestureDetector } from 'react-native-gesture-handler';
20
+ import Animated, { useSharedValue } from 'react-native-reanimated';
3
21
  import { CheckCircle, Close as CloseIcon, EmergencyHome, Warning } from '@cdx-ui/icons';
4
22
  import {
5
23
  DialogClose as DialogClosePrimitive,
6
- DialogPopup as DialogPopupPrimitive,
24
+ DialogContent as DialogContentPrimitive,
25
+ DialogOverlay as DialogOverlayPrimitive,
26
+ DialogPortal as DialogPortalPrimitive,
7
27
  DialogRoot as DialogRootPrimitive,
8
28
  DialogTrigger as DialogTriggerPrimitive,
29
+ FullWindowOverlay,
9
30
  Slot,
10
31
  dataAttributes,
11
32
  mergeDataAttributes,
12
33
  useDialog,
13
34
  type IDialogCloseProps,
14
- type IDialogPopupProps,
35
+ type IDialogContentProps,
15
36
  type IDialogRootProps,
16
37
  type IDialogTriggerProps,
17
38
  } from '@cdx-ui/primitives';
18
- import { cn, ParentContext, useParentContext, useStyleContext } from '@cdx-ui/utils';
39
+ import { cn, mergeRefs, ParentContext, useParentContext, useStyleContext } from '@cdx-ui/utils';
40
+ import {
41
+ usePopupDialogContentAnimation,
42
+ usePopupOverlayAnimation,
43
+ usePopupRootAnimation,
44
+ type AnimationRootDisableAll,
45
+ } from '../../animation';
46
+ import {
47
+ AnimationSettingsProvider,
48
+ useAnimationSettings,
49
+ } from '../../providers/animation-settings';
19
50
  import { Icon } from '../Icon';
51
+ import { DialogAnimationProvider, useDialogAnimation } from './animation';
20
52
  import {
21
53
  type DialogTitleVariantProps,
22
54
  type DialogVariantProps,
@@ -28,6 +60,7 @@ import {
28
60
  dialogHeaderVariants,
29
61
  dialogOverlayVariants,
30
62
  dialogPopupVariants,
63
+ dialogPortalVariants,
31
64
  dialogTextVariants,
32
65
  dialogTitleIconVariants,
33
66
  dialogTitleVariants,
@@ -35,6 +68,11 @@ import {
35
68
 
36
69
  export { useDialog } from '@cdx-ui/primitives';
37
70
 
71
+ const IS_WEB = Platform.OS === 'web';
72
+ const WEB_TRANSITION_SAFETY_MS = 300;
73
+
74
+ type DialogDataState = 'entering' | 'open' | 'closed';
75
+
38
76
  // =============================================================================
39
77
  // STYLE CONTEXT
40
78
  // =============================================================================
@@ -43,6 +81,12 @@ const SCOPE = 'DIALOG';
43
81
 
44
82
  const useDialogStyleContext = () => useStyleContext(SCOPE) as DialogVariantProps;
45
83
 
84
+ // =============================================================================
85
+ // ANIMATED PRIMITIVES
86
+ // =============================================================================
87
+
88
+ const AnimatedOverlay = Animated.createAnimatedComponent(DialogOverlayPrimitive);
89
+
46
90
  // =============================================================================
47
91
  // STYLED ROOT
48
92
  // =============================================================================
@@ -50,28 +94,46 @@ const useDialogStyleContext = () => useStyleContext(SCOPE) as DialogVariantProps
50
94
  export interface DialogRootProps
51
95
  extends Omit<ViewProps, 'children' | 'role'>, IDialogRootProps, DialogVariantProps {
52
96
  className?: string;
97
+ animation?: AnimationRootDisableAll;
53
98
  }
54
99
 
55
100
  const DialogRoot = forwardRef<View, DialogRootProps>(
56
- ({ size = 'md', fullBleed = false, className, children, style, ...props }, ref) => {
101
+ ({ size = 'md', fullBleed = false, animation, className, children, style, ...props }, ref) => {
57
102
  const parent = useParentContext();
58
103
  const ctx = useMemo(
59
104
  () => ({ ...parent, [SCOPE]: { size, fullBleed } }),
60
105
  [parent, size, fullBleed],
61
106
  );
62
107
 
108
+ const { progress, isDragging, isGestureReleaseAnimationRunning, isAllAnimationsDisabled } =
109
+ usePopupRootAnimation({ animation });
110
+
111
+ const animationContextValue = useMemo(
112
+ () => ({ progress, isDragging, isGestureReleaseAnimationRunning }),
113
+ [progress, isDragging, isGestureReleaseAnimationRunning],
114
+ );
115
+
116
+ const animationSettingsContextValue = useMemo(
117
+ () => ({ isAllAnimationsDisabled }),
118
+ [isAllAnimationsDisabled],
119
+ );
120
+
63
121
  return (
64
- <ParentContext.Provider value={ctx}>
65
- <DialogRootPrimitive
66
- ref={ref}
67
- className={className}
68
- style={style}
69
- {...props}
70
- {...mergeDataAttributes(props, dataAttributes({ slot: 'dialog' }))}
71
- >
72
- {children}
73
- </DialogRootPrimitive>
74
- </ParentContext.Provider>
122
+ <AnimationSettingsProvider value={animationSettingsContextValue}>
123
+ <DialogAnimationProvider value={animationContextValue}>
124
+ <ParentContext.Provider value={ctx}>
125
+ <DialogRootPrimitive
126
+ ref={ref}
127
+ className={className}
128
+ style={style}
129
+ {...props}
130
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'dialog' }))}
131
+ >
132
+ {children}
133
+ </DialogRootPrimitive>
134
+ </ParentContext.Provider>
135
+ </DialogAnimationProvider>
136
+ </AnimationSettingsProvider>
75
137
  );
76
138
  },
77
139
  );
@@ -104,32 +166,255 @@ const DialogTrigger = forwardRef<View, DialogTriggerProps>(
104
166
  DialogTrigger.displayName = 'Dialog.Trigger';
105
167
 
106
168
  // =============================================================================
107
- // STYLED POPUP
169
+ // STYLED POPUP (portal + overlay + content composition)
108
170
  // =============================================================================
109
171
 
110
- export interface DialogPopupProps extends IDialogPopupProps, Pick<DialogVariantProps, 'size'> {
172
+ export interface DialogPopupProps
173
+ extends Omit<IDialogContentProps, 'asChild'>, Pick<DialogVariantProps, 'size'> {
111
174
  className?: string;
112
175
  children?: ReactNode;
176
+ disableFullWindowOverlay?: boolean;
177
+ isSwipeable?: boolean;
113
178
  }
114
179
 
115
180
  const DialogPopup = forwardRef<View, DialogPopupProps>(
116
- ({ size: sizeProp, className, children, style, ...props }, ref) => {
181
+ (
182
+ {
183
+ size: sizeProp,
184
+ className,
185
+ children,
186
+ style,
187
+ forceMount = false,
188
+ avoidKeyboard = true,
189
+ keyboardVerticalOffset = 0,
190
+ disableFullWindowOverlay = false,
191
+ isSwipeable = true,
192
+ ...props
193
+ },
194
+ ref,
195
+ ) => {
117
196
  const { size: contextSize } = useDialogStyleContext();
118
197
  const size = sizeProp ?? contextSize;
119
- const computedClassName = cn(dialogPopupVariants({ size }), className);
198
+ const { open, onOpenChange } = useDialog();
199
+ const parentContext = useParentContext();
200
+
201
+ const animationContext = useDialogAnimation();
202
+ const parentAnimationSettings = useAnimationSettings();
203
+
204
+ const fallbackProgress = useSharedValue(0);
205
+ const fallbackIsDragging = useSharedValue(false);
206
+ const fallbackIsGestureReleaseAnimationRunning = useSharedValue(false);
207
+
208
+ const progress = animationContext?.progress ?? fallbackProgress;
209
+ const isDragging = animationContext?.isDragging ?? fallbackIsDragging;
210
+ const isGestureReleaseAnimationRunning =
211
+ animationContext?.isGestureReleaseAnimationRunning ??
212
+ fallbackIsGestureReleaseAnimationRunning;
213
+
214
+ const {
215
+ rContainerStyle,
216
+ entering: overlayEntering,
217
+ exiting: overlayExiting,
218
+ } = usePopupOverlayAnimation({
219
+ progress,
220
+ isDragging,
221
+ isGestureReleaseAnimationRunning,
222
+ });
223
+
224
+ const {
225
+ contentY,
226
+ contentHeight,
227
+ panGesture,
228
+ rDragContainerStyle,
229
+ entering: contentEntering,
230
+ exiting: contentExiting,
231
+ } = usePopupDialogContentAnimation({
232
+ isOpen: open,
233
+ progress,
234
+ isDragging,
235
+ isGestureReleaseAnimationRunning,
236
+ onOpenChange,
237
+ isSwipeable,
238
+ });
239
+
240
+ const dragContainerRef = useRef<View>(null);
241
+
242
+ useLayoutEffect(() => {
243
+ if (!open || IS_WEB) return;
244
+ dragContainerRef.current?.measure((_x, _y, _width, height, _pageX, pageY) => {
245
+ contentY.set(pageY);
246
+ contentHeight.set(height);
247
+ });
248
+ }, [open, contentY, contentHeight]);
249
+
250
+ // ---- Web enter/exit lifecycle ----
251
+ // Mirrors the Toast pattern: mount → 'entering' → (rAF) → 'open' → (close)
252
+ // → 'closed' → (transitionend) → unmount.
253
+ //
254
+ // Critical: `isExiting` is derived synchronously (via ref tracking the previous
255
+ // `open` value) so the component stays mounted in the SAME render that sees
256
+ // `open` go false — if we used useEffect the component would return null before
257
+ // the effect fires.
258
+ //
259
+ // The transitionend listener targets the popup element (via popupRef) instead of
260
+ // the portal wrapper. CSS exit transitions live on the overlay and popup children,
261
+ // not the wrapper — listening on the wrapper would unmount on the first bubbled
262
+ // event, potentially cutting the other child's animation short.
263
+ const wrapperRef = useRef<View | null>(null);
264
+ const popupRef = useRef<View | null>(null);
265
+ const prevOpenRef = useRef(open);
266
+ const [isExiting, setIsExiting] = useState(false);
267
+ const [webState, setWebState] = useState<DialogDataState>(open ? 'entering' : 'closed');
268
+
269
+ // Detect open → false transition synchronously during render
270
+ if (IS_WEB && prevOpenRef.current && !open && !isExiting) {
271
+ setIsExiting(true);
272
+ setWebState('closed');
273
+ }
274
+ // Detect closed → open transition synchronously during render
275
+ if (IS_WEB && !prevOpenRef.current && open) {
276
+ setIsExiting(false);
277
+ setWebState('entering');
278
+ }
279
+ prevOpenRef.current = open;
280
+
281
+ // Entrance: commit 'entering' on first paint, then transition to 'open'
282
+ useEffect(() => {
283
+ if (!IS_WEB || !open) return;
284
+ const raf = requestAnimationFrame(() => {
285
+ setWebState('open');
286
+ });
287
+ return () => cancelAnimationFrame(raf);
288
+ }, [open]);
289
+
290
+ // Exit: wait for CSS transition to finish, then unmount.
291
+ // Listens on the popup element directly — its exit transitions (opacity +
292
+ // transform) are at least as long as the overlay's, so when the popup
293
+ // finishes both children are done animating.
294
+ useEffect(() => {
295
+ if (!IS_WEB || !isExiting) return;
296
+
297
+ const node = popupRef.current as unknown as HTMLElement | null;
298
+ let done = false;
299
+
300
+ const finish = () => {
301
+ if (done) return;
302
+ done = true;
303
+ setIsExiting(false);
304
+ };
305
+
306
+ const safetyTimer = setTimeout(finish, WEB_TRANSITION_SAFETY_MS);
307
+
308
+ if (!node || typeof node.addEventListener !== 'function') {
309
+ return () => {
310
+ clearTimeout(safetyTimer);
311
+ };
312
+ }
313
+
314
+ const handler = (event: Event) => {
315
+ if (event.target !== node) return;
316
+ const propertyName = (event as unknown as { propertyName?: string }).propertyName ?? '';
317
+ if (propertyName === 'opacity' || propertyName === 'transform') {
318
+ finish();
319
+ }
320
+ };
321
+ node.addEventListener('transitionend', handler);
322
+
323
+ return () => {
324
+ clearTimeout(safetyTimer);
325
+ node.removeEventListener('transitionend', handler);
326
+ };
327
+ }, [isExiting]);
328
+
329
+ const dataState: DialogDataState = webState;
330
+ const shouldMount = IS_WEB ? open || isExiting : open;
331
+
332
+ if (!forceMount && !shouldMount) {
333
+ return null;
334
+ }
335
+
336
+ const portalClassName = cn(dialogPortalVariants());
120
337
  const overlayClassName = cn(dialogOverlayVariants());
338
+ const popupClassName = cn(dialogPopupVariants({ size }), className);
339
+
340
+ const keyboardBehavior = avoidKeyboard
341
+ ? Platform.select<'padding' | 'height'>({ ios: 'padding', default: 'height' })
342
+ : undefined;
121
343
 
122
344
  return (
123
- <DialogPopupPrimitive
124
- ref={ref}
125
- className={computedClassName}
126
- style={style}
127
- overlayClassName={overlayClassName}
128
- {...props}
129
- {...mergeDataAttributes(props, dataAttributes({ slot: 'dialog-popup' }))}
130
- >
131
- {children}
132
- </DialogPopupPrimitive>
345
+ <DialogPortalPrimitive forceMount>
346
+ <ParentContext.Provider value={parentContext}>
347
+ <AnimationSettingsProvider value={parentAnimationSettings}>
348
+ <DialogAnimationProvider value={animationContext}>
349
+ <FullWindowOverlay
350
+ disableFullWindowOverlay={disableFullWindowOverlay}
351
+ unstable_accessibilityContainerViewIsModal
352
+ >
353
+ <Animated.View
354
+ ref={wrapperRef}
355
+ className={portalClassName}
356
+ pointerEvents="box-none"
357
+ {...(IS_WEB ? dataAttributes({ state: dataState }) : undefined)}
358
+ >
359
+ {/* Overlay */}
360
+ <Animated.View
361
+ entering={overlayEntering}
362
+ exiting={overlayExiting}
363
+ style={StyleSheet.absoluteFill}
364
+ >
365
+ <AnimatedOverlay
366
+ forceMount
367
+ className={overlayClassName}
368
+ style={IS_WEB ? undefined : rContainerStyle}
369
+ {...(IS_WEB ? dataAttributes({ state: dataState }) : undefined)}
370
+ />
371
+ </Animated.View>
372
+
373
+ {/* Content with gesture + animation */}
374
+ <GestureDetector gesture={panGesture}>
375
+ <Animated.View
376
+ ref={dragContainerRef}
377
+ style={internalStyles.fill}
378
+ pointerEvents="box-none"
379
+ entering={contentEntering}
380
+ exiting={contentExiting}
381
+ >
382
+ <Animated.View
383
+ style={
384
+ IS_WEB ? internalStyles.fill : [internalStyles.fill, rDragContainerStyle]
385
+ }
386
+ pointerEvents="box-none"
387
+ >
388
+ <KeyboardAvoidingView
389
+ style={internalStyles.center}
390
+ pointerEvents="box-none"
391
+ behavior={keyboardBehavior}
392
+ keyboardVerticalOffset={keyboardVerticalOffset}
393
+ >
394
+ <DialogContentPrimitive
395
+ ref={mergeRefs(ref, popupRef)}
396
+ forceMount
397
+ className={popupClassName}
398
+ style={style}
399
+ {...props}
400
+ {...mergeDataAttributes(
401
+ props,
402
+ dataAttributes({ slot: 'dialog-popup' }),
403
+ IS_WEB ? dataAttributes({ state: dataState }) : undefined,
404
+ )}
405
+ >
406
+ {children}
407
+ </DialogContentPrimitive>
408
+ </KeyboardAvoidingView>
409
+ </Animated.View>
410
+ </Animated.View>
411
+ </GestureDetector>
412
+ </Animated.View>
413
+ </FullWindowOverlay>
414
+ </DialogAnimationProvider>
415
+ </AnimationSettingsProvider>
416
+ </ParentContext.Provider>
417
+ </DialogPortalPrimitive>
133
418
  );
134
419
  },
135
420
  );
@@ -408,3 +693,18 @@ export const Dialog = Object.assign(DialogRoot, {
408
693
  Footer: DialogFooter,
409
694
  Close: DialogClose,
410
695
  }) as DialogCompoundComponent;
696
+
697
+ // =============================================================================
698
+ // INTERNAL STYLES
699
+ // =============================================================================
700
+
701
+ const internalStyles = StyleSheet.create({
702
+ fill: {
703
+ flex: 1,
704
+ },
705
+ center: {
706
+ flex: 1,
707
+ alignItems: 'center',
708
+ justifyContent: 'center',
709
+ },
710
+ });
@@ -2,9 +2,25 @@ import { Platform } from 'react-native';
2
2
  import { cva, type VariantProps } from 'class-variance-authority';
3
3
  import { TRANSITION_COLORS } from '../../styles/constants';
4
4
 
5
+ // ── Portal container ────────────────────────────────────────
6
+
7
+ export const dialogPortalVariants = cva([
8
+ 'flex-1',
9
+ 'absolute inset-0',
10
+ 'web:fixed web:inset-0 web:z-50',
11
+ ]);
12
+
5
13
  // ── Overlay ─────────────────────────────────────────────────
6
14
 
7
- export const dialogOverlayVariants = cva(['absolute inset-0', 'bg-surface-backdrop']);
15
+ export const dialogOverlayVariants = cva([
16
+ 'absolute inset-0',
17
+ 'bg-surface-backdrop',
18
+ 'web:transition-opacity web:duration-200 web:ease-out',
19
+ 'web:data-[state=entering]:opacity-0',
20
+ 'web:data-[state=open]:opacity-100',
21
+ 'web:data-[state=closed]:opacity-0 web:data-[state=closed]:duration-150',
22
+ 'web:motion-reduce:transition-none web:motion-reduce:duration-0',
23
+ ]);
8
24
 
9
25
  // ── Popup (renamed from dialogContentVariants) ──────────────
10
26
 
@@ -14,6 +30,11 @@ export const dialogPopupVariants = cva(
14
30
  'rounded-lg',
15
31
  Platform.select({ web: 'shadow-lg', default: '' }),
16
32
  'max-h-[85vh] overflow-hidden',
33
+ 'web:transition-[opacity,transform] web:duration-200 web:ease-out',
34
+ 'web:data-[state=entering]:opacity-0 web:data-[state=entering]:scale-[0.96]',
35
+ 'web:data-[state=open]:opacity-100 web:data-[state=open]:scale-100',
36
+ 'web:data-[state=closed]:opacity-0 web:data-[state=closed]:scale-[0.96] web:data-[state=closed]:duration-150',
37
+ 'web:motion-reduce:transition-none web:motion-reduce:duration-0',
17
38
  ],
18
39
  {
19
40
  variants: {