@oxyhq/bloom 0.51.0 → 0.51.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 (54) hide show
  1. package/lib/commonjs/frosted-icon-button/shared.js +1 -1
  2. package/lib/commonjs/tab-bar/surface-solid.js +43 -0
  3. package/lib/commonjs/tab-bar/surface-solid.js.map +1 -0
  4. package/lib/commonjs/tab-bar/surface.js +5 -42
  5. package/lib/commonjs/tab-bar/surface.js.map +1 -1
  6. package/lib/commonjs/tab-bar/surface.native.js +65 -8
  7. package/lib/commonjs/tab-bar/surface.native.js.map +1 -1
  8. package/lib/module/frosted-icon-button/shared.js +1 -1
  9. package/lib/module/tab-bar/surface-solid.js +37 -0
  10. package/lib/module/tab-bar/surface-solid.js.map +1 -0
  11. package/lib/module/tab-bar/surface.js +10 -23
  12. package/lib/module/tab-bar/surface.js.map +1 -1
  13. package/lib/module/tab-bar/surface.native.js +68 -9
  14. package/lib/module/tab-bar/surface.native.js.map +1 -1
  15. package/lib/typescript/commonjs/benefit-list/types.d.ts +2 -1
  16. package/lib/typescript/commonjs/benefit-list/types.d.ts.map +1 -1
  17. package/lib/typescript/commonjs/fab/types.d.ts +4 -2
  18. package/lib/typescript/commonjs/fab/types.d.ts.map +1 -1
  19. package/lib/typescript/commonjs/frosted-icon-button/shared.d.ts +1 -1
  20. package/lib/typescript/commonjs/frosted-icon-button/types.d.ts +2 -1
  21. package/lib/typescript/commonjs/frosted-icon-button/types.d.ts.map +1 -1
  22. package/lib/typescript/commonjs/tab-bar/surface-solid.d.ts +6 -0
  23. package/lib/typescript/commonjs/tab-bar/surface-solid.d.ts.map +1 -0
  24. package/lib/typescript/commonjs/tab-bar/surface.d.ts +28 -5
  25. package/lib/typescript/commonjs/tab-bar/surface.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/tab-bar/surface.native.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/tab-bar/types.d.ts +6 -3
  28. package/lib/typescript/commonjs/tab-bar/types.d.ts.map +1 -1
  29. package/lib/typescript/module/benefit-list/types.d.ts +2 -1
  30. package/lib/typescript/module/benefit-list/types.d.ts.map +1 -1
  31. package/lib/typescript/module/fab/types.d.ts +4 -2
  32. package/lib/typescript/module/fab/types.d.ts.map +1 -1
  33. package/lib/typescript/module/frosted-icon-button/shared.d.ts +1 -1
  34. package/lib/typescript/module/frosted-icon-button/types.d.ts +2 -1
  35. package/lib/typescript/module/frosted-icon-button/types.d.ts.map +1 -1
  36. package/lib/typescript/module/tab-bar/surface-solid.d.ts +6 -0
  37. package/lib/typescript/module/tab-bar/surface-solid.d.ts.map +1 -0
  38. package/lib/typescript/module/tab-bar/surface.d.ts +28 -5
  39. package/lib/typescript/module/tab-bar/surface.d.ts.map +1 -1
  40. package/lib/typescript/module/tab-bar/surface.native.d.ts.map +1 -1
  41. package/lib/typescript/module/tab-bar/types.d.ts +6 -3
  42. package/lib/typescript/module/tab-bar/types.d.ts.map +1 -1
  43. package/package.json +2 -8
  44. package/src/__tests__/TabBarGlassFallback.test.tsx +234 -0
  45. package/src/__tests__/TabBarWebFork.test.ts +46 -0
  46. package/src/__tests__/icon-references.test.ts +243 -0
  47. package/src/benefit-list/types.ts +2 -1
  48. package/src/fab/types.ts +4 -2
  49. package/src/frosted-icon-button/shared.ts +1 -1
  50. package/src/frosted-icon-button/types.ts +2 -1
  51. package/src/tab-bar/surface-solid.tsx +40 -0
  52. package/src/tab-bar/surface.native.tsx +77 -15
  53. package/src/tab-bar/surface.tsx +10 -29
  54. package/src/tab-bar/types.ts +6 -3
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9D,MAAM,MAAM,UAAU,GAAG;IACvB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,IAAI,EAAE,SAAS,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG;IACpC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC7B;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,cAAc,GAAG;IAC/C,IAAI,EAAE,UAAU,CAAC;IACjB,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9D,MAAM,MAAM,UAAU,GAAG;IACvB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;OASG;IACH,IAAI,EAAE,SAAS,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG;IACpC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC7B;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,cAAc,GAAG;IAC/C,IAAI,EAAE,UAAU,CAAC;IACjB,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC"}
@@ -3,7 +3,8 @@ import type { StyleProp, TextStyle, ViewStyle } from 'react-native';
3
3
  export interface BenefitRowProps {
4
4
  /**
5
5
  * Leading icon — rendered inside a rounded `fill-secondary` square. Pass a
6
- * Bloom icon element (e.g. `<Icons.Lock size="sm" />`) or any node.
6
+ * Bloom icon element (e.g. `<Icons.Lock_Stroke2_Corner0_Rounded size="sm" />`,
7
+ * suffix included — there is no shortened alias) or any node.
7
8
  */
8
9
  icon: ReactNode;
9
10
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/benefit-list/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpE,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,IAAI,EAAE,SAAS,CAAC;IAChB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,sEAAsE;IACtE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,6CAA6C;IAC7C,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,QAAQ,EAAE,SAAS,CAAC;IACpB,wCAAwC;IACxC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/benefit-list/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpE,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,IAAI,EAAE,SAAS,CAAC;IAChB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,sEAAsE;IACtE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,6CAA6C;IAC7C,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,QAAQ,EAAE,SAAS,CAAC;IACpB,wCAAwC;IACxC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+CAA+C;IAC/C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B"}
@@ -53,8 +53,10 @@ export interface FabProps {
53
53
  onPress?: () => void;
54
54
  /**
55
55
  * The icon to render inside the FAB. Typically a Bloom icon element
56
- * (`<Icons.Plus .../>`). For an icon-only FAB this is the whole content; for
57
- * the extended variant it sits to the left of the `label`.
56
+ * (`<Icons.PlusLarge_Stroke2_Corner0_Rounded />` icon exports carry their full
57
+ * style and corner suffix, and there is no shortened alias). For an icon-only
58
+ * FAB this is the whole content; for the extended variant it sits to the left of
59
+ * the `label`.
58
60
  *
59
61
  * `children` is accepted as an alias/override of `icon` for ergonomics — if
60
62
  * both are given, `icon` wins and `children` is ignored.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/fab/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpE;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7D;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,YAAY,GACpB,cAAc,GACd,aAAa,GACb,WAAW,GACX,UAAU,GACV,QAAQ,CAAC;AAEb,MAAM,WAAW,QAAQ;IACvB,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAExB;;;;OAIG;IACH,SAAS,CAAC,EAAE,YAAY,CAAC;IAEzB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,8CAA8C;IAC9C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,8CAA8C;IAC9C,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAUhB,wDAAwD;IACxD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;IAC/D,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;CACtC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/fab/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpE;;;;;;;;GAQG;AACH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;AAE7D;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEnD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,YAAY,GACpB,cAAc,GACd,aAAa,GACb,WAAW,GACX,UAAU,GACV,QAAQ,CAAC;AAEb,MAAM,WAAW,QAAQ;IACvB,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAE3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAExB;;;;OAIG;IACH,SAAS,CAAC,EAAE,YAAY,CAAC;IAEzB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,8CAA8C;IAC9C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,8CAA8C;IAC9C,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAUhB,wDAAwD;IACxD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;IAC/D,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;CACtC"}
@@ -52,7 +52,7 @@ export declare function resolveFrostedPalette(colors: ThemeColors, isDark: boole
52
52
  * Inject `color` as the icon's `fill` — but only as a FALLBACK: if the caller
53
53
  * already set an explicit `fill` on the icon element, it wins and the node is
54
54
  * returned untouched. Bloom icons resolve their paint as `fill ?? style.color`,
55
- * so a bare `<Icons.X />` gets the theme-aware color for free while an
55
+ * so any bare Bloom icon element gets the theme-aware color for free while an
56
56
  * explicitly-colored icon is never overridden. Non-element children (plain
57
57
  * text/strings) pass through unchanged.
58
58
  */
@@ -15,7 +15,8 @@ export interface FrostedIconButtonProps {
15
15
  onPress?: () => void;
16
16
  /**
17
17
  * The icon to render, centered inside the circle. Typically a Bloom icon
18
- * element (`<Icons.ArrowLeft… />`). If the icon does not set its own `fill`,
18
+ * element (`<Icons.ArrowLeft_Stroke2_Corner0_Rounded />` icon exports carry
19
+ * their full style and corner suffix). If the icon does not set its own `fill`,
19
20
  * the button injects the theme-aware icon color (foreground when frosted,
20
21
  * `primaryForeground` when `active`) as a fallback so a bare icon is colored
21
22
  * correctly out of the box; an explicit `fill` on the icon always wins.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/frosted-icon-button/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD;;;;;;;;;GASG;AACH,MAAM,MAAM,qBAAqB,GAAG,IAAI,GAAG,IAAI,CAAC;AAEhD,MAAM,WAAW,sBAAsB;IACrC,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAE3B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,IAAI,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAAC;IAEtC,4EAA4E;IAC5E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,8CAA8C;IAC9C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,OAAO,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAUvE,wDAAwD;IACxD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;IAC/D,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;CACtC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/frosted-icon-button/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD;;;;;;;;;GASG;AACH,MAAM,MAAM,qBAAqB,GAAG,IAAI,GAAG,IAAI,CAAC;AAEhD,MAAM,WAAW,sBAAsB;IACrC,qBAAqB;IACrB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAE3B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;OAGG;IACH,IAAI,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAAC;IAEtC,4EAA4E;IAC5E,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,8CAA8C;IAC9C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,OAAO,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAUvE,wDAAwD;IACxD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,KAAK,IAAI,CAAC;IAC/D,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;CACtC"}
@@ -0,0 +1,6 @@
1
+ import type { TabBarSurfaceProps } from './shared';
2
+ export declare function SolidTabBarSurface({ theme, style }: TabBarSurfaceProps): import("react/jsx-runtime").JSX.Element;
3
+ export declare namespace SolidTabBarSurface {
4
+ var displayName: string;
5
+ }
6
+ //# sourceMappingURL=surface-solid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"surface-solid.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/surface-solid.tsx"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,kBAAkB,2CAWtE;yBAXe,kBAAkB"}
@@ -1,6 +1,29 @@
1
- import type { TabBarSurfaceProps } from './shared';
2
- export declare function TabBarSurface({ theme, style }: TabBarSurfaceProps): import("react/jsx-runtime").JSX.Element;
3
- export declare namespace TabBarSurface {
4
- var displayName: string;
5
- }
1
+ /**
2
+ * Ported from expo-glass-tabs v0.1.1 src/glass-tab-bar.tsx
3
+ * (MIT © 2026 David Mokos). See the top-level NOTICE.
4
+ *
5
+ * NEUTRAL default variant of the tab bar's capsule surface: the non-glass one.
6
+ *
7
+ * This is the file resolved by everything that does NOT do React Native's
8
+ * platform-extension resolution — a consumer's `tsc` (which follows Bloom's
9
+ * `"react-native"` source export condition) and any non-Metro bundler reading
10
+ * the published `lib/`. It therefore imports NOTHING platform-specific:
11
+ * `expo-glass-effect` is native-only, and a static import of it from a file a
12
+ * web bundler or a consumer's type-checker resolves would break module
13
+ * resolution outright. Same three-way split, for the same reason, as
14
+ * `theme/native-root-vars{,.native,.web}.ts`.
15
+ *
16
+ * The surface itself lives in `surface-solid.tsx`, because `surface.native.tsx`
17
+ * renders it as its own fallback and cannot import this file to get it — Metro
18
+ * resolves `./surface` back to `surface.native.tsx`, i.e. to itself. This module's
19
+ * job is to give that one implementation the name every entry module imports.
20
+ *
21
+ * The variants — all three implement `TabBarSurfaceProps` exactly, so the entry
22
+ * modules can swap them freely:
23
+ * - `surface.native.tsx` — iOS/Android (Metro), real `UIGlassEffect` glass, with
24
+ * the solid surface as its fallback;
25
+ * - `surface.web.tsx` — web, a CSS `backdrop-filter`;
26
+ * - this file — everywhere else, the opaque fallback alone.
27
+ */
28
+ export { SolidTabBarSurface as TabBarSurface } from './surface-solid';
6
29
  //# sourceMappingURL=surface.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"surface.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/surface.tsx"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAEnD,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,kBAAkB,2CAWjE;yBAXe,aAAa"}
1
+ {"version":3,"file":"surface.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/surface.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAE,kBAAkB,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"surface.native.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/surface.native.tsx"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAInD,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,kBAAkB,2CAyBjE;yBAzBe,aAAa"}
1
+ {"version":3,"file":"surface.native.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/surface.native.tsx"],"names":[],"mappings":"AAmDA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AA8CnD,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,kBAAkB,2CAgBjE;yBAhBe,aAAa"}
@@ -18,9 +18,12 @@ export type TabBarItem = {
18
18
  /** Text under the icon; fades out and is clipped when the bar minimizes. */
19
19
  label: string;
20
20
  /**
21
- * The glyph, as a Bloom icon element (`<Icons.Home />`) or any node. This is
22
- * the primary icon API same convention as `Tabs`/`TabsTrigger` and
23
- * `FrostedIconButton`. The bar renders it TWICE (an inactive layer with the
21
+ * The glyph, as a Bloom icon element (`<Icons.Home_Stroke2_Corner0_Rounded />`)
22
+ * or any node. Every `@oxyhq/bloom/icons` export carries its full style and
23
+ * corner suffix; a shortened `Home` is not an export, and a name that does not
24
+ * exist is simply `undefined`, which React then rejects as an invalid element
25
+ * type. This is the primary icon API — same convention as `Tabs`/`TabsTrigger`
26
+ * and `FrostedIconButton`. The bar renders it TWICE (an inactive layer with the
24
27
  * active layer crossfading on top), injecting the layer's tint as the icon's
25
28
  * `fill` unless the element already sets one.
26
29
  */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9D,MAAM,MAAM,UAAU,GAAG;IACvB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,IAAI,EAAE,SAAS,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG;IACpC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC7B;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,cAAc,GAAG;IAC/C,IAAI,EAAE,UAAU,CAAC;IACjB,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/tab-bar/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9D,MAAM,MAAM,UAAU,GAAG;IACvB,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;OASG;IACH,IAAI,EAAE,SAAS,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG;IACpC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC7B;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,cAAc,GAAG;IAC/C,IAAI,EAAE,UAAU,CAAC;IACjB,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;OASG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.51.0",
3
+ "version": "0.51.1",
4
4
  "description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -1189,7 +1189,7 @@
1189
1189
  "expo": "*",
1190
1190
  "expo-blur": "*",
1191
1191
  "expo-font": "*",
1192
- "expo-glass-effect": ">=0.1.5",
1192
+ "expo-glass-effect": ">=0.1.9",
1193
1193
  "expo-haptics": "*",
1194
1194
  "expo-image": "*",
1195
1195
  "expo-router": ">=3.0.0",
@@ -1211,18 +1211,12 @@
1211
1211
  "expo-font": {
1212
1212
  "optional": true
1213
1213
  },
1214
- "expo-glass-effect": {
1215
- "optional": true
1216
- },
1217
1214
  "expo-haptics": {
1218
1215
  "optional": true
1219
1216
  },
1220
1217
  "expo-router": {
1221
1218
  "optional": true
1222
1219
  },
1223
- "expo-symbols": {
1224
- "optional": true
1225
- },
1226
1220
  "react-dom": {
1227
1221
  "optional": true
1228
1222
  },
@@ -0,0 +1,234 @@
1
+ import React from 'react';
2
+ import type { ReactTestInstance } from 'react-test-renderer';
3
+ import { render } from '@testing-library/react-native';
4
+
5
+ import type { TabBarSurfaceProps } from '../tab-bar/shared';
6
+ import type { TabBarTheme } from '../tab-bar/types';
7
+
8
+ /**
9
+ * The native capsule surface must DEGRADE, never crash, when `expo-glass-effect`
10
+ * is not really usable.
11
+ *
12
+ * This is the one thing the rest of the tab-bar suite structurally cannot see.
13
+ * `TabBar.test.tsx` renders the native surface against the manual mock in
14
+ * `__mocks__/expo-glass-effect.ts`, which is a HEALTHY install reporting
15
+ * `isLiquidGlassAvailable() === false` — so it only ever exercises the branch a
16
+ * correctly-packaged Android device takes. Every failure mode below is a
17
+ * different install: the module resolved but handed back no exports, or the JS
18
+ * package is there while the native module is not.
19
+ *
20
+ * Each case is re-mocked into its own module registry because both hazards live at
21
+ * MODULE scope. `Animated.createAnimatedComponent(GlassView)` runs on import, and
22
+ * on iOS `isLiquidGlassAvailable()` reaches a native module through
23
+ * `requireNativeModule`, which throws by contract when it is absent. Mocking after
24
+ * the module under test was imported would test a render branch and miss both.
25
+ *
26
+ * The two counter-tests at the end are what keep the rest honest: a guard that
27
+ * always fell through to the solid surface would satisfy every other assertion
28
+ * here while silently deleting the glass look on every iOS 26 device.
29
+ */
30
+
31
+ /** Every export may be absent — that is the whole point of these fixtures. */
32
+ interface GlassEffectModule {
33
+ GlassView?: unknown;
34
+ isLiquidGlassAvailable?: () => boolean;
35
+ isGlassEffectAPIAvailable?: () => boolean;
36
+ }
37
+
38
+ /**
39
+ * A host tag stands in for the real `GlassView` component: under the reanimated
40
+ * mock `createAnimatedComponent` is the identity, so whatever the package exports
41
+ * IS the element type, and a string host is the cheapest valid one to assert on.
42
+ * It matches the tag the manual mock renders.
43
+ */
44
+ const GLASS_HOST = 'GlassView';
45
+
46
+ const THEME: TabBarTheme = {
47
+ activeTint: 'rgb(20 20 20)',
48
+ inactiveTint: 'rgb(120 120 120)',
49
+ highlight: 'rgb(230 230 230)',
50
+ glassTint: 'rgba(255, 255, 255, 0.55)',
51
+ solidFallback: 'rgb(246 246 246)',
52
+ };
53
+
54
+ const STYLE = { borderRadius: 29 };
55
+
56
+ function flattenStyle(style: unknown): Record<string, unknown> {
57
+ const out: Record<string, unknown> = {};
58
+ const visit = (s: unknown): void => {
59
+ if (Array.isArray(s)) s.forEach(visit);
60
+ else if (s && typeof s === 'object') Object.assign(out, s);
61
+ };
62
+ visit(style);
63
+ return out;
64
+ }
65
+
66
+ /** Every host node rendered as `tag`. */
67
+ function hosts(root: ReactTestInstance, tag: string): ReactTestInstance[] {
68
+ return root.findAll((node) => typeof node.type === 'string' && node.type === tag);
69
+ }
70
+
71
+ /**
72
+ * Render `surface.native.tsx` against a given `expo-glass-effect`.
73
+ *
74
+ * Only the surface comes from the isolated registry — testing-library registers
75
+ * suite-level hooks when it loads, so it has to be imported at the top of the file
76
+ * rather than inside a test. Nothing in the rendered tree calls a hook, and the
77
+ * surface renders host strings from the reanimated mock, so the two registries
78
+ * never have to agree on a React instance. `times` re-renders within one registry,
79
+ * for the assertion about module-scoped memoization.
80
+ */
81
+ function renderNativeSurface(glassEffect: GlassEffectModule, times = 1): ReactTestInstance {
82
+ let root: ReactTestInstance | undefined;
83
+
84
+ jest.isolateModules(() => {
85
+ jest.doMock('expo-glass-effect', () => glassEffect);
86
+
87
+ const { TabBarSurface } = require('../tab-bar/surface.native') as {
88
+ TabBarSurface: React.ComponentType<TabBarSurfaceProps>;
89
+ };
90
+
91
+ for (let i = 0; i < times; i += 1) {
92
+ root = render(<TabBarSurface theme={THEME} style={STYLE} />).UNSAFE_root;
93
+ }
94
+ });
95
+
96
+ if (!root) throw new Error('the native surface did not render');
97
+ return root;
98
+ }
99
+
100
+ /** Assert the tree is the opaque non-glass surface, filled from the theme. */
101
+ function expectSolidFallback(root: ReactTestInstance): void {
102
+ expect(hosts(root, GLASS_HOST)).toHaveLength(0);
103
+ const surface = hosts(root, 'Animated.View')[0];
104
+ expect(surface).toBeTruthy();
105
+ expect(flattenStyle(surface?.props.style).backgroundColor).toBe(THEME.solidFallback);
106
+ // The animated capsule radius must survive the fallback — the pill still
107
+ // reshapes as the bar minimizes.
108
+ expect(flattenStyle(surface?.props.style).borderRadius).toBe(STYLE.borderRadius);
109
+ }
110
+
111
+ describe('TabBarSurface (native) — degrading when glass is unusable', () => {
112
+ let warn: jest.SpyInstance<void, Parameters<typeof console.warn>>;
113
+
114
+ beforeEach(() => {
115
+ jest.resetModules();
116
+ warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
117
+ });
118
+
119
+ afterEach(() => {
120
+ warn.mockRestore();
121
+ });
122
+
123
+ it('falls back when the module resolved without GlassView', () => {
124
+ // `createAnimatedComponent(undefined)` produces an element type React rejects
125
+ // with "Element type is invalid … but got: undefined" — and it happens at
126
+ // import, so the whole screen the bar is on goes down with it.
127
+ expectSolidFallback(
128
+ renderNativeSurface({ GlassView: undefined, isLiquidGlassAvailable: () => true }),
129
+ );
130
+ });
131
+
132
+ it('falls back when the module delivered no exports at all', () => {
133
+ // A stub, an alias, or a resolver that landed on a platform-specific file:
134
+ // there is no `isLiquidGlassAvailable` to call either, so calling it blindly
135
+ // is a TypeError.
136
+ expectSolidFallback(renderNativeSurface({}));
137
+ });
138
+
139
+ it('falls back when the native module is missing from the build', () => {
140
+ // On iOS `isLiquidGlassAvailable()` resolves `ExpoGlassEffect` through
141
+ // `requireNativeModule`, which THROWS by contract when the module is not in
142
+ // the binary — an OTA JS update landing on a binary built before the package
143
+ // was linked reaches exactly this state.
144
+ expectSolidFallback(
145
+ renderNativeSurface({
146
+ GlassView: GLASS_HOST,
147
+ isLiquidGlassAvailable: () => {
148
+ throw new Error("Cannot find native module 'ExpoGlassEffect'");
149
+ },
150
+ }),
151
+ );
152
+ });
153
+
154
+ it('reports the missing native module instead of swallowing it', () => {
155
+ renderNativeSurface({
156
+ GlassView: GLASS_HOST,
157
+ isLiquidGlassAvailable: () => {
158
+ throw new Error("Cannot find native module 'ExpoGlassEffect'");
159
+ },
160
+ });
161
+
162
+ expect(warn).toHaveBeenCalledTimes(1);
163
+ const [message, detail] = warn.mock.calls[0] ?? [];
164
+ expect(String(message)).toContain('expo-glass-effect');
165
+ expect(String(detail)).toContain("Cannot find native module 'ExpoGlassEffect'");
166
+ });
167
+
168
+ it('falls back when the platform has no glass API (iOS 26 betas)', () => {
169
+ // Upstream's own guard: on those builds `isLiquidGlassAvailable()` answers
170
+ // true while touching `GlassView` crashes NATIVELY, below JS.
171
+ expectSolidFallback(
172
+ renderNativeSurface({
173
+ GlassView: GLASS_HOST,
174
+ isLiquidGlassAvailable: () => true,
175
+ isGlassEffectAPIAvailable: () => false,
176
+ }),
177
+ );
178
+ });
179
+
180
+ it('renders real glass when the package is fully there', () => {
181
+ const root = renderNativeSurface({
182
+ GlassView: GLASS_HOST,
183
+ isLiquidGlassAvailable: () => true,
184
+ isGlassEffectAPIAvailable: () => true,
185
+ });
186
+
187
+ const glass = hosts(root, GLASS_HOST)[0];
188
+ expect(glass).toBeTruthy();
189
+ expect(glass?.props.glassEffectStyle).toBe('regular');
190
+ expect(flattenStyle(glass?.props.style).backgroundColor).toBe(THEME.glassTint);
191
+ expect(warn).not.toHaveBeenCalled();
192
+ });
193
+
194
+ it('renders glass on a version that predates isGlassEffectAPIAvailable', () => {
195
+ // The probe first shipped in 0.1.9, so the `>=0.1.9` peer floor guarantees it
196
+ // — but a peer range only WARNS, so a consumer can still resolve an older
197
+ // copy. A missing probe means "not applicable", never "unavailable"; treating
198
+ // it as the latter would delete the glass look on every working device.
199
+ expect(
200
+ hosts(
201
+ renderNativeSurface({ GlassView: GLASS_HOST, isLiquidGlassAvailable: () => true }),
202
+ GLASS_HOST,
203
+ ),
204
+ ).toHaveLength(1);
205
+ });
206
+
207
+ it('probes availability once per session, not once per render', () => {
208
+ let probes = 0;
209
+ renderNativeSurface(
210
+ {
211
+ GlassView: GLASS_HOST,
212
+ isLiquidGlassAvailable: () => {
213
+ probes += 1;
214
+ return true;
215
+ },
216
+ },
217
+ 3,
218
+ );
219
+ expect(probes).toBe(1);
220
+ });
221
+
222
+ it('reuses the neutral surface rather than a second implementation', () => {
223
+ // Two copies of the fallback would drift: the neutral variant is what web
224
+ // bundlers and a consumer's `tsc` resolve, and it must paint exactly what an
225
+ // Android device paints.
226
+ jest.isolateModules(() => {
227
+ const { TabBarSurface } = require('../tab-bar/surface') as { TabBarSurface: unknown };
228
+ const { SolidTabBarSurface } = require('../tab-bar/surface-solid') as {
229
+ SolidTabBarSurface: unknown;
230
+ };
231
+ expect(TabBarSurface).toBe(SolidTabBarSurface);
232
+ });
233
+ });
234
+ });
@@ -50,6 +50,7 @@ const NEUTRAL_AND_WEB_FILES = [
50
50
  'tab-bar/TabBarBase.tsx',
51
51
  'tab-bar/minimize-context.tsx',
52
52
  'tab-bar/surface.tsx',
53
+ 'tab-bar/surface-solid.tsx',
53
54
  'tab-bar/surface.web.tsx',
54
55
  'tab-bar/glyph.tsx',
55
56
  'progressive-blur/index.web.tsx',
@@ -128,6 +129,17 @@ describe('tab-bar platform split', () => {
128
129
  expect(moduleSpecifiers(read('tab-bar/glyph.native.tsx'))).toContain('expo-symbols');
129
130
  });
130
131
 
132
+ it('the native surface takes its fallback from surface-solid, never ./surface', () => {
133
+ // Metro resolves a bare `./surface` to `surface.native.tsx` from ANY caller,
134
+ // so importing the neutral sibling by that name would be a self-import — which
135
+ // is the entire reason `surface-solid.tsx` exists as a separate name.
136
+ const specifiers = moduleSpecifiers(read('tab-bar/surface.native.tsx'));
137
+ expect(specifiers).toContain('./surface-solid');
138
+ expect(specifiers).not.toContain('./surface');
139
+ // And the neutral variant must be that same component, not a second copy.
140
+ expect(moduleSpecifiers(read('tab-bar/surface.tsx'))).toContain('./surface-solid');
141
+ });
142
+
131
143
  it('the router packages live only under tab-bar/expo-router', () => {
132
144
  const router = moduleSpecifiers(read('tab-bar/expo-router/RouterTabBar.tsx'));
133
145
  expect(router.some((s) => isFrom(s, 'expo-router'))).toBe(true);
@@ -160,6 +172,8 @@ describe('tab-bar platform split', () => {
160
172
  describe('tab-bar published surface', () => {
161
173
  const pkg = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf8')) as {
162
174
  exports: Record<string, { browser?: { import?: string; require?: string } }>;
175
+ peerDependencies: Record<string, string>;
176
+ peerDependenciesMeta: Record<string, { optional?: boolean }>;
163
177
  };
164
178
 
165
179
  it('exports ./tab-bar with a browser condition → index.web.js', () => {
@@ -184,6 +198,38 @@ describe('tab-bar published surface', () => {
184
198
  expect(entry?.browser).toBeUndefined();
185
199
  });
186
200
 
201
+ it('declares the two Apple-only native peers REQUIRED, keeping their ranges', () => {
202
+ // `expo-glass-effect` and `expo-symbols` are imported STATICALLY, by
203
+ // `tab-bar/surface.native` and `tab-bar/glyph.native`. That is what settles
204
+ // this: optional means "you may omit this", but omitting it does not degrade
205
+ // — the native bundle fails to RESOLVE the specifier and the build dies. An
206
+ // optional peer plus a static import is an incoherent pair, and the only
207
+ // thing marking them optional would achieve is silencing the one warning
208
+ // that tells a consumer their build is about to break. Declared the same way
209
+ // `expo-blur` already is, for the same reason.
210
+ //
211
+ // The RANGE carries the second half: both files import symbols that arrived
212
+ // after 1.0 — `isGlassEffectAPIAvailable` first shipped in
213
+ // expo-glass-effect 0.1.9 — so an older installed copy is a real
214
+ // incompatibility, not merely an absent one.
215
+ for (const peer of ['expo-glass-effect', 'expo-symbols']) {
216
+ expect(pkg.peerDependencies[peer]).toMatch(/^>=\d/);
217
+ expect(pkg.peerDependenciesMeta[peer]).toBeUndefined();
218
+ }
219
+ });
220
+
221
+ it('keeps the cross-platform native peers REQUIRED', () => {
222
+ // `expo-blur` and `expo-image` are apple+android native modules imported from
223
+ // platform-NEUTRAL files (`progressive-blur/index.tsx`,
224
+ // `frosted-icon-button/FrostedIconButton.tsx`, `zoomable-image-gallery/`), so
225
+ // every native platform genuinely needs them. Marking them optional would
226
+ // silence the one warning that tells a consumer their build is about to fail.
227
+ for (const peer of ['expo-blur', 'expo-image']) {
228
+ expect(pkg.peerDependencies[peer]).toBeDefined();
229
+ expect(pkg.peerDependenciesMeta[peer]).toBeUndefined();
230
+ }
231
+ });
232
+
187
233
  it('the generator lists exactly the right subpaths as web-forked', () => {
188
234
  const gen = readFileSync(join(REPO_ROOT, 'scripts/generate-platform-exports.mjs'), 'utf8');
189
235
  const block = /const WEB_FORKED_SUBPATHS = new Set\(\[([\s\S]*?)\]\);/.exec(gen)?.[1];