@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.
Files changed (97) hide show
  1. package/lib/commonjs/fab/Fab.js +25 -6
  2. package/lib/commonjs/fab/Fab.js.map +1 -1
  3. package/lib/commonjs/fab/Fab.web.js +26 -7
  4. package/lib/commonjs/fab/Fab.web.js.map +1 -1
  5. package/lib/commonjs/index.js +15 -0
  6. package/lib/commonjs/index.js.map +1 -1
  7. package/lib/commonjs/index.web.js +21 -6
  8. package/lib/commonjs/index.web.js.map +1 -1
  9. package/lib/commonjs/responsive-sheet/ResponsiveSheet.js +300 -0
  10. package/lib/commonjs/responsive-sheet/ResponsiveSheet.js.map +1 -0
  11. package/lib/commonjs/responsive-sheet/ResponsiveSheet.native.js +226 -0
  12. package/lib/commonjs/responsive-sheet/ResponsiveSheet.native.js.map +1 -0
  13. package/lib/commonjs/responsive-sheet/const.js +60 -0
  14. package/lib/commonjs/responsive-sheet/const.js.map +1 -0
  15. package/lib/commonjs/responsive-sheet/index.js +19 -0
  16. package/lib/commonjs/responsive-sheet/index.js.map +1 -0
  17. package/lib/commonjs/responsive-sheet/index.web.js +19 -0
  18. package/lib/commonjs/responsive-sheet/index.web.js.map +1 -0
  19. package/lib/commonjs/responsive-sheet/types.js +6 -0
  20. package/lib/commonjs/responsive-sheet/types.js.map +1 -0
  21. package/lib/module/fab/Fab.js +25 -6
  22. package/lib/module/fab/Fab.js.map +1 -1
  23. package/lib/module/fab/Fab.web.js +26 -7
  24. package/lib/module/fab/Fab.web.js.map +1 -1
  25. package/lib/module/index.js +2 -0
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/index.web.js +2 -0
  28. package/lib/module/index.web.js.map +1 -1
  29. package/lib/module/responsive-sheet/ResponsiveSheet.js +293 -0
  30. package/lib/module/responsive-sheet/ResponsiveSheet.js.map +1 -0
  31. package/lib/module/responsive-sheet/ResponsiveSheet.native.js +219 -0
  32. package/lib/module/responsive-sheet/ResponsiveSheet.native.js.map +1 -0
  33. package/lib/module/responsive-sheet/const.js +56 -0
  34. package/lib/module/responsive-sheet/const.js.map +1 -0
  35. package/lib/module/responsive-sheet/index.js +4 -0
  36. package/lib/module/responsive-sheet/index.js.map +1 -0
  37. package/lib/module/responsive-sheet/index.web.js +14 -0
  38. package/lib/module/responsive-sheet/index.web.js.map +1 -0
  39. package/lib/module/responsive-sheet/types.js +4 -0
  40. package/lib/module/responsive-sheet/types.js.map +1 -0
  41. package/lib/typescript/commonjs/fab/Fab.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/fab/Fab.web.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/fab/types.d.ts +12 -2
  44. package/lib/typescript/commonjs/fab/types.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/index.d.ts +2 -0
  46. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/index.web.d.ts +2 -0
  48. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.d.ts +26 -0
  50. package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.d.ts.map +1 -0
  51. package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.native.d.ts +20 -0
  52. package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.native.d.ts.map +1 -0
  53. package/lib/typescript/commonjs/responsive-sheet/const.d.ts +43 -0
  54. package/lib/typescript/commonjs/responsive-sheet/const.d.ts.map +1 -0
  55. package/lib/typescript/commonjs/responsive-sheet/index.d.ts +3 -0
  56. package/lib/typescript/commonjs/responsive-sheet/index.d.ts.map +1 -0
  57. package/lib/typescript/commonjs/responsive-sheet/index.web.d.ts +3 -0
  58. package/lib/typescript/commonjs/responsive-sheet/index.web.d.ts.map +1 -0
  59. package/lib/typescript/commonjs/responsive-sheet/types.d.ts +55 -0
  60. package/lib/typescript/commonjs/responsive-sheet/types.d.ts.map +1 -0
  61. package/lib/typescript/module/fab/Fab.d.ts.map +1 -1
  62. package/lib/typescript/module/fab/Fab.web.d.ts.map +1 -1
  63. package/lib/typescript/module/fab/types.d.ts +12 -2
  64. package/lib/typescript/module/fab/types.d.ts.map +1 -1
  65. package/lib/typescript/module/index.d.ts +2 -0
  66. package/lib/typescript/module/index.d.ts.map +1 -1
  67. package/lib/typescript/module/index.web.d.ts +2 -0
  68. package/lib/typescript/module/index.web.d.ts.map +1 -1
  69. package/lib/typescript/module/responsive-sheet/ResponsiveSheet.d.ts +26 -0
  70. package/lib/typescript/module/responsive-sheet/ResponsiveSheet.d.ts.map +1 -0
  71. package/lib/typescript/module/responsive-sheet/ResponsiveSheet.native.d.ts +20 -0
  72. package/lib/typescript/module/responsive-sheet/ResponsiveSheet.native.d.ts.map +1 -0
  73. package/lib/typescript/module/responsive-sheet/const.d.ts +43 -0
  74. package/lib/typescript/module/responsive-sheet/const.d.ts.map +1 -0
  75. package/lib/typescript/module/responsive-sheet/index.d.ts +3 -0
  76. package/lib/typescript/module/responsive-sheet/index.d.ts.map +1 -0
  77. package/lib/typescript/module/responsive-sheet/index.web.d.ts +3 -0
  78. package/lib/typescript/module/responsive-sheet/index.web.d.ts.map +1 -0
  79. package/lib/typescript/module/responsive-sheet/types.d.ts +55 -0
  80. package/lib/typescript/module/responsive-sheet/types.d.ts.map +1 -0
  81. package/package.json +18 -1
  82. package/src/__tests__/Fab.test.tsx +17 -0
  83. package/src/__tests__/Fab.web.test.tsx +14 -0
  84. package/src/__tests__/ResponsiveSheet.test.tsx +104 -0
  85. package/src/fab/Fab.stories.tsx +9 -0
  86. package/src/fab/Fab.tsx +33 -7
  87. package/src/fab/Fab.web.tsx +33 -8
  88. package/src/fab/types.ts +12 -2
  89. package/src/index.ts +8 -0
  90. package/src/index.web.ts +8 -0
  91. package/src/responsive-sheet/ResponsiveSheet.native.tsx +257 -0
  92. package/src/responsive-sheet/ResponsiveSheet.stories.tsx +54 -0
  93. package/src/responsive-sheet/ResponsiveSheet.tsx +327 -0
  94. package/src/responsive-sheet/const.ts +53 -0
  95. package/src/responsive-sheet/index.ts +6 -0
  96. package/src/responsive-sheet/index.web.ts +16 -0
  97. package/src/responsive-sheet/types.ts +57 -0
@@ -14,15 +14,40 @@ import type { FabPlacement, FabProps, FabSize, FabVariant } from './types';
14
14
 
15
15
  export type { FabProps, FabVariant, FabSize, FabPlacement } from './types';
16
16
 
17
- // Material-style FAB scale. `small` (40px, the compact FAB Mention uses) keeps a
18
- // full 24px icon box so the glyph reads clearly; `medium` (56px) is the
19
- // canonical FAB; `large` (64px) is the high-prominence action.
20
- const SIZE_CONFIG: Record<FabSize, { diameter: number; iconBox: number; fontSize: number }> = {
17
+ interface ResolvedSize {
18
+ diameter: number;
19
+ iconBox: number;
20
+ fontSize: number;
21
+ }
22
+
23
+ // Material-style FAB scale. `small` (40px) keeps a full 24px icon box so the
24
+ // glyph reads clearly; `medium` (56px) is the canonical FAB; `large` (64px) is
25
+ // the high-prominence action.
26
+ const SIZE_CONFIG: Record<FabSize, ResolvedSize> = {
21
27
  small: { diameter: 40, iconBox: 24, fontSize: 14 },
22
28
  medium: { diameter: 56, iconBox: 24, fontSize: 15 },
23
29
  large: { diameter: 64, iconBox: 28, fontSize: 16 },
24
30
  };
25
31
 
32
+ const MIN_NUMERIC_ICON_BOX = 22;
33
+
34
+ /**
35
+ * Resolve a `size` prop (preset name or raw pixel diameter) to concrete pixel
36
+ * geometry. Mirrors the native `resolveSize` exactly so a numeric size renders
37
+ * identically on both platforms. A numeric size sets the diameter directly and
38
+ * derives the icon box as `round(size * 0.5)` (clamped to a sensible minimum).
39
+ */
40
+ function resolveSize(size: FabSize | number): ResolvedSize {
41
+ if (typeof size === 'number') {
42
+ return {
43
+ diameter: size,
44
+ iconBox: Math.max(MIN_NUMERIC_ICON_BOX, Math.round(size * 0.5)),
45
+ fontSize: Math.max(13, Math.round(size * 0.27)),
46
+ };
47
+ }
48
+ return SIZE_CONFIG[size];
49
+ }
50
+
26
51
  const PILL_RADIUS = 999;
27
52
  const PRESS_SCALE = 0.94;
28
53
  const DEFAULT_OFFSET = 16;
@@ -176,7 +201,7 @@ const FabWebComponent: React.FC<FabProps> = ({
176
201
  const reactId = useId();
177
202
  const resolvedId = id ?? `bloom-fab-${reactId}`;
178
203
 
179
- const sizeConfig = SIZE_CONFIG[size];
204
+ const sizeConfig = useMemo(() => resolveSize(size), [size]);
180
205
  const isExtended = label != null && label.length > 0;
181
206
  const content = icon ?? children;
182
207
  const variantColors = useMemo(() => resolveVariant(variant, theme.colors), [variant, theme.colors]);
@@ -202,8 +227,9 @@ const FabWebComponent: React.FC<FabProps> = ({
202
227
  ...placementStyle(placement, offset),
203
228
  };
204
229
  if (isExtended) {
205
- base.paddingLeft = size === 'small' ? 14 : 20;
206
- base.paddingRight = size === 'small' ? 14 : 20;
230
+ const pad = sizeConfig.diameter <= 44 ? 14 : 20;
231
+ base.paddingLeft = pad;
232
+ base.paddingRight = pad;
207
233
  } else {
208
234
  base.width = sizeConfig.diameter;
209
235
  }
@@ -218,7 +244,6 @@ const FabWebComponent: React.FC<FabProps> = ({
218
244
  placement,
219
245
  offset,
220
246
  isExtended,
221
- size,
222
247
  ]);
223
248
 
224
249
  const handleClick = useCallback(
package/src/fab/types.ts CHANGED
@@ -12,13 +12,19 @@ import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
12
12
  export type FabVariant = 'primary' | 'secondary' | 'surface';
13
13
 
14
14
  /**
15
- * FAB sizes, matching Material-ish conventions but in Bloom's scale:
15
+ * FAB size presets, matching Material-ish conventions but in Bloom's scale:
16
16
  * - `small` — 40px (compact / dense layouts)
17
17
  * - `medium` — 56px (the canonical FAB; the default)
18
18
  * - `large` — 64px (high-prominence primary action)
19
19
  *
20
20
  * In the `extended` variant the height tracks the size but the FAB grows
21
21
  * horizontally to fit its icon + label as a pill.
22
+ *
23
+ * The `size` prop ALSO accepts a raw pixel `number` for an exact diameter
24
+ * between (or outside) the presets — e.g. `size={48}` for a FAB halfway between
25
+ * `small` and `medium`. A numeric size sets the diameter directly, derives the
26
+ * icon box as `round(size * 0.5)` (min 22px) and keeps the circle radius at
27
+ * `size / 2`.
22
28
  */
23
29
  export type FabSize = 'small' | 'medium' | 'large';
24
30
 
@@ -73,7 +79,11 @@ export interface FabProps {
73
79
  label?: string;
74
80
 
75
81
  variant?: FabVariant;
76
- size?: FabSize;
82
+ /**
83
+ * A preset (`'small' | 'medium' | 'large'`) or a raw pixel diameter
84
+ * (`number`). Defaults to `'medium'`. See {@link FabSize}.
85
+ */
86
+ size?: FabSize | number;
77
87
 
78
88
  /**
79
89
  * Corner of the containing block to anchor to. Defaults to `'bottom-right'`.
package/src/index.ts CHANGED
@@ -88,6 +88,14 @@ export type { ComboboxProps, ComboboxOption } from './combobox';
88
88
  export { BottomSheet } from './bottom-sheet';
89
89
  export type { BottomSheetRef, BottomSheetProps } from './bottom-sheet';
90
90
 
91
+ // Responsive sheet — side-sheet on wide screens, bottom-sheet on narrow.
92
+ export { ResponsiveSheet, RESPONSIVE_SHEET_BACKDROP_TESTID } from './responsive-sheet';
93
+ export type {
94
+ ResponsiveSheetProps,
95
+ ResponsiveSheetSide,
96
+ ResponsiveSheetInset,
97
+ } from './responsive-sheet';
98
+
91
99
  // Data display
92
100
  export * from './card';
93
101
  export * from './badge';
package/src/index.web.ts CHANGED
@@ -93,6 +93,14 @@ export type { ComboboxProps, ComboboxOption } from './combobox/index.web';
93
93
  export { BottomSheet } from './bottom-sheet';
94
94
  export type { BottomSheetRef, BottomSheetProps } from './bottom-sheet';
95
95
 
96
+ // Responsive sheet — side-sheet on wide screens, bottom-sheet on narrow.
97
+ export { ResponsiveSheet, RESPONSIVE_SHEET_BACKDROP_TESTID } from './responsive-sheet/index.web';
98
+ export type {
99
+ ResponsiveSheetProps,
100
+ ResponsiveSheetSide,
101
+ ResponsiveSheetInset,
102
+ } from './responsive-sheet/index.web';
103
+
96
104
  // Data display
97
105
  export * from './card';
98
106
  export * from './badge';
@@ -0,0 +1,257 @@
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
+ import Animated, {
16
+ cancelAnimation,
17
+ Easing,
18
+ runOnJS,
19
+ useAnimatedStyle,
20
+ useSharedValue,
21
+ withTiming,
22
+ } from 'react-native-reanimated';
23
+
24
+ import { Z_INDEX } from '../styles/z-index';
25
+ import { useTheme } from '../theme/use-theme';
26
+ import {
27
+ ANIMATION_DURATION,
28
+ BACKDROP_OPACITY,
29
+ DEFAULT_BREAKPOINT,
30
+ DEFAULT_LABEL,
31
+ DEFAULT_MAX_HEIGHT_RATIO,
32
+ DEFAULT_SIDE,
33
+ DEFAULT_WIDTH,
34
+ HANDLE_HEIGHT,
35
+ HANDLE_RADIUS,
36
+ HANDLE_WIDTH,
37
+ PANEL_RADIUS,
38
+ SIDE_SHEET_MIN_GUTTER,
39
+ } from './const';
40
+ import type { ResponsiveSheetProps } from './types';
41
+
42
+ /** Stable testID for the dimmed backdrop dismiss button. */
43
+ export const RESPONSIVE_SHEET_BACKDROP_TESTID = 'bloom-responsive-sheet-backdrop';
44
+
45
+ /**
46
+ * Native variant of `<ResponsiveSheet>` (selected only by Metro).
47
+ *
48
+ * Uses `react-native-reanimated` to drive the same motion as the web/CSS
49
+ * default fork: a shared `progress` value times 0 -> 1 on open and 1 -> 0 on
50
+ * close, mapped to the panel translate/opacity and the backdrop opacity.
51
+ * Native exit animations are safe here — the web-only `removeChild` crash does
52
+ * not apply — so we keep the node mounted through the close timing and unmount
53
+ * from the animation completion callback.
54
+ *
55
+ * The panel itself is measured (not percentage-translated): we offset by the
56
+ * panel's own width/height so the hidden state sits fully off its side, which
57
+ * is the reanimated-friendly equivalent of the web `translate(±100%)`.
58
+ */
59
+ export function ResponsiveSheet({
60
+ open,
61
+ onClose,
62
+ side = DEFAULT_SIDE,
63
+ breakpoint = DEFAULT_BREAKPOINT,
64
+ width = DEFAULT_WIDTH,
65
+ maxHeightRatio = DEFAULT_MAX_HEIGHT_RATIO,
66
+ inset,
67
+ showHandle = true,
68
+ dismissOnBackdrop = true,
69
+ panelStyle,
70
+ panelClassName,
71
+ containerStyle,
72
+ containerClassName,
73
+ label = DEFAULT_LABEL,
74
+ children,
75
+ }: ResponsiveSheetProps) {
76
+ const theme = useTheme();
77
+ const { width: viewportWidth, height: viewportHeight } = useWindowDimensions();
78
+
79
+ // Keep the tree mounted through the exit timing; unmount once it settles.
80
+ const [mounted, setMounted] = useState(open);
81
+ // `progress`: 0 = fully hidden, 1 = fully shown. Drives both transform and
82
+ // opacity so a single timing controls the whole transition.
83
+ const progress = useSharedValue(open ? 1 : 0);
84
+
85
+ const isBottom = viewportWidth < breakpoint;
86
+
87
+ // Measured panel extent along the travel axis. Falls back to the geometry
88
+ // target so the very first frame is already off-screen before layout lands.
89
+ const insetTop = inset?.top ?? 0;
90
+ const insetBottom = inset?.bottom ?? 0;
91
+ const insetLeft = inset?.left ?? 0;
92
+ const insetRight = inset?.right ?? 0;
93
+ const anchorInset = side === 'left' ? insetLeft : insetRight;
94
+ const oppositeInset = side === 'left' ? insetRight : insetLeft;
95
+
96
+ const sideWidth = useMemo(() => {
97
+ const available = viewportWidth - anchorInset - oppositeInset - SIDE_SHEET_MIN_GUTTER;
98
+ return Math.max(0, Math.min(width, available));
99
+ }, [viewportWidth, anchorInset, oppositeInset, width]);
100
+
101
+ const bottomMaxHeight = useMemo(
102
+ () => Math.round(viewportHeight * maxHeightRatio),
103
+ [viewportHeight, maxHeightRatio],
104
+ );
105
+
106
+ // Travel distance for the hidden state: the panel's measured extent (height
107
+ // for the bottom-sheet, width for the side-sheet), with the geometry target
108
+ // as the pre-measurement fallback.
109
+ const travel = useSharedValue(isBottom ? bottomMaxHeight : sideWidth);
110
+ useEffect(() => {
111
+ travel.value = isBottom ? bottomMaxHeight : sideWidth;
112
+ }, [isBottom, bottomMaxHeight, sideWidth, travel]);
113
+
114
+ const finishClose = useCallback(() => {
115
+ setMounted(false);
116
+ }, []);
117
+
118
+ useEffect(() => {
119
+ cancelAnimation(progress);
120
+ const timing = { duration: ANIMATION_DURATION, easing: Easing.out(Easing.cubic) };
121
+ if (open) {
122
+ setMounted(true);
123
+ progress.value = withTiming(1, timing);
124
+ return;
125
+ }
126
+ if (!mounted) return;
127
+ progress.value = withTiming(0, timing, (finished) => {
128
+ if (finished) runOnJS(finishClose)();
129
+ });
130
+ }, [open, mounted, progress, finishClose]);
131
+
132
+ const handleBackdropPress = useCallback(() => {
133
+ if (dismissOnBackdrop) onClose();
134
+ }, [dismissOnBackdrop, onClose]);
135
+
136
+ const backdropAnimatedStyle = useAnimatedStyle(() => ({
137
+ opacity: progress.value * BACKDROP_OPACITY,
138
+ }));
139
+
140
+ const hiddenSign = side === 'left' ? -1 : 1;
141
+ const panelAnimatedStyle = useAnimatedStyle(() => {
142
+ const hidden = 1 - progress.value;
143
+ if (isBottom) {
144
+ return {
145
+ opacity: progress.value,
146
+ transform: [{ translateY: hidden * travel.value }],
147
+ };
148
+ }
149
+ return {
150
+ opacity: progress.value,
151
+ transform: [{ translateX: hidden * travel.value * hiddenSign }],
152
+ };
153
+ }, [isBottom, hiddenSign]);
154
+
155
+ const measurePanel = useCallback(
156
+ (e: { nativeEvent: { layout: { width: number; height: number } } }) => {
157
+ const measured = isBottom ? e.nativeEvent.layout.height : e.nativeEvent.layout.width;
158
+ if (measured > 0) travel.value = measured;
159
+ },
160
+ [isBottom, travel],
161
+ );
162
+
163
+ const panelGeometry = useMemo<ViewStyle>(() => {
164
+ if (isBottom) {
165
+ return {
166
+ left: 0,
167
+ right: 0,
168
+ bottom: 0,
169
+ maxHeight: bottomMaxHeight,
170
+ borderTopLeftRadius: PANEL_RADIUS,
171
+ borderTopRightRadius: PANEL_RADIUS,
172
+ };
173
+ }
174
+ return {
175
+ top: insetTop,
176
+ bottom: insetBottom,
177
+ [side]: anchorInset,
178
+ width: sideWidth,
179
+ borderRadius: PANEL_RADIUS,
180
+ } as ViewStyle;
181
+ }, [isBottom, bottomMaxHeight, insetTop, insetBottom, side, anchorInset, sideWidth]);
182
+
183
+ if (!mounted) return null;
184
+
185
+ return (
186
+ <View
187
+ style={[styles.root, containerStyle]}
188
+ {...(containerClassName ? ({ className: containerClassName } as Record<string, string>) : {})}
189
+ pointerEvents="box-none"
190
+ >
191
+ <Animated.View style={[styles.backdrop, backdropAnimatedStyle]} pointerEvents="auto">
192
+ <Pressable
193
+ testID={RESPONSIVE_SHEET_BACKDROP_TESTID}
194
+ accessibilityRole="button"
195
+ accessibilityLabel={`Dismiss ${label}`}
196
+ onPress={handleBackdropPress}
197
+ disabled={!dismissOnBackdrop}
198
+ style={StyleSheet.absoluteFill}
199
+ />
200
+ </Animated.View>
201
+
202
+ <Animated.View
203
+ accessibilityViewIsModal
204
+ {...(panelClassName ? ({ className: panelClassName } as Record<string, string>) : {})}
205
+ onLayout={measurePanel}
206
+ style={[
207
+ styles.panel,
208
+ { backgroundColor: theme.colors.background, shadowColor: theme.colors.shadow },
209
+ panelGeometry,
210
+ panelAnimatedStyle,
211
+ panelStyle,
212
+ ]}
213
+ pointerEvents="auto"
214
+ >
215
+ {isBottom && showHandle ? (
216
+ <View style={styles.handleRow} pointerEvents="none">
217
+ <View style={[styles.handle, { backgroundColor: theme.colors.border }]} />
218
+ </View>
219
+ ) : null}
220
+ {children}
221
+ </Animated.View>
222
+ </View>
223
+ );
224
+ }
225
+
226
+ const styles = StyleSheet.create({
227
+ root: {
228
+ ...StyleSheet.absoluteFill,
229
+ zIndex: Z_INDEX.fullscreen,
230
+ },
231
+ backdrop: {
232
+ ...StyleSheet.absoluteFill,
233
+ backgroundColor: '#000',
234
+ },
235
+ panel: {
236
+ position: 'absolute',
237
+ overflow: 'hidden',
238
+ shadowOpacity: 0.18,
239
+ shadowRadius: 24,
240
+ shadowOffset: { width: 0, height: 8 },
241
+ elevation: 12,
242
+ },
243
+ handleRow: {
244
+ width: '100%',
245
+ alignItems: 'center',
246
+ paddingTop: 8,
247
+ paddingBottom: 4,
248
+ },
249
+ handle: {
250
+ width: HANDLE_WIDTH,
251
+ height: HANDLE_HEIGHT,
252
+ borderRadius: HANDLE_RADIUS,
253
+ opacity: 0.5,
254
+ },
255
+ });
256
+
257
+ export default ResponsiveSheet;
@@ -0,0 +1,54 @@
1
+ import React, { useState } from 'react';
2
+ import { View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import { ResponsiveSheet } from './index';
6
+ import type { ResponsiveSheetSide } from './types';
7
+ import { Button } from '../button';
8
+ import { Text } from '../typography';
9
+
10
+ const meta: Meta = {
11
+ title: 'Overlays/ResponsiveSheet',
12
+ };
13
+
14
+ export default meta;
15
+
16
+ type Story = StoryObj;
17
+
18
+ function Demo({ side }: { side: ResponsiveSheetSide }) {
19
+ const [open, setOpen] = useState(false);
20
+
21
+ return (
22
+ <View style={{ padding: 24, minHeight: 480 }}>
23
+ <Button onPress={() => setOpen(true)}>Open sheet ({side})</Button>
24
+ <ResponsiveSheet
25
+ open={open}
26
+ onClose={() => setOpen(false)}
27
+ side={side}
28
+ width={420}
29
+ inset={{ top: 16, bottom: 16, left: 16 }}
30
+ label="Demo sheet"
31
+ >
32
+ <View style={{ padding: 20, gap: 12 }}>
33
+ <Text style={{ fontSize: 20, fontWeight: '700' }}>Responsive sheet</Text>
34
+ <Text>
35
+ On a wide viewport this is an anchored side-sheet; below the
36
+ breakpoint it becomes a bottom-sheet with a drag handle. Resize the
37
+ preview to switch modes.
38
+ </Text>
39
+ <Button variant="secondary" onPress={() => setOpen(false)}>
40
+ Close
41
+ </Button>
42
+ </View>
43
+ </ResponsiveSheet>
44
+ </View>
45
+ );
46
+ }
47
+
48
+ export const SideLeft: Story = {
49
+ render: () => <Demo side="left" />,
50
+ };
51
+
52
+ export const SideRight: Story = {
53
+ render: () => <Demo side="right" />,
54
+ };