@12-apps/ui 6.23.0 → 6.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/NATIVE.md +2 -1
  2. package/dist/_internal/components/data-display/Avatar/Avatar.js +3 -3
  3. package/dist/_internal/components/data-display/Avatar/Avatar.view.js +2 -2
  4. package/dist/_internal/components/data-display/Badge/Badge.js +2 -2
  5. package/dist/_internal/components/data-display/Chart/Chart.js +3 -3
  6. package/dist/_internal/components/data-display/Chart/SpecChart.js +6 -6
  7. package/dist/_internal/components/data-display/Chart/chart-renderers.js +2 -2
  8. package/dist/_internal/components/data-display/Chart/chart-spec.js +2 -2
  9. package/dist/_internal/components/data-display/Chip/Chip.js +3 -3
  10. package/dist/_internal/components/data-display/Chip/Chip.styles.js +2 -2
  11. package/dist/_internal/components/data-display/DataGrid/DataGrid.js +2 -2
  12. package/dist/_internal/components/data-display/DataViews/DataViewsGrid.js +24 -24
  13. package/dist/_internal/components/data-display/DataViews/DataViewsTableBase.js +30 -30
  14. package/dist/_internal/components/data-display/DataViews/base-list-card-geometry.js +4 -4
  15. package/dist/_internal/components/data-display/DataViews/base-list-card-slots.js +3 -3
  16. package/dist/_internal/components/data-display/DataViews/base-list-card.js +6 -6
  17. package/dist/_internal/components/data-display/DataViews/data-views-display-panel.js +3 -3
  18. package/dist/_internal/components/data-display/DataViews/data-views-filter-panel.js +4 -4
  19. package/dist/_internal/components/data-display/DataViews/data-views-grid-bodies.js +6 -6
  20. package/dist/_internal/components/data-display/DataViews/data-views-grid-parts.js +21 -21
  21. package/dist/_internal/components/data-display/DataViews/data-views-inline-bar.js +6 -6
  22. package/dist/_internal/components/data-display/DataViews/data-views-more-fields.js +4 -4
  23. package/dist/_internal/components/data-display/DataViews/data-views-more-filters.js +5 -5
  24. package/dist/_internal/components/data-display/DataViews/data-views-range-pill.js +3 -3
  25. package/dist/_internal/components/data-display/DataViews/data-views-scope-tabs.js +2 -2
  26. package/dist/_internal/components/data-display/DataViews/data-views-shell-toolbar.js +6 -6
  27. package/dist/_internal/components/data-display/DataViews/data-views-table-parts.js +2 -2
  28. package/dist/_internal/components/data-display/DataViews/data-views-toolbar.js +5 -5
  29. package/dist/_internal/components/data-display/DataViews/list-card-rails.js +2 -2
  30. package/dist/_internal/components/data-display/DataViews/use-data-views-state.js +2 -2
  31. package/dist/_internal/components/data-display/FleetMap/FleetBody.js +4 -3
  32. package/dist/_internal/components/data-display/FleetMap/FleetMap.base.js +1 -0
  33. package/dist/_internal/components/data-display/FleetMap/FleetMap.base.js.map +1 -0
  34. package/dist/_internal/components/data-display/FleetMap/FleetMap.helpers.js +1 -1
  35. package/dist/_internal/components/data-display/FleetMap/FleetMap.hooks.js +2 -2
  36. package/dist/_internal/components/data-display/FleetMap/FleetMap.js +6 -5
  37. package/dist/_internal/components/data-display/FleetMap/FleetMap.metrics.js +20 -0
  38. package/dist/_internal/components/data-display/FleetMap/FleetMap.metrics.js.map +1 -0
  39. package/dist/_internal/components/data-display/FleetMap/FleetRoster.js +3 -2
  40. package/dist/_internal/components/data-display/index.js +26 -25
  41. package/dist/_internal/components/index.js +26 -25
  42. package/dist/_internal/platform/aria.js.map +1 -1
  43. package/dist/_internal/user-avatar/user-avatar.js +4 -4
  44. package/dist/charts.js +10 -10
  45. package/dist/{chunk-WGRS4PGY.js → chunk-2C47DEVU.js} +4 -4
  46. package/dist/{chunk-KHWCX4LI.js → chunk-6LGWJFQK.js} +2 -2
  47. package/dist/{chunk-XSF6AXEU.js → chunk-6RZDDUSM.js} +2 -2
  48. package/dist/{chunk-L4U23SBX.js → chunk-B7DKSBAE.js} +13 -13
  49. package/dist/{chunk-3FLVIVOK.js → chunk-BHIGHSXA.js} +2 -2
  50. package/dist/chunk-BTQZ3EX3.js +45 -0
  51. package/dist/chunk-BTQZ3EX3.js.map +1 -0
  52. package/dist/{chunk-ACFGEZ4Q.js → chunk-C6KTQF4F.js} +3 -3
  53. package/dist/{chunk-LYSSO6WC.js → chunk-CZPU5TW3.js} +4 -4
  54. package/dist/{chunk-BZTFPSWT.js → chunk-DFLTUI7W.js} +7 -7
  55. package/dist/{chunk-URXT5TOY.js → chunk-E46PG577.js} +4 -4
  56. package/dist/{chunk-4XZXLB4I.js → chunk-EMUBMZMA.js} +3 -3
  57. package/dist/{chunk-DRHIIUDO.js → chunk-FPWB5ILF.js} +5 -5
  58. package/dist/{chunk-R6SBP5M5.js → chunk-GOWBYHYW.js} +4 -4
  59. package/dist/{chunk-MQNKRIGC.js → chunk-HMV4QMXS.js} +25 -11
  60. package/dist/chunk-HMV4QMXS.js.map +1 -0
  61. package/dist/{chunk-N2TUX4A5.js → chunk-I3E2QHTH.js} +3 -3
  62. package/dist/{chunk-I7HYGIEJ.js → chunk-JUHUKZGU.js} +2 -2
  63. package/dist/{chunk-TWQYDKCA.js → chunk-JZXWCEH5.js} +12 -9
  64. package/dist/chunk-JZXWCEH5.js.map +1 -0
  65. package/dist/{chunk-WQA4LIIK.js → chunk-KT6HWAEN.js} +4 -4
  66. package/dist/{chunk-2UGJIKVA.js → chunk-MZQTHSLN.js} +3 -3
  67. package/dist/{chunk-4SQFO4GW.js → chunk-NA7MIEG6.js} +7 -7
  68. package/dist/{chunk-MYGNIFZC.js → chunk-O4VCZTK4.js} +2 -2
  69. package/dist/{chunk-LS4FMSHY.js → chunk-OAYX4XN7.js} +4 -4
  70. package/dist/{chunk-Y5UCOBQN.js → chunk-OGGHD64H.js} +4 -4
  71. package/dist/{chunk-CBTY36TU.js → chunk-OH3YGJT5.js} +2 -2
  72. package/dist/{chunk-VNNG46TS.js → chunk-ORSLFKKZ.js} +6 -6
  73. package/dist/{chunk-VON4BFK4.js → chunk-PN5CQKA2.js} +2 -2
  74. package/dist/{chunk-CF3FAAPC.js → chunk-RP5FK3SO.js} +2 -2
  75. package/dist/{chunk-X5MPHXG5.js → chunk-RPWEXIAW.js} +6 -6
  76. package/dist/{chunk-YQ6TNHAC.js → chunk-TTLI6PF7.js} +5 -5
  77. package/dist/{chunk-OIKQO73C.js → chunk-TX5V42AY.js} +5 -5
  78. package/dist/{chunk-AJGYHDA5.js → chunk-U2YJCNRU.js} +4 -4
  79. package/dist/{chunk-W7Y6RA6W.js → chunk-WR5JFDQ5.js} +4 -4
  80. package/dist/{chunk-5V4UPOWK.js → chunk-XPMGFAJ3.js} +1 -1
  81. package/dist/chunk-XPMGFAJ3.js.map +1 -0
  82. package/dist/{chunk-AEGXHZQT.js → chunk-YW5T6KZ5.js} +7 -7
  83. package/dist/{chunk-F7VBGS4C.js → chunk-ZCPSND5Y.js} +2 -2
  84. package/dist/chunk-ZCPSND5Y.js.map +1 -0
  85. package/dist/{chunk-4LRFGB3K.js → chunk-ZRPGVHTR.js} +2 -2
  86. package/dist/data-display/Avatar.js +3 -3
  87. package/dist/data-display/Badge.js +2 -2
  88. package/dist/data-display/CardKit.js +10 -10
  89. package/dist/data-display/Chart.js +10 -10
  90. package/dist/data-display/Chip.js +3 -3
  91. package/dist/data-display/DataGrid.js +2 -2
  92. package/dist/data-display/DataViews.js +48 -48
  93. package/dist/data-display/FleetMap.js +6 -5
  94. package/dist/native/chunks/{chunk-T6BJPP3E.js → chunk-DN73KYIZ.js} +1 -1
  95. package/dist/native/chunks/chunk-DN73KYIZ.js.map +1 -0
  96. package/dist/native/chunks/chunk-GW7TQ7HO.js +215 -0
  97. package/dist/native/chunks/chunk-GW7TQ7HO.js.map +1 -0
  98. package/dist/native/chunks/chunk-MJVSQXOD.js +9 -0
  99. package/dist/native/chunks/chunk-MJVSQXOD.js.map +1 -0
  100. package/dist/native/chunks/chunk-ZAQ3AZJM.js +292 -0
  101. package/dist/native/chunks/chunk-ZAQ3AZJM.js.map +1 -0
  102. package/dist/native/data-display/Alert.js +4 -4
  103. package/dist/native/data-display/Avatar.js +3 -3
  104. package/dist/native/data-display/Badge.js +4 -4
  105. package/dist/native/data-display/Chip.js +3 -5
  106. package/dist/native/data-display/Chip.js.map +1 -1
  107. package/dist/native/data-display/EmptyState.js +9 -286
  108. package/dist/native/data-display/EmptyState.js.map +1 -1
  109. package/dist/native/data-display/ErrorState.js +6 -6
  110. package/dist/native/data-display/FleetMap.js +498 -0
  111. package/dist/native/data-display/FleetMap.js.map +1 -0
  112. package/dist/native/data-display/LoadingState.js +3 -3
  113. package/dist/native/feedback/Dialog.js +3 -3
  114. package/dist/native/form/Checkbox.js +1 -1
  115. package/dist/native/form/Input.js +1 -1
  116. package/dist/native/form/Select.js +1 -1
  117. package/dist/native/form/Switch.js +1 -1
  118. package/dist/native/layout/Card.js +3 -3
  119. package/dist/native/layout/Skeleton.js +6 -209
  120. package/dist/native/layout/Skeleton.js.map +1 -1
  121. package/dist/types/components/data-display/FleetMap/FleetMap.base.d.ts +141 -0
  122. package/dist/types/components/data-display/FleetMap/FleetMap.base.d.ts.map +1 -0
  123. package/dist/types/components/data-display/FleetMap/FleetMap.d.ts.map +1 -1
  124. package/dist/types/components/data-display/FleetMap/FleetMap.helpers.d.ts +1 -1
  125. package/dist/types/components/data-display/FleetMap/FleetMap.helpers.d.ts.map +1 -1
  126. package/dist/types/components/data-display/FleetMap/FleetMap.hooks.d.ts +20 -3
  127. package/dist/types/components/data-display/FleetMap/FleetMap.hooks.d.ts.map +1 -1
  128. package/dist/types/components/data-display/FleetMap/FleetMap.metrics.d.ts +76 -0
  129. package/dist/types/components/data-display/FleetMap/FleetMap.metrics.d.ts.map +1 -0
  130. package/dist/types/components/data-display/FleetMap/FleetMap.types.d.ts +13 -113
  131. package/dist/types/components/data-display/FleetMap/FleetMap.types.d.ts.map +1 -1
  132. package/dist/types/components/data-display/FleetMap/FleetRoster.d.ts.map +1 -1
  133. package/dist/types/platform/aria.d.ts +13 -0
  134. package/dist/types/platform/aria.d.ts.map +1 -1
  135. package/dist/types-native/components/data-display/FleetMap/FleetMap.base.d.ts +141 -0
  136. package/dist/types-native/components/data-display/FleetMap/FleetMap.base.d.ts.map +1 -0
  137. package/dist/types-native/components/data-display/FleetMap/FleetMap.helpers.d.ts +45 -0
  138. package/dist/types-native/components/data-display/FleetMap/FleetMap.helpers.d.ts.map +1 -0
  139. package/dist/types-native/components/data-display/FleetMap/FleetMap.hooks.d.ts +71 -0
  140. package/dist/types-native/components/data-display/FleetMap/FleetMap.hooks.d.ts.map +1 -0
  141. package/dist/types-native/components/data-display/FleetMap/FleetMap.metrics.d.ts +76 -0
  142. package/dist/types-native/components/data-display/FleetMap/FleetMap.metrics.d.ts.map +1 -0
  143. package/dist/types-native/components/data-display/FleetMap/FleetMap.native.d.ts +30 -0
  144. package/dist/types-native/components/data-display/FleetMap/FleetMap.native.d.ts.map +1 -0
  145. package/dist/types-native/components/data-display/FleetMap/FleetMap.types.native.d.ts +36 -0
  146. package/dist/types-native/components/data-display/FleetMap/FleetMap.types.native.d.ts.map +1 -0
  147. package/dist/types-native/components/data-display/FleetMap/FleetRoster.native.d.ts +17 -0
  148. package/dist/types-native/components/data-display/FleetMap/FleetRoster.native.d.ts.map +1 -0
  149. package/dist/types-native/components/data-display/FleetMap/index.native.d.ts +4 -0
  150. package/dist/types-native/components/data-display/FleetMap/index.native.d.ts.map +1 -0
  151. package/dist/types-native/platform/aria.d.ts +13 -0
  152. package/dist/types-native/platform/aria.d.ts.map +1 -1
  153. package/dist/user-avatar.js +4 -4
  154. package/package.json +5 -1
  155. package/src/components/data-display/FleetMap/FleetMap.base.ts +144 -0
  156. package/src/components/data-display/FleetMap/FleetMap.helpers.ts +1 -1
  157. package/src/components/data-display/FleetMap/FleetMap.hooks.ts +23 -4
  158. package/src/components/data-display/FleetMap/FleetMap.md +15 -8
  159. package/src/components/data-display/FleetMap/FleetMap.metrics.ts +81 -0
  160. package/src/components/data-display/FleetMap/FleetMap.native.tsx +185 -0
  161. package/src/components/data-display/FleetMap/FleetMap.tsx +8 -14
  162. package/src/components/data-display/FleetMap/FleetMap.types.native.ts +44 -0
  163. package/src/components/data-display/FleetMap/FleetMap.types.ts +19 -114
  164. package/src/components/data-display/FleetMap/FleetRoster.native.tsx +382 -0
  165. package/src/components/data-display/FleetMap/FleetRoster.tsx +17 -11
  166. package/src/components/data-display/FleetMap/NATIVE-NOTES.md +14 -0
  167. package/src/components/data-display/FleetMap/index.native.ts +10 -0
  168. package/src/components/data-display/FleetMap/tests.md +6 -1
  169. package/src/platform/aria.ts +13 -0
  170. package/dist/chunk-5V4UPOWK.js.map +0 -1
  171. package/dist/chunk-F7VBGS4C.js.map +0 -1
  172. package/dist/chunk-MQNKRIGC.js.map +0 -1
  173. package/dist/chunk-TWQYDKCA.js.map +0 -1
  174. package/dist/native/chunks/chunk-T6BJPP3E.js.map +0 -1
  175. /package/dist/{chunk-WGRS4PGY.js.map → chunk-2C47DEVU.js.map} +0 -0
  176. /package/dist/{chunk-KHWCX4LI.js.map → chunk-6LGWJFQK.js.map} +0 -0
  177. /package/dist/{chunk-XSF6AXEU.js.map → chunk-6RZDDUSM.js.map} +0 -0
  178. /package/dist/{chunk-L4U23SBX.js.map → chunk-B7DKSBAE.js.map} +0 -0
  179. /package/dist/{chunk-3FLVIVOK.js.map → chunk-BHIGHSXA.js.map} +0 -0
  180. /package/dist/{chunk-ACFGEZ4Q.js.map → chunk-C6KTQF4F.js.map} +0 -0
  181. /package/dist/{chunk-LYSSO6WC.js.map → chunk-CZPU5TW3.js.map} +0 -0
  182. /package/dist/{chunk-BZTFPSWT.js.map → chunk-DFLTUI7W.js.map} +0 -0
  183. /package/dist/{chunk-URXT5TOY.js.map → chunk-E46PG577.js.map} +0 -0
  184. /package/dist/{chunk-4XZXLB4I.js.map → chunk-EMUBMZMA.js.map} +0 -0
  185. /package/dist/{chunk-DRHIIUDO.js.map → chunk-FPWB5ILF.js.map} +0 -0
  186. /package/dist/{chunk-R6SBP5M5.js.map → chunk-GOWBYHYW.js.map} +0 -0
  187. /package/dist/{chunk-N2TUX4A5.js.map → chunk-I3E2QHTH.js.map} +0 -0
  188. /package/dist/{chunk-I7HYGIEJ.js.map → chunk-JUHUKZGU.js.map} +0 -0
  189. /package/dist/{chunk-WQA4LIIK.js.map → chunk-KT6HWAEN.js.map} +0 -0
  190. /package/dist/{chunk-2UGJIKVA.js.map → chunk-MZQTHSLN.js.map} +0 -0
  191. /package/dist/{chunk-4SQFO4GW.js.map → chunk-NA7MIEG6.js.map} +0 -0
  192. /package/dist/{chunk-MYGNIFZC.js.map → chunk-O4VCZTK4.js.map} +0 -0
  193. /package/dist/{chunk-LS4FMSHY.js.map → chunk-OAYX4XN7.js.map} +0 -0
  194. /package/dist/{chunk-Y5UCOBQN.js.map → chunk-OGGHD64H.js.map} +0 -0
  195. /package/dist/{chunk-CBTY36TU.js.map → chunk-OH3YGJT5.js.map} +0 -0
  196. /package/dist/{chunk-VNNG46TS.js.map → chunk-ORSLFKKZ.js.map} +0 -0
  197. /package/dist/{chunk-VON4BFK4.js.map → chunk-PN5CQKA2.js.map} +0 -0
  198. /package/dist/{chunk-CF3FAAPC.js.map → chunk-RP5FK3SO.js.map} +0 -0
  199. /package/dist/{chunk-X5MPHXG5.js.map → chunk-RPWEXIAW.js.map} +0 -0
  200. /package/dist/{chunk-YQ6TNHAC.js.map → chunk-TTLI6PF7.js.map} +0 -0
  201. /package/dist/{chunk-OIKQO73C.js.map → chunk-TX5V42AY.js.map} +0 -0
  202. /package/dist/{chunk-AJGYHDA5.js.map → chunk-U2YJCNRU.js.map} +0 -0
  203. /package/dist/{chunk-W7Y6RA6W.js.map → chunk-WR5JFDQ5.js.map} +0 -0
  204. /package/dist/{chunk-AEGXHZQT.js.map → chunk-YW5T6KZ5.js.map} +0 -0
  205. /package/dist/{chunk-4LRFGB3K.js.map → chunk-ZRPGVHTR.js.map} +0 -0
@@ -0,0 +1,44 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
2
+
3
+ import type { FleetMapBaseProps, FleetMapCopyBase } from './FleetMap.base';
4
+
5
+ /**
6
+ * The REACT NATIVE half of `FleetMap`'s contract.
7
+ *
8
+ * Everything platform-neutral is re-exported from `FleetMap.base.ts` unchanged.
9
+ * What differs is at both ends of the file: there is no `map` copy and no
10
+ * `mapLabel`, because there is no map — `MapPreview` has no native build, and
11
+ * the roster is this component's own stated "accessible representation, not a
12
+ * sidebar", which is exactly the half that ports. And `style` stands where the
13
+ * web takes `className`, which is the ordinary `sx`/`style` split.
14
+ */
15
+ export type {
16
+ FleetFreshness,
17
+ FleetMapBaseProps,
18
+ FleetMapCopyBase,
19
+ FleetUnit,
20
+ } from './FleetMap.base';
21
+
22
+ /**
23
+ * Every word the native board prints.
24
+ *
25
+ * An alias rather than an empty `extends`, so that the day `MapPreview` is
26
+ * ported this becomes an interface with the map's words in it and no consumer
27
+ * import has to move.
28
+ */
29
+ export type FleetMapCopy = FleetMapCopyBase;
30
+
31
+ /** Props for the native {@link FleetMap} board. */
32
+ export interface FleetMapProps extends FleetMapBaseProps {
33
+ copy: FleetMapCopy;
34
+ /**
35
+ * A ceiling for the roster, so a fleet of thirty scrolls inside the panel
36
+ * rather than pushing everything below it off the screen.
37
+ *
38
+ * A NUMBER of pixels, where the web takes any CSS length: React Native has no
39
+ * `calc`, no viewport units and no `%` that resolves against a parent which
40
+ * has not sized itself.
41
+ */
42
+ maxHeight?: number;
43
+ style?: StyleProp<ViewStyle>;
44
+ }
@@ -1,70 +1,30 @@
1
1
  import type { MapPreviewCopy } from '../../../copy';
2
2
 
3
+ import type { FleetMapBaseProps, FleetMapCopyBase } from './FleetMap.base';
4
+
3
5
  /**
4
- * One tracked unit's newest position, as the caller resolved it.
6
+ * The WEB half of `FleetMap`'s contract.
5
7
  *
6
- * Deliberately says nothing about WHAT is being tracked. A courier, a service
7
- * van, a field engineer — the component draws a labelled dot with a freshness,
8
- * and every word that names the domain arrives through {@link FleetMapCopy}.
8
+ * Everything platform-neutral lives in `FleetMap.base.ts` and is re-exported
9
+ * here unchanged, so a consumer's `import type { FleetUnit } from
10
+ * '@12-apps/ui/data-display/FleetMap'` keeps resolving exactly as it did. What
11
+ * this file adds is what only the web has: the map.
9
12
  */
10
- export interface FleetUnit {
11
- id: string;
12
- /** The name a human reads, on the pin and in the roster. */
13
- label: string;
14
- latitude: number;
15
- longitude: number;
16
- /**
17
- * The fix's accuracy radius in metres, when the platform reported one.
18
- *
19
- * Kept because it is what separates a rooftop GPS fix from a cell-tower guess
20
- * three blocks wide, and a map that draws both identically tells its reader
21
- * something false.
22
- */
23
- accuracyM?: number | null;
24
- /**
25
- * Seconds since the fix was ACCEPTED, on the server's clock.
26
- *
27
- * The caller computes it, and the distinction matters: a device whose own
28
- * clock is an hour slow would otherwise report itself as an hour stale while
29
- * reporting perfectly.
30
- */
31
- staleSeconds: number;
32
- /** A short pre-formatted badge beside the name, e.g. `"2 entregas"`. */
33
- badge?: string;
34
- }
35
-
36
- /** How current a unit's newest fix is, derived from the thresholds below. */
37
- export type FleetFreshness = 'live' | 'lagging' | 'stale';
13
+ export type {
14
+ FleetFreshness,
15
+ FleetMapBaseProps,
16
+ FleetMapCopyBase,
17
+ FleetUnit,
18
+ } from './FleetMap.base';
38
19
 
39
20
  /**
40
- * Every word the board prints, and the two it FORMATS.
21
+ * Every word the board prints, plus the map control bar's own six.
41
22
  *
42
- * `lastSeen` and `accuracy` are functions rather than strings because a
43
- * duration and a distance are locale rules, and this package holds the same
44
- * line `StatCard` does: presentation only, the caller owns the formatting. A
45
- * `lastSeenMinutes: string` template would have made "há 2 min" the only
46
- * shape any consumer could ever render.
23
+ * `map` is web-only because the map half is: `MapPreview` has no React Native
24
+ * build yet, so the native copy interface stops at the roster's words rather
25
+ * than asking a caller to name controls nothing renders. See `NATIVE-NOTES.md`.
47
26
  */
48
- export interface FleetMapCopy {
49
- /** The panel's heading. */
50
- title: string;
51
- /** What the roster says when nobody is reporting. */
52
- emptyTitle: string;
53
- emptyDescription?: string;
54
- /** The roster list's accessible name — it is the map's readable half. */
55
- rosterLabel: string;
56
- /**
57
- * What a screen reader hears while the roster reloads, e.g. "Atualizando a
58
- * frota". OPTIONAL, and silence is the honest default: `aria-busy` marks the
59
- * region stale without uttering anything, the skeletons are `aria-hidden`,
60
- * and this component cannot invent the sentence in the reader's language.
61
- * Set it and the panel announces politely; leave it and it stays quiet.
62
- *
63
- * Announced on a TRANSITION into loading. A board mounted already loading is
64
- * silent — as any live region is at mount, since there is no change for a
65
- * reader to observe.
66
- */
67
- loading?: string;
27
+ export interface FleetMapCopy extends FleetMapCopyBase {
68
28
  /**
69
29
  * The map region's accessible name.
70
30
  *
@@ -80,69 +40,14 @@ export interface FleetMapCopy {
80
40
  * re-centre. See `FleetCanvas` for why that is stated rather than fixed here.
81
41
  */
82
42
  mapLabel: string;
83
- /** The three freshness words, as a reader sees them on a row. */
84
- freshness: Record<FleetFreshness, string>;
85
- /** `(seconds) => "há 2 min"`. */
86
- lastSeen: (staleSeconds: number) => string;
87
- /** `(metres) => "±12 m"`. A unit with no accuracy renders none. */
88
- accuracy: (metres: number) => string;
89
43
  /** The map control bar's own six names. */
90
44
  map: MapPreviewCopy;
91
45
  }
92
46
 
93
47
  /** Props for the {@link FleetMap} live board. */
94
- export interface FleetMapProps {
95
- /** Everyone currently reporting. An empty array renders the empty state. */
96
- units: readonly FleetUnit[];
48
+ export interface FleetMapProps extends FleetMapBaseProps {
97
49
  copy: FleetMapCopy;
98
- /**
99
- * Which unit is highlighted: the map centres on it and the roster marks it
100
- * selected. `null` selects nobody and centres on the whole fleet.
101
- *
102
- * PASSING IT AT ALL IS THE DECISION. Present on the first render — `null`
103
- * included — and the board is controlled for life: it renders what you pass
104
- * and never moves the selection itself. Absent on the first render and it
105
- * keeps its own, reporting each change through {@link onSelect}. The mode is
106
- * latched on that first render, the way React latches an `<input>`, so the
107
- * component cannot change its own semantics halfway through a session.
108
- *
109
- * A controlled board reads `undefined` as "nobody is selected", so
110
- * `useState<string>()` in the caller behaves: clearing it clears the roster.
111
- */
112
- selectedId?: string | null;
113
- /**
114
- * A click on a row or a pin, and each arrow-key move. Fires in both modes, so
115
- * an uncontrolled board can still be observed.
116
- *
117
- * Passing {@link selectedId} WITHOUT this is a selection nothing can move:
118
- * the arrow keys then decline to act and leave the page to scroll, rather
119
- * than swallowing a keypress that would do nothing.
120
- */
121
- onSelect?: (id: string) => void;
122
- /**
123
- * Seconds after which a unit stops reading as `live`, and then as `lagging`.
124
- *
125
- * Both carry a GENERIC default (90s and 300s, sized for a phone reporting
126
- * every twenty seconds) and no domain one, because the answer is entirely a
127
- * property of the fleet's own ping cadence: a phone reporting every twenty
128
- * seconds is late at ninety, and a tracker reporting every five minutes is
129
- * not. A component that picked one would be picking it for every consumer.
130
- */
131
- laggingAfterSeconds?: number;
132
- staleAfterSeconds?: number;
133
50
  /** Map height, any CSS length. */
134
51
  height?: string;
135
- /**
136
- * While true the map and roster are marked `aria-busy`, and the roster renders
137
- * skeletons IF no units have landed yet — a poll over a populated roster keeps
138
- * the list, because unmounting it throws away the focus inside it.
139
- *
140
- * It only ANNOUNCES if {@link FleetMapCopy.loading} is set — `aria-busy` is a
141
- * state, not an utterance. The panel root is deliberately not the busy
142
- * element, so that announcement sits outside the busy subtree.
143
- */
144
- loading?: boolean;
145
52
  className?: string;
146
- /** Test id for the panel root; every child id is derived from it. */
147
- dataTestId?: string;
148
53
  }
@@ -0,0 +1,382 @@
1
+ import * as React from 'react';
2
+ import {
3
+ Pressable,
4
+ ScrollView,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ type LayoutChangeEvent,
9
+ type NativeScrollEvent,
10
+ type NativeSyntheticEvent,
11
+ } from 'react-native';
12
+
13
+ import { Skeleton } from '../../layout/Skeleton/Skeleton.native';
14
+
15
+ import { freshnessOf } from './FleetMap.helpers';
16
+ import type { FleetKeyEvent } from './FleetMap.hooks';
17
+ import { FLEET_DOT, FLEET_ROSTER, FLEET_ROW, FLEET_SKELETON } from './FleetMap.metrics';
18
+ import type { FleetFreshness, FleetMapCopy, FleetUnit } from './FleetMap.types.native';
19
+ import { webAria, webRole } from '../../../platform/aria';
20
+ import { webKeyDown } from '../../../platform/web-keys';
21
+ import { useUiTheme } from '../../../provider/use-ui-theme.native';
22
+ import { muiTypeStyle } from '../../../tokens/mui-type';
23
+ import type { UiTheme } from '../../../tokens/theme';
24
+
25
+ /**
26
+ * The roster — which on React Native is the WHOLE component, not half of it.
27
+ *
28
+ * On the web this list sits beside a map and carries the information a picture
29
+ * cannot give a screen reader. Here there is no map to sit beside: `MapPreview`
30
+ * has no React Native build, so the native `FleetMap` renders this and says so
31
+ * in `NATIVE-NOTES.md`. That is a smaller change than it sounds, because the
32
+ * web half already treats this list as the component's real content — "the
33
+ * accessible representation, not a sidebar" — and every fact a pin carries is
34
+ * on a row here.
35
+ */
36
+
37
+ /**
38
+ * `data-freshness`, as something JSX can spread onto a react-native element.
39
+ *
40
+ * Through `dataSet` and NOT as a bare `data-freshness` prop: react-native-web
41
+ * drops unknown props rather than forwarding them, and special-cases only
42
+ * `data-testid`. Measured — the attribute was simply absent from the row, so
43
+ * the shared freshness stories read `null`. `dataSet` is rn-web's documented
44
+ * channel for custom data attributes and renders `data-freshness`; React Native
45
+ * ignores it on a device, which is the same trade `webRole` and `webAria` make.
46
+ *
47
+ * It is not decoration: the SHARED stories read this attribute to assert which
48
+ * threshold a row landed on, so without it they cannot run here at all.
49
+ */
50
+ function webDataFreshness(freshness: FleetFreshness): object {
51
+ return { dataSet: { freshness } };
52
+ }
53
+
54
+ /** Each freshness draws from the semantic palette, never a hardcoded hex. */
55
+ function freshnessColor(freshness: FleetFreshness, theme: UiTheme): string {
56
+ if (freshness === 'live') return theme.palette.success.main;
57
+ if (freshness === 'lagging') return theme.palette.warning.main;
58
+ return theme.palette.text.disabled;
59
+ }
60
+
61
+ /** Where each row sits inside the scrolling content, and what is on screen. */
62
+ interface RosterGeometry {
63
+ rows: Map<string, { y: number; height: number }>;
64
+ offset: number;
65
+ viewport: number;
66
+ }
67
+
68
+ /**
69
+ * Keep the selected row visible INSIDE the roster — the same arithmetic the web
70
+ * does with `scrollTop`, expressed in the only terms React Native has.
71
+ *
72
+ * The web reads `box.offsetTop - list.offsetTop` against `list.scrollTop` and
73
+ * `list.clientHeight`. None of those exist here, so the three numbers are
74
+ * gathered as they arrive: a row's `onLayout` gives its `y` inside the content
75
+ * container (already relative to the list, which is what the web computes by
76
+ * subtraction), the `ScrollView`'s own `onLayout` gives the viewport height,
77
+ * and `onScroll` gives the offset. The comparison below is then line-for-line
78
+ * the web's.
79
+ *
80
+ * `scrollTo` rather than react-native's `scrollToEnd`/`ScrollView.scrollIntoView`
81
+ * for the same reason the web refuses `scrollIntoView({ block: 'nearest' })`:
82
+ * this must move THIS list and nothing else. A native `ScrollView` cannot walk
83
+ * its ancestors, so the hazard is smaller here — but the arithmetic is shared,
84
+ * and two renderers agreeing by construction beats two agreeing by coincidence.
85
+ *
86
+ * Watches `position` as well as the id, because the roster re-sorts on every
87
+ * poll: a rider who stays selected while their staleness moves them down the
88
+ * list would otherwise slide out of view with the effect never re-running.
89
+ */
90
+ function useKeptInView(
91
+ scroll: React.RefObject<ScrollView | null>,
92
+ geometry: React.RefObject<RosterGeometry>,
93
+ activeId: string | null,
94
+ position: number,
95
+ ): void {
96
+ React.useEffect(() => {
97
+ if (activeId === null) return;
98
+ const { rows, offset, viewport } = geometry.current;
99
+ const row = rows.get(activeId);
100
+ // Nothing has laid out yet on the first commit; the next poll re-runs this.
101
+ if (!row || viewport === 0) return;
102
+ if (row.y < offset) {
103
+ scroll.current?.scrollTo({ y: row.y, animated: false });
104
+ } else if (row.y + row.height > offset + viewport) {
105
+ scroll.current?.scrollTo({ y: row.y + row.height - viewport, animated: false });
106
+ }
107
+ }, [scroll, geometry, activeId, position]);
108
+ }
109
+
110
+ /**
111
+ * A row's two lines of text — the name, and the one-line summary under it.
112
+ *
113
+ * Split out of {@link FleetRow} to keep that function under the size gate, and
114
+ * the seam is the natural one: everything here is READ, nothing is interactive.
115
+ */
116
+ function RowText({
117
+ unit,
118
+ copy,
119
+ freshness,
120
+ testId,
121
+ }: Pick<RowProps, 'unit' | 'copy' | 'freshness' | 'testId'>): React.JSX.Element {
122
+ const theme = useUiTheme();
123
+
124
+ // Dropped rather than rendered empty: a phone that reports no accuracy radius
125
+ // would otherwise print a trailing separator with nothing after it.
126
+ const meta = [
127
+ copy.freshness[freshness],
128
+ copy.lastSeen(unit.staleSeconds),
129
+ unit.accuracyM == null ? null : copy.accuracy(unit.accuracyM),
130
+ ]
131
+ .filter((part): part is string => Boolean(part))
132
+ .join(' · ');
133
+
134
+ return (
135
+ <View style={styles.rowText}>
136
+ <Text
137
+ testID={`${testId}-label`}
138
+ style={[muiTypeStyle(theme, 'body2'), styles.label, { color: theme.palette.text.primary }]}
139
+ >
140
+ {unit.label}
141
+ </Text>
142
+ <Text
143
+ testID={`${testId}-meta`}
144
+ style={[muiTypeStyle(theme, 'caption'), { color: theme.palette.text.secondary }]}
145
+ >
146
+ {meta}
147
+ </Text>
148
+ </View>
149
+ );
150
+ }
151
+
152
+ interface RowProps {
153
+ unit: FleetUnit;
154
+ copy: FleetMapCopy;
155
+ freshness: FleetFreshness;
156
+ selected: boolean;
157
+ onSelect: (id: string) => void;
158
+ onMeasured: (id: string, y: number, height: number) => void;
159
+ /** The row's own DOM id, which `aria-activedescendant` points at. */
160
+ optionId: string;
161
+ testId: string;
162
+ }
163
+
164
+ /**
165
+ * One roster row.
166
+ *
167
+ * A `Pressable` rather than a `View` with an `onClick`: a press is the input on
168
+ * a device, and `Pressable` is what gives the row its pressed state and its
169
+ * accessibility role in one element. The PRESSED wash stands in for the web's
170
+ * hover — and, as on the web, it must never weaken the selection, so a selected
171
+ * row keeps `action.selected` whether or not a finger is on it.
172
+ */
173
+ function FleetRow({
174
+ unit,
175
+ copy,
176
+ freshness,
177
+ selected,
178
+ onSelect,
179
+ onMeasured,
180
+ optionId,
181
+ testId,
182
+ }: RowProps): React.JSX.Element {
183
+ const theme = useUiTheme();
184
+
185
+ const measure = React.useCallback(
186
+ (event: LayoutChangeEvent) => {
187
+ const { y, height } = event.nativeEvent.layout;
188
+ onMeasured(unit.id, y, height);
189
+ },
190
+ [onMeasured, unit.id],
191
+ );
192
+
193
+ return (
194
+ <Pressable
195
+ {...webRole('option')}
196
+ {...webDataFreshness(freshness)}
197
+ // Without this the roster's `aria-activedescendant` names an element that
198
+ // does not exist, which announces nothing and fails an axe audit — worse
199
+ // than omitting the attribute. Measured: the row carried no id at all.
200
+ id={optionId}
201
+ // BOTH, and neither is redundant. `accessibilityState` is what a DEVICE
202
+ // reads; react-native-web does not turn it into `aria-selected`, so
203
+ // without the attribute beside it the web build conveys the selection by
204
+ // background colour alone — which is the one thing this component refuses
205
+ // to do for freshness, and no more acceptable here.
206
+ accessibilityState={{ selected }}
207
+ aria-selected={selected}
208
+ testID={testId}
209
+ onPress={() => onSelect(unit.id)}
210
+ onLayout={measure}
211
+ style={({ pressed }) => [
212
+ styles.row,
213
+ {
214
+ gap: theme.spacing(FLEET_ROW.gapUnits),
215
+ paddingVertical: theme.spacing(FLEET_ROW.paddingYUnits),
216
+ paddingHorizontal: theme.spacing(FLEET_ROW.paddingXUnits),
217
+ borderRadius: FLEET_ROW.radiusMultiple * theme.radius.md,
218
+ backgroundColor: selected
219
+ ? theme.palette.action.selected
220
+ : pressed
221
+ ? theme.palette.action.hover
222
+ : 'transparent',
223
+ },
224
+ ]}
225
+ >
226
+ {/* The dot repeats what the freshness word beside it already says, so it
227
+ is hidden rather than read twice — and colour is never the only
228
+ carrier of the state. */}
229
+ <View
230
+ accessibilityElementsHidden
231
+ importantForAccessibility="no-hide-descendants"
232
+ testID={`${testId}-dot`}
233
+ style={[styles.dot, { backgroundColor: freshnessColor(freshness, theme) }]}
234
+ />
235
+ <RowText unit={unit} copy={copy} freshness={freshness} testId={testId} />
236
+ {unit.badge !== undefined && unit.badge !== '' && (
237
+ <Text
238
+ testID={`${testId}-badge`}
239
+ style={[
240
+ muiTypeStyle(theme, 'caption'),
241
+ styles.badge,
242
+ { color: theme.palette.text.secondary },
243
+ ]}
244
+ >
245
+ {unit.badge}
246
+ </Text>
247
+ )}
248
+ </Pressable>
249
+ );
250
+ }
251
+
252
+ /**
253
+ * What stands in for the roster before any unit has landed.
254
+ *
255
+ * Hidden from the reader, because three grey bars are not information — the
256
+ * panel's busy state and its optional announcement carry that. The radius is
257
+ * the same MULTIPLE a real row uses rather than a raw pixel count.
258
+ */
259
+ function FirstLoad({ testId }: { testId: string }): React.JSX.Element {
260
+ const theme = useUiTheme();
261
+ return (
262
+ <View
263
+ testID={`${testId}-skeleton`}
264
+ accessibilityElementsHidden
265
+ importantForAccessibility="no-hide-descendants"
266
+ style={{ gap: theme.spacing(FLEET_SKELETON.gapUnits) }}
267
+ >
268
+ {Array.from({ length: FLEET_SKELETON.rows }, (_, row) => (
269
+ <Skeleton
270
+ key={row}
271
+ variant="rectangular"
272
+ height={FLEET_SKELETON.height}
273
+ borderRadius={FLEET_SKELETON.radiusMultiple}
274
+ />
275
+ ))}
276
+ </View>
277
+ );
278
+ }
279
+
280
+ export interface FleetRosterProps {
281
+ units: readonly FleetUnit[];
282
+ copy: FleetMapCopy;
283
+ selectedId: string | null | undefined;
284
+ onSelect: (id: string) => void;
285
+ laggingAfterSeconds: number;
286
+ staleAfterSeconds: number;
287
+ loading: boolean;
288
+ maxHeight: number | undefined;
289
+ testId: string;
290
+ onKeyDown: (event: FleetKeyEvent) => void;
291
+ }
292
+
293
+ export function FleetRoster({
294
+ units,
295
+ copy,
296
+ selectedId,
297
+ onSelect,
298
+ laggingAfterSeconds,
299
+ staleAfterSeconds,
300
+ loading,
301
+ maxHeight,
302
+ testId,
303
+ onKeyDown,
304
+ }: FleetRosterProps): React.JSX.Element {
305
+ const theme = useUiTheme();
306
+ const scroll = React.useRef<ScrollView>(null);
307
+ const geometry = React.useRef<RosterGeometry>({ rows: new Map(), offset: 0, viewport: 0 });
308
+
309
+ // Generated, not derived from `dataTestId`: two boards on one screen filtered
310
+ // from the same fleet would otherwise emit duplicate ids and an ambiguous
311
+ // `aria-activedescendant` under react-native-web.
312
+ const rowIdPrefix = React.useId();
313
+
314
+ // The selection is CONTROLLED, so it can name a unit that has since dropped
315
+ // out of the freshness window — a courier ending their shift mid-poll. The
316
+ // attribute must then be absent rather than pointing at a row that no longer
317
+ // renders. `mapCentre` and `nextSelection` both handle the same case.
318
+ const active = units.some((unit) => unit.id === selectedId) ? (selectedId ?? null) : null;
319
+ const position = active === null ? -1 : units.findIndex((unit) => unit.id === active);
320
+
321
+ const onMeasured = React.useCallback((id: string, y: number, height: number) => {
322
+ geometry.current.rows.set(id, { y, height });
323
+ }, []);
324
+
325
+ useKeptInView(scroll, geometry, active, position);
326
+
327
+ if (loading && units.length === 0) return <FirstLoad testId={testId} />;
328
+
329
+ return (
330
+ <ScrollView
331
+ ref={scroll}
332
+ {...webRole('listbox')}
333
+ // A single tab stop with the arrow keys inside it, which is the listbox
334
+ // pattern and what `aria-activedescendant` below is for: a dispatcher
335
+ // tabbing past a fleet of thirty should not press it thirty times. React
336
+ // Native has no tab order, so this reaches the DOM through
337
+ // react-native-web and does nothing on a device — where each row is its
338
+ // own touch target anyway.
339
+ tabIndex={0}
340
+ {...webAria({ 'aria-activedescendant': active ? `${rowIdPrefix}-${active}` : undefined })}
341
+ {...webKeyDown(onKeyDown)}
342
+ accessibilityLabel={copy.rosterLabel}
343
+ testID={`${testId}-roster`}
344
+ style={maxHeight === undefined ? undefined : { maxHeight }}
345
+ contentContainerStyle={{ gap: theme.spacing(FLEET_ROSTER.gapUnits) }}
346
+ onLayout={(event: LayoutChangeEvent) => {
347
+ geometry.current.viewport = event.nativeEvent.layout.height;
348
+ }}
349
+ onScroll={(event: NativeSyntheticEvent<NativeScrollEvent>) => {
350
+ geometry.current.offset = event.nativeEvent.contentOffset.y;
351
+ }}
352
+ scrollEventThrottle={16}
353
+ >
354
+ {units.map((unit) => (
355
+ <FleetRow
356
+ key={unit.id}
357
+ unit={unit}
358
+ copy={copy}
359
+ freshness={freshnessOf(unit.staleSeconds, laggingAfterSeconds, staleAfterSeconds)}
360
+ selected={unit.id === active}
361
+ onSelect={onSelect}
362
+ onMeasured={onMeasured}
363
+ optionId={`${rowIdPrefix}-${unit.id}`}
364
+ testId={`${testId}-${unit.id}`}
365
+ />
366
+ ))}
367
+ </ScrollView>
368
+ );
369
+ }
370
+
371
+ const styles = StyleSheet.create({
372
+ badge: { flexShrink: 0 },
373
+ dot: {
374
+ borderRadius: FLEET_DOT.size / 2,
375
+ flexShrink: 0,
376
+ height: FLEET_DOT.size,
377
+ width: FLEET_DOT.size,
378
+ },
379
+ label: { fontWeight: '500' },
380
+ row: { alignItems: 'center', flexDirection: 'row' },
381
+ rowText: { flexGrow: 1, flexShrink: 1, minWidth: 0 },
382
+ });
@@ -7,6 +7,7 @@ import React from 'react';
7
7
  import { Skeleton } from '../../layout/Skeleton/Skeleton';
8
8
 
9
9
  import { freshnessOf } from './FleetMap.helpers';
10
+ import { FLEET_DOT, FLEET_ROSTER, FLEET_ROW, FLEET_SKELETON } from './FleetMap.metrics';
10
11
  import type { FleetFreshness, FleetMapCopy, FleetUnit } from './FleetMap.types';
11
12
 
12
13
  /**
@@ -133,15 +134,15 @@ function rowSx(theme: Theme, selected: boolean): SxProps<Theme> {
133
134
  return {
134
135
  display: 'flex',
135
136
  alignItems: 'center',
136
- gap: theme.spacing(1.5),
137
- padding: theme.spacing(1, 1.5),
137
+ gap: theme.spacing(FLEET_ROW.gapUnits),
138
+ padding: theme.spacing(FLEET_ROW.paddingYUnits, FLEET_ROW.paddingXUnits),
138
139
  // `1`, not `theme.shape.borderRadius / 4`. MUI's `sx` reads a NUMBER here as
139
140
  // a multiple of `theme.shape.borderRadius`, so dividing the theme value out
140
141
  // and letting `sx` multiply it back in is quadratic: measured 4px on the
141
142
  // default theme — which is why this looked right — and 36px on a host that
142
143
  // sets its own radius to 12. In a package whose whole premise is being
143
144
  // re-themed, only the multiple is portable.
144
- borderRadius: 1,
145
+ borderRadius: FLEET_ROW.radiusMultiple,
145
146
  cursor: 'pointer',
146
147
  backgroundColor: resting,
147
148
  '&:hover': {
@@ -195,8 +196,8 @@ function FleetRow({
195
196
  aria-hidden="true"
196
197
  data-testid={`${testId}-dot`}
197
198
  sx={{
198
- width: 10,
199
- height: 10,
199
+ width: FLEET_DOT.size,
200
+ height: FLEET_DOT.size,
200
201
  borderRadius: '50%',
201
202
  flexShrink: 0,
202
203
  backgroundColor: color,
@@ -229,9 +230,14 @@ function FleetRow({
229
230
  */
230
231
  function FirstLoad({ testId }: { testId: string }): React.JSX.Element {
231
232
  return (
232
- <Stack spacing={1} data-testid={`${testId}-skeleton`} aria-hidden="true">
233
- {[0, 1, 2].map((row) => (
234
- <Skeleton key={row} variant="rectangular" height={44} borderRadius={1} />
233
+ <Stack spacing={FLEET_SKELETON.gapUnits} data-testid={`${testId}-skeleton`} aria-hidden="true">
234
+ {Array.from({ length: FLEET_SKELETON.rows }, (_, row) => (
235
+ <Skeleton
236
+ key={row}
237
+ variant="rectangular"
238
+ height={FLEET_SKELETON.height}
239
+ borderRadius={FLEET_SKELETON.radiusMultiple}
240
+ />
235
241
  ))}
236
242
  </Stack>
237
243
  );
@@ -294,12 +300,12 @@ export function FleetRoster({
294
300
  sx={{
295
301
  display: 'flex',
296
302
  flexDirection: 'column',
297
- gap: theme.spacing(0.5),
303
+ gap: theme.spacing(FLEET_ROSTER.gapUnits),
298
304
  overflowY: 'auto',
299
305
  minWidth: 0,
300
306
  '&:focus-visible': {
301
- outline: `2px solid ${theme.palette.primary.main}`,
302
- outlineOffset: 2,
307
+ outline: `${FLEET_ROSTER.focusRingWidth}px solid ${theme.palette.primary.main}`,
308
+ outlineOffset: FLEET_ROSTER.focusRingOffset,
303
309
  },
304
310
  }}
305
311
  >
@@ -0,0 +1,14 @@
1
+ # FleetMap on React Native
2
+
3
+ Known rendering gaps, one bullet each; `pnpm native:ledger` reads them.
4
+
5
+ - There is no MAP. `MapPreview` — which projects, tiles and decorates its own surface across ~2,200 lines — has no React Native build, so the native board renders the roster alone and its copy interface has neither `map` nor `mapLabel`: a caller should not be asked to name controls nothing draws. What that costs is smaller than the missing half suggests, because the web already treats this list as the component's real content rather than a sidebar — every fact a pin carries (who, how recently, how precisely, what they are carrying) is on a row, in an order the map cannot express. It is where the map goes when `MapPreview` is ported.
6
+ - Every row is ALSO a tab stop. The list itself carries `tabIndex={0}` and `aria-activedescendant` exactly as the web's listbox does, so the arrow keys work and the selection is announced — but react-native-web additionally gives each `Pressable` its own `tabindex`, which the web half's `Box` rows never had. Measured on the rendered DOM: a fleet of thirty is thirty-one tab stops here against the web's one. Removing them would mean making the rows non-focusable, which would take the press target away on the renderer that has no touch.
7
+ - The arrow keys are web-only. `onKeyDown` reaches the list through react-native-web's DOM events, so a browser still walks the roster with Up and Down, wrapping at both ends, declining a modified arrow and leaving the page to scroll when nothing can move. React Native delivers no key events to a `View`, so on a DEVICE none of it runs and the selection is made by touch.
8
+ - `aria-selected` is passed as the attribute AND as `accessibilityState.selected`, because react-native-web derives neither from the other: without the attribute the web build announced no selection at all and conveyed it by background colour alone, which is the one thing this component refuses to do for freshness.
9
+ - Hover has no touch equivalent, so the pressed wash stands in for it — and, as on the web, it must never WEAKEN the selection: a selected row keeps `action.selected` whether or not a finger is on it, rather than taking the lighter `action.hover`.
10
+ - Keeping the selected row in view is the web's own arithmetic against different numbers. The web reads `offsetTop`/`scrollTop`/`clientHeight`; native gathers the same three from a row's `onLayout`, the `ScrollView`'s `onLayout` and its `onScroll`. So it needs one layout pass before it can act: a board mounted with a selection already set scrolls on the next commit rather than the first, where the web scrolls immediately.
11
+ - The reload announcement is `accessibilityLiveRegion="polite"` plus `aria-live="polite"`. The first is Android's and the second is react-native-web's; iOS has no live region at all, and announcing there needs `AccessibilityInfo.announceForAccessibility`, which this component does not call — a host that must be heard on iOS makes that call itself.
12
+ - A long unbroken label overflows rather than breaking mid-token: the web sets `overflowWrap: 'anywhere'` and React Native has no equivalent style, so a 40-character name with no spaces is clipped by the row instead of wrapped inside it.
13
+ - `maxHeight` is a NUMBER of pixels where the web's `height` is any CSS length: React Native has no `calc`, no viewport units, and no percentage that resolves against a parent which has not sized itself.
14
+ - The title, the row label and the meta line are set in MUI's default `body1`/`body2`/`caption` numbers; a host that re-themes MUI's typography variants moves the web only.