@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.
- package/NATIVE.md +2 -1
- package/components-catalog.md +7 -0
- package/dist/_internal/components/data-display/AsyncStateContainer/AsyncStateContainer.js +3 -3
- package/dist/_internal/components/data-display/Avatar/Avatar.js +4 -4
- package/dist/_internal/components/data-display/Avatar/Avatar.view.js +2 -2
- package/dist/_internal/components/data-display/Badge/Badge.js +2 -2
- package/dist/_internal/components/data-display/CardKit/use-remove-confirm.js +1 -1
- package/dist/_internal/components/data-display/CardKit/use-row-confirm.js +1 -1
- package/dist/_internal/components/data-display/Carousel/Carousel.js +2 -2
- package/dist/_internal/components/data-display/Chart/Chart.js +4 -4
- package/dist/_internal/components/data-display/Chart/SpecChart.js +7 -7
- package/dist/_internal/components/data-display/Chart/chart-context.js +2 -2
- package/dist/_internal/components/data-display/Chart/chart-renderers.js +3 -3
- package/dist/_internal/components/data-display/Chart/chart-spec.js +2 -2
- package/dist/_internal/components/data-display/DataGrid/DataGrid.js +2 -2
- package/dist/_internal/components/data-display/DataViews/DataViewsGrid.js +41 -41
- package/dist/_internal/components/data-display/DataViews/DataViewsTableBase.js +47 -47
- package/dist/_internal/components/data-display/DataViews/ManageViewsDialog.js +1 -1
- package/dist/_internal/components/data-display/DataViews/base-list-card-geometry.js +4 -4
- package/dist/_internal/components/data-display/DataViews/base-list-card-slots.js +3 -3
- package/dist/_internal/components/data-display/DataViews/base-list-card.js +6 -6
- package/dist/_internal/components/data-display/DataViews/data-views-category-pill.js +5 -5
- package/dist/_internal/components/data-display/DataViews/data-views-display-panel.js +3 -3
- package/dist/_internal/components/data-display/DataViews/data-views-filter-panel.js +10 -10
- package/dist/_internal/components/data-display/DataViews/data-views-grid-bodies.js +6 -6
- package/dist/_internal/components/data-display/DataViews/data-views-grid-parts.js +36 -36
- package/dist/_internal/components/data-display/DataViews/data-views-inline-bar.js +11 -11
- package/dist/_internal/components/data-display/DataViews/data-views-more-fields.js +3 -3
- package/dist/_internal/components/data-display/DataViews/data-views-more-filters.js +4 -4
- package/dist/_internal/components/data-display/DataViews/data-views-pagination.js +3 -3
- package/dist/_internal/components/data-display/DataViews/data-views-range-pill.js +2 -2
- package/dist/_internal/components/data-display/DataViews/data-views-scope-tabs.js +2 -2
- package/dist/_internal/components/data-display/DataViews/data-views-shell-toolbar.js +6 -6
- package/dist/_internal/components/data-display/DataViews/data-views-table-parts.js +1 -1
- package/dist/_internal/components/data-display/DataViews/data-views-toolbar.js +5 -5
- package/dist/_internal/components/data-display/DataViews/list-card-rails.js +2 -2
- package/dist/_internal/components/data-display/DataViews/use-data-views-state.js +4 -4
- package/dist/_internal/components/data-display/FleetMap/FleetBody.js +32 -0
- package/dist/_internal/components/data-display/FleetMap/FleetCanvas.js +23 -0
- package/dist/_internal/components/data-display/FleetMap/FleetCanvas.js.map +1 -0
- package/dist/_internal/components/data-display/FleetMap/FleetMap.base.js +1 -0
- package/dist/_internal/components/data-display/FleetMap/FleetMap.base.js.map +1 -0
- package/dist/_internal/components/data-display/FleetMap/FleetMap.fixtures.js +57 -0
- package/dist/_internal/components/data-display/FleetMap/FleetMap.fixtures.js.map +1 -0
- package/dist/_internal/components/data-display/FleetMap/FleetMap.helpers.js +14 -0
- package/dist/_internal/components/data-display/FleetMap/FleetMap.helpers.js.map +1 -0
- package/dist/_internal/components/data-display/FleetMap/FleetMap.hooks.js +9 -0
- package/dist/_internal/components/data-display/FleetMap/FleetMap.hooks.js.map +1 -0
- package/dist/_internal/components/data-display/FleetMap/FleetMap.js +47 -0
- package/dist/_internal/components/data-display/FleetMap/FleetMap.js.map +1 -0
- package/dist/_internal/components/data-display/FleetMap/FleetMap.metrics.js +20 -0
- package/dist/_internal/components/data-display/FleetMap/FleetMap.metrics.js.map +1 -0
- package/dist/_internal/components/data-display/FleetMap/FleetMap.types.js +1 -0
- package/dist/_internal/components/data-display/FleetMap/FleetMap.types.js.map +1 -0
- package/dist/_internal/components/data-display/FleetMap/FleetRoster.js +15 -0
- package/dist/_internal/components/data-display/FleetMap/FleetRoster.js.map +1 -0
- package/dist/_internal/components/data-display/HoverCard/HoverCard.js +2 -2
- package/dist/_internal/components/data-display/Lightbox/Lightbox.js +2 -2
- package/dist/_internal/components/data-display/MapPreview/MapLayers.js +2 -2
- package/dist/_internal/components/data-display/MapPreview/MapPreview.js +6 -6
- package/dist/_internal/components/data-display/Sheet/Sheet.hooks.js +2 -2
- package/dist/_internal/components/data-display/Sheet/Sheet.js +3 -3
- package/dist/_internal/components/data-display/Table/Table.js +3 -3
- package/dist/_internal/components/data-display/index.js +90 -69
- package/dist/_internal/components/feedback/Modal/Modal.js +2 -2
- package/dist/_internal/components/feedback/StackedModal/StackedModal.js +2 -2
- package/dist/_internal/components/feedback/index.js +14 -14
- package/dist/_internal/components/form/AddressAutocomplete/AddressAutocomplete.hooks.js +3 -3
- package/dist/_internal/components/form/AddressAutocomplete/AddressAutocomplete.js +5 -5
- package/dist/_internal/components/form/AddressAutocomplete/AddressAutocompleteField.js +2 -2
- package/dist/_internal/components/form/CategorySelect/CategoryPanel.js +2 -2
- package/dist/_internal/components/form/CategorySelect/CategorySelect.js +4 -4
- package/dist/_internal/components/form/Command/Command.js +2 -2
- package/dist/_internal/components/form/DateRangePicker/DateRangePicker.js +3 -3
- package/dist/_internal/components/form/PhoneInput/PhoneInput.hooks.js +2 -2
- package/dist/_internal/components/form/PhoneInput/PhoneInput.js +3 -3
- package/dist/_internal/components/form/index.js +30 -30
- package/dist/_internal/components/form/total-form/Fields/CategoryField.js +5 -5
- package/dist/_internal/components/form/total-form/Fields/index.js +9 -9
- package/dist/_internal/components/index.js +160 -140
- package/dist/_internal/components/layout/SettingsLayout/SettingsLayout.js +2 -2
- package/dist/_internal/components/layout/index.js +20 -20
- package/dist/_internal/components/navigation/AppHeader/AppHeader.details.js +4 -4
- package/dist/_internal/components/navigation/Pagination/Pagination.js +2 -2
- package/dist/_internal/components/navigation/index.js +12 -12
- package/dist/_internal/components/utility/index.js +4 -4
- package/dist/_internal/platform/aria.js.map +1 -1
- package/dist/_internal/pt-BR.data-display.js +2 -2
- package/dist/_internal/user-avatar/user-avatar.js +5 -5
- package/dist/charts.js +10 -10
- package/dist/{chunk-WGRS4PGY.js → chunk-2C47DEVU.js} +4 -4
- package/dist/chunk-2RQINU5T.js +41 -0
- package/dist/chunk-2RQINU5T.js.map +1 -0
- package/dist/{chunk-WMALDOF5.js → chunk-4G3FGCWX.js} +4 -4
- package/dist/{chunk-ZOUWHGV3.js → chunk-4MGFUPQT.js} +4 -4
- package/dist/{chunk-QFJRB27Q.js → chunk-6LGWJFQK.js} +2 -2
- package/dist/{chunk-XSF6AXEU.js → chunk-6RZDDUSM.js} +2 -2
- package/dist/{chunk-VIK3HAUQ.js → chunk-73NJCGQG.js} +2 -2
- package/dist/{chunk-UU3QAKG6.js → chunk-B7DKSBAE.js} +14 -14
- package/dist/{chunk-3FLVIVOK.js → chunk-BHIGHSXA.js} +2 -2
- package/dist/{chunk-HKV4MQ2W.js → chunk-BLBOYCOV.js} +5 -5
- package/dist/chunk-BTQZ3EX3.js +45 -0
- package/dist/chunk-BTQZ3EX3.js.map +1 -0
- package/dist/{chunk-PFV6RL4Q.js → chunk-C6KTQF4F.js} +3 -3
- package/dist/chunk-CDCTLPMD.js +1 -0
- package/dist/chunk-CDCTLPMD.js.map +1 -0
- package/dist/{chunk-LYSSO6WC.js → chunk-CZPU5TW3.js} +4 -4
- package/dist/{chunk-PMS3H3OO.js → chunk-DF2FFYZ7.js} +4 -4
- package/dist/{chunk-QZBUGUOY.js → chunk-DFLTUI7W.js} +4 -4
- package/dist/{chunk-WLQTHPMX.js → chunk-EJQW2IYW.js} +2 -2
- package/dist/{chunk-VQAM2XZV.js → chunk-EMUBMZMA.js} +2 -2
- package/dist/{chunk-XFUNJYFT.js → chunk-EPNUVSNK.js} +7 -7
- package/dist/{chunk-MSUSZLRE.js → chunk-EWT3WMVY.js} +5 -5
- package/dist/{chunk-Q75SJ35X.js → chunk-GMTM6EN7.js} +3 -3
- package/dist/{chunk-R6SBP5M5.js → chunk-GOWBYHYW.js} +4 -4
- package/dist/{chunk-6NZTYZP5.js → chunk-GVA5CYKP.js} +2 -2
- package/dist/chunk-HMV4QMXS.js +219 -0
- package/dist/chunk-HMV4QMXS.js.map +1 -0
- package/dist/{chunk-VR5HDUBK.js → chunk-HP2ODXNA.js} +2 -2
- package/dist/{chunk-2MMEIN4T.js → chunk-I3E2QHTH.js} +4 -4
- package/dist/{chunk-UVGRRI7N.js → chunk-IPVYEPXO.js} +4 -4
- package/dist/{chunk-5ZTUP65G.js → chunk-JM64OEU6.js} +5 -5
- package/dist/{chunk-7BYFC2MR.js → chunk-JUHUKZGU.js} +7 -7
- package/dist/chunk-JZXWCEH5.js +116 -0
- package/dist/chunk-JZXWCEH5.js.map +1 -0
- package/dist/{chunk-DVUPIVI2.js → chunk-KT6HWAEN.js} +4 -4
- package/dist/{chunk-TLMLEC4F.js → chunk-KW7H5O3K.js} +6 -6
- package/dist/{chunk-VLHGBFR6.js → chunk-MZQTHSLN.js} +3 -3
- package/dist/{chunk-4SQFO4GW.js → chunk-NA7MIEG6.js} +7 -7
- package/dist/{chunk-QWDEOITU.js → chunk-O4VCZTK4.js} +2 -2
- package/dist/{chunk-LS4FMSHY.js → chunk-OAYX4XN7.js} +4 -4
- package/dist/{chunk-Y5UCOBQN.js → chunk-OGGHD64H.js} +4 -4
- package/dist/{chunk-7TZ6OC5T.js → chunk-OH3YGJT5.js} +2 -2
- package/dist/{chunk-REMK55JI.js → chunk-OLZTGVBT.js} +4 -4
- package/dist/{chunk-VNNG46TS.js → chunk-ORSLFKKZ.js} +6 -6
- package/dist/{chunk-ZIYMBR7K.js → chunk-OTH7H3IP.js} +2 -2
- package/dist/{chunk-5NKUYCZD.js → chunk-PN5CQKA2.js} +4 -4
- package/dist/{chunk-J4W4TQPC.js → chunk-QJRDDVZ2.js} +5 -5
- package/dist/{chunk-VNDOQEJ7.js → chunk-QRX5BYUQ.js} +4 -4
- package/dist/{chunk-XT7HMNBW.js → chunk-R6SNYC3D.js} +5 -5
- package/dist/{chunk-W2UTDN4T.js → chunk-RBGOGTT2.js} +4 -4
- package/dist/chunk-RP5FK3SO.js +79 -0
- package/dist/chunk-RP5FK3SO.js.map +1 -0
- package/dist/{chunk-X5MPHXG5.js → chunk-RPWEXIAW.js} +6 -6
- package/dist/chunk-RQWZYD6H.js +1 -0
- package/dist/chunk-RQWZYD6H.js.map +1 -0
- package/dist/{chunk-EXJBPKGW.js → chunk-SHDIZSN5.js} +5 -5
- package/dist/{chunk-23FCBIUN.js → chunk-TNVO2DTB.js} +8 -8
- package/dist/{chunk-AGSBIUPV.js → chunk-TPLKJHNN.js} +4 -4
- package/dist/{chunk-YQ6TNHAC.js → chunk-TTLI6PF7.js} +5 -5
- package/dist/{chunk-U2AC4AQX.js → chunk-TU5EIKJA.js} +4 -4
- package/dist/{chunk-KTK4A3IF.js → chunk-TX5V42AY.js} +11 -11
- package/dist/{chunk-BGYLL467.js → chunk-TXHJX2NG.js} +4 -4
- package/dist/{chunk-FL54G7PV.js → chunk-WIT3Y7JT.js} +2 -2
- package/dist/{chunk-W7Y6RA6W.js → chunk-WR5JFDQ5.js} +4 -4
- package/dist/chunk-XPMGFAJ3.js +36 -0
- package/dist/chunk-XPMGFAJ3.js.map +1 -0
- package/dist/{chunk-2UYR2OOE.js → chunk-YRUCIKPO.js} +4 -4
- package/dist/{chunk-AEGXHZQT.js → chunk-YW5T6KZ5.js} +7 -7
- package/dist/{chunk-QKIJ6YXZ.js → chunk-ZA4U5GDU.js} +4 -4
- package/dist/chunk-ZCPSND5Y.js +64 -0
- package/dist/chunk-ZCPSND5Y.js.map +1 -0
- package/dist/{chunk-ZKR5B6YJ.js → chunk-ZRPGVHTR.js} +2 -2
- package/dist/{chunk-OXQP3N6X.js → chunk-ZTZ6S5Q6.js} +4 -4
- package/dist/data-display/AsyncStateContainer.js +3 -3
- package/dist/data-display/Avatar.js +4 -4
- package/dist/data-display/Badge.js +2 -2
- package/dist/data-display/CardKit.js +11 -11
- package/dist/data-display/Carousel.js +2 -2
- package/dist/data-display/Chart.js +10 -10
- package/dist/data-display/DataGrid.js +2 -2
- package/dist/data-display/DataViews.js +68 -68
- package/dist/data-display/FleetMap.js +55 -0
- package/dist/data-display/FleetMap.js.map +1 -0
- package/dist/data-display/HoverCard.js +2 -2
- package/dist/data-display/Lightbox.js +2 -2
- package/dist/data-display/MapPreview.js +6 -6
- package/dist/data-display/Sheet.js +3 -3
- package/dist/data-display/Table.js +3 -3
- package/dist/feedback/ConfirmAction.js +3 -3
- package/dist/feedback/Modal.js +2 -2
- package/dist/feedback/StackedModal.js +2 -2
- package/dist/form/AddressAutocomplete.js +5 -5
- package/dist/form/CategorySelect.js +4 -4
- package/dist/form/Command.js +2 -2
- package/dist/form/DateRangePicker.js +5 -5
- package/dist/form/PhoneInput.js +3 -3
- package/dist/form/total-form.js +9 -9
- package/dist/layout/SettingsLayout.js +2 -2
- package/dist/locales.js +4 -4
- package/dist/native/chunks/{chunk-T6BJPP3E.js → chunk-DN73KYIZ.js} +1 -1
- package/dist/native/chunks/chunk-DN73KYIZ.js.map +1 -0
- package/dist/native/chunks/chunk-GW7TQ7HO.js +215 -0
- package/dist/native/chunks/chunk-GW7TQ7HO.js.map +1 -0
- package/dist/native/chunks/chunk-MJVSQXOD.js +9 -0
- package/dist/native/chunks/chunk-MJVSQXOD.js.map +1 -0
- package/dist/native/chunks/chunk-ZAQ3AZJM.js +292 -0
- package/dist/native/chunks/chunk-ZAQ3AZJM.js.map +1 -0
- package/dist/native/data-display/Alert.js +4 -4
- package/dist/native/data-display/Avatar.js +3 -3
- package/dist/native/data-display/Badge.js +4 -4
- package/dist/native/data-display/Chip.js +3 -5
- package/dist/native/data-display/Chip.js.map +1 -1
- package/dist/native/data-display/EmptyState.js +9 -286
- package/dist/native/data-display/EmptyState.js.map +1 -1
- package/dist/native/data-display/ErrorState.js +6 -6
- package/dist/native/data-display/FleetMap.js +498 -0
- package/dist/native/data-display/FleetMap.js.map +1 -0
- package/dist/native/data-display/LoadingState.js +3 -3
- package/dist/native/feedback/Dialog.js +3 -3
- package/dist/native/form/Checkbox.js +1 -1
- package/dist/native/form/Input.js +1 -1
- package/dist/native/form/Select.js +1 -1
- package/dist/native/form/Switch.js +1 -1
- package/dist/native/layout/Card.js +3 -3
- package/dist/native/layout/Skeleton.js +6 -209
- package/dist/native/layout/Skeleton.js.map +1 -1
- package/dist/navigation/AppHeader.js +6 -6
- package/dist/navigation/Pagination.js +2 -2
- package/dist/pt-BR.js +4 -4
- package/dist/types/components/data-display/FleetMap/FleetBody.d.ts +22 -0
- package/dist/types/components/data-display/FleetMap/FleetBody.d.ts.map +1 -0
- package/dist/types/components/data-display/FleetMap/FleetCanvas.d.ts +50 -0
- package/dist/types/components/data-display/FleetMap/FleetCanvas.d.ts.map +1 -0
- package/dist/types/components/data-display/FleetMap/FleetMap.base.d.ts +141 -0
- package/dist/types/components/data-display/FleetMap/FleetMap.base.d.ts.map +1 -0
- package/dist/types/components/data-display/FleetMap/FleetMap.d.ts +32 -0
- package/dist/types/components/data-display/FleetMap/FleetMap.d.ts.map +1 -0
- package/dist/types/components/data-display/FleetMap/FleetMap.fixtures.d.ts +23 -0
- package/dist/types/components/data-display/FleetMap/FleetMap.fixtures.d.ts.map +1 -0
- package/dist/types/components/data-display/FleetMap/FleetMap.helpers.d.ts +45 -0
- package/dist/types/components/data-display/FleetMap/FleetMap.helpers.d.ts.map +1 -0
- package/dist/types/components/data-display/FleetMap/FleetMap.hooks.d.ts +71 -0
- package/dist/types/components/data-display/FleetMap/FleetMap.hooks.d.ts.map +1 -0
- package/dist/types/components/data-display/FleetMap/FleetMap.metrics.d.ts +76 -0
- package/dist/types/components/data-display/FleetMap/FleetMap.metrics.d.ts.map +1 -0
- package/dist/types/components/data-display/FleetMap/FleetMap.types.d.ts +45 -0
- package/dist/types/components/data-display/FleetMap/FleetMap.types.d.ts.map +1 -0
- package/dist/types/components/data-display/FleetMap/FleetRoster.d.ts +15 -0
- package/dist/types/components/data-display/FleetMap/FleetRoster.d.ts.map +1 -0
- package/dist/types/components/data-display/FleetMap/index.d.ts +5 -0
- package/dist/types/components/data-display/FleetMap/index.d.ts.map +1 -0
- package/dist/types/components/data-display/index.d.ts +1 -0
- package/dist/types/components/data-display/index.d.ts.map +1 -1
- package/dist/types/platform/aria.d.ts +13 -0
- package/dist/types/platform/aria.d.ts.map +1 -1
- package/dist/types-native/components/data-display/FleetMap/FleetMap.base.d.ts +141 -0
- package/dist/types-native/components/data-display/FleetMap/FleetMap.base.d.ts.map +1 -0
- package/dist/types-native/components/data-display/FleetMap/FleetMap.helpers.d.ts +45 -0
- package/dist/types-native/components/data-display/FleetMap/FleetMap.helpers.d.ts.map +1 -0
- package/dist/types-native/components/data-display/FleetMap/FleetMap.hooks.d.ts +71 -0
- package/dist/types-native/components/data-display/FleetMap/FleetMap.hooks.d.ts.map +1 -0
- package/dist/types-native/components/data-display/FleetMap/FleetMap.metrics.d.ts +76 -0
- package/dist/types-native/components/data-display/FleetMap/FleetMap.metrics.d.ts.map +1 -0
- package/dist/types-native/components/data-display/FleetMap/FleetMap.native.d.ts +30 -0
- package/dist/types-native/components/data-display/FleetMap/FleetMap.native.d.ts.map +1 -0
- package/dist/types-native/components/data-display/FleetMap/FleetMap.types.native.d.ts +36 -0
- package/dist/types-native/components/data-display/FleetMap/FleetMap.types.native.d.ts.map +1 -0
- package/dist/types-native/components/data-display/FleetMap/FleetRoster.native.d.ts +17 -0
- package/dist/types-native/components/data-display/FleetMap/FleetRoster.native.d.ts.map +1 -0
- package/dist/types-native/components/data-display/FleetMap/index.native.d.ts +4 -0
- package/dist/types-native/components/data-display/FleetMap/index.native.d.ts.map +1 -0
- package/dist/types-native/platform/aria.d.ts +13 -0
- package/dist/types-native/platform/aria.d.ts.map +1 -1
- package/dist/user-avatar.js +5 -5
- package/package.json +9 -1
- package/src/components/data-display/FleetMap/FleetBody.tsx +96 -0
- package/src/components/data-display/FleetMap/FleetCanvas.tsx +80 -0
- package/src/components/data-display/FleetMap/FleetMap.base.ts +144 -0
- package/src/components/data-display/FleetMap/FleetMap.fixtures.ts +70 -0
- package/src/components/data-display/FleetMap/FleetMap.helpers.ts +85 -0
- package/src/components/data-display/FleetMap/FleetMap.hooks.ts +183 -0
- package/src/components/data-display/FleetMap/FleetMap.md +189 -0
- package/src/components/data-display/FleetMap/FleetMap.metrics.ts +81 -0
- package/src/components/data-display/FleetMap/FleetMap.native.tsx +185 -0
- package/src/components/data-display/FleetMap/FleetMap.tsx +163 -0
- package/src/components/data-display/FleetMap/FleetMap.types.native.ts +44 -0
- package/src/components/data-display/FleetMap/FleetMap.types.ts +53 -0
- package/src/components/data-display/FleetMap/FleetRoster.native.tsx +382 -0
- package/src/components/data-display/FleetMap/FleetRoster.tsx +327 -0
- package/src/components/data-display/FleetMap/NATIVE-NOTES.md +14 -0
- package/src/components/data-display/FleetMap/index.native.ts +10 -0
- package/src/components/data-display/FleetMap/index.ts +9 -0
- package/src/components/data-display/FleetMap/tests.md +84 -0
- package/src/components/data-display/FleetMap/track.md +50 -0
- package/src/components/data-display/index.ts +1 -0
- package/src/components/feedback/ConfirmAction/ConfirmAction.md +10 -4
- package/src/platform/aria.ts +13 -0
- package/dist/chunk-X3HL5C6H.js +0 -1
- package/dist/native/chunks/chunk-T6BJPP3E.js.map +0 -1
- /package/dist/{chunk-X3HL5C6H.js.map → _internal/components/data-display/FleetMap/FleetBody.js.map} +0 -0
- /package/dist/{chunk-WGRS4PGY.js.map → chunk-2C47DEVU.js.map} +0 -0
- /package/dist/{chunk-WMALDOF5.js.map → chunk-4G3FGCWX.js.map} +0 -0
- /package/dist/{chunk-ZOUWHGV3.js.map → chunk-4MGFUPQT.js.map} +0 -0
- /package/dist/{chunk-QFJRB27Q.js.map → chunk-6LGWJFQK.js.map} +0 -0
- /package/dist/{chunk-XSF6AXEU.js.map → chunk-6RZDDUSM.js.map} +0 -0
- /package/dist/{chunk-VIK3HAUQ.js.map → chunk-73NJCGQG.js.map} +0 -0
- /package/dist/{chunk-UU3QAKG6.js.map → chunk-B7DKSBAE.js.map} +0 -0
- /package/dist/{chunk-3FLVIVOK.js.map → chunk-BHIGHSXA.js.map} +0 -0
- /package/dist/{chunk-HKV4MQ2W.js.map → chunk-BLBOYCOV.js.map} +0 -0
- /package/dist/{chunk-PFV6RL4Q.js.map → chunk-C6KTQF4F.js.map} +0 -0
- /package/dist/{chunk-LYSSO6WC.js.map → chunk-CZPU5TW3.js.map} +0 -0
- /package/dist/{chunk-PMS3H3OO.js.map → chunk-DF2FFYZ7.js.map} +0 -0
- /package/dist/{chunk-QZBUGUOY.js.map → chunk-DFLTUI7W.js.map} +0 -0
- /package/dist/{chunk-WLQTHPMX.js.map → chunk-EJQW2IYW.js.map} +0 -0
- /package/dist/{chunk-VQAM2XZV.js.map → chunk-EMUBMZMA.js.map} +0 -0
- /package/dist/{chunk-XFUNJYFT.js.map → chunk-EPNUVSNK.js.map} +0 -0
- /package/dist/{chunk-MSUSZLRE.js.map → chunk-EWT3WMVY.js.map} +0 -0
- /package/dist/{chunk-Q75SJ35X.js.map → chunk-GMTM6EN7.js.map} +0 -0
- /package/dist/{chunk-R6SBP5M5.js.map → chunk-GOWBYHYW.js.map} +0 -0
- /package/dist/{chunk-6NZTYZP5.js.map → chunk-GVA5CYKP.js.map} +0 -0
- /package/dist/{chunk-VR5HDUBK.js.map → chunk-HP2ODXNA.js.map} +0 -0
- /package/dist/{chunk-2MMEIN4T.js.map → chunk-I3E2QHTH.js.map} +0 -0
- /package/dist/{chunk-UVGRRI7N.js.map → chunk-IPVYEPXO.js.map} +0 -0
- /package/dist/{chunk-5ZTUP65G.js.map → chunk-JM64OEU6.js.map} +0 -0
- /package/dist/{chunk-7BYFC2MR.js.map → chunk-JUHUKZGU.js.map} +0 -0
- /package/dist/{chunk-DVUPIVI2.js.map → chunk-KT6HWAEN.js.map} +0 -0
- /package/dist/{chunk-TLMLEC4F.js.map → chunk-KW7H5O3K.js.map} +0 -0
- /package/dist/{chunk-VLHGBFR6.js.map → chunk-MZQTHSLN.js.map} +0 -0
- /package/dist/{chunk-4SQFO4GW.js.map → chunk-NA7MIEG6.js.map} +0 -0
- /package/dist/{chunk-QWDEOITU.js.map → chunk-O4VCZTK4.js.map} +0 -0
- /package/dist/{chunk-LS4FMSHY.js.map → chunk-OAYX4XN7.js.map} +0 -0
- /package/dist/{chunk-Y5UCOBQN.js.map → chunk-OGGHD64H.js.map} +0 -0
- /package/dist/{chunk-7TZ6OC5T.js.map → chunk-OH3YGJT5.js.map} +0 -0
- /package/dist/{chunk-REMK55JI.js.map → chunk-OLZTGVBT.js.map} +0 -0
- /package/dist/{chunk-VNNG46TS.js.map → chunk-ORSLFKKZ.js.map} +0 -0
- /package/dist/{chunk-ZIYMBR7K.js.map → chunk-OTH7H3IP.js.map} +0 -0
- /package/dist/{chunk-5NKUYCZD.js.map → chunk-PN5CQKA2.js.map} +0 -0
- /package/dist/{chunk-J4W4TQPC.js.map → chunk-QJRDDVZ2.js.map} +0 -0
- /package/dist/{chunk-VNDOQEJ7.js.map → chunk-QRX5BYUQ.js.map} +0 -0
- /package/dist/{chunk-XT7HMNBW.js.map → chunk-R6SNYC3D.js.map} +0 -0
- /package/dist/{chunk-W2UTDN4T.js.map → chunk-RBGOGTT2.js.map} +0 -0
- /package/dist/{chunk-X5MPHXG5.js.map → chunk-RPWEXIAW.js.map} +0 -0
- /package/dist/{chunk-EXJBPKGW.js.map → chunk-SHDIZSN5.js.map} +0 -0
- /package/dist/{chunk-23FCBIUN.js.map → chunk-TNVO2DTB.js.map} +0 -0
- /package/dist/{chunk-AGSBIUPV.js.map → chunk-TPLKJHNN.js.map} +0 -0
- /package/dist/{chunk-YQ6TNHAC.js.map → chunk-TTLI6PF7.js.map} +0 -0
- /package/dist/{chunk-U2AC4AQX.js.map → chunk-TU5EIKJA.js.map} +0 -0
- /package/dist/{chunk-KTK4A3IF.js.map → chunk-TX5V42AY.js.map} +0 -0
- /package/dist/{chunk-BGYLL467.js.map → chunk-TXHJX2NG.js.map} +0 -0
- /package/dist/{chunk-FL54G7PV.js.map → chunk-WIT3Y7JT.js.map} +0 -0
- /package/dist/{chunk-W7Y6RA6W.js.map → chunk-WR5JFDQ5.js.map} +0 -0
- /package/dist/{chunk-2UYR2OOE.js.map → chunk-YRUCIKPO.js.map} +0 -0
- /package/dist/{chunk-AEGXHZQT.js.map → chunk-YW5T6KZ5.js.map} +0 -0
- /package/dist/{chunk-QKIJ6YXZ.js.map → chunk-ZA4U5GDU.js.map} +0 -0
- /package/dist/{chunk-ZKR5B6YJ.js.map → chunk-ZRPGVHTR.js.map} +0 -0
- /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
|
+
}
|