@oxyhq/bloom 0.12.1 → 0.14.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/lib/commonjs/fab/Fab.js +25 -6
- package/lib/commonjs/fab/Fab.js.map +1 -1
- package/lib/commonjs/fab/Fab.web.js +26 -7
- package/lib/commonjs/fab/Fab.web.js.map +1 -1
- package/lib/commonjs/index.js +15 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +21 -6
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/responsive-sheet/ResponsiveSheet.js +300 -0
- package/lib/commonjs/responsive-sheet/ResponsiveSheet.js.map +1 -0
- package/lib/commonjs/responsive-sheet/ResponsiveSheet.native.js +226 -0
- package/lib/commonjs/responsive-sheet/ResponsiveSheet.native.js.map +1 -0
- package/lib/commonjs/responsive-sheet/const.js +60 -0
- package/lib/commonjs/responsive-sheet/const.js.map +1 -0
- package/lib/commonjs/responsive-sheet/index.js +19 -0
- package/lib/commonjs/responsive-sheet/index.js.map +1 -0
- package/lib/commonjs/responsive-sheet/index.web.js +19 -0
- package/lib/commonjs/responsive-sheet/index.web.js.map +1 -0
- package/lib/commonjs/responsive-sheet/types.js +6 -0
- package/lib/commonjs/responsive-sheet/types.js.map +1 -0
- package/lib/module/fab/Fab.js +25 -6
- package/lib/module/fab/Fab.js.map +1 -1
- package/lib/module/fab/Fab.web.js +26 -7
- package/lib/module/fab/Fab.web.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -0
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/responsive-sheet/ResponsiveSheet.js +293 -0
- package/lib/module/responsive-sheet/ResponsiveSheet.js.map +1 -0
- package/lib/module/responsive-sheet/ResponsiveSheet.native.js +219 -0
- package/lib/module/responsive-sheet/ResponsiveSheet.native.js.map +1 -0
- package/lib/module/responsive-sheet/const.js +56 -0
- package/lib/module/responsive-sheet/const.js.map +1 -0
- package/lib/module/responsive-sheet/index.js +4 -0
- package/lib/module/responsive-sheet/index.js.map +1 -0
- package/lib/module/responsive-sheet/index.web.js +14 -0
- package/lib/module/responsive-sheet/index.web.js.map +1 -0
- package/lib/module/responsive-sheet/types.js +4 -0
- package/lib/module/responsive-sheet/types.js.map +1 -0
- package/lib/typescript/commonjs/fab/Fab.d.ts.map +1 -1
- package/lib/typescript/commonjs/fab/Fab.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/fab/types.d.ts +12 -2
- package/lib/typescript/commonjs/fab/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +2 -0
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.d.ts +26 -0
- package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.d.ts.map +1 -0
- package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.native.d.ts +20 -0
- package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.native.d.ts.map +1 -0
- package/lib/typescript/commonjs/responsive-sheet/const.d.ts +43 -0
- package/lib/typescript/commonjs/responsive-sheet/const.d.ts.map +1 -0
- package/lib/typescript/commonjs/responsive-sheet/index.d.ts +3 -0
- package/lib/typescript/commonjs/responsive-sheet/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/responsive-sheet/index.web.d.ts +3 -0
- package/lib/typescript/commonjs/responsive-sheet/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/responsive-sheet/types.d.ts +55 -0
- package/lib/typescript/commonjs/responsive-sheet/types.d.ts.map +1 -0
- package/lib/typescript/module/fab/Fab.d.ts.map +1 -1
- package/lib/typescript/module/fab/Fab.web.d.ts.map +1 -1
- package/lib/typescript/module/fab/types.d.ts +12 -2
- package/lib/typescript/module/fab/types.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +2 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +2 -0
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/responsive-sheet/ResponsiveSheet.d.ts +26 -0
- package/lib/typescript/module/responsive-sheet/ResponsiveSheet.d.ts.map +1 -0
- package/lib/typescript/module/responsive-sheet/ResponsiveSheet.native.d.ts +20 -0
- package/lib/typescript/module/responsive-sheet/ResponsiveSheet.native.d.ts.map +1 -0
- package/lib/typescript/module/responsive-sheet/const.d.ts +43 -0
- package/lib/typescript/module/responsive-sheet/const.d.ts.map +1 -0
- package/lib/typescript/module/responsive-sheet/index.d.ts +3 -0
- package/lib/typescript/module/responsive-sheet/index.d.ts.map +1 -0
- package/lib/typescript/module/responsive-sheet/index.web.d.ts +3 -0
- package/lib/typescript/module/responsive-sheet/index.web.d.ts.map +1 -0
- package/lib/typescript/module/responsive-sheet/types.d.ts +55 -0
- package/lib/typescript/module/responsive-sheet/types.d.ts.map +1 -0
- package/package.json +18 -1
- package/src/__tests__/Fab.test.tsx +17 -0
- package/src/__tests__/Fab.web.test.tsx +14 -0
- package/src/__tests__/ResponsiveSheet.test.tsx +104 -0
- package/src/fab/Fab.stories.tsx +9 -0
- package/src/fab/Fab.tsx +33 -7
- package/src/fab/Fab.web.tsx +33 -8
- package/src/fab/types.ts +12 -2
- package/src/index.ts +8 -0
- package/src/index.web.ts +8 -0
- package/src/responsive-sheet/ResponsiveSheet.native.tsx +257 -0
- package/src/responsive-sheet/ResponsiveSheet.stories.tsx +54 -0
- package/src/responsive-sheet/ResponsiveSheet.tsx +327 -0
- package/src/responsive-sheet/const.ts +53 -0
- package/src/responsive-sheet/index.ts +6 -0
- package/src/responsive-sheet/index.web.ts +16 -0
- package/src/responsive-sheet/types.ts +57 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import {
|
|
9
|
+
Pressable,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
useWindowDimensions,
|
|
12
|
+
View,
|
|
13
|
+
type ViewStyle,
|
|
14
|
+
} from 'react-native';
|
|
15
|
+
|
|
16
|
+
import { Z_INDEX } from '../styles/z-index';
|
|
17
|
+
import { useTheme } from '../theme/use-theme';
|
|
18
|
+
import {
|
|
19
|
+
ANIMATION_DURATION,
|
|
20
|
+
BACKDROP_OPACITY,
|
|
21
|
+
DEFAULT_BREAKPOINT,
|
|
22
|
+
DEFAULT_LABEL,
|
|
23
|
+
DEFAULT_MAX_HEIGHT_RATIO,
|
|
24
|
+
DEFAULT_SIDE,
|
|
25
|
+
DEFAULT_WIDTH,
|
|
26
|
+
EASE_OUT,
|
|
27
|
+
HANDLE_HEIGHT,
|
|
28
|
+
HANDLE_RADIUS,
|
|
29
|
+
HANDLE_WIDTH,
|
|
30
|
+
PANEL_RADIUS,
|
|
31
|
+
SIDE_SHEET_MIN_GUTTER,
|
|
32
|
+
} from './const';
|
|
33
|
+
import type { ResponsiveSheetProps } from './types';
|
|
34
|
+
|
|
35
|
+
/** Stable testID for the dimmed backdrop dismiss button. */
|
|
36
|
+
export const RESPONSIVE_SHEET_BACKDROP_TESTID = 'bloom-responsive-sheet-backdrop';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Default / web variant of `<ResponsiveSheet>`.
|
|
40
|
+
*
|
|
41
|
+
* This file is BOTH the package's default entry (`react-native` export
|
|
42
|
+
* condition resolves it for downstream `tsc`, and non-Metro web bundlers pick
|
|
43
|
+
* it via the `browser` condition through `index.web`) AND the canonical web
|
|
44
|
+
* implementation. It MUST NOT import `react-native-reanimated` — reanimated has
|
|
45
|
+
* no web build and its worklets Babel plugin is native-only, so a static import
|
|
46
|
+
* here would break every web bundler and downstream type-check. The native
|
|
47
|
+
* fork (`ResponsiveSheet.native.tsx`, selected only by Metro) drives the same
|
|
48
|
+
* motion with reanimated.
|
|
49
|
+
*
|
|
50
|
+
* Motion is pure CSS transitions: react-native-web emits `transition-*` style
|
|
51
|
+
* props to the DOM, and the panel/backdrop toggle their transform/opacity by a
|
|
52
|
+
* `shown` flag. Presence is handled with a `mounted` + `shown` pair so both the
|
|
53
|
+
* open AND close directions animate while the node stays mounted through the
|
|
54
|
+
* exit. We deliberately do NOT use reanimated `exiting` on web — concurrent
|
|
55
|
+
* React unmounts there throw `Failed to execute 'removeChild' on 'Node'`, the
|
|
56
|
+
* exact bug this component exists to avoid.
|
|
57
|
+
*/
|
|
58
|
+
export function ResponsiveSheet({
|
|
59
|
+
open,
|
|
60
|
+
onClose,
|
|
61
|
+
side = DEFAULT_SIDE,
|
|
62
|
+
breakpoint = DEFAULT_BREAKPOINT,
|
|
63
|
+
width = DEFAULT_WIDTH,
|
|
64
|
+
maxHeightRatio = DEFAULT_MAX_HEIGHT_RATIO,
|
|
65
|
+
inset,
|
|
66
|
+
showHandle = true,
|
|
67
|
+
dismissOnBackdrop = true,
|
|
68
|
+
panelStyle,
|
|
69
|
+
panelClassName,
|
|
70
|
+
containerStyle,
|
|
71
|
+
containerClassName,
|
|
72
|
+
label = DEFAULT_LABEL,
|
|
73
|
+
children,
|
|
74
|
+
}: ResponsiveSheetProps) {
|
|
75
|
+
const theme = useTheme();
|
|
76
|
+
const { width: viewportWidth, height: viewportHeight } = useWindowDimensions();
|
|
77
|
+
|
|
78
|
+
// Presence: `mounted` keeps the node in the tree through the exit transition;
|
|
79
|
+
// `shown` drives the enter/exit transform & opacity. On open we mount first,
|
|
80
|
+
// then flip `shown` on the next frame so the browser registers the start
|
|
81
|
+
// state and animates to the end. On close we flip `shown` off, then unmount
|
|
82
|
+
// after the transition duration.
|
|
83
|
+
const [mounted, setMounted] = useState(open);
|
|
84
|
+
const [shown, setShown] = useState(false);
|
|
85
|
+
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
86
|
+
const frameRef = useRef<FrameToken | null>(null);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (open) {
|
|
90
|
+
if (closeTimerRef.current) {
|
|
91
|
+
clearTimeout(closeTimerRef.current);
|
|
92
|
+
closeTimerRef.current = null;
|
|
93
|
+
}
|
|
94
|
+
setMounted(true);
|
|
95
|
+
// Defer to the next frame so the entry start state paints before we
|
|
96
|
+
// transition to the shown state. Two frames guarantees the start
|
|
97
|
+
// transform has committed before the toggle (a single frame can be
|
|
98
|
+
// coalesced with the mount paint in some browsers).
|
|
99
|
+
frameRef.current = scheduleFrame(() => {
|
|
100
|
+
frameRef.current = scheduleFrame(() => setShown(true));
|
|
101
|
+
});
|
|
102
|
+
return () => {
|
|
103
|
+
if (frameRef.current !== null) {
|
|
104
|
+
cancelFrame(frameRef.current);
|
|
105
|
+
frameRef.current = null;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Closing: only run the exit if something is currently mounted.
|
|
111
|
+
setShown(false);
|
|
112
|
+
if (!mounted) return;
|
|
113
|
+
closeTimerRef.current = setTimeout(() => {
|
|
114
|
+
setMounted(false);
|
|
115
|
+
closeTimerRef.current = null;
|
|
116
|
+
}, ANIMATION_DURATION);
|
|
117
|
+
return () => {
|
|
118
|
+
if (closeTimerRef.current) {
|
|
119
|
+
clearTimeout(closeTimerRef.current);
|
|
120
|
+
closeTimerRef.current = null;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}, [open, mounted]);
|
|
124
|
+
|
|
125
|
+
// Escape-to-close while open (web only; document is undefined elsewhere).
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (!open || typeof document === 'undefined') return;
|
|
128
|
+
const handler = (e: KeyboardEvent) => {
|
|
129
|
+
if (e.key === 'Escape') {
|
|
130
|
+
e.stopPropagation();
|
|
131
|
+
onClose();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
document.addEventListener('keydown', handler);
|
|
135
|
+
return () => document.removeEventListener('keydown', handler);
|
|
136
|
+
}, [open, onClose]);
|
|
137
|
+
|
|
138
|
+
const handleBackdropPress = useCallback(() => {
|
|
139
|
+
if (dismissOnBackdrop) onClose();
|
|
140
|
+
}, [dismissOnBackdrop, onClose]);
|
|
141
|
+
|
|
142
|
+
const isBottom = viewportWidth < breakpoint;
|
|
143
|
+
const panelBackground = theme.colors.background;
|
|
144
|
+
|
|
145
|
+
const panelTransition = useMemo<ViewStyle>(
|
|
146
|
+
() =>
|
|
147
|
+
({
|
|
148
|
+
transitionProperty: 'transform, opacity',
|
|
149
|
+
transitionDuration: `${ANIMATION_DURATION}ms`,
|
|
150
|
+
transitionTimingFunction: EASE_OUT,
|
|
151
|
+
}) as ViewStyle,
|
|
152
|
+
[],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const backdropTransition = useMemo<ViewStyle>(
|
|
156
|
+
() =>
|
|
157
|
+
({
|
|
158
|
+
transitionProperty: 'opacity',
|
|
159
|
+
transitionDuration: `${ANIMATION_DURATION}ms`,
|
|
160
|
+
transitionTimingFunction: EASE_OUT,
|
|
161
|
+
}) as ViewStyle,
|
|
162
|
+
[],
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const panelGeometry = useMemo<ViewStyle>(() => {
|
|
166
|
+
if (isBottom) {
|
|
167
|
+
return {
|
|
168
|
+
left: 0,
|
|
169
|
+
right: 0,
|
|
170
|
+
bottom: 0,
|
|
171
|
+
maxHeight: Math.round(viewportHeight * maxHeightRatio),
|
|
172
|
+
borderTopLeftRadius: PANEL_RADIUS,
|
|
173
|
+
borderTopRightRadius: PANEL_RADIUS,
|
|
174
|
+
transform: [{ translateY: shown ? 0 : '100%' }],
|
|
175
|
+
opacity: shown ? 1 : 0,
|
|
176
|
+
} as ViewStyle;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Side-sheet: full height minus top/bottom inset, anchored to `side`,
|
|
180
|
+
// offset by the matching horizontal inset. Width is capped so the panel
|
|
181
|
+
// always leaves a gutter on the opposite edge.
|
|
182
|
+
const insetTop = inset?.top ?? 0;
|
|
183
|
+
const insetBottom = inset?.bottom ?? 0;
|
|
184
|
+
const insetLeft = inset?.left ?? 0;
|
|
185
|
+
const insetRight = inset?.right ?? 0;
|
|
186
|
+
const anchorInset = side === 'left' ? insetLeft : insetRight;
|
|
187
|
+
const oppositeInset = side === 'left' ? insetRight : insetLeft;
|
|
188
|
+
const available = viewportWidth - anchorInset - oppositeInset - SIDE_SHEET_MIN_GUTTER;
|
|
189
|
+
const cappedWidth = Math.max(0, Math.min(width, available));
|
|
190
|
+
const hiddenSign = side === 'left' ? '-100%' : '100%';
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
top: insetTop,
|
|
194
|
+
bottom: insetBottom,
|
|
195
|
+
[side]: anchorInset,
|
|
196
|
+
width: cappedWidth,
|
|
197
|
+
borderRadius: PANEL_RADIUS,
|
|
198
|
+
transform: [{ translateX: shown ? 0 : hiddenSign }],
|
|
199
|
+
opacity: shown ? 1 : 0,
|
|
200
|
+
} as ViewStyle;
|
|
201
|
+
}, [isBottom, shown, side, width, inset, viewportWidth, viewportHeight, maxHeightRatio]);
|
|
202
|
+
|
|
203
|
+
if (!mounted) return null;
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<View
|
|
207
|
+
style={[styles.root, containerStyle]}
|
|
208
|
+
// NativeWind reads `className` at runtime; RN core does not type it on
|
|
209
|
+
// View, so spread it conditionally (matching the Button/Fab pattern).
|
|
210
|
+
{...(containerClassName ? ({ className: containerClassName } as Record<string, string>) : {})}
|
|
211
|
+
pointerEvents="box-none"
|
|
212
|
+
>
|
|
213
|
+
<Pressable
|
|
214
|
+
testID={RESPONSIVE_SHEET_BACKDROP_TESTID}
|
|
215
|
+
accessibilityRole="button"
|
|
216
|
+
accessibilityLabel={`Dismiss ${label}`}
|
|
217
|
+
onPress={handleBackdropPress}
|
|
218
|
+
disabled={!dismissOnBackdrop}
|
|
219
|
+
style={[
|
|
220
|
+
styles.backdrop,
|
|
221
|
+
backdropTransition,
|
|
222
|
+
{ opacity: shown ? BACKDROP_OPACITY : 0 },
|
|
223
|
+
isWeb ? webBackdropBlur : null,
|
|
224
|
+
]}
|
|
225
|
+
/>
|
|
226
|
+
|
|
227
|
+
<View
|
|
228
|
+
accessibilityViewIsModal
|
|
229
|
+
aria-label={label}
|
|
230
|
+
{...(panelClassName ? ({ className: panelClassName } as Record<string, string>) : {})}
|
|
231
|
+
style={[
|
|
232
|
+
styles.panel,
|
|
233
|
+
{ backgroundColor: panelBackground, shadowColor: theme.colors.shadow },
|
|
234
|
+
panelGeometry,
|
|
235
|
+
panelTransition,
|
|
236
|
+
panelStyle,
|
|
237
|
+
]}
|
|
238
|
+
pointerEvents="auto"
|
|
239
|
+
>
|
|
240
|
+
{isBottom && showHandle ? (
|
|
241
|
+
<View style={styles.handleRow} pointerEvents="none">
|
|
242
|
+
<View style={[styles.handle, { backgroundColor: theme.colors.border }]} />
|
|
243
|
+
</View>
|
|
244
|
+
) : null}
|
|
245
|
+
{children}
|
|
246
|
+
</View>
|
|
247
|
+
</View>
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// This module is the web/default impl, but it is also the fallback resolved by
|
|
252
|
+
// a downstream consumer's `tsc` for the native target, so guard the web-only
|
|
253
|
+
// `backdrop-blur` style behind a runtime check rather than assuming web.
|
|
254
|
+
const isWeb = typeof document !== 'undefined';
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Opaque token returned by `scheduleFrame`. `requestAnimationFrame` returns a
|
|
258
|
+
* number; the `setTimeout` fallback returns a timer handle — both are kept so
|
|
259
|
+
* `cancelFrame` can route to the matching canceller.
|
|
260
|
+
*/
|
|
261
|
+
type FrameToken = { raf: number } | { timer: ReturnType<typeof setTimeout> };
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Schedule a callback for the next paint. Uses `requestAnimationFrame` in the
|
|
265
|
+
* browser; degrades to a 0ms timeout when rAF is unavailable (SSR/hydration,
|
|
266
|
+
* non-DOM test envs) so the enter transition's `shown` flip still fires.
|
|
267
|
+
*/
|
|
268
|
+
function scheduleFrame(cb: () => void): FrameToken {
|
|
269
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
270
|
+
return { raf: requestAnimationFrame(cb) };
|
|
271
|
+
}
|
|
272
|
+
return { timer: setTimeout(cb, 0) };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function cancelFrame(token: FrameToken): void {
|
|
276
|
+
if ('raf' in token) {
|
|
277
|
+
if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(token.raf);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
clearTimeout(token.timer);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// `backdrop-filter` is web-only; react-native-web passes it through to the DOM.
|
|
284
|
+
const webBackdropBlur = { backdropFilter: 'blur(2px)' } as unknown as ViewStyle;
|
|
285
|
+
|
|
286
|
+
const styles = StyleSheet.create({
|
|
287
|
+
root: {
|
|
288
|
+
// `position: fixed` pins the overlay to the viewport on web; react-native
|
|
289
|
+
// does not type `'fixed'`, so cast to the RN-accepted `'absolute'`. On
|
|
290
|
+
// native the `.native` fork uses `StyleSheet.absoluteFill` instead.
|
|
291
|
+
position: 'fixed' as 'absolute',
|
|
292
|
+
top: 0,
|
|
293
|
+
left: 0,
|
|
294
|
+
right: 0,
|
|
295
|
+
bottom: 0,
|
|
296
|
+
zIndex: Z_INDEX.fullscreen,
|
|
297
|
+
},
|
|
298
|
+
backdrop: {
|
|
299
|
+
position: 'absolute',
|
|
300
|
+
top: 0,
|
|
301
|
+
left: 0,
|
|
302
|
+
right: 0,
|
|
303
|
+
bottom: 0,
|
|
304
|
+
backgroundColor: '#000',
|
|
305
|
+
},
|
|
306
|
+
panel: {
|
|
307
|
+
position: 'absolute',
|
|
308
|
+
overflow: 'hidden',
|
|
309
|
+
shadowOpacity: 0.18,
|
|
310
|
+
shadowRadius: 24,
|
|
311
|
+
shadowOffset: { width: 0, height: 8 },
|
|
312
|
+
},
|
|
313
|
+
handleRow: {
|
|
314
|
+
width: '100%',
|
|
315
|
+
alignItems: 'center',
|
|
316
|
+
paddingTop: 8,
|
|
317
|
+
paddingBottom: 4,
|
|
318
|
+
},
|
|
319
|
+
handle: {
|
|
320
|
+
width: HANDLE_WIDTH,
|
|
321
|
+
height: HANDLE_HEIGHT,
|
|
322
|
+
borderRadius: HANDLE_RADIUS,
|
|
323
|
+
opacity: 0.5,
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
export default ResponsiveSheet;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared default constants for `ResponsiveSheet`.
|
|
3
|
+
*
|
|
4
|
+
* Platform-agnostic — imported by both the default/web CSS implementation
|
|
5
|
+
* (`ResponsiveSheet.tsx`) and the native reanimated implementation
|
|
6
|
+
* (`ResponsiveSheet.native.tsx`) so the two variants stay in lockstep.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Below this viewport width the sheet renders as a bottom-sheet. */
|
|
10
|
+
export const DEFAULT_BREAKPOINT = 768;
|
|
11
|
+
|
|
12
|
+
/** Side-sheet width (px) on wide screens. Capped to fit the viewport. */
|
|
13
|
+
export const DEFAULT_WIDTH = 460;
|
|
14
|
+
|
|
15
|
+
/** Bottom-sheet maximum height as a fraction of the viewport height. */
|
|
16
|
+
export const DEFAULT_MAX_HEIGHT_RATIO = 0.9;
|
|
17
|
+
|
|
18
|
+
/** Anchored edge of the side-sheet on wide screens. */
|
|
19
|
+
export const DEFAULT_SIDE = 'left' as const;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Open/close transition duration (ms). ~280–300ms ease-out reads as smooth
|
|
23
|
+
* without feeling sluggish; shared by the CSS transition and the native
|
|
24
|
+
* reanimated timing so both platforms match.
|
|
25
|
+
*/
|
|
26
|
+
export const ANIMATION_DURATION = 280;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Standard CSS approximation of reanimated's `Easing.out(Easing.cubic)` so the
|
|
30
|
+
* web transition curve matches the native one.
|
|
31
|
+
*/
|
|
32
|
+
export const EASE_OUT = 'cubic-bezier(0.33, 1, 0.68, 1)';
|
|
33
|
+
|
|
34
|
+
/** Corner radius for the side-sheet panel and the top of the bottom-sheet. */
|
|
35
|
+
export const PANEL_RADIUS = 20;
|
|
36
|
+
|
|
37
|
+
/** Backdrop dim opacity once fully shown (matches `bg-black/40`). */
|
|
38
|
+
export const BACKDROP_OPACITY = 0.4;
|
|
39
|
+
|
|
40
|
+
/** Drag-handle pill geometry for the bottom-sheet. */
|
|
41
|
+
export const HANDLE_WIDTH = 36;
|
|
42
|
+
export const HANDLE_HEIGHT = 5;
|
|
43
|
+
export const HANDLE_RADIUS = 3;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Minimum gutter kept between a wide side-sheet and the opposite viewport edge
|
|
47
|
+
* when capping its width, so the panel never spans the full screen on a narrow
|
|
48
|
+
* desktop window that is still above the breakpoint.
|
|
49
|
+
*/
|
|
50
|
+
export const SIDE_SHEET_MIN_GUTTER = 24;
|
|
51
|
+
|
|
52
|
+
/** A11y label fallback when the host does not provide one. */
|
|
53
|
+
export const DEFAULT_LABEL = 'Sheet';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Web variant of the `./responsive-sheet` barrel.
|
|
2
|
+
//
|
|
3
|
+
// The default barrel (`./index.ts`) re-exports from `./ResponsiveSheet`, which
|
|
4
|
+
// is itself the reanimated-free web/CSS implementation — Metro additionally
|
|
5
|
+
// resolves `./ResponsiveSheet.native` for native targets. Web bundlers select
|
|
6
|
+
// THIS file via the `"browser"` export condition in `package.json`'s
|
|
7
|
+
// `exports['./responsive-sheet']`, pointing them explicitly at the CSS
|
|
8
|
+
// implementation so a non-Metro bundler never follows the `.native` sibling
|
|
9
|
+
// (which statically imports `react-native-reanimated`, a native-only module).
|
|
10
|
+
// Types are platform-agnostic, so they come straight from `./types`.
|
|
11
|
+
export { ResponsiveSheet, RESPONSIVE_SHEET_BACKDROP_TESTID } from './ResponsiveSheet';
|
|
12
|
+
export type {
|
|
13
|
+
ResponsiveSheetProps,
|
|
14
|
+
ResponsiveSheetSide,
|
|
15
|
+
ResponsiveSheetInset,
|
|
16
|
+
} from './types';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/** Edge a wide-screen side-sheet anchors to. */
|
|
5
|
+
export type ResponsiveSheetSide = 'left' | 'right';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Inset (px) of the wide-screen side-sheet from its container edges. Lets a
|
|
9
|
+
* host place the sheet "inside its own shell" — e.g. offset from the top by a
|
|
10
|
+
* header, or from the left by a nav rail — with margins on every side.
|
|
11
|
+
*/
|
|
12
|
+
export interface ResponsiveSheetInset {
|
|
13
|
+
top?: number;
|
|
14
|
+
bottom?: number;
|
|
15
|
+
left?: number;
|
|
16
|
+
right?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A responsive overlay sheet: an anchored side-sheet on wide screens and a
|
|
21
|
+
* bottom-sheet (with drag handle) on small screens, switching by viewport
|
|
22
|
+
* width. It is a plain controlled component — the host drives `open`/`onClose`
|
|
23
|
+
* from its own state. It renders in place (no portal) so the host can scope it
|
|
24
|
+
* inside a themed/positioned subtree.
|
|
25
|
+
*/
|
|
26
|
+
export interface ResponsiveSheetProps {
|
|
27
|
+
/** Whether the sheet is open. Controlled by the host. */
|
|
28
|
+
open: boolean;
|
|
29
|
+
/** Fired when the sheet requests to close (backdrop tap, Escape on web). */
|
|
30
|
+
onClose: () => void;
|
|
31
|
+
/** Anchor edge on wide screens. Defaults to `'left'`. */
|
|
32
|
+
side?: ResponsiveSheetSide;
|
|
33
|
+
/** Viewport width (px) below which the sheet renders as a bottom-sheet. Defaults to `768`. */
|
|
34
|
+
breakpoint?: number;
|
|
35
|
+
/** Side-sheet width (px) on wide screens. Capped to fit. Defaults to `460`. */
|
|
36
|
+
width?: number;
|
|
37
|
+
/** Bottom-sheet max height as a fraction of the viewport height. Defaults to `0.9`. */
|
|
38
|
+
maxHeightRatio?: number;
|
|
39
|
+
/** Side-sheet inset (px) from the container edges. Defaults to `0` on all sides. */
|
|
40
|
+
inset?: ResponsiveSheetInset;
|
|
41
|
+
/** Whether to render the drag handle in bottom-sheet mode. Defaults to `true`. */
|
|
42
|
+
showHandle?: boolean;
|
|
43
|
+
/** Whether tapping the backdrop closes the sheet. Defaults to `true`. */
|
|
44
|
+
dismissOnBackdrop?: boolean;
|
|
45
|
+
/** Style for the panel surface (e.g. brand color). Composed after defaults. */
|
|
46
|
+
panelStyle?: StyleProp<ViewStyle>;
|
|
47
|
+
/** NativeWind classes for the panel surface. */
|
|
48
|
+
panelClassName?: string;
|
|
49
|
+
/** Style for the root overlay (theme-var scope, rail offset, etc.). */
|
|
50
|
+
containerStyle?: StyleProp<ViewStyle>;
|
|
51
|
+
/** NativeWind classes for the root overlay (e.g. `"md:left-[4.75rem]"`). */
|
|
52
|
+
containerClassName?: string;
|
|
53
|
+
/** Accessibility label for the sheet panel and the backdrop dismiss button. */
|
|
54
|
+
label?: string;
|
|
55
|
+
/** Sheet content. */
|
|
56
|
+
children: ReactNode;
|
|
57
|
+
}
|