@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,92 @@
1
+ import { createContext, createElement, useContext, useEffect, useRef, type ReactNode } from "react";
2
+
3
+ /**
4
+ * Live updates in React (Section 11.7, ADR 0044). Components say which tables they show with
5
+ * `useLive`; the app's `LiveProvider` supplies the source, a hub that keeps one Realtime channel per
6
+ * page (`@xos/data/live`). This module has no Realtime code of its own, so a page that never
7
+ * watches anything never loads it, and outside a provider (Storybook, tests) `useLive` does nothing.
8
+ */
9
+
10
+ /** One watched table, optionally narrowed with a Realtime filter such as `org_id=eq.<uuid>`. */
11
+ export interface LiveTopic {
12
+ readonly table: string;
13
+ readonly filter?: string;
14
+ }
15
+
16
+ /** A changed row, or `RESYNC` after a reconnect. */
17
+ export interface LiveChange {
18
+ readonly table: string;
19
+ readonly type: "INSERT" | "UPDATE" | "DELETE" | "RESYNC";
20
+ readonly id: string | null;
21
+ /** The row after an INSERT or UPDATE, as row level security let Realtime send it; the reread confirms it. */
22
+ readonly row?: Readonly<Record<string, unknown>>;
23
+ }
24
+
25
+ /** How fast batches may arrive; show mode follows cues faster than a list follows rows. */
26
+ export interface LivePacing {
27
+ readonly batchMs?: number;
28
+ readonly minIntervalMs?: number;
29
+ readonly maxIntervalMs?: number;
30
+ }
31
+
32
+ export interface LiveSource {
33
+ watch(
34
+ topics: readonly LiveTopic[],
35
+ listener: (changes: readonly LiveChange[]) => void,
36
+ pacing?: LivePacing,
37
+ ): () => void;
38
+ }
39
+
40
+ const LiveContext = createContext<LiveSource | null>(null);
41
+
42
+ export function LiveProvider({
43
+ source,
44
+ children,
45
+ }: {
46
+ source: LiveSource | null;
47
+ children?: ReactNode;
48
+ }): ReactNode {
49
+ return createElement(LiveContext.Provider, { value: source }, children);
50
+ }
51
+
52
+ /**
53
+ * One string for a set of topics, so a re-render with equal topics keeps its subscription. Topics
54
+ * are joined by a line break, which no filter holds; an `in` filter's values are comma-separated.
55
+ */
56
+ function topicsKey(topics: readonly LiveTopic[] | null): string {
57
+ if (!topics || topics.length === 0) return "";
58
+ return topics
59
+ .map((t) => `${t.table}|${t.filter ?? ""}`)
60
+ .sort()
61
+ .join("\n");
62
+ }
63
+
64
+ /**
65
+ * Calls `onChange` with each batch of changes to the topics while the component is mounted. Null or
66
+ * empty topics watch nothing. The latest `onChange` is always the one called.
67
+ */
68
+ export function useLive(
69
+ topics: readonly LiveTopic[] | null,
70
+ onChange: (changes: readonly LiveChange[]) => void,
71
+ pacing?: LivePacing,
72
+ ): void {
73
+ const source = useContext(LiveContext);
74
+ const handler = useRef(onChange);
75
+ handler.current = onChange;
76
+ const key = topicsKey(topics);
77
+ const batchMs = pacing?.batchMs;
78
+ const minIntervalMs = pacing?.minIntervalMs;
79
+ const maxIntervalMs = pacing?.maxIntervalMs;
80
+ useEffect(() => {
81
+ if (!source || key === "") return undefined;
82
+ const watched = key.split("\n").map((k): LiveTopic => {
83
+ const [table = "", filter = ""] = k.split("|");
84
+ return filter ? { table, filter } : { table };
85
+ });
86
+ return source.watch(watched, (changes) => handler.current(changes), {
87
+ ...(batchMs !== undefined ? { batchMs } : {}),
88
+ ...(minIntervalMs !== undefined ? { minIntervalMs } : {}),
89
+ ...(maxIntervalMs !== undefined ? { maxIntervalMs } : {}),
90
+ });
91
+ }, [source, key, batchMs, minIntervalMs, maxIntervalMs]);
92
+ }
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Display preferences from the settings registry (Section 4.5.6 Preferences and Accessibility, A65-001): the
3
+ * person's text size, contrast, motion, density, single-key shortcuts, date format, units, first day of the
4
+ * week and the zone schedule times show in. Atlas and Gateway resolve them from the person's settings, keep
5
+ * them in one cookie so the server renders the document with them, and set them on the document root as data
6
+ * attributes the styles read.
7
+ */
8
+ import type { TimeZoneDisplay } from "./format.ts";
9
+ import type { UnitsPreference } from "@xos/i18n/units";
10
+
11
+ export * from "@xos/i18n/units";
12
+ export { displayTimeZone, formatZonedDateTime, type TimeZoneDisplay } from "./format.ts";
13
+
14
+ export type TextSize = "default" | "large" | "larger";
15
+ export type Contrast = "default" | "more";
16
+ export type Motion = "system" | "reduce" | "full";
17
+ export type Density = "comfortable" | "compact";
18
+ export type DateFormat = "locale" | "iso";
19
+ /**
20
+ * locale.units (Section 15): "jurisdiction" follows the project's jurisdiction (imperial for the United States,
21
+ * metric otherwise), or a fixed system; resolve it with `resolveUnitSystem` where a value is shown.
22
+ */
23
+ export type Units = UnitsPreference;
24
+ /**
25
+ * 0 for Sunday (payroll.week_start's default), 1 for Monday. The default is the registry's, so a person who
26
+ * never chose one keeps the cookie empty and nothing resyncs.
27
+ */
28
+ export type WeekStart = 0 | 1;
29
+
30
+ export interface DisplayPreferences {
31
+ readonly textSize: TextSize;
32
+ readonly contrast: Contrast;
33
+ readonly motion: Motion;
34
+ readonly density: Density;
35
+ readonly shortcuts: boolean;
36
+ readonly dateFormat: DateFormat;
37
+ readonly units: Units;
38
+ readonly weekStart: WeekStart;
39
+ /** me.time_zone_display: schedule times in the event's zone (default) or the viewer's own. */
40
+ readonly timeZoneDisplay: TimeZoneDisplay;
41
+ }
42
+
43
+ export const DEFAULT_PREFERENCES: DisplayPreferences = {
44
+ textSize: "default",
45
+ contrast: "default",
46
+ motion: "system",
47
+ density: "comfortable",
48
+ shortcuts: true,
49
+ dateFormat: "locale",
50
+ units: "jurisdiction",
51
+ weekStart: 0,
52
+ timeZoneDisplay: "event",
53
+ };
54
+
55
+ /** The cookie that carries the preferences for server rendering. */
56
+ export const PREFERENCES_COOKIE = "xos-prefs";
57
+
58
+ const YEAR_SECONDS = 60 * 60 * 24 * 365;
59
+
60
+ /**
61
+ * The `document.cookie` assignment that keeps the preferences cookie in step with the person's settings. It is
62
+ * set in the browser, not by a server action, so keeping it current never re-renders or interrupts the page.
63
+ * The value holds only letters, digits, dots and bars; an empty value expires the cookie.
64
+ */
65
+ export function preferencesCookie(encoded: string, secure: boolean): string {
66
+ const value = /^[a-z0-9.|]*$/.test(encoded) ? encoded : "";
67
+ const age = value === "" ? 0 : YEAR_SECONDS;
68
+ return `${PREFERENCES_COOKIE}=${value}; path=/; max-age=${age}; samesite=lax${secure ? "; secure" : ""}`;
69
+ }
70
+
71
+ /** The registry key each preference reads (packages/schemas/settings). */
72
+ export const PREFERENCE_SETTINGS = {
73
+ textSize: "me.text_size",
74
+ contrast: "me.high_contrast",
75
+ motion: "me.reduced_motion",
76
+ density: "shell.density",
77
+ shortcuts: "me.shortcuts_enabled",
78
+ dateFormat: "locale.date_format",
79
+ units: "locale.units",
80
+ weekStart: "payroll.week_start",
81
+ timeZoneDisplay: "me.time_zone_display",
82
+ } as const;
83
+
84
+ const UNITS: readonly Units[] = ["jurisdiction", "imperial", "metric"];
85
+ const ZONE_DISPLAYS: readonly TimeZoneDisplay[] = ["event", "viewer"];
86
+
87
+ type SettingLookup = (key: string) => unknown;
88
+
89
+ function pick<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {
90
+ return typeof value === "string" && (allowed as readonly string[]).includes(value)
91
+ ? (value as T)
92
+ : fallback;
93
+ }
94
+
95
+ /** Preferences from resolved setting values, falling back to the defaults for anything unset or unknown. */
96
+ export function preferencesFromSettings(
97
+ values: ReadonlyMap<string, unknown> | SettingLookup,
98
+ ): DisplayPreferences {
99
+ const get: SettingLookup = typeof values === "function" ? values : (key) => values.get(key);
100
+ const week = get(PREFERENCE_SETTINGS.weekStart);
101
+ return {
102
+ textSize: pick(get(PREFERENCE_SETTINGS.textSize), ["default", "large", "larger"], "default"),
103
+ contrast: get(PREFERENCE_SETTINGS.contrast) === true ? "more" : "default",
104
+ motion: pick(get(PREFERENCE_SETTINGS.motion), ["system", "reduce", "full"], "system"),
105
+ density: pick(get(PREFERENCE_SETTINGS.density), ["comfortable", "compact"], "comfortable"),
106
+ shortcuts: get(PREFERENCE_SETTINGS.shortcuts) !== false,
107
+ dateFormat: pick(get(PREFERENCE_SETTINGS.dateFormat), ["locale", "iso"], "locale"),
108
+ units: pick(get(PREFERENCE_SETTINGS.units), UNITS, "jurisdiction"),
109
+ weekStart: week === "monday" ? 1 : 0,
110
+ timeZoneDisplay: pick(get(PREFERENCE_SETTINGS.timeZoneDisplay), ZONE_DISPLAYS, "event"),
111
+ };
112
+ }
113
+
114
+ const CODES: Record<keyof DisplayPreferences, string> = {
115
+ textSize: "t",
116
+ contrast: "c",
117
+ motion: "m",
118
+ density: "d",
119
+ shortcuts: "s",
120
+ dateFormat: "f",
121
+ units: "u",
122
+ weekStart: "w",
123
+ timeZoneDisplay: "z",
124
+ };
125
+
126
+ /** A short cookie value holding only what differs from the defaults, such as `t.large|d.compact`. */
127
+ export function encodePreferences(prefs: DisplayPreferences): string {
128
+ const parts: string[] = [];
129
+ for (const key of Object.keys(CODES) as (keyof DisplayPreferences)[]) {
130
+ if (prefs[key] === DEFAULT_PREFERENCES[key]) continue;
131
+ const value = prefs[key];
132
+ const text = typeof value === "boolean" ? (value ? "1" : "0") : String(value);
133
+ parts.push(`${CODES[key]}.${text}`);
134
+ }
135
+ return parts.join("|");
136
+ }
137
+
138
+ /** Reads a cookie value written by encodePreferences; anything it does not recognize stays at its default. */
139
+ export function decodePreferences(raw: string | undefined | null): DisplayPreferences {
140
+ if (!raw) return DEFAULT_PREFERENCES;
141
+ const found = new Map<string, string>();
142
+ for (const part of raw.split("|")) {
143
+ const [code, value] = part.split(".");
144
+ if (code && value !== undefined) found.set(code, value);
145
+ }
146
+ const week = found.get(CODES.weekStart);
147
+ return {
148
+ textSize: pick(found.get(CODES.textSize), ["default", "large", "larger"], "default"),
149
+ contrast: pick(found.get(CODES.contrast), ["default", "more"], "default"),
150
+ motion: pick(found.get(CODES.motion), ["system", "reduce", "full"], "system"),
151
+ density: pick(found.get(CODES.density), ["comfortable", "compact"], "comfortable"),
152
+ shortcuts: found.get(CODES.shortcuts) !== "0",
153
+ dateFormat: pick(found.get(CODES.dateFormat), ["locale", "iso"], "locale"),
154
+ units: pick(found.get(CODES.units), UNITS, "jurisdiction"),
155
+ weekStart: week === "1" ? 1 : 0,
156
+ timeZoneDisplay: pick(found.get(CODES.timeZoneDisplay), ZONE_DISPLAYS, "event"),
157
+ };
158
+ }
159
+
160
+ /**
161
+ * The document root's data attributes for the preferences the styles read: data-text-size, data-contrast,
162
+ * data-motion, data-density and data-shortcuts. Defaults add nothing.
163
+ */
164
+ export function documentAttributes(prefs: DisplayPreferences): Record<string, string> {
165
+ const out: Record<string, string> = {};
166
+ if (prefs.textSize !== "default") out["data-text-size"] = prefs.textSize;
167
+ if (prefs.contrast === "more") out["data-contrast"] = "more";
168
+ if (prefs.motion !== "system") out["data-motion"] = prefs.motion;
169
+ if (prefs.density === "compact") out["data-density"] = "compact";
170
+ if (!prefs.shortcuts) out["data-shortcuts"] = "off";
171
+ return out;
172
+ }
173
+
174
+ const ATTRIBUTES = [
175
+ "data-text-size",
176
+ "data-contrast",
177
+ "data-motion",
178
+ "data-density",
179
+ "data-shortcuts",
180
+ ];
181
+
182
+ /** Sets the preference attributes on an element (the document root), removing the ones back at default. */
183
+ export function applyDocumentAttributes(element: HTMLElement, prefs: DisplayPreferences): void {
184
+ const next = documentAttributes(prefs);
185
+ for (const name of ATTRIBUTES) {
186
+ const value = next[name];
187
+ if (value === undefined) element.removeAttribute(name);
188
+ else element.setAttribute(name, value);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Whether single-key shortcuts (J, K, /, ?) are on: off while the document root carries
194
+ * data-shortcuts="off" (me.shortcuts_enabled). Shortcuts with Cmd or Ctrl always work.
195
+ */
196
+ export function singleKeyShortcutsEnabled(
197
+ doc: Document | undefined = globalThis.document,
198
+ ): boolean {
199
+ return doc?.documentElement.dataset["shortcuts"] !== "off";
200
+ }
201
+
202
+ const SQUARE_METERS_PER_SQUARE_FOOT = 0.09290304;
203
+ const METERS_PER_FOOT = 0.3048;
204
+ const METERS_PER_MILE = 1609.344;
205
+
206
+ /**
207
+ * An area stored in square feet in the person's units (locale.units): square feet, or square meters to one
208
+ * decimal. Callers label the unit from their message catalog, since Intl has no area units.
209
+ */
210
+ export function convertArea(
211
+ squareFeet: number,
212
+ units: "imperial" | "metric",
213
+ ): { readonly value: number; readonly unit: "squareFoot" | "squareMeter" } {
214
+ return units === "metric"
215
+ ? {
216
+ value: Math.round(squareFeet * SQUARE_METERS_PER_SQUARE_FOOT * 10) / 10,
217
+ unit: "squareMeter",
218
+ }
219
+ : { value: squareFeet, unit: "squareFoot" };
220
+ }
221
+
222
+ /**
223
+ * A distance stored in meters in the person's units (locale.units): feet up to 1,000 ft and miles
224
+ * beyond, or meters up to 1 km and kilometers beyond, formatted by Intl for the locale.
225
+ */
226
+ export function formatDistance(
227
+ locale: string,
228
+ meters: number,
229
+ units: "imperial" | "metric",
230
+ ): string {
231
+ const unit = (name: string, value: number, digits: number) =>
232
+ new Intl.NumberFormat(locale, {
233
+ style: "unit",
234
+ unit: name,
235
+ maximumFractionDigits: digits,
236
+ }).format(value);
237
+ if (units === "metric") {
238
+ return meters >= 1000 ? unit("kilometer", meters / 1000, 1) : unit("meter", meters, 0);
239
+ }
240
+ const feet = meters / METERS_PER_FOOT;
241
+ return feet >= 1000 ? unit("mile", meters / METERS_PER_MILE, 1) : unit("foot", feet, 0);
242
+ }
@@ -0,0 +1,22 @@
1
+ import { ViewTransition, type ReactElement, type ReactNode } from "react";
2
+
3
+ /** The view transition class the route transition's CSS (styles/components.css) animates. */
4
+ export const ROUTE_TRANSITION_CLASS = "xos-route";
5
+
6
+ /**
7
+ * The page region of a route, for an app's `template.tsx` (Section 11.7, ADR 0118). Next remounts a
8
+ * template when the route below it changes, and the navigation is a React transition, so the page
9
+ * that leaves fades out and the page that arrives fades in through the browser's View Transitions
10
+ * API. Nothing else animates it: refreshes, search parameters and live updates keep the same
11
+ * template, and `default="none"` keeps every other transition still. Under reduced motion the CSS
12
+ * runs no animation, so the page swaps at once.
13
+ *
14
+ * It holds no state and no hooks, so a server template renders it and no route JavaScript is added.
15
+ */
16
+ export function RouteTransition({ children }: { children: ReactNode }): ReactElement {
17
+ return (
18
+ <ViewTransition enter={ROUTE_TRANSITION_CLASS} exit={ROUTE_TRANSITION_CLASS} default="none">
19
+ {children}
20
+ </ViewTransition>
21
+ );
22
+ }
@@ -0,0 +1,92 @@
1
+ import { useRef, type KeyboardEvent, type MouseEvent } from "react";
2
+ import { singleKeyShortcutsEnabled } from "./preferences.ts";
3
+
4
+ /** Items selected by id (Section 11.7). The component only reports the selection; the page keeps it. */
5
+ export interface ItemSelection {
6
+ /** Ids of the selected items. */
7
+ selected: ReadonlySet<string>;
8
+ onChange: (next: ReadonlySet<string>) => void;
9
+ }
10
+
11
+ /** What an item's own gestures can do to the selection; see useItemSelection. */
12
+ export interface ItemSelect {
13
+ isSelected: (id: string) => boolean;
14
+ /** Toggles an item, or with `range` selects every item from the last toggled one to it. */
15
+ toggle: (id: string, range: boolean) => void;
16
+ /** X, Shift+X, Cmd or Ctrl+A and Esc on a focused item; true when the key was handled. */
17
+ onKey: (event: KeyboardEvent<HTMLElement>, id: string) => boolean;
18
+ /** Cmd or Ctrl+click and Shift+click on an item; true when the click was a selection. */
19
+ onClick: (event: MouseEvent<HTMLElement>, id: string) => boolean;
20
+ }
21
+
22
+ const NONE: ItemSelect = {
23
+ isSelected: () => false,
24
+ toggle: () => undefined,
25
+ onKey: () => false,
26
+ onClick: () => false,
27
+ };
28
+
29
+ /**
30
+ * Selection gestures for RecordList, Board and Calendar, the same as DataTable's: X toggles the
31
+ * focused item and Shift+X selects the range from the last toggled one; Cmd or Ctrl+click toggles and
32
+ * Shift+click selects a range; Cmd or Ctrl+A selects every item and Esc clears. X follows the
33
+ * single-key shortcuts setting. `order` lists the item ids in reading order when a gesture happens.
34
+ */
35
+ export function useItemSelection(
36
+ selection: ItemSelection | undefined,
37
+ order: () => readonly string[],
38
+ ): ItemSelect {
39
+ const anchor = useRef<string | null>(null);
40
+ if (!selection) return NONE;
41
+ const toggle = (id: string, range: boolean) => {
42
+ const next = new Set(selection.selected);
43
+ const ids = order();
44
+ const from = anchor.current === null ? -1 : ids.indexOf(anchor.current);
45
+ const to = ids.indexOf(id);
46
+ if (range && from !== -1 && to !== -1 && from !== to) {
47
+ for (const each of ids.slice(Math.min(from, to), Math.max(from, to) + 1)) next.add(each);
48
+ } else {
49
+ if (!next.delete(id)) next.add(id);
50
+ anchor.current = id;
51
+ }
52
+ selection.onChange(next);
53
+ };
54
+ return {
55
+ isSelected: (id) => selection.selected.has(id),
56
+ toggle,
57
+ onKey: (event, id) => {
58
+ const ctrl = event.ctrlKey || event.metaKey;
59
+ const key = event.key.toLowerCase();
60
+ if (ctrl && !event.altKey && !event.shiftKey && key === "a") {
61
+ event.preventDefault();
62
+ selection.onChange(new Set([...selection.selected, ...order()]));
63
+ return true;
64
+ }
65
+ if (!ctrl && !event.altKey && key === "x" && singleKeyShortcutsEnabled()) {
66
+ event.preventDefault();
67
+ toggle(id, event.shiftKey);
68
+ return true;
69
+ }
70
+ if (event.key === "Escape" && selection.selected.size > 0) {
71
+ // Esc clears the selection first; a second Esc reaches the page (closing a peek).
72
+ event.preventDefault();
73
+ event.stopPropagation();
74
+ selection.onChange(new Set());
75
+ return true;
76
+ }
77
+ return false;
78
+ },
79
+ onClick: (event, id) => {
80
+ if (event.metaKey || event.ctrlKey) toggle(id, false);
81
+ else if (event.shiftKey) toggle(id, true);
82
+ else return false;
83
+ event.preventDefault();
84
+ return true;
85
+ },
86
+ };
87
+ }
88
+
89
+ /** Whether a key opens an item's context menu: Shift+F10 or the Context Menu key (Section 11.7). */
90
+ export function isMenuKey(event: KeyboardEvent<HTMLElement>): boolean {
91
+ return (event.key === "F10" && event.shiftKey) || event.key === "ContextMenu";
92
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Shortcut targets (Section 11.2 principle 1): how a shell's key handler finds what a record or view
3
+ * shortcut acts on. A region of the page answers for a scope: the record full page and the side peek
4
+ * for `record`, a collection for `view`. The record and collection templates are regions by their
5
+ * template root (data-template), so no page carries anything more for them; any other region carries
6
+ * data-shortcut-scope, as the side peek does. The control that does an action carries
7
+ * data-shortcut="<registry id>". The handler clicks that control, or focuses it when it is a field,
8
+ * an option of a radio group or tab list, or carries data-shortcut-focus, so a feature binds its
9
+ * control to a shortcut by carrying the attribute.
10
+ *
11
+ * - The attribute may sit on the control itself or on a wrapper around it; in a wrapper, the selected
12
+ * option of a radio group or tab list wins, else the first control.
13
+ * - A control belongs to the nearest region around it, so a peek or a nested collection keeps its own.
14
+ * - Disabled and hidden controls are skipped, except a `hidden` button that carries the attribute
15
+ * itself: a page keeps such a button for an action that has no control of its own on screen.
16
+ */
17
+
18
+ export const SHORTCUT_REGION =
19
+ '[data-shortcut-scope], [data-template="record"], [data-template="collection"]';
20
+
21
+ /** The scope of each template root that is a region. */
22
+ const TEMPLATE_SCOPE: Readonly<Record<string, string>> = { record: "record", collection: "view" };
23
+
24
+ const CONTROL =
25
+ 'button, a[href], input, select, textarea, summary, [contenteditable="true"], [role="button"], [role="radio"], [role="tab"], [role="combobox"], [role="menuitem"]';
26
+ const SELECTED = '[aria-checked="true"], [aria-selected="true"], [aria-pressed="true"]';
27
+ /** Menu and list triggers open on a key or pointer press, not on a click. */
28
+ const POPUP = '[aria-haspopup="menu"], [aria-haspopup="true"], [aria-haspopup="listbox"]';
29
+ /** Options are focused, not clicked: the selected one is already on, and the arrow keys move on. */
30
+ const OPTION = '[role="radio"], [role="tab"]';
31
+ const FIELD =
32
+ 'input:not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="checkbox"]):not([type="radio"]), select, textarea, [contenteditable="true"]';
33
+
34
+ /**
35
+ * The region a key press acts in: the nearest marked region around the focused element or, while
36
+ * focus rests on the page or a container around the regions (the main content), the first region
37
+ * inside it.
38
+ */
39
+ export function shortcutRegion(target: EventTarget | null): Element | null {
40
+ if (!(target instanceof Element)) return null;
41
+ return target.closest(SHORTCUT_REGION) ?? target.querySelector(SHORTCUT_REGION);
42
+ }
43
+
44
+ /** The scope a region answers for, such as `record` or `view`. */
45
+ export function regionScope(region: Element | null): string | null {
46
+ if (!region) return null;
47
+ return (
48
+ region.getAttribute("data-shortcut-scope") ||
49
+ TEMPLATE_SCOPE[region.getAttribute("data-template") ?? ""] ||
50
+ null
51
+ );
52
+ }
53
+
54
+ function usable(control: HTMLElement): boolean {
55
+ if (control.matches(':disabled, [aria-disabled="true"]') || control.closest("[inert]")) {
56
+ return false;
57
+ }
58
+ return control.getClientRects().length > 0;
59
+ }
60
+
61
+ interface Target {
62
+ readonly control: HTMLElement;
63
+ readonly focus: boolean;
64
+ }
65
+
66
+ function targetIn(region: Element, id: string): Target | null {
67
+ for (const marked of region.querySelectorAll<HTMLElement>(`[data-shortcut="${id}"]`)) {
68
+ if (marked.closest(SHORTCUT_REGION) !== region) continue;
69
+ const focus = marked.hasAttribute("data-shortcut-focus");
70
+ if (marked.matches(CONTROL)) {
71
+ const kept = marked.hidden && marked.tagName === "BUTTON";
72
+ if (kept || usable(marked)) return { control: marked, focus };
73
+ continue;
74
+ }
75
+ const candidates = [
76
+ ...marked.querySelectorAll<HTMLElement>(SELECTED),
77
+ ...marked.querySelectorAll<HTMLElement>(CONTROL),
78
+ ];
79
+ const control = candidates.find((c) => c.matches(CONTROL) && usable(c));
80
+ if (control) return { control, focus };
81
+ }
82
+ return null;
83
+ }
84
+
85
+ /**
86
+ * The control in a region that does the shortcut's action, or null when the region has none. Ids
87
+ * are registry ids (letters, digits, dots and hyphens), so they quote safely in the selector.
88
+ */
89
+ export function shortcutControl(region: Element, id: string): HTMLElement | null {
90
+ return targetIn(region, id)?.control ?? null;
91
+ }
92
+
93
+ function openPicker(select: HTMLSelectElement): void {
94
+ try {
95
+ select.showPicker();
96
+ } catch {
97
+ // Not every browser opens a select's picker from script; the select keeps focus.
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Does the shortcut's action in a region: clicks its control, or focuses it (selecting a text
103
+ * input's text, such as a title). A menu trigger opens as the arrow key opens it, with focus on its
104
+ * first item, since menus open on a press rather than a click. Returns false when nothing in the
105
+ * region does the action, so the key goes on.
106
+ */
107
+ export function runShortcut(region: Element, id: string): boolean {
108
+ const target = targetIn(region, id);
109
+ if (!target) return false;
110
+ const { control } = target;
111
+ if (target.focus || control.matches(FIELD) || control.matches(OPTION)) {
112
+ control.focus();
113
+ if (control instanceof HTMLInputElement) control.select();
114
+ // A list to pick from opens where the browser allows it, as assigning an owner expects.
115
+ if (control instanceof HTMLSelectElement && !target.focus) openPicker(control);
116
+ } else if (control.matches(POPUP)) {
117
+ control.focus();
118
+ control.dispatchEvent(
119
+ new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }),
120
+ );
121
+ } else {
122
+ control.click();
123
+ }
124
+ return true;
125
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Theme switching by `data-theme` (ADR 0009): Dark is the default, Light is complete, Sunlight is the
3
+ * Compass maximum-contrast theme, and System follows the operating system between Dark and Light.
4
+ * The token build scopes every color variable to `[data-theme]`, so a switch needs no rebuild.
5
+ */
6
+
7
+ export const themes = ["dark", "light", "sunlight"] as const;
8
+ export type Theme = (typeof themes)[number];
9
+ export type ThemePreference = Theme | "system";
10
+
11
+ export const themePreferences: readonly ThemePreference[] = [...themes, "system"];
12
+
13
+ export function isThemePreference(value: string): value is ThemePreference {
14
+ return (themePreferences as readonly string[]).includes(value);
15
+ }
16
+
17
+ const DARK_QUERY = "(prefers-color-scheme: dark)";
18
+
19
+ function systemPrefersDark(): boolean {
20
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return true;
21
+ return window.matchMedia(DARK_QUERY).matches;
22
+ }
23
+
24
+ /** The theme a preference renders: System resolves to Dark or Light from the operating system. */
25
+ export function resolveTheme(
26
+ preference: ThemePreference,
27
+ prefersDark = systemPrefersDark(),
28
+ ): Theme {
29
+ if (preference === "system") return prefersDark ? "dark" : "light";
30
+ return preference;
31
+ }
32
+
33
+ /**
34
+ * Applies a preference to an element (the document root by default) and, for System, follows
35
+ * operating system changes until the returned cleanup runs.
36
+ */
37
+ export function applyTheme(
38
+ preference: ThemePreference,
39
+ root: HTMLElement = document.documentElement,
40
+ ): () => void {
41
+ root.dataset["theme"] = resolveTheme(preference);
42
+ root.dataset["themePreference"] = preference;
43
+ if (preference !== "system" || typeof window.matchMedia !== "function") return () => undefined;
44
+ const query = window.matchMedia(DARK_QUERY);
45
+ const follow = (event: MediaQueryListEvent) => {
46
+ root.dataset["theme"] = event.matches ? "dark" : "light";
47
+ };
48
+ query.addEventListener("change", follow);
49
+ return () => query.removeEventListener("change", follow);
50
+ }