@oxyhq/bloom 0.1.10 → 0.1.12

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 (37) hide show
  1. package/lib/commonjs/bottom-sheet/index.js +418 -0
  2. package/lib/commonjs/bottom-sheet/index.js.map +1 -0
  3. package/lib/commonjs/index.js +10 -1
  4. package/lib/commonjs/index.js.map +1 -1
  5. package/lib/commonjs/theme/color-presets.js +1 -4
  6. package/lib/commonjs/theme/color-presets.js.map +1 -1
  7. package/lib/commonjs/theme/index.js +0 -6
  8. package/lib/commonjs/theme/index.js.map +1 -1
  9. package/lib/module/bottom-sheet/index.js +415 -0
  10. package/lib/module/bottom-sheet/index.js.map +1 -0
  11. package/lib/module/index.js +2 -0
  12. package/lib/module/index.js.map +1 -1
  13. package/lib/module/theme/color-presets.js +0 -3
  14. package/lib/module/theme/color-presets.js.map +1 -1
  15. package/lib/module/theme/index.js +1 -1
  16. package/lib/module/theme/index.js.map +1 -1
  17. package/lib/typescript/commonjs/bottom-sheet/index.d.ts +30 -0
  18. package/lib/typescript/commonjs/bottom-sheet/index.d.ts.map +1 -0
  19. package/lib/typescript/commonjs/index.d.ts +2 -0
  20. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  21. package/lib/typescript/commonjs/theme/color-presets.d.ts +0 -2
  22. package/lib/typescript/commonjs/theme/color-presets.d.ts.map +1 -1
  23. package/lib/typescript/commonjs/theme/index.d.ts +1 -1
  24. package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
  25. package/lib/typescript/module/bottom-sheet/index.d.ts +30 -0
  26. package/lib/typescript/module/bottom-sheet/index.d.ts.map +1 -0
  27. package/lib/typescript/module/index.d.ts +2 -0
  28. package/lib/typescript/module/index.d.ts.map +1 -1
  29. package/lib/typescript/module/theme/color-presets.d.ts +0 -2
  30. package/lib/typescript/module/theme/color-presets.d.ts.map +1 -1
  31. package/lib/typescript/module/theme/index.d.ts +1 -1
  32. package/lib/typescript/module/theme/index.d.ts.map +1 -1
  33. package/package.json +13 -1
  34. package/src/bottom-sheet/index.tsx +453 -0
  35. package/src/index.ts +4 -0
  36. package/src/theme/color-presets.ts +0 -3
  37. package/src/theme/index.ts +1 -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;AAiCtB,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,yFA2Sf,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"}
@@ -6,8 +6,6 @@ export interface AppColorPreset {
6
6
  dark: Record<string, string>;
7
7
  }
8
8
  export declare const APP_COLOR_NAMES: AppColorName[];
9
- /** Premium-exclusive color names, not shown in the standard picker */
10
- export declare const PREMIUM_COLOR_NAMES: AppColorName[];
11
9
  export declare const HEX_TO_APP_COLOR: Record<string, AppColorName>;
12
10
  export declare function hexToAppColorName(hex: string): AppColorName;
13
11
  export declare const APP_COLOR_PRESETS: Record<AppColorName, AppColorPreset>;
@@ -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,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,sEAAsE;AACtE,eAAO,MAAM,mBAAmB,EAAE,YAAY,EAAY,CAAC;AAE3D,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"}
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"}
@@ -3,7 +3,7 @@ export type { BloomThemeProviderProps, BloomThemeContextValue } from './BloomThe
3
3
  export { useTheme, useThemeColor, useBloomTheme } from './use-theme';
4
4
  export type { Theme, ThemeColors, ThemeMode } from './types';
5
5
  export type { AppColorName, AppColorPreset } from './color-presets';
6
- export { APP_COLOR_NAMES, APP_COLOR_PRESETS, PREMIUM_COLOR_NAMES, HEX_TO_APP_COLOR, hexToAppColorName } from './color-presets';
6
+ export { APP_COLOR_NAMES, APP_COLOR_PRESETS, HEX_TO_APP_COLOR, hexToAppColorName } from './color-presets';
7
7
  export { applyDarkClass } from './apply-dark-class';
8
8
  export { setColorSchemeSafe } from './set-color-scheme-safe';
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/theme/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC5F,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACrE,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC7D,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC/H,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/theme/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,YAAY,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC5F,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACrE,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC7D,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC1G,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
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 — uses a no-op fallback if not installed
27
+ // The hook is always called (Rules of Hooks) but does nothing without the library
28
+ const noopKeyboardHandler = (_handlers: Record<string, (e: { height: number }) => void>, _deps: unknown[]) => {};
29
+ let useKeyboardHandler: (handlers: Record<string, (e: { height: number }) => void>, deps: unknown[]) => void = noopKeyboardHandler;
30
+ try {
31
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
32
+ useKeyboardHandler = require('react-native-keyboard-controller').useKeyboardHandler;
33
+ } catch {
34
+ // react-native-keyboard-controller not available
35
+ }
36
+
37
+ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
38
+
39
+ const SPRING_CONFIG = {
40
+ damping: 25,
41
+ stiffness: 300,
42
+ mass: 0.8,
43
+ };
44
+
45
+ export interface BottomSheetRef {
46
+ present: () => void;
47
+ dismiss: () => void;
48
+ close: () => void;
49
+ expand: () => void;
50
+ collapse: () => void;
51
+ scrollTo: (y: number, animated?: boolean) => void;
52
+ }
53
+
54
+ export interface BottomSheetProps {
55
+ children: React.ReactNode;
56
+ onDismiss?: () => void;
57
+ enablePanDownToClose?: boolean;
58
+ backgroundComponent?: (props: { style?: StyleProp<ViewStyle> }) => React.ReactElement | null;
59
+ backdropComponent?: (props: { style?: StyleProp<ViewStyle>; onPress?: () => void }) => React.ReactElement | null;
60
+ style?: StyleProp<ViewStyle>;
61
+ enableHandlePanningGesture?: boolean;
62
+ onDismissAttempt?: () => boolean;
63
+ detached?: boolean; // If true, shows with margins and rounded corners. If false, full width with rounded top only.
64
+ }
65
+
66
+ const BottomSheet = forwardRef((props: BottomSheetProps, ref: React.ForwardedRef<BottomSheetRef>) => {
67
+ const {
68
+ children,
69
+ onDismiss,
70
+ enablePanDownToClose = true,
71
+ backgroundComponent,
72
+ backdropComponent,
73
+ style,
74
+ enableHandlePanningGesture = true,
75
+ onDismissAttempt,
76
+ detached = false,
77
+ } = props;
78
+
79
+ const insets = useSafeAreaInsets();
80
+ const { colors } = useTheme();
81
+ const [visible, setVisible] = useState(false);
82
+ const [rendered, setRendered] = useState(false); // keep mounted for exit animation
83
+ const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
84
+ const hasClosedRef = useRef(false);
85
+ const scrollViewRef = useRef<Animated.ScrollView>(null);
86
+
87
+ const translateY = useSharedValue(SCREEN_HEIGHT);
88
+ const opacity = useSharedValue(0);
89
+ const scrollOffsetY = useSharedValue(0);
90
+ const isScrollAtTop = useSharedValue(true);
91
+ const allowPanClose = useSharedValue(true);
92
+ const keyboardHeight = useSharedValue(0);
93
+ const context = useSharedValue({ y: 0 });
94
+
95
+ useKeyboardHandler({
96
+ onMove: (e) => {
97
+ 'worklet';
98
+ keyboardHeight.value = e.height;
99
+ },
100
+ onEnd: (e) => {
101
+ 'worklet';
102
+ keyboardHeight.value = e.height;
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';
@@ -9,9 +9,6 @@ export interface AppColorPreset {
9
9
 
10
10
  export const APP_COLOR_NAMES: AppColorName[] = ['teal', 'blue', 'green', 'amber', 'red', 'purple', 'pink', 'sky', 'orange', 'mint'];
11
11
 
12
- /** Premium-exclusive color names, not shown in the standard picker */
13
- export const PREMIUM_COLOR_NAMES: AppColorName[] = ['oxy'];
14
-
15
12
  export const HEX_TO_APP_COLOR: Record<string, AppColorName> = {
16
13
  '#005c67': 'teal',
17
14
  '#1d9bf0': 'blue',
@@ -3,6 +3,6 @@ export type { BloomThemeProviderProps, BloomThemeContextValue } from './BloomThe
3
3
  export { useTheme, useThemeColor, useBloomTheme } from './use-theme';
4
4
  export type { Theme, ThemeColors, ThemeMode } from './types';
5
5
  export type { AppColorName, AppColorPreset } from './color-presets';
6
- export { APP_COLOR_NAMES, APP_COLOR_PRESETS, PREMIUM_COLOR_NAMES, HEX_TO_APP_COLOR, hexToAppColorName } from './color-presets';
6
+ export { APP_COLOR_NAMES, APP_COLOR_PRESETS, HEX_TO_APP_COLOR, hexToAppColorName } from './color-presets';
7
7
  export { applyDarkClass } from './apply-dark-class';
8
8
  export { setColorSchemeSafe } from './set-color-scheme-safe';