@12-apps/ui 6.22.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 (347) hide show
  1. package/NATIVE.md +2 -1
  2. package/components-catalog.md +7 -0
  3. package/dist/_internal/components/data-display/AsyncStateContainer/AsyncStateContainer.js +3 -3
  4. package/dist/_internal/components/data-display/Avatar/Avatar.js +4 -4
  5. package/dist/_internal/components/data-display/Avatar/Avatar.view.js +2 -2
  6. package/dist/_internal/components/data-display/Badge/Badge.js +2 -2
  7. package/dist/_internal/components/data-display/CardKit/use-remove-confirm.js +1 -1
  8. package/dist/_internal/components/data-display/CardKit/use-row-confirm.js +1 -1
  9. package/dist/_internal/components/data-display/Carousel/Carousel.js +2 -2
  10. package/dist/_internal/components/data-display/Chart/Chart.js +4 -4
  11. package/dist/_internal/components/data-display/Chart/SpecChart.js +7 -7
  12. package/dist/_internal/components/data-display/Chart/chart-context.js +2 -2
  13. package/dist/_internal/components/data-display/Chart/chart-renderers.js +3 -3
  14. package/dist/_internal/components/data-display/Chart/chart-spec.js +2 -2
  15. package/dist/_internal/components/data-display/DataGrid/DataGrid.js +2 -2
  16. package/dist/_internal/components/data-display/DataViews/DataViewsGrid.js +41 -41
  17. package/dist/_internal/components/data-display/DataViews/DataViewsTableBase.js +47 -47
  18. package/dist/_internal/components/data-display/DataViews/ManageViewsDialog.js +1 -1
  19. package/dist/_internal/components/data-display/DataViews/base-list-card-geometry.js +4 -4
  20. package/dist/_internal/components/data-display/DataViews/base-list-card-slots.js +3 -3
  21. package/dist/_internal/components/data-display/DataViews/base-list-card.js +6 -6
  22. package/dist/_internal/components/data-display/DataViews/data-views-category-pill.js +5 -5
  23. package/dist/_internal/components/data-display/DataViews/data-views-display-panel.js +3 -3
  24. package/dist/_internal/components/data-display/DataViews/data-views-filter-panel.js +10 -10
  25. package/dist/_internal/components/data-display/DataViews/data-views-grid-bodies.js +6 -6
  26. package/dist/_internal/components/data-display/DataViews/data-views-grid-parts.js +36 -36
  27. package/dist/_internal/components/data-display/DataViews/data-views-inline-bar.js +11 -11
  28. package/dist/_internal/components/data-display/DataViews/data-views-more-fields.js +3 -3
  29. package/dist/_internal/components/data-display/DataViews/data-views-more-filters.js +4 -4
  30. package/dist/_internal/components/data-display/DataViews/data-views-pagination.js +3 -3
  31. package/dist/_internal/components/data-display/DataViews/data-views-range-pill.js +2 -2
  32. package/dist/_internal/components/data-display/DataViews/data-views-scope-tabs.js +2 -2
  33. package/dist/_internal/components/data-display/DataViews/data-views-shell-toolbar.js +6 -6
  34. package/dist/_internal/components/data-display/DataViews/data-views-table-parts.js +1 -1
  35. package/dist/_internal/components/data-display/DataViews/data-views-toolbar.js +5 -5
  36. package/dist/_internal/components/data-display/DataViews/list-card-rails.js +2 -2
  37. package/dist/_internal/components/data-display/DataViews/use-data-views-state.js +4 -4
  38. package/dist/_internal/components/data-display/FleetMap/FleetBody.js +32 -0
  39. package/dist/_internal/components/data-display/FleetMap/FleetCanvas.js +23 -0
  40. package/dist/_internal/components/data-display/FleetMap/FleetCanvas.js.map +1 -0
  41. package/dist/_internal/components/data-display/FleetMap/FleetMap.base.js +1 -0
  42. package/dist/_internal/components/data-display/FleetMap/FleetMap.base.js.map +1 -0
  43. package/dist/_internal/components/data-display/FleetMap/FleetMap.fixtures.js +57 -0
  44. package/dist/_internal/components/data-display/FleetMap/FleetMap.fixtures.js.map +1 -0
  45. package/dist/_internal/components/data-display/FleetMap/FleetMap.helpers.js +14 -0
  46. package/dist/_internal/components/data-display/FleetMap/FleetMap.helpers.js.map +1 -0
  47. package/dist/_internal/components/data-display/FleetMap/FleetMap.hooks.js +9 -0
  48. package/dist/_internal/components/data-display/FleetMap/FleetMap.hooks.js.map +1 -0
  49. package/dist/_internal/components/data-display/FleetMap/FleetMap.js +47 -0
  50. package/dist/_internal/components/data-display/FleetMap/FleetMap.js.map +1 -0
  51. package/dist/_internal/components/data-display/FleetMap/FleetMap.metrics.js +20 -0
  52. package/dist/_internal/components/data-display/FleetMap/FleetMap.metrics.js.map +1 -0
  53. package/dist/_internal/components/data-display/FleetMap/FleetMap.types.js +1 -0
  54. package/dist/_internal/components/data-display/FleetMap/FleetMap.types.js.map +1 -0
  55. package/dist/_internal/components/data-display/FleetMap/FleetRoster.js +15 -0
  56. package/dist/_internal/components/data-display/FleetMap/FleetRoster.js.map +1 -0
  57. package/dist/_internal/components/data-display/HoverCard/HoverCard.js +2 -2
  58. package/dist/_internal/components/data-display/Lightbox/Lightbox.js +2 -2
  59. package/dist/_internal/components/data-display/MapPreview/MapLayers.js +2 -2
  60. package/dist/_internal/components/data-display/MapPreview/MapPreview.js +6 -6
  61. package/dist/_internal/components/data-display/Sheet/Sheet.hooks.js +2 -2
  62. package/dist/_internal/components/data-display/Sheet/Sheet.js +3 -3
  63. package/dist/_internal/components/data-display/Table/Table.js +3 -3
  64. package/dist/_internal/components/data-display/index.js +90 -69
  65. package/dist/_internal/components/feedback/Modal/Modal.js +2 -2
  66. package/dist/_internal/components/feedback/StackedModal/StackedModal.js +2 -2
  67. package/dist/_internal/components/feedback/index.js +14 -14
  68. package/dist/_internal/components/form/AddressAutocomplete/AddressAutocomplete.hooks.js +3 -3
  69. package/dist/_internal/components/form/AddressAutocomplete/AddressAutocomplete.js +5 -5
  70. package/dist/_internal/components/form/AddressAutocomplete/AddressAutocompleteField.js +2 -2
  71. package/dist/_internal/components/form/CategorySelect/CategoryPanel.js +2 -2
  72. package/dist/_internal/components/form/CategorySelect/CategorySelect.js +4 -4
  73. package/dist/_internal/components/form/Command/Command.js +2 -2
  74. package/dist/_internal/components/form/DateRangePicker/DateRangePicker.js +3 -3
  75. package/dist/_internal/components/form/PhoneInput/PhoneInput.hooks.js +2 -2
  76. package/dist/_internal/components/form/PhoneInput/PhoneInput.js +3 -3
  77. package/dist/_internal/components/form/index.js +30 -30
  78. package/dist/_internal/components/form/total-form/Fields/CategoryField.js +5 -5
  79. package/dist/_internal/components/form/total-form/Fields/index.js +9 -9
  80. package/dist/_internal/components/index.js +160 -140
  81. package/dist/_internal/components/layout/SettingsLayout/SettingsLayout.js +2 -2
  82. package/dist/_internal/components/layout/index.js +20 -20
  83. package/dist/_internal/components/navigation/AppHeader/AppHeader.details.js +4 -4
  84. package/dist/_internal/components/navigation/Pagination/Pagination.js +2 -2
  85. package/dist/_internal/components/navigation/index.js +12 -12
  86. package/dist/_internal/components/utility/index.js +4 -4
  87. package/dist/_internal/platform/aria.js.map +1 -1
  88. package/dist/_internal/pt-BR.data-display.js +2 -2
  89. package/dist/_internal/user-avatar/user-avatar.js +5 -5
  90. package/dist/charts.js +10 -10
  91. package/dist/{chunk-WGRS4PGY.js → chunk-2C47DEVU.js} +4 -4
  92. package/dist/chunk-2RQINU5T.js +41 -0
  93. package/dist/chunk-2RQINU5T.js.map +1 -0
  94. package/dist/{chunk-WMALDOF5.js → chunk-4G3FGCWX.js} +4 -4
  95. package/dist/{chunk-ZOUWHGV3.js → chunk-4MGFUPQT.js} +4 -4
  96. package/dist/{chunk-QFJRB27Q.js → chunk-6LGWJFQK.js} +2 -2
  97. package/dist/{chunk-XSF6AXEU.js → chunk-6RZDDUSM.js} +2 -2
  98. package/dist/{chunk-VIK3HAUQ.js → chunk-73NJCGQG.js} +2 -2
  99. package/dist/{chunk-UU3QAKG6.js → chunk-B7DKSBAE.js} +14 -14
  100. package/dist/{chunk-3FLVIVOK.js → chunk-BHIGHSXA.js} +2 -2
  101. package/dist/{chunk-HKV4MQ2W.js → chunk-BLBOYCOV.js} +5 -5
  102. package/dist/chunk-BTQZ3EX3.js +45 -0
  103. package/dist/chunk-BTQZ3EX3.js.map +1 -0
  104. package/dist/{chunk-PFV6RL4Q.js → chunk-C6KTQF4F.js} +3 -3
  105. package/dist/chunk-CDCTLPMD.js +1 -0
  106. package/dist/chunk-CDCTLPMD.js.map +1 -0
  107. package/dist/{chunk-LYSSO6WC.js → chunk-CZPU5TW3.js} +4 -4
  108. package/dist/{chunk-PMS3H3OO.js → chunk-DF2FFYZ7.js} +4 -4
  109. package/dist/{chunk-QZBUGUOY.js → chunk-DFLTUI7W.js} +4 -4
  110. package/dist/{chunk-WLQTHPMX.js → chunk-EJQW2IYW.js} +2 -2
  111. package/dist/{chunk-VQAM2XZV.js → chunk-EMUBMZMA.js} +2 -2
  112. package/dist/{chunk-XFUNJYFT.js → chunk-EPNUVSNK.js} +7 -7
  113. package/dist/{chunk-MSUSZLRE.js → chunk-EWT3WMVY.js} +5 -5
  114. package/dist/{chunk-Q75SJ35X.js → chunk-GMTM6EN7.js} +3 -3
  115. package/dist/{chunk-R6SBP5M5.js → chunk-GOWBYHYW.js} +4 -4
  116. package/dist/{chunk-6NZTYZP5.js → chunk-GVA5CYKP.js} +2 -2
  117. package/dist/chunk-HMV4QMXS.js +219 -0
  118. package/dist/chunk-HMV4QMXS.js.map +1 -0
  119. package/dist/{chunk-VR5HDUBK.js → chunk-HP2ODXNA.js} +2 -2
  120. package/dist/{chunk-2MMEIN4T.js → chunk-I3E2QHTH.js} +4 -4
  121. package/dist/{chunk-UVGRRI7N.js → chunk-IPVYEPXO.js} +4 -4
  122. package/dist/{chunk-5ZTUP65G.js → chunk-JM64OEU6.js} +5 -5
  123. package/dist/{chunk-7BYFC2MR.js → chunk-JUHUKZGU.js} +7 -7
  124. package/dist/chunk-JZXWCEH5.js +116 -0
  125. package/dist/chunk-JZXWCEH5.js.map +1 -0
  126. package/dist/{chunk-DVUPIVI2.js → chunk-KT6HWAEN.js} +4 -4
  127. package/dist/{chunk-TLMLEC4F.js → chunk-KW7H5O3K.js} +6 -6
  128. package/dist/{chunk-VLHGBFR6.js → chunk-MZQTHSLN.js} +3 -3
  129. package/dist/{chunk-4SQFO4GW.js → chunk-NA7MIEG6.js} +7 -7
  130. package/dist/{chunk-QWDEOITU.js → chunk-O4VCZTK4.js} +2 -2
  131. package/dist/{chunk-LS4FMSHY.js → chunk-OAYX4XN7.js} +4 -4
  132. package/dist/{chunk-Y5UCOBQN.js → chunk-OGGHD64H.js} +4 -4
  133. package/dist/{chunk-7TZ6OC5T.js → chunk-OH3YGJT5.js} +2 -2
  134. package/dist/{chunk-REMK55JI.js → chunk-OLZTGVBT.js} +4 -4
  135. package/dist/{chunk-VNNG46TS.js → chunk-ORSLFKKZ.js} +6 -6
  136. package/dist/{chunk-ZIYMBR7K.js → chunk-OTH7H3IP.js} +2 -2
  137. package/dist/{chunk-5NKUYCZD.js → chunk-PN5CQKA2.js} +4 -4
  138. package/dist/{chunk-J4W4TQPC.js → chunk-QJRDDVZ2.js} +5 -5
  139. package/dist/{chunk-VNDOQEJ7.js → chunk-QRX5BYUQ.js} +4 -4
  140. package/dist/{chunk-XT7HMNBW.js → chunk-R6SNYC3D.js} +5 -5
  141. package/dist/{chunk-W2UTDN4T.js → chunk-RBGOGTT2.js} +4 -4
  142. package/dist/chunk-RP5FK3SO.js +79 -0
  143. package/dist/chunk-RP5FK3SO.js.map +1 -0
  144. package/dist/{chunk-X5MPHXG5.js → chunk-RPWEXIAW.js} +6 -6
  145. package/dist/chunk-RQWZYD6H.js +1 -0
  146. package/dist/chunk-RQWZYD6H.js.map +1 -0
  147. package/dist/{chunk-EXJBPKGW.js → chunk-SHDIZSN5.js} +5 -5
  148. package/dist/{chunk-23FCBIUN.js → chunk-TNVO2DTB.js} +8 -8
  149. package/dist/{chunk-AGSBIUPV.js → chunk-TPLKJHNN.js} +4 -4
  150. package/dist/{chunk-YQ6TNHAC.js → chunk-TTLI6PF7.js} +5 -5
  151. package/dist/{chunk-U2AC4AQX.js → chunk-TU5EIKJA.js} +4 -4
  152. package/dist/{chunk-KTK4A3IF.js → chunk-TX5V42AY.js} +11 -11
  153. package/dist/{chunk-BGYLL467.js → chunk-TXHJX2NG.js} +4 -4
  154. package/dist/{chunk-FL54G7PV.js → chunk-WIT3Y7JT.js} +2 -2
  155. package/dist/{chunk-W7Y6RA6W.js → chunk-WR5JFDQ5.js} +4 -4
  156. package/dist/chunk-XPMGFAJ3.js +36 -0
  157. package/dist/chunk-XPMGFAJ3.js.map +1 -0
  158. package/dist/{chunk-2UYR2OOE.js → chunk-YRUCIKPO.js} +4 -4
  159. package/dist/{chunk-AEGXHZQT.js → chunk-YW5T6KZ5.js} +7 -7
  160. package/dist/{chunk-QKIJ6YXZ.js → chunk-ZA4U5GDU.js} +4 -4
  161. package/dist/chunk-ZCPSND5Y.js +64 -0
  162. package/dist/chunk-ZCPSND5Y.js.map +1 -0
  163. package/dist/{chunk-ZKR5B6YJ.js → chunk-ZRPGVHTR.js} +2 -2
  164. package/dist/{chunk-OXQP3N6X.js → chunk-ZTZ6S5Q6.js} +4 -4
  165. package/dist/data-display/AsyncStateContainer.js +3 -3
  166. package/dist/data-display/Avatar.js +4 -4
  167. package/dist/data-display/Badge.js +2 -2
  168. package/dist/data-display/CardKit.js +11 -11
  169. package/dist/data-display/Carousel.js +2 -2
  170. package/dist/data-display/Chart.js +10 -10
  171. package/dist/data-display/DataGrid.js +2 -2
  172. package/dist/data-display/DataViews.js +68 -68
  173. package/dist/data-display/FleetMap.js +55 -0
  174. package/dist/data-display/FleetMap.js.map +1 -0
  175. package/dist/data-display/HoverCard.js +2 -2
  176. package/dist/data-display/Lightbox.js +2 -2
  177. package/dist/data-display/MapPreview.js +6 -6
  178. package/dist/data-display/Sheet.js +3 -3
  179. package/dist/data-display/Table.js +3 -3
  180. package/dist/feedback/ConfirmAction.js +3 -3
  181. package/dist/feedback/Modal.js +2 -2
  182. package/dist/feedback/StackedModal.js +2 -2
  183. package/dist/form/AddressAutocomplete.js +5 -5
  184. package/dist/form/CategorySelect.js +4 -4
  185. package/dist/form/Command.js +2 -2
  186. package/dist/form/DateRangePicker.js +5 -5
  187. package/dist/form/PhoneInput.js +3 -3
  188. package/dist/form/total-form.js +9 -9
  189. package/dist/layout/SettingsLayout.js +2 -2
  190. package/dist/locales.js +4 -4
  191. package/dist/native/chunks/{chunk-T6BJPP3E.js → chunk-DN73KYIZ.js} +1 -1
  192. package/dist/native/chunks/chunk-DN73KYIZ.js.map +1 -0
  193. package/dist/native/chunks/chunk-GW7TQ7HO.js +215 -0
  194. package/dist/native/chunks/chunk-GW7TQ7HO.js.map +1 -0
  195. package/dist/native/chunks/chunk-MJVSQXOD.js +9 -0
  196. package/dist/native/chunks/chunk-MJVSQXOD.js.map +1 -0
  197. package/dist/native/chunks/chunk-ZAQ3AZJM.js +292 -0
  198. package/dist/native/chunks/chunk-ZAQ3AZJM.js.map +1 -0
  199. package/dist/native/data-display/Alert.js +4 -4
  200. package/dist/native/data-display/Avatar.js +3 -3
  201. package/dist/native/data-display/Badge.js +4 -4
  202. package/dist/native/data-display/Chip.js +3 -5
  203. package/dist/native/data-display/Chip.js.map +1 -1
  204. package/dist/native/data-display/EmptyState.js +9 -286
  205. package/dist/native/data-display/EmptyState.js.map +1 -1
  206. package/dist/native/data-display/ErrorState.js +6 -6
  207. package/dist/native/data-display/FleetMap.js +498 -0
  208. package/dist/native/data-display/FleetMap.js.map +1 -0
  209. package/dist/native/data-display/LoadingState.js +3 -3
  210. package/dist/native/feedback/Dialog.js +3 -3
  211. package/dist/native/form/Checkbox.js +1 -1
  212. package/dist/native/form/Input.js +1 -1
  213. package/dist/native/form/Select.js +1 -1
  214. package/dist/native/form/Switch.js +1 -1
  215. package/dist/native/layout/Card.js +3 -3
  216. package/dist/native/layout/Skeleton.js +6 -209
  217. package/dist/native/layout/Skeleton.js.map +1 -1
  218. package/dist/navigation/AppHeader.js +6 -6
  219. package/dist/navigation/Pagination.js +2 -2
  220. package/dist/pt-BR.js +4 -4
  221. package/dist/types/components/data-display/FleetMap/FleetBody.d.ts +22 -0
  222. package/dist/types/components/data-display/FleetMap/FleetBody.d.ts.map +1 -0
  223. package/dist/types/components/data-display/FleetMap/FleetCanvas.d.ts +50 -0
  224. package/dist/types/components/data-display/FleetMap/FleetCanvas.d.ts.map +1 -0
  225. package/dist/types/components/data-display/FleetMap/FleetMap.base.d.ts +141 -0
  226. package/dist/types/components/data-display/FleetMap/FleetMap.base.d.ts.map +1 -0
  227. package/dist/types/components/data-display/FleetMap/FleetMap.d.ts +32 -0
  228. package/dist/types/components/data-display/FleetMap/FleetMap.d.ts.map +1 -0
  229. package/dist/types/components/data-display/FleetMap/FleetMap.fixtures.d.ts +23 -0
  230. package/dist/types/components/data-display/FleetMap/FleetMap.fixtures.d.ts.map +1 -0
  231. package/dist/types/components/data-display/FleetMap/FleetMap.helpers.d.ts +45 -0
  232. package/dist/types/components/data-display/FleetMap/FleetMap.helpers.d.ts.map +1 -0
  233. package/dist/types/components/data-display/FleetMap/FleetMap.hooks.d.ts +71 -0
  234. package/dist/types/components/data-display/FleetMap/FleetMap.hooks.d.ts.map +1 -0
  235. package/dist/types/components/data-display/FleetMap/FleetMap.metrics.d.ts +76 -0
  236. package/dist/types/components/data-display/FleetMap/FleetMap.metrics.d.ts.map +1 -0
  237. package/dist/types/components/data-display/FleetMap/FleetMap.types.d.ts +45 -0
  238. package/dist/types/components/data-display/FleetMap/FleetMap.types.d.ts.map +1 -0
  239. package/dist/types/components/data-display/FleetMap/FleetRoster.d.ts +15 -0
  240. package/dist/types/components/data-display/FleetMap/FleetRoster.d.ts.map +1 -0
  241. package/dist/types/components/data-display/FleetMap/index.d.ts +5 -0
  242. package/dist/types/components/data-display/FleetMap/index.d.ts.map +1 -0
  243. package/dist/types/components/data-display/index.d.ts +1 -0
  244. package/dist/types/components/data-display/index.d.ts.map +1 -1
  245. package/dist/types/platform/aria.d.ts +13 -0
  246. package/dist/types/platform/aria.d.ts.map +1 -1
  247. package/dist/types-native/components/data-display/FleetMap/FleetMap.base.d.ts +141 -0
  248. package/dist/types-native/components/data-display/FleetMap/FleetMap.base.d.ts.map +1 -0
  249. package/dist/types-native/components/data-display/FleetMap/FleetMap.helpers.d.ts +45 -0
  250. package/dist/types-native/components/data-display/FleetMap/FleetMap.helpers.d.ts.map +1 -0
  251. package/dist/types-native/components/data-display/FleetMap/FleetMap.hooks.d.ts +71 -0
  252. package/dist/types-native/components/data-display/FleetMap/FleetMap.hooks.d.ts.map +1 -0
  253. package/dist/types-native/components/data-display/FleetMap/FleetMap.metrics.d.ts +76 -0
  254. package/dist/types-native/components/data-display/FleetMap/FleetMap.metrics.d.ts.map +1 -0
  255. package/dist/types-native/components/data-display/FleetMap/FleetMap.native.d.ts +30 -0
  256. package/dist/types-native/components/data-display/FleetMap/FleetMap.native.d.ts.map +1 -0
  257. package/dist/types-native/components/data-display/FleetMap/FleetMap.types.native.d.ts +36 -0
  258. package/dist/types-native/components/data-display/FleetMap/FleetMap.types.native.d.ts.map +1 -0
  259. package/dist/types-native/components/data-display/FleetMap/FleetRoster.native.d.ts +17 -0
  260. package/dist/types-native/components/data-display/FleetMap/FleetRoster.native.d.ts.map +1 -0
  261. package/dist/types-native/components/data-display/FleetMap/index.native.d.ts +4 -0
  262. package/dist/types-native/components/data-display/FleetMap/index.native.d.ts.map +1 -0
  263. package/dist/types-native/platform/aria.d.ts +13 -0
  264. package/dist/types-native/platform/aria.d.ts.map +1 -1
  265. package/dist/user-avatar.js +5 -5
  266. package/package.json +9 -1
  267. package/src/components/data-display/FleetMap/FleetBody.tsx +96 -0
  268. package/src/components/data-display/FleetMap/FleetCanvas.tsx +80 -0
  269. package/src/components/data-display/FleetMap/FleetMap.base.ts +144 -0
  270. package/src/components/data-display/FleetMap/FleetMap.fixtures.ts +70 -0
  271. package/src/components/data-display/FleetMap/FleetMap.helpers.ts +85 -0
  272. package/src/components/data-display/FleetMap/FleetMap.hooks.ts +183 -0
  273. package/src/components/data-display/FleetMap/FleetMap.md +189 -0
  274. package/src/components/data-display/FleetMap/FleetMap.metrics.ts +81 -0
  275. package/src/components/data-display/FleetMap/FleetMap.native.tsx +185 -0
  276. package/src/components/data-display/FleetMap/FleetMap.tsx +163 -0
  277. package/src/components/data-display/FleetMap/FleetMap.types.native.ts +44 -0
  278. package/src/components/data-display/FleetMap/FleetMap.types.ts +53 -0
  279. package/src/components/data-display/FleetMap/FleetRoster.native.tsx +382 -0
  280. package/src/components/data-display/FleetMap/FleetRoster.tsx +327 -0
  281. package/src/components/data-display/FleetMap/NATIVE-NOTES.md +14 -0
  282. package/src/components/data-display/FleetMap/index.native.ts +10 -0
  283. package/src/components/data-display/FleetMap/index.ts +9 -0
  284. package/src/components/data-display/FleetMap/tests.md +84 -0
  285. package/src/components/data-display/FleetMap/track.md +50 -0
  286. package/src/components/data-display/index.ts +1 -0
  287. package/src/components/feedback/ConfirmAction/ConfirmAction.md +10 -4
  288. package/src/platform/aria.ts +13 -0
  289. package/dist/chunk-X3HL5C6H.js +0 -1
  290. package/dist/native/chunks/chunk-T6BJPP3E.js.map +0 -1
  291. /package/dist/{chunk-X3HL5C6H.js.map → _internal/components/data-display/FleetMap/FleetBody.js.map} +0 -0
  292. /package/dist/{chunk-WGRS4PGY.js.map → chunk-2C47DEVU.js.map} +0 -0
  293. /package/dist/{chunk-WMALDOF5.js.map → chunk-4G3FGCWX.js.map} +0 -0
  294. /package/dist/{chunk-ZOUWHGV3.js.map → chunk-4MGFUPQT.js.map} +0 -0
  295. /package/dist/{chunk-QFJRB27Q.js.map → chunk-6LGWJFQK.js.map} +0 -0
  296. /package/dist/{chunk-XSF6AXEU.js.map → chunk-6RZDDUSM.js.map} +0 -0
  297. /package/dist/{chunk-VIK3HAUQ.js.map → chunk-73NJCGQG.js.map} +0 -0
  298. /package/dist/{chunk-UU3QAKG6.js.map → chunk-B7DKSBAE.js.map} +0 -0
  299. /package/dist/{chunk-3FLVIVOK.js.map → chunk-BHIGHSXA.js.map} +0 -0
  300. /package/dist/{chunk-HKV4MQ2W.js.map → chunk-BLBOYCOV.js.map} +0 -0
  301. /package/dist/{chunk-PFV6RL4Q.js.map → chunk-C6KTQF4F.js.map} +0 -0
  302. /package/dist/{chunk-LYSSO6WC.js.map → chunk-CZPU5TW3.js.map} +0 -0
  303. /package/dist/{chunk-PMS3H3OO.js.map → chunk-DF2FFYZ7.js.map} +0 -0
  304. /package/dist/{chunk-QZBUGUOY.js.map → chunk-DFLTUI7W.js.map} +0 -0
  305. /package/dist/{chunk-WLQTHPMX.js.map → chunk-EJQW2IYW.js.map} +0 -0
  306. /package/dist/{chunk-VQAM2XZV.js.map → chunk-EMUBMZMA.js.map} +0 -0
  307. /package/dist/{chunk-XFUNJYFT.js.map → chunk-EPNUVSNK.js.map} +0 -0
  308. /package/dist/{chunk-MSUSZLRE.js.map → chunk-EWT3WMVY.js.map} +0 -0
  309. /package/dist/{chunk-Q75SJ35X.js.map → chunk-GMTM6EN7.js.map} +0 -0
  310. /package/dist/{chunk-R6SBP5M5.js.map → chunk-GOWBYHYW.js.map} +0 -0
  311. /package/dist/{chunk-6NZTYZP5.js.map → chunk-GVA5CYKP.js.map} +0 -0
  312. /package/dist/{chunk-VR5HDUBK.js.map → chunk-HP2ODXNA.js.map} +0 -0
  313. /package/dist/{chunk-2MMEIN4T.js.map → chunk-I3E2QHTH.js.map} +0 -0
  314. /package/dist/{chunk-UVGRRI7N.js.map → chunk-IPVYEPXO.js.map} +0 -0
  315. /package/dist/{chunk-5ZTUP65G.js.map → chunk-JM64OEU6.js.map} +0 -0
  316. /package/dist/{chunk-7BYFC2MR.js.map → chunk-JUHUKZGU.js.map} +0 -0
  317. /package/dist/{chunk-DVUPIVI2.js.map → chunk-KT6HWAEN.js.map} +0 -0
  318. /package/dist/{chunk-TLMLEC4F.js.map → chunk-KW7H5O3K.js.map} +0 -0
  319. /package/dist/{chunk-VLHGBFR6.js.map → chunk-MZQTHSLN.js.map} +0 -0
  320. /package/dist/{chunk-4SQFO4GW.js.map → chunk-NA7MIEG6.js.map} +0 -0
  321. /package/dist/{chunk-QWDEOITU.js.map → chunk-O4VCZTK4.js.map} +0 -0
  322. /package/dist/{chunk-LS4FMSHY.js.map → chunk-OAYX4XN7.js.map} +0 -0
  323. /package/dist/{chunk-Y5UCOBQN.js.map → chunk-OGGHD64H.js.map} +0 -0
  324. /package/dist/{chunk-7TZ6OC5T.js.map → chunk-OH3YGJT5.js.map} +0 -0
  325. /package/dist/{chunk-REMK55JI.js.map → chunk-OLZTGVBT.js.map} +0 -0
  326. /package/dist/{chunk-VNNG46TS.js.map → chunk-ORSLFKKZ.js.map} +0 -0
  327. /package/dist/{chunk-ZIYMBR7K.js.map → chunk-OTH7H3IP.js.map} +0 -0
  328. /package/dist/{chunk-5NKUYCZD.js.map → chunk-PN5CQKA2.js.map} +0 -0
  329. /package/dist/{chunk-J4W4TQPC.js.map → chunk-QJRDDVZ2.js.map} +0 -0
  330. /package/dist/{chunk-VNDOQEJ7.js.map → chunk-QRX5BYUQ.js.map} +0 -0
  331. /package/dist/{chunk-XT7HMNBW.js.map → chunk-R6SNYC3D.js.map} +0 -0
  332. /package/dist/{chunk-W2UTDN4T.js.map → chunk-RBGOGTT2.js.map} +0 -0
  333. /package/dist/{chunk-X5MPHXG5.js.map → chunk-RPWEXIAW.js.map} +0 -0
  334. /package/dist/{chunk-EXJBPKGW.js.map → chunk-SHDIZSN5.js.map} +0 -0
  335. /package/dist/{chunk-23FCBIUN.js.map → chunk-TNVO2DTB.js.map} +0 -0
  336. /package/dist/{chunk-AGSBIUPV.js.map → chunk-TPLKJHNN.js.map} +0 -0
  337. /package/dist/{chunk-YQ6TNHAC.js.map → chunk-TTLI6PF7.js.map} +0 -0
  338. /package/dist/{chunk-U2AC4AQX.js.map → chunk-TU5EIKJA.js.map} +0 -0
  339. /package/dist/{chunk-KTK4A3IF.js.map → chunk-TX5V42AY.js.map} +0 -0
  340. /package/dist/{chunk-BGYLL467.js.map → chunk-TXHJX2NG.js.map} +0 -0
  341. /package/dist/{chunk-FL54G7PV.js.map → chunk-WIT3Y7JT.js.map} +0 -0
  342. /package/dist/{chunk-W7Y6RA6W.js.map → chunk-WR5JFDQ5.js.map} +0 -0
  343. /package/dist/{chunk-2UYR2OOE.js.map → chunk-YRUCIKPO.js.map} +0 -0
  344. /package/dist/{chunk-AEGXHZQT.js.map → chunk-YW5T6KZ5.js.map} +0 -0
  345. /package/dist/{chunk-QKIJ6YXZ.js.map → chunk-ZA4U5GDU.js.map} +0 -0
  346. /package/dist/{chunk-ZKR5B6YJ.js.map → chunk-ZRPGVHTR.js.map} +0 -0
  347. /package/dist/{chunk-OXQP3N6X.js.map → chunk-ZTZ6S5Q6.js.map} +0 -0
@@ -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
+ });
@@ -0,0 +1,327 @@
1
+ import Box from '@mui/material/Box/index.js';
2
+ import Stack from '@mui/material/Stack/index.js';
3
+ import Typography from '@mui/material/Typography/index.js';
4
+ import { useTheme, type SxProps, type Theme } from '@mui/material/styles/index.js';
5
+ import React from 'react';
6
+
7
+ import { Skeleton } from '../../layout/Skeleton/Skeleton';
8
+
9
+ import { freshnessOf } from './FleetMap.helpers';
10
+ import { FLEET_DOT, FLEET_ROSTER, FLEET_ROW, FLEET_SKELETON } from './FleetMap.metrics';
11
+ import type { FleetFreshness, FleetMapCopy, FleetUnit } from './FleetMap.types';
12
+
13
+ /**
14
+ * The roster beside the map — and the ACCESSIBLE half of this component.
15
+ *
16
+ * A map is a picture of positions and a screen reader cannot read a picture, so
17
+ * the panel's information is carried by this list. That is not a consolation
18
+ * prize: the list answers the questions a dispatcher actually asks — who is
19
+ * reporting, how recently, and what are they carrying — and it answers them in
20
+ * an order the map cannot express.
21
+ *
22
+ * The map beside it is a NAMED LANDMARK rather than an `aria-hidden` one, so a
23
+ * reader can skip it in one gesture — see `FleetCanvas`, which carries the
24
+ * reason it must not be hidden.
25
+ */
26
+
27
+ /** Each freshness draws from the semantic palette, never a hardcoded hex. */
28
+ function freshnessColor(freshness: FleetFreshness, theme: Theme): string {
29
+ if (freshness === 'live') return theme.palette.success.main;
30
+ if (freshness === 'lagging') return theme.palette.warning.main;
31
+ return theme.palette.text.disabled;
32
+ }
33
+
34
+ /**
35
+ * A row's two lines of text — the name, and the one-line summary under it.
36
+ *
37
+ * Split out of `FleetRow` to keep that function under the size gate, and the
38
+ * seam is the natural one: everything here is READ, nothing is interactive.
39
+ */
40
+ function RowText({
41
+ unit,
42
+ copy,
43
+ freshness,
44
+ testId,
45
+ }: Pick<RowProps, 'unit' | 'copy' | 'freshness' | 'testId'>): React.JSX.Element {
46
+ const theme = useTheme();
47
+
48
+ // Dropped rather than rendered empty: a phone that reports no accuracy radius
49
+ // would otherwise print a trailing separator with nothing after it.
50
+ const meta = [
51
+ copy.freshness[freshness],
52
+ copy.lastSeen(unit.staleSeconds),
53
+ unit.accuracyM == null ? null : copy.accuracy(unit.accuracyM),
54
+ ]
55
+ .filter((part): part is string => Boolean(part))
56
+ .join(' · ');
57
+
58
+ return (
59
+ <Stack sx={{ minWidth: 0, flexGrow: 1 }}>
60
+ <Typography
61
+ variant="body2"
62
+ component="p"
63
+ data-testid={`${testId}-label`}
64
+ sx={{ fontWeight: theme.typography.fontWeightMedium, overflowWrap: 'anywhere' }}
65
+ >
66
+ {unit.label}
67
+ </Typography>
68
+ <Typography
69
+ variant="caption"
70
+ component="p"
71
+ data-testid={`${testId}-meta`}
72
+ sx={{ color: theme.palette.text.secondary }}
73
+ >
74
+ {meta}
75
+ </Typography>
76
+ </Stack>
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Keep the selected row visible INSIDE the roster.
82
+ *
83
+ * The roster caps its own height, so a dispatcher arrowing through a fleet of
84
+ * thirty otherwise walks the selection below the fold with nothing moving —
85
+ * `aria-activedescendant` follows, and a SIGHTED keyboard user sees no feedback
86
+ * at all.
87
+ *
88
+ * Watches the row's POSITION as well as its selectedness, because the roster
89
+ * re-sorts on every poll: a rider who stays selected while their staleness
90
+ * moves them down the list would otherwise slide out of view with the effect
91
+ * never re-running.
92
+ *
93
+ * Arithmetic rather than `scrollIntoView({ block: 'nearest' })` because that
94
+ * walks every scrollable ancestor, the document included, and `nearest` only
95
+ * spares an ancestor the row is already visible in — which is exactly false
96
+ * when the panel is off screen. Re-running on every re-sort, it would yank the
97
+ * whole page back to the map on a timer, and drag it there on mount for any
98
+ * board rendered below the fold with a selection already set.
99
+ */
100
+ function useKeptInView(
101
+ selected: boolean,
102
+ position: number,
103
+ ): React.RefObject<HTMLDivElement | null> {
104
+ const row = React.useRef<HTMLDivElement>(null);
105
+
106
+ React.useEffect(() => {
107
+ const box = row.current;
108
+ const list = box?.parentElement;
109
+ if (!selected || !box || !list) return;
110
+ const top = box.offsetTop - list.offsetTop;
111
+ if (top < list.scrollTop) {
112
+ list.scrollTop = top;
113
+ } else if (top + box.offsetHeight > list.scrollTop + list.clientHeight) {
114
+ list.scrollTop = top + box.offsetHeight - list.clientHeight;
115
+ }
116
+ }, [selected, position]);
117
+
118
+ return row;
119
+ }
120
+
121
+ /**
122
+ * A row's own box, selected or not.
123
+ *
124
+ * Out of line because the hover rule needs the explanation more than the JSX
125
+ * has room for: hover must never WEAKEN the selection. Written flat as
126
+ * `'&:hover': { backgroundColor: action.hover }`, the pseudo-class wins on
127
+ * specificity and repaints the selected row with the hover tint — measured at
128
+ * `rgba(0,0,0,0.08)` idle against `rgba(0,0,0,0.04)` hovered, so pointing at
129
+ * the selected row visually DESELECTED it and made it indistinguishable from
130
+ * any other row under the cursor.
131
+ */
132
+ function rowSx(theme: Theme, selected: boolean): SxProps<Theme> {
133
+ const resting = selected ? theme.palette.action.selected : 'transparent';
134
+ return {
135
+ display: 'flex',
136
+ alignItems: 'center',
137
+ gap: theme.spacing(FLEET_ROW.gapUnits),
138
+ padding: theme.spacing(FLEET_ROW.paddingYUnits, FLEET_ROW.paddingXUnits),
139
+ // `1`, not `theme.shape.borderRadius / 4`. MUI's `sx` reads a NUMBER here as
140
+ // a multiple of `theme.shape.borderRadius`, so dividing the theme value out
141
+ // and letting `sx` multiply it back in is quadratic: measured 4px on the
142
+ // default theme — which is why this looked right — and 36px on a host that
143
+ // sets its own radius to 12. In a package whose whole premise is being
144
+ // re-themed, only the multiple is portable.
145
+ borderRadius: FLEET_ROW.radiusMultiple,
146
+ cursor: 'pointer',
147
+ backgroundColor: resting,
148
+ '&:hover': {
149
+ backgroundColor: selected ? theme.palette.action.selected : theme.palette.action.hover,
150
+ },
151
+ };
152
+ }
153
+
154
+ interface RowProps {
155
+ unit: FleetUnit;
156
+ copy: FleetMapCopy;
157
+ freshness: FleetFreshness;
158
+ selected: boolean;
159
+ onSelect: (id: string) => void;
160
+ /** The row's DOM id, which `aria-activedescendant` points at. */
161
+ optionId: string;
162
+ /** Where the row sits in the re-sorted roster — see the scroll effect. */
163
+ position: number;
164
+ testId: string;
165
+ }
166
+
167
+ function FleetRow({
168
+ unit,
169
+ copy,
170
+ freshness,
171
+ selected,
172
+ onSelect,
173
+ optionId,
174
+ position,
175
+ testId,
176
+ }: RowProps): React.JSX.Element {
177
+ const theme = useTheme();
178
+ const color = freshnessColor(freshness, theme);
179
+ const row = useKeptInView(selected, position);
180
+
181
+ return (
182
+ <Box
183
+ ref={row}
184
+ role="option"
185
+ aria-selected={selected}
186
+ id={optionId}
187
+ data-testid={testId}
188
+ data-freshness={freshness}
189
+ onClick={() => onSelect(unit.id)}
190
+ sx={rowSx(theme, selected)}
191
+ >
192
+ {/* The dot repeats what the freshness word beside it already says, so it
193
+ is hidden rather than read twice — and colour is never the only
194
+ carrier of the state. */}
195
+ <Box
196
+ aria-hidden="true"
197
+ data-testid={`${testId}-dot`}
198
+ sx={{
199
+ width: FLEET_DOT.size,
200
+ height: FLEET_DOT.size,
201
+ borderRadius: '50%',
202
+ flexShrink: 0,
203
+ backgroundColor: color,
204
+ }}
205
+ />
206
+ <RowText unit={unit} copy={copy} freshness={freshness} testId={testId} />
207
+ {unit.badge && (
208
+ <Typography
209
+ variant="caption"
210
+ component="span"
211
+ data-testid={`${testId}-badge`}
212
+ sx={{ color: theme.palette.text.secondary, flexShrink: 0 }}
213
+ >
214
+ {unit.badge}
215
+ </Typography>
216
+ )}
217
+ </Box>
218
+ );
219
+ }
220
+
221
+ /**
222
+ * What stands in for the roster before any unit has landed.
223
+ *
224
+ * `aria-hidden`, because three grey bars are not information — the panel's
225
+ * `aria-busy` and its optional announcement carry that. The radius is the same
226
+ * MULTIPLE a real row uses rather than a raw pixel count: `Skeleton` forwards
227
+ * the number into `sx`, where it scales with `theme.shape.borderRadius`, so a
228
+ * literal `4` drew 16px against the rows' 4px on the default theme and 48px
229
+ * against 12px on a re-themed host.
230
+ */
231
+ function FirstLoad({ testId }: { testId: string }): React.JSX.Element {
232
+ return (
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
+ />
241
+ ))}
242
+ </Stack>
243
+ );
244
+ }
245
+
246
+ export interface FleetRosterProps {
247
+ units: readonly FleetUnit[];
248
+ copy: FleetMapCopy;
249
+ selectedId: string | null | undefined;
250
+ onSelect: (id: string) => void;
251
+ laggingAfterSeconds: number;
252
+ staleAfterSeconds: number;
253
+ loading: boolean;
254
+ testId: string;
255
+ onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void;
256
+ }
257
+
258
+ export function FleetRoster({
259
+ units,
260
+ copy,
261
+ selectedId,
262
+ onSelect,
263
+ laggingAfterSeconds,
264
+ staleAfterSeconds,
265
+ loading,
266
+ testId,
267
+ onKeyDown,
268
+ }: FleetRosterProps): React.JSX.Element {
269
+ const theme = useTheme();
270
+ // Generated, not derived from `dataTestId`: two boards on one page filtered
271
+ // from the same fleet would otherwise emit duplicate DOM ids and an ambiguous
272
+ // `aria-activedescendant`. Same reason the heading uses one.
273
+ const rowIdPrefix = React.useId();
274
+ const optionId = (id: string): string => `${rowIdPrefix}-${id}`;
275
+
276
+ // The selection is CONTROLLED, so it can name a unit that has since dropped
277
+ // out of the freshness window — a courier ending their shift mid-poll. The
278
+ // attribute must then be absent rather than pointing at an element that no
279
+ // longer renders, which announces nothing and fails an axe audit. `mapCentre`
280
+ // and `nextSelection` both handle the same case; this is the third place.
281
+ const active = units.some((unit) => unit.id === selectedId) ? selectedId : null;
282
+
283
+ // Only when there is nothing to show yet. Unmounting a populated listbox on
284
+ // every poll destroys the focus inside it, so a dispatcher arrowing through
285
+ // the roster is thrown back to the top of the page each time the data
286
+ // refreshes — and the rows they were reading vanish and return.
287
+ if (loading && units.length === 0) return <FirstLoad testId={testId} />;
288
+
289
+ return (
290
+ <Box
291
+ role="listbox"
292
+ // A single tab stop with arrow keys inside, which is the listbox pattern:
293
+ // a dispatcher tabbing past a fleet of thirty should not have to press it
294
+ // thirty times to reach the map.
295
+ tabIndex={0}
296
+ aria-label={copy.rosterLabel}
297
+ aria-activedescendant={active ? optionId(active) : undefined}
298
+ data-testid={`${testId}-roster`}
299
+ onKeyDown={onKeyDown}
300
+ sx={{
301
+ display: 'flex',
302
+ flexDirection: 'column',
303
+ gap: theme.spacing(FLEET_ROSTER.gapUnits),
304
+ overflowY: 'auto',
305
+ minWidth: 0,
306
+ '&:focus-visible': {
307
+ outline: `${FLEET_ROSTER.focusRingWidth}px solid ${theme.palette.primary.main}`,
308
+ outlineOffset: FLEET_ROSTER.focusRingOffset,
309
+ },
310
+ }}
311
+ >
312
+ {units.map((unit, index) => (
313
+ <FleetRow
314
+ key={unit.id}
315
+ unit={unit}
316
+ copy={copy}
317
+ freshness={freshnessOf(unit.staleSeconds, laggingAfterSeconds, staleAfterSeconds)}
318
+ selected={unit.id === active}
319
+ onSelect={onSelect}
320
+ optionId={optionId(unit.id)}
321
+ position={index}
322
+ testId={`${testId}-${unit.id}`}
323
+ />
324
+ ))}
325
+ </Box>
326
+ );
327
+ }