@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.
- package/docs/layout.mdx +79 -0
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js +14 -4
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/fab/Fab.js +12 -3
- package/lib/commonjs/fab/Fab.js.map +1 -1
- package/lib/commonjs/fab/Fab.web.js +12 -4
- package/lib/commonjs/fab/Fab.web.js.map +1 -1
- package/lib/commonjs/index.js +137 -101
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +137 -101
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/layout/bottom-edge.js +148 -0
- package/lib/commonjs/layout/bottom-edge.js.map +1 -0
- package/lib/commonjs/layout/edge.js +67 -0
- package/lib/commonjs/layout/edge.js.map +1 -0
- package/lib/commonjs/layout/index.js +38 -0
- package/lib/commonjs/layout/index.js.map +1 -0
- package/lib/commonjs/provider/BloomProvider.js +5 -2
- package/lib/commonjs/provider/BloomProvider.js.map +1 -1
- package/lib/commonjs/tab-bar/TabBarBase.js +11 -1
- package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
- package/lib/commonjs/tab-bar/shared.js +0 -22
- package/lib/commonjs/tab-bar/shared.js.map +1 -1
- package/lib/commonjs/tab-bar/use-footprint.js +17 -8
- package/lib/commonjs/tab-bar/use-footprint.js.map +1 -1
- package/lib/commonjs/theme/color-scope/ColorScope.web.js +25 -13
- package/lib/commonjs/theme/color-scope/ColorScope.web.js.map +1 -1
- package/lib/commonjs/toast/Positioner.js +6 -1
- package/lib/commonjs/toast/Positioner.js.map +1 -1
- package/lib/commonjs/toast/container-geometry.js +19 -13
- package/lib/commonjs/toast/container-geometry.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +14 -4
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/fab/Fab.js +12 -3
- package/lib/module/fab/Fab.js.map +1 -1
- package/lib/module/fab/Fab.web.js +12 -4
- package/lib/module/fab/Fab.web.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -0
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/layout/bottom-edge.js +141 -0
- package/lib/module/layout/bottom-edge.js.map +1 -0
- package/lib/module/layout/edge.js +62 -0
- package/lib/module/layout/edge.js.map +1 -0
- package/lib/module/layout/index.js +11 -0
- package/lib/module/layout/index.js.map +1 -0
- package/lib/module/provider/BloomProvider.js +5 -2
- package/lib/module/provider/BloomProvider.js.map +1 -1
- package/lib/module/tab-bar/TabBarBase.js +12 -2
- package/lib/module/tab-bar/TabBarBase.js.map +1 -1
- package/lib/module/tab-bar/shared.js +0 -21
- package/lib/module/tab-bar/shared.js.map +1 -1
- package/lib/module/tab-bar/use-footprint.js +18 -9
- package/lib/module/tab-bar/use-footprint.js.map +1 -1
- package/lib/module/theme/color-scope/ColorScope.web.js +25 -13
- package/lib/module/theme/color-scope/ColorScope.web.js.map +1 -1
- package/lib/module/toast/Positioner.js +6 -1
- package/lib/module/toast/Positioner.js.map +1 -1
- package/lib/module/toast/container-geometry.js +20 -13
- package/lib/module/toast/container-geometry.js.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/fab/Fab.d.ts.map +1 -1
- package/lib/typescript/commonjs/fab/Fab.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +1 -0
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/bottom-edge.d.ts +77 -0
- package/lib/typescript/commonjs/layout/bottom-edge.d.ts.map +1 -0
- package/lib/typescript/commonjs/layout/edge.d.ts +56 -0
- package/lib/typescript/commonjs/layout/edge.d.ts.map +1 -0
- package/lib/typescript/commonjs/layout/index.d.ts +9 -0
- package/lib/typescript/commonjs/layout/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/provider/BloomProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/shared.d.ts +0 -9
- package/lib/typescript/commonjs/tab-bar/shared.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/use-footprint.d.ts +11 -7
- package/lib/typescript/commonjs/tab-bar/use-footprint.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-scope/ColorScope.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/toast/Positioner.d.ts.map +1 -1
- package/lib/typescript/commonjs/toast/container-geometry.d.ts +14 -5
- package/lib/typescript/commonjs/toast/container-geometry.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/fab/Fab.d.ts.map +1 -1
- package/lib/typescript/module/fab/Fab.web.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +1 -0
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/layout/bottom-edge.d.ts +77 -0
- package/lib/typescript/module/layout/bottom-edge.d.ts.map +1 -0
- package/lib/typescript/module/layout/edge.d.ts +56 -0
- package/lib/typescript/module/layout/edge.d.ts.map +1 -0
- package/lib/typescript/module/layout/index.d.ts +9 -0
- package/lib/typescript/module/layout/index.d.ts.map +1 -0
- package/lib/typescript/module/provider/BloomProvider.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/shared.d.ts +0 -9
- package/lib/typescript/module/tab-bar/shared.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/use-footprint.d.ts +11 -7
- package/lib/typescript/module/tab-bar/use-footprint.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-scope/ColorScope.web.d.ts.map +1 -1
- package/lib/typescript/module/toast/Positioner.d.ts.map +1 -1
- package/lib/typescript/module/toast/container-geometry.d.ts +14 -5
- package/lib/typescript/module/toast/container-geometry.d.ts.map +1 -1
- package/package.json +15 -1
- package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
- package/src/__tests__/support/constructed-style-sheets.ts +68 -0
- package/src/__tests__/support/press-host.ts +30 -0
- package/src/__tests__/support/rendered-style.ts +99 -0
- package/src/__tests__/support/unread-hook-fixture.ts +33 -0
- package/src/bottom-sheet/BottomSheetBase.tsx +14 -4
- package/src/fab/Fab.tsx +16 -3
- package/src/fab/Fab.web.tsx +16 -4
- package/src/index.ts +7 -0
- package/src/index.web.ts +7 -0
- package/src/layout/bottom-edge.tsx +162 -0
- package/src/layout/edge.ts +59 -0
- package/src/layout/index.ts +8 -0
- package/src/provider/BloomProvider.tsx +11 -1
- package/src/tab-bar/TabBarBase.tsx +11 -2
- package/src/tab-bar/shared.ts +0 -20
- package/src/tab-bar/use-footprint.ts +18 -9
- package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +7682 -0
- package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
- package/src/theme/color-scope/ColorScope.web.tsx +23 -13
- package/src/toast/Positioner.tsx +5 -0
- 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":"
|
|
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;
|
|
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;
|
|
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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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":"
|
|
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;
|
|
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;
|
|
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.
|
|
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
|
-
*
|
|
50
|
-
*
|
|
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;
|
|
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.
|
|
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 -
|
|
531
|
-
}), [insets.top,
|
|
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 ?
|
|
547
|
+
marginBottom: keyboardHeight.value > 0 ? EDGE_GAP : detachedGap,
|
|
538
548
|
};
|
|
539
549
|
}
|
|
540
550
|
return {
|
|
541
551
|
marginBottom: 0,
|
|
542
552
|
};
|
|
543
|
-
}, [
|
|
553
|
+
}, [detachedGap, detached, keyboardHeight]);
|
|
544
554
|
|
|
545
555
|
const handleBackdropPress = useCallback(() => {
|
|
546
556
|
// Always animate close on backdrop press
|