@oxy.so/bloom 4.18.1 → 4.19.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 (126) hide show
  1. package/docs/adoption-matrix.mdx +2 -2
  2. package/docs/overlay.mdx +11 -0
  3. package/docs/portal.mdx +37 -0
  4. package/docs/rating.mdx +19 -2
  5. package/lib/commonjs/bottom-sheet/BottomSheet.web.js +1 -0
  6. package/lib/commonjs/bottom-sheet/BottomSheet.web.js.map +1 -1
  7. package/lib/commonjs/dialog/Dialog.web.js +4 -1
  8. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  9. package/lib/commonjs/index.js +7 -0
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/index.web.js +7 -0
  12. package/lib/commonjs/index.web.js.map +1 -1
  13. package/lib/commonjs/input-otp/InputOtp.js +13 -3
  14. package/lib/commonjs/input-otp/InputOtp.js.map +1 -1
  15. package/lib/commonjs/overlay/Overlay.js +4 -0
  16. package/lib/commonjs/overlay/Overlay.js.map +1 -1
  17. package/lib/commonjs/overlay/OverlayInertBoundary.js +78 -0
  18. package/lib/commonjs/overlay/OverlayInertBoundary.js.map +1 -0
  19. package/lib/commonjs/overlay/index.js +14 -0
  20. package/lib/commonjs/overlay/index.js.map +1 -1
  21. package/lib/commonjs/overlay/modal-registry.js +104 -0
  22. package/lib/commonjs/overlay/modal-registry.js.map +1 -0
  23. package/lib/commonjs/overlay/use-modal-overlay-active.js +33 -0
  24. package/lib/commonjs/overlay/use-modal-overlay-active.js.map +1 -0
  25. package/lib/commonjs/portal/index.js +7 -0
  26. package/lib/commonjs/portal/index.js.map +1 -1
  27. package/lib/commonjs/portal/index.web.js +7 -0
  28. package/lib/commonjs/portal/index.web.js.map +1 -1
  29. package/lib/commonjs/rating/Rating.js +33 -2
  30. package/lib/commonjs/rating/Rating.js.map +1 -1
  31. package/lib/module/bottom-sheet/BottomSheet.web.js +1 -0
  32. package/lib/module/bottom-sheet/BottomSheet.web.js.map +1 -1
  33. package/lib/module/dialog/Dialog.web.js +4 -1
  34. package/lib/module/dialog/Dialog.web.js.map +1 -1
  35. package/lib/module/index.js +1 -1
  36. package/lib/module/index.js.map +1 -1
  37. package/lib/module/index.web.js +1 -1
  38. package/lib/module/index.web.js.map +1 -1
  39. package/lib/module/input-otp/InputOtp.js +13 -3
  40. package/lib/module/input-otp/InputOtp.js.map +1 -1
  41. package/lib/module/overlay/Overlay.js +4 -0
  42. package/lib/module/overlay/Overlay.js.map +1 -1
  43. package/lib/module/overlay/OverlayInertBoundary.js +73 -0
  44. package/lib/module/overlay/OverlayInertBoundary.js.map +1 -0
  45. package/lib/module/overlay/index.js +2 -0
  46. package/lib/module/overlay/index.js.map +1 -1
  47. package/lib/module/overlay/modal-registry.js +96 -0
  48. package/lib/module/overlay/modal-registry.js.map +1 -0
  49. package/lib/module/overlay/use-modal-overlay-active.js +28 -0
  50. package/lib/module/overlay/use-modal-overlay-active.js.map +1 -0
  51. package/lib/module/portal/index.js +4 -0
  52. package/lib/module/portal/index.js.map +1 -1
  53. package/lib/module/portal/index.web.js +4 -0
  54. package/lib/module/portal/index.web.js.map +1 -1
  55. package/lib/module/rating/Rating.js +32 -2
  56. package/lib/module/rating/Rating.js.map +1 -1
  57. package/lib/typescript/commonjs/bottom-sheet/BottomSheet.web.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts +2 -1
  59. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/index.d.ts +1 -1
  61. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/index.web.d.ts +1 -1
  63. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/input-otp/InputOtp.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/overlay/Overlay.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/overlay/OverlayInertBoundary.d.ts +6 -0
  67. package/lib/typescript/commonjs/overlay/OverlayInertBoundary.d.ts.map +1 -0
  68. package/lib/typescript/commonjs/overlay/index.d.ts +3 -1
  69. package/lib/typescript/commonjs/overlay/index.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/overlay/modal-registry.d.ts +53 -0
  71. package/lib/typescript/commonjs/overlay/modal-registry.d.ts.map +1 -0
  72. package/lib/typescript/commonjs/overlay/types.d.ts +21 -3
  73. package/lib/typescript/commonjs/overlay/types.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/overlay/use-modal-overlay-active.d.ts +7 -0
  75. package/lib/typescript/commonjs/overlay/use-modal-overlay-active.d.ts.map +1 -0
  76. package/lib/typescript/commonjs/portal/index.d.ts +2 -0
  77. package/lib/typescript/commonjs/portal/index.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/portal/index.web.d.ts +2 -0
  79. package/lib/typescript/commonjs/portal/index.web.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/rating/Rating.d.ts +9 -1
  81. package/lib/typescript/commonjs/rating/Rating.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/rating/types.d.ts +9 -0
  83. package/lib/typescript/commonjs/rating/types.d.ts.map +1 -1
  84. package/lib/typescript/module/bottom-sheet/BottomSheet.web.d.ts.map +1 -1
  85. package/lib/typescript/module/dialog/Dialog.web.d.ts +2 -1
  86. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  87. package/lib/typescript/module/index.d.ts +1 -1
  88. package/lib/typescript/module/index.d.ts.map +1 -1
  89. package/lib/typescript/module/index.web.d.ts +1 -1
  90. package/lib/typescript/module/index.web.d.ts.map +1 -1
  91. package/lib/typescript/module/input-otp/InputOtp.d.ts.map +1 -1
  92. package/lib/typescript/module/overlay/Overlay.d.ts.map +1 -1
  93. package/lib/typescript/module/overlay/OverlayInertBoundary.d.ts +6 -0
  94. package/lib/typescript/module/overlay/OverlayInertBoundary.d.ts.map +1 -0
  95. package/lib/typescript/module/overlay/index.d.ts +3 -1
  96. package/lib/typescript/module/overlay/index.d.ts.map +1 -1
  97. package/lib/typescript/module/overlay/modal-registry.d.ts +53 -0
  98. package/lib/typescript/module/overlay/modal-registry.d.ts.map +1 -0
  99. package/lib/typescript/module/overlay/types.d.ts +21 -3
  100. package/lib/typescript/module/overlay/types.d.ts.map +1 -1
  101. package/lib/typescript/module/overlay/use-modal-overlay-active.d.ts +7 -0
  102. package/lib/typescript/module/overlay/use-modal-overlay-active.d.ts.map +1 -0
  103. package/lib/typescript/module/portal/index.d.ts +2 -0
  104. package/lib/typescript/module/portal/index.d.ts.map +1 -1
  105. package/lib/typescript/module/portal/index.web.d.ts +2 -0
  106. package/lib/typescript/module/portal/index.web.d.ts.map +1 -1
  107. package/lib/typescript/module/rating/Rating.d.ts +9 -1
  108. package/lib/typescript/module/rating/Rating.d.ts.map +1 -1
  109. package/lib/typescript/module/rating/types.d.ts +9 -0
  110. package/lib/typescript/module/rating/types.d.ts.map +1 -1
  111. package/package.json +1 -1
  112. package/src/bottom-sheet/BottomSheet.web.tsx +6 -2
  113. package/src/dialog/Dialog.web.tsx +7 -4
  114. package/src/index.ts +1 -0
  115. package/src/index.web.ts +1 -0
  116. package/src/input-otp/InputOtp.tsx +11 -2
  117. package/src/overlay/Overlay.tsx +4 -0
  118. package/src/overlay/OverlayInertBoundary.tsx +72 -0
  119. package/src/overlay/index.ts +3 -1
  120. package/src/overlay/modal-registry.ts +101 -0
  121. package/src/overlay/types.ts +22 -3
  122. package/src/overlay/use-modal-overlay-active.ts +31 -0
  123. package/src/portal/index.ts +5 -0
  124. package/src/portal/index.web.ts +5 -0
  125. package/src/rating/Rating.tsx +37 -2
  126. package/src/rating/types.ts +9 -0
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/portal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,YAAY,EACZ,MAAM,GACP,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/portal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,YAAY,EACZ,MAAM,GACP,MAAM,UAAU,CAAC;AAIlB,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AACvE,YAAY,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -1,2 +1,4 @@
1
1
  export { PortalProvider, PortalOutlet, Portal, } from './Portal.web';
2
+ export { OverlayInertBoundary } from '../overlay/OverlayInertBoundary';
3
+ export type { OverlayInertBoundaryProps } from '../overlay/types';
2
4
  //# sourceMappingURL=index.web.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/portal/index.web.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,cAAc,EACd,YAAY,EACZ,MAAM,GACP,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/portal/index.web.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,cAAc,EACd,YAAY,EACZ,MAAM,GACP,MAAM,cAAc,CAAC;AAItB,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AACvE,YAAY,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -2,9 +2,17 @@ import React from 'react';
2
2
  import type { RatingProps } from './types';
3
3
  /** 5 → "5.0", 4.9 → "4.9", 4.923 → "4.92". */
4
4
  export declare function formatRatingValue(value: number | string): string;
5
+ /**
6
+ * The number a drawn rating value stands for, for the `stars` fill: a number
7
+ * is itself; a string is read after dropping bidi/format characters, with a
8
+ * comma or Arabic decimal separator (U+066B) accepted as the decimal point and
9
+ * Arabic-Indic digits accepted — so "4,5" wrapped in FSI…PDI is 4.5. Otherwise it is
10
+ * `Number()`'s reading, so anything unreadable is `NaN` (no fill).
11
+ */
12
+ export declare function parseRatingValue(value: number | string): number;
5
13
  /** How much of star `index` (0-based) a rating fills, `0..1`. */
6
14
  export declare function starFill(rating: number, index: number): number;
7
- declare function RatingComponent({ value, count, countStyle, reviewsLabel, newLabel, size, variant, color, starColor, countColor, emptyStarColor, accessibilityLabel, style, testID, }: RatingProps): import("react/jsx-runtime").JSX.Element;
15
+ declare function RatingComponent({ value, count, countStyle, reviewsLabel, newLabel, size, variant, color, starColor, countColor, emptyStarColor, fillValue, accessibilityLabel, style, testID, }: RatingProps): import("react/jsx-runtime").JSX.Element;
8
16
  export declare const Rating: React.MemoExoticComponent<typeof RatingComponent>;
9
17
  export {};
10
18
  //# sourceMappingURL=Rating.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Rating.d.ts","sourceRoot":"","sources":["../../../../src/rating/Rating.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAe,MAAM,OAAO,CAAC;AAQpC,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,SAAS,CAAC;AAiCvD,8CAA8C;AAC9C,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAIhE;AAQD,iEAAiE;AACjE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAG9D;AA6CD,iBAAS,eAAe,CAAC,EACvB,KAAK,EACL,KAAK,EACL,UAA0B,EAC1B,YAAwB,EACxB,QAAgB,EAChB,IAAe,EACf,OAAmB,EACnB,KAAK,EACL,SAAS,EACT,UAAU,EACV,cAAc,EACd,kBAAkB,EAClB,KAAK,EACL,MAAM,GACP,EAAE,WAAW,2CAmDb;AAED,eAAO,MAAM,MAAM,mDAAwB,CAAC"}
1
+ {"version":3,"file":"Rating.d.ts","sourceRoot":"","sources":["../../../../src/rating/Rating.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAe,MAAM,OAAO,CAAC;AAQpC,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,SAAS,CAAC;AAkCvD,8CAA8C;AAC9C,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAIhE;AAkBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAS/D;AAED,iEAAiE;AACjE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAG9D;AAkDD,iBAAS,eAAe,CAAC,EACvB,KAAK,EACL,KAAK,EACL,UAA0B,EAC1B,YAAwB,EACxB,QAAgB,EAChB,IAAe,EACf,OAAmB,EACnB,KAAK,EACL,SAAS,EACT,UAAU,EACV,cAAc,EACd,SAAS,EACT,kBAAkB,EAClB,KAAK,EACL,MAAM,GACP,EAAE,WAAW,2CAmDb;AAED,eAAO,MAAM,MAAM,mDAAwB,CAAC"}
@@ -40,6 +40,15 @@ export interface RatingProps {
40
40
  countColor?: string;
41
41
  /** `stars` variant: the unfilled part of each star. Default the theme's border colour. */
42
42
  emptyStarColor?: string;
43
+ /**
44
+ * `stars` variant: the number the stars are filled to, clamped to `0..5`,
45
+ * independent of the drawn `value`. Pass it with a localised `value` string
46
+ * (`value="4,5"` with `fillValue={4.5}`) so the text follows the locale and
47
+ * the fill does not depend on parsing it. Without it a string `value` is
48
+ * parsed (bidi marks dropped, a comma decimal accepted). Ignored by
49
+ * `compact` and while there is no rating (`value` empty).
50
+ */
51
+ fillValue?: number;
43
52
  /**
44
53
  * Overrides the composed accessible name ("Rated 4.92 out of 5, 128
45
54
  * reviews"), which is English — pass a translated sentence here.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/rating/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,8FAA8F;AAC9F,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE5C,oEAAoE;AACpE,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG,SAAS,CAAC;AAEzD;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,OAAO,CAAC;AAEhD,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC/B,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,uGAAuG;IACvG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,sFAAsF;IACtF,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,iEAAiE;AACjE,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,iFAAiF;IACjF,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mCAAmC;IACnC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IACzD,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,yCAAyC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,wFAAwF;IACxF,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/rating/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,8FAA8F;AAC9F,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAC;AAE5C,oEAAoE;AACpE,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG,SAAS,CAAC;AAEzD;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,OAAO,CAAC;AAEhD,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC/B,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,uGAAuG;IACvG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,sFAAsF;IACtF,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,iEAAiE;AACjE,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,iFAAiF;IACjF,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,mCAAmC;IACnC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;IACzD,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,yCAAyC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,wFAAwF;IACxF,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/bloom",
3
- "version": "4.18.1",
3
+ "version": "4.19.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",
@@ -32,8 +32,12 @@ function WebShell({ children }: BottomSheetShellProps) {
32
32
  {/* `OverlayRoot` owns the portal-root pointer-events opt-in (see
33
33
  `src/overlay`). It used to ride in the style array below as an
34
34
  RN-only box-none value, which never reached the DOM — the whole
35
- sheet, backdrop included, was click-through on web. */}
36
- <OverlayRoot style={webStyles.rootView}>
35
+ sheet, backdrop included, was click-through on web. `modal`:
36
+ the sheet dims and blocks the page, so the app's
37
+ `OverlayInertBoundary` makes that page inert while it is open.
38
+ (Native needs no flag — its shell is an RN `Modal`, a separate
39
+ window the screen reader is already confined to.) */}
40
+ <OverlayRoot style={webStyles.rootView} modal>
37
41
  <GestureHandlerRootView style={StyleSheet.absoluteFill}>
38
42
  {children}
39
43
  </GestureHandlerRootView>
@@ -144,7 +144,8 @@ function useDialogCss(): void {
144
144
  * Accessibility: the panel has `role="dialog"` and the `title`/`description`
145
145
  * props (when provided) become the `aria-labelledby` / `aria-describedby`
146
146
  * targets. Pressing the backdrop dismisses. Pressing `Escape` dismisses.
147
- * Focus is locked inside the dialog while it's open.
147
+ * While it is open the page behind is `inert` — provided the app wraps its
148
+ * content in `OverlayInertBoundary` (`OverlayRoot modal` below registers it).
148
149
  *
149
150
  * Web motion never uses reanimated `exiting` (which throws `removeChild` on
150
151
  * concurrent React unmounts): the centered card uses CSS keyframes and the
@@ -354,8 +355,10 @@ function CenterOrSideDialog({
354
355
  overlay stack. It mounts here, inside the `isOpen` guard, which
355
356
  is what makes the rank track OPENING rather than mounting — the
356
357
  dialog is declared once at the app root and stays mounted for
357
- the app's lifetime. */}
358
- <OverlayRoot>
358
+ the app's lifetime. `modal` makes the app's
359
+ `OverlayInertBoundary` take the page out of the tab order and
360
+ the accessibility tree while it is open. */}
361
+ <OverlayRoot modal>
359
362
  {/* The press target IS the full-viewport box, so it uses
360
363
  `Backdrop` (which opts back in from the Portal root's
361
364
  `pointer-events: none` via the `pointerEvents` PROP — the style
@@ -746,7 +749,7 @@ function SheetSurface({
746
749
  );
747
750
 
748
751
  return (
749
- <OverlayRoot style={[sheetStyles.root, containerStyle]} className={containerClassName}>
752
+ <OverlayRoot style={[sheetStyles.root, containerStyle]} className={containerClassName} modal>
750
753
  <Backdrop
751
754
  testID={testID ? `${testID}-backdrop` : DIALOG_SHEET_BACKDROP_TESTID}
752
755
  accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
package/src/index.ts CHANGED
@@ -106,6 +106,7 @@ export {
106
106
  Backdrop,
107
107
  useOverlayLayer,
108
108
  useOverlayLayerContext,
109
+ useModalOverlayActive,
109
110
  layerForRank,
110
111
  BACKDROP_BLUR_INTENSITY,
111
112
  BACKDROP_DIM_OPACITY,
package/src/index.web.ts CHANGED
@@ -111,6 +111,7 @@ export {
111
111
  Backdrop,
112
112
  useOverlayLayer,
113
113
  useOverlayLayerContext,
114
+ useModalOverlayActive,
114
115
  layerForRank,
115
116
  BACKDROP_BLUR_INTENSITY,
116
117
  BACKDROP_DIM_OPACITY,
@@ -291,7 +291,13 @@ export function InputOtp({
291
291
  autoFocus={autoFocus && index === 0}
292
292
  // Long enough to accept a full autofilled code in one box.
293
293
  maxLength={length}
294
- selectTextOnFocus
294
+ // Typing into a filled box must REPLACE its digit, so the digit is
295
+ // selected on focus. Web's `selectTextOnFocus` does that. Android's
296
+ // does not: it selects on the input's next layout, and focusing a box
297
+ // lays nothing out, so the keystroke landed beside the old digit and
298
+ // `writeFrom` spread both across two boxes. Native selects in
299
+ // `onFocus` instead (see Search, OxyHQ/Mention#1126).
300
+ selectTextOnFocus={IS_WEB}
295
301
  caretHidden={false}
296
302
  editable={!disabled}
297
303
  accessibilityLabel={`Digit ${index + 1} of ${length}`}
@@ -300,7 +306,10 @@ export function InputOtp({
300
306
  value={digit === ' ' ? '' : digit}
301
307
  onChangeText={(text) => writeFrom(index, text)}
302
308
  onKeyPress={(event) => onKeyPress(event, index)}
303
- onFocus={() => setFocusedIndex(index)}
309
+ onFocus={() => {
310
+ setFocusedIndex(index);
311
+ if (!IS_WEB && digit !== '' && digit !== ' ') inputsRef.current[index]?.setSelection(0, 1);
312
+ }}
304
313
  onBlur={() => setFocusedIndex((current) => (current === index ? null : current))}
305
314
  keyboardAppearance={theme.isDark ? 'dark' : 'light'}
306
315
  style={[
@@ -58,6 +58,7 @@ import { StyledView } from '../styles/styled-primitives';
58
58
  import { SurfaceLevelProvider } from '../styles/surface-levels';
59
59
  import { layerForRank, type OverlayLayer } from './stack';
60
60
  import { useHardwareBack } from './use-hardware-back';
61
+ import { useModalOverlayRegistration } from './use-modal-overlay-active';
61
62
  import { useOverlayLayer } from './use-overlay-layer';
62
63
  import type { OverlayRootProps, BackdropProps } from './types';
63
64
 
@@ -181,6 +182,9 @@ function OverlayRootView({
181
182
  layer,
182
183
  modal,
183
184
  }: OverlayRootViewProps & { layer: OverlayLayer }) {
185
+ // Android cannot confine TalkBack from this side of the outlet — the app's
186
+ // `OverlayInertBoundary` hides the content once it hears a modal is open.
187
+ useModalOverlayRegistration(modal);
184
188
  return (
185
189
  <OverlayLayerContext.Provider value={layer}>
186
190
  {/* `pointerEvents` stays a PROP: react-native-web resolves the RN-only
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `OverlayInertBoundary` — the app content's half of a modal overlay.
3
+ *
4
+ * A modal Bloom surface renders at the portal outlet, which is the app
5
+ * content's SIBLING. It can mark itself modal (`accessibilityViewIsModal`, which
6
+ * iOS honours by hiding the siblings), but it cannot hide the app on Android:
7
+ * TalkBack only skips a subtree whose OWN root says
8
+ * `importantForAccessibility="no-hide-descendants"`, and that root belongs to
9
+ * the app. Without it, TalkBack walked from the settings modal straight into
10
+ * the Home feed behind it (OxyHQ/Mention#1126).
11
+ *
12
+ * The app wraps its content — everything that is NOT the `PortalOutlet` — in
13
+ * this boundary. While any modal overlay is open (`./modal-registry.ts`) it
14
+ * takes that content out of the accessibility tree:
15
+ *
16
+ * - Android: `importantForAccessibility="no-hide-descendants"`.
17
+ * - iOS: `accessibilityElementsHidden`, alongside the surface's own
18
+ * `accessibilityViewIsModal` (either alone is enough; both costs nothing).
19
+ * - Web: `inert`, which removes the subtree from the tab order AND the
20
+ * accessibility tree, plus `aria-hidden` for engines that lag on `inert`.
21
+ * The web portal root is a child of `document.body`, never of the app
22
+ * root, so the surface itself is untouched. Web dialogs set no focus trap
23
+ * of their own, so nothing is applied twice.
24
+ *
25
+ * Layout: on web the boundary is `display: contents`, so it generates no box
26
+ * and a document-scrolling page lays out exactly as without it. On native it is
27
+ * a `flex: 1` view — the content it wraps already fills the root there — and it
28
+ * is never collapsed, because flattening a layout-only view would drop the
29
+ * accessibility prop the moment it is needed.
30
+ */
31
+ import { Platform, StyleSheet, View } from 'react-native';
32
+
33
+ import type { OverlayInertBoundaryProps } from './types';
34
+ import { useModalOverlayActive } from './use-modal-overlay-active';
35
+
36
+ export function OverlayInertBoundary({ children, style, testID }: OverlayInertBoundaryProps) {
37
+ const active = useModalOverlayActive();
38
+
39
+ if (Platform.OS === 'web') {
40
+ return (
41
+ <View
42
+ testID={testID}
43
+ style={[styles.web, style]}
44
+ aria-hidden={active ? true : undefined}
45
+ {...(active ? { inert: true } : null)}
46
+ >
47
+ {children}
48
+ </View>
49
+ );
50
+ }
51
+
52
+ return (
53
+ <View
54
+ testID={testID}
55
+ collapsable={false}
56
+ style={[styles.native, style]}
57
+ importantForAccessibility={active ? 'no-hide-descendants' : 'auto'}
58
+ accessibilityElementsHidden={active}
59
+ >
60
+ {children}
61
+ </View>
62
+ );
63
+ }
64
+
65
+ OverlayInertBoundary.displayName = 'OverlayInertBoundary';
66
+
67
+ const styles = StyleSheet.create({
68
+ native: { flex: 1 },
69
+ // No box of its own on web: the wrapped content lays out against the
70
+ // boundary's parent, flex tree or document scroll alike.
71
+ web: { display: 'contents' },
72
+ });
@@ -7,6 +7,8 @@ export {
7
7
  type OverlayLayer,
8
8
  } from './stack';
9
9
  export { useOverlayLayer } from './use-overlay-layer';
10
+ export { useModalOverlayActive } from './use-modal-overlay-active';
11
+ export { OverlayInertBoundary } from './OverlayInertBoundary';
10
12
  export {
11
13
  useOverlayLayerContext,
12
14
  BACKDROP_BLUR_INTENSITY,
@@ -14,4 +16,4 @@ export {
14
16
  OverlayRoot,
15
17
  Backdrop,
16
18
  } from './Overlay';
17
- export type { OverlayRootProps, BackdropProps } from './types';
19
+ export type { OverlayRootProps, BackdropProps, OverlayInertBoundaryProps } from './types';
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Which MODAL overlays are open right now, as a subscribable store.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * A modal surface has to keep a screen reader inside it. iOS can do that from
7
+ * the overlay's side: `accessibilityViewIsModal` on the `OverlayRoot` hides its
8
+ * siblings, and at the native portal outlet the app content IS a sibling.
9
+ * Android has no equivalent. TalkBack keeps walking into the app behind the
10
+ * open surface (OxyHQ/Mention#1126: the Home feed stayed reachable under the
11
+ * settings modal), and the only switch that stops it —
12
+ * `importantForAccessibility="no-hide-descendants"` — has to sit ON the app
13
+ * content. A portaled surface cannot reach that view: the outlet renders it as
14
+ * the content's sibling, not its ancestor.
15
+ *
16
+ * So the knowledge travels the other way. Every `OverlayRoot` with `modal`
17
+ * registers here while it is mounted, and `OverlayInertBoundary` — which the
18
+ * app wraps around its own content — subscribes and hides what it wraps while
19
+ * anything modal is open.
20
+ *
21
+ * Non-modal overlays (menus, popovers, tooltips, toasts, the shell drawers)
22
+ * never register: the content under an anchored menu must stay reachable.
23
+ *
24
+ * Tokens rather than a counter so StrictMode's mount/unmount/remount and an
25
+ * unmatched release can never drift the count: a token is either in the set or
26
+ * not.
27
+ */
28
+
29
+ interface ModalRegistry {
30
+ live: Set<object>;
31
+ listeners: Set<() => void>;
32
+ }
33
+
34
+ declare global {
35
+ // eslint-disable-next-line no-var
36
+ var __oxy_so_bloom_modal_overlays__: ModalRegistry | undefined;
37
+ }
38
+
39
+ /**
40
+ * `globalThis`-anchored for the same reason as `./stack.ts`: the surfaces that
41
+ * register (`./dialog`, `./settings-modal`, …) and the boundary the app mounts
42
+ * (`./portal`) are imported through different subpaths, and a bundler can
43
+ * resolve those through different export conditions. Two physical copies of
44
+ * this module would each keep their own set, and the boundary would never hear
45
+ * about the surfaces.
46
+ */
47
+ function registry(): ModalRegistry {
48
+ globalThis.__oxy_so_bloom_modal_overlays__ ??= { live: new Set(), listeners: new Set() };
49
+ return globalThis.__oxy_so_bloom_modal_overlays__;
50
+ }
51
+
52
+ function notify(reg: ModalRegistry): void {
53
+ for (const listener of Array.from(reg.listeners)) listener();
54
+ }
55
+
56
+ /**
57
+ * Mark a modal overlay as open. Returns its release. Called from an effect in
58
+ * `OverlayRoot`, which React balances even under StrictMode.
59
+ */
60
+ export function registerModalOverlay(token: object): () => void {
61
+ const reg = registry();
62
+ const wasActive = reg.live.size > 0;
63
+ reg.live.add(token);
64
+ if (!wasActive) notify(reg);
65
+ return () => {
66
+ const current = registry();
67
+ if (!current.live.delete(token)) return;
68
+ if (current.live.size === 0) notify(current);
69
+ };
70
+ }
71
+
72
+ /** Whether any modal overlay is open. */
73
+ export function hasActiveModalOverlays(): boolean {
74
+ return registry().live.size > 0;
75
+ }
76
+
77
+ /** How many modal overlays are open. Exported for tests. */
78
+ export function activeModalOverlayCount(): number {
79
+ return registry().live.size;
80
+ }
81
+
82
+ /**
83
+ * Subscribe to changes in `hasActiveModalOverlays()`. Listeners fire only when
84
+ * the answer flips — a second modal opening over a first changes nothing for
85
+ * the content underneath.
86
+ */
87
+ export function subscribeModalOverlays(listener: () => void): () => void {
88
+ const reg = registry();
89
+ reg.listeners.add(listener);
90
+ return () => {
91
+ registry().listeners.delete(listener);
92
+ };
93
+ }
94
+
95
+ /** Test seam — drops all open modals, notifying current subscribers. */
96
+ export function resetModalOverlays(): void {
97
+ const reg = registry();
98
+ const wasActive = reg.live.size > 0;
99
+ reg.live.clear();
100
+ if (wasActive) notify(reg);
101
+ }
@@ -36,13 +36,32 @@ export interface OverlayRootProps {
36
36
  onRequestClose?: () => void;
37
37
  /**
38
38
  * The surface is modal: a screen reader must not wander into the app behind
39
- * it. Native only — sets `accessibilityViewIsModal` on this root, which is the
40
- * app content's sibling at the portal outlet (iOS confines VoiceOver to
41
- * it). Web surfaces already carry `aria-modal` on their own dialog node.
39
+ * it. Two effects:
40
+ *
41
+ * - Native: sets `accessibilityViewIsModal` on this root, which is the app
42
+ * content's sibling at the portal outlet (iOS confines VoiceOver to it).
43
+ * - Every platform: registers the surface as an open modal while mounted
44
+ * (`./modal-registry.ts`), so the app's `OverlayInertBoundary` hides the
45
+ * content behind it — the only way to confine TalkBack on Android, and
46
+ * what makes the content `inert` on web.
47
+ *
48
+ * Dialogs, sheets, the settings modal and the media viewer set it; anchored
49
+ * surfaces (menus, popovers, tooltips), toasts and the shell drawers do not.
42
50
  */
43
51
  modal?: boolean;
44
52
  }
45
53
 
54
+ export interface OverlayInertBoundaryProps {
55
+ /** The app's own content — everything EXCEPT the `PortalOutlet`. */
56
+ children?: ReactNode;
57
+ /**
58
+ * Native only (web renders no box). Defaults to `flex: 1`; override when the
59
+ * boundary's parent is not a column that the content should fill.
60
+ */
61
+ style?: StyleProp<ViewStyle>;
62
+ testID?: string;
63
+ }
64
+
46
65
  export interface BackdropProps {
47
66
  /** Dismiss handler. Omit (or pass `disabled`) for a backdrop that only dims. */
48
67
  onPress?: () => void;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `useModalOverlayActive` — whether any modal Bloom overlay is open.
3
+ *
4
+ * Re-renders only when the answer flips. See `./modal-registry.ts` for who
5
+ * registers and why.
6
+ */
7
+ import { useEffect, useSyncExternalStore } from 'react';
8
+
9
+ import {
10
+ hasActiveModalOverlays,
11
+ registerModalOverlay,
12
+ subscribeModalOverlays,
13
+ } from './modal-registry';
14
+
15
+ /** `false` on the server: nothing is open before hydration. */
16
+ const serverSnapshot = () => false;
17
+
18
+ export function useModalOverlayActive(): boolean {
19
+ return useSyncExternalStore(subscribeModalOverlays, hasActiveModalOverlays, serverSnapshot);
20
+ }
21
+
22
+ /**
23
+ * Register as an open modal overlay while mounted and `modal` is true. Used by
24
+ * `OverlayRoot`; exported only within the family.
25
+ */
26
+ export function useModalOverlayRegistration(modal: boolean): void {
27
+ useEffect(() => {
28
+ if (!modal) return undefined;
29
+ return registerModalOverlay({});
30
+ }, [modal]);
31
+ }
@@ -3,3 +3,8 @@ export {
3
3
  PortalOutlet,
4
4
  Portal,
5
5
  } from './Portal';
6
+ // The app-content half of a modal overlay: wrap everything EXCEPT the
7
+ // `PortalOutlet` in it. Lives in `overlay/` (it reads the modal registry) and is
8
+ // offered here because this is the subpath the app root already imports.
9
+ export { OverlayInertBoundary } from '../overlay/OverlayInertBoundary';
10
+ export type { OverlayInertBoundaryProps } from '../overlay/types';
@@ -8,3 +8,8 @@ export {
8
8
  PortalOutlet,
9
9
  Portal,
10
10
  } from './Portal.web';
11
+ // The app-content half of a modal overlay: wrap everything EXCEPT the
12
+ // `PortalOutlet` in it. Lives in `overlay/` (it reads the modal registry) and is
13
+ // offered here because this is the subpath the app root already imports.
14
+ export { OverlayInertBoundary } from '../overlay/OverlayInertBoundary';
15
+ export type { OverlayInertBoundaryProps } from '../overlay/types';
@@ -26,7 +26,8 @@ import type { RatingProps, RatingSize } from './types';
26
26
  * `variant="stars"` draws five stars before the value instead of one, 2px
27
27
  * apart, each filled to its share of the value: the unfilled star in
28
28
  * `emptyStarColor` (border), and over it the filled star clipped to the
29
- * fraction. The clip is anchored with a LOGICAL inset (`insetInlineStart`) so a
29
+ * fraction. The fill comes from `fillValue` when given, else from `value`
30
+ * (parsed by `parseRatingValue` when it is a localised string). The clip is anchored with a LOGICAL inset (`insetInlineStart`) so a
30
31
  * right-to-left row fills from the right — the root carries
31
32
  * `useDirectionProps()` for react-native-web to resolve it.
32
33
  *
@@ -52,12 +53,45 @@ const IS_WEB = Platform.OS === 'web';
52
53
  const STAR_COUNT = 5;
53
54
  const STAR_GAP = 2;
54
55
 
56
+ /**
57
+ * Bidi isolates and marks (U+2066–U+2069 LRI/RLI/FSI/PDI, U+200E/U+200F
58
+ * LRM/RLM, U+061C ALM) a localised string is wrapped in. None of them are
59
+ * part of the number.
60
+ */
61
+ const FORMAT_CHARACTERS = /[\u2066-\u2069\u200E\u200F\u061C]/g;
62
+ /** Arabic-Indic (U+0660) and extended Arabic-Indic (U+06F0) digit zeros. */
63
+ const ARABIC_ZERO = 0x0660;
64
+ const EXTENDED_ARABIC_ZERO = 0x06f0;
65
+
66
+ /**
67
+ * The number a drawn rating value stands for, for the `stars` fill: a number
68
+ * is itself; a string is read after dropping bidi/format characters, with a
69
+ * comma or Arabic decimal separator (U+066B) accepted as the decimal point and
70
+ * Arabic-Indic digits accepted — so "4,5" wrapped in FSI…PDI is 4.5. Otherwise it is
71
+ * `Number()`'s reading, so anything unreadable is `NaN` (no fill).
72
+ */
73
+ export function parseRatingValue(value: number | string): number {
74
+ if (typeof value === 'number') return value;
75
+ const text = value
76
+ .replace(FORMAT_CHARACTERS, '')
77
+ .trim()
78
+ .replace(/[\u0660-\u0669]/g, (d) => String(d.charCodeAt(0) - ARABIC_ZERO))
79
+ .replace(/[\u06F0-\u06F9]/g, (d) => String(d.charCodeAt(0) - EXTENDED_ARABIC_ZERO))
80
+ .replace(/[,\u066B]/, '.');
81
+ return Number(text);
82
+ }
83
+
55
84
  /** How much of star `index` (0-based) a rating fills, `0..1`. */
56
85
  export function starFill(rating: number, index: number): number {
57
86
  if (!Number.isFinite(rating)) return 0;
58
87
  return Math.min(1, Math.max(0, rating - index));
59
88
  }
60
89
 
90
+ /** A fill on the five-star scale, clamped to `0..5`; `NaN` stays `NaN` (no fill). */
91
+ function clampRating(rating: number): number {
92
+ return Math.min(STAR_COUNT, Math.max(0, rating));
93
+ }
94
+
61
95
  function StarRow({
62
96
  rating,
63
97
  size,
@@ -113,6 +147,7 @@ function RatingComponent({
113
147
  starColor,
114
148
  countColor,
115
149
  emptyStarColor,
150
+ fillValue,
116
151
  accessibilityLabel,
117
152
  style,
118
153
  testID,
@@ -148,7 +183,7 @@ function RatingComponent({
148
183
  >
149
184
  {variant === 'stars' && rated ? (
150
185
  <StarRow
151
- rating={typeof value === 'number' ? value : Number(value)}
186
+ rating={clampRating(fillValue ?? parseRatingValue(value))}
152
187
  size={config.star}
153
188
  fill={starPaint}
154
189
  empty={emptyStarColor ?? theme.colors.border}
@@ -44,6 +44,15 @@ export interface RatingProps {
44
44
  countColor?: string;
45
45
  /** `stars` variant: the unfilled part of each star. Default the theme's border colour. */
46
46
  emptyStarColor?: string;
47
+ /**
48
+ * `stars` variant: the number the stars are filled to, clamped to `0..5`,
49
+ * independent of the drawn `value`. Pass it with a localised `value` string
50
+ * (`value="4,5"` with `fillValue={4.5}`) so the text follows the locale and
51
+ * the fill does not depend on parsing it. Without it a string `value` is
52
+ * parsed (bidi marks dropped, a comma decimal accepted). Ignored by
53
+ * `compact` and while there is no rating (`value` empty).
54
+ */
55
+ fillValue?: number;
47
56
  /**
48
57
  * Overrides the composed accessible name ("Rated 4.92 out of 5, 128
49
58
  * reviews"), which is English — pass a translated sentence here.