@oxyhq/bloom 0.1.9 → 0.1.11
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/bottom-sheet/index.js +418 -0
- package/lib/commonjs/bottom-sheet/index.js.map +1 -0
- package/lib/commonjs/index.js +10 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/theme/color-presets.js +48 -1
- package/lib/commonjs/theme/color-presets.js.map +1 -1
- package/lib/module/bottom-sheet/index.js +415 -0
- package/lib/module/bottom-sheet/index.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/theme/color-presets.js +48 -1
- package/lib/module/theme/color-presets.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/index.d.ts +30 -0
- package/lib/typescript/commonjs/bottom-sheet/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +2 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-presets.d.ts +1 -1
- package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/index.d.ts +30 -0
- package/lib/typescript/module/bottom-sheet/index.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +2 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-presets.d.ts +1 -1
- package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
- package/package.json +13 -1
- package/src/bottom-sheet/index.tsx +453 -0
- package/src/index.ts +4 -0
- package/src/theme/color-presets.ts +49 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { type ViewStyle, type StyleProp } from 'react-native';
|
|
3
|
+
export interface BottomSheetRef {
|
|
4
|
+
present: () => void;
|
|
5
|
+
dismiss: () => void;
|
|
6
|
+
close: () => void;
|
|
7
|
+
expand: () => void;
|
|
8
|
+
collapse: () => void;
|
|
9
|
+
scrollTo: (y: number, animated?: boolean) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface BottomSheetProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
onDismiss?: () => void;
|
|
14
|
+
enablePanDownToClose?: boolean;
|
|
15
|
+
backgroundComponent?: (props: {
|
|
16
|
+
style?: StyleProp<ViewStyle>;
|
|
17
|
+
}) => React.ReactElement | null;
|
|
18
|
+
backdropComponent?: (props: {
|
|
19
|
+
style?: StyleProp<ViewStyle>;
|
|
20
|
+
onPress?: () => void;
|
|
21
|
+
}) => React.ReactElement | null;
|
|
22
|
+
style?: StyleProp<ViewStyle>;
|
|
23
|
+
enableHandlePanningGesture?: boolean;
|
|
24
|
+
onDismissAttempt?: () => boolean;
|
|
25
|
+
detached?: boolean;
|
|
26
|
+
}
|
|
27
|
+
declare const BottomSheet: React.ForwardRefExoticComponent<BottomSheetProps & React.RefAttributes<BottomSheetRef>>;
|
|
28
|
+
export default BottomSheet;
|
|
29
|
+
export { BottomSheet };
|
|
30
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/bottom-sheet/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAOH,KAAK,SAAS,EACd,KAAK,SAAS,EACjB,MAAM,cAAc,CAAC;AA+BtB,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;KAAE,KAAK,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;IAC7F,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;IACjH,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,QAAA,MAAM,WAAW,yFA6Sf,CAAC;AAuFH,eAAe,WAAW,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,CAAC"}
|
|
@@ -32,6 +32,8 @@ export { IconCircle } from './icon-circle';
|
|
|
32
32
|
export * as TextField from './text-field';
|
|
33
33
|
export * as SegmentedControl from './segmented-control';
|
|
34
34
|
export { SearchInput } from './search-input';
|
|
35
|
+
export { BottomSheet } from './bottom-sheet';
|
|
36
|
+
export type { BottomSheetRef, BottomSheetProps } from './bottom-sheet';
|
|
35
37
|
export * as Admonition from './admonition';
|
|
36
38
|
export * as Menu from './menu';
|
|
37
39
|
export * as Tooltip from './tooltip';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AACA,cAAc,SAAS,CAAC;AAGxB,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1C,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC7D,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAGhF,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAG9D,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,KAAK,KAAK,IAAI,SAAS,EAAE,KAAK,IAAI,SAAS,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAG3E,cAAc,UAAU,CAAC;AACzB,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AACnC,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AACnC,cAAc,UAAU,CAAC;AACzB,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,OAAO,KAAK,WAAW,MAAM,gBAAgB,CAAC;AAC9C,cAAc,UAAU,CAAC;AACzB,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AAGjC,OAAO,KAAK,UAAU,MAAM,cAAc,CAAC;AAG3C,OAAO,KAAK,QAAQ,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,QAAQ,CAAC;AAC/B,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAG3C,OAAO,KAAK,SAAS,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,gBAAgB,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,KAAK,UAAU,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,IAAI,MAAM,QAAQ,CAAC;AAC/B,OAAO,KAAK,OAAO,MAAM,WAAW,CAAC;AACrC,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AACnC,OAAO,KAAK,WAAW,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AACA,cAAc,SAAS,CAAC;AAGxB,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1C,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC7D,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAC1C,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAGhF,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAG9D,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,KAAK,KAAK,IAAI,SAAS,EAAE,KAAK,IAAI,SAAS,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAG3E,cAAc,UAAU,CAAC;AACzB,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AACnC,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AACnC,cAAc,UAAU,CAAC;AACzB,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,OAAO,KAAK,WAAW,MAAM,gBAAgB,CAAC;AAC9C,cAAc,UAAU,CAAC;AACzB,OAAO,KAAK,KAAK,MAAM,SAAS,CAAC;AAGjC,OAAO,KAAK,UAAU,MAAM,cAAc,CAAC;AAG3C,OAAO,KAAK,QAAQ,MAAM,YAAY,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,QAAQ,CAAC;AAC/B,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAG3C,OAAO,KAAK,SAAS,MAAM,cAAc,CAAC;AAC1C,OAAO,KAAK,gBAAgB,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGvE,OAAO,KAAK,UAAU,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,IAAI,MAAM,QAAQ,CAAC;AAC/B,OAAO,KAAK,OAAO,MAAM,WAAW,CAAC;AACrC,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AACnC,OAAO,KAAK,WAAW,MAAM,gBAAgB,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type AppColorName = 'teal' | 'blue' | 'green' | 'amber' | 'red' | 'purple' | 'pink' | 'sky' | 'orange' | 'mint';
|
|
1
|
+
export type AppColorName = 'teal' | 'blue' | 'green' | 'amber' | 'red' | 'purple' | 'pink' | 'sky' | 'orange' | 'mint' | 'oxy';
|
|
2
2
|
export interface AppColorPreset {
|
|
3
3
|
name: AppColorName;
|
|
4
4
|
hex: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"color-presets.d.ts","sourceRoot":"","sources":["../../../../src/theme/color-presets.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"color-presets.d.ts","sourceRoot":"","sources":["../../../../src/theme/color-presets.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAC;AAE/H,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAED,eAAO,MAAM,eAAe,EAAE,YAAY,EAAyF,CAAC;AAEpI,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAYzD,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAE3D;AAED,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,YAAY,EAAE,cAAc,CAqgBlE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxyhq/bloom",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -364,6 +364,17 @@
|
|
|
364
364
|
"default": "./lib/commonjs/select/index.js"
|
|
365
365
|
}
|
|
366
366
|
},
|
|
367
|
+
"./bottom-sheet": {
|
|
368
|
+
"react-native": "./src/bottom-sheet/index.tsx",
|
|
369
|
+
"import": {
|
|
370
|
+
"types": "./lib/typescript/module/bottom-sheet/index.d.ts",
|
|
371
|
+
"default": "./lib/module/bottom-sheet/index.js"
|
|
372
|
+
},
|
|
373
|
+
"require": {
|
|
374
|
+
"types": "./lib/typescript/commonjs/bottom-sheet/index.d.ts",
|
|
375
|
+
"default": "./lib/commonjs/bottom-sheet/index.js"
|
|
376
|
+
}
|
|
377
|
+
},
|
|
367
378
|
"./context-menu": {
|
|
368
379
|
"react-native": "./src/context-menu/index.tsx",
|
|
369
380
|
"import": {
|
|
@@ -409,6 +420,7 @@
|
|
|
409
420
|
"tooltip",
|
|
410
421
|
"select",
|
|
411
422
|
"context-menu",
|
|
423
|
+
"bottom-sheet",
|
|
412
424
|
"ai"
|
|
413
425
|
],
|
|
414
426
|
"repository": {
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { forwardRef, useImperativeHandle, useRef, useEffect, useState, useCallback, useMemo } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
View,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Modal,
|
|
7
|
+
Pressable,
|
|
8
|
+
Dimensions,
|
|
9
|
+
Platform,
|
|
10
|
+
type ViewStyle,
|
|
11
|
+
type StyleProp,
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
14
|
+
import Animated, {
|
|
15
|
+
interpolate,
|
|
16
|
+
runOnJS,
|
|
17
|
+
useAnimatedScrollHandler,
|
|
18
|
+
useAnimatedStyle,
|
|
19
|
+
useSharedValue,
|
|
20
|
+
withSpring,
|
|
21
|
+
withTiming,
|
|
22
|
+
} from 'react-native-reanimated';
|
|
23
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
24
|
+
import { useTheme } from '../theme/use-theme';
|
|
25
|
+
|
|
26
|
+
// Optional dependency — keyboard handling is skipped if not installed
|
|
27
|
+
let useKeyboardHandler: ((handlers: Record<string, (e: { height: number }) => void>, deps: unknown[]) => void) | undefined;
|
|
28
|
+
try {
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
|
+
useKeyboardHandler = require('react-native-keyboard-controller').useKeyboardHandler;
|
|
31
|
+
} catch {
|
|
32
|
+
// react-native-keyboard-controller not available
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
|
|
36
|
+
|
|
37
|
+
const SPRING_CONFIG = {
|
|
38
|
+
damping: 25,
|
|
39
|
+
stiffness: 300,
|
|
40
|
+
mass: 0.8,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface BottomSheetRef {
|
|
44
|
+
present: () => void;
|
|
45
|
+
dismiss: () => void;
|
|
46
|
+
close: () => void;
|
|
47
|
+
expand: () => void;
|
|
48
|
+
collapse: () => void;
|
|
49
|
+
scrollTo: (y: number, animated?: boolean) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface BottomSheetProps {
|
|
53
|
+
children: React.ReactNode;
|
|
54
|
+
onDismiss?: () => void;
|
|
55
|
+
enablePanDownToClose?: boolean;
|
|
56
|
+
backgroundComponent?: (props: { style?: StyleProp<ViewStyle> }) => React.ReactElement | null;
|
|
57
|
+
backdropComponent?: (props: { style?: StyleProp<ViewStyle>; onPress?: () => void }) => React.ReactElement | null;
|
|
58
|
+
style?: StyleProp<ViewStyle>;
|
|
59
|
+
enableHandlePanningGesture?: boolean;
|
|
60
|
+
onDismissAttempt?: () => boolean;
|
|
61
|
+
detached?: boolean; // If true, shows with margins and rounded corners. If false, full width with rounded top only.
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef<BottomSheetRef>) => {
|
|
65
|
+
const {
|
|
66
|
+
children,
|
|
67
|
+
onDismiss,
|
|
68
|
+
enablePanDownToClose = true,
|
|
69
|
+
backgroundComponent,
|
|
70
|
+
backdropComponent,
|
|
71
|
+
style,
|
|
72
|
+
enableHandlePanningGesture = true,
|
|
73
|
+
onDismissAttempt,
|
|
74
|
+
detached = false,
|
|
75
|
+
} = props;
|
|
76
|
+
|
|
77
|
+
const insets = useSafeAreaInsets();
|
|
78
|
+
const { colors } = useTheme();
|
|
79
|
+
const [visible, setVisible] = useState(false);
|
|
80
|
+
const [rendered, setRendered] = useState(false); // keep mounted for exit animation
|
|
81
|
+
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
82
|
+
const hasClosedRef = useRef(false);
|
|
83
|
+
const scrollViewRef = useRef<Animated.ScrollView>(null);
|
|
84
|
+
|
|
85
|
+
const translateY = useSharedValue(SCREEN_HEIGHT);
|
|
86
|
+
const opacity = useSharedValue(0);
|
|
87
|
+
const scrollOffsetY = useSharedValue(0);
|
|
88
|
+
const isScrollAtTop = useSharedValue(true);
|
|
89
|
+
const allowPanClose = useSharedValue(true);
|
|
90
|
+
const keyboardHeight = useSharedValue(0);
|
|
91
|
+
const context = useSharedValue({ y: 0 });
|
|
92
|
+
|
|
93
|
+
if (useKeyboardHandler) {
|
|
94
|
+
useKeyboardHandler({
|
|
95
|
+
onMove: (e) => {
|
|
96
|
+
'worklet';
|
|
97
|
+
keyboardHeight.value = e.height;
|
|
98
|
+
},
|
|
99
|
+
onEnd: (e) => {
|
|
100
|
+
'worklet';
|
|
101
|
+
keyboardHeight.value = e.height;
|
|
102
|
+
},
|
|
103
|
+
}, []);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Dismiss callbacks
|
|
107
|
+
const safeClose = () => {
|
|
108
|
+
if (onDismissAttempt?.()) {
|
|
109
|
+
onDismiss?.();
|
|
110
|
+
} else if (!onDismissAttempt) {
|
|
111
|
+
onDismiss?.();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const finishClose = useCallback(() => {
|
|
116
|
+
if (hasClosedRef.current) return;
|
|
117
|
+
hasClosedRef.current = true;
|
|
118
|
+
safeClose();
|
|
119
|
+
setRendered(false);
|
|
120
|
+
}, [safeClose]);
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (visible) {
|
|
124
|
+
if (closeTimeoutRef.current) {
|
|
125
|
+
clearTimeout(closeTimeoutRef.current);
|
|
126
|
+
closeTimeoutRef.current = null;
|
|
127
|
+
}
|
|
128
|
+
hasClosedRef.current = false;
|
|
129
|
+
opacity.value = withTiming(1, { duration: 250 });
|
|
130
|
+
translateY.value = withSpring(0, SPRING_CONFIG);
|
|
131
|
+
} else if (rendered) {
|
|
132
|
+
opacity.value = withTiming(0, { duration: 250 }, (finished) => {
|
|
133
|
+
if (finished) {
|
|
134
|
+
runOnJS(finishClose)();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
translateY.value = withSpring(SCREEN_HEIGHT, { ...SPRING_CONFIG, stiffness: 250 });
|
|
138
|
+
|
|
139
|
+
// Fallback timer to ensure close completes (especially on web)
|
|
140
|
+
if (closeTimeoutRef.current) {
|
|
141
|
+
clearTimeout(closeTimeoutRef.current);
|
|
142
|
+
}
|
|
143
|
+
closeTimeoutRef.current = setTimeout(() => {
|
|
144
|
+
finishClose();
|
|
145
|
+
closeTimeoutRef.current = null;
|
|
146
|
+
}, 300);
|
|
147
|
+
}
|
|
148
|
+
}, [visible, rendered, finishClose]);
|
|
149
|
+
|
|
150
|
+
// Clear pending timeout on unmount
|
|
151
|
+
useEffect(() => () => {
|
|
152
|
+
if (closeTimeoutRef.current) {
|
|
153
|
+
clearTimeout(closeTimeoutRef.current);
|
|
154
|
+
closeTimeoutRef.current = null;
|
|
155
|
+
}
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
158
|
+
// Apply web scrollbar styles when colors change
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (Platform.OS === 'web') {
|
|
161
|
+
createWebScrollbarStyle(colors.border);
|
|
162
|
+
}
|
|
163
|
+
}, [colors.border]);
|
|
164
|
+
|
|
165
|
+
const present = useCallback(() => {
|
|
166
|
+
setRendered(true);
|
|
167
|
+
setVisible(true);
|
|
168
|
+
}, []);
|
|
169
|
+
const dismiss = useCallback(() => {
|
|
170
|
+
setVisible(false);
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
const scrollTo = useCallback((y: number, animated = true) => {
|
|
174
|
+
scrollViewRef.current?.scrollTo({ y, animated });
|
|
175
|
+
}, []);
|
|
176
|
+
|
|
177
|
+
useImperativeHandle(ref, () => ({
|
|
178
|
+
present,
|
|
179
|
+
dismiss,
|
|
180
|
+
close: dismiss,
|
|
181
|
+
expand: present,
|
|
182
|
+
collapse: dismiss,
|
|
183
|
+
scrollTo,
|
|
184
|
+
}), [present, dismiss, scrollTo]);
|
|
185
|
+
|
|
186
|
+
const nativeGesture = useMemo(() => Gesture.Native(), []);
|
|
187
|
+
|
|
188
|
+
const panGesture = Gesture.Pan()
|
|
189
|
+
.enabled(enablePanDownToClose)
|
|
190
|
+
.simultaneousWithExternalGesture(nativeGesture)
|
|
191
|
+
.onStart(() => {
|
|
192
|
+
'worklet';
|
|
193
|
+
context.value = { y: translateY.value };
|
|
194
|
+
allowPanClose.value = scrollOffsetY.value <= 8;
|
|
195
|
+
})
|
|
196
|
+
.onUpdate((event) => {
|
|
197
|
+
'worklet';
|
|
198
|
+
if (!allowPanClose.value) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const newTranslateY = context.value.y + event.translationY;
|
|
202
|
+
// If user is scrolling down while content isn't at (or near) the top, let ScrollView handle it
|
|
203
|
+
const atTopOrNearTop = scrollOffsetY.value <= 8; // slightly larger tolerance for smoother handoff
|
|
204
|
+
if (event.translationY > 0 && !atTopOrNearTop) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (newTranslateY >= 0) {
|
|
208
|
+
translateY.value = newTranslateY;
|
|
209
|
+
} else if (detached) {
|
|
210
|
+
// Only allow overdrag (pulling up beyond top) when detached
|
|
211
|
+
translateY.value = newTranslateY * 0.3;
|
|
212
|
+
} else {
|
|
213
|
+
// In normal mode, prevent overdrag - clamp to 0
|
|
214
|
+
translateY.value = 0;
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
.onEnd((event) => {
|
|
218
|
+
'worklet';
|
|
219
|
+
if (!allowPanClose.value) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const velocity = event.velocityY;
|
|
223
|
+
const distance = translateY.value;
|
|
224
|
+
// Require a deeper pull to close (more like native bottom sheets)
|
|
225
|
+
const closeThreshold = Math.max(140, SCREEN_HEIGHT * 0.25);
|
|
226
|
+
const fastSwipeThreshold = 900;
|
|
227
|
+
const shouldClose =
|
|
228
|
+
velocity > fastSwipeThreshold ||
|
|
229
|
+
(distance > closeThreshold && velocity > -300);
|
|
230
|
+
|
|
231
|
+
if (shouldClose) {
|
|
232
|
+
translateY.value = withSpring(SCREEN_HEIGHT, {
|
|
233
|
+
...SPRING_CONFIG,
|
|
234
|
+
velocity: velocity,
|
|
235
|
+
});
|
|
236
|
+
opacity.value = withTiming(0, { duration: 250 }, (finished) => {
|
|
237
|
+
if (finished) {
|
|
238
|
+
runOnJS(finishClose)();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
translateY.value = withSpring(0, {
|
|
243
|
+
...SPRING_CONFIG,
|
|
244
|
+
velocity: velocity,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const backdropStyle = useAnimatedStyle(() => ({
|
|
250
|
+
opacity: opacity.value,
|
|
251
|
+
}));
|
|
252
|
+
|
|
253
|
+
const sheetStyle = useAnimatedStyle(() => {
|
|
254
|
+
const scale = interpolate(translateY.value, [0, SCREEN_HEIGHT], [1, 0.95]);
|
|
255
|
+
return {
|
|
256
|
+
transform: [
|
|
257
|
+
{ translateY: translateY.value - keyboardHeight.value },
|
|
258
|
+
{ scale },
|
|
259
|
+
],
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const sheetHeightStyle = useAnimatedStyle(() => ({
|
|
264
|
+
maxHeight: SCREEN_HEIGHT - keyboardHeight.value - insets.top - (detached ? insets.bottom + 16 : 0),
|
|
265
|
+
}), [insets.top, insets.bottom, detached]);
|
|
266
|
+
|
|
267
|
+
const sheetMarginStyle = useAnimatedStyle(() => {
|
|
268
|
+
// Only add margin when detached, otherwise extend behind safe area
|
|
269
|
+
if (detached) {
|
|
270
|
+
return {
|
|
271
|
+
marginBottom: keyboardHeight.value > 0 ? 16 : insets.bottom + 16,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
marginBottom: 0,
|
|
276
|
+
};
|
|
277
|
+
}, [insets.bottom, detached]);
|
|
278
|
+
|
|
279
|
+
const handleBackdropPress = useCallback(() => {
|
|
280
|
+
// Always animate close on backdrop press
|
|
281
|
+
if (onDismissAttempt && !onDismissAttempt()) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
dismiss();
|
|
285
|
+
}, [onDismissAttempt, dismiss]);
|
|
286
|
+
|
|
287
|
+
const scrollHandler = useAnimatedScrollHandler({
|
|
288
|
+
onScroll: (event) => {
|
|
289
|
+
scrollOffsetY.value = event.contentOffset.y;
|
|
290
|
+
isScrollAtTop.value = event.contentOffset.y <= 0;
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const dynamicStyles = useMemo(() => {
|
|
295
|
+
const isDark = colors.background === '#000000';
|
|
296
|
+
return StyleSheet.create({
|
|
297
|
+
handle: {
|
|
298
|
+
...styles.handle,
|
|
299
|
+
backgroundColor: isDark ? '#444' : '#C7C7CC',
|
|
300
|
+
},
|
|
301
|
+
sheet: {
|
|
302
|
+
...styles.sheet,
|
|
303
|
+
backgroundColor: colors.background,
|
|
304
|
+
...(detached ? styles.sheetDetached : styles.sheetNormal),
|
|
305
|
+
},
|
|
306
|
+
scrollContent: {
|
|
307
|
+
...styles.scrollContent,
|
|
308
|
+
// In normal mode, don't add padding here - screens handle their own padding
|
|
309
|
+
// The sheet extends behind safe area, and screens add padding as needed
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
}, [colors.background, detached, insets.bottom]);
|
|
313
|
+
|
|
314
|
+
if (!rendered) return null;
|
|
315
|
+
|
|
316
|
+
return (
|
|
317
|
+
<Modal visible={rendered} transparent animationType="none" statusBarTranslucent onRequestClose={dismiss}>
|
|
318
|
+
<View style={StyleSheet.absoluteFill}>
|
|
319
|
+
<Animated.View style={[styles.backdrop, backdropStyle]}>
|
|
320
|
+
{backdropComponent ? (
|
|
321
|
+
backdropComponent({ onPress: handleBackdropPress })
|
|
322
|
+
) : (
|
|
323
|
+
<Pressable style={styles.backdropTouchable} onPress={handleBackdropPress}>
|
|
324
|
+
<View style={StyleSheet.absoluteFill} />
|
|
325
|
+
</Pressable>
|
|
326
|
+
)}
|
|
327
|
+
</Animated.View>
|
|
328
|
+
|
|
329
|
+
<GestureDetector gesture={panGesture}>
|
|
330
|
+
<Animated.View style={[dynamicStyles.sheet, sheetMarginStyle, sheetStyle, sheetHeightStyle]}>
|
|
331
|
+
{backgroundComponent?.({ style: styles.background })}
|
|
332
|
+
|
|
333
|
+
<View style={dynamicStyles.handle} />
|
|
334
|
+
|
|
335
|
+
<GestureDetector gesture={nativeGesture}>
|
|
336
|
+
<Animated.ScrollView
|
|
337
|
+
ref={scrollViewRef}
|
|
338
|
+
style={[
|
|
339
|
+
styles.scrollView,
|
|
340
|
+
Platform.OS === 'web' && ({
|
|
341
|
+
scrollbarWidth: 'thin',
|
|
342
|
+
scrollbarColor: `${colors.border} transparent`,
|
|
343
|
+
} as ViewStyle),
|
|
344
|
+
]}
|
|
345
|
+
contentContainerStyle={dynamicStyles.scrollContent}
|
|
346
|
+
showsVerticalScrollIndicator={false}
|
|
347
|
+
keyboardShouldPersistTaps="handled"
|
|
348
|
+
onScroll={scrollHandler}
|
|
349
|
+
scrollEventThrottle={16}
|
|
350
|
+
{...(Platform.OS === 'web' ? { className: 'bottom-sheet-scrollview' } : undefined)}
|
|
351
|
+
onLayout={() => {
|
|
352
|
+
if (Platform.OS === 'web') {
|
|
353
|
+
createWebScrollbarStyle(colors.border);
|
|
354
|
+
}
|
|
355
|
+
}}
|
|
356
|
+
>
|
|
357
|
+
{children}
|
|
358
|
+
</Animated.ScrollView>
|
|
359
|
+
</GestureDetector>
|
|
360
|
+
</Animated.View>
|
|
361
|
+
</GestureDetector>
|
|
362
|
+
</View>
|
|
363
|
+
</Modal>
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
BottomSheet.displayName = 'BottomSheet';
|
|
368
|
+
|
|
369
|
+
const styles = StyleSheet.create({
|
|
370
|
+
backdrop: {
|
|
371
|
+
flex: 1,
|
|
372
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
373
|
+
},
|
|
374
|
+
backdropTouchable: {
|
|
375
|
+
flex: 1,
|
|
376
|
+
},
|
|
377
|
+
sheet: {
|
|
378
|
+
position: 'absolute',
|
|
379
|
+
bottom: 0,
|
|
380
|
+
overflow: 'hidden',
|
|
381
|
+
maxWidth: 800,
|
|
382
|
+
alignSelf: 'center',
|
|
383
|
+
marginHorizontal: 'auto',
|
|
384
|
+
},
|
|
385
|
+
sheetDetached: {
|
|
386
|
+
left: 16,
|
|
387
|
+
right: 16,
|
|
388
|
+
borderRadius: 24,
|
|
389
|
+
},
|
|
390
|
+
sheetNormal: {
|
|
391
|
+
left: 0,
|
|
392
|
+
right: 0,
|
|
393
|
+
borderTopLeftRadius: 24,
|
|
394
|
+
borderTopRightRadius: 24,
|
|
395
|
+
},
|
|
396
|
+
handle: {
|
|
397
|
+
position: 'absolute',
|
|
398
|
+
top: 10,
|
|
399
|
+
left: '50%',
|
|
400
|
+
marginLeft: -18,
|
|
401
|
+
width: 36,
|
|
402
|
+
height: 5,
|
|
403
|
+
borderRadius: 3,
|
|
404
|
+
zIndex: 100,
|
|
405
|
+
},
|
|
406
|
+
background: {
|
|
407
|
+
...StyleSheet.absoluteFillObject,
|
|
408
|
+
},
|
|
409
|
+
scrollView: {
|
|
410
|
+
flex: 1,
|
|
411
|
+
},
|
|
412
|
+
scrollContent: {
|
|
413
|
+
flexGrow: 1,
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Create web scrollbar styles dynamically based on theme
|
|
418
|
+
const createWebScrollbarStyle = (borderColor: string) => {
|
|
419
|
+
if (Platform.OS !== 'web' || typeof document === 'undefined') return;
|
|
420
|
+
|
|
421
|
+
const styleId = 'bottom-sheet-scrollbar-style';
|
|
422
|
+
let styleElement = document.getElementById(styleId) as HTMLStyleElement;
|
|
423
|
+
|
|
424
|
+
if (!styleElement) {
|
|
425
|
+
styleElement = document.createElement('style');
|
|
426
|
+
styleElement.id = styleId;
|
|
427
|
+
document.head.appendChild(styleElement);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Use theme border color for scrollbar
|
|
431
|
+
const scrollbarColor = borderColor;
|
|
432
|
+
const scrollbarHoverColor = borderColor === '#E5E5EA' ? '#C7C7CC' : '#555';
|
|
433
|
+
|
|
434
|
+
styleElement.textContent = `
|
|
435
|
+
.bottom-sheet-scrollview::-webkit-scrollbar {
|
|
436
|
+
width: 6px;
|
|
437
|
+
}
|
|
438
|
+
.bottom-sheet-scrollview::-webkit-scrollbar-track {
|
|
439
|
+
background: transparent;
|
|
440
|
+
border-radius: 10px;
|
|
441
|
+
}
|
|
442
|
+
.bottom-sheet-scrollview::-webkit-scrollbar-thumb {
|
|
443
|
+
background: ${scrollbarColor};
|
|
444
|
+
border-radius: 10px;
|
|
445
|
+
}
|
|
446
|
+
.bottom-sheet-scrollview::-webkit-scrollbar-thumb:hover {
|
|
447
|
+
background: ${scrollbarHoverColor};
|
|
448
|
+
}
|
|
449
|
+
`;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
export default BottomSheet;
|
|
453
|
+
export { BottomSheet };
|
package/src/index.ts
CHANGED
|
@@ -48,6 +48,10 @@ export * as TextField from './text-field';
|
|
|
48
48
|
export * as SegmentedControl from './segmented-control';
|
|
49
49
|
export { SearchInput } from './search-input';
|
|
50
50
|
|
|
51
|
+
// Bottom sheet
|
|
52
|
+
export { BottomSheet } from './bottom-sheet';
|
|
53
|
+
export type { BottomSheetRef, BottomSheetProps } from './bottom-sheet';
|
|
54
|
+
|
|
51
55
|
// Overlay components
|
|
52
56
|
export * as Admonition from './admonition';
|
|
53
57
|
export * as Menu from './menu';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type AppColorName = 'teal' | 'blue' | 'green' | 'amber' | 'red' | 'purple' | 'pink' | 'sky' | 'orange' | 'mint';
|
|
1
|
+
export type AppColorName = 'teal' | 'blue' | 'green' | 'amber' | 'red' | 'purple' | 'pink' | 'sky' | 'orange' | 'mint' | 'oxy';
|
|
2
2
|
|
|
3
3
|
export interface AppColorPreset {
|
|
4
4
|
name: AppColorName;
|
|
@@ -20,6 +20,7 @@ export const HEX_TO_APP_COLOR: Record<string, AppColorName> = {
|
|
|
20
20
|
'#0ea5e9': 'sky',
|
|
21
21
|
'#f97316': 'orange',
|
|
22
22
|
'#14b8a6': 'mint',
|
|
23
|
+
'#c46ede': 'oxy',
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
export function hexToAppColorName(hex: string): AppColorName {
|
|
@@ -496,4 +497,51 @@ export const APP_COLOR_PRESETS: Record<AppColorName, AppColorPreset> = {
|
|
|
496
497
|
'--sidebar': '173 30% 8%',
|
|
497
498
|
},
|
|
498
499
|
},
|
|
500
|
+
|
|
501
|
+
oxy: {
|
|
502
|
+
name: 'oxy',
|
|
503
|
+
hex: '#c46ede',
|
|
504
|
+
light: {
|
|
505
|
+
'--background': '0 0% 99%',
|
|
506
|
+
'--foreground': '0 0% 12%',
|
|
507
|
+
'--surface': '277 10% 97%',
|
|
508
|
+
'--surface-foreground': '0 0% 12%',
|
|
509
|
+
'--popover': '0 0% 100%',
|
|
510
|
+
'--popover-foreground': '0 0% 12%',
|
|
511
|
+
'--primary': '277 66% 56%',
|
|
512
|
+
'--primary-foreground': '0 0% 100%',
|
|
513
|
+
'--secondary': '277 8% 95%',
|
|
514
|
+
'--secondary-foreground': '0 0% 12%',
|
|
515
|
+
'--muted': '277 8% 95%',
|
|
516
|
+
'--muted-foreground': '277 5% 42%',
|
|
517
|
+
'--accent': '277 8% 95%',
|
|
518
|
+
'--accent-foreground': '0 0% 12%',
|
|
519
|
+
'--destructive': '0 84% 60%',
|
|
520
|
+
'--border': '277 8% 88%',
|
|
521
|
+
'--input': '277 8% 88%',
|
|
522
|
+
'--ring': '277 66% 56%',
|
|
523
|
+
'--sidebar': '277 10% 97%',
|
|
524
|
+
},
|
|
525
|
+
dark: {
|
|
526
|
+
'--background': '277 50% 5%',
|
|
527
|
+
'--foreground': '0 0% 93%',
|
|
528
|
+
'--surface': '277 20% 18%',
|
|
529
|
+
'--surface-foreground': '0 0% 93%',
|
|
530
|
+
'--popover': '277 20% 18%',
|
|
531
|
+
'--popover-foreground': '0 0% 93%',
|
|
532
|
+
'--primary': '277 66% 56%',
|
|
533
|
+
'--primary-foreground': '0 0% 100%',
|
|
534
|
+
'--secondary': '277 20% 18%',
|
|
535
|
+
'--secondary-foreground': '0 0% 93%',
|
|
536
|
+
'--muted': '277 18% 20%',
|
|
537
|
+
'--muted-foreground': '0 0% 70%',
|
|
538
|
+
'--accent': '277 18% 20%',
|
|
539
|
+
'--accent-foreground': '0 0% 93%',
|
|
540
|
+
'--destructive': '0 84% 60%',
|
|
541
|
+
'--border': '277 12% 20%',
|
|
542
|
+
'--input': '277 10% 23%',
|
|
543
|
+
'--ring': '277 66% 56%',
|
|
544
|
+
'--sidebar': '277 30% 8%',
|
|
545
|
+
},
|
|
546
|
+
},
|
|
499
547
|
};
|