@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,69 @@
1
+ /**
2
+ * Gloomberb attribution + deep-link conventions shared by the frontend
3
+ * surfaces (#4098).
4
+ *
5
+ * The constants mirror
6
+ * `digiquant/src/digiquant/data/gloomberb/attribution.py` — keep the strings
7
+ * and the `?ticker=` deep-link form in sync (spec §7).
8
+ */
9
+
10
+ import { asRecord } from "./record";
11
+
12
+ export const GLOOMBERB_ATTRIBUTION = "Sourced from Gloomberb";
13
+ export const GLOOMBERB_DELAY_NOTICE = "Data delayed up to 15 minutes";
14
+ export const GLOOMBERB_TERMINAL_URL = "https://term.gloom.sh/";
15
+
16
+ export type GloomberbAttribution = {
17
+ attribution: string;
18
+ delayNotice?: string;
19
+ sourceUrl?: string;
20
+ };
21
+
22
+ /** Deep link to the Gloomberb terminal page for *symbol*. */
23
+ export function gloomberbTickerUrl(symbol: string): string {
24
+ return `${GLOOMBERB_TERMINAL_URL}?ticker=${encodeURIComponent(symbol.trim())}`;
25
+ }
26
+
27
+ function parseMaybeJson(value: unknown): unknown {
28
+ if (typeof value !== "string") return value;
29
+ const trimmed = value.trim();
30
+ if (!trimmed.startsWith("{")) return value;
31
+ try {
32
+ return JSON.parse(trimmed);
33
+ } catch {
34
+ return value;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Reads the §7 attribution block off a tool result. Unwraps the `result`
40
+ * envelope (digifetch payloads) and JSON-string results, then requires a
41
+ * non-blank `attribution` string: payloads that do not carry one (Yahoo-backed
42
+ * tools) return null so no surface credits Gloomberb for data it did not
43
+ * source. `source_url` is surfaced only when it is a `term.gloom.sh` link, so
44
+ * an off-terminal URL never wears the branded link.
45
+ */
46
+ export function readGloomberbAttribution(result: unknown): GloomberbAttribution | null {
47
+ let payload = asRecord(parseMaybeJson(result));
48
+ if (payload && "result" in payload) {
49
+ payload = asRecord(parseMaybeJson(payload.result));
50
+ }
51
+ if (
52
+ !payload ||
53
+ typeof payload.attribution !== "string" ||
54
+ payload.attribution.trim() === ""
55
+ ) {
56
+ return null;
57
+ }
58
+ const attribution = payload.attribution;
59
+ const sourceUrl = payload.source_url;
60
+ return {
61
+ attribution,
62
+ delayNotice:
63
+ typeof payload.delay_notice === "string" ? payload.delay_notice : undefined,
64
+ sourceUrl:
65
+ typeof sourceUrl === "string" && sourceUrl.startsWith(GLOOMBERB_TERMINAL_URL)
66
+ ? sourceUrl
67
+ : undefined,
68
+ };
69
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The one JSON-object reader.
3
+ *
4
+ * `asRecord` / `isRecord` had been hand-copied into app modules, and the copies
5
+ * drifted in two ways: some versions let arrays through as records, and some
6
+ * returned `{}` where the caller expected `null`. A record here is a parsed
7
+ * JSON object and nothing else — an array is not a record, because every caller
8
+ * reads a named key off the result and an array answers `undefined` to every
9
+ * key, silently.
10
+ */
11
+
12
+ export function isRecord(value: unknown): value is Record<string, unknown> {
13
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14
+ }
15
+
16
+ /** Narrow to a record, or `null` when the value is anything else. */
17
+ export function asRecord(value: unknown): Record<string, unknown> | null {
18
+ return isRecord(value) ? value : null;
19
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * The one stub detector for every digiquant surface.
3
+ *
4
+ * Four surfaces painted the same "is this the stub envelope?" rule and drifted
5
+ * apart while they did it. Three of them matched numbers by value, one matched
6
+ * them as substrings of the serialised payload, and the substring copy is what
7
+ * put a stub banner over a live NAV block: a real figure that merely contained
8
+ * the fixture's digits (`204.045`, `1204.04`, `154.10`, `180.5`, `54.15`) read as
9
+ * the fixture. Every rule here is value-exact instead, so a longer number that
10
+ * only contains those digits is not the fixture.
11
+ *
12
+ * Two input shapes are accepted, because the surfaces do not agree on one:
13
+ *
14
+ * - A parsed payload (object or array). Checked structurally: numbers by value,
15
+ * the fixture's string spellings as whole values, and the two fixture row
16
+ * shapes as row conjunctions rather than as text.
17
+ * - Display lines (`string[]`, or a bare string). Checked per line by numeric
18
+ * token, deliberately without the row conjunctions: a painted line carries
19
+ * bare numbers, so a conjunction over them would be a guess. The surfaces that
20
+ * hand over display lines have already run the structural check on the payload
21
+ * they painted from, so nothing is lost.
22
+ */
23
+
24
+ /**
25
+ * NAV values the dashboard-api stub fixtures emit. A live series never lands on
26
+ * one of these exactly.
27
+ */
28
+ const STUB_NAV_VALUES = [99.909, 204.04];
29
+
30
+ /**
31
+ * Since-inception style figures the stub fixtures emit. Matched by value so a
32
+ * live percentage that merely starts with the same digits is not the fixture.
33
+ */
34
+ const STUB_SINCE_INCEPTION_VALUES = [103.040192, 104.44808, 3.040191838399986];
35
+
36
+ /**
37
+ * String spellings the fixtures emit. Matched as whole values only, so the
38
+ * padded `204.040` is the fixture and a longer string that contains it is not.
39
+ */
40
+ const STUB_TEXT_VALUES = new Set(["99.909", "204.04", "204.040"]);
41
+
42
+ /**
43
+ * The only benchmark tape the stub fixtures emit. All three parts are required,
44
+ * exactly as the four copies required them: a tape that happens to close at 500
45
+ * on that date is a real tape until it also closes at 515.
46
+ */
47
+ const STUB_TAPE_DATE = "2026-08-20";
48
+ const STUB_TAPE_CLOSES = [500, 515];
49
+
50
+ /**
51
+ * The two ledger rows the stub fixture always emits together. One row alone is
52
+ * ordinary house activity, so both are required.
53
+ */
54
+ const STUB_LEDGER_ROWS = [
55
+ { date: "2026-09-03", ticker: "XLF", value: 54.1 },
56
+ { date: "2026-06-01", ticker: "GLD", value: 180 },
57
+ ];
58
+
59
+ /** JSON round-trips a float through its shortest form, so this only absorbs that. */
60
+ const EPSILON = 1e-6;
61
+
62
+ /** A numeric literal inside a painted line. Signs are kept so `-54.15` stays negative. */
63
+ const NUMBER_TOKEN = /[-+]?\d+(?:\.\d+)?/g;
64
+
65
+ function near(value: number, target: number): boolean {
66
+ return Math.abs(value - target) < EPSILON;
67
+ }
68
+
69
+ /** True when `value` is one of the fixture's figures, to the precision JSON can carry. */
70
+ function isStubNumber(value: number): boolean {
71
+ return (
72
+ STUB_NAV_VALUES.some((target) => near(value, target)) ||
73
+ STUB_SINCE_INCEPTION_VALUES.some((target) => near(value, target))
74
+ );
75
+ }
76
+
77
+ /** Every number painted in a display line, as values rather than as digit runs. */
78
+ function numbersIn(text: string): number[] {
79
+ return (text.match(NUMBER_TOKEN) ?? [])
80
+ .map(Number)
81
+ .filter((value) => Number.isFinite(value));
82
+ }
83
+
84
+ /** A fixture NAV carried as a string rather than as a number. */
85
+ function isStubTextValue(value: string): boolean {
86
+ return STUB_TEXT_VALUES.has(value.trim());
87
+ }
88
+
89
+ /** The price of a ledger row. The surfaces name it `price` or `fill_price`. */
90
+ function rowPrice(row: Record<string, unknown>): number | null {
91
+ for (const key of ["price", "fill_price"]) {
92
+ const value = row[key];
93
+ if (typeof value === "number" && Number.isFinite(value)) return value;
94
+ if (typeof value === "string" && value.trim() !== "") {
95
+ const parsed = Number(value);
96
+ if (Number.isFinite(parsed)) return parsed;
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+
102
+ function matchesLedgerRow(row: Record<string, unknown>, expected: (typeof STUB_LEDGER_ROWS)[number]): boolean {
103
+ const price = rowPrice(row);
104
+ return (
105
+ price != null &&
106
+ near(price, expected.value) &&
107
+ row.date === expected.date &&
108
+ row.ticker === expected.ticker
109
+ );
110
+ }
111
+
112
+ /** Records whichever parts of the stub tape this row carries. */
113
+ function noteStubTapeRow(row: Record<string, unknown>, out: Scan): void {
114
+ if (row.date === STUB_TAPE_DATE) out.tapeDate = true;
115
+ const close = row.close;
116
+ if (typeof close !== "number" || !Number.isFinite(close)) return;
117
+ STUB_TAPE_CLOSES.forEach((target, index) => {
118
+ if (near(close, target)) out.tapeCloses.add(index);
119
+ });
120
+ }
121
+
122
+ /** What one walk of a parsed payload found. The ledger rows accumulate across the whole tree. */
123
+ interface Scan {
124
+ stubValue: boolean;
125
+ tapeDate: boolean;
126
+ tapeCloses: Set<number>;
127
+ ledgerRows: Set<number>;
128
+ }
129
+
130
+ function scan(value: unknown, seen: Set<object>, out: Scan): void {
131
+ if (typeof value === "number") {
132
+ if (isStubNumber(value)) out.stubValue = true;
133
+ return;
134
+ }
135
+ if (typeof value === "string") {
136
+ if (isStubTextValue(value)) out.stubValue = true;
137
+ return;
138
+ }
139
+ if (value == null || typeof value !== "object") return;
140
+ if (seen.has(value)) return;
141
+ seen.add(value);
142
+
143
+ if (Array.isArray(value)) {
144
+ for (const item of value) scan(item, seen, out);
145
+ return;
146
+ }
147
+
148
+ const record = value as Record<string, unknown>;
149
+ noteStubTapeRow(record, out);
150
+ STUB_LEDGER_ROWS.forEach((expected, index) => {
151
+ if (!out.ledgerRows.has(index) && matchesLedgerRow(record, expected)) out.ledgerRows.add(index);
152
+ });
153
+ for (const nested of Object.values(record)) scan(nested, seen, out);
154
+ }
155
+
156
+ /**
157
+ * True when `payload` is the dashboard-api stub envelope rather than a house read.
158
+ *
159
+ * A `legacy_estimate` contract on its own is NOT a stub: three of the four
160
+ * surfaces that painted this rule never treated it as one, and a house NAV series
161
+ * carries that contract on real points. It only appears in fixtures alongside one
162
+ * of the figures above, which is where it is caught.
163
+ */
164
+ export function isStubPayload(payload: unknown): boolean {
165
+ if (typeof payload === "string") return isStubLine(payload);
166
+ if (Array.isArray(payload) && payload.every((line) => typeof line === "string")) {
167
+ return (payload as string[]).some(isStubLine);
168
+ }
169
+
170
+ const out: Scan = { stubValue: false, tapeDate: false, tapeCloses: new Set(), ledgerRows: new Set() };
171
+ scan(payload, new Set<object>(), out);
172
+ const isStubTape = out.tapeDate && out.tapeCloses.size === STUB_TAPE_CLOSES.length;
173
+ return out.stubValue || isStubTape || out.ledgerRows.size === STUB_LEDGER_ROWS.length;
174
+ }
175
+
176
+ /**
177
+ * A painted display line is stub evidence only when it carries a fixture figure
178
+ * as a value. The row conjunctions are deliberately not re-applied here: a line
179
+ * holds bare numbers, so requiring them would be a guess about the format.
180
+ */
181
+ function isStubLine(line: string): boolean {
182
+ return numbersIn(line).some(isStubNumber);
183
+ }
@@ -0,0 +1,44 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+
5
+ /**
6
+ * Reference-counted document.body scroll lock — safe when more than one
7
+ * overlay in the same app (a nav drawer AND a command palette, say) may
8
+ * want the page locked at once, in any open/close order.
9
+ *
10
+ * A naive per-component `document.body.style.overflow = active ? "hidden"
11
+ * : ""` effect is NOT safe here: the moment ANY ONE overlay closes, its
12
+ * cleanup unconditionally clears the property, clobbering a second
13
+ * overlay's lock even if that second overlay is still open — and this
14
+ * happens regardless of which one opened first (an in-session PR review
15
+ * on #2269 confirmed both the naive pattern's cross-overlay collision
16
+ * itself, live-traced against NavShell's own pre-existing lock effect, and
17
+ * that the failure survives even the "close in reverse-open order" case a
18
+ * plain save/restore-on-mount pattern would otherwise handle).
19
+ *
20
+ * This instead increments/decrements a shared counter: the first `active`
21
+ * locker captures the pre-lock value and applies "hidden"; every
22
+ * subsequent concurrent locker is a no-op past the counter bump; only the
23
+ * LAST one releasing restores the true original value. Correct for any
24
+ * nesting or interleaving of open/close across every consumer.
25
+ */
26
+ let lockCount = 0;
27
+ let previousOverflow = "";
28
+
29
+ export function useBodyScrollLock(active: boolean) {
30
+ useEffect(() => {
31
+ if (!active) return;
32
+ if (lockCount === 0) {
33
+ previousOverflow = document.body.style.overflow;
34
+ document.body.style.overflow = "hidden";
35
+ }
36
+ lockCount += 1;
37
+ return () => {
38
+ lockCount -= 1;
39
+ if (lockCount === 0) {
40
+ document.body.style.overflow = previousOverflow;
41
+ }
42
+ };
43
+ }, [active]);
44
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,105 @@
1
+ "use client";
2
+ /**
3
+ * Shared Framer Motion vocabulary. All transitions use the brand easing.
4
+ * Everything degrades under prefers-reduced-motion via useMotionSafe().
5
+ * Animate transform / opacity / pathLength only.
6
+ */
7
+ import {
8
+ LazyMotion,
9
+ domAnimation,
10
+ m,
11
+ useReducedMotion,
12
+ type Variants,
13
+ type Transition,
14
+ } from "motion/react";
15
+ import { useEffect, useState, type ReactNode } from "react";
16
+
17
+ export const EASE: [number, number, number, number] = [0.22, 1, 0.36, 1];
18
+ export const baseTransition: Transition = { duration: 0.6, ease: EASE };
19
+
20
+ /** True when motion is allowed (not reduced). Hydration-safe: SSR cannot know
21
+ * the media query, so the first client render reports motion-safe to match the
22
+ * server markup, then resolves the real preference after mount. For reduced-
23
+ * motion users the primitives swap to their static branch one effect-tick
24
+ * later — no animation ever plays. */
25
+ export function useMotionSafe(): boolean {
26
+ const reduced = useReducedMotion();
27
+ const [mounted, setMounted] = useState(false);
28
+ useEffect(() => setMounted(true), []);
29
+ return mounted ? !reduced : true;
30
+ }
31
+
32
+ /** Wrap an app/tree once so `m.*` components have animation features (small bundle). */
33
+ export function MotionProvider({ children }: { children: ReactNode }) {
34
+ return <LazyMotion features={domAnimation} strict>{children}</LazyMotion>;
35
+ }
36
+
37
+ export const revealVariants: Variants = {
38
+ hidden: { opacity: 0, y: 16 },
39
+ show: { opacity: 1, y: 0, transition: baseTransition },
40
+ };
41
+
42
+ export const staggerParent: Variants = {
43
+ hidden: {},
44
+ show: { transition: { staggerChildren: 0.06, delayChildren: 0.04 } },
45
+ };
46
+
47
+ /** Fade + rise when scrolled into view (once). Falls back to static if reduced. */
48
+ export function Reveal({
49
+ children,
50
+ className,
51
+ as = "div",
52
+ delay = 0,
53
+ }: {
54
+ children: ReactNode;
55
+ className?: string;
56
+ as?: keyof typeof m;
57
+ delay?: number;
58
+ }) {
59
+ const safe = useMotionSafe();
60
+ const Comp = m[as] as typeof m.div;
61
+ if (!safe) return <Comp className={className}>{children}</Comp>;
62
+ return (
63
+ <Comp
64
+ className={className}
65
+ data-motion=""
66
+ initial="hidden"
67
+ whileInView="show"
68
+ viewport={{ once: true, margin: "0px 0px -10% 0px" }}
69
+ variants={{ hidden: { opacity: 0, y: 16 }, show: { opacity: 1, y: 0, transition: { ...baseTransition, delay } } }}
70
+ >
71
+ {children}
72
+ </Comp>
73
+ );
74
+ }
75
+
76
+ /** Stagger container; children should use <Reveal> or variants={revealVariants}. */
77
+ export function Stagger({ children, className }: { children: ReactNode; className?: string }) {
78
+ const safe = useMotionSafe();
79
+ if (!safe) return <div className={className}>{children}</div>;
80
+ return (
81
+ <m.div
82
+ className={className}
83
+ data-motion=""
84
+ initial="hidden"
85
+ whileInView="show"
86
+ viewport={{ once: true, margin: "0px 0px -10% 0px" }}
87
+ variants={staggerParent}
88
+ >
89
+ {children}
90
+ </m.div>
91
+ );
92
+ }
93
+
94
+ /** One-shot hero entrance on mount (no scroll). */
95
+ export function HeroEntrance({ children, className }: { children: ReactNode; className?: string }) {
96
+ const safe = useMotionSafe();
97
+ if (!safe) return <div className={className}>{children}</div>;
98
+ return (
99
+ <m.div className={className} data-motion="" initial="hidden" animate="show" variants={staggerParent}>
100
+ {children}
101
+ </m.div>
102
+ );
103
+ }
104
+
105
+ export { m };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Skip-through / rest glide for a pinned scrolly section.
3
+ *
4
+ * The page scroll stays in charge. These helpers never return a scroll offset:
5
+ * while the reader is moving, the section shows the scroll-linked pose as-is
6
+ * (skip-through). Once the page has been still, the camera may ease from that
7
+ * live pose back onto the narrated boxes. A glide that is already more than
8
+ * half done is left where the reader stopped — rewinding it would fight the
9
+ * skip, and the page is not pulled to finish the beat.
10
+ */
11
+
12
+ const clamp01 = (n: number): number => Math.max(0, Math.min(1, n));
13
+
14
+ /** Quiet gap after the last scroll sample before a rest glide may start. */
15
+ export const REST_QUIET_MS = 180;
16
+
17
+ /** How long the camera takes to settle onto the parked boxes. */
18
+ export const REST_GLIDE_MS = 520;
19
+
20
+ /** Below this the camera is already on the narrated boxes. */
21
+ export const REST_PARKED_GLIDE = 0.02;
22
+
23
+ /**
24
+ * At or above this the reader has committed to the next beat. Settling would
25
+ * rewind the picture under a stationary page.
26
+ */
27
+ export const REST_COMMIT_GLIDE = 0.5;
28
+
29
+ export interface RestGlideInput {
30
+ /** Milliseconds since scroll position last changed. */
31
+ quietMs: number;
32
+ /** Scroll-linked camera blend, 0 parked on this beat and 1 on the next. */
33
+ liveGlide: number;
34
+ quietThreshold?: number;
35
+ }
36
+
37
+ /**
38
+ * Whether the camera should ease back to the parked boxes. False while the
39
+ * page is still moving, when the beat is already parked, and when the glide
40
+ * is committed — those stay with the page scroll.
41
+ */
42
+ export function shouldStartRestGlide(input: RestGlideInput): boolean {
43
+ const quiet = input.quietThreshold ?? REST_QUIET_MS;
44
+ if (input.quietMs < quiet) return false;
45
+ const live = clamp01(input.liveGlide);
46
+ return live > REST_PARKED_GLIDE && live < REST_COMMIT_GLIDE;
47
+ }
48
+
49
+ /** Smoothstep progress of a rest glide, 0 at the start and 1 at `durationMs`. */
50
+ export function restBlendAmount(elapsedMs: number, durationMs = REST_GLIDE_MS): number {
51
+ if (durationMs <= 0) return 1;
52
+ const t = clamp01(elapsedMs / durationMs);
53
+ return t * t * (3 - 2 * t);
54
+ }
55
+
56
+ /**
57
+ * The glide the camera should paint. `blend` 0 is the live scroll pose;
58
+ * `blend` 1 is the parked pose of the narrated beat.
59
+ */
60
+ export function applyRestBlend(liveGlide: number, blend: number): number {
61
+ return clamp01(liveGlide) * (1 - clamp01(blend));
62
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Pure logic for the ScrollyFeatures primitive (pinned section + progress rail).
3
+ * Framework-free and side-effect-free so it can be unit-tested without a DOM —
4
+ * the React hook in `./scrolly.tsx` wraps these with `useScroll` + `matchMedia`.
5
+ *
6
+ * Generalizes the scroll→slide mapping that `ScrollyGraph` (components/graph.tsx)
7
+ * and digiquant-web's `PipelineScene` both hand-roll.
8
+ */
9
+
10
+ /**
11
+ * Map a 0..1 scroll progress to an active slide index, clamped to
12
+ * `[0, slideCount - 1]`. `floor(progress * slideCount)` gives each slide an
13
+ * equal dwell; progress exactly at 1 maps to the last slide (not out of range).
14
+ */
15
+ export function progressToIndex(progress: number, slideCount: number): number {
16
+ if (slideCount <= 0) return 0;
17
+ const raw = Math.floor(progress * slideCount);
18
+ return Math.max(0, Math.min(slideCount - 1, raw));
19
+ }
20
+
21
+ /**
22
+ * Vertical scroll budget (in `vh`) for a pinned scrolly: one dwell per slide.
23
+ * Derived from the slide count (content) rather than a hardcoded viewport
24
+ * multiple — the #1198 lesson (a pin's scroll budget must track its content so
25
+ * the next section can't overlap the pin and it holds up under browser zoom).
26
+ * Consumers with tall/variable slides can pass a larger `vhPerSlide`.
27
+ */
28
+ export function scrollyTrackHeightVh(slideCount: number, vhPerSlide = 90): number {
29
+ return Math.max(1, slideCount) * vhPerSlide;
30
+ }
31
+
32
+ /**
33
+ * Media query that forces the flow-layout fallback: small viewport OR
34
+ * reduced-motion. When it matches, consumers render every slide in normal flow
35
+ * (no pinning) — satisfying `prefers-reduced-motion: reduce` by showing all
36
+ * slides, and giving small screens a static stepper.
37
+ */
38
+ export const STEPPER_MEDIA_QUERY = "(max-width: 860px), (prefers-reduced-motion: reduce)";
@@ -0,0 +1,95 @@
1
+ "use client";
2
+ /**
3
+ * ScrollyFeatures — shared pinned-section + progress-rail primitive (Graphite
4
+ * pattern). Desktop: a `position: sticky` pin whose active slide advances with
5
+ * scroll. Small viewport / reduced-motion: consumers render every slide in flow
6
+ * (see `stepper`). Generalizes the scroll→slide logic that `ScrollyGraph`
7
+ * (components/graph.tsx) and digiquant-web's `PipelineScene` hand-roll.
8
+ *
9
+ * The hook owns state only; markup + CSS stay with the consumer (the
10
+ * `scrolly-*` classes in styles/web-theme.css are the reference skin). Pure math
11
+ * lives in ./scrolly-core (unit-tested).
12
+ *
13
+ * Usage:
14
+ * const trackRef = useRef<HTMLDivElement>(null);
15
+ * const { activeIndex, stepper } = useScrollyFeatures(trackRef, { slideCount: slides.length });
16
+ * if (stepper) return <FlowStepper slides={slides} />; // all slides, no pin
17
+ * return (
18
+ * <div ref={trackRef} className="scrolly-track"
19
+ * style={{ height: `${scrollyTrackHeightVh(slides.length)}vh` }}>
20
+ * <div className="scrolly-pin">…active slide…<ScrollyRail count={slides.length} activeIndex={activeIndex} /></div>
21
+ * </div>
22
+ * );
23
+ *
24
+ * Docs note (EVOLUTION.md §9): **max one pinned section per page.**
25
+ */
26
+ import { useEffect, useRef, useState, type RefObject } from "react";
27
+ import { useScroll, useMotionValueEvent } from "motion/react";
28
+ import { progressToIndex, STEPPER_MEDIA_QUERY } from "./scrolly-core";
29
+
30
+ export { progressToIndex, scrollyTrackHeightVh, STEPPER_MEDIA_QUERY } from "./scrolly-core";
31
+
32
+ export interface ScrollyFeatures {
33
+ /** Scroll-driven active slide index, clamped to the slide range. */
34
+ activeIndex: number;
35
+ /** True on small viewport OR reduced-motion — render all slides in flow, unpinned. */
36
+ stepper: boolean;
37
+ }
38
+
39
+ export interface UseScrollyFeaturesOptions {
40
+ slideCount: number;
41
+ /** Fired once per active-slide change (not per scroll frame). */
42
+ onSlideChange?: (index: number) => void;
43
+ }
44
+
45
+ export function useScrollyFeatures(
46
+ trackRef: RefObject<HTMLElement | null>,
47
+ { slideCount, onSlideChange }: UseScrollyFeaturesOptions,
48
+ ): ScrollyFeatures {
49
+ const [activeIndex, setActiveIndex] = useState(0);
50
+ const [stepper, setStepper] = useState(false);
51
+ const prevIndex = useRef(0);
52
+
53
+ useEffect(() => {
54
+ if (typeof matchMedia !== "function") return;
55
+ const mq = matchMedia(STEPPER_MEDIA_QUERY);
56
+ const apply = () => setStepper(mq.matches);
57
+ apply();
58
+ mq.addEventListener("change", apply);
59
+ return () => mq.removeEventListener("change", apply);
60
+ }, []);
61
+
62
+ const { scrollYProgress } = useScroll({ target: trackRef, offset: ["start start", "end end"] });
63
+ useMotionValueEvent(scrollYProgress, "change", (p) => {
64
+ const i = progressToIndex(p, slideCount);
65
+ if (i !== prevIndex.current) {
66
+ prevIndex.current = i;
67
+ setActiveIndex(i);
68
+ onSlideChange?.(i);
69
+ }
70
+ });
71
+
72
+ return { activeIndex, stepper };
73
+ }
74
+
75
+ /**
76
+ * Progress rail — one tick per slide, `.on` at the active index. Decorative
77
+ * (`aria-hidden`); slide state is conveyed by the slide content itself.
78
+ */
79
+ export function ScrollyRail({
80
+ count,
81
+ activeIndex,
82
+ className = "scrolly-rail",
83
+ }: {
84
+ count: number;
85
+ activeIndex: number;
86
+ className?: string;
87
+ }) {
88
+ return (
89
+ <div className={className} aria-hidden="true">
90
+ {Array.from({ length: count }, (_, i) => (
91
+ <span key={i} className={`scrolly-tick${i === activeIndex ? " on" : ""}`} />
92
+ ))}
93
+ </div>
94
+ );
95
+ }