@ghxstship/xos-ui 0.0.0-stage → 4.16.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 (487) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +32 -2
  3. package/package.json +76 -4
  4. package/scripts/generate-stories.mjs +85 -0
  5. package/src/AIDisclosureBadge/README.md +7 -0
  6. package/src/AccessGridMatrix/README.md +3 -0
  7. package/src/Accordion/README.md +7 -0
  8. package/src/ActivityFeedItem/README.md +3 -0
  9. package/src/AgencySlate/README.md +3 -0
  10. package/src/AppShell/README.md +5 -0
  11. package/src/ApplicationForm/README.md +3 -0
  12. package/src/AssertionRankBadge/README.md +5 -0
  13. package/src/AvailabilityCalendar/README.md +3 -0
  14. package/src/Avatar/Avatar.tsx +56 -0
  15. package/src/Avatar/README.md +3 -0
  16. package/src/Avatar/avatar-class.ts +19 -0
  17. package/src/AvatarStack/README.md +3 -0
  18. package/src/Badge/README.md +3 -0
  19. package/src/Banner/README.md +3 -0
  20. package/src/BidSheet/README.md +3 -0
  21. package/src/Board/README.md +6 -0
  22. package/src/BrandMark/BrandMark.tsx +95 -0
  23. package/src/BrandMark/README.md +7 -0
  24. package/src/BrandSlot/README.md +6 -0
  25. package/src/Breadcrumbs/README.md +3 -0
  26. package/src/BudgetGrid/README.md +10 -0
  27. package/src/BulkActionBar/README.md +3 -0
  28. package/src/Button/Button.tsx +100 -0
  29. package/src/Button/README.md +10 -0
  30. package/src/Button/button-class.ts +54 -0
  31. package/src/ButtonLink/README.md +7 -0
  32. package/src/Calendar/README.md +3 -0
  33. package/src/Card/README.md +3 -0
  34. package/src/Chart/README.md +9 -0
  35. package/src/Charts/Calendar.tsx +228 -0
  36. package/src/Charts/Chart.tsx +547 -0
  37. package/src/Charts/Charts.tsx +20 -0
  38. package/src/Charts/Charts.types.ts +40 -0
  39. package/src/Charts/Gallery.tsx +25 -0
  40. package/src/Charts/Heatmap.tsx +116 -0
  41. package/src/Charts/OrgChart.tsx +53 -0
  42. package/src/Charts/ResourceSchedule.tsx +91 -0
  43. package/src/Charts/Sparkline.tsx +59 -0
  44. package/src/Charts/Timeline.tsx +777 -0
  45. package/src/Charts/categories.ts +21 -0
  46. package/src/Charts/timeline-plan.ts +191 -0
  47. package/src/Checkbox/README.md +6 -0
  48. package/src/ChecklistRunner/README.md +3 -0
  49. package/src/Chips/Badge.tsx +27 -0
  50. package/src/Chips/Chips.shared.tsx +34 -0
  51. package/src/Chips/Chips.tsx +21 -0
  52. package/src/Chips/Chips.types.ts +35 -0
  53. package/src/Chips/DepartmentChip.tsx +16 -0
  54. package/src/Chips/PhaseChip.tsx +16 -0
  55. package/src/Chips/PropertyChip.tsx +15 -0
  56. package/src/Chips/RecordKindChip.tsx +14 -0
  57. package/src/Chips/StateChip.tsx +19 -0
  58. package/src/Chips/Tag.tsx +47 -0
  59. package/src/Chips/URIDChip.tsx +14 -0
  60. package/src/Chips/chipClass.tsx +8 -0
  61. package/src/Chips/codeClass.tsx +6 -0
  62. package/src/ClockButton/README.md +6 -0
  63. package/src/CodeSample/README.md +7 -0
  64. package/src/ColorPicker/README.md +5 -0
  65. package/src/Combobox/README.md +7 -0
  66. package/src/CommandMenu/CommandMenu.tsx +179 -0
  67. package/src/CommandMenu/README.md +5 -0
  68. package/src/ComparisonTable/README.md +9 -0
  69. package/src/Compass/ChecklistRunner.tsx +128 -0
  70. package/src/Compass/ClockButton.tsx +57 -0
  71. package/src/Compass/Compass.shared.tsx +58 -0
  72. package/src/Compass/Compass.tsx +26 -0
  73. package/src/Compass/Compass.types.ts +79 -0
  74. package/src/Compass/DaySheetView.tsx +104 -0
  75. package/src/Compass/ForceUpdateScreen.tsx +29 -0
  76. package/src/Compass/LiveActivity.tsx +60 -0
  77. package/src/Compass/OfflineBanner.tsx +31 -0
  78. package/src/Compass/PhotoCapture.tsx +69 -0
  79. package/src/Compass/QuickIncidentSheet.tsx +46 -0
  80. package/src/Compass/ScanSheet.tsx +83 -0
  81. package/src/Compass/ShiftCard.tsx +55 -0
  82. package/src/Compass/SignaturePad.tsx +71 -0
  83. package/src/Compass/SyncConflictSheet.tsx +60 -0
  84. package/src/ConsentBanner/ConsentBanner.tsx +210 -0
  85. package/src/ConsentBanner/ConsentBannerStatic.tsx +163 -0
  86. package/src/ConsentBanner/ConsentSettings.tsx +213 -0
  87. package/src/ConsentBanner/README.md +101 -0
  88. package/src/ConsentBanner/consent.ts +238 -0
  89. package/src/ConsentBanner/index.ts +40 -0
  90. package/src/ConsentBanner/static.ts +11 -0
  91. package/src/ConsentBanner/useConsent.ts +160 -0
  92. package/src/ContextMenu/README.md +3 -0
  93. package/src/CoordinateMatrix/README.md +6 -0
  94. package/src/CredentialBadge/README.md +7 -0
  95. package/src/CurrencyInput/README.md +7 -0
  96. package/src/DataTable/DataTable.tsx +1034 -0
  97. package/src/DataTable/DataTableResizer.tsx +72 -0
  98. package/src/DataTable/DataTableVirtualizer.tsx +58 -0
  99. package/src/DataTable/README.md +9 -0
  100. package/src/DatePicker/README.md +5 -0
  101. package/src/DateRangePicker/README.md +3 -0
  102. package/src/DaySheetView/README.md +3 -0
  103. package/src/DepartmentChip/README.md +3 -0
  104. package/src/DepartmentGlyph/README.md +18 -0
  105. package/src/Dialog/README.md +3 -0
  106. package/src/DiffViewer/README.md +3 -0
  107. package/src/Divider/README.md +3 -0
  108. package/src/Drawer/README.md +3 -0
  109. package/src/DropdownMenu/README.md +3 -0
  110. package/src/EPKViewer/README.md +3 -0
  111. package/src/Editing/ColorPicker.tsx +125 -0
  112. package/src/Editing/DiffViewer.tsx +49 -0
  113. package/src/Editing/Editing.tsx +29 -0
  114. package/src/Editing/Editing.types.ts +71 -0
  115. package/src/Editing/FilterBuilder.tsx +72 -0
  116. package/src/Editing/QuickAdd.tsx +102 -0
  117. package/src/Editing/QuickAddChips.tsx +180 -0
  118. package/src/Editing/contrastRatio.tsx +11 -0
  119. package/src/Editing/parseQuickAdd.tsx +187 -0
  120. package/src/Editing/quickAddCore.ts +204 -0
  121. package/src/Editing/timeShorthand.ts +157 -0
  122. package/src/Editing/useQuickAdd.ts +201 -0
  123. package/src/EmergencyCodeCard/README.md +10 -0
  124. package/src/EmptyState/README.md +5 -0
  125. package/src/EngagementTimeline/README.md +3 -0
  126. package/src/ErrorState/README.md +3 -0
  127. package/src/FAQ/README.md +7 -0
  128. package/src/FeatureGrid/README.md +7 -0
  129. package/src/Feedback/Banner.tsx +40 -0
  130. package/src/Feedback/EmptyState.tsx +32 -0
  131. package/src/Feedback/ErrorState.tsx +32 -0
  132. package/src/Feedback/Feedback.shared.tsx +38 -0
  133. package/src/Feedback/Feedback.tsx +18 -0
  134. package/src/Feedback/Feedback.types.ts +40 -0
  135. package/src/Feedback/InlineAlert.tsx +19 -0
  136. package/src/Feedback/RefusalNotice.tsx +46 -0
  137. package/src/Feedback/RegionBoundary.tsx +102 -0
  138. package/src/Feedback/Toast.tsx +31 -0
  139. package/src/Feedback/ToastRegion.tsx +26 -0
  140. package/src/Feedback/UndoToast.tsx +97 -0
  141. package/src/FeedbackWidget/README.md +3 -0
  142. package/src/Field/Field.tsx +81 -0
  143. package/src/FileDrop/README.md +3 -0
  144. package/src/FilePreviewer/README.md +3 -0
  145. package/src/FilterBuilder/README.md +3 -0
  146. package/src/ForceUpdateScreen/README.md +3 -0
  147. package/src/Forms/Checkbox.tsx +14 -0
  148. package/src/Forms/CurrencyInput.tsx +40 -0
  149. package/src/Forms/Forms.shared.tsx +5 -0
  150. package/src/Forms/Forms.tsx +20 -0
  151. package/src/Forms/Forms.types.ts +33 -0
  152. package/src/Forms/Input.tsx +28 -0
  153. package/src/Forms/Money.tsx +15 -0
  154. package/src/Forms/NumberInput.tsx +85 -0
  155. package/src/Forms/Radio.tsx +31 -0
  156. package/src/Forms/Select.tsx +54 -0
  157. package/src/Forms/Switch.tsx +14 -0
  158. package/src/Forms/Textarea.tsx +30 -0
  159. package/src/Gallery/README.md +5 -0
  160. package/src/GateLifecycleRail/README.md +7 -0
  161. package/src/GateReadinessPanel/README.md +8 -0
  162. package/src/Gateway/AgencySlate.tsx +76 -0
  163. package/src/Gateway/ApplicationForm.tsx +48 -0
  164. package/src/Gateway/AvailabilityCalendar.tsx +82 -0
  165. package/src/Gateway/EPKViewer.tsx +77 -0
  166. package/src/Gateway/EngagementTimeline.tsx +43 -0
  167. package/src/Gateway/Gateway.shared.tsx +5 -0
  168. package/src/Gateway/Gateway.tsx +30 -0
  169. package/src/Gateway/Gateway.types.ts +69 -0
  170. package/src/Gateway/OnboardingPacket.tsx +61 -0
  171. package/src/Gateway/OpportunityCard.tsx +73 -0
  172. package/src/Gateway/OpportunityFilters.tsx +64 -0
  173. package/src/Gateway/PaymentTracker.tsx +71 -0
  174. package/src/Gateway/PayoutDetailsForm.tsx +62 -0
  175. package/src/Gateway/ProfileEditor.tsx +45 -0
  176. package/src/Gateway/ProgressBar.tsx +30 -0
  177. package/src/Gateway/RatingDialog.tsx +83 -0
  178. package/src/Gateway/UpNextCard.tsx +76 -0
  179. package/src/Glyphs/Glyphs.tsx +66 -0
  180. package/src/Grids/BidSheet.tsx +63 -0
  181. package/src/Grids/BudgetGrid.tsx +164 -0
  182. package/src/Grids/Grids.shared.tsx +5 -0
  183. package/src/Grids/Grids.tsx +11 -0
  184. package/src/Grids/Grids.types.ts +38 -0
  185. package/src/Grids/SpreadsheetGrid.tsx +445 -0
  186. package/src/Grids/columnTotal.tsx +14 -0
  187. package/src/Grids/glForCode.tsx +11 -0
  188. package/src/Grids/parseCell.tsx +16 -0
  189. package/src/Help/FeedbackWidget.tsx +89 -0
  190. package/src/Help/Help.tsx +14 -0
  191. package/src/Help/Help.types.ts +51 -0
  192. package/src/Help/HelpPanel.tsx +116 -0
  193. package/src/Help/NotificationCenter.tsx +100 -0
  194. package/src/Help/OnboardingChecklist.tsx +57 -0
  195. package/src/Help/ShortcutEditor.tsx +81 -0
  196. package/src/Help/WhatsNew.tsx +33 -0
  197. package/src/HelpPanel/README.md +5 -0
  198. package/src/HoverCard/README.md +3 -0
  199. package/src/Icon/Icon.tsx +47 -0
  200. package/src/Icon/README.md +9 -0
  201. package/src/IconButton/README.md +6 -0
  202. package/src/Identity/BrandSlot.tsx +20 -0
  203. package/src/Identity/BrandSlotFrame.tsx +38 -0
  204. package/src/Identity/CredentialBadge.tsx +92 -0
  205. package/src/Identity/Identity.tsx +14 -0
  206. package/src/Identity/Identity.types.ts +25 -0
  207. package/src/Identity/PresenceIndicator.tsx +37 -0
  208. package/src/Identity/QRCode.tsx +22 -0
  209. package/src/Identity/WhiteLabelPreview.tsx +33 -0
  210. package/src/Identity/qrPath.tsx +16 -0
  211. package/src/Identity/tokenVariable.tsx +9 -0
  212. package/src/InlineAlert/README.md +3 -0
  213. package/src/Input/README.md +8 -0
  214. package/src/Kbd/Kbd.tsx +30 -0
  215. package/src/Kbd/README.md +6 -0
  216. package/src/Layout/AppShell.tsx +151 -0
  217. package/src/Layout/Layout.tsx +14 -0
  218. package/src/Layout/Layout.types.ts +43 -0
  219. package/src/Layout/PageHeader.tsx +22 -0
  220. package/src/Layout/ResizablePanel.tsx +81 -0
  221. package/src/Layout/ScrollRegion.tsx +107 -0
  222. package/src/Layout/SplitView.tsx +78 -0
  223. package/src/Layout/Stepper.tsx +39 -0
  224. package/src/LegalText/LegalText.tsx +289 -0
  225. package/src/LegalText/README.md +10 -0
  226. package/src/LegalText/index.ts +29 -0
  227. package/src/LegalText/legal-markdown.ts +581 -0
  228. package/src/Link/Link.tsx +25 -0
  229. package/src/Link/README.md +5 -0
  230. package/src/Link/link-class.ts +10 -0
  231. package/src/LiveActivity/README.md +3 -0
  232. package/src/LogoSlotWall/README.md +7 -0
  233. package/src/MapView/README.md +5 -0
  234. package/src/Marketing/AIDisclosureBadge.tsx +31 -0
  235. package/src/Marketing/ButtonLink.tsx +32 -0
  236. package/src/Marketing/CodeSample.tsx +47 -0
  237. package/src/Marketing/ComparisonScroll.tsx +7 -0
  238. package/src/Marketing/ComparisonTable.tsx +104 -0
  239. package/src/Marketing/FAQ.tsx +26 -0
  240. package/src/Marketing/FeatureGrid.tsx +65 -0
  241. package/src/Marketing/GateLifecycleRail.tsx +52 -0
  242. package/src/Marketing/LogoSlotWall.tsx +53 -0
  243. package/src/Marketing/Marketing.tsx +49 -0
  244. package/src/Marketing/Marketing.types.ts +64 -0
  245. package/src/Marketing/MarketingFooter.tsx +88 -0
  246. package/src/Marketing/MarketingHero.tsx +45 -0
  247. package/src/Marketing/MarketingNav.tsx +76 -0
  248. package/src/Marketing/MarketingPoints.tsx +16 -0
  249. package/src/Marketing/MarketingSection.tsx +52 -0
  250. package/src/Marketing/PersonaSwitcher.tsx +137 -0
  251. package/src/Marketing/PriceDisplay.tsx +56 -0
  252. package/src/Marketing/PricingTable.tsx +122 -0
  253. package/src/Marketing/ProofStatStrip.tsx +35 -0
  254. package/src/Marketing/ScreenshotFrame.tsx +56 -0
  255. package/src/Marketing/SurfaceTriptych.tsx +43 -0
  256. package/src/Marketing/Testimonial.tsx +45 -0
  257. package/src/Marketing/marketing-copy.ts +25 -0
  258. package/src/Marketing/provided.tsx +36 -0
  259. package/src/MarketingFooter/README.md +7 -0
  260. package/src/MarketingHero/README.md +7 -0
  261. package/src/MarketingNav/README.md +7 -0
  262. package/src/MarketingSection/README.md +7 -0
  263. package/src/Media/FilePreviewer.tsx +86 -0
  264. package/src/Media/MapView.tsx +200 -0
  265. package/src/Media/Media.tsx +11 -0
  266. package/src/Media/Media.types.ts +47 -0
  267. package/src/Media/RichTextEditor.tsx +198 -0
  268. package/src/Media/toLngLat.tsx +13 -0
  269. package/src/Menus/ContextMenu.tsx +106 -0
  270. package/src/Menus/DropdownMenu.tsx +51 -0
  271. package/src/Menus/Menus.shared.tsx +67 -0
  272. package/src/Menus/Menus.tsx +13 -0
  273. package/src/Menus/Menus.types.ts +49 -0
  274. package/src/Menus/OrgSwitcher.tsx +69 -0
  275. package/src/Menus/compactTriggerClass.tsx +7 -0
  276. package/src/Menus/menuClass.tsx +4 -0
  277. package/src/Menus/menuItemClass.tsx +7 -0
  278. package/src/Menus/orgSwitcherTriggerClass.tsx +7 -0
  279. package/src/Money/README.md +6 -0
  280. package/src/MultiSelect/README.md +3 -0
  281. package/src/Navigation/Accordion.tsx +59 -0
  282. package/src/Navigation/Breadcrumbs.tsx +59 -0
  283. package/src/Navigation/MoreFields.tsx +42 -0
  284. package/src/Navigation/Navigation.shared.tsx +32 -0
  285. package/src/Navigation/Navigation.tsx +21 -0
  286. package/src/Navigation/Navigation.types.ts +63 -0
  287. package/src/Navigation/Pagination.tsx +30 -0
  288. package/src/Navigation/SegmentedControl.tsx +87 -0
  289. package/src/Navigation/Sidebar.tsx +106 -0
  290. package/src/Navigation/TabBar.tsx +61 -0
  291. package/src/Navigation/Tabs.tsx +79 -0
  292. package/src/Navigation/TopNav.tsx +86 -0
  293. package/src/Navigation/sidebar-classes.ts +18 -0
  294. package/src/NotificationCenter/README.md +3 -0
  295. package/src/NumberInput/README.md +3 -0
  296. package/src/OfflineBanner/README.md +3 -0
  297. package/src/OnboardingChecklist/README.md +3 -0
  298. package/src/OnboardingPacket/README.md +3 -0
  299. package/src/OpportunityCard/README.md +5 -0
  300. package/src/OpportunityFilters/README.md +3 -0
  301. package/src/OrgChart/README.md +5 -0
  302. package/src/OrgSwitcher/README.md +3 -0
  303. package/src/Overlays/Dialog.tsx +54 -0
  304. package/src/Overlays/Drawer.tsx +62 -0
  305. package/src/Overlays/HoverCard.tsx +59 -0
  306. package/src/Overlays/Overlays.shared.tsx +12 -0
  307. package/src/Overlays/Overlays.tsx +20 -0
  308. package/src/Overlays/Overlays.types.ts +66 -0
  309. package/src/Overlays/Popover.tsx +70 -0
  310. package/src/Overlays/PropertyList.tsx +19 -0
  311. package/src/Overlays/ShortcutSheet.tsx +62 -0
  312. package/src/Overlays/SidePeek.tsx +117 -0
  313. package/src/Overlays/Tooltip.tsx +73 -0
  314. package/src/Overlays/panelClass.tsx +6 -0
  315. package/src/Overlays/popoverPanelClass.tsx +6 -0
  316. package/src/PageHeader/README.md +3 -0
  317. package/src/Pagination/README.md +3 -0
  318. package/src/PaymentTracker/README.md +3 -0
  319. package/src/PayoutDetailsForm/README.md +3 -0
  320. package/src/PersonaSwitcher/README.md +7 -0
  321. package/src/PhaseChip/README.md +5 -0
  322. package/src/PhaseGlyph/README.md +17 -0
  323. package/src/PhotoCapture/README.md +3 -0
  324. package/src/Pickers/Combobox.tsx +91 -0
  325. package/src/Pickers/DatePicker.tsx +60 -0
  326. package/src/Pickers/DateRangePicker.tsx +78 -0
  327. package/src/Pickers/FileDrop.tsx +84 -0
  328. package/src/Pickers/MultiSelect.tsx +118 -0
  329. package/src/Pickers/Pickers.shared.tsx +337 -0
  330. package/src/Pickers/Pickers.tsx +17 -0
  331. package/src/Pickers/Pickers.types.ts +47 -0
  332. package/src/Pickers/Slider.tsx +59 -0
  333. package/src/Pickers/TimePicker.tsx +68 -0
  334. package/src/Pickers/rankOptions.tsx +12 -0
  335. package/src/Popover/README.md +3 -0
  336. package/src/PresenceIndicator/README.md +3 -0
  337. package/src/PriceDisplay/README.md +7 -0
  338. package/src/PricingTable/README.md +9 -0
  339. package/src/ProfileEditor/README.md +3 -0
  340. package/src/ProgressBar/README.md +3 -0
  341. package/src/ProofStatStrip/README.md +7 -0
  342. package/src/PropertyChip/README.md +3 -0
  343. package/src/ProvenanceBadge/README.md +3 -0
  344. package/src/QRCode/README.md +6 -0
  345. package/src/QuickAdd/README.md +5 -0
  346. package/src/QuickIncidentSheet/README.md +3 -0
  347. package/src/Radio/README.md +3 -0
  348. package/src/RadioChannelTable/README.md +7 -0
  349. package/src/RatingDialog/README.md +3 -0
  350. package/src/ReconciliationTable/README.md +3 -0
  351. package/src/RecordKindChip/README.md +3 -0
  352. package/src/RecordKindGlyph/README.md +34 -0
  353. package/src/RecordRow/README.md +7 -0
  354. package/src/RefusalNotice/README.md +5 -0
  355. package/src/ResizablePanel/README.md +3 -0
  356. package/src/ResourceSchedule/README.md +6 -0
  357. package/src/RichTextEditor/README.md +3 -0
  358. package/src/RichTextEditor/extensions.ts +217 -0
  359. package/src/RunOfShowLive/README.md +5 -0
  360. package/src/ScanSheet/README.md +3 -0
  361. package/src/ScreenshotFrame/README.md +7 -0
  362. package/src/ScrollRegion/README.md +9 -0
  363. package/src/SegmentedControl/README.md +5 -0
  364. package/src/Select/README.md +5 -0
  365. package/src/ShiftCard/README.md +3 -0
  366. package/src/ShortcutEditor/README.md +3 -0
  367. package/src/ShortcutSheet/README.md +3 -0
  368. package/src/SidePeek/README.md +6 -0
  369. package/src/Sidebar/README.md +5 -0
  370. package/src/SignaturePad/README.md +3 -0
  371. package/src/Skeleton/README.md +3 -0
  372. package/src/Slider/README.md +3 -0
  373. package/src/SplitView/README.md +3 -0
  374. package/src/SpreadsheetGrid/README.md +9 -0
  375. package/src/StalenessIndicator/README.md +3 -0
  376. package/src/StatTile/README.md +6 -0
  377. package/src/StateChip/README.md +6 -0
  378. package/src/StateIcon/README.md +6 -0
  379. package/src/StateIcon/StateIcon.tsx +127 -0
  380. package/src/StatusHeatmap/README.md +15 -0
  381. package/src/Stepper/README.md +3 -0
  382. package/src/Surface/Skeleton.tsx +21 -0
  383. package/src/Surface/Surface.tsx +63 -0
  384. package/src/SurfaceTriptych/README.md +7 -0
  385. package/src/Switch/README.md +5 -0
  386. package/src/SyncConflictSheet/README.md +3 -0
  387. package/src/TabBar/README.md +7 -0
  388. package/src/Tabs/README.md +6 -0
  389. package/src/Tag/README.md +3 -0
  390. package/src/TemplateAuth/README.md +6 -0
  391. package/src/TemplateCollection/README.md +6 -0
  392. package/src/TemplateDashboard/README.md +8 -0
  393. package/src/TemplateDocument/README.md +5 -0
  394. package/src/TemplateGatewayShell/README.md +5 -0
  395. package/src/TemplateGridEditor/README.md +3 -0
  396. package/src/TemplateRecord/README.md +3 -0
  397. package/src/TemplateSettings/README.md +5 -0
  398. package/src/TemplateSplitView/README.md +3 -0
  399. package/src/TemplateSystem/README.md +5 -0
  400. package/src/TemplateWizard/README.md +3 -0
  401. package/src/Templates/TemplateAuth.tsx +16 -0
  402. package/src/Templates/TemplateCollection.tsx +35 -0
  403. package/src/Templates/TemplateDashboard.tsx +200 -0
  404. package/src/Templates/TemplateDocument.tsx +24 -0
  405. package/src/Templates/TemplateGatewayShell.tsx +36 -0
  406. package/src/Templates/TemplateGridEditor.tsx +17 -0
  407. package/src/Templates/TemplateRecord.tsx +31 -0
  408. package/src/Templates/TemplateSettings.tsx +26 -0
  409. package/src/Templates/TemplateSplitView.tsx +27 -0
  410. package/src/Templates/TemplateSystem.tsx +19 -0
  411. package/src/Templates/TemplateSystemLink.tsx +55 -0
  412. package/src/Templates/TemplateWizard.tsx +43 -0
  413. package/src/Templates/Templates.tsx +29 -0
  414. package/src/Templates/Templates.types.ts +153 -0
  415. package/src/Testimonial/README.md +7 -0
  416. package/src/Textarea/README.md +3 -0
  417. package/src/TimePicker/README.md +5 -0
  418. package/src/Timeline/README.md +6 -0
  419. package/src/Toast/README.md +3 -0
  420. package/src/Tooltip/README.md +3 -0
  421. package/src/TopNav/README.md +5 -0
  422. package/src/TreeView/README.md +3 -0
  423. package/src/URIDChip/README.md +5 -0
  424. package/src/UndoToast/README.md +3 -0
  425. package/src/UpNextCard/README.md +6 -0
  426. package/src/Views/ActivityFeedItem.tsx +52 -0
  427. package/src/Views/Board.tsx +229 -0
  428. package/src/Views/BulkActionBar.tsx +76 -0
  429. package/src/Views/RecordList.tsx +146 -0
  430. package/src/Views/RecordRow.tsx +132 -0
  431. package/src/Views/StatTile.tsx +88 -0
  432. package/src/Views/TreeView.tsx +161 -0
  433. package/src/Views/Views.tsx +18 -0
  434. package/src/Views/Views.types.ts +99 -0
  435. package/src/WhatsNew/README.md +3 -0
  436. package/src/WhiteLabelPreview/README.md +8 -0
  437. package/src/XOS/AccessGridMatrix.tsx +111 -0
  438. package/src/XOS/AssertionRankBadge.tsx +27 -0
  439. package/src/XOS/CoordinateMatrix.tsx +123 -0
  440. package/src/XOS/EmergencyCodeCard.tsx +109 -0
  441. package/src/XOS/GateReadinessPanel.tsx +100 -0
  442. package/src/XOS/ProvenanceBadge.tsx +19 -0
  443. package/src/XOS/RadioChannelTable.tsx +50 -0
  444. package/src/XOS/ReconciliationTable.tsx +64 -0
  445. package/src/XOS/RunOfShowLive.tsx +97 -0
  446. package/src/XOS/StalenessIndicator.tsx +31 -0
  447. package/src/XOS/StatusHeatmap.tsx +200 -0
  448. package/src/XOS/XOS.shared.tsx +90 -0
  449. package/src/XOS/XOS.tsx +29 -0
  450. package/src/XOS/XOS.types.ts +99 -0
  451. package/src/XOS/resolveStep.tsx +16 -0
  452. package/src/brand/defaults/README.md +13 -0
  453. package/src/brand/defaults/app-icon.svg +6 -0
  454. package/src/brand/defaults/favicon.svg +6 -0
  455. package/src/brand/defaults/logo-dark.svg +6 -0
  456. package/src/brand/defaults/logo-light.svg +6 -0
  457. package/src/brand/defaults/splash.svg +6 -0
  458. package/src/index.ts +606 -0
  459. package/src/lib/context.tsx +156 -0
  460. package/src/lib/cx.ts +4 -0
  461. package/src/lib/format.ts +200 -0
  462. package/src/lib/hooks.ts +187 -0
  463. package/src/lib/i18n.ts +97 -0
  464. package/src/lib/icons.ts +45 -0
  465. package/src/lib/live.ts +92 -0
  466. package/src/lib/preferences.ts +242 -0
  467. package/src/lib/route-transition.tsx +22 -0
  468. package/src/lib/selection.ts +92 -0
  469. package/src/lib/shortcut-targets.ts +125 -0
  470. package/src/lib/theme.ts +50 -0
  471. package/src/lib/undo-stack.ts +125 -0
  472. package/src/marketing.ts +33 -0
  473. package/src/pwa/action-outcome.tsx +81 -0
  474. package/src/pwa/analytics.ts +236 -0
  475. package/src/pwa/browser-push.tsx +132 -0
  476. package/src/pwa/client.ts +126 -0
  477. package/src/pwa/index.ts +40 -0
  478. package/src/pwa/offline-key.ts +18 -0
  479. package/src/pwa/service-worker.ts +229 -0
  480. package/src/static.ts +10 -0
  481. package/src/styles/base.css +208 -0
  482. package/src/styles/components.css +453 -0
  483. package/src/styles/index.css +16 -0
  484. package/src/styles/marketing.css +1260 -0
  485. package/src/styles/reference.css +5792 -0
  486. package/src/types/reference.d.ts +1608 -0
  487. package/tailwind.config.cjs +3 -0
@@ -0,0 +1,1034 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useLayoutEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ type ComponentType,
9
+ type CSSProperties,
10
+ type KeyboardEvent,
11
+ type MouseEvent,
12
+ type ReactElement,
13
+ type ReactNode,
14
+ type TouchEvent,
15
+ } from "react";
16
+ import type { ColumnSizingState, Updater, columnResizingState } from "@tanstack/react-table";
17
+ import { Direction } from "radix-ui";
18
+ import type {
19
+ DataTableColumn as ReferenceDataTableColumn,
20
+ DataTableProps as ReferenceDataTableProps,
21
+ RecordState,
22
+ } from "../types/reference";
23
+ import { cx } from "../lib/cx.ts";
24
+ import { useFormat, usePreferences, useT } from "../lib/context.tsx";
25
+ import { useNarrow } from "../lib/hooks.ts";
26
+ import { singleKeyShortcutsEnabled } from "../lib/preferences.ts";
27
+ import { ScrollRegion } from "../Layout/Layout.tsx";
28
+ import { Money } from "../Forms/Forms.tsx";
29
+ import { StateIcon } from "../StateIcon/StateIcon.tsx";
30
+ import type { DataTableResizer, StartResize } from "./DataTableResizer.tsx";
31
+ import type { DataTableVirtualizer, RowWindow } from "./DataTableVirtualizer.tsx";
32
+
33
+ export interface DataTableColumn extends ReferenceDataTableColumn {
34
+ /** Width in pixels; defaults by type. */
35
+ width?: number;
36
+ }
37
+
38
+ /**
39
+ * Row selection (Section 11.7): a checkbox column; click a box, Cmd or Ctrl+click a row, Shift+click
40
+ * for a range; X on the focused row and Shift+X for a range; Cmd or Ctrl+A selects every loaded row
41
+ * and Esc clears. The table only reports the selection; the page keeps it.
42
+ */
43
+ export interface DataTableSelection {
44
+ /** Ids of the selected rows (`rowId`). */
45
+ selected: ReadonlySet<string>;
46
+ onChange: (next: ReadonlySet<string>) => void;
47
+ /** The accessible name of a row's checkbox; "Select {first text cell}" by default. */
48
+ rowLabel?: (row: Record<string, unknown>, index: number) => string;
49
+ }
50
+
51
+ export type DataTableProps = Omit<ReferenceDataTableProps, "columns"> & {
52
+ columns: DataTableColumn[];
53
+ /** Rows can be selected; see DataTableSelection. */
54
+ selection?: DataTableSelection;
55
+ /** A row's stable id for selection; its `id` field, or its index, by default. */
56
+ rowId?: (row: Record<string, unknown>, index: number) => string;
57
+ /**
58
+ * Right-click, Shift+F10 or the Context Menu key on a row (Section 11.7). The point is the
59
+ * pointer's, or null from the keyboard, when the menu opens at the focused cell.
60
+ */
61
+ onRowContextMenu?: (
62
+ row: Record<string, unknown>,
63
+ index: number,
64
+ at: { x: number; y: number } | null,
65
+ cell: HTMLElement,
66
+ ) => void;
67
+ /** Names the scroll region; the caption by default. */
68
+ label?: string;
69
+ /** Column keys pinned to the logical start or end; they stay visible while the grid scrolls. */
70
+ pinStart?: string[];
71
+ pinEnd?: string[];
72
+ /** Columns resize by pointer or keyboard; on by default. */
73
+ resizable?: boolean;
74
+ /** Rows above which the body virtualizes; 100 by default. */
75
+ virtualizeAbove?: number;
76
+ /** Height of the scroll region when virtualized, as a CSS length; 60 percent of the viewport by default. */
77
+ height?: string;
78
+ /** Enter on a cell, or a double click, activates its row. */
79
+ onRowActivate?: (row: Record<string, unknown>, index: number) => void;
80
+ /**
81
+ * Below md the table becomes stacked cards with the three most important fields (Section 11.6):
82
+ * these column keys, in order, the first as the card's title. By default the title is the first
83
+ * text column among the first four (a name rather than a code) and the other two of the first
84
+ * three follow. Pass false to keep the grid.
85
+ */
86
+ cardFields?: string[] | false;
87
+ };
88
+
89
+ /** A column as the grid lays it out: its id and label, current width and pinned position. */
90
+ interface LaidColumn {
91
+ id: string;
92
+ label: string;
93
+ size: number;
94
+ pinned: "start" | "end" | false;
95
+ /** Offset from the logical start among start-pinned columns, or from the end among end-pinned ones. */
96
+ offset: number;
97
+ }
98
+
99
+ const NONE: string[] = [];
100
+
101
+ const MIN_WIDTH = 64;
102
+ const MAX_WIDTH = 640;
103
+ /** The selection checkbox column. */
104
+ const CHECK_WIDTH = 40;
105
+
106
+ const IDLE_RESIZE: columnResizingState = {
107
+ startOffset: null,
108
+ startSize: null,
109
+ deltaOffset: null,
110
+ deltaPercentage: null,
111
+ isResizingColumn: false,
112
+ columnSizingStart: [],
113
+ };
114
+
115
+ const RELEASE_EVENTS = ["mouseup", "touchend", "touchcancel"] as const;
116
+
117
+ type ResizerComponent = ComponentType<Parameters<typeof DataTableResizer>[0]>;
118
+
119
+ /** The pointer resize engine (TanStack Table) loads on the first approach to a resize handle. */
120
+ let resizerModule: Promise<{ DataTableResizer: typeof DataTableResizer }> | null = null;
121
+ const loadResizer = () => (resizerModule ??= import("./DataTableResizer.tsx"));
122
+
123
+ type VirtualizerComponent = ComponentType<Parameters<typeof DataTableVirtualizer>[0]>;
124
+
125
+ /** The window engine (TanStack Virtual) loads on the first interaction with a table over the limit. */
126
+ let virtualizerModule: Promise<{
127
+ DataTableVirtualizer: typeof DataTableVirtualizer;
128
+ }> | null = null;
129
+ const loadVirtualizer = () => (virtualizerModule ??= import("./DataTableVirtualizer.tsx"));
130
+
131
+ /** Rows rendered past each end of the visible ones. */
132
+ const OVERSCAN = 10;
133
+ /** The first screen's height in rows before the table has measured itself, as on the server. */
134
+ const INITIAL_SCREEN_ROWS = 20;
135
+ /** What reaches the engine first: a pointer over the table, a focus inside it, or a scroll. */
136
+ const WINDOW_INTERACTIONS = ["pointermove", "pointerdown", "focusin", "scroll"] as const;
137
+
138
+ /** The rows of the table's first screen and overscan, which TanStack Virtual renders at the top. */
139
+ function firstWindow(viewport: number, rowHeight: number, count: number): RowWindow {
140
+ return viewport === 0
141
+ ? { start: 0, end: -1 }
142
+ : { start: 0, end: Math.min(count - 1, Math.ceil(viewport / rowHeight) - 1 + OVERSCAN) };
143
+ }
144
+
145
+ function apply<T>(updater: Updater<T>, old: T): T {
146
+ return typeof updater === "function" ? (updater as (old: T) => T)(old) : updater;
147
+ }
148
+
149
+ const DEFAULT_WIDTH: Record<NonNullable<ReferenceDataTableColumn["type"]>, number> = {
150
+ text: 200,
151
+ code: 128,
152
+ number: 112,
153
+ money: 128,
154
+ state: 144,
155
+ };
156
+
157
+ const ROW_HEIGHT: Record<NonNullable<ReferenceDataTableProps["density"]>, number> = {
158
+ compact: 32,
159
+ default: 36,
160
+ comfortable: 44,
161
+ };
162
+
163
+ const ROW_CLASS: Record<NonNullable<ReferenceDataTableProps["density"]>, string> = {
164
+ compact: cx("h-row-compact"),
165
+ default: cx("h-row-default"),
166
+ comfortable: cx("h-row-comfortable"),
167
+ };
168
+
169
+ function isNumeric(column: ReferenceDataTableColumn): boolean {
170
+ return column.type === "money" || column.type === "number";
171
+ }
172
+
173
+ function isStateValue(value: unknown): value is { state: RecordState; label: string } {
174
+ return typeof value === "object" && value !== null && "state" in value && "label" in value;
175
+ }
176
+
177
+ function CellValue({
178
+ column,
179
+ value,
180
+ }: {
181
+ column: ReferenceDataTableColumn;
182
+ value: unknown;
183
+ }): ReactNode {
184
+ if (column.type === "money") {
185
+ return (
186
+ <Money
187
+ amount={typeof value === "number" ? value : null}
188
+ {...(column.currency ? { currency: column.currency } : {})}
189
+ />
190
+ );
191
+ }
192
+ if (column.type === "state") {
193
+ return isStateValue(value) ? (
194
+ <span className="inline-flex items-center gap-6 whitespace-nowrap">
195
+ <StateIcon state={value.state} />
196
+ {value.label}
197
+ </span>
198
+ ) : null;
199
+ }
200
+ if (value === null || value === undefined) return null;
201
+ if (column.type === "code")
202
+ return <code className="font-mono text-text-12">{String(value)}</code>;
203
+ if (column.type === "number") return <span className="tabular-nums">{String(value)}</span>;
204
+ return String(value);
205
+ }
206
+
207
+ /**
208
+ * The full text of a cell that may be cut off with an ellipsis, as its title (Section 15: a longer
209
+ * translation never hides text). Numbers never clip. A wrap column's cell carries the title too:
210
+ * cellClass still keeps it to one line, because cx only joins classes and its whitespace-nowrap
211
+ * wins over the column's whitespace-normal.
212
+ */
213
+ function cellTitle(
214
+ column: ReferenceDataTableColumn | undefined,
215
+ value: unknown,
216
+ ): string | undefined {
217
+ if (!column || column.type === "money" || column.type === "number") return undefined;
218
+ if (column.type === "state") return isStateValue(value) ? value.label : undefined;
219
+ return typeof value === "string" && value !== "" ? value : undefined;
220
+ }
221
+
222
+ function defaultWidth(column: DataTableColumn): number {
223
+ return column.width ?? (column.wrap ? 280 : DEFAULT_WIDTH[column.type ?? "text"]);
224
+ }
225
+
226
+ function pinStyle(column: LaidColumn): CSSProperties {
227
+ return {
228
+ inlineSize: column.size,
229
+ ...(column.pinned === "start" ? { position: "sticky", insetInlineStart: column.offset } : {}),
230
+ ...(column.pinned === "end" ? { position: "sticky", insetInlineEnd: column.offset } : {}),
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Lays the columns out as TanStack Table's sizing and pinning features do: a width is the resized
236
+ * width or the default, clamped to 64 to 640 px; start-pinned columns are offset from the logical
237
+ * start in pinning order, end-pinned ones from the end.
238
+ */
239
+ function layColumns(
240
+ columns: DataTableColumn[],
241
+ sizing: ColumnSizingState,
242
+ pinning: { start: string[]; end: string[] },
243
+ startOffset = 0,
244
+ ): LaidColumn[] {
245
+ const laid = columns.map((c): LaidColumn => {
246
+ const size = Object.hasOwn(sizing, c.key) ? sizing[c.key] : defaultWidth(c);
247
+ return {
248
+ id: c.key,
249
+ label: c.label,
250
+ size: Math.min(Math.max(MIN_WIDTH, size ?? 0), MAX_WIDTH),
251
+ pinned: pinning.start.includes(c.key) ? "start" : pinning.end.includes(c.key) ? "end" : false,
252
+ offset: 0,
253
+ };
254
+ });
255
+ const byId = new Map(laid.map((c) => [c.id, c]));
256
+ let start = startOffset;
257
+ for (const id of pinning.start) {
258
+ const c = byId.get(id);
259
+ if (c) {
260
+ c.offset = start;
261
+ start += c.size;
262
+ }
263
+ }
264
+ let after = 0;
265
+ for (const id of [...pinning.end].reverse()) {
266
+ const c = byId.get(id);
267
+ if (c) {
268
+ c.offset = after;
269
+ after += c.size;
270
+ }
271
+ }
272
+ return laid;
273
+ }
274
+
275
+ const cellClass = cx(
276
+ "px-12 border-b border-border-subtle whitespace-nowrap overflow-hidden text-ellipsis box-border",
277
+ );
278
+
279
+ /** What the cards and the grid need to show and change the selection. */
280
+ interface SelectApi {
281
+ checked: (index: number) => boolean;
282
+ /** Toggles a row, or with `range` selects every row from the last toggled one to it. */
283
+ toggle: (index: number, range: boolean) => void;
284
+ label: (row: Record<string, unknown>, index: number) => string;
285
+ }
286
+
287
+ /** The checkbox column stays at the logical start while the grid scrolls. */
288
+ const CHECK_STYLE: CSSProperties = {
289
+ inlineSize: CHECK_WIDTH,
290
+ position: "sticky",
291
+ insetInlineStart: 0,
292
+ };
293
+
294
+ /** A cell's value as plain text, for a row's accessible name. */
295
+ function plainText(value: unknown): string {
296
+ if (isStateValue(value)) return value.label;
297
+ return value === null || value === undefined ? "" : String(value);
298
+ }
299
+
300
+ /** A card's title (a plain-text column, so a name leads rather than a code) and two more fields. */
301
+ function defaultCardFields(columns: DataTableColumn[]): DataTableColumn[] {
302
+ const title =
303
+ columns.slice(0, 4).find((c) => (c.type ?? "text") === "text") ?? columns[0] ?? null;
304
+ if (!title) return [];
305
+ return [title, ...columns.slice(0, 3).filter((c) => c !== title)].slice(0, 3);
306
+ }
307
+
308
+ /**
309
+ * The table as stacked cards below md (Section 11.6, A59-013): one card per row with its most
310
+ * important fields, the first as the card's title. A row that opens is a button, so a tap or Enter
311
+ * opens it as Enter on the grid does. Compact density (shell.density or the table's own) packs the cards
312
+ * closer, with less padding.
313
+ */
314
+ function DataTableCards({
315
+ label,
316
+ caption,
317
+ fields,
318
+ rows,
319
+ totals,
320
+ totalsLabel,
321
+ onRowActivate,
322
+ compact,
323
+ select,
324
+ }: {
325
+ label: string;
326
+ caption: string | undefined;
327
+ fields: DataTableColumn[];
328
+ rows: Record<string, unknown>[];
329
+ totals: Record<string, unknown> | undefined;
330
+ totalsLabel: string;
331
+ onRowActivate: ((row: Record<string, unknown>, index: number) => void) | undefined;
332
+ compact: boolean;
333
+ select: SelectApi | null;
334
+ }): ReactElement {
335
+ const t = useT();
336
+ const [first, ...rest] = fields;
337
+ const empty = (value: unknown) =>
338
+ value === null ||
339
+ value === undefined ||
340
+ value === "" ||
341
+ (first?.type === "state" && !isStateValue(value));
342
+ const details = (values: Record<string, unknown>, columns: DataTableColumn[]) =>
343
+ columns.length > 0 ? (
344
+ <dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-12 gap-y-2 m-0 text-text-12">
345
+ {columns.map((c) => (
346
+ <div key={c.key} className="contents">
347
+ <dt className="text-text-secondary">{c.label}</dt>
348
+ <dd className="m-0 min-w-0 truncate" title={cellTitle(c, values[c.key])}>
349
+ <CellValue column={c} value={values[c.key]} />
350
+ </dd>
351
+ </div>
352
+ ))}
353
+ </dl>
354
+ ) : null;
355
+ return (
356
+ <section aria-label={label} className="flex flex-col gap-8 min-w-0">
357
+ {caption ? <p className="m-0 text-text-13 font-semibold">{caption}</p> : null}
358
+ <ul className={cx("flex flex-col m-0 p-0 list-none", compact ? "gap-4" : "gap-8")}>
359
+ {rows.map((row, index) => {
360
+ // A row whose title field is empty still gets a name, so its button is never unlabeled.
361
+ const title =
362
+ first && !empty(row[first.key]) ? (
363
+ <CellValue column={first} value={row[first.key]} />
364
+ ) : (
365
+ t("table.row", { n: index + 1 })
366
+ );
367
+ return (
368
+ <li
369
+ key={index}
370
+ className={cx(
371
+ "flex flex-col rounded-card border border-border-subtle bg-bg-surface",
372
+ compact ? "gap-4 p-8" : "gap-6 p-12",
373
+ )}
374
+ >
375
+ <span className="flex items-start gap-8">
376
+ {select ? (
377
+ <input
378
+ type="checkbox"
379
+ className="xos-check-input mt-2"
380
+ aria-label={select.label(row, index)}
381
+ checked={select.checked(index)}
382
+ onChange={() => select.toggle(index, false)}
383
+ />
384
+ ) : null}
385
+ {onRowActivate ? (
386
+ <button
387
+ type="button"
388
+ onClick={() => onRowActivate(row, index)}
389
+ className="p-0 border-0 bg-transparent text-start text-text-14 font-medium text-text-primary cursor-pointer underline-offset-2 hover:underline pointer-coarse:min-h-target-coarse"
390
+ >
391
+ {title}
392
+ </button>
393
+ ) : (
394
+ <span className="text-text-14 font-medium text-text-primary">{title}</span>
395
+ )}
396
+ </span>
397
+ {details(row, rest)}
398
+ </li>
399
+ );
400
+ })}
401
+ </ul>
402
+ {totals ? (
403
+ <div className="flex flex-col gap-4 px-12">
404
+ <p className="m-0 text-text-13 font-semibold">{totalsLabel}</p>
405
+ {details(
406
+ totals,
407
+ fields.filter((c) => c.key in totals),
408
+ )}
409
+ </div>
410
+ ) : null}
411
+ </section>
412
+ );
413
+ }
414
+
415
+ /**
416
+ * The table inside a ScrollRegion, over its row limit virtualized with TanStack Virtual: a keyboard grid (arrow keys,
417
+ * J and K between rows, Home and End, Ctrl+Home and Ctrl+End, Page Up and Page Down, Enter activates
418
+ * the row), with column pinning and resizing. Null money renders Unpriced; long text columns wrap.
419
+ * Each row carries its id as `data-item` and each cell its column as `data-column`, so a page can
420
+ * preview them (hover cards).
421
+ *
422
+ * The grid lays its columns out from its own column state. Keyboard resizing and reset change that
423
+ * state directly. A pointer drag on a resize handle runs on TanStack Table, which loads when a
424
+ * pointer first enters a handle or a handle takes focus, and then drives the same state. TanStack
425
+ * Virtual loads the same way, when a table over the limit is first scrolled, pointed at or focused,
426
+ * and then moves the window of rows (ADR 0027).
427
+ */
428
+ export function DataTable({
429
+ columns: givenColumns,
430
+ rows,
431
+ caption,
432
+ density: givenDensity,
433
+ totals,
434
+ totalsLabel,
435
+ label,
436
+ pinStart = NONE,
437
+ pinEnd = NONE,
438
+ resizable = true,
439
+ virtualizeAbove = 100,
440
+ height = "60vh",
441
+ onRowActivate,
442
+ cardFields,
443
+ selection,
444
+ rowId,
445
+ onRowContextMenu,
446
+ }: DataTableProps): ReactElement {
447
+ const t = useT();
448
+ const fmt = useFormat();
449
+ // shell.density: compact rows unless the table chooses its own density.
450
+ const preferred = usePreferences().density;
451
+ const density = givenDensity ?? (preferred === "compact" ? "compact" : "default");
452
+ const narrow = useNarrow();
453
+ const dir = Direction.useDirection();
454
+ const scrollRef = useRef<HTMLDivElement | null>(null);
455
+ const cellRefs = useRef(new Map<string, HTMLTableCellElement>());
456
+ const [active, setActive] = useState<[number, number]>([0, 0]);
457
+ const [focusPending, setFocusPending] = useState(false);
458
+ // Pinned columns lead and trail in their regions so sticky offsets match the DOM order.
459
+ const columns = useMemo(
460
+ () => [
461
+ ...givenColumns.filter((c) => pinStart.includes(c.key)),
462
+ ...givenColumns.filter((c) => !pinStart.includes(c.key) && !pinEnd.includes(c.key)),
463
+ ...givenColumns.filter((c) => pinEnd.includes(c.key)),
464
+ ],
465
+ [givenColumns, pinStart, pinEnd],
466
+ );
467
+
468
+ // Pinning is fixed at mount, as the table's initial state.
469
+ const [pinning] = useState(() => ({ start: pinStart, end: pinEnd }));
470
+ // Column state updates synchronously, as TanStack Table's own store does: one drag step sets the
471
+ // transient resize state and then commits the widths computed from it.
472
+ const [resizing, setResizingState] = useState<columnResizingState>(IDLE_RESIZE);
473
+ const [sizing, setSizingState] = useState<ColumnSizingState>({});
474
+ const resizingRef = useRef(resizing);
475
+ const sizingRef = useRef(sizing);
476
+ const setResizing = (updater: Updater<columnResizingState>) => {
477
+ resizingRef.current = apply(updater, resizingRef.current);
478
+ setResizingState(resizingRef.current);
479
+ };
480
+ const setSizing = (updater: Updater<ColumnSizingState>) => {
481
+ sizingRef.current = apply(updater, sizingRef.current);
482
+ setSizingState(sizingRef.current);
483
+ };
484
+
485
+ // Selection: a leading checkbox column, as column -1 of the keyboard grid.
486
+ const lead = selection ? 1 : 0;
487
+ const anchor = useRef<number | null>(null);
488
+ const idOf = (row: Record<string, unknown>, index: number) =>
489
+ rowId ? rowId(row, index) : String(row["id"] ?? index);
490
+ const isSelected = (index: number) => {
491
+ const row = rows[index];
492
+ return selection !== undefined && row !== undefined && selection.selected.has(idOf(row, index));
493
+ };
494
+ const selectedCount = selection ? rows.filter((_, i) => isSelected(i)).length : 0;
495
+ const allSelected = rows.length > 0 && selectedCount === rows.length;
496
+ const selectAll = (on: boolean) => {
497
+ if (!selection) return;
498
+ const next = new Set(selection.selected);
499
+ rows.forEach((row, i) => (on ? next.add(idOf(row, i)) : next.delete(idOf(row, i))));
500
+ selection.onChange(next);
501
+ };
502
+ const firstText = givenColumns.find((c) => (c.type ?? "text") === "text") ?? givenColumns[0];
503
+ const select: SelectApi | null = selection
504
+ ? {
505
+ checked: isSelected,
506
+ toggle: (index, range) => {
507
+ const row = rows[index];
508
+ if (!row) return;
509
+ const next = new Set(selection.selected);
510
+ const from = anchor.current;
511
+ if (range && from !== null && from !== index) {
512
+ for (let i = Math.min(from, index); i <= Math.max(from, index); i += 1) {
513
+ const r = rows[i];
514
+ if (r) next.add(idOf(r, i));
515
+ }
516
+ } else {
517
+ const id = idOf(row, index);
518
+ if (!next.delete(id)) next.add(id);
519
+ anchor.current = index;
520
+ }
521
+ selection.onChange(next);
522
+ },
523
+ label: (row, index) =>
524
+ selection.rowLabel?.(row, index) ??
525
+ t("table.selectRow", {
526
+ row:
527
+ (firstText ? plainText(row[firstText.key]) : "") || t("table.row", { n: index + 1 }),
528
+ }),
529
+ }
530
+ : null;
531
+
532
+ const leafColumns = useMemo(
533
+ () => layColumns(columns, sizing, pinning, lead * CHECK_WIDTH),
534
+ [columns, sizing, pinning, lead],
535
+ );
536
+ // Header order: start-pinned in pinning order, then the rest, then end-pinned in pinning order.
537
+ const headers = useMemo(() => {
538
+ const byId = new Map(leafColumns.map((c) => [c.id, c]));
539
+ const pick = (ids: string[]) =>
540
+ ids.map((id) => byId.get(id)).filter((c): c is LaidColumn => c !== undefined);
541
+ return [
542
+ ...pick(pinning.start),
543
+ ...leafColumns.filter((c) => !pinning.start.includes(c.id) && !pinning.end.includes(c.id)),
544
+ ...pick(pinning.end),
545
+ ];
546
+ }, [leafColumns, pinning]);
547
+ const totalSize = leafColumns.reduce((sum, c) => sum + c.size, lead * CHECK_WIDTH);
548
+ const resizerColumns = useMemo(
549
+ () =>
550
+ columns.map((c) => ({
551
+ id: c.key,
552
+ size: defaultWidth(c),
553
+ minSize: MIN_WIDTH,
554
+ maxSize: MAX_WIDTH,
555
+ })),
556
+ [columns],
557
+ );
558
+
559
+ const [Resizer, setResizer] = useState<ResizerComponent | null>(null);
560
+ const startRef = useRef<StartResize | null>(null);
561
+ const pendingStart = useRef<Parameters<StartResize> | null>(null);
562
+ const prepareResize = () => {
563
+ if (Resizer) return;
564
+ void loadResizer().then((m) => setResizer(() => m.DataTableResizer));
565
+ };
566
+ const startResize = (
567
+ columnId: string,
568
+ e: MouseEvent<HTMLDivElement> | TouchEvent<HTMLDivElement>,
569
+ ) => {
570
+ if (startRef.current) {
571
+ startRef.current(columnId, e);
572
+ return;
573
+ }
574
+ // The engine is still loading: the drag starts from this press once it is ready, unless the
575
+ // pointer is released first.
576
+ pendingStart.current = [
577
+ columnId,
578
+ "touches" in e
579
+ ? { type: e.type, touches: Array.from(e.touches, (touch) => ({ clientX: touch.clientX })) }
580
+ : { type: e.type, clientX: e.clientX },
581
+ ];
582
+ const release = () => {
583
+ pendingStart.current = null;
584
+ for (const type of RELEASE_EVENTS) document.removeEventListener(type, release);
585
+ };
586
+ for (const type of RELEASE_EVENTS) document.addEventListener(type, release);
587
+ prepareResize();
588
+ };
589
+ const onResizerReady = () => {
590
+ const pending = pendingStart.current;
591
+ pendingStart.current = null;
592
+ if (pending) startRef.current?.(...pending);
593
+ };
594
+
595
+ const virtual = rows.length > virtualizeAbove;
596
+ const rowHeight = ROW_HEIGHT[density];
597
+ // A table over the limit shows its first screen without the engine, which only moves the window:
598
+ // tables under the limit, and people who never scroll this one, do not download it (ADR 0027).
599
+ const [Virtualizer, setVirtualizer] = useState<VirtualizerComponent | null>(null);
600
+ const [viewport, setViewport] = useState(rowHeight * INITIAL_SCREEN_ROWS);
601
+ const [engineWindow, setEngineWindow] = useState<RowWindow | null>(null);
602
+ const scrollToRef = useRef<((index: number) => void) | null>(null);
603
+ const pendingScroll = useRef<number | null>(null);
604
+ const prepareWindow = useCallback(() => {
605
+ void loadVirtualizer().then((m) => setVirtualizer(() => m.DataTableVirtualizer));
606
+ }, []);
607
+ const onVirtualizerReady = useCallback(() => {
608
+ const pending = pendingScroll.current;
609
+ pendingScroll.current = null;
610
+ if (pending !== null) scrollToRef.current?.(pending);
611
+ }, []);
612
+ const engineRunning = Virtualizer !== null;
613
+ const cards = narrow && cardFields !== false;
614
+ useLayoutEffect(() => {
615
+ const element = scrollRef.current;
616
+ if (!virtual || engineRunning || !element) return;
617
+ const measure = () => setViewport(element.offsetHeight);
618
+ measure();
619
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(measure);
620
+ observer?.observe(element);
621
+ for (const type of WINDOW_INTERACTIONS) {
622
+ element.addEventListener(type, prepareWindow, { passive: true });
623
+ }
624
+ return () => {
625
+ observer?.disconnect();
626
+ for (const type of WINDOW_INTERACTIONS) element.removeEventListener(type, prepareWindow);
627
+ };
628
+ }, [virtual, engineRunning, cards, prepareWindow]);
629
+
630
+ const rowWindow = virtual
631
+ ? (engineWindow ?? firstWindow(viewport, rowHeight, rows.length))
632
+ : { start: 0, end: rows.length - 1 };
633
+ const items = Array.from(
634
+ { length: Math.max(0, rowWindow.end - rowWindow.start + 1) },
635
+ (_, i) => ({
636
+ index: rowWindow.start + i,
637
+ }),
638
+ );
639
+ const before = virtual ? rowWindow.start * rowHeight : 0;
640
+ const after = virtual && items.length > 0 ? (rows.length - 1 - rowWindow.end) * rowHeight : 0;
641
+
642
+ useEffect(() => {
643
+ if (!focusPending) return;
644
+ const cell = cellRefs.current.get(`${active[0]}:${active[1]}`);
645
+ if (cell) {
646
+ cell.focus();
647
+ setFocusPending(false);
648
+ }
649
+ });
650
+
651
+ const moveTo = (r: number, c: number) => {
652
+ const row = Math.max(0, Math.min(rows.length - 1, r));
653
+ const col = Math.max(-lead, Math.min(leafColumns.length - 1, c));
654
+ setActive([row, col]);
655
+ if (virtual) {
656
+ if (scrollToRef.current) scrollToRef.current(row);
657
+ else {
658
+ pendingScroll.current = row;
659
+ prepareWindow();
660
+ }
661
+ }
662
+ setFocusPending(true);
663
+ };
664
+
665
+ const onKeyDown = (e: KeyboardEvent<HTMLTableCellElement>, r: number, c: number) => {
666
+ const page = Math.max(
667
+ 1,
668
+ Math.floor((scrollRef.current?.clientHeight || rowHeight * 10) / rowHeight) - 1,
669
+ );
670
+ const forward = dir === "rtl" ? "ArrowLeft" : "ArrowRight";
671
+ const back = dir === "rtl" ? "ArrowRight" : "ArrowLeft";
672
+ const ctrl = e.ctrlKey || e.metaKey;
673
+ if (select) {
674
+ const key = e.key.toLowerCase();
675
+ if (ctrl && !e.altKey && !e.shiftKey && key === "a") {
676
+ e.preventDefault();
677
+ selectAll(true);
678
+ return;
679
+ }
680
+ if (
681
+ (!ctrl && !e.altKey && key === "x" && singleKeyShortcutsEnabled()) ||
682
+ (c === -1 && e.key === " " && !ctrl)
683
+ ) {
684
+ e.preventDefault();
685
+ select.toggle(r, e.shiftKey);
686
+ return;
687
+ }
688
+ if (e.key === "Escape" && selectedCount > 0) {
689
+ // Esc clears the selection first; a second Esc reaches the page (closing a peek).
690
+ e.preventDefault();
691
+ e.stopPropagation();
692
+ selection?.onChange(new Set());
693
+ return;
694
+ }
695
+ }
696
+ if (onRowContextMenu && ((e.key === "F10" && e.shiftKey) || e.key === "ContextMenu")) {
697
+ e.preventDefault();
698
+ const row = rows[r];
699
+ if (row) onRowContextMenu(row, r, null, e.currentTarget);
700
+ return;
701
+ }
702
+ // J and K move between rows as in every list (list.next and list.previous), as single keys.
703
+ const letters = !ctrl && !e.altKey && singleKeyShortcutsEnabled();
704
+ const moves: Record<string, [number, number] | undefined> = {
705
+ ArrowDown: [r + 1, c],
706
+ ArrowUp: [r - 1, c],
707
+ j: letters ? [r + 1, c] : undefined,
708
+ k: letters ? [r - 1, c] : undefined,
709
+ [forward]: [r, c + 1],
710
+ [back]: [r, c - 1],
711
+ Home: ctrl ? [0, -lead] : [r, -lead],
712
+ End: ctrl ? [rows.length - 1, leafColumns.length - 1] : [r, leafColumns.length - 1],
713
+ PageDown: [r + page, c],
714
+ PageUp: [r - page, c],
715
+ };
716
+ const target = moves[e.key];
717
+ if (target) {
718
+ e.preventDefault();
719
+ moveTo(target[0], target[1]);
720
+ } else if (e.key === "Enter" && onRowActivate) {
721
+ e.preventDefault();
722
+ const row = rows[r];
723
+ if (row) onRowActivate(row, r);
724
+ }
725
+ };
726
+
727
+ const resizeByKey = (column: LaidColumn, e: KeyboardEvent<HTMLDivElement>) => {
728
+ const grow = dir === "rtl" ? "ArrowLeft" : "ArrowRight";
729
+ const shrink = dir === "rtl" ? "ArrowRight" : "ArrowLeft";
730
+ const delta = e.key === grow ? 16 : e.key === shrink ? -16 : 0;
731
+ if (delta === 0) return;
732
+ e.preventDefault();
733
+ const next = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, column.size + delta));
734
+ setSizing((old) => ({ ...old, [column.id]: next }));
735
+ };
736
+ const resetSize = (column: LaidColumn) =>
737
+ setSizing((old) => Object.fromEntries(Object.entries(old).filter(([id]) => id !== column.id)));
738
+
739
+ const pinnedClass = (column: LaidColumn) => (column.pinned ? "z-sticky bg-bg-canvas" : undefined);
740
+
741
+ if (narrow && cardFields !== false) {
742
+ const fields = cardFields
743
+ ? cardFields
744
+ .map((key) => columns.find((c) => c.key === key))
745
+ .filter((c): c is DataTableColumn => c !== undefined)
746
+ : defaultCardFields(columns);
747
+ return (
748
+ <DataTableCards
749
+ label={label ?? caption ?? t("scroll.region")}
750
+ caption={caption}
751
+ fields={fields}
752
+ rows={rows}
753
+ totals={totals}
754
+ totalsLabel={totalsLabel ?? t("table.total")}
755
+ onRowActivate={onRowActivate}
756
+ compact={density === "compact"}
757
+ select={select}
758
+ />
759
+ );
760
+ }
761
+
762
+ return (
763
+ <>
764
+ <ScrollRegion
765
+ ref={scrollRef}
766
+ label={label ?? caption ?? t("scroll.region")}
767
+ {...(virtual ? { style: { maxBlockSize: height }, className: "overflow-y-auto" } : {})}
768
+ >
769
+ {Resizer ? (
770
+ <Resizer
771
+ columns={resizerColumns}
772
+ sizing={sizing}
773
+ resizing={resizing}
774
+ onSizingChange={setSizing}
775
+ onResizingChange={setResizing}
776
+ direction={dir === "rtl" ? "rtl" : "ltr"}
777
+ startRef={startRef}
778
+ onReady={onResizerReady}
779
+ />
780
+ ) : null}
781
+ {select ? (
782
+ <span role="status" className="sr-only">
783
+ {selectedCount > 0 ? t("bulk.selected", { n: fmt.number(selectedCount) }) : ""}
784
+ </span>
785
+ ) : null}
786
+ <table
787
+ role="grid"
788
+ aria-multiselectable={select ? true : undefined}
789
+ aria-rowcount={rows.length + 1 + (totals ? 1 : 0)}
790
+ aria-colcount={leafColumns.length + lead}
791
+ className="border-separate border-spacing-0 table-fixed text-text-13 print:text-paper-ink"
792
+ style={{ inlineSize: totalSize }}
793
+ >
794
+ {caption ? <caption className="pb-8 text-start font-semibold">{caption}</caption> : null}
795
+ <thead className="sticky top-0 z-sticky">
796
+ <tr aria-rowindex={1}>
797
+ {select ? (
798
+ <th
799
+ scope="col"
800
+ aria-colindex={1}
801
+ className="h-control-lg bg-bg-surface border-b border-border-subtle box-border z-sticky"
802
+ style={CHECK_STYLE}
803
+ >
804
+ <span className="flex justify-center">
805
+ <input
806
+ type="checkbox"
807
+ className="xos-check-input"
808
+ aria-label={t("table.selectAll")}
809
+ checked={allSelected}
810
+ ref={(el) => {
811
+ if (el) el.indeterminate = selectedCount > 0 && !allSelected;
812
+ }}
813
+ onChange={() => selectAll(!allSelected)}
814
+ />
815
+ </span>
816
+ </th>
817
+ ) : null}
818
+ {headers.map((header, ci) => {
819
+ const column = columns[ci];
820
+ return (
821
+ <th
822
+ key={header.id}
823
+ scope="col"
824
+ aria-colindex={ci + 1 + lead}
825
+ className={cx(
826
+ "relative h-control-lg px-12 font-medium text-text-12 text-text-secondary bg-bg-surface",
827
+ "border-b border-border-subtle box-border",
828
+ column && isNumeric(column) ? "text-end" : "text-start",
829
+ header.pinned && "z-sticky",
830
+ )}
831
+ style={pinStyle(header)}
832
+ >
833
+ {header.label}
834
+ {resizable ? (
835
+ <div
836
+ role="separator"
837
+ aria-orientation="vertical"
838
+ aria-label={t("table.resizeColumn", { column: header.label })}
839
+ aria-valuenow={header.size}
840
+ aria-valuemin={MIN_WIDTH}
841
+ aria-valuemax={MAX_WIDTH}
842
+ tabIndex={0}
843
+ onPointerEnter={prepareResize}
844
+ onFocus={prepareResize}
845
+ onMouseDown={(e) => startResize(header.id, e)}
846
+ onTouchStart={(e) => startResize(header.id, e)}
847
+ onDoubleClick={() => resetSize(header)}
848
+ onKeyDown={(e) => resizeByKey(header, e)}
849
+ className={cx(
850
+ "absolute inset-y-0 end-0 w-(--ui-handle) cursor-col-resize touch-none select-none",
851
+ "hover:bg-accent focus-visible:bg-accent",
852
+ resizing.isResizingColumn === header.id && "bg-accent",
853
+ )}
854
+ />
855
+ ) : null}
856
+ </th>
857
+ );
858
+ })}
859
+ </tr>
860
+ </thead>
861
+ <tbody>
862
+ {before > 0 ? (
863
+ <tr aria-hidden="true">
864
+ <td colSpan={leafColumns.length + lead} style={{ blockSize: before }} />
865
+ </tr>
866
+ ) : null}
867
+ {items.map((item) => {
868
+ const row = rows[item.index];
869
+ if (!row) return null;
870
+ const selected = select?.checked(item.index) ?? false;
871
+ return (
872
+ <tr
873
+ key={item.index}
874
+ aria-rowindex={item.index + 2}
875
+ data-item={idOf(row, item.index)}
876
+ aria-selected={select ? selected : undefined}
877
+ className={cx(
878
+ ROW_CLASS[density],
879
+ "hover:[&>td]:bg-bg-hover",
880
+ selected && "[&>td]:bg-bg-hover",
881
+ onRowActivate && "cursor-pointer",
882
+ )}
883
+ onDoubleClick={onRowActivate ? () => onRowActivate(row, item.index) : undefined}
884
+ onMouseDown={
885
+ select
886
+ ? (e) => {
887
+ // Shift+click selects a range, not the text between.
888
+ if (e.shiftKey) e.preventDefault();
889
+ }
890
+ : undefined
891
+ }
892
+ onClick={
893
+ select
894
+ ? (e) => {
895
+ if (e.metaKey || e.ctrlKey) select.toggle(item.index, false);
896
+ else if (e.shiftKey) select.toggle(item.index, true);
897
+ }
898
+ : undefined
899
+ }
900
+ onContextMenu={
901
+ onRowContextMenu
902
+ ? (e) => {
903
+ e.preventDefault();
904
+ onRowContextMenu(
905
+ row,
906
+ item.index,
907
+ { x: e.clientX, y: e.clientY },
908
+ e.target as HTMLElement,
909
+ );
910
+ }
911
+ : undefined
912
+ }
913
+ >
914
+ {select ? (
915
+ <td
916
+ ref={(el) => {
917
+ const key = `${item.index}:-1`;
918
+ if (el) cellRefs.current.set(key, el);
919
+ else cellRefs.current.delete(key);
920
+ }}
921
+ role="gridcell"
922
+ aria-colindex={1}
923
+ tabIndex={active[0] === item.index && active[1] === -1 ? 0 : -1}
924
+ onFocus={() => setActive([item.index, -1])}
925
+ onKeyDown={(e) => onKeyDown(e, item.index, -1)}
926
+ className={cx(
927
+ "border-b border-border-subtle box-border z-sticky bg-bg-canvas",
928
+ "focus-visible:outline-offset-[calc(var(--stroke-width-focus)*-1)]",
929
+ )}
930
+ style={CHECK_STYLE}
931
+ >
932
+ <span className="flex justify-center">
933
+ <input
934
+ type="checkbox"
935
+ tabIndex={-1}
936
+ className="xos-check-input"
937
+ aria-label={select.label(row, item.index)}
938
+ checked={selected}
939
+ onClick={(e) => {
940
+ e.stopPropagation();
941
+ select.toggle(item.index, e.shiftKey);
942
+ }}
943
+ onChange={() => undefined}
944
+ />
945
+ </span>
946
+ </td>
947
+ ) : null}
948
+ {leafColumns.map((cell, ci) => {
949
+ const column = columns[ci];
950
+ const isActive = active[0] === item.index && active[1] === ci;
951
+ return (
952
+ <td
953
+ key={cell.id}
954
+ ref={(el) => {
955
+ const key = `${item.index}:${ci}`;
956
+ if (el) cellRefs.current.set(key, el);
957
+ else cellRefs.current.delete(key);
958
+ }}
959
+ role="gridcell"
960
+ aria-colindex={ci + 1 + lead}
961
+ data-column={cell.id}
962
+ tabIndex={isActive ? 0 : -1}
963
+ onFocus={() => setActive([item.index, ci])}
964
+ onKeyDown={(e) => onKeyDown(e, item.index, ci)}
965
+ className={cx(
966
+ cellClass,
967
+ column && isNumeric(column) && "text-end",
968
+ column?.wrap && "whitespace-normal py-8",
969
+ "focus-visible:outline-offset-[calc(var(--stroke-width-focus)*-1)]",
970
+ pinnedClass(cell),
971
+ )}
972
+ style={pinStyle(cell)}
973
+ title={cellTitle(column, column ? row[column.key] : undefined)}
974
+ >
975
+ {column ? <CellValue column={column} value={row[column.key]} /> : null}
976
+ </td>
977
+ );
978
+ })}
979
+ </tr>
980
+ );
981
+ })}
982
+ {after > 0 ? (
983
+ <tr aria-hidden="true">
984
+ <td colSpan={leafColumns.length + lead} style={{ blockSize: after }} />
985
+ </tr>
986
+ ) : null}
987
+ </tbody>
988
+ {totals ? (
989
+ <tfoot className="sticky bottom-0 z-sticky">
990
+ <tr aria-rowindex={rows.length + 2} className={ROW_CLASS[density]}>
991
+ {select ? (
992
+ <td className={cx(cellClass, "bg-bg-surface")} style={CHECK_STYLE} />
993
+ ) : null}
994
+ {columns.map((c, ci) => {
995
+ const column = leafColumns[ci];
996
+ return (
997
+ <td
998
+ key={c.key}
999
+ className={cx(
1000
+ cellClass,
1001
+ "font-semibold bg-bg-surface",
1002
+ isNumeric(c) && "text-end",
1003
+ )}
1004
+ style={column ? pinStyle(column) : undefined}
1005
+ >
1006
+ {c.key in totals ? (
1007
+ <CellValue column={c} value={totals[c.key]} />
1008
+ ) : ci === 0 ? (
1009
+ (totalsLabel ?? t("table.total"))
1010
+ ) : null}
1011
+ </td>
1012
+ );
1013
+ })}
1014
+ </tr>
1015
+ </tfoot>
1016
+ ) : null}
1017
+ </table>
1018
+ </ScrollRegion>
1019
+ {/* After the region: the engine's layout effects read the region's ref, attached by then. */}
1020
+ {virtual && Virtualizer ? (
1021
+ <Virtualizer
1022
+ count={rows.length}
1023
+ rowHeight={rowHeight}
1024
+ overscan={OVERSCAN}
1025
+ viewport={viewport}
1026
+ scrollRef={scrollRef}
1027
+ scrollToRef={scrollToRef}
1028
+ onWindow={setEngineWindow}
1029
+ onReady={onVirtualizerReady}
1030
+ />
1031
+ ) : null}
1032
+ </>
1033
+ );
1034
+ }