@digithings-ai/ui 0.0.0-stage → 0.1.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 (424) hide show
  1. package/README.md +48 -2
  2. package/package.json +143 -4
  3. package/src/components/NavShell.tsx +776 -0
  4. package/src/components/SocialRow.tsx +87 -0
  5. package/src/components/Spinner.tsx +27 -0
  6. package/src/components/StackLogo.tsx +40 -0
  7. package/src/components/Terminal.tsx +110 -0
  8. package/src/components/TerminalManifest.tsx +212 -0
  9. package/src/components/ThemeProvider.tsx +271 -0
  10. package/src/components/account/AuthCard.tsx +309 -0
  11. package/src/components/assistant-ui/elements/mermaid-diagram.aui.tsx +63 -0
  12. package/src/components/assistant-ui/elements/mermaid-diagram.tsx +340 -0
  13. package/src/components/brand/BrandWordmark.tsx +32 -0
  14. package/src/components/changelog/ReleaseRail.tsx +81 -0
  15. package/src/components/changelog/index.ts +1 -0
  16. package/src/components/chat/ChatCodeBlock.tsx +67 -0
  17. package/src/components/chat/ChatMarkdown.tsx +66 -0
  18. package/src/components/chat/ChatMarkdownSource.tsx +203 -0
  19. package/src/components/chat/ChatMermaidBlock.tsx +202 -0
  20. package/src/components/chat/ChatMessage.tsx +77 -0
  21. package/src/components/chat/ChatPlayback.tsx +422 -0
  22. package/src/components/chat/ChatStreamCursor.tsx +25 -0
  23. package/src/components/chat/ChatThinking.tsx +113 -0
  24. package/src/components/chat/ChatToolCall.tsx +190 -0
  25. package/src/components/chat/ChatTranscript.tsx +60 -0
  26. package/src/components/chat/ChatWidgetFrame.tsx +107 -0
  27. package/src/components/chat/DigichatBootLoader.tsx +683 -0
  28. package/src/components/chat/DigichatLauncher.tsx +361 -0
  29. package/src/components/chat/DigichatThread.tsx +118 -0
  30. package/src/components/chat/DigichatThreadList.tsx +54 -0
  31. package/src/components/chat/DigichatWordmark.tsx +24 -0
  32. package/src/components/chat/DotMatrix.tsx +650 -0
  33. package/src/components/chat/boot-lab.tsx +766 -0
  34. package/src/components/chat/boot-signal.ts +144 -0
  35. package/src/components/chat/chat-surfaces.ts +52 -0
  36. package/src/components/chat/connection-status.ts +40 -0
  37. package/src/components/chat/digichat-wordmark.ts +355 -0
  38. package/src/components/chat/disclosure-chevron.tsx +49 -0
  39. package/src/components/chat/followable-href.ts +9 -0
  40. package/src/components/chat/gallery-thread/action-icons.tsx +25 -0
  41. package/src/components/chat/gallery-thread/attachment.aui.tsx +262 -0
  42. package/src/components/chat/gallery-thread/block-caret.tsx +439 -0
  43. package/src/components/chat/gallery-thread/cn.ts +1 -0
  44. package/src/components/chat/gallery-thread/composer-keyboard.ts +106 -0
  45. package/src/components/chat/gallery-thread/composer-trigger-popover.aui.tsx +235 -0
  46. package/src/components/chat/gallery-thread/css.d.ts +6 -0
  47. package/src/components/chat/gallery-thread/file.tsx +238 -0
  48. package/src/components/chat/gallery-thread/follow-up-suggestions.aui.tsx +92 -0
  49. package/src/components/chat/gallery-thread/format-json-dump.ts +112 -0
  50. package/src/components/chat/gallery-thread/hooks/use-attachment-src.ts +46 -0
  51. package/src/components/chat/gallery-thread/hooks/use-copy-to-clipboard.ts +9 -0
  52. package/src/components/chat/gallery-thread/image.tsx +519 -0
  53. package/src/components/chat/gallery-thread/markdown-text.tsx +278 -0
  54. package/src/components/chat/gallery-thread/message-error.aui.tsx +105 -0
  55. package/src/components/chat/gallery-thread/message-timing.aui.tsx +143 -0
  56. package/src/components/chat/gallery-thread/private-work-step.tsx +188 -0
  57. package/src/components/chat/gallery-thread/reasoning.tsx +332 -0
  58. package/src/components/chat/gallery-thread/scroll-mode.ts +174 -0
  59. package/src/components/chat/gallery-thread/source.tsx +76 -0
  60. package/src/components/chat/gallery-thread/thread.aui.tsx +1619 -0
  61. package/src/components/chat/gallery-thread/tooltip-icon-button.tsx +53 -0
  62. package/src/components/chat/gallery-thread/typed-welcome-copy.tsx +18 -0
  63. package/src/components/chat/gallery-thread/ui/tooltip.tsx +22 -0
  64. package/src/components/chat/gallery-thread/work-chain-summary.tsx +48 -0
  65. package/src/components/chat/gallery-thread/work-chain.tsx +110 -0
  66. package/src/components/chat/gallery-thread/work-status.ts +66 -0
  67. package/src/components/chat/overlay-scrollbar.tsx +86 -0
  68. package/src/components/chat/sanitize-mermaid-svg.ts +38 -0
  69. package/src/components/chat/skins/SOURCE.md +36 -0
  70. package/src/components/chat/skins/base/attachment.aui.tsx +287 -0
  71. package/src/components/chat/skins/base/aui-default/README.md +45 -0
  72. package/src/components/chat/skins/base/aui-default/collapsible-root.tsx +19 -0
  73. package/src/components/chat/skins/base/aui-default/collapsible.tsx +21 -0
  74. package/src/components/chat/skins/base/aui-default/reasoning.aui.tsx +120 -0
  75. package/src/components/chat/skins/base/aui-default/reasoning.tsx +331 -0
  76. package/src/components/chat/skins/base/aui-default/surfaces.ts +7 -0
  77. package/src/components/chat/skins/base/aui-default/tool-fallback.aui.tsx +772 -0
  78. package/src/components/chat/skins/base/aui-default/tool-group.aui.tsx +241 -0
  79. package/src/components/chat/skins/base/docs-model-options.ts +6 -0
  80. package/src/components/chat/skins/base/elements/assistant-trace.aui.tsx +169 -0
  81. package/src/components/chat/skins/base/elements/composer-trigger-popover.aui.tsx +271 -0
  82. package/src/components/chat/skins/base/elements/directive-text.aui.tsx +29 -0
  83. package/src/components/chat/skins/base/elements/directive-text.tsx +76 -0
  84. package/src/components/chat/skins/base/elements/message-timing.aui.tsx +104 -0
  85. package/src/components/chat/skins/base/elements/model-selector.aui.tsx +140 -0
  86. package/src/components/chat/skins/base/elements/model-selector.tsx +720 -0
  87. package/src/components/chat/skins/base/elements/quote.aui.tsx +290 -0
  88. package/src/components/chat/skins/base/elements/work-chain.tsx +100 -0
  89. package/src/components/chat/skins/base/markdown-text.tsx +294 -0
  90. package/src/components/chat/skins/base/model.ts +1 -0
  91. package/src/components/chat/skins/base/thread.tsx +1050 -0
  92. package/src/components/chat/skins/base/tooltip-icon-button.tsx +56 -0
  93. package/src/components/chat/skins/base/ui/avatar.tsx +115 -0
  94. package/src/components/chat/skins/base/ui/badge.tsx +51 -0
  95. package/src/components/chat/skins/base/ui/button.tsx +67 -0
  96. package/src/components/chat/skins/base/ui/command.tsx +185 -0
  97. package/src/components/chat/skins/base/ui/dialog.tsx +168 -0
  98. package/src/components/chat/skins/base/ui/dot-matrix.tsx +267 -0
  99. package/src/components/chat/skins/base/ui/popover.tsx +97 -0
  100. package/src/components/chat/skins/base/ui/skeleton.tsx +20 -0
  101. package/src/components/chat/skins/base/ui/textarea.tsx +24 -0
  102. package/src/components/chat/skins/base/ui/tooltip.tsx +73 -0
  103. package/src/components/chat/skins/base-assistant-ui/config-provider.tsx +90 -0
  104. package/src/components/chat/skins/base-assistant-ui/defaults.ts +419 -0
  105. package/src/components/chat/skins/base-assistant-ui/index.tsx +44 -0
  106. package/src/components/chat/skins/chatgpt.tsx +448 -0
  107. package/src/components/chat/skins/claude.tsx +380 -0
  108. package/src/components/chat/skins/digichat-composer-toolbar.tsx +151 -0
  109. package/src/components/chat/skins/digichat-context-meter-shares.ts +158 -0
  110. package/src/components/chat/skins/digichat-context-meter.tsx +243 -0
  111. package/src/components/chat/skins/digichat-meter-tone.ts +26 -0
  112. package/src/components/chat/skins/digichat-usage-meter.tsx +103 -0
  113. package/src/components/chat/skins/digichat.tsx +144 -0
  114. package/src/components/chat/skins/expo-react-native.tsx +29 -0
  115. package/src/components/chat/skins/gemini.tsx +322 -0
  116. package/src/components/chat/skins/grok-icon.tsx +34 -0
  117. package/src/components/chat/skins/grok.tsx +369 -0
  118. package/src/components/chat/skins/index.ts +46 -0
  119. package/src/components/chat/skins/perplexity.tsx +462 -0
  120. package/src/components/chat/skins/product-page-assistant/assistant-modal.tsx +73 -0
  121. package/src/components/chat/skins/product-page-assistant/index.tsx +19 -0
  122. package/src/components/chat/skins/product-page-assistant/product-dashboard.tsx +164 -0
  123. package/src/components/chat/skins/react-ink.tsx +97 -0
  124. package/src/components/chat/skins/thread-skin-host-contract.ts +102 -0
  125. package/src/components/chat/skins/thread-skin-view.tsx +156 -0
  126. package/src/components/chat/skins/thread-skins.ts +143 -0
  127. package/src/components/chat/skins/tooltip-icon-button.tsx +35 -0
  128. package/src/components/chat/skins/ui/dropdown-menu.tsx +272 -0
  129. package/src/components/chat/skins/webpage-assistant/assistant-config.ts +238 -0
  130. package/src/components/chat/skins/webpage-assistant/assistant-modal.tsx +78 -0
  131. package/src/components/chat/skins/webpage-assistant/config-provider.tsx +132 -0
  132. package/src/components/chat/skins/webpage-assistant/docs-app.tsx +106 -0
  133. package/src/components/chat/skins/webpage-assistant/docs-article.tsx +109 -0
  134. package/src/components/chat/skins/webpage-assistant/docs-assistant.tsx +73 -0
  135. package/src/components/chat/skins/webpage-assistant/docs-nav.tsx +81 -0
  136. package/src/components/chat/skins/webpage-assistant/host-ui.ts +79 -0
  137. package/src/components/chat/skins/webpage-assistant/index.tsx +12 -0
  138. package/src/components/chat/skins/webpage-assistant/markdown-page.tsx +11 -0
  139. package/src/components/chat/skins/webpage-assistant/pages.ts +9 -0
  140. package/src/components/chat/skins/webpage-assistant/types.ts +178 -0
  141. package/src/components/chat/skins/webpage-assistant/use-mobile.ts +19 -0
  142. package/src/components/chat/stock/SOURCE.md +90 -0
  143. package/src/components/chat/stock/artifacts-panel.tsx +305 -0
  144. package/src/components/chat/stock/attachment.aui.tsx +280 -0
  145. package/src/components/chat/stock/chat-notice.tsx +143 -0
  146. package/src/components/chat/stock/chat-private-mode.tsx +64 -0
  147. package/src/components/chat/stock/deploy-ui-context.tsx +89 -0
  148. package/src/components/chat/stock/embed-chat-prefs.tsx +471 -0
  149. package/src/components/chat/stock/file.tsx +265 -0
  150. package/src/components/chat/stock/follow-up-suggestions.aui.tsx +83 -0
  151. package/src/components/chat/stock/image.tsx +531 -0
  152. package/src/components/chat/stock/index.ts +129 -0
  153. package/src/components/chat/stock/json-view.tsx +140 -0
  154. package/src/components/chat/stock/markdown-text.tsx +319 -0
  155. package/src/components/chat/stock/message-error.aui.tsx +96 -0
  156. package/src/components/chat/stock/message-export-menu.tsx +156 -0
  157. package/src/components/chat/stock/palette-row-preview.tsx +26 -0
  158. package/src/components/chat/stock/reasoning.aui.tsx +120 -0
  159. package/src/components/chat/stock/reasoning.tsx +335 -0
  160. package/src/components/chat/stock/skin-chrome.tsx +81 -0
  161. package/src/components/chat/stock/skin-credit.tsx +63 -0
  162. package/src/components/chat/stock/skin-palette-bridge.tsx +235 -0
  163. package/src/components/chat/stock/skin-palette-context.ts +16 -0
  164. package/src/components/chat/stock/skin-runtime.tsx +52 -0
  165. package/src/components/chat/stock/skin-slash-palette.tsx +393 -0
  166. package/src/components/chat/stock/skin-types.ts +217 -0
  167. package/src/components/chat/stock/stock-chat-prefs-host.tsx +501 -0
  168. package/src/components/chat/stock/stock-send-gate.tsx +94 -0
  169. package/src/components/chat/stock/stock-shape.tsx +39 -0
  170. package/src/components/chat/stock/thread.aui.tsx +645 -0
  171. package/src/components/chat/stock/tool-display.ts +6 -0
  172. package/src/components/chat/stock/tool-fallback.aui.tsx +866 -0
  173. package/src/components/chat/stock/tool-group.aui.tsx +244 -0
  174. package/src/components/chat/stock/tool-surface.ts +120 -0
  175. package/src/components/chat/stock/tooltip-icon-button.tsx +49 -0
  176. package/src/components/chat/stock/ui/avatar.tsx +108 -0
  177. package/src/components/chat/stock/ui/button.tsx +81 -0
  178. package/src/components/chat/stock/ui/collapsible.tsx +21 -0
  179. package/src/components/chat/stock/ui/dialog.tsx +163 -0
  180. package/src/components/chat/stock/ui/skeleton.tsx +13 -0
  181. package/src/components/chat/stock/ui/textarea.tsx +20 -0
  182. package/src/components/chat/stock/ui/tooltip.tsx +68 -0
  183. package/src/components/chat/stock/use-attachment-src.ts +42 -0
  184. package/src/components/chat/stock/use-copy-to-clipboard.ts +29 -0
  185. package/src/components/chat/strip-model-boilerplate.ts +42 -0
  186. package/src/components/chat/suggestion-cycle.tsx +230 -0
  187. package/src/components/chat/suggestion-scroll-row.tsx +81 -0
  188. package/src/components/chat/trace-grouping/chain-group-by.ts +44 -0
  189. package/src/components/chat/trace-grouping/chain-state.ts +141 -0
  190. package/src/components/chat/trace-grouping/index.ts +32 -0
  191. package/src/components/chat/trace-grouping/tool-row.ts +74 -0
  192. package/src/components/chat/trace-grouping/use-deploy-work-disclosure.ts +35 -0
  193. package/src/components/chat/trace-grouping/use-work-chain.ts +103 -0
  194. package/src/components/chat/trace-i18n/core.ts +48 -0
  195. package/src/components/chat/trace-i18n/index.tsx +23 -0
  196. package/src/components/chat/trace-i18n/locales/en.ts +24 -0
  197. package/src/components/chat/trace-i18n/locales/fr.ts +28 -0
  198. package/src/components/chat/trace-i18n/types.ts +35 -0
  199. package/src/components/chat/trace-spacing.ts +249 -0
  200. package/src/components/chat/transcript/activity-view.ts +375 -0
  201. package/src/components/chat/transcript/chat-export.ts +133 -0
  202. package/src/components/chat/transcript/index.ts +10 -0
  203. package/src/components/chat/transcript/save-file.ts +209 -0
  204. package/src/components/chat/transcript/transcript-markdown.ts +428 -0
  205. package/src/components/chat/transcript/types.ts +81 -0
  206. package/src/components/chat/turn-outcome.ts +52 -0
  207. package/src/components/chat/use-warmup-phase.ts +18 -0
  208. package/src/components/chat/work-status-lead.tsx +49 -0
  209. package/src/components/chrome.tsx +264 -0
  210. package/src/components/command-palette/CommandPalette.tsx +404 -0
  211. package/src/components/command-palette/index.ts +6 -0
  212. package/src/components/contact/ContactMailto.tsx +112 -0
  213. package/src/components/contact/index.ts +5 -0
  214. package/src/components/conviction/ConvictionMeter.tsx +40 -0
  215. package/src/components/conviction/SignedConvictionBadge.tsx +26 -0
  216. package/src/components/conviction/index.ts +7 -0
  217. package/src/components/data-layout/BentoGrid.tsx +52 -0
  218. package/src/components/data-layout/CardRail.tsx +150 -0
  219. package/src/components/data-layout/DotMatrixStat.tsx +65 -0
  220. package/src/components/data-layout/FeatureCell.tsx +80 -0
  221. package/src/components/data-layout/MediaFrame.tsx +125 -0
  222. package/src/components/data-layout/ModuleGrid.tsx +735 -0
  223. package/src/components/data-layout/Odometer.tsx +149 -0
  224. package/src/components/data-layout/ProductFrame.tsx +79 -0
  225. package/src/components/data-layout/TestimonialWall.tsx +139 -0
  226. package/src/components/data-layout/index.ts +19 -0
  227. package/src/components/data-layout/treemap.ts +368 -0
  228. package/src/components/deck/DeckStack.tsx +243 -0
  229. package/src/components/desk/DataTable.tsx +373 -0
  230. package/src/components/desk/KpiGrid.tsx +80 -0
  231. package/src/components/desk/Sparkline.tsx +63 -0
  232. package/src/components/desk/StateBlock.tsx +132 -0
  233. package/src/components/desk/index.ts +18 -0
  234. package/src/components/diagrams/ArchitectureDiagram.tsx +350 -0
  235. package/src/components/diagrams/ArchitectureTour.tsx +912 -0
  236. package/src/components/diagrams/TerminalSchematic.tsx +192 -0
  237. package/src/components/diagrams/architecture-svg.tsx +384 -0
  238. package/src/components/diagrams/index.ts +30 -0
  239. package/src/components/diagrams/mermaid-theme.ts +115 -0
  240. package/src/components/diagrams/tour-camera.ts +171 -0
  241. package/src/components/docs/CodeTabs.tsx +90 -0
  242. package/src/components/docs/CopyCommand.tsx +151 -0
  243. package/src/components/docs/DocsLayout.tsx +197 -0
  244. package/src/components/docs/DocsSearch.tsx +97 -0
  245. package/src/components/docs/Endpoint.tsx +135 -0
  246. package/src/components/document/DocumentFrame.tsx +43 -0
  247. package/src/components/document/GlyphList.tsx +51 -0
  248. package/src/components/document/PageTitle.tsx +44 -0
  249. package/src/components/document/Prose.tsx +38 -0
  250. package/src/components/document/Section.tsx +55 -0
  251. package/src/components/document/index.ts +13 -0
  252. package/src/components/effects-chrome/AnnouncementBar.tsx +88 -0
  253. package/src/components/effects-chrome/HorizontalScrollTrack.tsx +461 -0
  254. package/src/components/effects-chrome/Pipeline.tsx +226 -0
  255. package/src/components/effects-chrome/RotatingPrompts.tsx +116 -0
  256. package/src/components/effects-chrome/StackingPanels.tsx +101 -0
  257. package/src/components/effects-chrome/TabStrip.tsx +232 -0
  258. package/src/components/effects-chrome/ToastStack.tsx +201 -0
  259. package/src/components/effects-chrome/horizontal-track-core.ts +70 -0
  260. package/src/components/effects-chrome/index.ts +35 -0
  261. package/src/components/emblems.tsx +188 -0
  262. package/src/components/faviconTheme.ts +23 -0
  263. package/src/components/figure/Figure.tsx +38 -0
  264. package/src/components/figure/index.ts +2 -0
  265. package/src/components/finance-charts/DrawdownPlot.tsx +76 -0
  266. package/src/components/finance-charts/EquityCurve.tsx +71 -0
  267. package/src/components/finance-charts/PriceChart.tsx +119 -0
  268. package/src/components/finance-charts/chart-host.tsx +236 -0
  269. package/src/components/finance-charts/chart-scale.ts +93 -0
  270. package/src/components/finance-charts/demo-data.ts +88 -0
  271. package/src/components/finance-charts/index.ts +46 -0
  272. package/src/components/finance-charts/lw-chart.tsx +292 -0
  273. package/src/components/finance-composites/MarketBar.tsx +243 -0
  274. package/src/components/finance-composites/OrderBook.tsx +133 -0
  275. package/src/components/finance-composites/PerformanceDashboard.tsx +185 -0
  276. package/src/components/finance-composites/SortableTable.tsx +144 -0
  277. package/src/components/finance-composites/StockTicker.tsx +65 -0
  278. package/src/components/finance-composites/SyncedTearsheet.tsx +226 -0
  279. package/src/components/finance-composites/index.ts +28 -0
  280. package/src/components/finance-tearsheet/KpiStrip.tsx +49 -0
  281. package/src/components/finance-tearsheet/LiveBadge.tsx +29 -0
  282. package/src/components/finance-tearsheet/ReturnsMatrix.tsx +321 -0
  283. package/src/components/finance-tearsheet/StrategyTearsheet.tsx +99 -0
  284. package/src/components/finance-tearsheet/TearsheetCard.tsx +54 -0
  285. package/src/components/finance-tearsheet/TradeLogTable.tsx +73 -0
  286. package/src/components/finance-tearsheet/charts.tsx +2624 -0
  287. package/src/components/finance-tearsheet/demo-data.ts +177 -0
  288. package/src/components/finance-tearsheet/format.ts +41 -0
  289. package/src/components/finance-tearsheet/index.ts +120 -0
  290. package/src/components/finance-tearsheet/print.ts +52 -0
  291. package/src/components/finance-tearsheet/risk-bands.ts +48 -0
  292. package/src/components/finance-tearsheet/stats.ts +35 -0
  293. package/src/components/finance-tearsheet/strategy-sheet-model.ts +238 -0
  294. package/src/components/finance-tearsheet/types.ts +58 -0
  295. package/src/components/footer/FooterCells.tsx +109 -0
  296. package/src/components/footer/index.ts +6 -0
  297. package/src/components/graph.tsx +158 -0
  298. package/src/components/icons.tsx +98 -0
  299. package/src/components/landing/PixelWordmark.tsx +36 -0
  300. package/src/components/logos.ts +39 -0
  301. package/src/components/marquee/Marquee.tsx +204 -0
  302. package/src/components/metrics/PerfMetrics.tsx +68 -0
  303. package/src/components/metrics/StatCounter.tsx +104 -0
  304. package/src/components/page-geometry.tsx +177 -0
  305. package/src/components/pricing/Pricing.tsx +174 -0
  306. package/src/components/pricing/PricingMatrix.tsx +219 -0
  307. package/src/components/prose/index.ts +2 -0
  308. package/src/components/prose/prose.tsx +86 -0
  309. package/src/components/repo-activity/RepoActivity.tsx +599 -0
  310. package/src/components/repo-activity/RepoHeatmap.tsx +225 -0
  311. package/src/components/repo-activity/demo.ts +201 -0
  312. package/src/components/repo-activity/fetch.ts +211 -0
  313. package/src/components/repo-activity/heatmap.ts +123 -0
  314. package/src/components/repo-activity/index.ts +28 -0
  315. package/src/components/repo-activity/types.ts +99 -0
  316. package/src/components/stages/NumberedStages.tsx +116 -0
  317. package/src/components/symbols/marks.tsx +116 -0
  318. package/src/components/symbols/terminal-lockup.tsx +136 -0
  319. package/src/components/symbols/terminal-marks.tsx +226 -0
  320. package/src/components/terminal/ChatResponseLoader.tsx +67 -0
  321. package/src/components/terminal/ContainerBootLoader.tsx +82 -0
  322. package/src/components/terminal/TerminalStepCaret.tsx +202 -0
  323. package/src/components/themeInit.ts +24 -0
  324. package/src/components/typography/MotionReveal.tsx +108 -0
  325. package/src/components/typography/TypewriterReveal.tsx +101 -0
  326. package/src/components/typography/WordReveal.tsx +143 -0
  327. package/src/data/modules.ts +378 -0
  328. package/src/data/subsystems.ts +101 -0
  329. package/src/index.ts +587 -0
  330. package/src/lib/base-path.ts +60 -0
  331. package/src/lib/cx.ts +25 -0
  332. package/src/lib/gloomberb.ts +69 -0
  333. package/src/lib/record.ts +19 -0
  334. package/src/lib/stub-payload.ts +183 -0
  335. package/src/lib/useBodyScrollLock.ts +44 -0
  336. package/src/lib/utils.ts +6 -0
  337. package/src/motion/primitives.tsx +105 -0
  338. package/src/motion/scroll-glide.ts +62 -0
  339. package/src/motion/scrolly-core.ts +38 -0
  340. package/src/motion/scrolly.tsx +95 -0
  341. package/src/navigation/HashScrollManager.tsx +167 -0
  342. package/src/navigation/hashScroll.ts +78 -0
  343. package/src/replay/browser.ts +280 -0
  344. package/src/replay/chat-export-run.ts +121 -0
  345. package/src/replay/export-format.ts +25 -0
  346. package/src/replay/export-mount.ts +32 -0
  347. package/src/replay/filename.ts +54 -0
  348. package/src/replay/index.ts +21 -0
  349. package/src/replay/redact-dom.ts +178 -0
  350. package/src/replay/redact.ts +113 -0
  351. package/src/replay/snapshot.ts +659 -0
  352. package/src/styles/account-auth.css +339 -0
  353. package/src/styles/brand-wordmark.css +11 -0
  354. package/src/styles/card-rail.css +120 -0
  355. package/src/styles/chat-aui.css +1143 -0
  356. package/src/styles/chat-core.css +469 -0
  357. package/src/styles/chat-digichat.css +2531 -0
  358. package/src/styles/chat-math.css +70 -0
  359. package/src/styles/chat-skin-palette.css +138 -0
  360. package/src/styles/chat-stock-menu.css +37 -0
  361. package/src/styles/chat-widgets.css +190 -0
  362. package/src/styles/command-palette.css +372 -0
  363. package/src/styles/controls-core.css +2261 -0
  364. package/src/styles/controls-overlay.css +603 -0
  365. package/src/styles/data-layout.css +288 -0
  366. package/src/styles/deck.css +141 -0
  367. package/src/styles/diagrams.css +1860 -0
  368. package/src/styles/digichat-app-theme.css +250 -0
  369. package/src/styles/digichat-boot-loader.css +487 -0
  370. package/src/styles/digichat-launcher.css +377 -0
  371. package/src/styles/docs.css +165 -0
  372. package/src/styles/effects-chrome.css +888 -0
  373. package/src/styles/finance-composites.css +214 -0
  374. package/src/styles/finance-tearsheet-overlay.css +90 -0
  375. package/src/styles/finance-tearsheet.css +945 -0
  376. package/src/styles/marquee.css +168 -0
  377. package/src/styles/metrics.css +82 -0
  378. package/src/styles/nav-shell.css +551 -0
  379. package/src/styles/pricing.css +79 -0
  380. package/src/styles/repo-activity.css +575 -0
  381. package/src/styles/stages.css +69 -0
  382. package/src/styles/terminal-loaders.css +122 -0
  383. package/src/styles/terminal-manifest.css +228 -0
  384. package/src/styles/web-theme.css +1007 -0
  385. package/src/styles/word-reveal.css +204 -0
  386. package/src/theme/apply-theme.ts +214 -0
  387. package/src/theme/index.ts +27 -0
  388. package/src/theme/palette-registry.ts +855 -0
  389. package/src/theme/tui-prefs.ts +64 -0
  390. package/src/ui/alert.tsx +78 -0
  391. package/src/ui/avatar.tsx +104 -0
  392. package/src/ui/badge.tsx +75 -0
  393. package/src/ui/breadcrumb.tsx +61 -0
  394. package/src/ui/button.tsx +67 -0
  395. package/src/ui/card.tsx +139 -0
  396. package/src/ui/checkbox.tsx +27 -0
  397. package/src/ui/collapsible.tsx +32 -0
  398. package/src/ui/date-pager.tsx +325 -0
  399. package/src/ui/dialog.tsx +157 -0
  400. package/src/ui/dropdown-menu.tsx +267 -0
  401. package/src/ui/empty-state.tsx +165 -0
  402. package/src/ui/field.tsx +104 -0
  403. package/src/ui/form.tsx +36 -0
  404. package/src/ui/icon-button.tsx +36 -0
  405. package/src/ui/index.ts +34 -0
  406. package/src/ui/input.tsx +33 -0
  407. package/src/ui/kbd.tsx +30 -0
  408. package/src/ui/label.tsx +32 -0
  409. package/src/ui/pager.tsx +119 -0
  410. package/src/ui/pagination.tsx +126 -0
  411. package/src/ui/radio-group.tsx +51 -0
  412. package/src/ui/search-bar.tsx +85 -0
  413. package/src/ui/segmented-control.tsx +76 -0
  414. package/src/ui/select.tsx +257 -0
  415. package/src/ui/separator.tsx +24 -0
  416. package/src/ui/sheet.tsx +141 -0
  417. package/src/ui/skeleton.tsx +76 -0
  418. package/src/ui/slider.tsx +76 -0
  419. package/src/ui/switch.tsx +31 -0
  420. package/src/ui/table.tsx +179 -0
  421. package/src/ui/tabs.tsx +89 -0
  422. package/src/ui/tags-input.tsx +168 -0
  423. package/src/ui/textarea.tsx +17 -0
  424. package/src/ui/tooltip.tsx +71 -0
@@ -0,0 +1,373 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState, type KeyboardEvent, type ReactNode } from "react";
4
+ import { cn } from "../../lib/utils";
5
+
6
+ /** How a desk column behaves when the table is narrower than its rows.
7
+ * `key` is the row label, pinned left while the rest scrolls.
8
+ * `num` is a number or a date: never wrapped, right-aligned, tabular figures.
9
+ * `text` is prose: the column that gives up width and wraps on word breaks. */
10
+ export type ColumnRole = "key" | "num" | "text";
11
+
12
+ const EMPTY_CELL = new Set(["", "—", "–", "-", "n/a", "N/A"]);
13
+ const NUMBER_CELL = /^[+\-−]?[$€£¥]?\d[\d,]*(\.\d+)?\s?(%|bp|bps|x|k|m|bn|[A-Z]{3})?$/i;
14
+ const DATE_CELL = /^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?Z?)?$/;
15
+
16
+ const isNumeric = (cell: string): boolean => NUMBER_CELL.test(cell) || DATE_CELL.test(cell);
17
+
18
+ /** One role per column. A column is `num` only when every filled cell reads as
19
+ * a number or a date; the first column of a multi-column table is the row label. */
20
+ export function columnRoles(columns: string[], rows: string[][]): ColumnRole[] {
21
+ const width = Math.max(columns.length, ...rows.map((row) => row.length), 0);
22
+ const roles: ColumnRole[] = [];
23
+ for (let col = 0; col < width; col += 1) {
24
+ if (col === 0 && width > 1) {
25
+ roles.push("key");
26
+ continue;
27
+ }
28
+ const filled = rows
29
+ .map((row) => (row[col] ?? "").trim())
30
+ .filter((cell) => !EMPTY_CELL.has(cell));
31
+ roles.push(filled.length > 0 && filled.every(isNumeric) ? "num" : "text");
32
+ }
33
+ return roles;
34
+ }
35
+
36
+ export type DataTableTone = "pos" | "neg";
37
+ export type DataTableRowVariant = "grp" | "total" | "sel";
38
+ type Direction = "asc" | "desc";
39
+
40
+ export type DataTableColumn<Row> = {
41
+ key: string;
42
+ label: string;
43
+ /** Desk column role. `role="num"` and `num: true` are the same thing. */
44
+ role?: ColumnRole;
45
+ /** Numeric column — right-aligned, tabular figures. Shorthand for `role="num"`. */
46
+ num?: boolean;
47
+ /** Long text: wraps within the column and clamps instead of widening the table. */
48
+ wrap?: boolean;
49
+ /** Inline weight bar under the figure, 0–100. */
50
+ bar?: (row: Row) => number | null;
51
+ cell: (row: Row) => ReactNode;
52
+ /** Money semantics for this cell — `--up` / `--down`, never a livery. */
53
+ tone?: (row: Row) => DataTableTone | undefined;
54
+ /** Sortable column. `true` sorts by the cell text/number; a function gives
55
+ * the sort value (null sorts last). */
56
+ sort?: boolean | ((row: Row) => string | number | null | undefined);
57
+ /** Plain text for the filter box; defaults to the sort value or a string/number cell. */
58
+ text?: (row: Row) => string;
59
+ };
60
+
61
+ export type DataTableProps<Row> = {
62
+ rows: Row[];
63
+ cols: DataTableColumn<Row>[];
64
+ rowKey: (row: Row, index: number) => string;
65
+ variant?: (row: Row) => DataTableRowVariant | undefined;
66
+ /** Shown when there are no rows at all. */
67
+ empty?: string;
68
+ /** Shown when a filter matches nothing. */
69
+ noMatch?: string;
70
+ /** Show a text filter box (matches any column's text). */
71
+ filter?: boolean;
72
+ /** Accessible name for the scroll region and the filter box. */
73
+ label?: string;
74
+ /** Row click / Enter / Space. Rows become keyboard-focusable. */
75
+ onRowClick?: (row: Row, index: number) => void;
76
+ /** Pin the first column while scrolling horizontally. */
77
+ stickyFirst?: boolean;
78
+ /** Rows per page; omit for no pagination. */
79
+ pageSize?: number;
80
+ className?: string;
81
+ };
82
+
83
+ const roleOf = <Row,>(column: DataTableColumn<Row>): ColumnRole =>
84
+ column.role ?? (column.num ? "num" : "text");
85
+
86
+ const primitive = (value: ReactNode): string | number | null =>
87
+ typeof value === "string" || typeof value === "number" ? value : null;
88
+
89
+ const sortValue = <Row,>(column: DataTableColumn<Row>, row: Row): string | number | null => {
90
+ const value = typeof column.sort === "function" ? column.sort(row) : primitive(column.cell(row));
91
+ return typeof value === "number" && !Number.isFinite(value) ? null : (value ?? null);
92
+ };
93
+
94
+ /** Nulls sort last in both directions, so a missing figure never heads the table. */
95
+ const compare = (a: string | number | null, b: string | number | null, dir: Direction): number => {
96
+ if (a == null || b == null) return a == null ? (b == null ? 0 : 1) : -1;
97
+ const delta =
98
+ typeof a === "number" && typeof b === "number"
99
+ ? a - b
100
+ : String(a).localeCompare(String(b), undefined, { numeric: true });
101
+ return dir === "asc" ? delta : -delta;
102
+ };
103
+
104
+ /** A `<button>` does not inherit the page face, so the header voice is restated. */
105
+ const SORT_BUTTON =
106
+ "inline-flex w-full cursor-pointer items-center gap-1 bg-transparent p-0 text-left " +
107
+ "font-mono text-[0.64rem] tracking-[0.08em] hover:text-ink " +
108
+ "focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-accent";
109
+
110
+ const CONTROL =
111
+ "cursor-pointer border border-hair px-2 py-0.5 font-mono text-[0.72rem] text-ink " +
112
+ "disabled:cursor-not-allowed disabled:text-ink-mute " +
113
+ "focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-accent";
114
+
115
+ /**
116
+ * DataTable — hairline rows, mono right-aligned figures, sticky header.
117
+ * Every extra is opt-in: sortable columns, text filter, row click, sticky first
118
+ * column, pagination. With none of them it renders the bare table.
119
+ *
120
+ * The sort is a real button per header (keyboard-native) and reports `aria-sort`
121
+ * on its `<th>`; `tone` wears the money colours; `variant` marks group and total
122
+ * rows, which stay pinned below the sorted body.
123
+ */
124
+ export function DataTable<Row>({
125
+ rows,
126
+ cols,
127
+ rowKey,
128
+ variant,
129
+ empty = "no rows",
130
+ noMatch = "no match",
131
+ filter,
132
+ label,
133
+ onRowClick,
134
+ stickyFirst,
135
+ pageSize,
136
+ className,
137
+ }: DataTableProps<Row>) {
138
+ const [query, setQuery] = useState("");
139
+ const [sort, setSort] = useState<{ key: string; dir: Direction } | null>(null);
140
+ const [page, setPage] = useState(0);
141
+ const safe = Array.isArray(rows) ? rows : [];
142
+
143
+ const view = useMemo(() => {
144
+ const needle = query.trim().toLowerCase();
145
+ let out = safe.map((row, index) => ({ row, index }));
146
+ if (needle) {
147
+ out = out.filter(({ row }) =>
148
+ cols.some((column) => {
149
+ const text = column.text ? column.text(row) : sortValue(column, row);
150
+ return text != null && String(text).toLowerCase().includes(needle);
151
+ }),
152
+ );
153
+ }
154
+ const column = sort && cols.find((c) => c.key === sort.key);
155
+ if (column && sort) {
156
+ // Total rows stay pinned at the end; everything else sorts.
157
+ const tail = out.filter(({ row }) => variant?.(row) === "total");
158
+ const body = out.filter(({ row }) => variant?.(row) !== "total");
159
+ body.sort(
160
+ (a, b) =>
161
+ compare(sortValue(column, a.row), sortValue(column, b.row), sort.dir) ||
162
+ a.index - b.index,
163
+ );
164
+ out = [...body, ...tail];
165
+ }
166
+ return out;
167
+ }, [safe, cols, query, sort, variant]);
168
+
169
+ if (!safe.length) return <p className="py-2 font-mono text-[0.72rem] text-ink-mute">{empty}</p>;
170
+
171
+ const pages = pageSize ? Math.max(1, Math.ceil(view.length / pageSize)) : 1;
172
+ const current = Math.min(page, pages - 1);
173
+ const shown = pageSize ? view.slice(current * pageSize, (current + 1) * pageSize) : view;
174
+
175
+ const toggle = (key: string) => {
176
+ setPage(0);
177
+ setSort((active) =>
178
+ !active || active.key !== key
179
+ ? { key, dir: "asc" }
180
+ : active.dir === "asc"
181
+ ? { key, dir: "desc" }
182
+ : null,
183
+ );
184
+ };
185
+
186
+ const onKey = (event: KeyboardEvent, row: Row, index: number) => {
187
+ if (event.target !== event.currentTarget) return;
188
+ if (event.key === "Enter" || event.key === " ") {
189
+ event.preventDefault();
190
+ onRowClick?.(row, index);
191
+ }
192
+ };
193
+
194
+ const table = (
195
+ <div
196
+ // A horizontally-scrolling region has no keyboard route on its own: without
197
+ // a tab stop a keyboard user cannot arrow-scroll the clipped columns.
198
+ tabIndex={0}
199
+ role="region"
200
+ aria-label={label ? `${label} table` : "Table, scrollable horizontally"}
201
+ className={cn(
202
+ "overflow-x-auto rounded-none border border-hair bg-surface text-ink",
203
+ "focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-accent",
204
+ className,
205
+ )}
206
+ >
207
+ <table className="w-full border-collapse text-[0.8125rem]">
208
+ <thead>
209
+ <tr>
210
+ {cols.map((column, position) => {
211
+ const role = roleOf(column);
212
+ const active = sort?.key === column.key ? sort.dir : null;
213
+ return (
214
+ <th
215
+ key={column.key}
216
+ scope="col"
217
+ data-col={role}
218
+ aria-sort={
219
+ active
220
+ ? active === "asc"
221
+ ? "ascending"
222
+ : "descending"
223
+ : column.sort
224
+ ? "none"
225
+ : undefined
226
+ }
227
+ className={cn(
228
+ "sticky top-0 z-[2] border-b border-hair-2 bg-surface px-3 py-2 align-bottom",
229
+ "font-normal tracking-[0.08em] text-ink-mute uppercase text-[0.64rem]",
230
+ role === "num" ? "text-right" : "text-left",
231
+ stickyFirst && position === 0 && "left-0 z-[3]",
232
+ )}
233
+ >
234
+ {column.sort ? (
235
+ <button
236
+ type="button"
237
+ className={cn(SORT_BUTTON, role === "num" && "justify-end")}
238
+ onClick={() => toggle(column.key)}
239
+ >
240
+ {column.label}
241
+ <span aria-hidden="true">
242
+ {active === "asc" ? "▲" : active === "desc" ? "▼" : ""}
243
+ </span>
244
+ </button>
245
+ ) : (
246
+ column.label
247
+ )}
248
+ </th>
249
+ );
250
+ })}
251
+ </tr>
252
+ </thead>
253
+ <tbody>
254
+ {shown.map(({ row, index }) => {
255
+ const state = variant?.(row);
256
+ return (
257
+ <tr
258
+ key={rowKey(row, index)}
259
+ className={cn(
260
+ "border-b border-hair last:border-b-0",
261
+ state === "total" && "border-t border-hair-2 font-semibold",
262
+ state === "grp" && "text-[0.72rem] tracking-[0.08em] text-ink-mute uppercase",
263
+ state === "sel" && "bg-surface-2",
264
+ onRowClick &&
265
+ "cursor-pointer hover:bg-surface-2 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-[-2px] focus-visible:outline-accent",
266
+ )}
267
+ tabIndex={onRowClick ? 0 : undefined}
268
+ onClick={onRowClick ? () => onRowClick(row, index) : undefined}
269
+ onKeyDown={onRowClick ? (event) => onKey(event, row, index) : undefined}
270
+ >
271
+ {cols.map((column, position) => {
272
+ const role = roleOf(column);
273
+ const bar = column.bar?.(row);
274
+ const filled = bar != null && Number.isFinite(bar);
275
+ return (
276
+ <td
277
+ key={column.key}
278
+ data-col={role}
279
+ className={cn(
280
+ "px-3 py-1.5 align-baseline",
281
+ role === "num" &&
282
+ "whitespace-nowrap text-right [font-variant-numeric:tabular-nums]",
283
+ role === "key" && "text-ink",
284
+ role === "text" && "text-ink-soft",
285
+ column.wrap ? "max-w-[28rem]" : role === "text" && "whitespace-nowrap",
286
+ column.tone?.(row) === "pos" && "text-up",
287
+ column.tone?.(row) === "neg" && "text-down",
288
+ stickyFirst && position === 0 && "sticky left-0 z-[1] bg-surface",
289
+ )}
290
+ >
291
+ {column.wrap ? (
292
+ <span className="line-clamp-3 break-words">{column.cell(row)}</span>
293
+ ) : (
294
+ column.cell(row)
295
+ )}
296
+ {filled ? (
297
+ <span className="mt-1 block h-[2px] w-full bg-hair-2" aria-hidden="true">
298
+ <i
299
+ className="block h-full bg-accent"
300
+ style={{ width: `${Math.max(0, Math.min(100, bar))}%` }}
301
+ />
302
+ </span>
303
+ ) : null}
304
+ </td>
305
+ );
306
+ })}
307
+ </tr>
308
+ );
309
+ })}
310
+ </tbody>
311
+ </table>
312
+ </div>
313
+ );
314
+
315
+ if (!filter && !pageSize) return table;
316
+
317
+ return (
318
+ <div className="flex flex-col">
319
+ {filter ? (
320
+ <div className="flex items-center gap-3 pb-2">
321
+ <input
322
+ type="search"
323
+ value={query}
324
+ aria-label={label ? `Filter ${label}` : "Filter rows"}
325
+ placeholder="filter"
326
+ onChange={(event) => {
327
+ setQuery(event.target.value);
328
+ setPage(0);
329
+ }}
330
+ className={cn(
331
+ "min-w-[10rem] flex-1 rounded-none border border-hair bg-transparent px-2 py-1",
332
+ "font-mono text-[0.75rem] text-ink placeholder:text-ink-mute",
333
+ "focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-accent",
334
+ )}
335
+ />
336
+ <span aria-live="polite" className="font-mono text-[0.72rem] text-ink-mute">
337
+ {view.length}/{safe.length}
338
+ </span>
339
+ </div>
340
+ ) : null}
341
+ {view.length ? (
342
+ table
343
+ ) : (
344
+ <p className="py-2 font-mono text-[0.72rem] text-ink-mute">{noMatch}</p>
345
+ )}
346
+ {pageSize && pages > 1 ? (
347
+ <div className="flex items-center gap-3 border-t border-hair pt-2 font-mono text-[0.72rem]">
348
+ <button
349
+ type="button"
350
+ className={CONTROL}
351
+ disabled={current === 0}
352
+ aria-label="Previous page"
353
+ onClick={() => setPage(current - 1)}
354
+ >
355
+ ‹ prev
356
+ </button>
357
+ <span className="text-ink-soft">
358
+ {current + 1} / {pages}
359
+ </span>
360
+ <button
361
+ type="button"
362
+ className={CONTROL}
363
+ disabled={current >= pages - 1}
364
+ aria-label="Next page"
365
+ onClick={() => setPage(current + 1)}
366
+ >
367
+ next ›
368
+ </button>
369
+ </div>
370
+ ) : null}
371
+ </div>
372
+ );
373
+ }
@@ -0,0 +1,80 @@
1
+ import { cn } from "../../lib/utils";
2
+
3
+ export type KpiTone = "pos" | "neg";
4
+
5
+ export type KpiCell = {
6
+ /** Visible cell label. */
7
+ label: string;
8
+ /** The figure as it should read. `null`, `undefined`, `""` and non-finite
9
+ * numbers render the missing mark — never a zero the data did not contain. */
10
+ value: string | number | null | undefined;
11
+ /** Money semantics — `--up` / `--down`, signed P&L only. */
12
+ tone?: KpiTone;
13
+ /** Secondary line under the figure. */
14
+ note?: string;
15
+ /** Accessible name when the figure alone does not read as a value to a
16
+ * screen reader (a glyph, a dash, a bare ratio). Defaults to the visible text. */
17
+ valueLabel?: string;
18
+ };
19
+
20
+ export type KpiGridProps = {
21
+ items: KpiCell[];
22
+ /** Mark shown in place of a figure the payload did not carry. */
23
+ missing?: string;
24
+ className?: string;
25
+ };
26
+
27
+ const read = (value: KpiCell["value"], missing: string): { text: string; absent: boolean } => {
28
+ if (value == null) return { text: missing, absent: true };
29
+ if (typeof value === "number") {
30
+ return Number.isFinite(value)
31
+ ? { text: String(value), absent: false }
32
+ : { text: missing, absent: true };
33
+ }
34
+ const text = value.trim();
35
+ return text ? { text, absent: false } : { text: missing, absent: true };
36
+ };
37
+
38
+ /**
39
+ * KpiGrid — a dense label / figure grid on hairlines, one `<dl>` so each figure
40
+ * is announced with its label. Figures are tabular, so a column of them lines up
41
+ * digit for digit. No motion, so reduced-motion needs no special case.
42
+ */
43
+ export function KpiGrid({ items, missing = "—", className }: KpiGridProps) {
44
+ return (
45
+ <dl
46
+ className={cn(
47
+ "grid grid-cols-[repeat(auto-fit,minmax(8.5rem,1fr))] rounded-none border-t border-l border-hair",
48
+ className,
49
+ )}
50
+ >
51
+ {items.map((item) => {
52
+ const value = read(item.value, missing);
53
+ return (
54
+ <div key={item.label} className="border-r border-b border-hair px-3 py-2">
55
+ <dt className="font-mono text-[0.64rem] tracking-[0.08em] text-ink-mute uppercase">
56
+ {item.label}
57
+ </dt>
58
+ <dd
59
+ data-state={value.absent ? "missing" : "present"}
60
+ aria-label={item.valueLabel}
61
+ className={cn(
62
+ "mt-1 font-mono text-[1.15rem] leading-none [font-variant-numeric:tabular-nums]",
63
+ value.absent ? "text-ink-mute" : "text-ink",
64
+ item.tone === "pos" && !value.absent && "text-up",
65
+ item.tone === "neg" && !value.absent && "text-down",
66
+ )}
67
+ >
68
+ {value.text}
69
+ {item.note ? (
70
+ <span className="mt-1 block font-mono text-[0.68rem] text-ink-mute">
71
+ {item.note}
72
+ </span>
73
+ ) : null}
74
+ </dd>
75
+ </div>
76
+ );
77
+ })}
78
+ </dl>
79
+ );
80
+ }
@@ -0,0 +1,63 @@
1
+ export type SparklineProps = {
2
+ /** The series in reading order. Non-finite points are dropped, not zero-filled. */
3
+ values: (number | null | undefined)[];
4
+ height?: number;
5
+ /** Accessible name. Defaults to a summary derived from the points that are drawn. */
6
+ label?: string;
7
+ /** The figure is printed next to the line — hide the drawing from assistive tech. */
8
+ decorative?: boolean;
9
+ /** Shown when there are fewer than two finite points to draw. */
10
+ placeholder?: string;
11
+ };
12
+
13
+ /** Compact read of a plotted value: integers as-is, floats to four places with
14
+ * trailing zeros stripped. Presentation only — no point is invented. */
15
+ const read = (value: number): string =>
16
+ Number.isInteger(value) ? String(value) : String(Number(value.toFixed(4)));
17
+
18
+ export function Sparkline({
19
+ values: raw,
20
+ height = 56,
21
+ label,
22
+ decorative,
23
+ placeholder = "not enough points",
24
+ }: SparklineProps) {
25
+ const values = (raw ?? []).filter(
26
+ (value): value is number => typeof value === "number" && Number.isFinite(value),
27
+ );
28
+ if (values.length < 2) {
29
+ return <p className="font-mono text-[0.72rem] text-ink-mute">{placeholder}</p>;
30
+ }
31
+
32
+ const low = Math.min(...values);
33
+ const high = Math.max(...values);
34
+ const span = high - low || 1;
35
+ const points = values
36
+ .map((value, index) => {
37
+ const x = (index / (values.length - 1)) * 100;
38
+ const y = 100 - ((value - low) / span) * 100;
39
+ return `${x.toFixed(2)},${y.toFixed(2)}`;
40
+ })
41
+ .join(" ");
42
+ const summary = `sparkline, ${values.length} points, ${read(values[0])} to ${read(values[values.length - 1])}, low ${read(low)}, high ${read(high)}`;
43
+
44
+ return (
45
+ <svg
46
+ viewBox="0 0 100 100"
47
+ preserveAspectRatio="none"
48
+ role={decorative ? undefined : "img"}
49
+ aria-hidden={decorative || undefined}
50
+ aria-label={decorative ? undefined : (label ?? summary)}
51
+ style={{ height }}
52
+ className="block w-full overflow-visible text-accent"
53
+ >
54
+ <polyline
55
+ points={points}
56
+ fill="none"
57
+ stroke="currentColor"
58
+ strokeWidth="1.2"
59
+ vectorEffect="non-scaling-stroke"
60
+ />
61
+ </svg>
62
+ );
63
+ }
@@ -0,0 +1,132 @@
1
+ "use client";
2
+
3
+ import { Component, type ReactNode } from "react";
4
+ import { cn } from "../../lib/utils";
5
+ import { Spinner } from "../Spinner";
6
+
7
+ export type StateKind = "empty" | "loading" | "error" | "withheld";
8
+
9
+ export type StateBlockProps = {
10
+ kind: StateKind;
11
+ /** One line naming the state; defaults per kind. */
12
+ title?: string;
13
+ /** Why the state is showing — the route, the failure, the missing input.
14
+ * A withheld block without a reason is an unexplained blank. */
15
+ reason?: string;
16
+ /** Optional retry, rendered as a real button. Omit when the state is terminal. */
17
+ onRetry?: () => void;
18
+ retryLabel?: string;
19
+ className?: string;
20
+ };
21
+
22
+ const MARKER: Record<StateKind, string> = {
23
+ empty: "[empty]",
24
+ loading: "[loading]",
25
+ error: "[!]",
26
+ withheld: "[withheld]",
27
+ };
28
+
29
+ const TITLE: Record<StateKind, string> = {
30
+ empty: "Nothing here yet.",
31
+ loading: "Loading.",
32
+ error: "Could not load.",
33
+ withheld: "Withheld.",
34
+ };
35
+
36
+ /**
37
+ * StateBlock — one layout for the four states a data surface can be in.
38
+ *
39
+ * Loading is announced (`role="status"`, `aria-busy`) and its spinner stops under
40
+ * reduced motion. An error is announced (`role="alert"`) and carries a `[!]`
41
+ * marker, so its meaning survives someone who cannot see the danger colour.
42
+ * Withheld is a refusal, not a failure: it says why the figure is not shown.
43
+ */
44
+ export function StateBlock({
45
+ kind,
46
+ title,
47
+ reason,
48
+ onRetry,
49
+ retryLabel = "retry",
50
+ className,
51
+ }: StateBlockProps) {
52
+ const role =
53
+ kind === "error"
54
+ ? "alert"
55
+ : kind === "loading"
56
+ ? "status"
57
+ : kind === "withheld"
58
+ ? "note"
59
+ : undefined;
60
+ return (
61
+ <div
62
+ data-state={kind}
63
+ role={role}
64
+ aria-busy={kind === "loading" || undefined}
65
+ className={cn(
66
+ "rounded-none border border-hair bg-surface px-3 py-3",
67
+ kind === "error" && "border-l-danger",
68
+ kind === "withheld" && "bg-surface-2",
69
+ className,
70
+ )}
71
+ >
72
+ <p className="flex items-baseline gap-2 font-mono text-[0.72rem]">
73
+ {kind === "loading" ? <Spinner className="text-ink-mute" /> : null}
74
+ <span
75
+ aria-hidden="true"
76
+ className={cn(
77
+ "shrink-0 tracking-[0.06em]",
78
+ kind === "error" ? "text-danger" : kind === "withheld" ? "text-ink" : "text-ink-mute",
79
+ )}
80
+ >
81
+ {MARKER[kind]}
82
+ </span>
83
+ <span className={cn("text-[0.8125rem]", kind === "error" ? "text-danger" : "text-ink")}>
84
+ {title ?? TITLE[kind]}
85
+ </span>
86
+ </p>
87
+ {reason ? (
88
+ <p className="mt-1 font-mono text-[0.72rem] leading-relaxed text-ink-soft">{reason}</p>
89
+ ) : null}
90
+ {onRetry ? (
91
+ <button
92
+ type="button"
93
+ onClick={onRetry}
94
+ className={cn(
95
+ "mt-2 cursor-pointer rounded-none border border-hair px-2 py-1 font-mono text-[0.72rem] text-ink",
96
+ "hover:border-hair-2 focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-accent",
97
+ )}
98
+ >
99
+ {retryLabel}
100
+ </button>
101
+ ) : null}
102
+ </div>
103
+ );
104
+ }
105
+
106
+ export type BlockBoundaryProps = {
107
+ /** Names the source in the withheld reason — usually the API route. */
108
+ label?: string;
109
+ children: ReactNode;
110
+ };
111
+
112
+ type BoundaryState = { error: string | null };
113
+
114
+ /**
115
+ * BlockBoundary — one block throwing on an odd payload must not take the page
116
+ * down. It is replaced by a withheld state that names the source and the fault.
117
+ * Remount it (`key`) when the payload changes to retry the block.
118
+ */
119
+ export class BlockBoundary extends Component<BlockBoundaryProps, BoundaryState> {
120
+ state: BoundaryState = { error: null };
121
+
122
+ static getDerivedStateFromError(error: unknown): BoundaryState {
123
+ return { error: error instanceof Error ? error.message : "render failed" };
124
+ }
125
+
126
+ render() {
127
+ const { error } = this.state;
128
+ if (!error) return this.props.children;
129
+ const source = this.props.label ? `${this.props.label}: ` : "";
130
+ return <StateBlock kind="withheld" reason={`${source}unexpected response shape (${error})`} />;
131
+ }
132
+ }
@@ -0,0 +1,18 @@
1
+ export {
2
+ DataTable,
3
+ columnRoles,
4
+ type ColumnRole,
5
+ type DataTableColumn,
6
+ type DataTableProps,
7
+ type DataTableRowVariant,
8
+ type DataTableTone,
9
+ } from "./DataTable";
10
+ export { KpiGrid, type KpiCell, type KpiGridProps, type KpiTone } from "./KpiGrid";
11
+ export { Sparkline, type SparklineProps } from "./Sparkline";
12
+ export {
13
+ BlockBoundary,
14
+ StateBlock,
15
+ type BlockBoundaryProps,
16
+ type StateBlockProps,
17
+ type StateKind,
18
+ } from "./StateBlock";