@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,226 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Button } from "../../ui";
5
+
6
+ /**
7
+ * Pipeline — the dashboard's workflow visualization promoted from the
8
+ * design reference (effects/pipeline). Stages flow left→right, a `parallel`
9
+ * column fans out into a dashed group, each node carries its diagnostics
10
+ * (wall time · tokens · model), and selecting a node opens its inputs/outputs
11
+ * and full diagnostics in the detail panel below. Works for sequential and
12
+ * parallel graphs alike; an optional summary strip carries the run-level
13
+ * readout. All copy and numbers arrive via props — preformatted display
14
+ * strings, the component never formats.
15
+ *
16
+ * Selection is uncontrolled (`defaultSelectedId` + `onSelect`); status wears
17
+ * the accent for `running` and fades `queued`, per the reference grammar.
18
+ *
19
+ * Client component (selection state). Wiring (in the consuming app):
20
+ * globals.css @import "@digithings-ai/ui/styles/effects-chrome.css";
21
+ * @source "<path-to>/packages/ui/src/components/effects-chrome";
22
+ */
23
+ export type PipelineStatus = "done" | "running" | "queued";
24
+
25
+ export type PipelineNode = {
26
+ id: string;
27
+ /** Mono node name — "ingest", "backtest" … */
28
+ label: string;
29
+ status: PipelineStatus;
30
+ /**
31
+ * One line saying what this stage does, rendered under the name in the detail
32
+ * panel. Optional, and the only field here that is not a run diagnostic: a
33
+ * pipeline drawn as a *definition* rather than as a completed run has nothing
34
+ * to report for wall time or tokens but still has to explain itself.
35
+ */
36
+ note?: string;
37
+ /** Preformatted wall-time read — "2.41s", "120ms", "—". */
38
+ ms?: string;
39
+ /** Preformatted token read — "8.2k", "—". */
40
+ tokens?: string;
41
+ /** Model / engine chip — "opus", "nautilus", "polars" … */
42
+ model?: string;
43
+ /** What fed in — one preformatted line. Absent for a stage drawn as a
44
+ * definition, where nothing was measured and no artefact was handed over. */
45
+ inputs?: string;
46
+ /** What came out — one preformatted line. */
47
+ outputs?: string;
48
+ /** Preformatted cost read — "$0.11", "—". */
49
+ cost?: string;
50
+ };
51
+
52
+ export type PipelineColumn = {
53
+ id: string;
54
+ /** `parallel` draws the dashed fan-out group around the column's nodes. */
55
+ kind: "step" | "parallel";
56
+ /** Tag over a parallel group — "parallel · 3". */
57
+ label?: string;
58
+ nodes: PipelineNode[];
59
+ };
60
+
61
+ export type PipelineSummaryItem = {
62
+ /** Mono micro-caps key — "wall time", "cost" … */
63
+ label: string;
64
+ /** Preformatted value — "8.4s", "$0.13". */
65
+ value: string;
66
+ /** Accent the value (the live "running" read). */
67
+ running?: boolean;
68
+ };
69
+
70
+ export type PipelineProps = {
71
+ columns: PipelineColumn[];
72
+ /** Optional run-level readout strip above the flow. */
73
+ summary?: PipelineSummaryItem[];
74
+ /** Node opened on first render — defaults to the first node. */
75
+ defaultSelectedId?: string;
76
+ onSelect?: (node: PipelineNode) => void;
77
+ className?: string;
78
+ };
79
+
80
+ export function Pipeline({ columns, summary, defaultSelectedId, onSelect, className }: PipelineProps) {
81
+ const [selId, setSelId] = useState(defaultSelectedId);
82
+ const all = columns.flatMap((c) => c.nodes);
83
+ const sel = all.find((n) => n.id === selId) ?? all[0];
84
+
85
+ return (
86
+ <div className={className}>
87
+ {summary && summary.length > 0 ? (
88
+ <div
89
+ className="pl-summary flex flex-wrap gap-x-[2rem] gap-y-0 rounded-none border border-hair bg-surface/55 px-[1.1rem] py-[0.85rem]"
90
+ role="list"
91
+ >
92
+ {summary.map((s) => (
93
+ <span key={s.label} role="listitem">
94
+ <span className="pl-sum-k">{s.label}</span>
95
+ <span className={`pl-sum-v${s.running ? " pl-running" : ""}`}>{s.value}</span>
96
+ </span>
97
+ ))}
98
+ </div>
99
+ ) : null}
100
+
101
+ <div
102
+ className={`pl-flow flex items-center gap-0 overflow-x-auto pt-[0.4rem] px-[0.1rem] pb-[0.8rem]${
103
+ summary && summary.length > 0 ? " mt-[1rem]" : ""
104
+ }`}
105
+ >
106
+ {columns.map((col, i) => (
107
+ <div key={col.id} className="pl-colwrap flex items-center shrink-0">
108
+ <div className={`pl-col flex flex-col gap-[0.5rem]${col.kind === "parallel" ? " pl-col--par" : ""}`}>
109
+ {col.label ? <span className="pl-col-tag">{col.label}</span> : null}
110
+ {col.nodes.map((n) => (
111
+ <Button
112
+ key={n.id}
113
+ type="button"
114
+ variant="ghost"
115
+ className={`pl-node h-auto items-stretch justify-start border-hair bg-surface px-[0.7rem] py-[0.6rem] font-normal whitespace-normal hover:bg-transparent pl-node--${n.status}${sel && n.id === sel.id ? " is-sel" : ""}`}
116
+ aria-pressed={sel ? n.id === sel.id : false}
117
+ onClick={() => {
118
+ setSelId(n.id);
119
+ onSelect?.(n);
120
+ }}
121
+ >
122
+ <span className="pl-node-head">
123
+ <span className="pl-pip" aria-hidden="true" />
124
+ <span className="pl-node-label">{n.label}</span>
125
+ {/* The pip dot (aria-hidden) and the pl-node--${status}
126
+ class are the only place a node's done/running/queued
127
+ state renders — a screen reader gets aria-pressed
128
+ (selection) but nothing for run status unless this
129
+ node happens to be the one currently open in the
130
+ detail panel below. .pl-sr, not Tailwind's sr-only:
131
+ this is a shared component (see the .tl-sr/.tm-sr
132
+ precedent in terminal-loaders.css/terminal-manifest.css
133
+ for why). */}
134
+ <span className="pl-sr">{n.status}</span>
135
+ </span>
136
+ {n.ms || n.tokens ? (
137
+ <span className="pl-node-diag">
138
+ {n.ms ? <span>{n.ms}</span> : null}
139
+ {n.tokens ? <span className="pl-node-tok">{n.tokens} tok</span> : null}
140
+ </span>
141
+ ) : null}
142
+ </Button>
143
+ ))}
144
+ </div>
145
+ {i < columns.length - 1 ? (
146
+ <span className="pl-conn" aria-hidden="true">
147
+ <svg
148
+ viewBox="0 0 24 24"
149
+ width="18"
150
+ height="18"
151
+ fill="none"
152
+ stroke="currentColor"
153
+ strokeWidth="1.6"
154
+ strokeLinecap="round"
155
+ strokeLinejoin="round"
156
+ >
157
+ <path d="M4 12h15M13 6l6 6-6 6" />
158
+ </svg>
159
+ </span>
160
+ ) : null}
161
+ </div>
162
+ ))}
163
+ </div>
164
+
165
+ {sel ? (
166
+ <div className="pl-detail">
167
+ <div className="pl-detail-head">
168
+ <span className={`pl-pip pl-pip--${sel.status}`} aria-hidden="true" />
169
+ <span className="pl-detail-name">{sel.label}</span>
170
+ {sel.model ? <span className="pl-detail-model">{sel.model}</span> : null}
171
+ <span className={`pl-detail-status pl-detail-status--${sel.status}`}>{sel.status}</span>
172
+ </div>
173
+ {sel.note ? <p className="pl-note">{sel.note}</p> : null}
174
+ {sel.inputs || sel.outputs ? (
175
+ <dl className="pl-io">
176
+ {sel.inputs ? (
177
+ <div>
178
+ <dt>inputs</dt>
179
+ <dd>{sel.inputs}</dd>
180
+ </div>
181
+ ) : null}
182
+ {sel.outputs ? (
183
+ <div>
184
+ <dt>outputs</dt>
185
+ <dd>{sel.outputs}</dd>
186
+ </div>
187
+ ) : null}
188
+ </dl>
189
+ ) : null}
190
+ {/* Only the diagnostics this node actually has. A pipeline drawn as a
191
+ definition has no wall time or token count to report, and printing
192
+ an em dash row for each of them would read as a measurement that
193
+ came back empty rather than one that was never taken. */}
194
+ {[sel.ms, sel.tokens, sel.model, sel.cost].some(Boolean) ? (
195
+ <div className="pl-diag-grid">
196
+ {sel.ms ? (
197
+ <div>
198
+ <span className="pl-diag-k">wall time</span>
199
+ <span className="pl-diag-v">{sel.ms}</span>
200
+ </div>
201
+ ) : null}
202
+ {sel.tokens ? (
203
+ <div>
204
+ <span className="pl-diag-k">tokens</span>
205
+ <span className="pl-diag-v">{sel.tokens}</span>
206
+ </div>
207
+ ) : null}
208
+ {sel.model ? (
209
+ <div>
210
+ <span className="pl-diag-k">model</span>
211
+ <span className="pl-diag-v">{sel.model}</span>
212
+ </div>
213
+ ) : null}
214
+ {sel.cost ? (
215
+ <div>
216
+ <span className="pl-diag-k">cost</span>
217
+ <span className="pl-diag-v">{sel.cost}</span>
218
+ </div>
219
+ ) : null}
220
+ </div>
221
+ ) : null}
222
+ </div>
223
+ ) : null}
224
+ </div>
225
+ );
226
+ }
@@ -0,0 +1,116 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { m, useReducedMotion } from "motion/react";
5
+ import { Button } from "../../ui";
6
+
7
+ /**
8
+ * RotatingPrompts — the rotating hero teaser line promoted from the design
9
+ * reference (effects/rotating-prompts): a mono `>` prompt shell whose text
10
+ * cycles through real developer questions with a rise-in transition and a
11
+ * blinking caret. The teaser is a door, not a poster — consumers pass the
12
+ * prompts and typically link the shell into the live product. Link the
13
+ * shell AS A SIBLING (wrap both in a common container and stretch the
14
+ * anchor over it, e.g. an absolutely-positioned full-cover `<a>`), not by
15
+ * nesting this component INSIDE an `<a>` — the shell renders a real
16
+ * `<button>` (the pause control below), and nesting interactive content
17
+ * inside other interactive content is invalid HTML: some browsers also
18
+ * resolve the ambiguous click target inconsistently, and the button's own
19
+ * preventDefault() cannot reliably cancel a `<button>`-inside-`<a>` parse
20
+ * in every engine the way it can for a normal, non-nested ancestor click
21
+ * listener.
22
+ *
23
+ * aria-live-safe: the shell announces politely; under reduced motion the
24
+ * rotation stops and the FIRST prompt renders statically (also the no-JS /
25
+ * server-rendered state, so the line always reads without JS). The caret's
26
+ * @keyframes blink lives in styles/effects-chrome.css.
27
+ *
28
+ * WCAG 2.2.2 (Pause, Stop, Hide): the rotation runs longer than 5s and starts
29
+ * automatically, so a visible pause control is required beyond the OS-level
30
+ * reduced-motion escape hatch — a click on the shell (or Enter/Space, since
31
+ * it is a real button) toggles it, aria-pressed carries the state.
32
+ *
33
+ * Client component (interval state). Wiring (in the consuming app):
34
+ * globals.css @import "@digithings-ai/ui/styles/effects-chrome.css";
35
+ * @source "<path-to>/packages/ui/src/components/effects-chrome";
36
+ */
37
+ export type RotatingPromptsProps = {
38
+ /** The rotation, in order. First item is the reduced-motion / no-JS state. */
39
+ prompts: string[];
40
+ /** Dwell per prompt, ms. */
41
+ intervalMs?: number;
42
+ className?: string;
43
+ };
44
+
45
+ export function RotatingPrompts({ prompts, intervalMs = 3200, className }: RotatingPromptsProps) {
46
+ const reduced = useReducedMotion();
47
+ const [index, setIndex] = useState(0);
48
+ const [paused, setPaused] = useState(false);
49
+
50
+ useEffect(() => {
51
+ if (reduced || paused || prompts.length < 2) return;
52
+ const timer = setInterval(() => setIndex((i) => (i + 1) % prompts.length), intervalMs);
53
+ return () => clearInterval(timer);
54
+ }, [reduced, paused, prompts.length, intervalMs]);
55
+
56
+ if (prompts.length === 0) return null;
57
+ const current = prompts[reduced ? 0 : index % prompts.length];
58
+ const showPauseControl = !reduced && prompts.length > 1;
59
+
60
+ return (
61
+ <div
62
+ className={`flex items-baseline gap-[0.55rem] rounded-none border border-hair bg-surface px-[1.2rem] py-[1rem] font-mono text-[0.9rem] min-h-[3.4rem]${
63
+ className ? ` ${className}` : ""
64
+ }`}
65
+ aria-live="polite"
66
+ >
67
+ <span className="text-accent" aria-hidden="true">
68
+ &gt;
69
+ </span>
70
+ <m.span
71
+ key={reduced ? "static" : index}
72
+ className="text-ink"
73
+ initial={reduced ? false : { opacity: 0, y: 6 }}
74
+ animate={{ opacity: 1, y: 0 }}
75
+ transition={{ duration: 0.32, ease: [0.22, 1, 0.36, 1] }}
76
+ >
77
+ {current}
78
+ </m.span>
79
+ <span className="prompt-caret" aria-hidden="true" />
80
+ {showPauseControl ? (
81
+ <Button
82
+ type="button"
83
+ variant="ghost"
84
+ className="prompt-pause h-auto w-auto border-0 bg-transparent p-[0.2rem] hover:bg-transparent ms-auto"
85
+ // Belt-and-suspenders even though the header comment now tells
86
+ // consumers to link the shell as a SIBLING, not by nesting it
87
+ // inside an <a>: stopPropagation alone does NOT stop an ancestor
88
+ // <a>'s native navigation — only preventDefault does (the
89
+ // anchor's default action is checked once against the event's
90
+ // defaultPrevented flag after the full bubble phase, regardless
91
+ // of where propagation was stopped) — so both are kept: preventDefault
92
+ // so a click meant to pause never also navigates away, stopPropagation
93
+ // so it never also fires some other ancestor's own click handler.
94
+ onClick={(e) => {
95
+ e.preventDefault();
96
+ e.stopPropagation();
97
+ setPaused((p) => !p);
98
+ }}
99
+ aria-pressed={paused}
100
+ aria-label={paused ? "Resume rotating prompts" : "Pause rotating prompts"}
101
+ >
102
+ {paused ? (
103
+ <svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor" aria-hidden="true">
104
+ <path d="M6 4l14 8-14 8z" />
105
+ </svg>
106
+ ) : (
107
+ <svg viewBox="0 0 24 24" width="13" height="13" fill="currentColor" aria-hidden="true">
108
+ <rect x="5" y="4" width="5" height="16" />
109
+ <rect x="14" y="4" width="5" height="16" />
110
+ </svg>
111
+ )}
112
+ </Button>
113
+ ) : null}
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,101 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, type ReactNode } from "react";
4
+
5
+ /**
6
+ * StackingPanels — the layered between-sections transition promoted from the
7
+ * design reference (effects/stacking-panels; mined from revolut.com). Each
8
+ * panel pins in turn and the next slides up over it (hairline + shadow
9
+ * give the layered seam); as a panel is covered it scales back and dims.
10
+ *
11
+ * The "covered" amount keys off the *next* panel's approach to the pin line —
12
+ * not the panel's own view progress — so it's driven by a manual scroll
13
+ * handler writing to refs (the ResearchPipeline / zoom-morph pattern), which
14
+ * also keeps it verifiable. Scroll mechanics stay here per migrate-vs-leave;
15
+ * only the panel CONTENT arrives via props. Reduced motion: a plain sticky
16
+ * stack, no transform/dim writes. Content is server-rendered and fully
17
+ * readable with no JS.
18
+ *
19
+ * The sticky pin offset reads `--nav-h` from the consuming app (fallback
20
+ * 3.4rem) — see styles/effects-chrome.css.
21
+ *
22
+ * Wiring (in the consuming app):
23
+ * globals.css @import "@digithings-ai/ui/styles/effects-chrome.css";
24
+ * @source "<path-to>/packages/ui/src/components/effects-chrome";
25
+ */
26
+ export type StackingPanel = {
27
+ /** Mono accent index over the title — "01", "2.0" … */
28
+ tag: string;
29
+ /** Serif panel title. */
30
+ title: string;
31
+ /** One or two sentences of mechanism. */
32
+ body: ReactNode;
33
+ };
34
+
35
+ export type StackingPanelsProps = {
36
+ panels: StackingPanel[];
37
+ className?: string;
38
+ };
39
+
40
+ const clamp = (v: number, a: number, b: number) => Math.max(a, Math.min(b, v));
41
+
42
+ export function StackingPanels({ panels, className }: StackingPanelsProps) {
43
+ const panelRefs = useRef<(HTMLElement | null)[]>([]);
44
+
45
+ useEffect(() => {
46
+ const els = panelRefs.current.filter(Boolean) as HTMLElement[];
47
+ if (els.length < 2) return;
48
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
49
+
50
+ const stickyTop = parseFloat(getComputedStyle(els[0]).top) || 70;
51
+
52
+ let ticking = false;
53
+ const onScroll = () => {
54
+ if (ticking) return;
55
+ ticking = true;
56
+ requestAnimationFrame(() => {
57
+ const coverStart = window.innerHeight * 0.55;
58
+ for (let i = 0; i < els.length - 1; i++) {
59
+ const nextTop = els[i + 1].getBoundingClientRect().top;
60
+ const covered = clamp((stickyTop + coverStart - nextTop) / coverStart, 0, 1);
61
+ els[i].style.transform = `translateY(${covered * -14}px) scale(${1 - covered * 0.06})`;
62
+ const dim = els[i].querySelector<HTMLElement>(".stk-dim");
63
+ if (dim) dim.style.opacity = `${covered * 0.55}`;
64
+ }
65
+ ticking = false;
66
+ });
67
+ };
68
+ onScroll();
69
+ window.addEventListener("scroll", onScroll, { passive: true });
70
+ window.addEventListener("resize", onScroll, { passive: true });
71
+ return () => {
72
+ window.removeEventListener("scroll", onScroll);
73
+ window.removeEventListener("resize", onScroll);
74
+ };
75
+ }, [panels.length]);
76
+
77
+ return (
78
+ <div className={className}>
79
+ {panels.map((p, i) => (
80
+ <section
81
+ key={p.tag}
82
+ className="stk-panel"
83
+ ref={(el) => {
84
+ panelRefs.current[i] = el;
85
+ }}
86
+ >
87
+ <div className="relative z-[1] max-w-[34rem]">
88
+ <span className="font-mono text-[0.7rem] tracking-[0.14em] text-accent">{p.tag}</span>
89
+ <h3 className="mt-[0.4rem] font-display font-normal text-[clamp(1.8rem,4vw,2.8rem)] tracking-[-0.015em] text-ink">
90
+ {p.title}
91
+ </h3>
92
+ <p className="mt-[0.7rem] max-w-[42ch] text-[0.95rem] leading-[1.6] text-ink-soft">
93
+ {p.body}
94
+ </p>
95
+ </div>
96
+ <span className="stk-dim" aria-hidden="true" />
97
+ </section>
98
+ ))}
99
+ </div>
100
+ );
101
+ }
@@ -0,0 +1,232 @@
1
+ "use client";
2
+
3
+ import { useCallback, useLayoutEffect, useRef, type ReactNode } from "react";
4
+ import { useReducedMotion } from "motion/react";
5
+ import { Button } from "../../ui";
6
+
7
+ /**
8
+ * TabStrip — the sliding-indicator tab strip promoted from the design
9
+ * reference (chrome/tabs). A proper tablist: role="tablist"/"tab",
10
+ * aria-selected, roving tabindex, ArrowLeft/Right + Home/End keyboard nav.
11
+ * The active indicator is a single absolutely-positioned element whose
12
+ * transform/width (and, for the box dresses, height) are measured from the
13
+ * active tab and written straight to a ref, so the slide is a CSS transition
14
+ * — no `layoutId` (the apps' LazyMotion runs `domAnimation`, which omits
15
+ * layout animations) and no per-frame React state. Survives resize and
16
+ * re-measures on a `dir` flip (ResizeObserver on the strip and its tabs;
17
+ * MutationObserver on <html>'s `dir`/`class`/`data-theme`/`style`), so the
18
+ * ink is correct the instant the document mirrors — not only after the next
19
+ * tab click. Honours reduced motion (indicator jumps, no slide). The ink
20
+ * anchors on
21
+ * `inset-inline-start` and measures the active tab from that same inline-start
22
+ * edge, so it lands under the active tab in both LTR and RTL; arrow-key nav
23
+ * swaps ArrowLeft/ArrowRight under RTL to match the visual order.
24
+ *
25
+ * Three dresses: `underline` for content regions, `pill` for a compact
26
+ * filled-rect mode switch (API name kept; shape is radius 0), `chip` for a
27
+ * wrapping row of bordered chips (dashboard sub-nav — mono face,
28
+ * accent-tinted active chip). `pill` and `chip` position the ink with
29
+ * translate(x, y) + measured height, so `chip` rows may flex-wrap. The strip is controlled (`active` + `onChange`);
30
+ * panels are consumer-owned — wire them with the exported `tabId`/
31
+ * `tabPanelId` helpers:
32
+ *
33
+ * <TabStrip tabs={TABS} active={i} onChange={setI} label="Account view" />
34
+ * <div role="tabpanel" id={tabPanelId("Account view", tab.id)}
35
+ * aria-labelledby={tabId("Account view", tab.id)}>…</div>
36
+ *
37
+ * That per-tab-id default is only correct when the consumer genuinely mounts
38
+ * ONE role="tabpanel" per tab. Content-swapping consumers — one physical
39
+ * panel whose content changes with `active`, which is the common case —
40
+ * must pass `sharedPanel` instead: every tab's aria-controls then points at
41
+ * the CURRENTLY ACTIVE tab's panel id (matching the one real panel, which
42
+ * the consumer keys the same way: id={tabPanelId(label, tabs[active].id)}),
43
+ * rather than each tab dangling a reference to an id that only exists while
44
+ * IT is the active one. Found live in two shipped consumers (#2272) and,
45
+ * closer to home, in this file's own reference showcase before this fix —
46
+ * the tell is a single tabpanel below the strip whose `key` changes with
47
+ * `active` rather than one tabpanel per tab.
48
+ *
49
+ * When the consumer's panels carry no DOM ids at all (e.g. a wrapper
50
+ * adapting legacy children, or no panel semantics apply), pass
51
+ * `linkPanels={false}` to omit `aria-controls` entirely — the attribute is
52
+ * optional in the APG tabs pattern, and a dangling reference is worse than
53
+ * none.
54
+ *
55
+ * Wiring (in the consuming app):
56
+ * globals.css @import "@digithings-ai/ui/styles/effects-chrome.css";
57
+ * @source "<path-to>/packages/ui/src/components/effects-chrome";
58
+ */
59
+ export type TabItem = { id: string; label: ReactNode };
60
+
61
+ export type TabStripProps = {
62
+ tabs: TabItem[];
63
+ /** Index of the active tab. */
64
+ active: number;
65
+ onChange: (index: number) => void;
66
+ /** Accessible tablist name; also seeds the tab/panel id pairing. */
67
+ label: string;
68
+ variant?: "underline" | "pill" | "chip";
69
+ /** Emit aria-controls → tabPanelId(label, id) (default). False = omit. */
70
+ linkPanels?: boolean;
71
+ /**
72
+ * True when the consumer mounts ONE physical role="tabpanel" whose content
73
+ * swaps with `active`, rather than one tabpanel per tab (see the module
74
+ * doc comment above). Every tab's aria-controls then points at the
75
+ * currently active tab's panel id instead of its own — the one id that
76
+ * actually exists in the DOM — so inactive tabs never dangle a reference.
77
+ * No-op when `linkPanels` is false.
78
+ */
79
+ sharedPanel?: boolean;
80
+ className?: string;
81
+ };
82
+
83
+ /** Id base derived from the tablist label (spaces collapsed to dashes). */
84
+ export function tabBaseId(label: string): string {
85
+ return label.replace(/\s+/g, "-");
86
+ }
87
+
88
+ /** DOM id of the tab button for `tabId(label, tab.id)` — pair with aria-labelledby. */
89
+ export function tabId(label: string, id: string): string {
90
+ return `${tabBaseId(label)}-tab-${id}`;
91
+ }
92
+
93
+ /** DOM id the consumer's role="tabpanel" must carry (aria-controls points here). */
94
+ export function tabPanelId(label: string, id: string): string {
95
+ return `${tabBaseId(label)}-panel-${id}`;
96
+ }
97
+
98
+ export function TabStrip({
99
+ tabs,
100
+ active,
101
+ onChange,
102
+ label,
103
+ variant = "underline",
104
+ linkPanels = true,
105
+ sharedPanel = false,
106
+ className,
107
+ }: TabStripProps) {
108
+ const listRef = useRef<HTMLDivElement>(null);
109
+ const inkRef = useRef<HTMLSpanElement>(null);
110
+ const mounted = useRef(false);
111
+ const reduced = useReducedMotion();
112
+
113
+ const position = useCallback(
114
+ (animate: boolean) => {
115
+ const list = listRef.current;
116
+ const ink = inkRef.current;
117
+ if (!list || !ink) return;
118
+ const el = list.querySelectorAll<HTMLButtonElement>('[role="tab"]')[active];
119
+ if (!el) return;
120
+ const listRect = list.getBoundingClientRect();
121
+ const elRect = el.getBoundingClientRect();
122
+ // The ink is anchored with inset-inline-start: 0, so its origin mirrors
123
+ // with the strip. Measure the active tab from that same inline-start
124
+ // edge (physical left in LTR, physical right in RTL) and translate by
125
+ // the physical delta — `transform` has no logical form, so RTL negates.
126
+ const rtl = getComputedStyle(list).direction === "rtl";
127
+ const x = rtl ? listRect.right - elRect.right : elRect.left - listRect.left;
128
+ const y = elRect.top - listRect.top;
129
+ ink.style.transition = animate ? "" : "none";
130
+ // translate(x, y): measured against the strip's padding box — the same
131
+ // origin as the ink's absolute inset-inline-start/top — so the box
132
+ // dresses (pill/chip) track the tab even across wrapped rows.
133
+ ink.style.transform = `translate(${rtl ? -x : x}px, ${y}px)`;
134
+ ink.style.width = `${elRect.width}px`;
135
+ // The underline ink keeps its CSS height (2px, strip-bottom anchored);
136
+ // the box dresses take the tab's own height.
137
+ if (variant !== "underline") ink.style.height = `${elRect.height}px`;
138
+ if (!animate) {
139
+ // flush the jump before restoring the transition so it never animates
140
+ void ink.offsetWidth;
141
+ ink.style.transition = "";
142
+ }
143
+ },
144
+ [active, variant],
145
+ );
146
+
147
+ useLayoutEffect(() => {
148
+ position(mounted.current && !reduced);
149
+ mounted.current = true;
150
+ }, [position, reduced]);
151
+
152
+ // Re-measure whenever the geometry the ink is derived from can change: the
153
+ // strip's own box (font/zoom/layout), any tab's box (label reflow, wrap),
154
+ // and the document's writing direction. A `dir` flip is the case the
155
+ // reference audit caught (/rtl): the ink kept its LTR offset ~372px from the
156
+ // active tab until the next click, because nothing re-ran `position()` — a
157
+ // window `resize` never fires for it. ResizeObserver covers the box changes;
158
+ // MutationObserver on <html>'s `dir` (plus `class`/`data-theme`/`style`,
159
+ // which can restyle the strip through a theme/livery swap) covers the
160
+ // direction flip and any out-of-React dir change, not just the one the kit's
161
+ // own DirectionProvider makes.
162
+ useLayoutEffect(() => {
163
+ const list = listRef.current;
164
+ if (!list) return;
165
+ const remeasure = () => position(false);
166
+ const ro = new ResizeObserver(remeasure);
167
+ ro.observe(list);
168
+ for (const tab of list.querySelectorAll<HTMLElement>('[role="tab"]')) ro.observe(tab);
169
+ const mo = new MutationObserver(remeasure);
170
+ mo.observe(document.documentElement, {
171
+ attributes: true,
172
+ attributeFilter: ["dir", "class", "data-theme", "style"],
173
+ });
174
+ window.addEventListener("resize", remeasure);
175
+ return () => {
176
+ ro.disconnect();
177
+ mo.disconnect();
178
+ window.removeEventListener("resize", remeasure);
179
+ };
180
+ }, [position]);
181
+
182
+ const onKeyDown = (e: React.KeyboardEvent) => {
183
+ // In an RTL strip the tabs run right-to-left, so "next" is visually to the
184
+ // left: ArrowLeft/ArrowRight swap their DOM-order meaning.
185
+ const rtl = listRef.current
186
+ ? getComputedStyle(listRef.current).direction === "rtl"
187
+ : false;
188
+ const nextInDom = (active + 1) % tabs.length;
189
+ const prevInDom = (active - 1 + tabs.length) % tabs.length;
190
+ let next: number;
191
+ if (e.key === "ArrowRight") next = rtl ? prevInDom : nextInDom;
192
+ else if (e.key === "ArrowLeft") next = rtl ? nextInDom : prevInDom;
193
+ else if (e.key === "Home") next = 0;
194
+ else if (e.key === "End") next = tabs.length - 1;
195
+ else return;
196
+ e.preventDefault();
197
+ onChange(next);
198
+ listRef.current?.querySelectorAll<HTMLButtonElement>('[role="tab"]')[next]?.focus();
199
+ };
200
+
201
+ return (
202
+ <div
203
+ ref={listRef}
204
+ className={`tab-strip ${variant}${className ? ` ${className}` : ""}`}
205
+ role="tablist"
206
+ aria-label={label}
207
+ onKeyDown={onKeyDown}
208
+ >
209
+ <span ref={inkRef} className={`tab-ink ${variant}`} aria-hidden="true" />
210
+ {tabs.map((t, i) => (
211
+ <Button
212
+ key={t.id}
213
+ type="button"
214
+ role="tab"
215
+ variant="ghost"
216
+ id={tabId(label, t.id)}
217
+ aria-selected={i === active}
218
+ aria-controls={
219
+ linkPanels
220
+ ? tabPanelId(label, sharedPanel ? tabs[active].id : t.id)
221
+ : undefined
222
+ }
223
+ tabIndex={i === active ? 0 : -1}
224
+ className="tab-btn h-auto inline-block border-0 bg-transparent text-[0.76rem] font-normal hover:bg-transparent"
225
+ onClick={() => onChange(i)}
226
+ >
227
+ {t.label}
228
+ </Button>
229
+ ))}
230
+ </div>
231
+ );
232
+ }