@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,164 @@
1
+ import { useState, type ReactElement } from "react";
2
+ import type { BudgetLine, GLAccount } from "../types/reference";
3
+ import { cx } from "../lib/cx.ts";
4
+ import { useFormat, useT } from "../lib/context.tsx";
5
+ import { Icon } from "../Icon/Icon.tsx";
6
+ import { Money } from "../Forms/Forms.tsx";
7
+ import { SegmentedControl } from "../Navigation/Navigation.tsx";
8
+ import { ScrollRegion } from "../Layout/Layout.tsx";
9
+ import { type BudgetGridProps, type Grade } from "./Grids.types.ts";
10
+ import { glForCode } from "./glForCode.tsx";
11
+
12
+ function blank(v: number | null | undefined): v is null | undefined {
13
+ return v === null || v === undefined;
14
+ }
15
+
16
+ /**
17
+ * Budget lines grouped by GL account in numeric order, with a grade toggle. Posting and amounts are
18
+ * derived, never stored; any unpriced line makes its subtotal and the total Unpriced.
19
+ */
20
+ export function BudgetGrid({
21
+ accounts,
22
+ accountType = "Expense",
23
+ lines,
24
+ grades,
25
+ grade: initial = "base",
26
+ scopeLabel,
27
+ label,
28
+ onGradeChange,
29
+ }: BudgetGridProps): ReactElement {
30
+ const t = useT();
31
+ const fmt = useFormat();
32
+ const [grade, setGrade] = useState<Grade>(initial);
33
+ const amount = (l: BudgetLine): number | null => {
34
+ const rate = l[grade];
35
+ if (blank(rate)) return null;
36
+ if (l.qty === undefined) return rate;
37
+ return blank(l.qty) ? null : l.qty * rate;
38
+ };
39
+ const sum = (ls: BudgetLine[]): number | null => {
40
+ let total = 0;
41
+ for (const l of ls) {
42
+ const a = amount(l);
43
+ if (a === null) return null;
44
+ total += a;
45
+ }
46
+ return ls.length ? total : null;
47
+ };
48
+ const groups = new Map<string, { account: GLAccount | null; lines: BudgetLine[] }>();
49
+ for (const l of lines) {
50
+ const account = glForCode(l.urid, accounts, accountType);
51
+ const key = account?.code ?? "~";
52
+ const group = groups.get(key) ?? { account, lines: [] };
53
+ group.lines.push(l);
54
+ groups.set(key, group);
55
+ }
56
+ const ordered = [...groups.entries()]
57
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
58
+ .map(([, g]) => g);
59
+ const hasQty = lines.some((l) => l.qty !== undefined);
60
+ const heads = [
61
+ t("budget.gl"),
62
+ t("budget.costCenter"),
63
+ t("budget.urid"),
64
+ t("budget.line"),
65
+ ...(hasQty ? [t("budget.qty"), t("budget.rate")] : []),
66
+ t("budget.amount"),
67
+ ];
68
+ const lead = heads.length - 1;
69
+ const th = cx(
70
+ "h-control-lg px-12 font-medium text-text-12 text-text-secondary bg-bg-surface border-b border-border-subtle",
71
+ );
72
+ const td = cx("h-row-default px-12 border-b border-border-subtle whitespace-nowrap");
73
+ return (
74
+ <div className="flex flex-col gap-12">
75
+ <div className="flex flex-wrap items-center justify-between gap-8">
76
+ <SegmentedControl
77
+ label={t("budget.grade")}
78
+ value={grade}
79
+ options={grades}
80
+ onChange={(g) => {
81
+ setGrade(g as Grade);
82
+ onGradeChange?.(g);
83
+ }}
84
+ />
85
+ {scopeLabel ? <span className="text-text-secondary">{scopeLabel}</span> : null}
86
+ </div>
87
+ <ScrollRegion label={label ?? t("scroll.region")}>
88
+ <table className="w-full border-collapse text-text-13">
89
+ <thead>
90
+ <tr>
91
+ {heads.map((h, i) => (
92
+ <th key={h} scope="col" className={cx(th, i >= 4 ? "text-end" : "text-start")}>
93
+ {h}
94
+ </th>
95
+ ))}
96
+ </tr>
97
+ </thead>
98
+ {ordered.map((g) => (
99
+ <tbody key={g.account?.code ?? "unmapped"}>
100
+ <tr className="bg-bg-surface">
101
+ <th scope="rowgroup" colSpan={lead} className={cx(td, "text-start font-regular")}>
102
+ {g.account ? (
103
+ <span className="font-mono text-text-12">{g.account.code}</span>
104
+ ) : (
105
+ <span className="text-warning">
106
+ <Icon name="TriangleAlert" size={14} />
107
+ </span>
108
+ )}{" "}
109
+ <strong className="font-semibold">
110
+ {g.account?.name ?? t("budget.unmapped")}
111
+ </strong>
112
+ </th>
113
+ <td className={cx(td, "text-end font-semibold")}>
114
+ <Money amount={sum(g.lines)} />
115
+ </td>
116
+ </tr>
117
+ {g.lines.map((l, i) => (
118
+ <tr key={`${l.urid}-${i}`} className="hover:bg-bg-hover">
119
+ <td className={td} />
120
+ <td className={cx(td, "font-mono text-text-12 text-text-secondary")}>{l.cc}</td>
121
+ <td className={td}>
122
+ <span className="font-mono text-text-12">{l.urid}</span>
123
+ </td>
124
+ <td className={cx(td, "whitespace-normal")}>
125
+ {l.name}
126
+ {l.type ? <span className="text-text-secondary"> · {l.type}</span> : null}
127
+ </td>
128
+ {hasQty ? (
129
+ <td className={cx(td, "text-end tabular-nums")}>
130
+ {blank(l.qty) ? "" : fmt.number(l.qty)}
131
+ {l.unit ? <span className="text-text-secondary"> {l.unit}</span> : null}
132
+ </td>
133
+ ) : null}
134
+ {hasQty ? (
135
+ <td className={cx(td, "text-end")}>
136
+ <Money amount={l[grade]} />
137
+ </td>
138
+ ) : null}
139
+ <td className={cx(td, "text-end")}>
140
+ <Money amount={amount(l)} />
141
+ </td>
142
+ </tr>
143
+ ))}
144
+ </tbody>
145
+ ))}
146
+ <tfoot>
147
+ <tr>
148
+ <th
149
+ scope="row"
150
+ colSpan={lead}
151
+ className={cx(td, "text-start font-semibold bg-bg-surface")}
152
+ >
153
+ {t("table.total")}
154
+ </th>
155
+ <td className={cx(td, "text-end font-semibold bg-bg-surface")}>
156
+ <Money amount={sum(lines)} />
157
+ </td>
158
+ </tr>
159
+ </tfoot>
160
+ </table>
161
+ </ScrollRegion>
162
+ </div>
163
+ );
164
+ }
@@ -0,0 +1,5 @@
1
+ import type { GridColumn } from "../types/reference";
2
+
3
+ export function isNumeric(c: GridColumn): boolean {
4
+ return c.type === "money" || c.type === "number" || c.type === "multiplier";
5
+ }
@@ -0,0 +1,11 @@
1
+ export {
2
+ type SpreadsheetGridProps,
3
+ type BudgetGridProps,
4
+ type BidSheetProps,
5
+ } from "./Grids.types.ts";
6
+ export { parseCell } from "./parseCell.tsx";
7
+ export { columnTotal } from "./columnTotal.tsx";
8
+ export { SpreadsheetGrid } from "./SpreadsheetGrid.tsx";
9
+ export { glForCode } from "./glForCode.tsx";
10
+ export { BudgetGrid } from "./BudgetGrid.tsx";
11
+ export { BidSheet } from "./BidSheet.tsx";
@@ -0,0 +1,38 @@
1
+ import type {
2
+ BidSheetProps as ReferenceBidSheetProps,
3
+ BudgetGridProps as ReferenceBudgetGridProps,
4
+ SpreadsheetGridProps as ReferenceSpreadsheetGridProps,
5
+ } from "../types/reference";
6
+
7
+ export type Cell = string | number | null;
8
+
9
+ export type Row = Record<string, Cell>;
10
+
11
+ export type SpreadsheetGridProps = ReferenceSpreadsheetGridProps & {
12
+ /** Called with every row after an edit, clear or paste. */
13
+ onChange?: (rows: Row[]) => void;
14
+ /**
15
+ * The column that identifies a row. When set, new `rows` are merged into the grid by this key
16
+ * instead of being ignored: rows added or removed elsewhere appear or go, and a row whose incoming
17
+ * values differ from the last ones received for it takes them, except the row with an open cell,
18
+ * which keeps its draft. The selection and the open cell follow their row.
19
+ */
20
+ rowKey?: string;
21
+ /** Called with the key of the row whose cell opens for editing, and null when it closes. */
22
+ onEditingChange?: (rowKey: string | null) => void;
23
+ /**
24
+ * Called with the key of the row a person selects, once they pick a cell, and whenever the selected
25
+ * row changes; it follows the row when rows move, and reports null when the row leaves. Needs rowKey.
26
+ */
27
+ onSelectionChange?: (rowKey: string | null) => void;
28
+ };
29
+
30
+ export type BudgetGridProps = ReferenceBudgetGridProps & {
31
+ onGradeChange?: (grade: string) => void;
32
+ };
33
+
34
+ export type BidSheetProps = ReferenceBidSheetProps & {
35
+ onChange?: (lines: ReferenceBidSheetProps["lines"]) => void;
36
+ };
37
+
38
+ export type Grade = "base" | "elevated" | "premium";
@@ -0,0 +1,445 @@
1
+ import {
2
+ useEffect,
3
+ useId,
4
+ useRef,
5
+ useState,
6
+ type ClipboardEvent,
7
+ type KeyboardEvent,
8
+ type ReactElement,
9
+ type ReactNode,
10
+ } from "react";
11
+ import { Direction } from "radix-ui";
12
+ import type { GridColumn } from "../types/reference";
13
+ import { cx } from "../lib/cx.ts";
14
+ import { useFormat, useT } from "../lib/context.tsx";
15
+ import { Money } from "../Forms/Forms.tsx";
16
+ import { ScrollRegion } from "../Layout/Layout.tsx";
17
+ import { isNumeric } from "./Grids.shared.tsx";
18
+ import { type Cell, type Row, type SpreadsheetGridProps } from "./Grids.types.ts";
19
+ import { columnTotal } from "./columnTotal.tsx";
20
+ import { parseCell } from "./parseCell.tsx";
21
+
22
+ function editable(c: GridColumn): boolean {
23
+ return Boolean(c.editable) && !c.derive;
24
+ }
25
+
26
+ function sameRow(a: Row, b: Row): boolean {
27
+ const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
28
+ for (const k of keys) if ((a[k] ?? null) !== (b[k] ?? null)) return false;
29
+ return true;
30
+ }
31
+
32
+ interface Received {
33
+ readonly rows: readonly Row[];
34
+ /** The last row received for each key, which a new `rows` is compared against. */
35
+ readonly byKey: ReadonlyMap<string, Row>;
36
+ }
37
+
38
+ /**
39
+ * Merges new `rows` into the grid's rows by key. A row takes the incoming values only when they
40
+ * differ from the last ones received for it, never because they differ from what the person typed:
41
+ * a refresh carrying the version from before this tab's own save leaves the typed value in place.
42
+ * The row with an open cell keeps its values, and its last received row stays as it was, so the
43
+ * difference applies with the next rows after the cell closes.
44
+ */
45
+ function reconcile(
46
+ incoming: readonly Row[],
47
+ local: readonly Row[],
48
+ received: ReadonlyMap<string, Row>,
49
+ keyOf: (row: Row) => string,
50
+ editingKey: string | null,
51
+ ): { rows: Row[]; byKey: Map<string, Row> } {
52
+ const mine = new Map(local.map((r) => [keyOf(r), r]));
53
+ const byKey = new Map<string, Row>();
54
+ const rows = incoming.map((row) => {
55
+ const key = keyOf(row);
56
+ const current = mine.get(key);
57
+ const before = received.get(key);
58
+ if (current && key === editingKey) {
59
+ byKey.set(key, before ?? row);
60
+ return current;
61
+ }
62
+ byKey.set(key, row);
63
+ return current && before && sameRow(before, row) ? current : { ...row };
64
+ });
65
+ return { rows, byKey };
66
+ }
67
+
68
+ /**
69
+ * Spreadsheet-speed editing as an ARIA grid (the accessible model a Glide Data Grid surface keeps):
70
+ * arrows and Tab move, Enter or typing edits, Escape cancels, Delete clears to Unpriced, and a paste
71
+ * from Excel fills the range from the selected cell. Derived columns are read-only. With appendRow a
72
+ * trailing blank row becomes a new row once typed or pasted into; errors mark cells invalid in place,
73
+ * each message named by its cell (aria-describedby), and a column's parse reads its own shorthand.
74
+ */
75
+ export function SpreadsheetGrid({
76
+ label,
77
+ columns,
78
+ rows: given,
79
+ totalsLabel,
80
+ appendRow = false,
81
+ errors,
82
+ onChange,
83
+ rowKey,
84
+ onEditingChange,
85
+ onSelectionChange,
86
+ }: SpreadsheetGridProps): ReactElement {
87
+ const t = useT();
88
+ const fmt = useFormat();
89
+ const dir = Direction.useDirection();
90
+ const [rows, setRows] = useState<Row[]>(() => given.map((r) => ({ ...r })));
91
+ const [sel, setSel] = useState<[number, number]>([0, 0]);
92
+ const [editing, setEditing] = useState<{ draft: string } | null>(null);
93
+ const cells = useRef(new Map<string, HTMLTableCellElement>());
94
+ const input = useRef<HTMLInputElement>(null);
95
+ const keyOf = (row: Row | undefined) => (row && rowKey ? String(row[rowKey] ?? "") : "");
96
+ const [received, setReceived] = useState<Received>(() => ({
97
+ rows: given,
98
+ byKey: new Map(given.map((r) => [rowKey ? String(r[rowKey] ?? "") : "", r])),
99
+ }));
100
+
101
+ // New rows from the page, merged by key while the selection and the open cell follow their row.
102
+ if (rowKey && given !== received.rows) {
103
+ const selectedKey = keyOf(rows[sel[0]]);
104
+ const merged = reconcile(given, rows, received.byKey, keyOf, editing ? selectedKey : null);
105
+ const at = merged.rows.findIndex((r) => keyOf(r) === selectedKey);
106
+ setReceived({ rows: given, byKey: merged.byKey });
107
+ setRows(merged.rows);
108
+ if (at < 0) {
109
+ setSel([Math.max(0, Math.min(sel[0], merged.rows.length - 1)), sel[1]]);
110
+ if (editing) setEditing(null);
111
+ } else if (at !== sel[0]) {
112
+ setSel([at, sel[1]]);
113
+ }
114
+ }
115
+
116
+ const editingKey = rowKey && editing ? keyOf(rows[sel[0]]) : null;
117
+ const reportEditing = useRef(onEditingChange);
118
+ reportEditing.current = onEditingChange;
119
+ // The row a person selected, reported when it changes and following the row when rows move. The
120
+ // first cell holds the keyboard focus before anyone picks a row, so nothing reports until then.
121
+ const [picked, setPicked] = useState(false);
122
+ const selectedRowKey = rowKey && picked ? keyOf(rows[sel[0]]) || null : null;
123
+ const reportSelection = useRef(onSelectionChange);
124
+ reportSelection.current = onSelectionChange;
125
+ const reportedSelection = useRef<string | null>(null);
126
+ useEffect(() => {
127
+ if (reportedSelection.current === selectedRowKey) return;
128
+ reportedSelection.current = selectedRowKey;
129
+ reportSelection.current?.(selectedRowKey);
130
+ }, [selectedRowKey]);
131
+ const reported = useRef<string | null>(null);
132
+ useEffect(() => {
133
+ if (reported.current === editingKey) return;
134
+ reported.current = editingKey;
135
+ reportEditing.current?.(editingKey);
136
+ }, [editingKey]);
137
+ const errorId = useId();
138
+ const blank = (): Row => Object.fromEntries(columns.map((c) => [c.key, null]));
139
+ // The rows shown: the grid's rows and, with appendRow, one blank row after them.
140
+ const shown = appendRow ? [...rows, blank()] : rows;
141
+ const errorAt = (r: number, key: string) => errors?.[`${r}:${key}`];
142
+ const errorList = Object.entries(errors ?? {});
143
+
144
+ useEffect(() => {
145
+ if (editing) input.current?.focus();
146
+ else cells.current.get(`${sel[0]}:${sel[1]}`)?.focus({ preventScroll: false });
147
+ }, [sel, editing]);
148
+
149
+ const commit = (next: Row[]) => {
150
+ setRows(next);
151
+ onChange?.(next);
152
+ };
153
+ const setCell = (r: number, c: number, value: Cell) => {
154
+ const col = columns[c];
155
+ if (!col || !editable(col)) return;
156
+ if (r >= rows.length) {
157
+ // Typing into the blank row adds it; clearing it adds nothing.
158
+ if (value === null) return;
159
+ commit([...rows, { ...blank(), [col.key]: value }]);
160
+ return;
161
+ }
162
+ commit(rows.map((row, i) => (i === r ? { ...row, [col.key]: value } : row)));
163
+ };
164
+ const show = (col: GridColumn, row: Row): ReactNode => {
165
+ const v = col.derive ? col.derive(row) : row[col.key];
166
+ if (col.type === "money")
167
+ return (
168
+ <Money
169
+ amount={typeof v === "number" ? v : null}
170
+ {...(col.currency ? { currency: col.currency } : {})}
171
+ />
172
+ );
173
+ if (v === null || v === undefined || v === "") return null;
174
+ if (col.type === "multiplier" && typeof v === "number") {
175
+ return (
176
+ <span className="tabular-nums">
177
+ {t("fmt.multiplier", {
178
+ n: new Intl.NumberFormat(fmt.locale, {
179
+ minimumFractionDigits: 1,
180
+ maximumFractionDigits: 2,
181
+ }).format(v),
182
+ })}
183
+ </span>
184
+ );
185
+ }
186
+ if (col.type === "code") return <span className="font-mono text-text-12">{String(v)}</span>;
187
+ if (col.type === "number" && typeof v === "number")
188
+ return <span className="tabular-nums">{fmt.number(v)}</span>;
189
+ return String(v);
190
+ };
191
+
192
+ const move = (dr: number, dc: number) =>
193
+ setSel(([r, c]) => [
194
+ Math.max(0, Math.min(shown.length - 1, r + dr)),
195
+ Math.max(0, Math.min(columns.length - 1, c + dc)),
196
+ ]);
197
+
198
+ const onKeyDown = (e: KeyboardEvent<HTMLTableCellElement>) => {
199
+ if (editing) return;
200
+ const [r, c] = sel;
201
+ const col = columns[c];
202
+ const forward = dir === "rtl" ? "ArrowLeft" : "ArrowRight";
203
+ const back = dir === "rtl" ? "ArrowRight" : "ArrowLeft";
204
+ const moves: Record<string, [number, number]> = {
205
+ ArrowUp: [-1, 0],
206
+ ArrowDown: [1, 0],
207
+ [forward]: [0, 1],
208
+ [back]: [0, -1],
209
+ };
210
+ const step = e.key === "Tab" ? ([0, e.shiftKey ? -1 : 1] as [number, number]) : moves[e.key];
211
+ if (
212
+ step &&
213
+ !(e.key === "Tab" && ((e.shiftKey && c === 0) || (!e.shiftKey && c === columns.length - 1)))
214
+ ) {
215
+ e.preventDefault();
216
+ move(step[0], step[1]);
217
+ } else if (e.key === "Home") {
218
+ e.preventDefault();
219
+ setSel(e.ctrlKey || e.metaKey ? [0, 0] : [r, 0]);
220
+ } else if (e.key === "End") {
221
+ e.preventDefault();
222
+ setSel(
223
+ e.ctrlKey || e.metaKey ? [shown.length - 1, columns.length - 1] : [r, columns.length - 1],
224
+ );
225
+ } else if ((e.key === "Enter" || e.key === "F2") && col && editable(col)) {
226
+ e.preventDefault();
227
+ const v = shown[r]?.[col.key];
228
+ setEditing({ draft: v === null || v === undefined ? "" : String(v) });
229
+ } else if ((e.key === "Delete" || e.key === "Backspace") && col && editable(col)) {
230
+ e.preventDefault();
231
+ setCell(r, c, null);
232
+ } else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && col && editable(col)) {
233
+ e.preventDefault();
234
+ setEditing({ draft: e.key });
235
+ }
236
+ };
237
+
238
+ const onPaste = (e: ClipboardEvent<HTMLTableElement>) => {
239
+ const text = e.clipboardData.getData("text/plain");
240
+ if (!text || editing) return;
241
+ e.preventDefault();
242
+ const grid = text
243
+ .replace(/\r/g, "")
244
+ .replace(/\n$/, "")
245
+ .split("\n")
246
+ .map((line) => line.split("\t"));
247
+ const next = rows.map((row) => ({ ...row }));
248
+ grid.forEach((line, dr) => {
249
+ const r = sel[0] + dr;
250
+ // A paste reaching past the last row adds rows when the grid appends.
251
+ if (appendRow) while (next.length <= r) next.push(blank());
252
+ line.forEach((value, dc) => {
253
+ const col = columns[sel[1] + dc];
254
+ const row = next[r];
255
+ if (row && col && editable(col)) row[col.key] = parseCell(col, value);
256
+ });
257
+ });
258
+ commit(next);
259
+ };
260
+
261
+ const finish = (save: boolean, draft: string) => {
262
+ const col = columns[sel[1]];
263
+ if (save && col) setCell(sel[0], sel[1], parseCell(col, draft));
264
+ setEditing(null);
265
+ };
266
+
267
+ const th = cx(
268
+ "h-control-lg px-12 font-medium text-text-12 text-text-secondary bg-bg-surface border-b border-e border-border-subtle",
269
+ );
270
+ const td = cx(
271
+ "relative h-control-lg px-12 border-b border-e border-border-subtle whitespace-nowrap",
272
+ );
273
+ return (
274
+ <div className="flex flex-col gap-6">
275
+ <ScrollRegion label={label}>
276
+ <table
277
+ role="grid"
278
+ aria-label={label}
279
+ aria-rowcount={shown.length + 2}
280
+ onPaste={onPaste}
281
+ onPointerDownCapture={() => setPicked(true)}
282
+ onKeyDownCapture={() => setPicked(true)}
283
+ className="w-full border-collapse text-text-13"
284
+ >
285
+ <thead>
286
+ <tr aria-rowindex={1}>
287
+ <th scope="col" className={cx(th, "w-(--space-40) text-center")}>
288
+ <span className="sr-only">{t("grid.row")}</span>
289
+ </th>
290
+ {columns.map((c) => (
291
+ <th
292
+ key={c.key}
293
+ scope="col"
294
+ className={cx(th, isNumeric(c) ? "text-end" : "text-start")}
295
+ style={c.width ? { inlineSize: c.width } : undefined}
296
+ >
297
+ {c.label}
298
+ </th>
299
+ ))}
300
+ </tr>
301
+ </thead>
302
+ <tbody>
303
+ {shown.map((row, ri) => (
304
+ <tr
305
+ key={rowKey ? (ri < rows.length ? `k:${keyOf(row)}` : "blank") : ri}
306
+ aria-rowindex={ri + 2}
307
+ >
308
+ <td
309
+ className={cx(
310
+ td,
311
+ "text-center text-text-11 text-text-tertiary bg-bg-surface tabular-nums",
312
+ )}
313
+ >
314
+ {ri < rows.length ? (
315
+ ri + 1
316
+ ) : (
317
+ <span aria-label={t("grid.newRow")}>{t("grid.newRowMark")}</span>
318
+ )}
319
+ </td>
320
+ {columns.map((c, ci) => {
321
+ const on = sel[0] === ri && sel[1] === ci;
322
+ const isEditing = on && editing !== null;
323
+ const message = errorAt(ri, c.key);
324
+ return (
325
+ <td
326
+ key={c.key}
327
+ ref={(el) => {
328
+ if (el) cells.current.set(`${ri}:${ci}`, el);
329
+ else cells.current.delete(`${ri}:${ci}`);
330
+ }}
331
+ role="gridcell"
332
+ aria-selected={on}
333
+ aria-readonly={editable(c) ? undefined : true}
334
+ aria-invalid={message ? true : undefined}
335
+ aria-describedby={message ? `${errorId}-${ri}-${c.key}` : undefined}
336
+ tabIndex={on ? 0 : -1}
337
+ title={c.derive ? t("grid.derived") : undefined}
338
+ onClick={() => setSel([ri, ci])}
339
+ onDoubleClick={() => {
340
+ setSel([ri, ci]);
341
+ if (editable(c)) {
342
+ const v = row[c.key];
343
+ setEditing({ draft: v === null || v === undefined ? "" : String(v) });
344
+ }
345
+ }}
346
+ onKeyDown={onKeyDown}
347
+ className={cx(
348
+ td,
349
+ isNumeric(c) && "text-end",
350
+ !editable(c) && "text-text-secondary",
351
+ message && "ring-(length:--stroke-width-hairline) ring-inset ring-danger",
352
+ on && "ring-(length:--stroke-width-focus) ring-inset ring-accent",
353
+ "outline-none forced-colors:border",
354
+ )}
355
+ >
356
+ {isEditing ? (
357
+ <input
358
+ ref={input}
359
+ aria-label={c.label}
360
+ aria-invalid={message ? true : undefined}
361
+ aria-describedby={message ? `${errorId}-${ri}-${c.key}` : undefined}
362
+ defaultValue={editing.draft}
363
+ onBlur={(e) => finish(true, e.target.value)}
364
+ onKeyDown={(e) => {
365
+ e.stopPropagation();
366
+ if (e.key === "Enter") {
367
+ finish(true, e.currentTarget.value);
368
+ move(1, 0);
369
+ } else if (e.key === "Escape") finish(false, "");
370
+ else if (e.key === "Tab") {
371
+ e.preventDefault();
372
+ finish(true, e.currentTarget.value);
373
+ move(0, e.shiftKey ? -1 : 1);
374
+ }
375
+ }}
376
+ className="w-full border-0 bg-transparent outline-none text-inherit text-text-13 tabular-nums"
377
+ />
378
+ ) : (row[c.key] === null || row[c.key] === undefined || row[c.key] === "") &&
379
+ c.blankHint?.(row) ? (
380
+ <span className="text-text-tertiary">{c.blankHint(row)}</span>
381
+ ) : (
382
+ show(c, row)
383
+ )}
384
+ {on && !isEditing && editable(c) ? (
385
+ <span
386
+ aria-hidden="true"
387
+ className="absolute -end-(--stroke-width-focus) -bottom-(--stroke-width-focus) z-base size-(--space-6) bg-accent border border-bg-canvas"
388
+ />
389
+ ) : null}
390
+ </td>
391
+ );
392
+ })}
393
+ </tr>
394
+ ))}
395
+ </tbody>
396
+ <tfoot>
397
+ <tr aria-rowindex={shown.length + 2}>
398
+ <td className={cx(td, "bg-bg-surface")} />
399
+ {columns.map((c, i) => (
400
+ <td
401
+ key={c.key}
402
+ className={cx(td, "font-semibold bg-bg-surface", isNumeric(c) && "text-end")}
403
+ >
404
+ {c.total ? (
405
+ <Money
406
+ amount={columnTotal(
407
+ rows
408
+ .filter((r) => c.totalIncludes?.(r) ?? true)
409
+ .map((r) => (c.derive ? { ...r, [c.key]: c.derive(r) ?? null } : r)),
410
+ c.key,
411
+ )}
412
+ {...(c.currency ? { currency: c.currency } : {})}
413
+ />
414
+ ) : i === 0 ? (
415
+ (totalsLabel ?? t("table.total"))
416
+ ) : null}
417
+ </td>
418
+ ))}
419
+ </tr>
420
+ </tfoot>
421
+ </table>
422
+ </ScrollRegion>
423
+ {errorList.length > 0 ? (
424
+ <ul className="m-0 flex list-none flex-col gap-2 p-0 text-text-12 text-danger-text">
425
+ {errorList.map(([key, text]) => {
426
+ const [r, col] = key.split(":");
427
+ const column = columns.find((c) => c.key === col);
428
+ return (
429
+ <li key={key} id={`${errorId}-${r}-${col}`}>
430
+ {t("grid.cellError", {
431
+ row: Number(r) + 1,
432
+ column: column?.label ?? col ?? "",
433
+ message: text,
434
+ })}
435
+ </li>
436
+ );
437
+ })}
438
+ </ul>
439
+ ) : null}
440
+ <p className="m-0 text-text-11 text-text-secondary">
441
+ {appendRow ? t("grid.hintAppend") : t("grid.hint")}
442
+ </p>
443
+ </div>
444
+ );
445
+ }
@@ -0,0 +1,14 @@
1
+ import { type Row } from "./Grids.types.ts";
2
+
3
+ /** Column total, or null when any line is blank (the silence rule). */
4
+ export function columnTotal(rows: Row[], key: string): number | null {
5
+ let sum = 0;
6
+ let any = false;
7
+ for (const r of rows) {
8
+ const v = r[key];
9
+ if (v === null || v === undefined || v === "") return null;
10
+ sum += Number(v);
11
+ any = true;
12
+ }
13
+ return any ? sum : null;
14
+ }