@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,368 @@
1
+ /**
2
+ * Squarified treemap partition — the packing behind `ModuleGrid`.
3
+ *
4
+ * Splits a W×H box into one rectangle per weight, in order, with areas ∝
5
+ * weights and a worst-aspect-first row choice so tiles stay squarish as they
6
+ * reflow. Pure in `(weights, W, H)`, so the same focus always draws the same
7
+ * mosaic. Framework-free so it can be unit-tested without a DOM.
8
+ *
9
+ * Focusing a tile re-solves the whole partition, so tiles change neighbours
10
+ * freely instead of stretching their row-mates: proportions live in the
11
+ * weights, not in row membership. The render insets each rect by half of the
12
+ * gutter.
13
+ */
14
+ export interface TreemapRect {
15
+ x: number;
16
+ y: number;
17
+ w: number;
18
+ h: number;
19
+ }
20
+
21
+ export function treemapAreas(weights: number[], W: number, H: number): TreemapRect[] {
22
+ const total = weights.reduce((acc, w) => acc + w, 0) || 1;
23
+ const scale = (W * H) / total;
24
+ const items = weights.map((w, i) => ({ area: w * scale, i }));
25
+ const out: TreemapRect[] = new Array(weights.length);
26
+ const rowArea = (row: { area: number }[]) => row.reduce((acc, r) => acc + r.area, 0);
27
+ const worst = (row: { area: number }[], side: number) => {
28
+ const s = rowArea(row);
29
+ const hi = Math.max(...row.map((r) => r.area));
30
+ const lo = Math.min(...row.map((r) => r.area));
31
+ return Math.max((side * side * hi) / (s * s) || 0, (s * s) / (side * side * lo) || 0);
32
+ };
33
+ const layRow = (
34
+ row: { area: number; i: number }[],
35
+ x: number,
36
+ y: number,
37
+ w: number,
38
+ h: number,
39
+ horizontal: boolean,
40
+ ) => {
41
+ const s = rowArea(row);
42
+ if (horizontal) {
43
+ const rh = s / w;
44
+ let rx = x;
45
+ row.forEach((r) => {
46
+ const rw = r.area / rh;
47
+ out[r.i] = { x: rx, y, w: rw, h: rh };
48
+ rx += rw;
49
+ });
50
+ return { x, y: y + rh, w, h: h - rh };
51
+ }
52
+ const cw = s / h;
53
+ let ry = y;
54
+ row.forEach((r) => {
55
+ const rh = r.area / cw;
56
+ out[r.i] = { x, y: ry, w: cw, h: rh };
57
+ ry += rh;
58
+ });
59
+ return { x: x + cw, y, w: w - cw, h };
60
+ };
61
+ let x = 0;
62
+ let y = 0;
63
+ let w = W;
64
+ let h = H;
65
+ let row: { area: number; i: number }[] = [];
66
+ const rest = [...items];
67
+ while (rest.length > 0) {
68
+ /* The row spans the SHORTEST side of the remaining box: a wide remainder
69
+ packs a vertical column (full height), a tall remainder a horizontal
70
+ strip (full width). Spanning the longest side instead reads better in
71
+ prose but draws full-width slivers for every light tile past the first
72
+ row (measured: eleven tiles, ten of them 1216px-wide slivers) — the
73
+ `worst` metric above only agrees with the layout when the two share
74
+ this orientation. */
75
+ const horizontal = w < h;
76
+ const side = Math.min(w, h);
77
+ const next = rest[0];
78
+ if (row.length === 0 || worst([...row, next], side) <= worst(row, side)) {
79
+ row.push(next);
80
+ rest.shift();
81
+ } else {
82
+ ({ x, y, w, h } = layRow(row, x, y, w, h, horizontal));
83
+ row = [];
84
+ }
85
+ }
86
+ if (row.length > 0) layRow(row, x, y, w, h, w < h);
87
+ return out;
88
+ }
89
+
90
+ /**
91
+ * Anchor-focused solve (v18): the focused tile grows in place while the rest
92
+ * reflow around it.
93
+ *
94
+ * The owner found the full re-solve hard to follow — the open module jumped
95
+ * to a new spot every step. So the rest layout (`rest`, solved with
96
+ * unfocused weights) is the stable map: the focused tile keeps its rest
97
+ * top-left (clamped into the box when its grown size would overflow) and the
98
+ * non-focused tiles repack into the four regions around it (top / bottom /
99
+ * left / right strips), each region filled by its own constrained solve.
100
+ * Tiles are assigned to the region holding their rest centre, so a tile stays
101
+ * near its resting zone; within a region, weight ratios stay exact. Global
102
+ * cross-region proportions are approximate rather than exact — followability
103
+ * won that trade. Pure in its inputs; covered in `treemap.test.ts`.
104
+ */
105
+ export interface TreemapMins {
106
+ minW: number;
107
+ minH: number;
108
+ }
109
+
110
+ /**
111
+ * Constrained solve: every tile's weight first covers its minimum area
112
+ * outright (margin 1.5x for aspect slop), then one pure partition, then a
113
+ * short gentle polish for packing-geometry residuals.
114
+ *
115
+ * Deliberately NOT an iterative weight chase: when several tiles violate at
116
+ * once, joint multiplicative lifts freeze their ratios and can starve a
117
+ * small tile forever (measured live: a roadmap tile ended 101px wide against
118
+ * a 184px minimum while the loop insisted it was "lifting" it). Direct
119
+ * floors cannot freeze — areas always match weights exactly — so the minimums
120
+ * hold jointly whenever the box can fit them. Past that (a box smaller than
121
+ * every minimum combined) it degrades best-effort: still a full partition,
122
+ * still finite, still no overlaps.
123
+ *
124
+ * Squarified packing assumes descending weights, but boosts, floors and
125
+ * measured top-ups can invert the LOC order (a floored roadmap can outweigh
126
+ * digiquant), which degrades aspect quality into slivers — measured live as
127
+ * 107px-wide resting tiles with clipped titles beside a 776px focused one.
128
+ * So every solve sorts descending first and maps rects back; packing quality
129
+ * never depends on focus luck. The sort is stable, so equal weights keep
130
+ * their LOC order.
131
+ *
132
+ * The landing mosaic uses this to guarantee the owner's floor: every tile
133
+ * wide enough for its title + version with padding, every focused tile large
134
+ * enough for its detail. Minimums are in cell space (the render insets the
135
+ * gutter afterwards), so callers inflate them by the gutter.
136
+ */
137
+ export function treemapAreasConstrained(
138
+ weights: number[],
139
+ W: number,
140
+ H: number,
141
+ mins: TreemapMins[],
142
+ maxIters = 3,
143
+ ): TreemapRect[] {
144
+ const total = weights.reduce((acc, w) => acc + w, 0) || 1;
145
+ const boxArea = Math.max(W * H, 1);
146
+ const order = weights.map((_, i) => i).sort((a, b) => weights[b] - weights[a]);
147
+ const sorted = order.map((i) => weights[i]);
148
+ const sortedMins = order.map((i) => mins[i]);
149
+ const floored = sorted.map((w, k) => {
150
+ const m = sortedMins[k];
151
+ if (!m) return w;
152
+ return Math.max(w, ((m.minW * m.minH * 1.5) / boxArea) * total);
153
+ });
154
+ const lifted = [...floored];
155
+ const solveAll = () => treemapAreas(lifted, W, H);
156
+ let sortedRects = solveAll();
157
+ for (let k = 0; k < maxIters; k++) {
158
+ let clean = true;
159
+ for (let s = 0; s < sortedRects.length; s++) {
160
+ const m = sortedMins[s];
161
+ if (!m) continue;
162
+ const r = sortedRects[s];
163
+ if (r.w < m.minW || r.h < m.minH) {
164
+ clean = false;
165
+ lifted[s] *= 1.3;
166
+ }
167
+ }
168
+ if (clean) break;
169
+ sortedRects = solveAll();
170
+ }
171
+ const out: TreemapRect[] = new Array(weights.length);
172
+ sortedRects.forEach((r, s) => {
173
+ out[order[s]] = r;
174
+ });
175
+ return out;
176
+ }
177
+
178
+ /**
179
+ * Anchor-focused solve: the focused tile keeps its rest top-left and grows
180
+ * to `focusedSize` (clamped into the box); every other tile repacks into the
181
+ * strips around it. See the `TreemapMins` docblock above for the why.
182
+ *
183
+ * Tiles go to the strip holding their rest centre (nearest strip wins ties
184
+ * and covers centres swallowed by the grown focused rect); each strip is
185
+ * filled by its own constrained solve, so weight ratios hold within a strip.
186
+ * A strip only receives a tile when it is at least that tile's minimum, so a
187
+ * focused card cannot leave a neighbour a sliver with a clipped title. A
188
+ * strip nothing can use is folded back into the focused card. When the
189
+ * requested focus size still starves a neighbour, the focus shrinks toward
190
+ * its own minimum until the others fit, and only then gives up (best-effort,
191
+ * still a full partition, never overlaps, never NaNs).
192
+ */
193
+ export function treemapAnchored(
194
+ weights: number[],
195
+ W: number,
196
+ H: number,
197
+ mins: TreemapMins[],
198
+ rest: TreemapRect[],
199
+ focusIndex: number,
200
+ focusedSize: { w: number; h: number },
201
+ ): TreemapRect[] {
202
+ const anchor = rest[focusIndex];
203
+ if (!anchor) return treemapAreasConstrained(weights, W, H, mins);
204
+ const focusMin = mins[focusIndex] ?? { minW: 0, minH: 0 };
205
+ const floorW = Math.min(Math.max(focusMin.minW, 0), W);
206
+ const floorH = Math.min(Math.max(focusMin.minH, 0), H);
207
+ const targetW = Math.min(Math.max(focusedSize.w, floorW), W);
208
+ const targetH = Math.min(Math.max(focusedSize.h, floorH), H);
209
+
210
+ interface Strip {
211
+ x: number;
212
+ y: number;
213
+ w: number;
214
+ h: number;
215
+ side: "top" | "bottom" | "left" | "right";
216
+ }
217
+
218
+ const fits = (s: Strip, i: number) => {
219
+ const m = mins[i];
220
+ if (!m) return true;
221
+ return s.w + 0.5 >= m.minW && s.h + 0.5 >= m.minH;
222
+ };
223
+
224
+ const centre = (i: number) => {
225
+ const r = rest[i];
226
+ return { x: r.x + r.w / 2, y: r.y + r.h / 2 };
227
+ };
228
+ const contains = (s: Strip, p: { x: number; y: number }) =>
229
+ p.x >= s.x && p.x <= s.x + s.w && p.y >= s.y && p.y <= s.y + s.h;
230
+ const distance = (s: Strip, p: { x: number; y: number }) => {
231
+ const dx = Math.max(s.x - p.x, 0, p.x - (s.x + s.w));
232
+ const dy = Math.max(s.y - p.y, 0, p.y - (s.y + s.h));
233
+ return dx * dx + dy * dy;
234
+ };
235
+
236
+ const pack = (wantW: number, wantH: number) => {
237
+ let fw = Math.min(Math.max(wantW, floorW), W);
238
+ let fh = Math.min(Math.max(wantH, floorH), H);
239
+ let fx = Math.min(Math.max(anchor.x, 0), Math.max(W - fw, 0));
240
+ let fy = Math.min(Math.max(anchor.y, 0), Math.max(H - fh, 0));
241
+ const others = weights.map((_, i) => i).filter((i) => i !== focusIndex);
242
+
243
+ const build = (): Strip[] =>
244
+ (
245
+ [
246
+ { x: 0, y: 0, w: W, h: fy, side: "top" },
247
+ { x: 0, y: fy + fh, w: W, h: H - fy - fh, side: "bottom" },
248
+ { x: 0, y: fy, w: fx, h: fh, side: "left" },
249
+ { x: fx + fw, y: fy, w: W - fx - fw, h: fh, side: "right" },
250
+ ] as Strip[]
251
+ ).filter((s) => s.w > 4 && s.h > 4);
252
+
253
+ let strips = build();
254
+ for (let guard = 0; guard < 4 && strips.length > 0; guard++) {
255
+ const claimed = new Set<number>();
256
+ const buckets = strips.map(() => [] as number[]);
257
+ others.forEach((i) => {
258
+ let best = -1;
259
+ let bestScore: number | null = null;
260
+ strips.forEach((s, k) => {
261
+ if (!fits(s, i)) return;
262
+ const p = centre(i);
263
+ const score = contains(s, p) ? -1 : distance(s, p);
264
+ if (bestScore === null || score < bestScore) {
265
+ bestScore = score;
266
+ best = k;
267
+ }
268
+ });
269
+ if (best >= 0) {
270
+ buckets[best].push(i);
271
+ claimed.add(i);
272
+ }
273
+ });
274
+ const empty = strips.filter((_, k) => buckets[k].length === 0);
275
+ if (empty.length === 0 || claimed.size < others.length) break;
276
+ const sides = new Set(empty.map((s) => s.side));
277
+ if (sides.has("top")) {
278
+ fh += fy;
279
+ fy = 0;
280
+ }
281
+ if (sides.has("bottom")) fh = H - fy;
282
+ if (sides.has("left")) {
283
+ fw += fx;
284
+ fx = 0;
285
+ }
286
+ if (sides.has("right")) fw = W - fx;
287
+ fw = Math.min(Math.max(fw, floorW), W - fx);
288
+ fh = Math.min(Math.max(fh, floorH), H - fy);
289
+ strips = build();
290
+ }
291
+
292
+ const out: TreemapRect[] = new Array(weights.length);
293
+ out[focusIndex] = { x: fx, y: fy, w: fw, h: fh };
294
+ if (strips.length === 0) {
295
+ others.forEach((i) => {
296
+ out[i] = { ...out[focusIndex] };
297
+ });
298
+ return { rects: out, ok: others.length === 0 };
299
+ }
300
+
301
+ const buckets = strips.map(() => [] as number[]);
302
+ let ok = true;
303
+ others.forEach((i) => {
304
+ const p = centre(i);
305
+ let best = 0;
306
+ let bestScore: number | null = null;
307
+ let bestFit = 0;
308
+ let bestFitScore: number | null = null;
309
+ strips.forEach((s, k) => {
310
+ const score = contains(s, p) ? -1 : distance(s, p);
311
+ if (bestScore === null || score < bestScore) {
312
+ bestScore = score;
313
+ best = k;
314
+ }
315
+ if (!fits(s, i)) return;
316
+ if (bestFitScore === null || score < bestFitScore) {
317
+ bestFitScore = score;
318
+ bestFit = k;
319
+ }
320
+ });
321
+ if (bestFitScore === null) ok = false;
322
+ buckets[bestFitScore === null ? best : bestFit].push(i);
323
+ });
324
+
325
+ strips.forEach((s, k) => {
326
+ const members = buckets[k];
327
+ if (members.length === 0) return;
328
+ if (members.length === 1) {
329
+ const only = members[0];
330
+ out[only] = { x: s.x, y: s.y, w: s.w, h: s.h };
331
+ if (!fits(s, only)) ok = false;
332
+ return;
333
+ }
334
+ const sub = treemapAreasConstrained(
335
+ members.map((i) => weights[i]),
336
+ s.w,
337
+ s.h,
338
+ members.map((i) => mins[i]),
339
+ );
340
+ members.forEach((id, j) => {
341
+ const r = sub[j];
342
+ const m = mins[id];
343
+ out[id] = { x: s.x + r.x, y: s.y + r.y, w: r.w, h: r.h };
344
+ if (m && (r.w + 0.5 < m.minW || r.h + 0.5 < m.minH)) ok = false;
345
+ });
346
+ });
347
+
348
+ others.forEach((i) => {
349
+ out[i] ??= { ...out[focusIndex] };
350
+ });
351
+ return { rects: out, ok };
352
+ };
353
+
354
+ let best = pack(floorW, floorH);
355
+ let lo = 0;
356
+ let hi = 1;
357
+ for (let i = 0; i < 12; i++) {
358
+ const mid = (lo + hi) / 2;
359
+ const attempt = pack(floorW + (targetW - floorW) * mid, floorH + (targetH - floorH) * mid);
360
+ if (attempt.ok) {
361
+ best = attempt;
362
+ lo = mid;
363
+ } else {
364
+ hi = mid;
365
+ }
366
+ }
367
+ return best.rects;
368
+ }
@@ -0,0 +1,243 @@
1
+ "use client";
2
+
3
+ import {
4
+ Children,
5
+ cloneElement,
6
+ isValidElement,
7
+ memo,
8
+ useRef,
9
+ useState,
10
+ useSyncExternalStore,
11
+ type CSSProperties,
12
+ type ReactElement,
13
+ type ReactNode,
14
+ } from "react";
15
+ import { m, useMotionValueEvent, useScroll, useTransform } from "motion/react";
16
+ import type { MotionStyle, MotionValue } from "motion/react";
17
+ import { useMotionSafe } from "../../motion/primitives";
18
+
19
+ /**
20
+ * DeckStack / DeckCard — the sticky stacking card deck promoted from the
21
+ * design reference (data/card-deck; the cavemen.dev pattern, #1450). Cards
22
+ * live in normal document flow as DIRECT children of one shared `.deck-slots`
23
+ * grid; on wide screens each card pins at a cascaded top offset
24
+ * (`--deck-top` + `--deck-peek` × `--stack-index`) and the next card slides
25
+ * up over it, so every buried card keeps a visible top edge — the next
26
+ * opaque card's coverage IS the seam (1px hairline, no drop shadow, no clip
27
+ * box, nothing ever cut off). Content is server-rendered, fully laid out,
28
+ * and readable top-to-bottom with no JS; the only JS enhancements are the
29
+ * recede transform (a slight scale-down while a card is covered) and the
30
+ * optional rail's active state. Below 901px the CSS drops sticky entirely
31
+ * and the cards simply appear in sequence. The deck knows nothing about
32
+ * card content.
33
+ *
34
+ * Wiring (in the consuming app):
35
+ * globals.css @import "@digithings-ai/ui/styles/deck.css";
36
+ * @source "<path-to>/packages/ui/src/components/deck";
37
+ */
38
+
39
+ const WIDE_QUERY = "(min-width: 901px)";
40
+
41
+ /* useSyncExternalStore keeps the media-query state hydration-safe: the server
42
+ snapshot is `false`, so wide-only inline transforms never appear in server
43
+ markup and the hydrated tree matches exactly. */
44
+ function subscribeWide(callback: () => void) {
45
+ const mq = window.matchMedia(WIDE_QUERY);
46
+ mq.addEventListener("change", callback);
47
+ return () => mq.removeEventListener("change", callback);
48
+ }
49
+ const getWideSnapshot = () => window.matchMedia(WIDE_QUERY).matches;
50
+ const getWideServerSnapshot = () => false;
51
+
52
+ export type DeckCardProps = {
53
+ /** Card content — the deck stays content-agnostic. */
54
+ children: ReactNode;
55
+ /** Extra classes on the sticky card element — the app-dress hook. Both
56
+ * unlayered app CSS and call-site token utilities outrank the card's
57
+ * `@layer components` surface defaults in styles/deck.css. */
58
+ className?: string;
59
+ /** Injected by the parent DeckStack — never set manually. */
60
+ index?: number;
61
+ /** Injected by the parent DeckStack — never set manually. */
62
+ total?: number;
63
+ /** Injected by the parent DeckStack — never set manually. */
64
+ animate?: boolean;
65
+ /** Injected by the parent DeckStack — never set manually. */
66
+ progress?: MotionValue<number>;
67
+ };
68
+
69
+ type AnimatedDeckCardProps = {
70
+ children: ReactNode;
71
+ className?: string;
72
+ index: number;
73
+ total: number;
74
+ animate: boolean;
75
+ progress: MotionValue<number>;
76
+ };
77
+
78
+ function cardClass(className?: string): string {
79
+ return className ? `deck-card ${className}` : "deck-card";
80
+ }
81
+
82
+ /**
83
+ * One sticky card. While the next card's slot scrolls through, this card is
84
+ * being covered: it recedes slightly (scale only — a brightness dim read as
85
+ * grime on light-theme paper cards; user ruling #1450) so the stack reads
86
+ * as depth. Function transforms only — numeric range-maps get compiled to a
87
+ * native view() timeline that cannot express this mapping (see the word
88
+ * reveal components for the same pitfall).
89
+ */
90
+ const AnimatedDeckCard = memo(function AnimatedDeckCard({
91
+ children,
92
+ className,
93
+ index,
94
+ total,
95
+ animate,
96
+ progress,
97
+ }: AnimatedDeckCardProps) {
98
+ const cover = (p: number) => {
99
+ if (!animate || index >= total - 1) return 0;
100
+ const start = (index + 1) / total - 1 / total;
101
+ return Math.min(1, Math.max(0, (p - start) * total));
102
+ };
103
+ const scale = useTransform(progress, (p) => 1 - cover(p) * 0.05);
104
+
105
+ return (
106
+ <m.div
107
+ role="listitem"
108
+ className={cardClass(className)}
109
+ style={{ "--stack-index": index, scale, transformOrigin: "top center" } as MotionStyle}
110
+ >
111
+ {children}
112
+ </m.div>
113
+ );
114
+ });
115
+
116
+ export function DeckCard({ children, className, index, total, animate, progress }: DeckCardProps) {
117
+ if (progress === undefined || index === undefined || total === undefined) {
118
+ // Rendered outside a DeckStack: a plain (still sticky-capable) card with
119
+ // no recede transform.
120
+ return (
121
+ <div
122
+ role="listitem"
123
+ className={cardClass(className)}
124
+ style={{ "--stack-index": index ?? 0 } as CSSProperties}
125
+ >
126
+ {children}
127
+ </div>
128
+ );
129
+ }
130
+ return (
131
+ <AnimatedDeckCard
132
+ className={className}
133
+ index={index}
134
+ total={total}
135
+ animate={animate ?? false}
136
+ progress={progress}
137
+ >
138
+ {children}
139
+ </AnimatedDeckCard>
140
+ );
141
+ }
142
+
143
+ export type DeckStackProps = {
144
+ /** DeckCard children. They MUST render as the deck's direct children so
145
+ * the sticky mechanics hold — a sticky element can never leave its
146
+ * containing block, so a per-card wrapper would drag its card away as the
147
+ * wrapper scrolls past (see styles/deck.css). */
148
+ children: ReactNode;
149
+ /** Accessible name for the card list. */
150
+ ariaLabel?: string;
151
+ /** Optional progress rail: one mono label per card, pinned beside the
152
+ * stack on wide screens (the CSS hides it below 901px). Omit to render
153
+ * the slots full-width with no side column. */
154
+ rail?: readonly string[];
155
+ /** Accessible name for the rail list. */
156
+ railAriaLabel?: string;
157
+ /** Extra classes on the outer wrapper (spacing utilities etc.). */
158
+ className?: string;
159
+ };
160
+
161
+ /**
162
+ * The deck container: owns the shared slots grid, scroll progress, and the
163
+ * optional side rail. Each DeckCard child is cloned with its stack index and
164
+ * the deck's scroll progress so it can drive its own recede transform.
165
+ */
166
+ export function DeckStack({
167
+ children,
168
+ ariaLabel = "Card stack",
169
+ rail,
170
+ railAriaLabel = "Stack progress",
171
+ className,
172
+ }: DeckStackProps) {
173
+ const deckRef = useRef<HTMLDivElement | null>(null);
174
+ // useMotionSafe(), not raw useReducedMotion() -- see WordReveal.tsx's fix
175
+ // comment (#2244) for the hydration-mismatch mechanism this avoids. `wide`
176
+ // defaults to false on the first render either way (useSyncExternalStore's
177
+ // own server-snapshot guard), which already short-circuits `animate` below
178
+ // to false regardless of `reduced` -- so this specific value never caused
179
+ // a mismatch in practice, but it's the same unsafe pattern and not worth
180
+ // leaving as an exception.
181
+ const reduced = !useMotionSafe();
182
+ const wide = useSyncExternalStore(subscribeWide, getWideSnapshot, getWideServerSnapshot);
183
+ const [active, setActive] = useState(0);
184
+
185
+ const cards = Children.toArray(children).filter(isValidElement) as ReactElement<DeckCardProps>[];
186
+ const total = cards.length;
187
+
188
+ const { scrollYProgress } = useScroll({
189
+ target: deckRef,
190
+ offset: ["start start", "end end"],
191
+ });
192
+
193
+ useMotionValueEvent(scrollYProgress, "change", (progress) => {
194
+ if (total === 0) return;
195
+ const idx = Math.min(total - 1, Math.max(0, Math.floor(progress * total)));
196
+ setActive(idx);
197
+ });
198
+
199
+ const animate = wide && !reduced;
200
+
201
+ const slots = (
202
+ <div className="deck-slots" role="list" aria-label={ariaLabel}>
203
+ {cards.map((card, index) =>
204
+ cloneElement(card, { index, total, animate, progress: scrollYProgress }),
205
+ )}
206
+ </div>
207
+ );
208
+
209
+ if (!rail || rail.length === 0) {
210
+ return (
211
+ <div ref={deckRef} className={className}>
212
+ {slots}
213
+ </div>
214
+ );
215
+ }
216
+
217
+ return (
218
+ <div
219
+ ref={deckRef}
220
+ className={
221
+ "grid grid-cols-[minmax(0,1fr)_220px] items-start gap-[1.2rem] max-[900px]:grid-cols-1" +
222
+ (className ? ` ${className}` : "")
223
+ }
224
+ >
225
+ {slots}
226
+ {/* The column wrapper stops short of the slots' tail runway (deck.css
227
+ .deck-rail-col) so the pinned rail releases with the last card
228
+ instead of hanging through the empty scroll-hold. */}
229
+ <div className="deck-rail-col">
230
+ <div className="deck-rail-pin">
231
+ <ol className="deck-rail" aria-label={railAriaLabel}>
232
+ {rail.map((label, idx) => (
233
+ <li key={`${label}-${idx}`} className={idx === active ? "on" : undefined}>
234
+ <span className="dot" />
235
+ <span className="name">{label}</span>
236
+ </li>
237
+ ))}
238
+ </ol>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ );
243
+ }