@oxyhq/bloom 1.9.2 → 1.10.0
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/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/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/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/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/toast/Positioner.tsx +5 -0
- package/src/toast/container-geometry.ts +20 -8
|
@@ -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":"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.0",
|
|
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
|
package/src/fab/Fab.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from 'react-native';
|
|
12
12
|
import { styled } from 'react-native-css';
|
|
13
13
|
|
|
14
|
+
import { useBottomEdgeInset } from '../layout/bottom-edge';
|
|
14
15
|
import { useTheme } from '../theme/use-theme';
|
|
15
16
|
import { animation, borderRadius } from '../styles/tokens';
|
|
16
17
|
import { bloomShadowStyle } from '../design-tokens/shadows';
|
|
@@ -65,12 +66,23 @@ const HIT_SLOP = { top: 8, bottom: 8, left: 8, right: 8 } as const;
|
|
|
65
66
|
* the nearest positioned ancestor (the consumer's column container), NOT the
|
|
66
67
|
* screen — so it never escapes a constrained layout. `static` returns no
|
|
67
68
|
* positioning (consumer-controlled).
|
|
69
|
+
*
|
|
70
|
+
* `bottomEdgeInset` applies to the BOTTOM axis only, and only for a bottom
|
|
71
|
+
* placement. It is not a gap preference — `offset` is that — but the height of
|
|
72
|
+
* whatever floating surface has already claimed the bottom edge, so the FAB
|
|
73
|
+
* lands above a floating tab bar instead of behind it. A z-index cannot fix that
|
|
74
|
+
* pairing: the bar's host is the last sibling of the app shell and paints over
|
|
75
|
+
* every descendant, so the FAB has to be somewhere else, not merely on top.
|
|
68
76
|
*/
|
|
69
|
-
function placementStyle(
|
|
77
|
+
function placementStyle(
|
|
78
|
+
placement: FabPlacement,
|
|
79
|
+
offset: number,
|
|
80
|
+
bottomEdgeInset: number,
|
|
81
|
+
): ViewStyle {
|
|
70
82
|
if (placement === 'static') return {};
|
|
71
83
|
const style: ViewStyle = { position: 'absolute' };
|
|
72
84
|
if (placement === 'bottom-right' || placement === 'bottom-left') {
|
|
73
|
-
style.bottom = offset;
|
|
85
|
+
style.bottom = offset + bottomEdgeInset;
|
|
74
86
|
} else {
|
|
75
87
|
style.top = offset;
|
|
76
88
|
}
|
|
@@ -145,6 +157,7 @@ const FabComponent: React.FC<FabProps> = ({
|
|
|
145
157
|
testID,
|
|
146
158
|
zIndex = DEFAULT_Z_INDEX,
|
|
147
159
|
}) => {
|
|
160
|
+
const bottomEdgeInset = useBottomEdgeInset();
|
|
148
161
|
const theme = useTheme();
|
|
149
162
|
const sizeConfig = useMemo(() => resolveSize(size), [size]);
|
|
150
163
|
const isExtended = label != null && label.length > 0;
|
|
@@ -213,7 +226,7 @@ const FabComponent: React.FC<FabProps> = ({
|
|
|
213
226
|
<AnimatedPressable
|
|
214
227
|
className={className}
|
|
215
228
|
style={[
|
|
216
|
-
placementStyle(placement, offset),
|
|
229
|
+
placementStyle(placement, offset, bottomEdgeInset),
|
|
217
230
|
{ zIndex },
|
|
218
231
|
containerStyle,
|
|
219
232
|
disabled && { opacity: 0.5 },
|
package/src/fab/Fab.web.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import React, {
|
|
|
7
7
|
type MouseEvent,
|
|
8
8
|
} from 'react';
|
|
9
9
|
|
|
10
|
+
import { useBottomEdgeInset } from '../layout/bottom-edge';
|
|
10
11
|
import { useTheme } from '../theme/use-theme';
|
|
11
12
|
import { animation, borderRadius } from '../styles/tokens';
|
|
12
13
|
import { pressedSurface } from '../theme/press-colors';
|
|
@@ -121,14 +122,24 @@ const BLOOM_FAB_CSS = interactiveWebCss({
|
|
|
121
122
|
* Horizontal placement uses `align-self` (sticky elements can't be pushed by
|
|
122
123
|
* `left`/`right` the way absolute ones are) plus an inline-edge margin for the
|
|
123
124
|
* `offset`. The consumer column MUST be a flex column that fills the available
|
|
124
|
-
* height, and the FAB MUST be its last child — documented in the usage story.
|
|
125
|
+
* height, and the FAB MUST be its last child — documented in the usage story. *
|
|
126
|
+
* `bottomEdgeInset` applies to the BOTTOM axis only. It is not a gap preference
|
|
127
|
+
* — `offset` is that — but the height of whatever floating surface has already
|
|
128
|
+
* claimed the bottom edge, so the FAB lands above a floating tab bar instead of
|
|
129
|
+
* behind it. A z-index cannot fix that pairing: the bar's host is the last
|
|
130
|
+
* sibling of the app shell and paints over every descendant, so the FAB has to be
|
|
131
|
+
* somewhere else, not merely on top.
|
|
125
132
|
*/
|
|
126
|
-
function placementStyle(
|
|
133
|
+
function placementStyle(
|
|
134
|
+
placement: FabPlacement,
|
|
135
|
+
offset: number,
|
|
136
|
+
bottomEdgeInset: number,
|
|
137
|
+
): CSSProperties {
|
|
127
138
|
if (placement === 'static') return {};
|
|
128
139
|
const style: CSSProperties = { position: 'sticky' };
|
|
129
140
|
const isBottom = placement === 'bottom-right' || placement === 'bottom-left';
|
|
130
141
|
if (isBottom) {
|
|
131
|
-
style.bottom = offset;
|
|
142
|
+
style.bottom = offset + bottomEdgeInset;
|
|
132
143
|
// Push the FAB to the bottom of a (flex-column) container even when the
|
|
133
144
|
// content is too short to scroll, so it never floats mid-column.
|
|
134
145
|
style.marginTop = 'auto';
|
|
@@ -186,6 +197,7 @@ const FabWebComponent: React.FC<FabProps> = ({
|
|
|
186
197
|
type = 'button',
|
|
187
198
|
}) => {
|
|
188
199
|
useInteractiveWebCss(STYLE_ID, BLOOM_FAB_CSS);
|
|
200
|
+
const bottomEdgeInset = useBottomEdgeInset();
|
|
189
201
|
const theme = useTheme();
|
|
190
202
|
const reactId = useId();
|
|
191
203
|
const resolvedId = id ?? `bloom-fab-${reactId}`;
|
|
@@ -220,7 +232,7 @@ const FabWebComponent: React.FC<FabProps> = ({
|
|
|
220
232
|
variantColors.foreground,
|
|
221
233
|
),
|
|
222
234
|
['--bloom-fab-press-scale' as string]: animation.pressScale,
|
|
223
|
-
...placementStyle(placement, offset),
|
|
235
|
+
...placementStyle(placement, offset, bottomEdgeInset),
|
|
224
236
|
};
|
|
225
237
|
if (isExtended) {
|
|
226
238
|
const pad = sizeConfig.diameter <= 44 ? 14 : 20;
|
package/src/index.ts
CHANGED
|
@@ -153,6 +153,13 @@ export type {
|
|
|
153
153
|
SurfacePromptOptions,
|
|
154
154
|
} from './surfaces';
|
|
155
155
|
export * from './button';
|
|
156
|
+
export {
|
|
157
|
+
EDGE_GAP,
|
|
158
|
+
windowEdgeGap,
|
|
159
|
+
BottomEdgeProvider,
|
|
160
|
+
useBottomEdgeInset,
|
|
161
|
+
useClaimBottomEdge,
|
|
162
|
+
} from './layout';
|
|
156
163
|
export { Fab } from './fab';
|
|
157
164
|
export type { FabProps, FabVariant, FabSize, FabPlacement } from './fab';
|
|
158
165
|
export { FrostedIconButton } from './frosted-icon-button';
|