@oxy.so/bloom 4.17.0 → 4.18.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 (272) hide show
  1. package/docs/adoption-matrix.mdx +7 -7
  2. package/docs/breadcrumb.mdx +11 -0
  3. package/docs/cart-panel.mdx +4 -2
  4. package/docs/checkout-summary.mdx +4 -0
  5. package/docs/chip.mdx +14 -1
  6. package/docs/dialog.mdx +5 -4
  7. package/docs/loading.mdx +11 -0
  8. package/docs/overlay.mdx +21 -0
  9. package/docs/price-breakdown.mdx +17 -0
  10. package/docs/rating.mdx +22 -0
  11. package/docs/select.mdx +10 -0
  12. package/docs/stepper.mdx +25 -0
  13. package/lib/commonjs/breadcrumb/Breadcrumb.js +36 -6
  14. package/lib/commonjs/breadcrumb/Breadcrumb.js.map +1 -1
  15. package/lib/commonjs/cart-panel/CartLine.js +32 -10
  16. package/lib/commonjs/cart-panel/CartLine.js.map +1 -1
  17. package/lib/commonjs/cart-panel/CartPanel.js +3 -0
  18. package/lib/commonjs/cart-panel/CartPanel.js.map +1 -1
  19. package/lib/commonjs/cart-panel/shared.js +2 -0
  20. package/lib/commonjs/cart-panel/shared.js.map +1 -1
  21. package/lib/commonjs/chat-list/parts.js +1 -1
  22. package/lib/commonjs/chat-list/parts.js.map +1 -1
  23. package/lib/commonjs/chat-screen/ChatBackground.js +6 -6
  24. package/lib/commonjs/chat-screen/ChatBackground.js.map +1 -1
  25. package/lib/commonjs/checkout-summary/CheckoutConfirm.js +11 -1
  26. package/lib/commonjs/checkout-summary/CheckoutConfirm.js.map +1 -1
  27. package/lib/commonjs/chip/Chip.js +3 -1
  28. package/lib/commonjs/chip/Chip.js.map +1 -1
  29. package/lib/commonjs/dialog/Dialog.js +20 -6
  30. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  31. package/lib/commonjs/dialog/Dialog.web.js +11 -5
  32. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  33. package/lib/commonjs/dialog/placement.js +18 -1
  34. package/lib/commonjs/dialog/placement.js.map +1 -1
  35. package/lib/commonjs/loading/Loading.js +7 -3
  36. package/lib/commonjs/loading/Loading.js.map +1 -1
  37. package/lib/commonjs/loading/Loading.web.js +7 -3
  38. package/lib/commonjs/loading/Loading.web.js.map +1 -1
  39. package/lib/commonjs/loading/LoadingBase.js +49 -2
  40. package/lib/commonjs/loading/LoadingBase.js.map +1 -1
  41. package/lib/commonjs/location-puck/LocationPuck.js +1 -1
  42. package/lib/commonjs/location-puck/LocationPuck.js.map +1 -1
  43. package/lib/commonjs/mail-list/parts.js +1 -1
  44. package/lib/commonjs/mail-list/parts.js.map +1 -1
  45. package/lib/commonjs/media-card/parts.js +1 -1
  46. package/lib/commonjs/media-card/parts.js.map +1 -1
  47. package/lib/commonjs/overlay/Overlay.js +19 -4
  48. package/lib/commonjs/overlay/Overlay.js.map +1 -1
  49. package/lib/commonjs/overlay/use-hardware-back.js +32 -0
  50. package/lib/commonjs/overlay/use-hardware-back.js.map +1 -0
  51. package/lib/commonjs/price-breakdown/PriceSummary.js +11 -1
  52. package/lib/commonjs/price-breakdown/PriceSummary.js.map +1 -1
  53. package/lib/commonjs/price-breakdown/PriceSummaryLine.js +13 -1
  54. package/lib/commonjs/price-breakdown/PriceSummaryLine.js.map +1 -1
  55. package/lib/commonjs/rating/Rating.js +90 -4
  56. package/lib/commonjs/rating/Rating.js.map +1 -1
  57. package/lib/commonjs/search/Search.js +36 -3
  58. package/lib/commonjs/search/Search.js.map +1 -1
  59. package/lib/commonjs/settings-modal/SettingsModal.js +19 -12
  60. package/lib/commonjs/settings-modal/SettingsModal.js.map +1 -1
  61. package/lib/commonjs/stepper/Stepper.js +18 -5
  62. package/lib/commonjs/stepper/Stepper.js.map +1 -1
  63. package/lib/commonjs/teleport/PortalHost.js +1 -1
  64. package/lib/commonjs/zoomable-media-gallery/ZoomableMediaGallery.js +32 -12
  65. package/lib/commonjs/zoomable-media-gallery/ZoomableMediaGallery.js.map +1 -1
  66. package/lib/module/breadcrumb/Breadcrumb.js +36 -6
  67. package/lib/module/breadcrumb/Breadcrumb.js.map +1 -1
  68. package/lib/module/cart-panel/CartLine.js +32 -10
  69. package/lib/module/cart-panel/CartLine.js.map +1 -1
  70. package/lib/module/cart-panel/CartPanel.js +3 -0
  71. package/lib/module/cart-panel/CartPanel.js.map +1 -1
  72. package/lib/module/cart-panel/shared.js +2 -0
  73. package/lib/module/cart-panel/shared.js.map +1 -1
  74. package/lib/module/chat-list/parts.js +1 -1
  75. package/lib/module/chat-list/parts.js.map +1 -1
  76. package/lib/module/chat-screen/ChatBackground.js +6 -6
  77. package/lib/module/chat-screen/ChatBackground.js.map +1 -1
  78. package/lib/module/checkout-summary/CheckoutConfirm.js +11 -1
  79. package/lib/module/checkout-summary/CheckoutConfirm.js.map +1 -1
  80. package/lib/module/chip/Chip.js +3 -1
  81. package/lib/module/chip/Chip.js.map +1 -1
  82. package/lib/module/dialog/Dialog.js +21 -7
  83. package/lib/module/dialog/Dialog.js.map +1 -1
  84. package/lib/module/dialog/Dialog.web.js +12 -6
  85. package/lib/module/dialog/Dialog.web.js.map +1 -1
  86. package/lib/module/dialog/placement.js +17 -1
  87. package/lib/module/dialog/placement.js.map +1 -1
  88. package/lib/module/loading/Loading.js +8 -4
  89. package/lib/module/loading/Loading.js.map +1 -1
  90. package/lib/module/loading/Loading.web.js +8 -4
  91. package/lib/module/loading/Loading.web.js.map +1 -1
  92. package/lib/module/loading/LoadingBase.js +49 -3
  93. package/lib/module/loading/LoadingBase.js.map +1 -1
  94. package/lib/module/location-puck/LocationPuck.js +1 -1
  95. package/lib/module/location-puck/LocationPuck.js.map +1 -1
  96. package/lib/module/mail-list/parts.js +1 -1
  97. package/lib/module/mail-list/parts.js.map +1 -1
  98. package/lib/module/media-card/parts.js +1 -1
  99. package/lib/module/media-card/parts.js.map +1 -1
  100. package/lib/module/overlay/Overlay.js +19 -4
  101. package/lib/module/overlay/Overlay.js.map +1 -1
  102. package/lib/module/overlay/use-hardware-back.js +29 -0
  103. package/lib/module/overlay/use-hardware-back.js.map +1 -0
  104. package/lib/module/price-breakdown/PriceSummary.js +11 -1
  105. package/lib/module/price-breakdown/PriceSummary.js.map +1 -1
  106. package/lib/module/price-breakdown/PriceSummaryLine.js +13 -1
  107. package/lib/module/price-breakdown/PriceSummaryLine.js.map +1 -1
  108. package/lib/module/rating/Rating.js +89 -4
  109. package/lib/module/rating/Rating.js.map +1 -1
  110. package/lib/module/search/Search.js +37 -4
  111. package/lib/module/search/Search.js.map +1 -1
  112. package/lib/module/settings-modal/SettingsModal.js +20 -13
  113. package/lib/module/settings-modal/SettingsModal.js.map +1 -1
  114. package/lib/module/stepper/Stepper.js +18 -5
  115. package/lib/module/stepper/Stepper.js.map +1 -1
  116. package/lib/module/teleport/PortalHost.js +1 -1
  117. package/lib/module/zoomable-media-gallery/ZoomableMediaGallery.js +32 -12
  118. package/lib/module/zoomable-media-gallery/ZoomableMediaGallery.js.map +1 -1
  119. package/lib/typescript/commonjs/breadcrumb/Breadcrumb.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/breadcrumb/types.d.ts +7 -0
  121. package/lib/typescript/commonjs/breadcrumb/types.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/cart-panel/CartLine.d.ts +10 -5
  123. package/lib/typescript/commonjs/cart-panel/CartLine.d.ts.map +1 -1
  124. package/lib/typescript/commonjs/cart-panel/CartPanel.d.ts +1 -1
  125. package/lib/typescript/commonjs/cart-panel/CartPanel.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/cart-panel/shared.d.ts +1 -1
  127. package/lib/typescript/commonjs/cart-panel/shared.d.ts.map +1 -1
  128. package/lib/typescript/commonjs/cart-panel/types.d.ts +18 -0
  129. package/lib/typescript/commonjs/cart-panel/types.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/checkout-summary/CheckoutConfirm.d.ts +4 -1
  131. package/lib/typescript/commonjs/checkout-summary/CheckoutConfirm.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/checkout-summary/types.d.ts +6 -0
  133. package/lib/typescript/commonjs/checkout-summary/types.d.ts.map +1 -1
  134. package/lib/typescript/commonjs/chip/Chip.d.ts.map +1 -1
  135. package/lib/typescript/commonjs/chip/types.d.ts +8 -6
  136. package/lib/typescript/commonjs/chip/types.d.ts.map +1 -1
  137. package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  139. package/lib/typescript/commonjs/dialog/placement.d.ts +13 -2
  140. package/lib/typescript/commonjs/dialog/placement.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/dialog/types.d.ts +3 -2
  142. package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
  143. package/lib/typescript/commonjs/loading/Loading.d.ts.map +1 -1
  144. package/lib/typescript/commonjs/loading/Loading.web.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/loading/LoadingBase.d.ts +18 -0
  146. package/lib/typescript/commonjs/loading/LoadingBase.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/loading/types.d.ts +11 -0
  148. package/lib/typescript/commonjs/loading/types.d.ts.map +1 -1
  149. package/lib/typescript/commonjs/overlay/Overlay.d.ts +1 -1
  150. package/lib/typescript/commonjs/overlay/Overlay.d.ts.map +1 -1
  151. package/lib/typescript/commonjs/overlay/types.d.ts +18 -0
  152. package/lib/typescript/commonjs/overlay/types.d.ts.map +1 -1
  153. package/lib/typescript/commonjs/overlay/use-hardware-back.d.ts +11 -0
  154. package/lib/typescript/commonjs/overlay/use-hardware-back.d.ts.map +1 -0
  155. package/lib/typescript/commonjs/price-breakdown/PriceSummary.d.ts.map +1 -1
  156. package/lib/typescript/commonjs/price-breakdown/PriceSummaryLine.d.ts +3 -1
  157. package/lib/typescript/commonjs/price-breakdown/PriceSummaryLine.d.ts.map +1 -1
  158. package/lib/typescript/commonjs/price-breakdown/types.d.ts +12 -0
  159. package/lib/typescript/commonjs/price-breakdown/types.d.ts.map +1 -1
  160. package/lib/typescript/commonjs/rating/Rating.d.ts +3 -1
  161. package/lib/typescript/commonjs/rating/Rating.d.ts.map +1 -1
  162. package/lib/typescript/commonjs/rating/index.d.ts +1 -1
  163. package/lib/typescript/commonjs/rating/index.d.ts.map +1 -1
  164. package/lib/typescript/commonjs/rating/types.d.ts +20 -0
  165. package/lib/typescript/commonjs/rating/types.d.ts.map +1 -1
  166. package/lib/typescript/commonjs/search/Search.d.ts.map +1 -1
  167. package/lib/typescript/commonjs/select/types.d.ts +7 -0
  168. package/lib/typescript/commonjs/select/types.d.ts.map +1 -1
  169. package/lib/typescript/commonjs/settings-modal/SettingsModal.d.ts.map +1 -1
  170. package/lib/typescript/commonjs/stepper/Stepper.d.ts +1 -1
  171. package/lib/typescript/commonjs/stepper/Stepper.d.ts.map +1 -1
  172. package/lib/typescript/commonjs/stepper/types.d.ts +9 -0
  173. package/lib/typescript/commonjs/stepper/types.d.ts.map +1 -1
  174. package/lib/typescript/commonjs/teleport/PortalHost.d.ts +1 -1
  175. package/lib/typescript/commonjs/zoomable-media-gallery/ZoomableMediaGallery.d.ts.map +1 -1
  176. package/lib/typescript/module/breadcrumb/Breadcrumb.d.ts.map +1 -1
  177. package/lib/typescript/module/breadcrumb/types.d.ts +7 -0
  178. package/lib/typescript/module/breadcrumb/types.d.ts.map +1 -1
  179. package/lib/typescript/module/cart-panel/CartLine.d.ts +10 -5
  180. package/lib/typescript/module/cart-panel/CartLine.d.ts.map +1 -1
  181. package/lib/typescript/module/cart-panel/CartPanel.d.ts +1 -1
  182. package/lib/typescript/module/cart-panel/CartPanel.d.ts.map +1 -1
  183. package/lib/typescript/module/cart-panel/shared.d.ts +1 -1
  184. package/lib/typescript/module/cart-panel/shared.d.ts.map +1 -1
  185. package/lib/typescript/module/cart-panel/types.d.ts +18 -0
  186. package/lib/typescript/module/cart-panel/types.d.ts.map +1 -1
  187. package/lib/typescript/module/checkout-summary/CheckoutConfirm.d.ts +4 -1
  188. package/lib/typescript/module/checkout-summary/CheckoutConfirm.d.ts.map +1 -1
  189. package/lib/typescript/module/checkout-summary/types.d.ts +6 -0
  190. package/lib/typescript/module/checkout-summary/types.d.ts.map +1 -1
  191. package/lib/typescript/module/chip/Chip.d.ts.map +1 -1
  192. package/lib/typescript/module/chip/types.d.ts +8 -6
  193. package/lib/typescript/module/chip/types.d.ts.map +1 -1
  194. package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
  195. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  196. package/lib/typescript/module/dialog/placement.d.ts +13 -2
  197. package/lib/typescript/module/dialog/placement.d.ts.map +1 -1
  198. package/lib/typescript/module/dialog/types.d.ts +3 -2
  199. package/lib/typescript/module/dialog/types.d.ts.map +1 -1
  200. package/lib/typescript/module/loading/Loading.d.ts.map +1 -1
  201. package/lib/typescript/module/loading/Loading.web.d.ts.map +1 -1
  202. package/lib/typescript/module/loading/LoadingBase.d.ts +18 -0
  203. package/lib/typescript/module/loading/LoadingBase.d.ts.map +1 -1
  204. package/lib/typescript/module/loading/types.d.ts +11 -0
  205. package/lib/typescript/module/loading/types.d.ts.map +1 -1
  206. package/lib/typescript/module/overlay/Overlay.d.ts +1 -1
  207. package/lib/typescript/module/overlay/Overlay.d.ts.map +1 -1
  208. package/lib/typescript/module/overlay/types.d.ts +18 -0
  209. package/lib/typescript/module/overlay/types.d.ts.map +1 -1
  210. package/lib/typescript/module/overlay/use-hardware-back.d.ts +11 -0
  211. package/lib/typescript/module/overlay/use-hardware-back.d.ts.map +1 -0
  212. package/lib/typescript/module/price-breakdown/PriceSummary.d.ts.map +1 -1
  213. package/lib/typescript/module/price-breakdown/PriceSummaryLine.d.ts +3 -1
  214. package/lib/typescript/module/price-breakdown/PriceSummaryLine.d.ts.map +1 -1
  215. package/lib/typescript/module/price-breakdown/types.d.ts +12 -0
  216. package/lib/typescript/module/price-breakdown/types.d.ts.map +1 -1
  217. package/lib/typescript/module/rating/Rating.d.ts +3 -1
  218. package/lib/typescript/module/rating/Rating.d.ts.map +1 -1
  219. package/lib/typescript/module/rating/index.d.ts +1 -1
  220. package/lib/typescript/module/rating/index.d.ts.map +1 -1
  221. package/lib/typescript/module/rating/types.d.ts +20 -0
  222. package/lib/typescript/module/rating/types.d.ts.map +1 -1
  223. package/lib/typescript/module/search/Search.d.ts.map +1 -1
  224. package/lib/typescript/module/select/types.d.ts +7 -0
  225. package/lib/typescript/module/select/types.d.ts.map +1 -1
  226. package/lib/typescript/module/settings-modal/SettingsModal.d.ts.map +1 -1
  227. package/lib/typescript/module/stepper/Stepper.d.ts +1 -1
  228. package/lib/typescript/module/stepper/Stepper.d.ts.map +1 -1
  229. package/lib/typescript/module/stepper/types.d.ts +9 -0
  230. package/lib/typescript/module/stepper/types.d.ts.map +1 -1
  231. package/lib/typescript/module/teleport/PortalHost.d.ts +1 -1
  232. package/lib/typescript/module/zoomable-media-gallery/ZoomableMediaGallery.d.ts.map +1 -1
  233. package/package.json +1 -1
  234. package/src/breadcrumb/Breadcrumb.tsx +29 -4
  235. package/src/breadcrumb/types.ts +7 -0
  236. package/src/cart-panel/CartLine.tsx +30 -7
  237. package/src/cart-panel/CartPanel.tsx +3 -0
  238. package/src/cart-panel/shared.ts +11 -1
  239. package/src/cart-panel/types.ts +18 -0
  240. package/src/chat-list/parts.tsx +1 -1
  241. package/src/chat-screen/ChatBackground.tsx +6 -6
  242. package/src/checkout-summary/CheckoutConfirm.tsx +9 -0
  243. package/src/checkout-summary/types.ts +6 -0
  244. package/src/chip/Chip.tsx +3 -1
  245. package/src/chip/types.ts +8 -6
  246. package/src/dialog/Dialog.tsx +28 -9
  247. package/src/dialog/Dialog.web.tsx +15 -7
  248. package/src/dialog/placement.ts +19 -2
  249. package/src/dialog/types.ts +3 -2
  250. package/src/loading/Loading.tsx +6 -2
  251. package/src/loading/Loading.web.tsx +6 -2
  252. package/src/loading/LoadingBase.tsx +55 -5
  253. package/src/loading/types.ts +11 -0
  254. package/src/location-puck/LocationPuck.tsx +1 -1
  255. package/src/mail-list/parts.tsx +1 -1
  256. package/src/media-card/parts.tsx +1 -1
  257. package/src/overlay/Overlay.tsx +32 -14
  258. package/src/overlay/types.ts +18 -0
  259. package/src/overlay/use-hardware-back.ts +27 -0
  260. package/src/price-breakdown/PriceSummary.tsx +10 -0
  261. package/src/price-breakdown/PriceSummaryLine.tsx +12 -0
  262. package/src/price-breakdown/types.ts +12 -0
  263. package/src/rating/Rating.tsx +87 -3
  264. package/src/rating/index.ts +1 -0
  265. package/src/rating/types.ts +21 -0
  266. package/src/search/Search.tsx +42 -4
  267. package/src/select/types.ts +7 -0
  268. package/src/settings-modal/SettingsModal.tsx +20 -15
  269. package/src/stepper/Stepper.tsx +18 -5
  270. package/src/stepper/types.ts +9 -0
  271. package/src/teleport/PortalHost.tsx +1 -1
  272. package/src/zoomable-media-gallery/ZoomableMediaGallery.tsx +33 -12
@@ -32,6 +32,12 @@ export interface PriceLine {
32
32
  * app owns currency, locale and the minus sign. Omit it for a `pending` line.
33
33
  */
34
34
  amount?: string;
35
+ /**
36
+ * A second, PRE-FORMATTED rendering of the same amount, drawn de-emphasised
37
+ * under it — the buyer's currency beside the shop's: `"≈ 12,00 €"`. Like
38
+ * `ListingPriceLine.secondary`, it is drawn as given; nothing converts.
39
+ */
40
+ secondaryAmount?: string;
35
41
  /** Default `default`. */
36
42
  tone?: PriceLineTone;
37
43
  /** Default `final`. */
@@ -54,6 +60,12 @@ export interface PriceTotal {
54
60
  label: string;
55
61
  /** PRE-FORMATTED, like every other amount here. */
56
62
  amount?: string;
63
+ /**
64
+ * A second, PRE-FORMATTED rendering of the same amount, drawn de-emphasised
65
+ * under it — the buyer's currency beside the shop's: `"≈ 12,00 €"`. Like
66
+ * `ListingPriceLine.secondary`, it is drawn as given; nothing converts.
67
+ */
68
+ secondaryAmount?: string;
57
69
  /** A quieter line under the total — "Includes taxes". */
58
70
  note?: string;
59
71
  /** Default `final`. */
@@ -2,6 +2,7 @@ import React, { memo } from 'react';
2
2
  import { Platform, View } from 'react-native';
3
3
 
4
4
  import { RiStarFill } from '../icons/remix/RiStarFill';
5
+ import { useDirectionProps } from '../hooks/use-is-rtl';
5
6
  import { useTheme } from '../theme/use-theme';
6
7
  import { Text } from '../typography';
7
8
  import type { TypeScaleVariant } from '../typography/scale';
@@ -21,6 +22,16 @@ import type { RatingProps, RatingSize } from './types';
21
22
  * reviews") on the row — `role="img"` on web, where a plain `div` with an
22
23
  * `aria-label` is not announced, and one `accessible` element on native — so a
23
24
  * screen reader never reads "star, 4.92, open paren, 128".
25
+ *
26
+ * `variant="stars"` draws five stars before the value instead of one, 2px
27
+ * apart, each filled to its share of the value: the unfilled star in
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
30
+ * right-to-left row fills from the right — the root carries
31
+ * `useDirectionProps()` for react-native-web to resolve it.
32
+ *
33
+ * `color` / `starColor` / `countColor` recolour it for a surface the theme's
34
+ * text pair cannot sit on (a brand-coloured hero).
24
35
  */
25
36
 
26
37
  const SIZE_CONFIG: Record<RatingSize, { star: number; value: TypeScaleVariant; count: TypeScaleVariant }> = {
@@ -37,6 +48,59 @@ export function formatRatingValue(value: number | string): string {
37
48
 
38
49
  const IS_WEB = Platform.OS === 'web';
39
50
 
51
+ /** The number of stars in the `stars` variant. */
52
+ const STAR_COUNT = 5;
53
+ const STAR_GAP = 2;
54
+
55
+ /** How much of star `index` (0-based) a rating fills, `0..1`. */
56
+ export function starFill(rating: number, index: number): number {
57
+ if (!Number.isFinite(rating)) return 0;
58
+ return Math.min(1, Math.max(0, rating - index));
59
+ }
60
+
61
+ function StarRow({
62
+ rating,
63
+ size,
64
+ fill,
65
+ empty,
66
+ testID,
67
+ }: {
68
+ rating: number;
69
+ size: number;
70
+ fill: string;
71
+ empty: string;
72
+ testID?: string;
73
+ }) {
74
+ return (
75
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: STAR_GAP }} testID={testID}>
76
+ {Array.from({ length: STAR_COUNT }, (_, index) => {
77
+ const share = starFill(rating, index);
78
+ return (
79
+ <View key={index} style={{ width: size, height: size }}>
80
+ <RiStarFill width={size} height={size} fill={empty} />
81
+ {share > 0 ? (
82
+ <View
83
+ testID={testID ? `${testID}-${index}` : undefined}
84
+ style={{
85
+ position: 'absolute',
86
+ top: 0,
87
+ bottom: 0,
88
+ insetInlineStart: 0,
89
+ width: size * share,
90
+ overflow: 'hidden',
91
+ alignItems: 'flex-start',
92
+ }}
93
+ >
94
+ <RiStarFill width={size} height={size} fill={fill} />
95
+ </View>
96
+ ) : null}
97
+ </View>
98
+ );
99
+ })}
100
+ </View>
101
+ );
102
+ }
103
+
40
104
  function RatingComponent({
41
105
  value,
42
106
  count,
@@ -44,11 +108,20 @@ function RatingComponent({
44
108
  reviewsLabel = 'reviews',
45
109
  newLabel = 'New',
46
110
  size = 'medium',
111
+ variant = 'compact',
112
+ color,
113
+ starColor,
114
+ countColor,
115
+ emptyStarColor,
47
116
  accessibilityLabel,
48
117
  style,
49
118
  testID,
50
119
  }: RatingProps) {
51
120
  const theme = useTheme();
121
+ const directionProps = useDirectionProps();
122
+ const textColor = color ?? theme.colors.text;
123
+ const starPaint = starColor ?? textColor;
124
+ const countPaint = countColor ?? color ?? theme.colors.textSecondary;
52
125
  const config = SIZE_CONFIG[size];
53
126
  const rated = value != null && value !== '';
54
127
  const shownValue = rated ? formatRatingValue(value) : newLabel;
@@ -66,18 +139,29 @@ function RatingComponent({
66
139
 
67
140
  return (
68
141
  <View
142
+ {...directionProps}
69
143
  testID={testID}
70
144
  accessible
71
145
  accessibilityLabel={name}
72
146
  {...(IS_WEB ? { role: 'img' as const } : null)}
73
147
  style={[{ flexDirection: 'row', alignItems: 'center', gap: 4 }, style]}
74
148
  >
75
- <RiStarFill width={config.star} height={config.star} fill={theme.colors.text} />
76
- <Text variant={config.value} style={{ color: theme.colors.text, fontVariant: ['tabular-nums'] }}>
149
+ {variant === 'stars' && rated ? (
150
+ <StarRow
151
+ rating={typeof value === 'number' ? value : Number(value)}
152
+ size={config.star}
153
+ fill={starPaint}
154
+ empty={emptyStarColor ?? theme.colors.border}
155
+ testID={testID ? `${testID}-stars` : undefined}
156
+ />
157
+ ) : variant === 'stars' ? null : (
158
+ <RiStarFill width={config.star} height={config.star} fill={starPaint} />
159
+ )}
160
+ <Text variant={config.value} style={{ color: textColor, fontVariant: ['tabular-nums'] }}>
77
161
  {shownValue}
78
162
  </Text>
79
163
  {countText != null && (
80
- <Text variant={config.count} style={{ color: theme.colors.textSecondary }}>
164
+ <Text variant={config.count} style={{ color: countPaint }}>
81
165
  {countText}
82
166
  </Text>
83
167
  )}
@@ -8,4 +8,5 @@ export type {
8
8
  RatingInputSize,
9
9
  RatingProps,
10
10
  RatingSize,
11
+ RatingVariant,
11
12
  } from './types';
@@ -6,6 +6,13 @@ export type RatingSize = 'small' | 'medium';
6
6
  /** `parenthesis` draws "(128)"; `reviews` draws "· 128 reviews". */
7
7
  export type RatingCountStyle = 'parenthesis' | 'reviews';
8
8
 
9
+ /**
10
+ * `compact` (default) is one star and the value; `stars` a full row of `max`
11
+ * stars (five) filled to the value — fractionally, so 4.3 fills the fifth star
12
+ * 30% — before the value.
13
+ */
14
+ export type RatingVariant = 'compact' | 'stars';
15
+
9
16
  export interface RatingProps {
10
17
  /**
11
18
  * The rating on a 5 scale. A number is drawn with up to two decimals (4.9,
@@ -23,6 +30,20 @@ export interface RatingProps {
23
30
  newLabel?: string;
24
31
  /** Default `medium`. */
25
32
  size?: RatingSize;
33
+ /** Default `compact` — one star. `stars` draws a row of five, filled fractionally. */
34
+ variant?: RatingVariant;
35
+ /**
36
+ * Paints the star(s) and the value — and the count, unless `countColor` is
37
+ * given — e.g. white over a brand-coloured hero, where text-primary and
38
+ * text-secondary are illegible. Default: text-primary (count text-secondary).
39
+ */
40
+ color?: string;
41
+ /** The filled star(s) alone, e.g. a gold star beside neutral text. Default `color`. */
42
+ starColor?: string;
43
+ /** The count alone. Default `color` when given, else text-secondary. */
44
+ countColor?: string;
45
+ /** `stars` variant: the unfilled part of each star. Default the theme's border colour. */
46
+ emptyStarColor?: string;
26
47
  /**
27
48
  * Overrides the composed accessible name ("Rated 4.92 out of 5, 128
28
49
  * reviews"), which is English — pass a translated sentence here.
@@ -1,6 +1,8 @@
1
- import { forwardRef } from 'react';
1
+ import { forwardRef, useCallback, useMemo, useRef } from 'react';
2
2
  import { Platform, type TextInput, View } from 'react-native';
3
3
 
4
+ import { mergeRefs } from '../hooks/merge-refs';
5
+
4
6
  import { useTheme } from '../theme/use-theme';
5
7
  import { atoms as a } from '../styles';
6
8
  import { borderRadius } from '../styles/tokens';
@@ -18,22 +20,56 @@ type SearchProps = Omit<TextFieldInputProps, 'label'> & {
18
20
  };
19
21
 
20
22
  export const Search = forwardRef<TextInput, SearchProps>(
21
- function Search({ value, label = 'Search', onClearText, ...rest }, ref) {
23
+ function Search({ value, label = 'Search', onClearText, onFocus, onPressIn, ...rest }, ref) {
22
24
  const theme = useTheme();
23
25
  const showClear = value != null && value.length > 0;
24
26
 
27
+ // Select-on-focus, done here rather than through RN's `selectTextOnFocus`.
28
+ // On Android that prop does not select on focus: it arms a one-shot flag and
29
+ // calls `selectAll()` on the input's NEXT layout while focused. An
30
+ // autofocused search lays out nothing after focusing until the first
31
+ // keystroke mounts the clear button and pads the input for it — and that
32
+ // layout selects everything, so the SECOND keystroke replaced the query.
33
+ // Typing "mention" in one burst left "on" (OxyHQ/Mention#1126). Now a
34
+ // press-driven focus selects the current query once, immediately, and a
35
+ // programmatic focus (autoFocus, `ref.focus()`) selects nothing. Web never
36
+ // selected on focus and still does not.
37
+ const inputRef = useRef<TextInput | null>(null);
38
+ const refs = useMemo(() => mergeRefs([inputRef, ref]), [ref]);
39
+ const pressedRef = useRef(false);
40
+ const valueRef = useRef(value);
41
+ valueRef.current = value;
42
+ const handlePressIn = useCallback<NonNullable<SearchProps['onPressIn']>>(
43
+ (event) => {
44
+ pressedRef.current = true;
45
+ onPressIn?.(event);
46
+ },
47
+ [onPressIn],
48
+ );
49
+ const handleFocus = useCallback<NonNullable<SearchProps['onFocus']>>(
50
+ (event) => {
51
+ onFocus?.(event);
52
+ const pressed = pressedRef.current;
53
+ pressedRef.current = false;
54
+ const length = valueRef.current?.length ?? 0;
55
+ if (pressed && Platform.OS !== 'web' && length > 0) {
56
+ inputRef.current?.setSelection(0, length);
57
+ }
58
+ },
59
+ [onFocus],
60
+ );
61
+
25
62
  return (
26
63
  <View style={[a.w_full, a.relative]}>
27
64
  <TextField radius={borderRadius.full}>
28
65
  <TextFieldIcon icon={MagnifyingGlassIcon} />
29
66
  <TextFieldInput
30
- inputRef={ref}
67
+ inputRef={refs}
31
68
  label={label}
32
69
  value={value}
33
70
  placeholder={label}
34
71
  returnKeyType="search"
35
72
  keyboardAppearance={theme.mode === 'light' ? 'light' : 'dark'}
36
- selectTextOnFocus={Platform.OS !== 'web'}
37
73
  autoFocus={false}
38
74
  accessibilityRole="search"
39
75
  autoCorrect={false}
@@ -41,6 +77,8 @@ export const Search = forwardRef<TextInput, SearchProps>(
41
77
  autoCapitalize="none"
42
78
  style={showClear ? { paddingRight: 24 } : undefined}
43
79
  {...rest}
80
+ onPressIn={handlePressIn}
81
+ onFocus={handleFocus}
44
82
  />
45
83
  </TextField>
46
84
 
@@ -37,6 +37,13 @@ export type SelectTriggerProps = {
37
37
  /**
38
38
  * Utility classes APPENDED to the part's own — never substituted for them, so
39
39
  * a single layout class cannot strip the chrome.
40
+ *
41
+ * Never pin a HEIGHT here (`h-8`). The value's line box is 20 × the system
42
+ * font scale, and on native Yoga measures the text at most as tall as the
43
+ * space left inside the field: a fixed height cuts the glyphs in half on
44
+ * Android (OxyHQ/Mention#1126 — `h-8` is 28dp there, leaving 15.5dp for a
45
+ * 20dp line). Size a compact trigger with its padding (`fieldStyle`) so the
46
+ * text sets the height.
40
47
  */
41
48
  className?: string;
42
49
  /**
@@ -6,7 +6,7 @@ import React, {
6
6
  useRef,
7
7
  useState,
8
8
  } from 'react';
9
- import { BackHandler, Pressable, StyleSheet, View, useWindowDimensions } from 'react-native';
9
+ import { Pressable, StyleSheet, View, useWindowDimensions } from 'react-native';
10
10
  import Animated, {
11
11
  Easing,
12
12
  useAnimatedStyle,
@@ -263,18 +263,13 @@ export function SettingsModal({
263
263
  const compactPageOpenRef = useRef(false);
264
264
  compactPageOpenRef.current = compactPageOpen && layoutRef.current === 'compact';
265
265
 
266
- // Android back closes, like a native modal.
267
- useEffect(() => {
268
- // `BackHandler` is absent from some test environments' react-native mocks.
269
- if (!mounted || IS_WEB || typeof BackHandler?.addEventListener !== 'function') return;
270
- const sub = BackHandler.addEventListener('hardwareBackPress', () => {
271
- // On a pushed page (compact), back returns to the section list first.
272
- if (compactPageOpenRef.current) requestNavigation();
273
- else requestClose();
274
- return true;
275
- });
276
- return () => sub.remove();
277
- }, [mounted, requestClose, requestNavigation]);
266
+ // Android back closes, like a native modal — handed to `OverlayRoot`, which
267
+ // owns hardware back for every portaled surface.
268
+ const onHardwareBack = useCallback(() => {
269
+ // On a pushed page (compact), back returns to the section list first.
270
+ if (compactPageOpenRef.current) requestNavigation();
271
+ else requestClose();
272
+ }, [requestClose, requestNavigation]);
278
273
 
279
274
  // ----- saved toast -------------------------------------------------------
280
275
  const [savedPhase, setSavedPhase] = useState<SavedPhase>('hidden');
@@ -351,7 +346,7 @@ export function SettingsModal({
351
346
  return (
352
347
  <ModalPortal>
353
348
  <SettingsModalContext.Provider value={context}>
354
- <OverlayRoot>
349
+ <OverlayRoot onRequestClose={onHardwareBack} modal>
355
350
  <Backdrop
356
351
  onPress={requestClose}
357
352
  progress={progress}
@@ -813,7 +808,17 @@ function SavedToast({
813
808
 
814
809
  const styles = StyleSheet.create({
815
810
  center: {
816
- ...StyleSheet.absoluteFillObject,
811
+ // Spelled out, not `StyleSheet.absoluteFillObject`: RN 0.85 removed it, and
812
+ // spreading the resulting `undefined` left this box in flow at ZERO height.
813
+ // The panel still painted (nothing clips it) and still took touches, but
814
+ // Android reports every view under a zero-area ancestor as not visible to
815
+ // the user, so TalkBack and UI Automator saw no modal content at all —
816
+ // only the backdrop's full-screen "Close".
817
+ position: 'absolute',
818
+ top: 0,
819
+ left: 0,
820
+ right: 0,
821
+ bottom: 0,
817
822
  alignItems: 'center',
818
823
  justifyContent: 'center',
819
824
  padding: VIEWPORT_GUTTER,
@@ -5,6 +5,7 @@ import { Button } from '../button';
5
5
  import { webDataSet } from '../styles/web-data';
6
6
  import { useAccessibleNameWarning } from '../hooks/use-accessible-name-warning';
7
7
  import { RiAddLine } from '../icons/remix/RiAddLine';
8
+ import { RiDeleteBinLine } from '../icons/remix/RiDeleteBinLine';
8
9
  import { RiSubtractLine } from '../icons/remix/RiSubtractLine';
9
10
  import { useRingOffsetStyle } from '../styles/surface-levels';
10
11
  import { interactiveWebCss, useInteractiveWebCss } from '../styles/interactive-web-css';
@@ -33,6 +34,14 @@ import { useFieldMembership } from '../field/membership';
33
34
  * the tab order, being pointer affordances for the same action. On native it
34
35
  * answers the `increment`/`decrement` accessibility actions (VoiceOver swipe
35
36
  * up/down, TalkBack volume keys).
37
+ *
38
+ * REMOVE AT THE FLOOR is opt-in (`onRemove`). With it, the `−` button at `min`
39
+ * turns into a trash button named `removeLabel` that calls `onRemove` — a
40
+ * basket line that goes from 1 to gone. Without it the button disables at
41
+ * `min`, as it always has. The keyboard and the accessibility actions on the
42
+ * value never remove: an arrow key or a volume-key swipe that deletes is a
43
+ * destructive action behind an arithmetic one. So that a keyboard user can
44
+ * still reach it, the button joins the tab order while it is the remove control.
36
45
  */
37
46
 
38
47
  const IS_WEB = Platform.OS === 'web';
@@ -83,6 +92,8 @@ function StepperComponent({
83
92
  accessibilityLabel,
84
93
  decrementLabel = 'Decrease',
85
94
  incrementLabel = 'Increase',
95
+ onRemove,
96
+ removeLabel = 'Remove',
86
97
  style,
87
98
  testID,
88
99
  }: StepperProps) {
@@ -99,6 +110,8 @@ function StepperComponent({
99
110
 
100
111
  const canDecrement = !disabled && value > min;
101
112
  const canIncrement = !disabled && (max === undefined || value < max);
113
+ // At the floor with `onRemove`, the decrement slot IS the remove control.
114
+ const removes = onRemove !== undefined && !disabled && value <= min;
102
115
 
103
116
  const change = useCallback(
104
117
  (target: number) => {
@@ -180,11 +193,11 @@ function StepperComponent({
180
193
  variant="secondary"
181
194
  size={config.button}
182
195
  iconOnly
183
- icon={RiSubtractLine}
184
- onPress={decrement}
185
- disabled={!canDecrement}
186
- accessibilityLabel={decrementLabel}
187
- tabIndex={-1}
196
+ icon={removes ? RiDeleteBinLine : RiSubtractLine}
197
+ onPress={removes ? onRemove : decrement}
198
+ disabled={!(canDecrement || removes)}
199
+ accessibilityLabel={removes ? removeLabel : decrementLabel}
200
+ tabIndex={removes ? 0 : -1}
188
201
  testID={testID ? `${testID}-decrement` : undefined}
189
202
  />
190
203
  <View
@@ -38,6 +38,15 @@ export interface StepperProps {
38
38
  decrementLabel?: string;
39
39
  /** Name of the `+` button. Default `"Increase"`. */
40
40
  incrementLabel?: string;
41
+ /**
42
+ * Opt-in remove at the floor: at `min` the `−` button becomes a trash button
43
+ * that calls this instead of disabling (a basket line going from 1 to gone).
44
+ * Only the button removes — the keyboard and accessibility actions on the
45
+ * value still stop at `min`. Without it the stepper floors at `min`.
46
+ */
47
+ onRemove?: () => void;
48
+ /** Name of the trash button drawn at `min` with `onRemove`. Default `"Remove"`. */
49
+ removeLabel?: string;
41
50
  style?: StyleProp<ViewStyle>;
42
51
  testID?: string;
43
52
  }
@@ -25,7 +25,7 @@ import PortalHost from "./PortalHostView";
25
25
  * export default function App() {
26
26
  * return (
27
27
  * <PortalProvider>
28
- * <PortalHost style={StyleSheet.absoluteFillObject} name="overlay" />
28
+ * <PortalHost style={StyleSheet.absoluteFill} name="overlay" />
29
29
  * </PortalProvider>
30
30
  * );
31
31
  * }
@@ -25,6 +25,7 @@ import {
25
25
  Gesture,
26
26
  GestureDetector,
27
27
  } from 'react-native-gesture-handler';
28
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
28
29
  import { borderRadius } from '../styles/tokens';
29
30
  import { Backdrop, OverlayRoot } from '../overlay';
30
31
  import { Portal } from '../portal';
@@ -109,6 +110,13 @@ function shareUri(item: GalleryMedia): string | undefined {
109
110
  // image. `Platform.select` keeps them off native. `cursor` is a real
110
111
  // `ViewStyle` key on RN 0.83; `userSelect` is declared on `TextStyle` only,
111
112
  // which is what `WebCssStyle` covers.
113
+ /** Share button inset from the top-right corner of the safe area. */
114
+ const CHROME_EDGE = 16;
115
+ /** Page indicator (and a lone caption) above the bottom of the safe area. */
116
+ const INDICATOR_BOTTOM = 48;
117
+ /** A caption lifted above the page counter + dots when the indicator shows. */
118
+ const CAPTION_BOTTOM_WITH_INDICATOR = 110;
119
+
112
120
  const WEB_POINTER: WebCssStyle = { userSelect: 'none', cursor: 'pointer' };
113
121
  const WEB_USER_SELECT_NONE: WebCssStyle = { userSelect: 'none' };
114
122
 
@@ -207,6 +215,9 @@ function resolveCornerRadius(cornerRadius: number | 'circle', fit: FittedSize):
207
215
  */
208
216
  const ZoomableMediaGalleryInner = React.forwardRef<ZoomableMediaGalleryHandle, ZoomableMediaGalleryProps>(({ measureThumb, cornerRadius = DEFAULT_CORNER_RADIUS, indicatorVariant = 'dots', videoControls = false }, ref) => {
209
217
  const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = useWindowDimensions();
218
+ // The viewer is full-bleed, so its chrome must clear the status bar, the
219
+ // cutout and the home indicator itself — nothing above it applies them.
220
+ const insets = useSafeAreaInsets();
210
221
 
211
222
  const radiusFor = useCallback(
212
223
  (fit: FittedSize) => resolveCornerRadius(cornerRadius, fit),
@@ -928,7 +939,10 @@ const ZoomableMediaGalleryInner = React.forwardRef<ZoomableMediaGalleryHandle, Z
928
939
  }, [handleDismiss, isOpen, pageTo]);
929
940
 
930
941
  const renderContent = () => (
931
- <OverlayRoot style={styles.modalContainer}>
942
+ // `onRequestClose`: Android back flies the media home like every other
943
+ // dismiss. The viewer is a Portal, not an RN `Modal`, so without it nothing
944
+ // consumed the press and back finished the whole app.
945
+ <OverlayRoot style={styles.modalContainer} onRequestClose={handleDismiss} modal>
932
946
  <GestureHandlerRootView style={StyleSheet.absoluteFill}>
933
947
  {/* Shared `Backdrop` (a Pressable) rather than a `Gesture.Tap()`: the
934
948
  viewer renders through the web Portal, whose root is
@@ -1063,7 +1077,10 @@ const ZoomableMediaGalleryInner = React.forwardRef<ZoomableMediaGalleryHandle, Z
1063
1077
  )}
1064
1078
 
1065
1079
  {pagerReady && items.length > 1 && (
1066
- <Animated.View style={[styles.indicatorWrap, backdropStyle]} pointerEvents="box-none">
1080
+ <Animated.View
1081
+ style={[styles.indicatorWrap, { bottom: INDICATOR_BOTTOM + insets.bottom }, backdropStyle]}
1082
+ pointerEvents="box-none"
1083
+ >
1067
1084
  <View style={styles.counterPill} pointerEvents="none">
1068
1085
  <Text style={styles.counterText}>{`${activeIndex + 1} / ${items.length}`}</Text>
1069
1086
  </View>
@@ -1120,7 +1137,11 @@ const ZoomableMediaGalleryInner = React.forwardRef<ZoomableMediaGalleryHandle, Z
1120
1137
  accessibilityRole="button"
1121
1138
  accessibilityLabel="Share media"
1122
1139
  hitSlop={8}
1123
- style={[styles.shareButton, webPointerStyle]}
1140
+ style={[
1141
+ styles.shareButton,
1142
+ { top: CHROME_EDGE + insets.top, right: CHROME_EDGE + insets.right },
1143
+ webPointerStyle,
1144
+ ]}
1124
1145
  >
1125
1146
  <RiUpload2Line fill="#fff" size="md" />
1126
1147
  </Pressable>
@@ -1130,7 +1151,11 @@ const ZoomableMediaGalleryInner = React.forwardRef<ZoomableMediaGalleryHandle, Z
1130
1151
  <Animated.View
1131
1152
  style={[
1132
1153
  styles.altCaptionWrap,
1133
- items.length > 1 && styles.altCaptionWrapWithIndicator,
1154
+ {
1155
+ bottom:
1156
+ (items.length > 1 ? CAPTION_BOTTOM_WITH_INDICATOR : INDICATOR_BOTTOM) +
1157
+ insets.bottom,
1158
+ },
1134
1159
  backdropStyle,
1135
1160
  ]}
1136
1161
  pointerEvents="none"
@@ -1209,9 +1234,9 @@ const styles = StyleSheet.create({
1209
1234
  mediaBox: {
1210
1235
  overflow: 'hidden',
1211
1236
  },
1237
+ // `bottom` is inline: INDICATOR_BOTTOM above the bottom safe-area inset.
1212
1238
  indicatorWrap: {
1213
1239
  position: 'absolute',
1214
- bottom: 48,
1215
1240
  left: 0,
1216
1241
  right: 0,
1217
1242
  alignItems: 'center',
@@ -1298,10 +1323,10 @@ const styles = StyleSheet.create({
1298
1323
  backgroundColor: 'rgba(0,0,0,0.7)',
1299
1324
  transform: [{ translateY: -22 }, { scale: 1.08 }],
1300
1325
  },
1326
+ // `top`/`right` are inline: CHROME_EDGE inside the safe area. A bare 16
1327
+ // put the button under the Android status bar, over the battery icon.
1301
1328
  shareButton: {
1302
1329
  position: 'absolute',
1303
- top: 16,
1304
- right: 16,
1305
1330
  width: 40,
1306
1331
  height: 40,
1307
1332
  borderRadius: borderRadius.full,
@@ -1309,18 +1334,14 @@ const styles = StyleSheet.create({
1309
1334
  justifyContent: 'center',
1310
1335
  backgroundColor: 'rgba(0,0,0,0.45)',
1311
1336
  },
1337
+ // `bottom` is inline, above the indicator when there is one, inside the safe area.
1312
1338
  altCaptionWrap: {
1313
1339
  position: 'absolute',
1314
- bottom: 48,
1315
1340
  left: 0,
1316
1341
  right: 0,
1317
1342
  alignItems: 'center',
1318
1343
  paddingHorizontal: 24,
1319
1344
  },
1320
- // Lifted above the page counter + dots when the multi-image indicator shows.
1321
- altCaptionWrapWithIndicator: {
1322
- bottom: 110,
1323
- },
1324
1345
  altCaptionPill: {
1325
1346
  maxWidth: 600,
1326
1347
  paddingHorizontal: 14,