@oxyhq/bloom 1.9.2 → 1.10.1

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 (130) hide show
  1. package/docs/layout.mdx +79 -0
  2. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +14 -4
  3. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  4. package/lib/commonjs/fab/Fab.js +12 -3
  5. package/lib/commonjs/fab/Fab.js.map +1 -1
  6. package/lib/commonjs/fab/Fab.web.js +12 -4
  7. package/lib/commonjs/fab/Fab.web.js.map +1 -1
  8. package/lib/commonjs/index.js +137 -101
  9. package/lib/commonjs/index.js.map +1 -1
  10. package/lib/commonjs/index.web.js +137 -101
  11. package/lib/commonjs/index.web.js.map +1 -1
  12. package/lib/commonjs/layout/bottom-edge.js +148 -0
  13. package/lib/commonjs/layout/bottom-edge.js.map +1 -0
  14. package/lib/commonjs/layout/edge.js +67 -0
  15. package/lib/commonjs/layout/edge.js.map +1 -0
  16. package/lib/commonjs/layout/index.js +38 -0
  17. package/lib/commonjs/layout/index.js.map +1 -0
  18. package/lib/commonjs/provider/BloomProvider.js +5 -2
  19. package/lib/commonjs/provider/BloomProvider.js.map +1 -1
  20. package/lib/commonjs/tab-bar/TabBarBase.js +11 -1
  21. package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
  22. package/lib/commonjs/tab-bar/shared.js +0 -22
  23. package/lib/commonjs/tab-bar/shared.js.map +1 -1
  24. package/lib/commonjs/tab-bar/use-footprint.js +17 -8
  25. package/lib/commonjs/tab-bar/use-footprint.js.map +1 -1
  26. package/lib/commonjs/theme/color-scope/ColorScope.web.js +25 -13
  27. package/lib/commonjs/theme/color-scope/ColorScope.web.js.map +1 -1
  28. package/lib/commonjs/toast/Positioner.js +6 -1
  29. package/lib/commonjs/toast/Positioner.js.map +1 -1
  30. package/lib/commonjs/toast/container-geometry.js +19 -13
  31. package/lib/commonjs/toast/container-geometry.js.map +1 -1
  32. package/lib/module/bottom-sheet/BottomSheetBase.js +14 -4
  33. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  34. package/lib/module/fab/Fab.js +12 -3
  35. package/lib/module/fab/Fab.js.map +1 -1
  36. package/lib/module/fab/Fab.web.js +12 -4
  37. package/lib/module/fab/Fab.web.js.map +1 -1
  38. package/lib/module/index.js +1 -0
  39. package/lib/module/index.js.map +1 -1
  40. package/lib/module/index.web.js +1 -0
  41. package/lib/module/index.web.js.map +1 -1
  42. package/lib/module/layout/bottom-edge.js +141 -0
  43. package/lib/module/layout/bottom-edge.js.map +1 -0
  44. package/lib/module/layout/edge.js +62 -0
  45. package/lib/module/layout/edge.js.map +1 -0
  46. package/lib/module/layout/index.js +11 -0
  47. package/lib/module/layout/index.js.map +1 -0
  48. package/lib/module/provider/BloomProvider.js +5 -2
  49. package/lib/module/provider/BloomProvider.js.map +1 -1
  50. package/lib/module/tab-bar/TabBarBase.js +12 -2
  51. package/lib/module/tab-bar/TabBarBase.js.map +1 -1
  52. package/lib/module/tab-bar/shared.js +0 -21
  53. package/lib/module/tab-bar/shared.js.map +1 -1
  54. package/lib/module/tab-bar/use-footprint.js +18 -9
  55. package/lib/module/tab-bar/use-footprint.js.map +1 -1
  56. package/lib/module/theme/color-scope/ColorScope.web.js +25 -13
  57. package/lib/module/theme/color-scope/ColorScope.web.js.map +1 -1
  58. package/lib/module/toast/Positioner.js +6 -1
  59. package/lib/module/toast/Positioner.js.map +1 -1
  60. package/lib/module/toast/container-geometry.js +20 -13
  61. package/lib/module/toast/container-geometry.js.map +1 -1
  62. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/fab/Fab.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/fab/Fab.web.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/index.d.ts +1 -0
  66. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/index.web.d.ts +1 -0
  68. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/layout/bottom-edge.d.ts +77 -0
  70. package/lib/typescript/commonjs/layout/bottom-edge.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/layout/edge.d.ts +56 -0
  72. package/lib/typescript/commonjs/layout/edge.d.ts.map +1 -0
  73. package/lib/typescript/commonjs/layout/index.d.ts +9 -0
  74. package/lib/typescript/commonjs/layout/index.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/provider/BloomProvider.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/tab-bar/shared.d.ts +0 -9
  78. package/lib/typescript/commonjs/tab-bar/shared.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/tab-bar/use-footprint.d.ts +11 -7
  80. package/lib/typescript/commonjs/tab-bar/use-footprint.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/theme/color-scope/ColorScope.web.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/toast/Positioner.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/toast/container-geometry.d.ts +14 -5
  84. package/lib/typescript/commonjs/toast/container-geometry.d.ts.map +1 -1
  85. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  86. package/lib/typescript/module/fab/Fab.d.ts.map +1 -1
  87. package/lib/typescript/module/fab/Fab.web.d.ts.map +1 -1
  88. package/lib/typescript/module/index.d.ts +1 -0
  89. package/lib/typescript/module/index.d.ts.map +1 -1
  90. package/lib/typescript/module/index.web.d.ts +1 -0
  91. package/lib/typescript/module/index.web.d.ts.map +1 -1
  92. package/lib/typescript/module/layout/bottom-edge.d.ts +77 -0
  93. package/lib/typescript/module/layout/bottom-edge.d.ts.map +1 -0
  94. package/lib/typescript/module/layout/edge.d.ts +56 -0
  95. package/lib/typescript/module/layout/edge.d.ts.map +1 -0
  96. package/lib/typescript/module/layout/index.d.ts +9 -0
  97. package/lib/typescript/module/layout/index.d.ts.map +1 -0
  98. package/lib/typescript/module/provider/BloomProvider.d.ts.map +1 -1
  99. package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
  100. package/lib/typescript/module/tab-bar/shared.d.ts +0 -9
  101. package/lib/typescript/module/tab-bar/shared.d.ts.map +1 -1
  102. package/lib/typescript/module/tab-bar/use-footprint.d.ts +11 -7
  103. package/lib/typescript/module/tab-bar/use-footprint.d.ts.map +1 -1
  104. package/lib/typescript/module/theme/color-scope/ColorScope.web.d.ts.map +1 -1
  105. package/lib/typescript/module/toast/Positioner.d.ts.map +1 -1
  106. package/lib/typescript/module/toast/container-geometry.d.ts +14 -5
  107. package/lib/typescript/module/toast/container-geometry.d.ts.map +1 -1
  108. package/package.json +15 -1
  109. package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
  110. package/src/__tests__/support/constructed-style-sheets.ts +68 -0
  111. package/src/__tests__/support/press-host.ts +30 -0
  112. package/src/__tests__/support/rendered-style.ts +99 -0
  113. package/src/__tests__/support/unread-hook-fixture.ts +33 -0
  114. package/src/bottom-sheet/BottomSheetBase.tsx +14 -4
  115. package/src/fab/Fab.tsx +16 -3
  116. package/src/fab/Fab.web.tsx +16 -4
  117. package/src/index.ts +7 -0
  118. package/src/index.web.ts +7 -0
  119. package/src/layout/bottom-edge.tsx +162 -0
  120. package/src/layout/edge.ts +59 -0
  121. package/src/layout/index.ts +8 -0
  122. package/src/provider/BloomProvider.tsx +11 -1
  123. package/src/tab-bar/TabBarBase.tsx +11 -2
  124. package/src/tab-bar/shared.ts +0 -20
  125. package/src/tab-bar/use-footprint.ts +18 -9
  126. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +7682 -0
  127. package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
  128. package/src/theme/color-scope/ColorScope.web.tsx +23 -13
  129. package/src/toast/Positioner.tsx +5 -0
  130. package/src/toast/container-geometry.ts +20 -8
@@ -0,0 +1,77 @@
1
+ /**
2
+ * The bottom edge's OCCUPANCY — how much of it is already claimed by a floating
3
+ * surface, so the next surface can stack above it instead of underneath it.
4
+ *
5
+ * ## The problem
6
+ *
7
+ * `windowEdgeGap` tells a surface where the window edge is. It cannot tell it
8
+ * that a tab bar is already parked there. Nothing could, so nothing did: Bloom's
9
+ * `Fab` anchored itself 16px off the edge and landed squarely behind the
10
+ * floating tab bar, and every app that hit it patched around Bloom instead —
11
+ * Mention grew a `BottomBarAwareFab` that read `useTabBarFootprint()` and lifted
12
+ * the FAB by hand, on web only, leaving native broken across seven screens.
13
+ *
14
+ * A z-index cannot rescue that placement either. The tab bar host is the last
15
+ * sibling of the app shell, so it paints above every descendant regardless of
16
+ * what they claim; the FAB has to be somewhere ELSE, not merely on top.
17
+ *
18
+ * ## The model
19
+ *
20
+ * A surface that parks at the bottom edge CLAIMS its footprint. A surface that
21
+ * wants to avoid it READS the total and offsets itself. Neither one imports the
22
+ * other, so a consumer composes them in any combination — a tab bar with no FAB,
23
+ * a FAB with no tab bar, both, or a third surface written later — and the
24
+ * geometry still resolves.
25
+ *
26
+ * Claims combine with MAX, not sum. Every claim is measured from the same window
27
+ * edge, so two surfaces at that edge overlap rather than stack; the tallest is
28
+ * what a new surface has to clear. Summing would strand it at twice the height.
29
+ *
30
+ * ## Why an external store
31
+ *
32
+ * The claim set is mutable state living outside React. Reading it in a memoized
33
+ * position is exactly the stale-read the React Compiler produces, so it is read
34
+ * through `useSyncExternalStore` and never touched directly. The store is
35
+ * created per provider (not at module scope) so tests, and any app mounting two
36
+ * roots, stay isolated from each other.
37
+ */
38
+ import { type PropsWithChildren } from 'react';
39
+ /**
40
+ * Wrap the app once — `BloomProvider` already does. Rendering a second one
41
+ * scopes a nested subtree to its own claims, which is a real (if rare) thing to
42
+ * want: a bottom-edge surface inside a full-screen modal should not be offset by
43
+ * the tab bar the modal is covering.
44
+ */
45
+ export declare function BottomEdgeProvider({ children }: PropsWithChildren): import("react/jsx-runtime").JSX.Element;
46
+ export declare namespace BottomEdgeProvider {
47
+ var displayName: string;
48
+ }
49
+ /**
50
+ * How much of the bottom edge is already occupied, in px.
51
+ *
52
+ * Add it to whatever offset the surface would otherwise use:
53
+ *
54
+ * ```tsx
55
+ * const occupied = useBottomEdgeInset();
56
+ * <View style={{ position: 'absolute', bottom: windowEdgeGap(insets.bottom) + occupied }} />
57
+ * ```
58
+ *
59
+ * `0` outside a provider, and `0` on the first commit even inside one — a claim
60
+ * registers in an effect, so a reader mounted in the same commit as its claimant
61
+ * settles one frame later. That is a layout offset rather than a measurement, so
62
+ * the settle is invisible in practice; nothing here waits on a real layout pass.
63
+ */
64
+ export declare function useBottomEdgeInset(): number;
65
+ /**
66
+ * Claim `height` px of the bottom edge for as long as the caller is mounted.
67
+ *
68
+ * The claimant owns its own placement — claiming does not move it. It declares
69
+ * the space it occupies so that everything reading `useBottomEdgeInset()` stays
70
+ * off it. Pass the FULL footprint (the surface's height plus the gap it holds
71
+ * off the window edge), which is the same number the surface positions itself
72
+ * with.
73
+ *
74
+ * A no-op outside a provider, so a surface stays usable standalone.
75
+ */
76
+ export declare function useClaimBottomEdge(height: number): void;
77
+ //# sourceMappingURL=bottom-edge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bottom-edge.d.ts","sourceRoot":"","sources":["../../../../src/layout/bottom-edge.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,EAOL,KAAK,iBAAiB,EACvB,MAAM,OAAO,CAAC;AAsDf;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,QAAQ,EAAE,EAAE,iBAAiB,2CAGjE;yBAHe,kBAAkB;;;AAYlC;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAO3C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CASvD"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * How far a floating surface sits from a window edge.
3
+ *
4
+ * ## The rule this file encodes
5
+ *
6
+ * A floating surface CLEARS THE OS'S RESERVED BAND IN FULL. The safe-area inset
7
+ * is the space the platform has reserved for its own affordance — the iOS home
8
+ * indicator, the Android gesture handle — and a surface that sits inside it is
9
+ * sitting on top of a system control.
10
+ *
11
+ * Bloom used to answer this question three times, differently:
12
+ *
13
+ * - a detached `BottomSheet` `insets.bottom + 16`
14
+ * - the toast stack `bottom > 0 ? bottom + 8 : 16`
15
+ * - the floating tab bar `max(bottom - 16, 12)`
16
+ *
17
+ * The tab bar was the only one that SUBTRACTED, and subtracting is what put it
18
+ * on the Android gesture handle: a gesture-navigation device reserves ~24dp, the
19
+ * bar absorbed 16 of it, hit its own 12dp floor, and landed inside the band with
20
+ * the handle drawn through it. The other two were never reachable by that bug
21
+ * because they add.
22
+ *
23
+ * One function now answers it for all three, and the additive `gap` reproduces
24
+ * the sheet and the toast exactly — only the tab bar moves.
25
+ */
26
+ /**
27
+ * Breathing room from a window edge where the platform reserves nothing at all
28
+ * (web, a browser with no visual viewport inset, an Android device with the
29
+ * gesture bar hidden).
30
+ *
31
+ * It is also the FLOOR for every surface: a small-but-nonzero inset must never
32
+ * pull a surface closer to the edge than a device reporting no inset would.
33
+ */
34
+ export declare const EDGE_GAP = 16;
35
+ /**
36
+ * The distance a floating surface holds off a window edge, given that edge's
37
+ * safe-area inset.
38
+ *
39
+ * `gap` is the surface's own breathing room ON TOP of the reserved band, not an
40
+ * allowance to spend inside it — a bigger `gap` floats the surface further from
41
+ * the affordance, never closer. Pass `0` for a surface that should hug the safe
42
+ * area (the tab bar), a positive number for one that should stand clear of it (a
43
+ * detached sheet, a toast stack).
44
+ *
45
+ * ```ts
46
+ * windowEdgeGap(insets.bottom) // 34pt iOS · 24dp Android · 16 web
47
+ * windowEdgeGap(insets.bottom, 16) // 50pt iOS · 40dp Android · 16 web
48
+ * ```
49
+ *
50
+ * Anything laying out AROUND such a surface wants its total footprint (this gap
51
+ * plus the surface's own height) and must get it from the surface — never by
52
+ * adding `insets.bottom` to a hardcoded height, which double-counts the inset
53
+ * because the inset is already folded in here.
54
+ */
55
+ export declare function windowEdgeGap(inset: number, gap?: number): number;
56
+ //# sourceMappingURL=edge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge.d.ts","sourceRoot":"","sources":["../../../../src/layout/edge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,KAAK,CAAC;AAE3B;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAI,GAAG,MAAM,CAE5D"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Window-edge geometry, shared by every floating surface.
3
+ *
4
+ * `edge` answers "how far from the edge"; `bottom-edge` answers "what is already
5
+ * parked there". A surface that floats at the bottom needs both.
6
+ */
7
+ export { EDGE_GAP, windowEdgeGap } from './edge';
8
+ export { BottomEdgeProvider, useBottomEdgeInset, useClaimBottomEdge } from './bottom-edge';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/layout/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"BloomProvider.d.ts","sourceRoot":"","sources":["../../../../src/provider/BloomProvider.tsx"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,aAAa,EACb,OAAc,EACd,GAAG,UAAU,EACd,EAAE,kBAAkB,2CA2BpB;yBAhCe,aAAa"}
1
+ {"version":3,"file":"BloomProvider.d.ts","sourceRoot":"","sources":["../../../../src/provider/BloomProvider.tsx"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,aAAa,EACb,OAAc,EACd,GAAG,UAAU,EACd,EAAE,kBAAkB,2CAoCpB;yBAzCe,aAAa"}
@@ -1 +1 @@
1
- {"version":3,"file":"TabBarBase.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/TabBarBase.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAOL,KAAK,aAAa,EACnB,MAAM,OAAO,CAAC;AAiBf,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAEtE,OAAO,EAkBL,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACxB,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAe,MAAM,SAAS,CAAC;AA+iB3E;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,aAAa,CAAC,kBAAkB,CAAC,EAC1C,IAAI,EAAE,aAAa,CAAC,oBAAoB,CAAC;YAElB,WAAW;;EAGnC;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,CAAC,gBAAgB,CAAC;YAC1C,iBAAiB;;EAG/C"}
1
+ {"version":3,"file":"TabBarBase.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/TabBarBase.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAOL,KAAK,aAAa,EACnB,MAAM,OAAO,CAAC;AAmBf,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAEtE,OAAO,EAiBL,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACxB,MAAM,UAAU,CAAC;AAClB,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAe,MAAM,SAAS,CAAC;AAujB3E;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,aAAa,CAAC,kBAAkB,CAAC,EAC1C,IAAI,EAAE,aAAa,CAAC,oBAAoB,CAAC;YAElB,WAAW;;EAGnC;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,aAAa,CAAC,gBAAgB,CAAC;YAC1C,iBAAiB;;EAG/C"}
@@ -26,15 +26,6 @@ export declare const HIGHLIGHT_MINIMIZED: number;
26
26
  export declare const BLUR_BLEED = 44;
27
27
  /** Label type size. Small by design — the icon carries the meaning. */
28
28
  export declare const LABEL_FONT_SIZE = 9.5;
29
- /**
30
- * Vertical gap between the bottom of the pill and the bottom of the window.
31
- *
32
- * The safe-area inset is folded in HERE, which is why anything accounting for
33
- * the bar in its own layout must go through `useTabBarFootprint` (which derives
34
- * its number from this same function) rather than adding `insets.bottom` to a
35
- * hardcoded height — that double-counts the inset.
36
- */
37
- export declare function tabBarBottomGap(bottomInset: number): number;
38
29
  /**
39
30
  * Slide spring: interruptible by design — rapid tab-hopping retargets with
40
31
  * preserved velocity. Slight under-damping gives the pill a tiny settle, safe
@@ -1 +1 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/shared.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAG7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAWvD,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,4EAA4E;AAC5E,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,qEAAqE;AACrE,eAAO,MAAM,UAAU,KAAK,CAAC;AAC7B,8DAA8D;AAC9D,eAAO,MAAM,SAAS,IAAI,CAAC;AAC3B,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,SAAS,KAAK,CAAC;AAC5B;;;GAGG;AACH,eAAO,MAAM,QAAQ,IAAI,CAAC;AAC1B,eAAO,MAAM,WAAW,QAA0B,CAAC;AACnD,eAAO,MAAM,UAAU,IAAI,CAAC;AAC5B,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,QAA2C,CAAC;AAC3E,eAAO,MAAM,mBAAmB,QAA6B,CAAC;AAC9D,iEAAiE;AACjE,eAAO,MAAM,UAAU,KAAK,CAAC;AAC7B,uEAAuE;AACvE,eAAO,MAAM,eAAe,MAAM,CAAC;AAWnC;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAE3D;AAED;;;;GAIG;AACH,eAAO,MAAM,YAAY;;;CAAwC,CAAC;AAElE;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc;;CAAoB,CAAC;AAEhD;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAM3C;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,WAAW,EACnB,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC/B,WAAW,CAQb;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CAe5E;AAaD,MAAM,WAAW,kBAAkB;IACjC,yEAAyE;IACzE,KAAK,EAAE,WAAW,CAAC;IACnB;;;;;;;;;;;;OAYG;IACH,KAAK,EAAE,SAAS,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,MAAM,EAAE,OAAO,CAAC;CACjB"}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/shared.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAG7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAWvD,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,4EAA4E;AAC5E,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,qEAAqE;AACrE,eAAO,MAAM,UAAU,KAAK,CAAC;AAC7B,8DAA8D;AAC9D,eAAO,MAAM,SAAS,IAAI,CAAC;AAC3B,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,SAAS,KAAK,CAAC;AAC5B;;;GAGG;AACH,eAAO,MAAM,QAAQ,IAAI,CAAC;AAC1B,eAAO,MAAM,WAAW,QAA0B,CAAC;AACnD,eAAO,MAAM,UAAU,IAAI,CAAC;AAC5B,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,QAA2C,CAAC;AAC3E,eAAO,MAAM,mBAAmB,QAA6B,CAAC;AAC9D,iEAAiE;AACjE,eAAO,MAAM,UAAU,KAAK,CAAC;AAC7B,uEAAuE;AACvE,eAAO,MAAM,eAAe,MAAM,CAAC;AAGnC;;;;GAIG;AACH,eAAO,MAAM,YAAY;;;CAAwC,CAAC;AAElE;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc;;CAAoB,CAAC;AAEhD;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAM3C;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,WAAW,EACnB,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC/B,WAAW,CAQb;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CAe5E;AAaD,MAAM,WAAW,kBAAkB;IACjC,yEAAyE;IACzE,KAAK,EAAE,WAAW,CAAC;IACnB;;;;;;;;;;;;OAYG;IACH,KAAK,EAAE,SAAS,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC;CAC5C;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,MAAM,EAAE,OAAO,CAAC;CACjB"}
@@ -13,15 +13,19 @@
13
13
  * const footprint = useTabBarFootprint();
14
14
  *
15
15
  * <Animated.ScrollView contentContainerStyle={{ paddingBottom: footprint + 12 }} />
16
- * <Fab style={{ position: 'absolute', right: 16, bottom: footprint }} />
17
16
  * ```
18
17
  *
19
- * The value ALREADY INCLUDES the bottom safe-area inset — the bar folds the
20
- * inset into its own gap, so this is measured from the true window edge, not
21
- * from the safe area. Never write `footprint + insets.bottom`: that counts the
22
- * home indicator twice and strands a visible band of dead space under every
23
- * list (114px accounted for a 76px bar on an iOS device or PWA). Whatever you
24
- * add on top is your own clearance — never the inset a second time.
18
+ * A FAB no longer needs this hook at all `Fab` reads the bottom edge's
19
+ * occupancy itself (see `layout/bottom-edge`), which is what the bar publishes
20
+ * this exact number to. Reach for the hook when laying out something that is not
21
+ * a Bloom surface.
22
+ *
23
+ * The value ALREADY INCLUDES the bottom safe-area inset `windowEdgeGap` clears
24
+ * the OS's reserved band in full — so this is measured from the true window
25
+ * edge, not from the safe area. Never write `footprint + insets.bottom`: that
26
+ * counts the home indicator twice and strands a visible band of dead space under
27
+ * every list. Whatever you add on top is your own clearance, never the inset a
28
+ * second time.
25
29
  *
26
30
  * It is the EXPANDED height, deliberately. The bar minimizes while scrolling
27
31
  * down and re-expands on the way up, so measuring the minimized height would
@@ -1 +1 @@
1
- {"version":3,"file":"use-footprint.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/use-footprint.ts"],"names":[],"mappings":"AAiBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAG3C"}
1
+ {"version":3,"file":"use-footprint.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/use-footprint.ts"],"names":[],"mappings":"AAkBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAO3C"}
@@ -1 +1 @@
1
- {"version":3,"file":"ColorScope.web.d.ts","sourceRoot":"","sources":["../../../../../src/theme/color-scope/ColorScope.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsE,MAAM,OAAO,CAAC;AAI3F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAgBrD,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,WAAW,EAAE,YAAY,GAAG,SAAS,CAAC;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yGAAyG;IACzG,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAuBD,wBAAgB,eAAe,CAAC,EAC9B,WAAW,EACX,OAAe,EACf,KAAK,EACL,QAAQ,GACT,EAAE,oBAAoB,2CAoDtB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,YAAY,GAAG,KAAK,CAAC,aAAa,CAYjF"}
1
+ {"version":3,"file":"ColorScope.web.d.ts","sourceRoot":"","sources":["../../../../../src/theme/color-scope/ColorScope.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAsE,MAAM,OAAO,CAAC;AAI3F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAgBrD,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,WAAW,EAAE,YAAY,GAAG,SAAS,CAAC;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yGAAyG;IACzG,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC;IAC5B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AA+BD,wBAAgB,eAAe,CAAC,EAC9B,WAAW,EACX,OAAe,EACf,KAAK,EACL,QAAQ,GACT,EAAE,oBAAoB,2CAsDtB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,YAAY,GAAG,KAAK,CAAC,aAAa,CAYjF"}
@@ -1 +1 @@
1
- {"version":3,"file":"Positioner.d.ts","sourceRoot":"","sources":["../../../../src/toast/Positioner.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAa/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAwB5C,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC,EAAE,CAC/B,KAAK,CAAC,iBAAiB,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,GAAG,OAAO,CAAC,CAAC,CA4DlE,CAAC"}
1
+ {"version":3,"file":"Positioner.d.ts","sourceRoot":"","sources":["../../../../src/toast/Positioner.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAc/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAwB5C,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC,EAAE,CAC/B,KAAK,CAAC,iBAAiB,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,GAAG,OAAO,CAAC,CAAC,CAgElE,CAAC"}
@@ -43,19 +43,28 @@ import type { ToastPosition } from './types';
43
43
  */
44
44
  export declare const getContainerStyle: () => ViewStyle;
45
45
  /**
46
- * How far the stack sits from its screen edge. A falsy `offset` means "derive from
47
- * the safe area": `inset + 8`, or 16 when there is no inset.
46
+ * How far the stack sits from its screen edge.
48
47
  *
49
- * `Positioner` applies this over ONE of `getContainerStyle`'s four pinned edges;
50
- * `calculateOutsidePressableArea` reads the same numbers as a plain edge distance.
48
+ * The gap is the shared `windowEdgeGap` rule with the stack's own breathing room
49
+ * on top, so the stack clears the OS's reserved band the same way a detached
50
+ * sheet and the tab bar do. `offset` overrides that gap outright — it is the
51
+ * consumer saying where the stack goes.
52
+ *
53
+ * `bottomEdgeInset` is separate from both, and additive to either: it is not a
54
+ * gap preference but an OBSTRUCTION — whatever floating surface has already
55
+ * claimed the bottom edge (see `layout/bottom-edge`). A floating tab bar occupies
56
+ * ~82dp on a gesture-navigation Android device while this stack sat at 32dp, so
57
+ * every bottom toast rendered on top of the bar until the stack started reading
58
+ * it. An explicit `offset` does not excuse the stack from clearing it.
51
59
  */
52
- export declare const getInsetValues: ({ position, offset, safeAreaInsets, }: {
60
+ export declare const getInsetValues: ({ position, offset, safeAreaInsets, bottomEdgeInset, }: {
53
61
  position: ToastPosition;
54
62
  offset?: number;
55
63
  safeAreaInsets?: {
56
64
  top: number;
57
65
  bottom: number;
58
66
  };
67
+ bottomEdgeInset?: number;
59
68
  }) => {
60
69
  top?: number;
61
70
  bottom?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"container-geometry.d.ts","sourceRoot":"","sources":["../../../../src/toast/container-geometry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,iBAAiB,QAAO,SASnC,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,uCAI5B;IACD,QAAQ,EAAE,aAAa,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAClD,KAAG;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAclC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,GAAI,8DAM3C;IACD,QAAQ,EAAE,aAAa,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAChD,KAAG,SA2CH,CAAC"}
1
+ {"version":3,"file":"container-geometry.d.ts","sourceRoot":"","sources":["../../../../src/toast/container-geometry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAM9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,iBAAiB,QAAO,SASnC,CAAC;AAEH;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,GAAI,wDAK5B;IACD,QAAQ,EAAE,aAAa,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,KAAG;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAYlC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,GAAI,8DAM3C;IACD,QAAQ,EAAE,aAAa,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAChD,KAAG,SA2CH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "1.9.2",
3
+ "version": "1.10.1",
4
4
  "packageManager": "bun@1.3.14",
5
5
  "description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
6
6
  "main": "lib/commonjs/index.js",
@@ -258,6 +258,20 @@
258
258
  "default": "./lib/commonjs/button/index.js"
259
259
  }
260
260
  },
261
+ "./layout": {
262
+ "react-native": {
263
+ "types": "./lib/typescript/module/layout/index.d.ts",
264
+ "default": "./src/layout/index.ts"
265
+ },
266
+ "import": {
267
+ "types": "./lib/typescript/module/layout/index.d.ts",
268
+ "default": "./lib/module/layout/index.js"
269
+ },
270
+ "require": {
271
+ "types": "./lib/typescript/commonjs/layout/index.d.ts",
272
+ "default": "./lib/commonjs/layout/index.js"
273
+ }
274
+ },
261
275
  "./fab": {
262
276
  "react-native": {
263
277
  "types": "./lib/typescript/module/fab/index.d.ts",
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A barrel that DELIBERATELY offers one name from two declarations, so that
3
+ * `barrel-name-collisions.test.ts` can prove its detector fires. It reproduces
4
+ * the shipped `Item` shape: a star export carrying `Item` from `item/`, and an
5
+ * explicit re-export binding that same name to a different component.
6
+ *
7
+ * Nothing imports this at runtime. bob's `exclude` keeps it out of `lib/`, and
8
+ * `files: ["src", …]` does ship it inside the tarball's `src/` like the other
9
+ * test files there — but no entry point reaches it, so no bundler links it, and
10
+ * `package.json#exports` never names it.
11
+ *
12
+ * The `Card` lines below are the other half of the
13
+ * control: one name offered twice from ONE declaration, which the detector must
14
+ * stay quiet about — otherwise its cheapest fix would be deleting a legitimate
15
+ * redundant re-export.
16
+ */
17
+ export * from '../../item';
18
+ export { Card as Item } from '../../card';
19
+ export { Card } from '../../card';
20
+ export * from '../../card';
@@ -0,0 +1,68 @@
1
+ /**
2
+ * A spec-shaped stand-in for constructed stylesheets, for the jsdom suites.
3
+ *
4
+ * jsdom 26 ships the `CSSStyleSheet` CONSTRUCTOR but neither `replaceSync` nor
5
+ * `document.adoptedStyleSheets`, so every jsdom test takes Bloom's `<style>`
6
+ * fallback. That makes the whole CSP-safe path — the reason
7
+ * `styles/adopt-style-sheet.ts` exists — invisible to jest unless a test
8
+ * installs the API itself. Without this, a mutation that deleted the adoption
9
+ * branch entirely would leave the suite green.
10
+ *
11
+ * Not collected as a suite: jest's `testMatch` wants `*.test.ts` / `*.spec.ts`.
12
+ */
13
+
14
+ export interface FakeStyleSheet {
15
+ /** The CSS most recently handed to `replaceSync`. */
16
+ cssText: string;
17
+ /** How many times the sheet has been re-parsed, so a test can prove it wasn't. */
18
+ replaceSyncCalls: number;
19
+ replaceSync(css: string): void;
20
+ }
21
+
22
+ export interface ConstructedStyleSheetsHarness {
23
+ /** The document's adopted sheets, read live (the code reassigns the array). */
24
+ adopted(): readonly FakeStyleSheet[];
25
+ /** Drop the API again, restoring jsdom's own `CSSStyleSheet`. */
26
+ uninstall(): void;
27
+ }
28
+
29
+ export function installConstructedStyleSheets(): ConstructedStyleSheetsHarness {
30
+ class FakeCSSStyleSheet implements FakeStyleSheet {
31
+ cssText = '';
32
+ replaceSyncCalls = 0;
33
+
34
+ replaceSync(css: string): void {
35
+ this.cssText = css;
36
+ this.replaceSyncCalls += 1;
37
+ }
38
+ }
39
+
40
+ const previousConstructor = Object.getOwnPropertyDescriptor(
41
+ globalThis,
42
+ 'CSSStyleSheet',
43
+ );
44
+
45
+ Object.defineProperty(globalThis, 'CSSStyleSheet', {
46
+ value: FakeCSSStyleSheet,
47
+ writable: true,
48
+ configurable: true,
49
+ });
50
+ Object.defineProperty(document, 'adoptedStyleSheets', {
51
+ value: [],
52
+ writable: true,
53
+ configurable: true,
54
+ });
55
+
56
+ return {
57
+ adopted: () =>
58
+ document.adoptedStyleSheets as unknown as readonly FakeStyleSheet[],
59
+ uninstall: () => {
60
+ Reflect.deleteProperty(document, 'adoptedStyleSheets');
61
+ if (previousConstructor) {
62
+ Object.defineProperty(globalThis, 'CSSStyleSheet', previousConstructor);
63
+ } else {
64
+ Reflect.deleteProperty(globalThis, 'CSSStyleSheet');
65
+ }
66
+ },
67
+ };
68
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Pressing a component's OWN host node, and proving the component is what
3
+ * installed the handler being measured.
4
+ *
5
+ * `fireEvent.press` walks UP from the element it is handed to the nearest
6
+ * ancestor carrying an `onPress` prop, and it does not stop at host nodes — a
7
+ * COMPOSITE counts, because `getEventHandler` only reads `element.props`. So
8
+ * `<Thing onPress={fn}>` written in a test's own JSX catches the press itself.
9
+ * Measured on `ProfileCard`: with the handler deleted from the component the
10
+ * card rendered as an inert `View`, and the test still reported exactly one
11
+ * call. Green, and measuring nothing. Nine suites had the same shape.
12
+ *
13
+ * Asserting the host node's own `onPress` FIRST closes both halves:
14
+ *
15
+ * - a component that installs no handler at all fails here, and
16
+ * - a component that installs one which drops the caller's fails on the call
17
+ * count, because `fireEvent` now finds a handler AT the node and never walks
18
+ * past it to the test's own JSX.
19
+ *
20
+ * `host` must therefore be the node the component itself made pressable — the
21
+ * one carrying its `testID` / `accessibilityLabel` — not a `Text` deep inside
22
+ * it, or the walk-up is back and so is the hole.
23
+ */
24
+ import { fireEvent } from '@testing-library/react-native';
25
+ import type { ReactTestInstance } from 'react-test-renderer';
26
+
27
+ export function pressHost(host: ReactTestInstance): void {
28
+ expect(typeof host.props.onPress).toBe('function');
29
+ fireEvent.press(host);
30
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Reading what actually landed on a rendered node — shared by every suite that
3
+ * asserts the "className lands on the node the parent lays out" rule.
4
+ *
5
+ * Three components had the same two-node defect (`Button`, then `Fab` and
6
+ * `FrostedIconButton`), and asserting it needs the same two things each time:
7
+ * the HOST tree (composites do not appear in `toJSON()`, so a wrapper is only
8
+ * visible there) and a deep flatten of the `style` prop, because react-native-css
9
+ * merges a caller's class in as a `{ $$css: true, className }` descriptor
10
+ * ALONGSIDE the style objects rather than in a prop of its own.
11
+ */
12
+ import type { ReactTestRendererJSON } from 'react-test-renderer';
13
+
14
+ export type StyleEntry = Record<string, unknown>;
15
+
16
+ export interface HostNode {
17
+ type: string;
18
+ props: Record<string, unknown>;
19
+ children: Array<HostNode | string> | null;
20
+ }
21
+
22
+ /**
23
+ * Deep-flatten an RN `style` prop (nested arrays, `false`/`null` holes) into the
24
+ * list of style objects that actually reached the node.
25
+ */
26
+ export function styleEntries(style: unknown, out: StyleEntry[] = []): StyleEntry[] {
27
+ if (!style || typeof style !== 'object') return out;
28
+ if (Array.isArray(style)) {
29
+ for (const entry of style) styleEntries(entry, out);
30
+ return out;
31
+ }
32
+ out.push(style as StyleEntry);
33
+ return out;
34
+ }
35
+
36
+ /** Every style key/value that landed on a node, later entries winning. */
37
+ export function resolvedStyle(style: unknown): StyleEntry {
38
+ return Object.assign({}, ...styleEntries(style)) as StyleEntry;
39
+ }
40
+
41
+ /** The class tokens react-native-css accepted for a node, in arrival order. */
42
+ export function classNamesOn(style: unknown): string[] {
43
+ return styleEntries(style)
44
+ .filter((entry) => entry.$$css === true && typeof entry.className === 'string')
45
+ .map((entry) => String(entry.className));
46
+ }
47
+
48
+ export function isHostNode(value: unknown): value is HostNode {
49
+ return (
50
+ typeof value === 'object' && value !== null && typeof (value as HostNode).type === 'string'
51
+ );
52
+ }
53
+
54
+ /** Walk the HOST tree (composites are absent from `toJSON()`) for a testID. */
55
+ export function findHost(
56
+ node: ReactTestRendererJSON | ReactTestRendererJSON[] | unknown,
57
+ testID: string,
58
+ ): HostNode | null {
59
+ if (Array.isArray(node)) {
60
+ for (const child of node) {
61
+ const hit = findHost(child, testID);
62
+ if (hit) return hit;
63
+ }
64
+ return null;
65
+ }
66
+ if (!isHostNode(node)) return null;
67
+ if (node.props.testID === testID) return node;
68
+ return findHost(node.children, testID);
69
+ }
70
+
71
+ /**
72
+ * Every host node in a rendered tree, in document order.
73
+ *
74
+ * The reason to walk `toJSON()` rather than `UNSAFE_root.findAll` is that the
75
+ * latter yields COMPOSITE instances too, so a `memo(fn)` component contributes
76
+ * two matches for one rendered element and every count is silently doubled —
77
+ * which reads as "the component rendered the thing twice", not as an artefact
78
+ * of the query.
79
+ */
80
+ export function hostNodes(tree: unknown, out: HostNode[] = []): HostNode[] {
81
+ if (Array.isArray(tree)) {
82
+ for (const child of tree) hostNodes(child, out);
83
+ return out;
84
+ }
85
+ if (!isHostNode(tree)) return out;
86
+ out.push(tree);
87
+ return hostNodes(tree.children, out);
88
+ }
89
+
90
+ /**
91
+ * The host nodes a parent laid out. Callers assert the LENGTH themselves —
92
+ * "exactly one" is the property, and a helper that returned only the first would
93
+ * hide the second.
94
+ */
95
+ export function renderedChildren(tree: unknown, hostTestID: string): HostNode[] {
96
+ const host = findHost(tree, hostTestID);
97
+ if (host === null) throw new Error(`no host rendered for testID "${hostTestID}"`);
98
+ return (host.children ?? []).filter(isHostNode);
99
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * A module that DELIBERATELY throws away one of the values a hook hands back, so
3
+ * `hook-subscriptions-are-read.test.ts` can prove its detector fires. It carries
4
+ * both halves of the control: `unreadFlag` is bound and never read, which the
5
+ * detector must report, and `readFlag` is bound and read, which it must stay
6
+ * quiet about — otherwise the cheapest way to green the gate would be deleting a
7
+ * binding that is doing its job.
8
+ *
9
+ * Nothing imports this at runtime. bob's `exclude` keeps it out of `lib/`, and
10
+ * `files: ["src", …]` does ship it inside the tarball's `src/` like the other
11
+ * 137 test files there — but no entry point reaches it, so no bundler links it.
12
+ * It is a plain `.ts` with no React import because the
13
+ * detector reads source, never types: a call named `use…` bound to a name is all
14
+ * the shape there is.
15
+ *
16
+ * The destructuring RENAMES, which is not decoration — it reproduces the shipped
17
+ * shape (`const { state: pressed, onIn: onPressIn } = useInteractionState()`) and
18
+ * it is what keeps the control honest. A hook declared in the same file as its
19
+ * caller mentions its own property names in its return type and its return
20
+ * object, and the detector counts every occurrence of a spelling as a possible
21
+ * read, so an unrenamed fixture would be silently unreportable for a reason no
22
+ * real call site has.
23
+ */
24
+
25
+ /** Stands in for a real subscription — a hook by name, with two return values. */
26
+ function useFixtureInteractionState(): { first: boolean; second: boolean } {
27
+ return { first: false, second: false };
28
+ }
29
+
30
+ export function fixtureComponentBody(): boolean {
31
+ const { first: readFlag, second: unreadFlag } = useFixtureInteractionState();
32
+ return readFlag;
33
+ }
@@ -27,6 +27,7 @@ import Animated, {
27
27
  withTiming,
28
28
  } from 'react-native-reanimated';
29
29
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
30
+ import { EDGE_GAP, windowEdgeGap } from '../layout/edge';
30
31
  import { Backdrop, BACKDROP_DIM_OPACITY } from '../overlay';
31
32
  import { useTheme } from '../theme/use-theme';
32
33
  import type { BottomSheetRef, BottomSheetProps, BottomSheetShellProps, BottomSheetBaseProps } from './types';
@@ -526,21 +527,30 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
526
527
  };
527
528
  }, [translateY, screenHeightSV, keyboardHeight]);
528
529
 
530
+ // The shared window-edge rule, resolved on the JS thread: `windowEdgeGap` is
531
+ // not a worklet, so the worklets below capture the number rather than call it.
532
+ //
533
+ // A detached sheet deliberately does NOT read `useBottomEdgeInset()`. It is a
534
+ // modal surface rendered in the portal ABOVE whatever owns the bottom edge and
535
+ // it covers that surface — offsetting it by a tab bar it is already hiding
536
+ // would float it off the bottom of the screen for nothing.
537
+ const detachedGap = detached ? windowEdgeGap(insets.bottom, EDGE_GAP) : 0;
538
+
529
539
  const sheetHeightStyle = useAnimatedStyle(() => ({
530
- maxHeight: screenHeightSV.value - keyboardHeight.value - insets.top - (detached ? insets.bottom + 16 : 0),
531
- }), [insets.top, insets.bottom, detached, screenHeightSV, keyboardHeight]);
540
+ maxHeight: screenHeightSV.value - keyboardHeight.value - insets.top - detachedGap,
541
+ }), [insets.top, detachedGap, screenHeightSV, keyboardHeight]);
532
542
 
533
543
  const sheetMarginStyle = useAnimatedStyle(() => {
534
544
  // Only add margin when detached, otherwise extend behind safe area
535
545
  if (detached) {
536
546
  return {
537
- marginBottom: keyboardHeight.value > 0 ? 16 : insets.bottom + 16,
547
+ marginBottom: keyboardHeight.value > 0 ? EDGE_GAP : detachedGap,
538
548
  };
539
549
  }
540
550
  return {
541
551
  marginBottom: 0,
542
552
  };
543
- }, [insets.bottom, detached, keyboardHeight]);
553
+ }, [detachedGap, detached, keyboardHeight]);
544
554
 
545
555
  const handleBackdropPress = useCallback(() => {
546
556
  // Always animate close on backdrop press