@lerx/promise-modal 0.2.7 → 0.2.9

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.
@@ -3,5 +3,217 @@ interface ModalAnimationHandler {
3
3
  onVisible?: Fn;
4
4
  onHidden?: Fn;
5
5
  }
6
+ /**
7
+ * Hook that triggers animation callbacks based on modal visibility changes.
8
+ *
9
+ * Uses requestAnimationFrame to ensure callbacks are executed at the optimal time
10
+ * for animations. Callbacks are triggered on the next frame after visibility changes,
11
+ * allowing for smooth CSS transitions and JavaScript animations.
12
+ *
13
+ * @param visible - Current visibility state of the modal
14
+ * @param handler - Object containing onVisible and onHidden callbacks
15
+ *
16
+ * @example
17
+ * Basic fade animation:
18
+ * ```tsx
19
+ * function FadeModal({ visible }) {
20
+ * const modalRef = useRef<HTMLDivElement>(null);
21
+ *
22
+ * useModalAnimation(visible, {
23
+ * onVisible: () => {
24
+ * if (modalRef.current) {
25
+ * modalRef.current.style.opacity = '0';
26
+ * modalRef.current.style.transform = 'scale(0.9)';
27
+ *
28
+ * requestAnimationFrame(() => {
29
+ * modalRef.current.style.opacity = '1';
30
+ * modalRef.current.style.transform = 'scale(1)';
31
+ * });
32
+ * }
33
+ * },
34
+ * onHidden: () => {
35
+ * if (modalRef.current) {
36
+ * modalRef.current.style.opacity = '0';
37
+ * modalRef.current.style.transform = 'scale(0.9)';
38
+ * }
39
+ * },
40
+ * });
41
+ *
42
+ * return (
43
+ * <div
44
+ * ref={modalRef}
45
+ * style={{
46
+ * transition: 'all 300ms ease',
47
+ * opacity: 0,
48
+ * }}
49
+ * >
50
+ * Modal Content
51
+ * </div>
52
+ * );
53
+ * }
54
+ * ```
55
+ *
56
+ * @example
57
+ * Slide animation with dynamic direction:
58
+ * ```tsx
59
+ * function SlideModal({ visible, direction = 'bottom' }) {
60
+ * const modalRef = useRef<HTMLDivElement>(null);
61
+ *
62
+ * const getTransform = (hidden: boolean) => {
63
+ * if (!hidden) return 'translate(0, 0)';
64
+ *
65
+ * switch (direction) {
66
+ * case 'top': return 'translate(0, -100%)';
67
+ * case 'bottom': return 'translate(0, 100%)';
68
+ * case 'left': return 'translate(-100%, 0)';
69
+ * case 'right': return 'translate(100%, 0)';
70
+ * }
71
+ * };
72
+ *
73
+ * useModalAnimation(visible, {
74
+ * onVisible: () => {
75
+ * if (modalRef.current) {
76
+ * modalRef.current.style.transform = getTransform(false);
77
+ * }
78
+ * },
79
+ * onHidden: () => {
80
+ * if (modalRef.current) {
81
+ * modalRef.current.style.transform = getTransform(true);
82
+ * }
83
+ * },
84
+ * });
85
+ *
86
+ * return (
87
+ * <div
88
+ * ref={modalRef}
89
+ * style={{
90
+ * transition: 'transform 400ms cubic-bezier(0.4, 0, 0.2, 1)',
91
+ * transform: getTransform(true),
92
+ * }}
93
+ * >
94
+ * // Content
95
+ * </div>
96
+ * );
97
+ * }
98
+ * ```
99
+ *
100
+ * @example
101
+ * Complex animation sequence:
102
+ * ```tsx
103
+ * function SequenceModal({ visible }) {
104
+ * const overlayRef = useRef<HTMLDivElement>(null);
105
+ * const contentRef = useRef<HTMLDivElement>(null);
106
+ *
107
+ * useModalAnimation(visible, {
108
+ * onVisible: () => {
109
+ * // Animate overlay first
110
+ * if (overlayRef.current) {
111
+ * overlayRef.current.style.opacity = '0';
112
+ * requestAnimationFrame(() => {
113
+ * overlayRef.current.style.opacity = '1';
114
+ * });
115
+ * }
116
+ *
117
+ * // Then animate content with delay
118
+ * setTimeout(() => {
119
+ * if (contentRef.current) {
120
+ * contentRef.current.style.transform = 'scale(0.8)';
121
+ * contentRef.current.style.opacity = '0';
122
+ *
123
+ * requestAnimationFrame(() => {
124
+ * contentRef.current.style.transform = 'scale(1)';
125
+ * contentRef.current.style.opacity = '1';
126
+ * });
127
+ * }
128
+ * }, 150);
129
+ * },
130
+ * onHidden: () => {
131
+ * // Reverse animation
132
+ * if (contentRef.current) {
133
+ * contentRef.current.style.transform = 'scale(0.8)';
134
+ * contentRef.current.style.opacity = '0';
135
+ * }
136
+ *
137
+ * setTimeout(() => {
138
+ * if (overlayRef.current) {
139
+ * overlayRef.current.style.opacity = '0';
140
+ * }
141
+ * }, 150);
142
+ * },
143
+ * });
144
+ *
145
+ * return (
146
+ * <>
147
+ * <div
148
+ * ref={overlayRef}
149
+ * className="modal-overlay"
150
+ * style={{ transition: 'opacity 300ms' }}
151
+ * />
152
+ * <div
153
+ * ref={contentRef}
154
+ * className="modal-content"
155
+ * style={{ transition: 'all 300ms ease' }}
156
+ * >
157
+ * // Content
158
+ * </div>
159
+ * </>
160
+ * );
161
+ * }
162
+ * ```
163
+ *
164
+ * @example
165
+ * With class-based animations:
166
+ * ```tsx
167
+ * function ClassAnimatedModal({ visible }) {
168
+ * const [animationClass, setAnimationClass] = useState('');
169
+ *
170
+ * useModalAnimation(visible, {
171
+ * onVisible: () => setAnimationClass('modal-enter'),
172
+ * onHidden: () => setAnimationClass('modal-exit'),
173
+ * });
174
+ *
175
+ * return (
176
+ * <div className={`modal ${animationClass}`}>
177
+ * // Content
178
+ * </div>
179
+ * );
180
+ * }
181
+ * ```
182
+ *
183
+ * @example
184
+ * Integrating with animation libraries:
185
+ * ```tsx
186
+ * function SpringModal({ visible }) {
187
+ * const springRef = useRef<SpringRef>(null);
188
+ *
189
+ * useModalAnimation(visible, {
190
+ * onVisible: () => {
191
+ * springRef.current?.start({
192
+ * from: { opacity: 0, scale: 0.8 },
193
+ * to: { opacity: 1, scale: 1 },
194
+ * });
195
+ * },
196
+ * onHidden: () => {
197
+ * springRef.current?.start({
198
+ * to: { opacity: 0, scale: 0.8 },
199
+ * });
200
+ * },
201
+ * });
202
+ *
203
+ * return (
204
+ * <animated.div ref={springRef}>
205
+ * // Content
206
+ * </animated.div>
207
+ * );
208
+ * }
209
+ * ```
210
+ *
211
+ * @remarks
212
+ * - Uses useLayoutEffect to ensure DOM updates before animations
213
+ * - Callbacks are wrapped in requestAnimationFrame for optimal timing
214
+ * - Handler reference is kept fresh without causing re-renders
215
+ * - Automatically cancels pending animation frames on cleanup
216
+ * - Perfect for coordinating CSS transitions and JS animations
217
+ */
6
218
  export declare const useModalAnimation: (visible: boolean, handler: ModalAnimationHandler) => void;
7
219
  export {};
@@ -1,2 +1,116 @@
1
1
  import type { ModalNode } from '../core';
2
+ /**
3
+ * Hook that subscribes to modal state changes and triggers re-renders.
4
+ *
5
+ * Listens to any changes in the modal node (visibility, alive state, content, etc.)
6
+ * and returns a version number that increments on each change. This allows components
7
+ * to react to modal state changes without directly accessing the modal state.
8
+ *
9
+ * @param modal - The modal node to subscribe to
10
+ * @returns Version number that increments on each modal state change
11
+ *
12
+ * @example
13
+ * Basic usage to react to modal changes:
14
+ * ```tsx
15
+ * function ModalContent({ modalId }) {
16
+ * const { modal } = useModal(modalId);
17
+ * const version = useSubscribeModal(modal);
18
+ *
19
+ * // Component re-renders whenever modal state changes
20
+ * return (
21
+ * <div>
22
+ * <p>Modal is {modal?.visible ? 'visible' : 'hidden'}</p>
23
+ * <p>Update count: {version}</p>
24
+ * </div>
25
+ * );
26
+ * }
27
+ * ```
28
+ *
29
+ * @example
30
+ * Triggering side effects on modal state changes:
31
+ * ```tsx
32
+ * function ModalLogger({ modalId }) {
33
+ * const { modal } = useModal(modalId);
34
+ * const version = useSubscribeModal(modal);
35
+ *
36
+ * useEffect(() => {
37
+ * if (version === 0) return; // Skip initial render
38
+ *
39
+ * console.log('Modal state changed:', {
40
+ * visible: modal?.visible,
41
+ * alive: modal?.alive,
42
+ * type: modal?.type,
43
+ * });
44
+ * }, [version, modal]);
45
+ *
46
+ * return null;
47
+ * }
48
+ * ```
49
+ *
50
+ * @example
51
+ * Conditional rendering based on modal state:
52
+ * ```tsx
53
+ * function ModalAnimation({ modalId, children }) {
54
+ * const { modal } = useModal(modalId);
55
+ * const version = useSubscribeModal(modal);
56
+ * const [shouldRender, setShouldRender] = useState(false);
57
+ *
58
+ * useEffect(() => {
59
+ * if (modal?.visible) {
60
+ * setShouldRender(true);
61
+ * } else if (!modal?.alive) {
62
+ * // Delay unmount for exit animation
63
+ * setTimeout(() => setShouldRender(false), 300);
64
+ * }
65
+ * }, [version, modal]);
66
+ *
67
+ * if (!shouldRender) return null;
68
+ *
69
+ * return (
70
+ * <div className={modal?.visible ? 'fade-in' : 'fade-out'}>
71
+ * {children}
72
+ * </div>
73
+ * );
74
+ * }
75
+ * ```
76
+ *
77
+ * @example
78
+ * Tracking specific modal properties:
79
+ * ```tsx
80
+ * function ModalProgressTracker({ modalId }) {
81
+ * const { modal } = useModal(modalId);
82
+ * const version = useSubscribeModal(modal);
83
+ * const [history, setHistory] = useState([]);
84
+ *
85
+ * useEffect(() => {
86
+ * if (!modal) return;
87
+ *
88
+ * setHistory(prev => [...prev, {
89
+ * timestamp: Date.now(),
90
+ * visible: modal.visible,
91
+ * value: modal.value,
92
+ * }]);
93
+ * }, [version]); // Only depend on version, not modal
94
+ *
95
+ * return (
96
+ * <div>
97
+ * <h4>Modal State History</h4>
98
+ * {history.map((entry, i) => (
99
+ * <div key={i}>
100
+ * {new Date(entry.timestamp).toLocaleTimeString()}:
101
+ * {entry.visible ? 'Shown' : 'Hidden'}
102
+ * (value: {JSON.stringify(entry.value)})
103
+ * </div>
104
+ * ))}
105
+ * </div>
106
+ * );
107
+ * }
108
+ * ```
109
+ *
110
+ * @remarks
111
+ * - Returns 0 on initial render, increments on each change
112
+ * - Automatically unsubscribes when component unmounts or modal changes
113
+ * - More efficient than directly depending on modal object in useEffect
114
+ * - Use this when you need to react to any modal state change
115
+ */
2
116
  export declare const useSubscribeModal: (modal?: ModalNode) => number;
package/dist/index.cjs CHANGED
@@ -426,7 +426,7 @@ const DEFAULT_OPTIONS = {
426
426
  manualDestroy: false,
427
427
  };
428
428
  const ConfigurationContextProvider = react.memo(({ ForegroundComponent, BackgroundComponent, TitleComponent, SubtitleComponent, ContentComponent, FooterComponent, options: inputOptions, children, }) => {
429
- const memoized = hook.useMemorize({
429
+ const constant = hook.useConstant({
430
430
  BackgroundComponent,
431
431
  ForegroundComponent: ForegroundComponent || FallbackForegroundFrame,
432
432
  TitleComponent: TitleComponent || FallbackTitle,
@@ -436,17 +436,17 @@ const ConfigurationContextProvider = react.memo(({ ForegroundComponent, Backgrou
436
436
  });
437
437
  const options = hook.useSnapshot(inputOptions);
438
438
  const value = react.useMemo(() => ({
439
- ForegroundComponent: memoized.ForegroundComponent,
440
- BackgroundComponent: memoized.BackgroundComponent,
441
- TitleComponent: memoized.TitleComponent,
442
- SubtitleComponent: memoized.SubtitleComponent,
443
- ContentComponent: memoized.ContentComponent,
444
- FooterComponent: memoized.FooterComponent,
439
+ ForegroundComponent: constant.ForegroundComponent,
440
+ BackgroundComponent: constant.BackgroundComponent,
441
+ TitleComponent: constant.TitleComponent,
442
+ SubtitleComponent: constant.SubtitleComponent,
443
+ ContentComponent: constant.ContentComponent,
444
+ FooterComponent: constant.FooterComponent,
445
445
  options: {
446
446
  ...DEFAULT_OPTIONS,
447
447
  ...options,
448
448
  },
449
- }), [memoized, options]);
449
+ }), [constant, options]);
450
450
  return (jsxRuntime.jsx(ConfigurationContext.Provider, { value: value, children: children }));
451
451
  });
452
452
 
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { useConfigurationOptions as useModalOptions, useConfigurationDuration as useModalDuration, useConfigurationBackdrop as useModalBackdrop, } from './providers';
2
2
  export { useBootstrap as useInitializeModal, BootstrapProvider as ModalProvider, type BootstrapProviderHandle as ModalProviderHandle, type BootstrapProviderProps as ModalProviderProps, } from './bootstrap';
3
- export { useSubscribeModal } from './hooks/useSubscribeModal';
4
- export { useDestroyAfter } from './hooks/useDestroyAfter';
5
3
  export { useActiveModalCount } from './hooks/useActiveModalCount';
4
+ export { useDestroyAfter } from './hooks/useDestroyAfter';
6
5
  export { useModalAnimation } from './hooks/useModalAnimation';
6
+ export { useSubscribeModal } from './hooks/useSubscribeModal';
7
7
  export { alert, confirm, prompt } from './core';
8
- export type { ModalFrameProps, FooterComponentProps, ModalBackground, PromptInputProps, AlertContentProps, ConfirmContentProps, PromptContentProps, WrapperComponentProps, } from './types';
8
+ export type { ModalOptions, ModalFrameProps, FooterComponentProps, ModalBackground, PromptInputProps, AlertContentProps, ConfirmContentProps, PromptContentProps, WrapperComponentProps, } from './types';
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
2
  import { createContext, useContext, useMemo, forwardRef, memo, useRef, useState, useLayoutEffect, useCallback, Fragment, useEffect, useImperativeHandle } from 'react';
3
3
  import { convertMsFromDuration } from '@winglet/common-utils/convert';
4
- import { useMemorize, useSnapshot, useReference, useOnMountLayout, useHandle, useVersion, useOnMount } from '@winglet/react-utils/hook';
4
+ import { useConstant, useSnapshot, useReference, useOnMountLayout, useHandle, useVersion, useOnMount } from '@winglet/react-utils/hook';
5
5
  import { polynomialHash } from '@winglet/common-utils/hash';
6
6
  import { getRandomString, counterFactory } from '@winglet/common-utils/lib';
7
7
  import { styleManagerFactory, destroyScope } from '@winglet/style-utils/style-manager';
@@ -424,7 +424,7 @@ const DEFAULT_OPTIONS = {
424
424
  manualDestroy: false,
425
425
  };
426
426
  const ConfigurationContextProvider = memo(({ ForegroundComponent, BackgroundComponent, TitleComponent, SubtitleComponent, ContentComponent, FooterComponent, options: inputOptions, children, }) => {
427
- const memoized = useMemorize({
427
+ const constant = useConstant({
428
428
  BackgroundComponent,
429
429
  ForegroundComponent: ForegroundComponent || FallbackForegroundFrame,
430
430
  TitleComponent: TitleComponent || FallbackTitle,
@@ -434,17 +434,17 @@ const ConfigurationContextProvider = memo(({ ForegroundComponent, BackgroundComp
434
434
  });
435
435
  const options = useSnapshot(inputOptions);
436
436
  const value = useMemo(() => ({
437
- ForegroundComponent: memoized.ForegroundComponent,
438
- BackgroundComponent: memoized.BackgroundComponent,
439
- TitleComponent: memoized.TitleComponent,
440
- SubtitleComponent: memoized.SubtitleComponent,
441
- ContentComponent: memoized.ContentComponent,
442
- FooterComponent: memoized.FooterComponent,
437
+ ForegroundComponent: constant.ForegroundComponent,
438
+ BackgroundComponent: constant.BackgroundComponent,
439
+ TitleComponent: constant.TitleComponent,
440
+ SubtitleComponent: constant.SubtitleComponent,
441
+ ContentComponent: constant.ContentComponent,
442
+ FooterComponent: constant.FooterComponent,
443
443
  options: {
444
444
  ...DEFAULT_OPTIONS,
445
445
  ...options,
446
446
  },
447
- }), [memoized, options]);
447
+ }), [constant, options]);
448
448
  return (jsx(ConfigurationContext.Provider, { value: value, children: children }));
449
449
  });
450
450
 
@@ -1,6 +1,5 @@
1
1
  import { type ComponentType } from 'react';
2
- import type { Color, Duration } from '../../@aileron/declare';
3
- import type { BackgroundComponent, FooterComponentProps, ForegroundComponent, WrapperComponentProps } from '../../types';
2
+ import type { BackgroundComponent, FooterComponentProps, ForegroundComponent, ModalOptions, WrapperComponentProps } from '../../types';
4
3
  export interface ConfigurationContextProps {
5
4
  ForegroundComponent: ForegroundComponent;
6
5
  BackgroundComponent?: BackgroundComponent;
@@ -8,11 +7,6 @@ export interface ConfigurationContextProps {
8
7
  SubtitleComponent: ComponentType<WrapperComponentProps>;
9
8
  ContentComponent: ComponentType<WrapperComponentProps>;
10
9
  FooterComponent: ComponentType<FooterComponentProps>;
11
- options: {
12
- duration: Duration;
13
- backdrop: Color;
14
- manualDestroy: boolean;
15
- closeOnBackdropClick: boolean;
16
- };
10
+ options: Required<ModalOptions>;
17
11
  }
18
12
  export declare const ConfigurationContext: import("react").Context<ConfigurationContextProps>;
@@ -1,6 +1,5 @@
1
1
  import { type ComponentType, type PropsWithChildren } from 'react';
2
- import type { Color, Duration } from '../../@aileron/declare';
3
- import type { FooterComponentProps, ModalFrameProps, WrapperComponentProps } from '../../types';
2
+ import type { FooterComponentProps, ModalFrameProps, ModalOptions, WrapperComponentProps } from '../../types';
4
3
  export interface ConfigurationContextProviderProps {
5
4
  BackgroundComponent?: ComponentType<ModalFrameProps>;
6
5
  ForegroundComponent?: ComponentType<ModalFrameProps>;
@@ -8,15 +7,6 @@ export interface ConfigurationContextProviderProps {
8
7
  SubtitleComponent?: ComponentType<WrapperComponentProps>;
9
8
  ContentComponent?: ComponentType<WrapperComponentProps>;
10
9
  FooterComponent?: ComponentType<FooterComponentProps>;
11
- options?: {
12
- /** Modal transition time(ms, s) */
13
- duration?: Duration;
14
- /** Modal backdrop color */
15
- backdrop?: Color;
16
- /** Whether to destroy the modal manually */
17
- manualDestroy?: boolean;
18
- /** Whether to close the modal when the backdrop is clicked */
19
- closeOnBackdropClick?: boolean;
20
- };
10
+ options?: ModalOptions;
21
11
  }
22
12
  export declare const ConfigurationContextProvider: import("react").MemoExoticComponent<({ ForegroundComponent, BackgroundComponent, TitleComponent, SubtitleComponent, ContentComponent, FooterComponent, options: inputOptions, children, }: PropsWithChildren<ConfigurationContextProviderProps>) => import("react/jsx-runtime").JSX.Element>;