@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,735 @@
1
+ "use client";
2
+
3
+ import {
4
+ useEffect,
5
+ useLayoutEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type CSSProperties,
10
+ type ReactNode,
11
+ } from "react";
12
+
13
+ import type { ModuleNode } from "../../data/modules";
14
+ import { Reveal } from "../../motion/primitives";
15
+ import { useScrollyFeatures } from "../../motion/scrolly";
16
+ import { scrollyTrackHeightVh } from "../../motion/scrolly-core";
17
+ import { CopyCommand } from "../docs/CopyCommand";
18
+ import { StackRow } from "../StackLogo";
19
+ import {
20
+ treemapAnchored,
21
+ treemapAreasConstrained,
22
+ type TreemapMins,
23
+ type TreemapRect,
24
+ } from "./treemap";
25
+
26
+ /**
27
+ * The module mosaic: one angular tile per module, each tile's area its share
28
+ * of the stack's lines of code, and a focus that walks the tiles as the page
29
+ * scrolls. Promoted from digithings-web's landing (#4429).
30
+ *
31
+ * - **Order is size.** Biggest module top-left, smallest last; roadmap modules
32
+ * (no lines) sink to the end. `graphOrder` breaks ties. The reading order is
33
+ * the scroll order, so the focus walks biggest to smallest.
34
+ * - **Packing.** A squarified treemap (`treemapAreasConstrained`) solved in
35
+ * pixels against the measured box. The rest layout is solved once and never
36
+ * moves; the focused tile keeps its rest top-left and grows there while the
37
+ * others repack around it (`treemapAnchored`). The open card is measured
38
+ * before that solve, so the morph eases once onto the size the copy needs
39
+ * (`web-theme.css`, `.dg-mosaic--rows .dg-cell`).
40
+ * - **Focus.** The pinned track's progress maps to a tile. The first tile
41
+ * holds longer than each later one. Focus only engages while the stage is
42
+ * centred in the viewport, and clicking a tile jumps the scroll to its step
43
+ * (instant, so exactly one tile animates). The focused tile shows its facts,
44
+ * lead, packages, the compose command (click to copy) and, with `onAsk`, an
45
+ * "ask digichat" control.
46
+ * - **Small screens / reduced motion.** `useScrollyFeatures` flips to
47
+ * `stepper`: the same tiles stack one per row, all open, and the one at the
48
+ * focal line lights (all of them under reduced motion) — paint only, so the column
49
+ * never changes height under the reader.
50
+ * - **Density tiers.** Each tile's measured size sets `data-tier`
51
+ * (full / medium / mini) with hysteresis, so a tile shows what fits and the
52
+ * name and version never clip.
53
+ */
54
+ export interface ModuleGridItem {
55
+ module: ModuleNode;
56
+ /** Lines of code. `null` marks a roadmap module: half the floor weight, sorted last. */
57
+ lines: number | null;
58
+ /** Declared version, or `null` (renders "roadmap" rather than a fabricated 0.0.0). */
59
+ version: string | null;
60
+ /** Extra facts after the line count, e.g. "12 endpoints · 9 MCP tools". */
61
+ counts?: string;
62
+ /** Replaces the text name in the tile head, e.g. the module's hero banner. */
63
+ mark?: ReactNode;
64
+ /** Optional external link shown as a small arrow beside the name mark. */
65
+ link?: { href: string; label: string };
66
+ }
67
+
68
+ export interface ModuleGridProps {
69
+ items: ModuleGridItem[];
70
+ /** Shows the focused tile's "ask digichat" control and handles it. */
71
+ onAsk?: (id: string) => void;
72
+ /** Accessible name of the tile list. */
73
+ label?: string;
74
+ /** Scroll length per module on the pinned track, in vh. */
75
+ vhPerModule?: number;
76
+ className?: string;
77
+ }
78
+
79
+ /** The exponent on the normalised log weight. >1 spreads the field. */
80
+ const WEIGHT = 1.5;
81
+ /** The share of the biggest module the smallest shipped module still draws, before weighting. */
82
+ const WEIGHT_FLOOR = 0.12;
83
+ /**
84
+ * Roadmap modules draw half the floor: a module with no code is not a module
85
+ * with no size, but it is honestly smaller than the smallest shipped one.
86
+ * Full-floor roadmaps would draw the same rect as the smallest shipped module.
87
+ */
88
+ const ROADMAP_WEIGHT = WEIGHT_FLOOR * 0.5;
89
+ /**
90
+ * How the focused module's weight is lifted while the layout is solved: the
91
+ * multiplier keeps big tiles modest, the floor guarantees small tiles room
92
+ * for their detail. Unfocused tiles keep their exact weight ratios.
93
+ */
94
+ const FOCUS_MULT = 1.3;
95
+ const FOCUS_FLOOR = 0.5;
96
+ /**
97
+ * Rest tiles fit the longest module name at full size, plus the tile padding,
98
+ * with the version free to wrap under it. The anchored solve honours this
99
+ * floor, so a focused neighbour cannot squeeze a title past the box edge.
100
+ * Focused tiles fit their detail (refined per module by `needExtra`).
101
+ */
102
+ const REST_MIN: TreemapMins = { minW: 176, minH: 96 };
103
+ const FOCUS_MIN: TreemapMins = { minW: 300, minH: 260 };
104
+ /** The gutter between packed tiles, in px — each rect is inset by half. */
105
+ const TREEMAP_GAP = 8;
106
+ /** The stacked face's focal line, as a share of the viewport height. */
107
+ const STACK_FOCAL = 0.38;
108
+ /**
109
+ * The first tile's scroll share, relative to each later tile. A flat walk
110
+ * plus the old 8% lead-in spent almost the whole first step before focus
111
+ * engaged, so the biggest module (digiquant) flashed past.
112
+ */
113
+ const FIRST_DWELL = 1.75;
114
+ const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
115
+
116
+ /** Hydration-safe reduced-motion read: `false` on the server and first client render. */
117
+ function usePrefersReducedMotion(): boolean {
118
+ const [reduced, setReduced] = useState(false);
119
+ useEffect(() => {
120
+ if (typeof matchMedia !== "function") return;
121
+ const mq = matchMedia(REDUCED_MOTION_QUERY);
122
+ const apply = () => setReduced(mq.matches);
123
+ apply();
124
+ mq.addEventListener("change", apply);
125
+ return () => mq.removeEventListener("change", apply);
126
+ }, []);
127
+ return reduced;
128
+ }
129
+
130
+ /** Biggest first; roadmap modules last, ordered among themselves by `graphOrder`. */
131
+ function sortBySize(items: ModuleGridItem[]): ModuleGridItem[] {
132
+ return [...items].sort((a, b) => {
133
+ if (a.lines === null && b.lines === null) return a.module.graphOrder - b.module.graphOrder;
134
+ if (a.lines === null) return 1;
135
+ if (b.lines === null) return -1;
136
+ return b.lines - a.lines;
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Size weights on a log axis, so a 188x line-count spread still leaves the
142
+ * smallest tile legible. The floor is applied before the exponent so no
143
+ * shipped module can reach zero.
144
+ */
145
+ function sizeWeights(ordered: ModuleGridItem[]): number[] {
146
+ const measured = ordered
147
+ .map((item) => item.lines)
148
+ .filter((n): n is number => n !== null && n > 0);
149
+ if (measured.length === 0) return ordered.map(() => 1);
150
+ const logMin = Math.min(...measured.map((n) => Math.log10(n)));
151
+ const logMax = Math.log10(Math.max(...measured));
152
+ const span = Math.max(logMax - logMin, 1);
153
+ return ordered.map((item) => {
154
+ if (item.lines === null || item.lines <= 0) return ROADMAP_WEIGHT;
155
+ const t = (Math.log10(item.lines) - logMin) / span;
156
+ return WEIGHT_FLOOR + (1 - WEIGHT_FLOOR) * Math.pow(t, WEIGHT);
157
+ });
158
+ }
159
+
160
+ function factsLine(item: ModuleGridItem): string {
161
+ const size = item.lines === null ? "roadmap" : `${item.lines.toLocaleString("en-US")} lines`;
162
+ return [size, item.counts].filter(Boolean).join(" · ");
163
+ }
164
+
165
+ /** Scroll units for the pinned track: the first tile holds `FIRST_DWELL`, the rest one each. */
166
+ function dwellUnits(count: number): number {
167
+ if (count <= 1) return 1;
168
+ return FIRST_DWELL + (count - 1);
169
+ }
170
+
171
+ /**
172
+ * Which tile owns this 0..1 track progress. The first tile's window is
173
+ * `FIRST_DWELL` times a later tile's, so it stays open longer.
174
+ */
175
+ export function moduleFocusIndex(progress: number, count: number): number {
176
+ if (count <= 1) return 0;
177
+ const total = dwellUnits(count);
178
+ const p = Math.min(Math.max(progress, 0), 0.999999);
179
+ let cursor = 0;
180
+ for (let i = 0; i < count; i++) {
181
+ cursor += (i === 0 ? FIRST_DWELL : 1) / total;
182
+ if (p < cursor) return i;
183
+ }
184
+ return count - 1;
185
+ }
186
+
187
+ /** Centre of tile `index` on the 0..1 track, matching `moduleFocusIndex`. */
188
+ function moduleFocusCenter(index: number, count: number): number {
189
+ if (count <= 1) return 0.5;
190
+ const total = dwellUnits(count);
191
+ let start = 0;
192
+ for (let i = 0; i < index; i++) start += (i === 0 ? FIRST_DWELL : 1) / total;
193
+ const share = (index === 0 ? FIRST_DWELL : 1) / total;
194
+ return start + share / 2;
195
+ }
196
+
197
+ /**
198
+ * Move the page so `index` is the step the pin shows: the centre of that
199
+ * tile's dwell. Instant, not smooth — a smooth scroll would walk the focus
200
+ * through every tile between.
201
+ */
202
+ function focusModule(track: HTMLElement | null, index: number, count: number) {
203
+ if (!track) return;
204
+ const top = track.getBoundingClientRect().top + window.scrollY;
205
+ const span = Math.max(track.offsetHeight - window.innerHeight, 1);
206
+ const target = top + moduleFocusCenter(index, count) * span;
207
+ window.scrollTo({ top: Math.max(target, 0), behavior: "auto" });
208
+ }
209
+
210
+ type ContentNeed = { h: number; w: number };
211
+
212
+ function restTileMins(count: number): TreemapMins[] {
213
+ return Array.from({ length: count }, () => ({
214
+ minW: REST_MIN.minW + TREEMAP_GAP,
215
+ minH: REST_MIN.minH + TREEMAP_GAP,
216
+ }));
217
+ }
218
+
219
+ function focusTileMins(
220
+ count: number,
221
+ focusIndex: number,
222
+ extra: ContentNeed | undefined,
223
+ ): TreemapMins[] {
224
+ return Array.from({ length: count }, (_, i) => {
225
+ if (i !== focusIndex) {
226
+ return { minW: REST_MIN.minW + TREEMAP_GAP, minH: REST_MIN.minH + TREEMAP_GAP };
227
+ }
228
+ const need = extra ?? { h: 0, w: 0 };
229
+ return {
230
+ minW: Math.max(FOCUS_MIN.minW, need.w) + TREEMAP_GAP,
231
+ minH: Math.max(FOCUS_MIN.minH, need.h) + TREEMAP_GAP,
232
+ };
233
+ });
234
+ }
235
+
236
+ /**
237
+ * Rest rects stay solved in the fixed mosaic, so a focused tile keeps the
238
+ * top-left the reader is looking at. The mosaic box itself never grows —
239
+ * the open card takes more of that same box, and content past the box
240
+ * scrolls inside the card.
241
+ */
242
+ function mosaicLayout(
243
+ baseWeights: number[],
244
+ focusIndex: number,
245
+ natural: { w: number; h: number },
246
+ extra: ContentNeed | undefined,
247
+ ): TreemapRect[] {
248
+ const rest = treemapAreasConstrained(
249
+ baseWeights,
250
+ natural.w,
251
+ natural.h,
252
+ restTileMins(baseWeights.length),
253
+ );
254
+ if (focusIndex < 0) return rest;
255
+ const weights = baseWeights.map((w, i) =>
256
+ i === focusIndex ? Math.max(w * FOCUS_MULT, FOCUS_FLOOR) : w,
257
+ );
258
+ const mins = focusTileMins(baseWeights.length, focusIndex, extra).map((min) => ({
259
+ minW: Math.min(min.minW, natural.w),
260
+ minH: Math.min(min.minH, natural.h),
261
+ }));
262
+ const size = treemapAreasConstrained(weights, natural.w, natural.h, mins)[focusIndex];
263
+ if (!size) return rest;
264
+ return treemapAnchored(weights, natural.w, natural.h, mins, rest, focusIndex, {
265
+ w: size.w,
266
+ h: size.h,
267
+ });
268
+ }
269
+
270
+ /** Border-box width the focused tile will be given for this box and content floor. */
271
+ function focusedContentWidth(
272
+ baseWeights: number[],
273
+ focusIndex: number,
274
+ natural: { w: number; h: number },
275
+ extra: ContentNeed | undefined,
276
+ ): number {
277
+ const rect = mosaicLayout(baseWeights, focusIndex, natural, extra)[focusIndex];
278
+ if (!rect) return FOCUS_MIN.minW;
279
+ return Math.max(rect.w - TREEMAP_GAP, FOCUS_MIN.minW);
280
+ }
281
+
282
+ /**
283
+ * The mosaic's unforced box. An explicit height (the focused card is taller
284
+ * than the stage) must not feed back into the treemap or the solve chases itself.
285
+ */
286
+ function naturalMosaicBox(mosaic: HTMLElement): { w: number; h: number } {
287
+ const w = mosaic.clientWidth;
288
+ const stage = mosaic.closest<HTMLElement>(".dg-stage--mosaic");
289
+ if (!stage) return { w, h: mosaic.clientHeight };
290
+ const stageStyle = getComputedStyle(stage);
291
+ const pad =
292
+ (parseFloat(stageStyle.paddingTop) || 0) + (parseFloat(stageStyle.paddingBottom) || 0);
293
+ const inner = Math.max(stage.clientHeight - pad, 0);
294
+ const maxRaw = getComputedStyle(mosaic).maxHeight;
295
+ const maxH = maxRaw.endsWith("px") ? parseFloat(maxRaw) : Number.POSITIVE_INFINITY;
296
+ return { w, h: Math.min(inner, Number.isFinite(maxH) ? maxH : inner) };
297
+ }
298
+
299
+ function readProbe(probe: HTMLElement, width: number): ContentNeed {
300
+ /* Floor, so a fractional tile width cannot hide a wrap the live card then
301
+ grows a second time to reveal. */
302
+ const w = Math.max(Math.floor(width), FOCUS_MIN.minW);
303
+ probe.style.width = `${w}px`;
304
+ const wantH = Math.ceil(probe.scrollHeight + 8);
305
+ const wantW = probe.scrollWidth > w + 4 ? Math.ceil(probe.scrollWidth + 8) : 0;
306
+ return { h: wantH, w: wantW };
307
+ }
308
+
309
+ export function ModuleGrid({
310
+ items,
311
+ onAsk,
312
+ label = "digithings modules, sized by lines of code",
313
+ vhPerModule = 48,
314
+ className,
315
+ }: ModuleGridProps) {
316
+ const ordered = useMemo(() => sortBySize(items), [items]);
317
+ const baseWeights = useMemo(() => sizeWeights(ordered), [ordered]);
318
+ const count = ordered.length;
319
+
320
+ const trackRef = useRef<HTMLDivElement>(null);
321
+ const probeRef = useRef<HTMLDivElement>(null);
322
+ const measuredWidth = useRef<Record<string, string>>({});
323
+ const { stepper } = useScrollyFeatures(trackRef, { slideCount: count });
324
+ const reduced = usePrefersReducedMotion();
325
+
326
+ /* The stacked face opens the tile nearest the focal line; the first tile
327
+ reads open at page top. */
328
+ const stackRefs = useRef<Array<HTMLElement | null>>([]);
329
+ const [stackActive, setStackActive] = useState(0);
330
+
331
+ /* Focus engages while the stage is centred (middle third, with a wider
332
+ release skirt against flapping). Gate on the stage box: the mosaic's own
333
+ box is what grows, so gating on it deadlocks. The walk index is weighted
334
+ so the first tile holds longer than the ones after it. */
335
+ const [engaged, setEngaged] = useState(false);
336
+ const [walkIndex, setWalkIndex] = useState(0);
337
+ /* The tile the layout actually shows. It lags the scroll index until the
338
+ open card has been measured, so the morph has one target, not a minimum
339
+ and then a correction. */
340
+ const [focus, setFocus] = useState(-1);
341
+ const [needExtra, setNeedExtra] = useState<Record<string, ContentNeed>>({});
342
+ const readyRef = useRef(false);
343
+ useEffect(() => {
344
+ let raf = 0;
345
+ const check = () => {
346
+ raf = 0;
347
+ const track = trackRef.current;
348
+ if (!track) return;
349
+ const top = track.getBoundingClientRect().top + window.scrollY;
350
+ const mosaic = track.querySelector(".dg-mosaic");
351
+ if (!mosaic) return;
352
+ const stage = track.querySelector(".dg-stage--mosaic");
353
+ const rect = (stage ?? mosaic).getBoundingClientRect();
354
+ const vh = window.innerHeight || 1;
355
+ const center = rect.top + rect.height / 2;
356
+ const inside = readyRef.current
357
+ ? center > vh * 0.3 && center < vh * 0.7
358
+ : center > vh * 0.33 && center < vh * 0.67;
359
+ readyRef.current = inside;
360
+ const span = Math.max(track.offsetHeight - vh, 1);
361
+ const progress = (window.scrollY - top) / span;
362
+ const index = moduleFocusIndex(progress, count);
363
+ setWalkIndex((prev) => (prev === index ? prev : index));
364
+ setEngaged((prev) => (prev === inside ? prev : inside));
365
+ };
366
+ const onScroll = () => {
367
+ if (raf === 0) raf = window.requestAnimationFrame(check);
368
+ };
369
+ check();
370
+ window.addEventListener("scroll", onScroll, { passive: true });
371
+ window.addEventListener("resize", onScroll);
372
+ return () => {
373
+ window.removeEventListener("scroll", onScroll);
374
+ window.removeEventListener("resize", onScroll);
375
+ if (raf !== 0) window.cancelAnimationFrame(raf);
376
+ };
377
+ }, [count]);
378
+
379
+ useEffect(() => {
380
+ if (!stepper || reduced) return;
381
+ let raf = 0;
382
+ const pick = () => {
383
+ raf = 0;
384
+ const line = window.innerHeight * STACK_FOCAL;
385
+ let best = 0;
386
+ let bestDistance = Number.POSITIVE_INFINITY;
387
+ stackRefs.current.forEach((el, i) => {
388
+ if (!el) return;
389
+ const distance = Math.abs(el.getBoundingClientRect().top - line);
390
+ if (distance < bestDistance) {
391
+ bestDistance = distance;
392
+ best = i;
393
+ }
394
+ });
395
+ setStackActive(best);
396
+ };
397
+ const onScroll = () => {
398
+ if (raf === 0) raf = window.requestAnimationFrame(pick);
399
+ };
400
+ pick();
401
+ window.addEventListener("scroll", onScroll, { passive: true });
402
+ window.addEventListener("resize", onScroll);
403
+ return () => {
404
+ window.removeEventListener("scroll", onScroll);
405
+ window.removeEventListener("resize", onScroll);
406
+ if (raf !== 0) window.cancelAnimationFrame(raf);
407
+ };
408
+ }, [stepper, reduced]);
409
+
410
+ const requestedFocus = !stepper && engaged ? walkIndex : -1;
411
+
412
+ /* Density tiers from measured width and height, with an 8px hysteresis skirt
413
+ per dimension. Written to data-tier directly: pure presentation, no state. */
414
+ const tierRef = useRef<Record<string, number>>({});
415
+ useEffect(() => {
416
+ const track = trackRef.current;
417
+ if (!track) return;
418
+ const applyTiers = () => {
419
+ track.querySelectorAll<HTMLElement>(".dg-cell").forEach((cell) => {
420
+ const id = cell.dataset.mod ?? "";
421
+ const h = cell.clientHeight;
422
+ const w = cell.clientWidth;
423
+ const t = tierRef.current[id] ?? 0;
424
+ let next: number;
425
+ if (h >= 210 && w >= 200) next = 0;
426
+ else if (t === 0 && h >= 202 && w >= 192) next = 0;
427
+ else if (h >= 175 && w >= 170) next = 1;
428
+ else if (t === 1 && h >= 167 && w >= 162) next = 1;
429
+ else next = 2;
430
+ tierRef.current[id] = next;
431
+ cell.dataset.tier = next === 0 ? "full" : next === 1 ? "medium" : "mini";
432
+ });
433
+ };
434
+ const ro = new ResizeObserver(() => applyTiers());
435
+ track.querySelectorAll(".dg-cell").forEach((c) => ro.observe(c));
436
+ applyTiers();
437
+ return () => ro.disconnect();
438
+ }, [focus]);
439
+
440
+ /* A tile's inside, shared by both faces. The body stays mounted: resting
441
+ tiles glide their rects, and the focus-only detail enters and leaves in
442
+ place (`.dg-mosaic-enter`) instead of remounting the name. The focus
443
+ overlay sits under the body (a button cannot contain the tile's own
444
+ controls). */
445
+ const tileContent = (index: number, on: boolean) => {
446
+ const item = ordered[index];
447
+ const m = item.module;
448
+ const dockerCmd = m.dockerCmd;
449
+ return (
450
+ <>
451
+ <button
452
+ type="button"
453
+ className="dg-cell-focus"
454
+ aria-label={`Focus ${m.id} — ${m.role}, ${factsLine(item)}`}
455
+ onClick={() =>
456
+ stepper ? setStackActive(index) : focusModule(trackRef.current, index, count)
457
+ }
458
+ />
459
+ <div className="dg-cell-body">
460
+ <span className="dg-mosaic-head">
461
+ {item.mark ? (
462
+ <span className="dg-mosaic-name dg-mosaic-mark">{item.mark}</span>
463
+ ) : (
464
+ <span className="dg-mosaic-name">
465
+ <span className="text-ink-mute">digi</span>
466
+ {m.id.replace(/^digi/, "")}
467
+ </span>
468
+ )}
469
+ {item.link ? (
470
+ <a
471
+ className="dg-mosaic-link"
472
+ href={item.link.href}
473
+ target="_blank"
474
+ rel="noopener noreferrer"
475
+ aria-label={item.link.label}
476
+ title={item.link.label}
477
+ >
478
+ <span aria-hidden="true">↗</span>
479
+ </a>
480
+ ) : null}
481
+ <span className="dg-loc dg-mosaic-version">
482
+ {item.version === null ? "roadmap" : `v${item.version}`}
483
+ </span>
484
+ </span>
485
+
486
+ <span className="dg-mosaic-role">{m.role}</span>
487
+
488
+ <span className="dg-mosaic-detail dg-mosaic-enter" aria-hidden={on ? undefined : true}>
489
+ <span className="dg-mosaic-facts">{factsLine(item)}</span>
490
+ {m.summary[0] ? <span className="dg-mosaic-serves">{m.summary[0]}</span> : null}
491
+ </span>
492
+
493
+ <span className="dg-mosaic-stack">
494
+ <StackRow items={m.stack} className={on ? "stack-row" : "stack-row compact"} />
495
+ </span>
496
+
497
+ {dockerCmd || onAsk ? (
498
+ <span className="dg-mosaic-foot dg-mosaic-enter" aria-hidden={on ? undefined : true}>
499
+ {dockerCmd ? (
500
+ <CopyCommand
501
+ inline
502
+ ariaLabel={`${m.id} compose command`}
503
+ samples={[{ label: m.id, protocol: "docker compose", code: dockerCmd }]}
504
+ />
505
+ ) : null}
506
+ {onAsk ? (
507
+ <button
508
+ type="button"
509
+ className="dg-mosaic-ask"
510
+ aria-label={`Ask digichat about ${m.id}`}
511
+ onClick={() => onAsk(m.id)}
512
+ >
513
+ ask <span className="text-ink">digi</span>
514
+ <span className="text-accent">chat</span> →
515
+ </button>
516
+ ) : null}
517
+ </span>
518
+ ) : null}
519
+ </div>
520
+ </>
521
+ );
522
+ };
523
+
524
+ /* The treemap solves in pixels against the measured box. */
525
+ const mosaicRef = useRef<HTMLDivElement>(null);
526
+ const [box, setBox] = useState({ w: 0, h: 0 });
527
+ useEffect(() => {
528
+ const el = mosaicRef.current;
529
+ if (!el) return;
530
+ const apply = () => {
531
+ const next = naturalMosaicBox(el);
532
+ setBox((prev) => (prev.w === next.w && prev.h === next.h ? prev : next));
533
+ };
534
+ apply();
535
+ const ro = new ResizeObserver(apply);
536
+ ro.observe(el);
537
+ const stage = el.closest(".dg-stage--mosaic");
538
+ if (stage) ro.observe(stage);
539
+ /* Browser zoom can resize the visual viewport without a ResizeObserver
540
+ notification on the stage in every engine; listen to both explicit
541
+ resize signals as a backstop so the treemap always re-solves. */
542
+ window.addEventListener("resize", apply);
543
+ window.visualViewport?.addEventListener("resize", apply);
544
+ return () => {
545
+ ro.disconnect();
546
+ window.removeEventListener("resize", apply);
547
+ window.visualViewport?.removeEventListener("resize", apply);
548
+ };
549
+ }, [stepper]);
550
+
551
+ /* A new box re-solves every rect. That is layout, not motion: tiles jump to
552
+ it rather than easing (only a focus change glides), and the mosaic stays
553
+ unseen until the box has held still once after the first solve, then fades
554
+ in whole — so a load never shows tiles piled in a corner or growing into
555
+ place. Attributes, not state: they must land in the same frame as the
556
+ rects, before paint. */
557
+ useLayoutEffect(() => {
558
+ const el = mosaicRef.current;
559
+ if (!el || stepper || box.w === 0 || box.h === 0) return;
560
+ el.setAttribute("data-resizing", "");
561
+ let second = 0;
562
+ const first = requestAnimationFrame(() => {
563
+ second = requestAnimationFrame(() => el.removeAttribute("data-resizing"));
564
+ });
565
+ const settle = window.setTimeout(() => el.setAttribute("data-settled", ""), 160);
566
+ return () => {
567
+ cancelAnimationFrame(first);
568
+ cancelAnimationFrame(second);
569
+ window.clearTimeout(settle);
570
+ };
571
+ }, [box.w, box.h, stepper]);
572
+
573
+ /* Measure the card the scroll is asking for before the mosaic commits to
574
+ it. Measuring after the morph had started grew the tile to the focus
575
+ floor, then again to the copy — two motions. The layout effect below
576
+ publishes one size, and the CSS transition eases onto that once. */
577
+ useLayoutEffect(() => {
578
+ if (requestedFocus < 0) {
579
+ if (focus !== -1) setFocus(-1);
580
+ return;
581
+ }
582
+ if (box.w <= 0 || box.h <= 0) return;
583
+ const id = ordered[requestedFocus].module.id;
584
+ const boxKey = `${Math.round(box.w)}x${Math.round(box.h)}`;
585
+ if (measuredWidth.current[id] === boxKey) {
586
+ if (focus !== requestedFocus) setFocus(requestedFocus);
587
+ return;
588
+ }
589
+ const probe = probeRef.current;
590
+ if (!probe) return;
591
+
592
+ const natural = { w: box.w, h: box.h };
593
+ let extra: ContentNeed = { h: 0, w: 0 };
594
+ let width = focusedContentWidth(baseWeights, requestedFocus, natural, undefined);
595
+ for (let i = 0; i < 4; i++) {
596
+ const next = readProbe(probe, width);
597
+ /* Keep the taller reading. A wider pass hides a wrap the narrower card
598
+ then has to grow again to show. */
599
+ next.h = Math.max(next.h, extra.h);
600
+ next.w = Math.max(next.w, extra.w);
601
+ extra = next;
602
+ const renderWidth = focusedContentWidth(baseWeights, requestedFocus, natural, extra);
603
+ if (Math.abs(renderWidth - width) <= 1) break;
604
+ width = Math.min(width, renderWidth);
605
+ }
606
+
607
+ measuredWidth.current[id] = `${Math.round(box.w)}x${Math.round(box.h)}`;
608
+ setNeedExtra((prev) => {
609
+ const cur = prev[id];
610
+ if (cur && cur.h === extra.h && cur.w === extra.w) return prev;
611
+ return { ...prev, [id]: extra };
612
+ });
613
+ if (focus !== requestedFocus) setFocus(requestedFocus);
614
+ }, [requestedFocus, focus, box.w, box.h, ordered, baseWeights]);
615
+
616
+ const focusExtra = focus >= 0 ? needExtra[ordered[focus]?.module.id ?? ""] : undefined;
617
+ const solvable = !stepper && box.w > 0 && box.h > 0;
618
+ const rects = solvable
619
+ ? mosaicLayout(baseWeights, focus, { w: box.w, h: box.h }, focusExtra)
620
+ : null;
621
+
622
+ /* The mosaic is a fixed box. Drop any height a previous focus wrote onto
623
+ it, so a hot reload cannot leave the grid taller than the stage. */
624
+ useLayoutEffect(() => {
625
+ const stage = trackRef.current?.querySelector<HTMLElement>(".dg-stage--mosaic");
626
+ const mosaic = mosaicRef.current;
627
+ if (stage) stage.style.overflowY = "";
628
+ if (mosaic) {
629
+ mosaic.style.height = "";
630
+ mosaic.style.flexShrink = "";
631
+ }
632
+ }, [stepper]);
633
+
634
+ const probeId = requestedFocus >= 0 ? ordered[requestedFocus].module.id : "";
635
+ const probeOpen =
636
+ requestedFocus >= 0 &&
637
+ box.w > 0 &&
638
+ box.h > 0 &&
639
+ measuredWidth.current[probeId] !== `${Math.round(box.w)}x${Math.round(box.h)}`;
640
+
641
+ return (
642
+ <>
643
+ {probeOpen ? (
644
+ <div
645
+ ref={probeRef}
646
+ className="dg-cell on"
647
+ aria-hidden="true"
648
+ inert
649
+ style={{
650
+ position: "fixed",
651
+ left: -10000,
652
+ top: 0,
653
+ visibility: "hidden",
654
+ height: "auto",
655
+ overflow: "visible",
656
+ pointerEvents: "none",
657
+ boxSizing: "border-box",
658
+ width: FOCUS_MIN.minW,
659
+ }}
660
+ >
661
+ {tileContent(requestedFocus, true)}
662
+ </div>
663
+ ) : null}
664
+ <div
665
+ ref={trackRef}
666
+ className={className}
667
+ style={
668
+ stepper
669
+ ? undefined
670
+ : { height: `min(${scrollyTrackHeightVh(dwellUnits(count), vhPerModule)}vh, 4800px)` }
671
+ }
672
+ >
673
+ <div className={stepper ? "dg-stack-wrap" : "dg-stage dg-stage--mosaic"}>
674
+ <div
675
+ ref={mosaicRef}
676
+ className={`dg-mosaic ${stepper ? "dg-mosaic--stack" : "dg-mosaic--rows"}`}
677
+ role="list"
678
+ aria-label={label}
679
+ >
680
+ {stepper
681
+ ? ordered.map((item, i) => {
682
+ const on = reduced || i === stackActive;
683
+ return (
684
+ <div key={item.module.id} className="dg-mosaic-row">
685
+ <Reveal className="dg-stack-reveal" delay={Math.min(i * 0.05, 0.3)}>
686
+ <div
687
+ data-mod={item.module.id}
688
+ ref={(el) => {
689
+ stackRefs.current[i] = el;
690
+ }}
691
+ role="listitem"
692
+ className={`dg-cell${on ? " on" : ""}`}
693
+ aria-current={on ? "true" : undefined}
694
+ >
695
+ {tileContent(i, true)}
696
+ </div>
697
+ </Reveal>
698
+ </div>
699
+ );
700
+ })
701
+ : ordered.map((item, i) => {
702
+ const r = rects?.[i];
703
+ const on = i === focus;
704
+ /* Hidden until the first solve lands, so unpositioned tiles
705
+ never flash piled at the corner. Placed by a physical
706
+ translate on purpose: these are the treemap's x/y in the
707
+ measured box, and a transform moves a tile without
708
+ registering as a layout shift. */
709
+ const style: CSSProperties = r
710
+ ? {
711
+ visibility: "visible",
712
+ transform: `translate(${r.x + TREEMAP_GAP / 2}px, ${r.y + TREEMAP_GAP / 2}px)`,
713
+ width: Math.max(r.w - TREEMAP_GAP, 0),
714
+ height: Math.max(r.h - TREEMAP_GAP, 0),
715
+ }
716
+ : { visibility: "hidden" };
717
+ return (
718
+ <div
719
+ key={item.module.id}
720
+ data-mod={item.module.id}
721
+ role="listitem"
722
+ className={`dg-cell${on ? " on" : ""}`}
723
+ aria-current={on ? "true" : undefined}
724
+ style={style}
725
+ >
726
+ {tileContent(i, on)}
727
+ </div>
728
+ );
729
+ })}
730
+ </div>
731
+ </div>
732
+ </div>
733
+ </>
734
+ );
735
+ }