@oxyhq/bloom 0.16.0 → 0.16.2

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 (53) hide show
  1. package/lib/commonjs/dialog/Dialog.js +45 -157
  2. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  3. package/lib/commonjs/dialog/Dialog.web.js +64 -198
  4. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  5. package/lib/commonjs/dialog/DialogBottomSheet.js +216 -0
  6. package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -0
  7. package/lib/commonjs/dialog/DialogContent.js +155 -0
  8. package/lib/commonjs/dialog/DialogContent.js.map +1 -0
  9. package/lib/commonjs/dialog/placement.js +12 -1
  10. package/lib/commonjs/dialog/placement.js.map +1 -1
  11. package/lib/module/dialog/Dialog.js +48 -160
  12. package/lib/module/dialog/Dialog.js.map +1 -1
  13. package/lib/module/dialog/Dialog.web.js +66 -200
  14. package/lib/module/dialog/Dialog.web.js.map +1 -1
  15. package/lib/module/dialog/DialogBottomSheet.js +211 -0
  16. package/lib/module/dialog/DialogBottomSheet.js.map +1 -0
  17. package/lib/module/dialog/DialogContent.js +148 -0
  18. package/lib/module/dialog/DialogContent.js.map +1 -0
  19. package/lib/module/dialog/placement.js +11 -0
  20. package/lib/module/dialog/placement.js.map +1 -1
  21. package/lib/typescript/commonjs/dialog/Dialog.d.ts +1 -1
  22. package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
  23. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts +11 -8
  24. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  25. package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts +29 -0
  26. package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -0
  27. package/lib/typescript/commonjs/dialog/DialogContent.d.ts +32 -0
  28. package/lib/typescript/commonjs/dialog/DialogContent.d.ts.map +1 -0
  29. package/lib/typescript/commonjs/dialog/placement.d.ts +10 -0
  30. package/lib/typescript/commonjs/dialog/placement.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/dialog/types.d.ts +12 -0
  32. package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
  33. package/lib/typescript/module/dialog/Dialog.d.ts +1 -1
  34. package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
  35. package/lib/typescript/module/dialog/Dialog.web.d.ts +11 -8
  36. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  37. package/lib/typescript/module/dialog/DialogBottomSheet.d.ts +29 -0
  38. package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -0
  39. package/lib/typescript/module/dialog/DialogContent.d.ts +32 -0
  40. package/lib/typescript/module/dialog/DialogContent.d.ts.map +1 -0
  41. package/lib/typescript/module/dialog/placement.d.ts +10 -0
  42. package/lib/typescript/module/dialog/placement.d.ts.map +1 -1
  43. package/lib/typescript/module/dialog/types.d.ts +12 -0
  44. package/lib/typescript/module/dialog/types.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/src/__tests__/BottomSheetGesture.test.tsx +135 -0
  47. package/src/__tests__/DialogBottomSheet.test.tsx +259 -0
  48. package/src/dialog/Dialog.tsx +41 -171
  49. package/src/dialog/Dialog.web.tsx +56 -211
  50. package/src/dialog/DialogBottomSheet.tsx +250 -0
  51. package/src/dialog/DialogContent.tsx +157 -0
  52. package/src/dialog/placement.ts +11 -0
  53. package/src/dialog/types.ts +12 -0
@@ -4,9 +4,10 @@ import type { DialogAction, DialogProps } from './types';
4
4
  * Web variant of `<Dialog>`.
5
5
  *
6
6
  * A centered modal card (default) rendered into the bloom Portal at the end of
7
- * `document.body`, or when `placement` is `left`/`right`/`bottom`an
8
- * anchored side-sheet or bottom-sheet. Same prop API as native, so call sites
9
- * are platform agnostic.
7
+ * `document.body`, an anchored side-sheet for `left`/`right`, or for
8
+ * `bottom` bloom's cross-platform `BottomSheet` (the SAME component native
9
+ * uses), so the bottom placement drags-to-dismiss on web too. Same prop API as
10
+ * native, so call sites are platform agnostic.
10
11
  *
11
12
  * Open/close is driven by EITHER the imperative `control` (legacy) OR the
12
13
  * controlled `open` prop. When `open` is provided it wins.
@@ -18,10 +19,11 @@ import type { DialogAction, DialogProps } from './types';
18
19
  *
19
20
  * Web motion never uses reanimated `exiting` (which throws `removeChild` on
20
21
  * concurrent React unmounts): the centered card uses CSS keyframes and the
21
- * side/bottom modes use self-contained CSS transitions on a
22
- * mounted-through-exit node.
22
+ * side modes use self-contained CSS transitions on a mounted-through-exit
23
+ * node. The bottom placement animates via `BottomSheet`'s shared-value/gesture
24
+ * animation (no `exiting` layout animation).
23
25
  */
24
- export declare function Dialog({ control, open: controlledOpen, onClose, testID, title, description, actions, placement, width, maxWidth, maxHeightRatio, inset, showHandle, dismissOnBackdrop, style, panelStyle, panelClassName, containerStyle, containerClassName, label, children, }: DialogProps): import("react/jsx-runtime").JSX.Element | null;
26
+ export declare function Dialog({ placement, ...rest }: DialogProps): import("react/jsx-runtime").JSX.Element;
25
27
  export { DIALOG_SHEET_BACKDROP_TESTID };
26
28
  /**
27
29
  * Inline imperative dialog used by `alert()`. Mounts and immediately
@@ -45,8 +47,9 @@ export declare function AutoMountedDialog({ title, description, actions, onResol
45
47
  * @keyframes bloomDialogZoomFadeOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.95); } }
46
48
  * ```
47
49
  *
48
- * The `left`/`right`/`bottom` placements do NOT depend on this — they animate
49
- * via self-contained inline CSS transitions and need no keyframe injection.
50
+ * The `left`/`right` placements do NOT depend on this — they animate via
51
+ * self-contained inline CSS transitions and need no keyframe injection. The
52
+ * `bottom` placement uses bloom's `BottomSheet` (reanimated) and needs none.
50
53
  */
51
54
  export declare const BLOOM_DIALOG_CSS = "\n@keyframes bloomDialogFadeIn { from { opacity: 0; } to { opacity: 1; } }\n@keyframes bloomDialogFadeOut { from { opacity: 1; } to { opacity: 0; } }\n@keyframes bloomDialogZoomFadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }\n@keyframes bloomDialogZoomFadeOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.95); } }\n";
52
55
  //# sourceMappingURL=Dialog.web.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Dialog.web.d.ts","sourceRoot":"","sources":["../../../../src/dialog/Dialog.web.tsx"],"names":[],"mappings":"AA2BA,OAAO,EAML,4BAA4B,EAU7B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,YAAY,EAIZ,WAAW,EACZ,MAAM,SAAS,CAAC;AASjB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,MAAM,CAAC,EACrB,OAAO,EACP,IAAI,EAAE,cAAc,EACpB,OAAO,EACP,MAAM,EACN,KAAK,EACL,WAAW,EACX,OAAO,EACP,SAAS,EACT,KAA0B,EAC1B,QAAmC,EACnC,cAAyC,EACzC,KAAK,EACL,UAAiB,EACjB,iBAAwB,EACxB,KAAK,EACL,UAAU,EACV,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,KAAK,EACL,QAAQ,GACT,EAAE,WAAW,kDA6Lb;AAkID,OAAO,EAAE,4BAA4B,EAAE,CAAC;AAyTxC;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,WAAW,EACX,OAAO,EACP,SAAS,GACV,EAAE;IACD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB,2CAgBA;AA4CD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,gBAAgB,mZAK5B,CAAC"}
1
+ {"version":3,"file":"Dialog.web.d.ts","sourceRoot":"","sources":["../../../../src/dialog/Dialog.web.tsx"],"names":[],"mappings":"AAyBA,OAAO,EAML,4BAA4B,EAM7B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,YAAY,EAGZ,WAAW,EACZ,MAAM,SAAS,CAAC;AASjB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,WAAW,2CAazD;AAoSD,OAAO,EAAE,4BAA4B,EAAE,CAAC;AAqNxC;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,WAAW,EACX,OAAO,EACP,SAAS,GACV,EAAE;IACD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB,2CAgBA;AA6BD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,mZAK5B,CAAC"}
@@ -0,0 +1,29 @@
1
+ import type { DialogProps } from './types';
2
+ /**
3
+ * Shared `BottomSheet`-backed surface for the `bottom` placement, used by BOTH
4
+ * `Dialog.tsx` (native) and `Dialog.web.tsx` (web). `BottomSheet` is itself a
5
+ * single cross-platform implementation (react-native-gesture-handler +
6
+ * reanimated, drag-to-dismiss on web and native alike), so routing the bottom
7
+ * placement through here gives ONE code path with drag working everywhere — no
8
+ * duplicated CSS slide-up sheet.
9
+ *
10
+ * This component owns the entire open↔present/dismiss bridge and the Dialog →
11
+ * BottomSheet prop mapping in one place:
12
+ *
13
+ * - `open` (controlled) / `control` (imperative) → `present()` / `dismiss()`
14
+ * - `onClose` → fired once the sheet settles
15
+ * - `maxHeightRatio` → sheet `maxHeight`
16
+ * - `showHandle` / `dismissOnBackdrop` / `label` → forwarded to the surface
17
+ * - `panelStyle` → paints the sheet surface
18
+ * - `containerStyle` → wraps the content subtree
19
+ * so a host's CSS-var theme scope (`vars()`) still applies to descendants
20
+ * - `title` / `description` / `actions` / `children` → declarative body
21
+ */
22
+ export declare function DialogBottomSheet({ control, open: controlledOpen, onClose, testID, title, description, actions, maxHeightRatio, showHandle, dismissOnBackdrop, contentPadding, style, panelStyle, panelClassName, containerStyle, containerClassName, label, children, }: DialogBottomSheetProps): import("react/jsx-runtime").JSX.Element;
23
+ /**
24
+ * Props the shared bottom-sheet surface consumes — the `bottom`-relevant subset
25
+ * of `DialogProps`. `width`, `maxWidth`, and `inset` are placement-specific to
26
+ * the side/centered surfaces and intentionally excluded.
27
+ */
28
+ export type DialogBottomSheetProps = Pick<DialogProps, 'control' | 'open' | 'onClose' | 'testID' | 'title' | 'description' | 'actions' | 'maxHeightRatio' | 'showHandle' | 'dismissOnBackdrop' | 'contentPadding' | 'style' | 'panelStyle' | 'panelClassName' | 'containerStyle' | 'containerClassName' | 'label' | 'children'>;
29
+ //# sourceMappingURL=DialogBottomSheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DialogBottomSheet.d.ts","sourceRoot":"","sources":["../../../../src/dialog/DialogBottomSheet.tsx"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,SAAS,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,IAAI,EAAE,cAAc,EACpB,OAAO,EACP,MAAM,EACN,KAAK,EACL,WAAW,EACX,OAAO,EACP,cAAyC,EACzC,UAAiB,EACjB,iBAAwB,EACxB,cAA+C,EAC/C,KAAK,EACL,UAAU,EACV,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,KAAK,EACL,QAAQ,GACT,EAAE,sBAAsB,2CAiKxB;AAED;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG,IAAI,CACvC,WAAW,EACT,SAAS,GACT,MAAM,GACN,SAAS,GACT,QAAQ,GACR,OAAO,GACP,aAAa,GACb,SAAS,GACT,gBAAgB,GAChB,YAAY,GACZ,mBAAmB,GACnB,gBAAgB,GAChB,OAAO,GACP,YAAY,GACZ,gBAAgB,GAChB,gBAAgB,GAChB,oBAAoB,GACpB,OAAO,GACP,UAAU,CACb,CAAC"}
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+ import type { ThemeColors } from '../theme/types';
3
+ import type { DialogAction, DialogActionColor } from './types';
4
+ /**
5
+ * Shared dialog content primitives used by every `<Dialog>` placement on both
6
+ * platforms — the centered panel, the side-sheets, and the `BottomSheet`-backed
7
+ * bottom placement. Keeping them here is the single source of truth for the
8
+ * declarative `title` / `description` / `actions` chrome, so the surfaces never
9
+ * drift in spacing, typography, or action-button behaviour.
10
+ */
11
+ /**
12
+ * Renders the dialog's body: optional declarative title + description, any
13
+ * `children`, then the action row. The `titleId`/`descriptionId` are wired by
14
+ * the caller onto the dialog role element for `aria-labelledby` /
15
+ * `aria-describedby`.
16
+ */
17
+ export declare function DialogBody({ titleId, descriptionId, title, description, actions, children, }: {
18
+ titleId: string;
19
+ descriptionId: string;
20
+ title?: string;
21
+ description?: string;
22
+ actions?: DialogAction[];
23
+ children?: React.ReactNode;
24
+ }): import("react/jsx-runtime").JSX.Element;
25
+ export declare function ActionRow({ actions }: {
26
+ actions: DialogAction[];
27
+ }): import("react/jsx-runtime").JSX.Element;
28
+ export declare function getActionPalette(color: DialogActionColor, colors: ThemeColors): {
29
+ background: string;
30
+ foreground: string;
31
+ };
32
+ //# sourceMappingURL=DialogContent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DialogContent.d.ts","sourceRoot":"","sources":["../../../../src/dialog/DialogContent.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsB,MAAM,OAAO,CAAC;AAQ3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGlD,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAE/D;;;;;;GAMG;AAEH;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,OAAO,EACP,aAAa,EACb,KAAK,EACL,WAAW,EACX,OAAO,EACP,QAAQ,GACT,EAAE;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B,2CAmCA;AAED,wBAAgB,SAAS,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,YAAY,EAAE,CAAA;CAAE,2CAQjE;AAkDD,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,iBAAiB,EACxB,MAAM,EAAE,WAAW,GAClB;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAiB5C"}
@@ -40,6 +40,16 @@ export declare const DEFAULT_CENTER_MAX_WIDTH = 480;
40
40
  export declare const DEFAULT_MAX_HEIGHT_RATIO = 0.9;
41
41
  /** Corner radius shared by the side-sheet panel and the bottom-sheet top. */
42
42
  export declare const PANEL_RADIUS = 20;
43
+ /**
44
+ * Default inner padding (px) of the dialog content container, applied uniformly
45
+ * across every placement (centered panel, side-sheet body, bottom-sheet body)
46
+ * so the chrome reads identically regardless of which surface resolves. This is
47
+ * the legacy value — omitting the `contentPadding` prop keeps 20px everywhere,
48
+ * byte-for-byte unchanged. A host that owns its own insets (e.g. custom
49
+ * `children` that already manage padding) can override it via
50
+ * `Dialog`'s `contentPadding` prop (e.g. `contentPadding={0}`).
51
+ */
52
+ export declare const DEFAULT_DIALOG_CONTENT_PADDING = 20;
43
53
  /** Open/close transition duration (ms). ~280ms ease-out reads smooth. */
44
54
  export declare const ANIMATION_DURATION = 280;
45
55
  /** Centered-card fade-out duration (ms) — preserves the legacy timing. */
@@ -1 +1 @@
1
- {"version":3,"file":"placement.d.ts","sourceRoot":"","sources":["../../../../src/dialog/placement.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAErE;;;;;;;;;GASG;AACH,MAAM,MAAM,yBAAyB,GACjC,eAAe,GACf;IACE,IAAI,EAAE,eAAe,CAAC;IACtB,EAAE,CAAC,EAAE,eAAe,CAAC;IACrB,EAAE,CAAC,EAAE,eAAe,CAAC;IACrB,EAAE,CAAC,EAAE,eAAe,CAAC;IACrB,EAAE,CAAC,EAAE,eAAe,CAAC;CACtB,CAAC;AAEN;;;;GAIG;AACH,eAAO,MAAM,aAAa,MAAM,CAAC;AACjC,eAAO,MAAM,aAAa,MAAM,CAAC;AACjC,eAAO,MAAM,aAAa,OAAO,CAAC;AAClC,eAAO,MAAM,aAAa,OAAO,CAAC;AAUlC,qDAAqD;AACrD,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,4EAA4E;AAC5E,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,6EAA6E;AAC7E,eAAO,MAAM,YAAY,KAAK,CAAC;AAE/B,yEAAyE;AACzE,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,0EAA0E;AAC1E,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,oEAAoE;AACpE,eAAO,MAAM,QAAQ,mCAAmC,CAAC;AAEzD,qEAAqE;AACrE,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,+EAA+E;AAC/E,eAAO,MAAM,4BAA4B,gCAAgC,CAAC;AAE1E,sDAAsD;AACtD,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B;;;GAGG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,yBAAyB,GAAG,SAAS,GAC/C,eAAe,CAcjB"}
1
+ {"version":3,"file":"placement.d.ts","sourceRoot":"","sources":["../../../../src/dialog/placement.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAErE;;;;;;;;;GASG;AACH,MAAM,MAAM,yBAAyB,GACjC,eAAe,GACf;IACE,IAAI,EAAE,eAAe,CAAC;IACtB,EAAE,CAAC,EAAE,eAAe,CAAC;IACrB,EAAE,CAAC,EAAE,eAAe,CAAC;IACrB,EAAE,CAAC,EAAE,eAAe,CAAC;IACrB,EAAE,CAAC,EAAE,eAAe,CAAC;CACtB,CAAC;AAEN;;;;GAIG;AACH,eAAO,MAAM,aAAa,MAAM,CAAC;AACjC,eAAO,MAAM,aAAa,MAAM,CAAC;AACjC,eAAO,MAAM,aAAa,OAAO,CAAC;AAClC,eAAO,MAAM,aAAa,OAAO,CAAC;AAUlC,qDAAqD;AACrD,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,4EAA4E;AAC5E,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,6EAA6E;AAC7E,eAAO,MAAM,YAAY,KAAK,CAAC;AAE/B;;;;;;;;GAQG;AACH,eAAO,MAAM,8BAA8B,KAAK,CAAC;AAEjD,yEAAyE;AACzE,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,0EAA0E;AAC1E,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,oEAAoE;AACpE,eAAO,MAAM,QAAQ,mCAAmC,CAAC;AAEzD,qEAAqE;AACrE,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,+EAA+E;AAC/E,eAAO,MAAM,4BAA4B,gCAAgC,CAAC;AAE1E,sDAAsD;AACtD,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,aAAa,IAAI,CAAC;AAC/B,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B;;;GAGG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,yBAAyB,GAAG,SAAS,GAC/C,eAAe,CAcjB"}
@@ -125,6 +125,18 @@ export type DialogProps = React.PropsWithChildren<{
125
125
  inset?: DialogInset;
126
126
  /** Whether to render the drag handle in bottom-sheet mode. Defaults to `true`. */
127
127
  showHandle?: boolean;
128
+ /**
129
+ * Inner padding (px) of the dialog content container, applied uniformly across
130
+ * every placement (centered panel, side-sheet body, bottom-sheet body).
131
+ * Defaults to `20`.
132
+ *
133
+ * The default chrome padding is correct for the declarative
134
+ * `title`/`description`/`actions` layout, but a host passing pure custom
135
+ * `children` that already own their insets can set `contentPadding={0}` to
136
+ * render flush content and manage padding itself. Omitting the prop keeps the
137
+ * 20px default on every surface.
138
+ */
139
+ contentPadding?: number;
128
140
  /** Whether tapping the backdrop dismisses the dialog. Defaults to `true`. */
129
141
  dismissOnBackdrop?: boolean;
130
142
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/dialog/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/dialog/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
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",
@@ -0,0 +1,135 @@
1
+ import React, { createRef } from 'react';
2
+ import { Text } from 'react-native';
3
+ import { act, render } from '@testing-library/react-native';
4
+
5
+ import { BloomThemeProvider } from '../theme/BloomThemeProvider';
6
+ import BottomSheet, { type BottomSheetRef } from '../bottom-sheet';
7
+
8
+ /**
9
+ * Pan-to-dismiss is the whole point of routing the Dialog `bottom` placement
10
+ * through `BottomSheet` on BOTH platforms. These tests assert the pan gesture
11
+ * is actually wired (an `onEnd` worklet is registered) and that the
12
+ * dismiss-threshold logic inside it runs end-to-end — i.e. a fast/far downward
13
+ * swipe commits the close and fires `onDismiss`, while a small drag springs the
14
+ * sheet back without dismissing.
15
+ *
16
+ * This file overrides the default gesture-handler mock (which discards the
17
+ * registered callbacks) with a RECORDING mock that captures the pan gesture's
18
+ * handlers. Invoking the captured `onEnd` exercises the real threshold math
19
+ * from `bottom-sheet/index.tsx` — there is no `Platform.OS` gating around the
20
+ * `<GestureDetector>` or the pan builder, so this is the SAME code path web
21
+ * runs (drag-to-dismiss is active on web).
22
+ */
23
+
24
+ interface PanEndEvent {
25
+ velocityY: number;
26
+ translationY: number;
27
+ }
28
+
29
+ interface RecordedGesture {
30
+ onEnd?: (event: PanEndEvent) => void;
31
+ onUpdate?: (event: { translationY: number }) => void;
32
+ }
33
+
34
+ // Recorded `Gesture.Pan()` instances from the most recent render. Must be
35
+ // `mock`-prefixed so the hoisted `jest.mock` factory may reference it.
36
+ const mockPanGestures: RecordedGesture[] = [];
37
+
38
+ jest.mock('react-native-gesture-handler', () => {
39
+ const builder = (record?: RecordedGesture) => {
40
+ const self = {
41
+ enabled: () => self,
42
+ simultaneousWithExternalGesture: () => self,
43
+ withRef: () => self,
44
+ manualActivation: () => self,
45
+ activeOffsetY: () => self,
46
+ activeOffsetX: () => self,
47
+ onStart: () => self,
48
+ onUpdate: (fn: RecordedGesture['onUpdate']) => {
49
+ if (record) record.onUpdate = fn;
50
+ return self;
51
+ },
52
+ onEnd: (fn: RecordedGesture['onEnd']) => {
53
+ if (record) record.onEnd = fn;
54
+ return self;
55
+ },
56
+ onTouchesDown: () => self,
57
+ onTouchesMove: () => self,
58
+ onTouchesUp: () => self,
59
+ onTouchesCancelled: () => self,
60
+ };
61
+ return self;
62
+ };
63
+
64
+ return {
65
+ Gesture: {
66
+ Pan: () => {
67
+ const record: RecordedGesture = {};
68
+ mockPanGestures.push(record);
69
+ return builder(record);
70
+ },
71
+ Native: () => builder(),
72
+ },
73
+ GestureDetector: ({ children }: { children: React.ReactNode }) => children,
74
+ GestureHandlerRootView: ({ children }: { children: React.ReactNode }) => children,
75
+ };
76
+ });
77
+
78
+ function renderSheet(onDismiss: () => void) {
79
+ const ref = createRef<BottomSheetRef>();
80
+ const utils = render(
81
+ <BloomThemeProvider mode="light" colorPreset="teal">
82
+ <BottomSheet ref={ref} onDismiss={onDismiss}>
83
+ <Text>Body</Text>
84
+ </BottomSheet>
85
+ </BloomThemeProvider>,
86
+ );
87
+ // Present so the sheet mounts and the pan gesture is constructed.
88
+ act(() => {
89
+ ref.current?.present();
90
+ });
91
+ return utils;
92
+ }
93
+
94
+ describe('BottomSheet pan-to-dismiss gesture', () => {
95
+ beforeEach(() => {
96
+ mockPanGestures.length = 0;
97
+ });
98
+
99
+ it('wires a pan gesture with an onEnd handler (drag is active, not web-gated)', () => {
100
+ renderSheet(() => {});
101
+ // A pan gesture must have been constructed with an onEnd worklet — proof the
102
+ // drag pipeline is built unconditionally (the source has no
103
+ // `Platform.OS !== 'web'` guard around the GestureDetector or pan builder).
104
+ expect(mockPanGestures.length).toBeGreaterThan(0);
105
+ const bodyPan = mockPanGestures.find((g) => typeof g.onEnd === 'function');
106
+ expect(bodyPan).toBeDefined();
107
+ expect(typeof bodyPan?.onEnd).toBe('function');
108
+ });
109
+
110
+ it('dismisses when a fast downward swipe crosses the close threshold', () => {
111
+ const onDismiss = jest.fn();
112
+ renderSheet(onDismiss);
113
+ const bodyPan = mockPanGestures.find((g) => typeof g.onEnd === 'function');
114
+
115
+ // A high downward velocity is past `fastSwipeThreshold` (900) → the onEnd
116
+ // worklet commits the close, which (under the reanimated mock that runs
117
+ // withTiming callbacks synchronously) drains through to `onDismiss`.
118
+ act(() => {
119
+ bodyPan?.onEnd?.({ velocityY: 1500, translationY: 50 });
120
+ });
121
+ expect(onDismiss).toHaveBeenCalledTimes(1);
122
+ });
123
+
124
+ it('does not dismiss on a small, slow drag below the threshold', () => {
125
+ const onDismiss = jest.fn();
126
+ renderSheet(onDismiss);
127
+ const bodyPan = mockPanGestures.find((g) => typeof g.onEnd === 'function');
128
+
129
+ // Tiny translation + near-zero velocity → springs back, no close.
130
+ act(() => {
131
+ bodyPan?.onEnd?.({ velocityY: 10, translationY: 12 });
132
+ });
133
+ expect(onDismiss).not.toHaveBeenCalled();
134
+ });
135
+ });
@@ -0,0 +1,259 @@
1
+ import React, { createRef } from 'react';
2
+ import { Text, type View } from 'react-native';
3
+ import { act, fireEvent, render } from '@testing-library/react-native';
4
+
5
+ import { Dialog, useDialogControl } from '../dialog';
6
+ import { BloomThemeProvider } from '../theme/BloomThemeProvider';
7
+
8
+ /**
9
+ * Minimal shape of a `react-test-renderer` JSON node — enough to walk the tree
10
+ * by host `type` string without depending on `@types/react-test-renderer`.
11
+ */
12
+ type RenderedNode = {
13
+ type: string;
14
+ children: Array<RenderedNode | string> | null;
15
+ };
16
+
17
+ /** Count host nodes of a given `type` in a rendered `toJSON()` tree. */
18
+ function countNodesByType(
19
+ tree: RenderedNode | RenderedNode[] | null,
20
+ type: string,
21
+ ): number {
22
+ if (tree === null) return 0;
23
+ const roots = Array.isArray(tree) ? tree : [tree];
24
+ let count = 0;
25
+ for (const node of roots) {
26
+ if (node.type === type) count += 1;
27
+ if (node.children) {
28
+ for (const child of node.children) {
29
+ if (typeof child !== 'string') count += countNodesByType(child, type);
30
+ }
31
+ }
32
+ }
33
+ return count;
34
+ }
35
+
36
+ /**
37
+ * The Dialog `bottom` placement must delegate to the shared cross-platform
38
+ * `BottomSheet` (via `DialogBottomSheet`) — the SAME surface native and web use
39
+ * — instead of a bespoke CSS slide-up sheet. These tests pin that delegation:
40
+ * the declarative content renders through the sheet, the close lifecycle bridges
41
+ * `open`/`onClose` correctly, and brand theming (`panelStyle` surface paint +
42
+ * `containerStyle` theme scope) threads through to the sheet surface.
43
+ *
44
+ * The jest environment resolves `../dialog` to the native barrel (`Dialog.tsx`),
45
+ * which routes `bottom` through `DialogBottomSheet` → `BottomSheet`. The web
46
+ * fork (`Dialog.web.tsx`) routes through the EXACT same `DialogBottomSheet`, so
47
+ * this delegation is one shared code path across platforms.
48
+ */
49
+
50
+ function renderWithTheme(ui: React.ReactElement) {
51
+ return render(
52
+ <BloomThemeProvider mode="light" colorPreset="teal">
53
+ {ui}
54
+ </BloomThemeProvider>,
55
+ );
56
+ }
57
+
58
+ function Harness({
59
+ children,
60
+ }: {
61
+ children: (control: ReturnType<typeof useDialogControl>) => React.ReactNode;
62
+ }) {
63
+ const control = useDialogControl();
64
+ return <>{children(control)}</>;
65
+ }
66
+
67
+ describe('Dialog bottom placement delegates to BottomSheet', () => {
68
+ it('does not render bottom-sheet content until opened', () => {
69
+ const { queryByText } = renderWithTheme(
70
+ <Harness>
71
+ {(control) => (
72
+ <Dialog control={control} placement="bottom" title="Sheet title">
73
+ <Text>Sheet body</Text>
74
+ </Dialog>
75
+ )}
76
+ </Harness>,
77
+ );
78
+ // BottomSheet keeps its subtree unmounted until present() — proof the bottom
79
+ // placement is the sheet, not an always-mounted CSS panel.
80
+ expect(queryByText('Sheet title')).toBeNull();
81
+ expect(queryByText('Sheet body')).toBeNull();
82
+ });
83
+
84
+ it('renders declarative title + description + actions through the sheet when opened', () => {
85
+ let control: ReturnType<typeof useDialogControl> | undefined;
86
+ const { getByText } = renderWithTheme(
87
+ <Harness>
88
+ {(c) => {
89
+ control = c;
90
+ return (
91
+ <Dialog
92
+ control={c}
93
+ placement="bottom"
94
+ title="Delete item?"
95
+ description="This cannot be undone."
96
+ actions={[{ label: 'Delete', color: 'destructive' }, { label: 'Cancel', color: 'cancel' }]}
97
+ />
98
+ );
99
+ }}
100
+ </Harness>,
101
+ );
102
+ act(() => {
103
+ control?.open();
104
+ });
105
+ expect(getByText('Delete item?')).toBeTruthy();
106
+ expect(getByText('This cannot be undone.')).toBeTruthy();
107
+ expect(getByText('Delete')).toBeTruthy();
108
+ expect(getByText('Cancel')).toBeTruthy();
109
+ });
110
+
111
+ it('requests close via onClose exactly once on action press in controlled mode (no double-fire)', () => {
112
+ const onClose = jest.fn();
113
+ const { getByText } = renderWithTheme(
114
+ <Dialog open onClose={onClose} placement="bottom" title="Confirm" actions={[{ label: 'Done' }]} />,
115
+ );
116
+ act(() => {
117
+ fireEvent.press(getByText('Done'));
118
+ });
119
+ // Controlled mode: the action's close() requests dismissal via onClose once;
120
+ // the host (not the dialog) then flips `open`. It must not fire the
121
+ // close-completion onClose a second time.
122
+ expect(onClose).toHaveBeenCalledTimes(1);
123
+ });
124
+
125
+ it('threads panelStyle (brand surface) and containerStyle (theme scope) onto the sheet content', () => {
126
+ let control: ReturnType<typeof useDialogControl> | undefined;
127
+ const panelRef = createRef<View>();
128
+ const { getByTestId } = renderWithTheme(
129
+ <Harness>
130
+ {(c) => {
131
+ control = c;
132
+ return (
133
+ <Dialog
134
+ control={c}
135
+ placement="bottom"
136
+ testID="brand-sheet"
137
+ panelStyle={{ backgroundColor: 'rebeccapurple' }}
138
+ containerStyle={{ paddingTop: 7 }}
139
+ >
140
+ <Text ref={panelRef}>Branded</Text>
141
+ </Dialog>
142
+ );
143
+ }}
144
+ </Harness>,
145
+ );
146
+ act(() => {
147
+ control?.open();
148
+ });
149
+ const content = getByTestId('brand-sheet');
150
+ const flattened = Array.isArray(content.props.style)
151
+ ? Object.assign({}, ...content.props.style.filter(Boolean))
152
+ : content.props.style;
153
+ // panelStyle paints the surface; containerStyle (theme-var scope) wraps the
154
+ // content subtree. Both land on the sheet content container.
155
+ expect(flattened.backgroundColor).toBe('rebeccapurple');
156
+ expect(flattened.paddingTop).toBe(7);
157
+ });
158
+
159
+ it('applies the default 20px content padding when contentPadding is omitted', () => {
160
+ let control: ReturnType<typeof useDialogControl> | undefined;
161
+ const { getByTestId } = renderWithTheme(
162
+ <Harness>
163
+ {(c) => {
164
+ control = c;
165
+ return (
166
+ <Dialog control={c} placement="bottom" testID="default-pad">
167
+ <Text>Body</Text>
168
+ </Dialog>
169
+ );
170
+ }}
171
+ </Harness>,
172
+ );
173
+ act(() => {
174
+ control?.open();
175
+ });
176
+ const content = getByTestId('default-pad');
177
+ const flattened = Array.isArray(content.props.style)
178
+ ? Object.assign({}, ...content.props.style.filter(Boolean))
179
+ : content.props.style;
180
+ // Omitting `contentPadding` keeps the legacy 20px chrome padding.
181
+ expect(flattened.padding).toBe(20);
182
+ });
183
+
184
+ it('removes the body padding when contentPadding={0} (host owns its insets)', () => {
185
+ let control: ReturnType<typeof useDialogControl> | undefined;
186
+ const { getByTestId } = renderWithTheme(
187
+ <Harness>
188
+ {(c) => {
189
+ control = c;
190
+ return (
191
+ <Dialog control={c} placement="bottom" testID="flush-pad" contentPadding={0}>
192
+ <Text>Flush body</Text>
193
+ </Dialog>
194
+ );
195
+ }}
196
+ </Harness>,
197
+ );
198
+ act(() => {
199
+ control?.open();
200
+ });
201
+ const content = getByTestId('flush-pad');
202
+ const flattened = Array.isArray(content.props.style)
203
+ ? Object.assign({}, ...content.props.style.filter(Boolean))
204
+ : content.props.style;
205
+ // `contentPadding={0}` yields flush content the host pads itself.
206
+ expect(flattened.padding).toBe(0);
207
+ });
208
+
209
+ it('does NOT wrap pure custom children in a second scroll container (no scroll-in-scroll)', () => {
210
+ let control: ReturnType<typeof useDialogControl> | undefined;
211
+ const result = renderWithTheme(
212
+ <Harness>
213
+ {(c) => {
214
+ control = c;
215
+ return (
216
+ <Dialog control={c} placement="bottom">
217
+ <Text>Custom children own their own scrolling</Text>
218
+ </Dialog>
219
+ );
220
+ }}
221
+ </Harness>,
222
+ );
223
+ act(() => {
224
+ control?.open();
225
+ });
226
+ // Pure custom children → DialogBottomSheet passes `scrollable={false}` to
227
+ // BottomSheet, so the sheet does NOT add its internal ScrollView. A child
228
+ // that contains its own scroller is then the ONLY scroll container.
229
+ const tree: RenderedNode | RenderedNode[] | null = result.toJSON();
230
+ expect(countNodesByType(tree, 'Animated.ScrollView')).toBe(0);
231
+ });
232
+
233
+ it('keeps the internal scrollable body for declarative dialogs (title/description/actions)', () => {
234
+ let control: ReturnType<typeof useDialogControl> | undefined;
235
+ const result = renderWithTheme(
236
+ <Harness>
237
+ {(c) => {
238
+ control = c;
239
+ return (
240
+ <Dialog
241
+ control={c}
242
+ placement="bottom"
243
+ title="Confirm"
244
+ description="Short declarative body."
245
+ actions={[{ label: 'OK' }]}
246
+ />
247
+ );
248
+ }}
249
+ </Harness>,
250
+ );
251
+ act(() => {
252
+ control?.open();
253
+ });
254
+ // Declarative chrome → the default scrollable body is retained, so a long
255
+ // title/description list still scrolls gracefully on a small viewport.
256
+ const tree: RenderedNode | RenderedNode[] | null = result.toJSON();
257
+ expect(countNodesByType(tree, 'Animated.ScrollView')).toBe(1);
258
+ });
259
+ });