@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,776 @@
1
+ "use client";
2
+ /**
3
+ * NavShell — the one scroll-aware top bar for every digithings marketing surface.
4
+ * Wide: brand · inline links · theme toggle + tail actions.
5
+ * Narrow: brand · theme + actions + hamburger — links and the app CTA live in a
6
+ * full-height portal sheet. Supersedes the per-app DqNav / DigiNav copies:
7
+ * everything app-specific arrives as props (brand, links, sheet CTA, tail
8
+ * actions such as a GitHub icon link); everything shared is owned here —
9
+ * settle after 8px (hairline + blurred band), yield past 180px on scroll-down
10
+ * and return on scroll-up, body scroll lock, Escape/scrim dismissal, and the
11
+ * SSR-safe mount gate for the portal. State dress + overlay machinery live in
12
+ * ../styles/nav-shell.css; static layout is token-backed utilities.
13
+ *
14
+ * A `links` entry may be a NavGroup ({ label, items }) instead of a NavLink: on
15
+ * the wide bar it becomes a real dropdown menu (button[aria-haspopup=menu] +
16
+ * div[role=menu] + a[role=menuitem], WAI-ARIA menu-button keyboard grammar,
17
+ * one open at a time); in the narrow sheet — already a vertical list — it
18
+ * becomes a labelled section of links, no disclosure at all.
19
+ */
20
+ import {
21
+ useCallback,
22
+ useEffect,
23
+ useId,
24
+ useRef,
25
+ useState,
26
+ useSyncExternalStore,
27
+ type FocusEvent as ReactFocusEvent,
28
+ type KeyboardEvent as ReactKeyboardEvent,
29
+ type ReactNode,
30
+ } from "react";
31
+ import { createPortal } from "react-dom";
32
+ import { Button } from "../ui";
33
+ import { ThemeToggle } from "./ThemeProvider";
34
+ import {
35
+ hrefIsCurrent,
36
+ isNavGroup,
37
+ SkipLink,
38
+ type NavGroup,
39
+ type NavItem,
40
+ type NavLink,
41
+ } from "./chrome";
42
+ import { useBodyScrollLock } from "../lib/useBodyScrollLock";
43
+
44
+ // Mount gate: server + first (hydration) client render read `false`; the client
45
+ // re-reads `true` post-hydration. Keeps the portal out of the SSR/hydration tree
46
+ // so it can't cause a mismatch — without a setState-in-effect cascade.
47
+ const emptySubscribe = () => () => {};
48
+
49
+ export interface NavShellProps {
50
+ /** Brand content (mark + wordmark); NavShell wraps it in the home link. */
51
+ brand: ReactNode;
52
+ /** Wayfinding entries — inline on wide viewports, stacked in the sheet on
53
+ * narrow. A NavGroup entry renders as a dropdown on the bar and as a
54
+ * labelled section in the sheet. `NavLink[]` is still accepted as-is. */
55
+ links: NavItem[];
56
+ /** App CTA for the narrow-viewport sheet (e.g. digiquant / Ask digichat button). */
57
+ cta?: ReactNode;
58
+ /** Extra tail actions between the theme toggle and the hamburger
59
+ * (e.g. a GitHub link dressed with `buttonVariants({ variant: "ghost",
60
+ * size: "icon-sm" })` from §ui) — kept a slot so the primitive carries no
61
+ * hardcoded external URLs. */
62
+ actions?: ReactNode;
63
+ /** Render the shared ThemeToggle in the tail cluster. Default true. */
64
+ showThemeToggle?: boolean;
65
+ /** Home link target for the brand. Default "/". */
66
+ homeHref?: string;
67
+ /** Accessible label for the brand home link (e.g. "digiquant home"). */
68
+ homeLabel?: string;
69
+ /** The visitor's current pathname (Next `usePathname()`, passed by the site's
70
+ * client wrapper). NavShell owns the `aria-current="page"` rule once —
71
+ * `hrefIsCurrent` matches an exact route only, never an external link or a
72
+ * same-page anchor. Omit and no link is marked. */
73
+ currentPath?: string;
74
+ /** Render the shared <SkipLink/> before the bar, targeting this id
75
+ * (e.g. "#main"). Omitted: no skip link — the consumer may not have a
76
+ * focusable target yet. */
77
+ skipTo?: string;
78
+ /**
79
+ * Start hidden: the bar is tucked away on first paint and only returns on
80
+ * the scroll-up grammar. Use on a page whose hero already carries the brand
81
+ * so the two banners are never on screen together at the top. */
82
+ startHidden?: boolean;
83
+ /**
84
+ * Show/hide grammar for the bar. Default "scroll" — settle after 8px,
85
+ * yield past 180px on scroll-down, return on scroll-up (the canon
86
+ * behavior, everywhere). "hover" is for a surface that doesn't scroll at
87
+ * all (a fixed-height app view, not a document) — the bar starts hidden
88
+ * and reveals only while the cursor sits in the top strip, so content can
89
+ * run all the way to the top the rest of the time. "pinned" settles after
90
+ * 8px like "scroll" but never yields — for a document page whose bar
91
+ * should stay fixed to the top. Keyboard/focus reach is unaffected in
92
+ * every mode: a hidden bar is still tab-reachable and
93
+ * `:focus-within` reveals it (nav-shell.css).
94
+ */
95
+ autoHide?: "scroll" | "hover" | "pinned";
96
+ /**
97
+ * Only read when `autoHide="scroll"`. Minimum cumulative upward scroll (in
98
+ * px) required before a bar hidden by scroll-down reappears. Default 0 —
99
+ * the canon behavior, any upward movement reveals. A surface where the
100
+ * reveal fires on the slightest wobble (trackpad jitter, an inertial
101
+ * micro-bounce) wants this raised so a deliberate scroll-up is what brings
102
+ * the bar back, not an incidental one.
103
+ */
104
+ scrollRevealDelta?: number;
105
+ /**
106
+ * Only read when `autoHide="scroll"`. When set, the bar also reveals while
107
+ * the cursor sits within this many px of the viewport top, independent of
108
+ * scroll position — a rescue path for a surface whose `scrollRevealDelta`
109
+ * is high enough that scrolling back up to the exact top is inconvenient.
110
+ * Unset (the default): no hover reveal in scroll mode, matching prior
111
+ * behavior.
112
+ */
113
+ hoverRevealZone?: number;
114
+ /**
115
+ * Clip the bar's own band (its scrolled backdrop + bottom hairline) to the
116
+ * page column instead of the viewport edges. The row was always capped to
117
+ * `--wrap`; this is for a document whose sections are drawn between the
118
+ * persistent `LayoutLines`, so a full-bleed bar would paint across the very
119
+ * rules that define the page. Unset (the default) keeps the bar full-bleed —
120
+ * right for an app shell, wrong for this kind of page. Pass the same register
121
+ * as `LayoutLines` (`--frame-w`) so the band's edges land on the rules.
122
+ */
123
+ clipToFrame?: boolean;
124
+ }
125
+
126
+ /** Key for a NavItem: groups have no href, so the label carries the identity. */
127
+ const itemKey = (item: NavItem, i: number) =>
128
+ isNavGroup(item) ? `g:${i}:${item.label}` : `${item.href}${item.label}`;
129
+
130
+ /** A plain wayfinding link, in the strip or in the sheet. Menu items are their
131
+ * own markup inside NavShellGroup — they carry roles, refs and a tabIndex. */
132
+ function NavShellLink({
133
+ link,
134
+ current,
135
+ onNavigate,
136
+ }: {
137
+ link: NavLink;
138
+ /** This link names the page the visitor is on (computed by the caller from
139
+ * `hrefIsCurrent(link.href, currentPath)`). */
140
+ current?: boolean;
141
+ onNavigate?: () => void;
142
+ }) {
143
+ return (
144
+ <a
145
+ href={link.href}
146
+ aria-current={current ? "page" : undefined}
147
+ target={link.external ? "_blank" : undefined}
148
+ rel={link.external ? "noopener noreferrer" : undefined}
149
+ onClick={onNavigate}
150
+ >
151
+ {link.label}
152
+ {link.external && <span aria-hidden="true"> ↗</span>}
153
+ </a>
154
+ );
155
+ }
156
+
157
+ /**
158
+ * One dropdown on the wide bar. Own component, not an inline branch, because
159
+ * each group needs its own useId + item refs (hooks can't run in a loop) — and
160
+ * because the open/closed decision belongs to the parent (one at a time) while
161
+ * the focus bookkeeping belongs here.
162
+ */
163
+ function NavShellGroup({
164
+ group,
165
+ groupKey,
166
+ open,
167
+ setOpenKey,
168
+ currentPath,
169
+ }: {
170
+ group: NavGroup;
171
+ /** This group's identity in the parent's single "which one is open" slot. */
172
+ groupKey: string;
173
+ open: boolean;
174
+ /** The parent's useState setter — stable, so the dismissal listeners below
175
+ * subscribe once per open instead of once per parent render. */
176
+ setOpenKey: (key: string | null) => void;
177
+ /** The visitor's current pathname, for `aria-current` on menu items. */
178
+ currentPath?: string;
179
+ }) {
180
+ const onOpen = useCallback(() => setOpenKey(groupKey), [setOpenKey, groupKey]);
181
+ const onClose = useCallback(() => setOpenKey(null), [setOpenKey]);
182
+ const menuId = useId();
183
+ const wrapRef = useRef<HTMLDivElement>(null);
184
+ const triggerRef = useRef<HTMLButtonElement>(null);
185
+ const itemRefs = useRef<(HTMLAnchorElement | null)[]>([]);
186
+ // Which item to focus once the panel is *visible*. null = leave focus on the
187
+ // trigger (pointer opens). Applied from an effect, never from the handler:
188
+ // the panel is still visibility:hidden while the handler runs, and a hidden
189
+ // element cannot take focus — the call would silently no-op.
190
+ const [focusIndex, setFocusIndex] = useState<number | null>(null);
191
+ const last = group.items.length - 1;
192
+
193
+ useEffect(() => {
194
+ if (!open || focusIndex === null) return;
195
+ const el = itemRefs.current[focusIndex];
196
+ if (!el) return;
197
+ el.focus();
198
+ if (document.activeElement === el) return;
199
+ // The panel unparks through a transition. If the browser has not recomputed
200
+ // its visibility by the time this effect runs, the element is still
201
+ // `visibility: hidden` and focus() is a silent no-op — so retry once on the
202
+ // next frame. (nav-shell.css flips visibility with no delay when opening
203
+ // precisely so the first call lands; this is the belt for anyone who
204
+ // restyles the panel.)
205
+ const raf = requestAnimationFrame(() => el.focus());
206
+ return () => cancelAnimationFrame(raf);
207
+ }, [open, focusIndex]);
208
+
209
+ const closeToTrigger = useCallback(() => {
210
+ onClose();
211
+ triggerRef.current?.focus();
212
+ }, [onClose]);
213
+
214
+ // Dismissal while open: a pointer press anywhere outside this group, or
215
+ // Escape from anywhere (focus may still be on the trigger, or have been
216
+ // clicked away entirely — a React onKeyDown on the wrapper would miss it).
217
+ useEffect(() => {
218
+ if (!open) return;
219
+ const onPointerDown = (e: PointerEvent) => {
220
+ if (!wrapRef.current?.contains(e.target as Node)) onClose();
221
+ };
222
+ const onKeyDown = (e: globalThis.KeyboardEvent) => {
223
+ if (e.key !== "Escape") return;
224
+ e.preventDefault();
225
+ onClose();
226
+ triggerRef.current?.focus();
227
+ };
228
+ document.addEventListener("pointerdown", onPointerDown);
229
+ document.addEventListener("keydown", onKeyDown);
230
+ return () => {
231
+ document.removeEventListener("pointerdown", onPointerDown);
232
+ document.removeEventListener("keydown", onKeyDown);
233
+ };
234
+ }, [open, onClose]);
235
+
236
+ // Enter/Space/ArrowDown/ArrowUp on the trigger: preventDefault stops the
237
+ // browser synthesising a click from Enter/Space, so onClick below stays the
238
+ // pointer path (which must NOT pull focus into the panel).
239
+ const onTriggerKeyDown = (e: ReactKeyboardEvent<HTMLButtonElement>) => {
240
+ if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
241
+ e.preventDefault();
242
+ if (open && e.key !== "ArrowDown") closeToTrigger();
243
+ else {
244
+ onOpen();
245
+ setFocusIndex(0);
246
+ }
247
+ } else if (e.key === "ArrowUp") {
248
+ e.preventDefault();
249
+ onOpen();
250
+ setFocusIndex(last);
251
+ }
252
+ };
253
+
254
+ const onMenuKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {
255
+ const current = focusIndex ?? 0;
256
+ if (e.key === "ArrowDown") {
257
+ e.preventDefault();
258
+ setFocusIndex(current >= last ? 0 : current + 1);
259
+ } else if (e.key === "ArrowUp") {
260
+ e.preventDefault();
261
+ setFocusIndex(current <= 0 ? last : current - 1);
262
+ } else if (e.key === "Home") {
263
+ e.preventDefault();
264
+ setFocusIndex(0);
265
+ } else if (e.key === " ") {
266
+ // A menuitem activates on Space as well as Enter — but this one is an <a>,
267
+ // where the browser's default for Space is to scroll the page, and no
268
+ // click is synthesised. So synthesise it and swallow the scroll. Read the
269
+ // focused element rather than focusIndex: focus is the source of truth
270
+ // here, and the index is still null after a pointer open.
271
+ const item = (e.target as HTMLElement).closest<HTMLAnchorElement>("a.nav-shell-menu-item");
272
+ if (item) {
273
+ e.preventDefault();
274
+ item.click();
275
+ }
276
+ } else if (e.key === "End") {
277
+ e.preventDefault();
278
+ setFocusIndex(last);
279
+ }
280
+ // Escape is handled by the document listener above (one code path for
281
+ // "close and return focus", wherever focus currently sits). Tab is handled
282
+ // by onFocusLeave below, not here — closing on keydown would re-hide the
283
+ // panel *before* the browser picks the next tab stop.
284
+ };
285
+
286
+ // Tab out of the group — from the trigger or from an item — closes it. Doing
287
+ // this on focusout rather than on the Tab keydown means focus has already
288
+ // landed before the panel re-hides, and it catches every other way focus can
289
+ // leave (a click that focuses something else, a screen reader's own
290
+ // navigation). React's onBlur is focusout, so it bubbles from the children.
291
+ // A null relatedTarget (the window itself lost focus) also closes: returning
292
+ // to the tab with a menu still hanging open is the worse of the two.
293
+ const onFocusLeave = (e: ReactFocusEvent<HTMLDivElement>) => {
294
+ if (!open) return;
295
+ if (wrapRef.current?.contains(e.relatedTarget as Node | null)) return;
296
+ onClose();
297
+ };
298
+
299
+ // Hover open/close, alongside the existing click toggle (kept for touch,
300
+ // which never fires hover, and for keyboard, which already opens via
301
+ // onTriggerKeyDown). A short close delay survives the gap between trigger
302
+ // and panel — .nav-shell-menu sits `margin-top` below the trigger, so a
303
+ // straight-line mouse path from one to the other briefly leaves the
304
+ // wrapper's box and would otherwise close it before the panel is reached.
305
+ const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
306
+ const cancelClose = useCallback(() => {
307
+ if (closeTimer.current == null) return;
308
+ clearTimeout(closeTimer.current);
309
+ closeTimer.current = null;
310
+ }, []);
311
+ const onMouseEnter = useCallback(() => {
312
+ cancelClose();
313
+ onOpen();
314
+ }, [cancelClose, onOpen]);
315
+ const onMouseLeave = useCallback(() => {
316
+ cancelClose();
317
+ closeTimer.current = setTimeout(onClose, 150);
318
+ }, [cancelClose, onClose]);
319
+ useEffect(() => cancelClose, [cancelClose]);
320
+
321
+ return (
322
+ <div
323
+ className="nav-shell-group"
324
+ ref={wrapRef}
325
+ data-open={open}
326
+ onBlur={onFocusLeave}
327
+ onMouseEnter={onMouseEnter}
328
+ onMouseLeave={onMouseLeave}
329
+ >
330
+ <Button
331
+ type="button"
332
+ ref={triggerRef}
333
+ className="nav-shell-group-trigger h-auto"
334
+ aria-haspopup="menu"
335
+ aria-expanded={open}
336
+ aria-controls={menuId}
337
+ onClick={() => {
338
+ if (open) onClose();
339
+ else {
340
+ onOpen();
341
+ setFocusIndex(null);
342
+ }
343
+ }}
344
+ onKeyDown={onTriggerKeyDown}
345
+ >
346
+ {group.label}
347
+ <span className="nav-shell-group-caret" aria-hidden="true" />
348
+ </Button>
349
+ {/* Always in the tree (so it can transition, and so SSR ships the index),
350
+ hidden by visibility + aria-hidden while closed — the same contract
351
+ the portal sheet below uses. */}
352
+ <div
353
+ id={menuId}
354
+ role="menu"
355
+ aria-label={group.label}
356
+ aria-hidden={!open}
357
+ className={`nav-shell-menu${open ? " is-open" : ""}`}
358
+ onKeyDown={onMenuKeyDown}
359
+ >
360
+ {group.items.map((item, i) => (
361
+ <a
362
+ key={item.href + item.label}
363
+ ref={(el) => {
364
+ itemRefs.current[i] = el;
365
+ }}
366
+ href={item.href}
367
+ aria-current={hrefIsCurrent(item.href, currentPath) ? "page" : undefined}
368
+ className="nav-shell-menu-item"
369
+ role="menuitem"
370
+ // Roving focus: the trigger is the menu's single tab stop and items
371
+ // are focused programmatically — which also keeps the closed
372
+ // panel's links out of the tab order, no inert dance needed.
373
+ tabIndex={-1}
374
+ target={item.external ? "_blank" : undefined}
375
+ rel={item.external ? "noopener noreferrer" : undefined}
376
+ onClick={onClose}
377
+ >
378
+ {item.label}
379
+ {item.external && <span aria-hidden="true"> ↗</span>}
380
+ </a>
381
+ ))}
382
+ </div>
383
+ </div>
384
+ );
385
+ }
386
+
387
+ /** The wide-viewport strip: links inline, groups as dropdowns. */
388
+ function NavShellStrip({
389
+ items,
390
+ className,
391
+ openKey,
392
+ setOpenKey,
393
+ currentPath,
394
+ }: {
395
+ items: NavItem[];
396
+ className?: string;
397
+ openKey: string | null;
398
+ setOpenKey: (key: string | null) => void;
399
+ currentPath?: string;
400
+ }) {
401
+ return (
402
+ <nav className={className} aria-label="Primary">
403
+ {items.map((item, i) => {
404
+ const key = itemKey(item, i);
405
+ return isNavGroup(item) ? (
406
+ <NavShellGroup
407
+ key={key}
408
+ group={item}
409
+ groupKey={key}
410
+ open={openKey === key}
411
+ setOpenKey={setOpenKey}
412
+ currentPath={currentPath}
413
+ />
414
+ ) : (
415
+ <NavShellLink key={key} link={item} current={hrefIsCurrent(item.href, currentPath)} />
416
+ );
417
+ })}
418
+ </nav>
419
+ );
420
+ }
421
+
422
+ /** A group inside the sheet: a label and its links, no disclosure — the sheet
423
+ * is already a vertical list, so a dropdown inside it would be a second
424
+ * needless tap. Own component for the useId behind aria-labelledby. */
425
+ function NavShellSheetSection({
426
+ group,
427
+ currentPath,
428
+ onNavigate,
429
+ }: {
430
+ group: NavGroup;
431
+ currentPath?: string;
432
+ onNavigate?: () => void;
433
+ }) {
434
+ const labelId = useId();
435
+ return (
436
+ <div className="nav-shell-sheet-group" role="group" aria-labelledby={labelId}>
437
+ <p className="nav-shell-sheet-group-label" id={labelId}>
438
+ {group.label}
439
+ </p>
440
+ {group.items.map((item) => (
441
+ <NavShellLink
442
+ key={item.href + item.label}
443
+ link={item}
444
+ current={hrefIsCurrent(item.href, currentPath)}
445
+ onNavigate={onNavigate}
446
+ />
447
+ ))}
448
+ </div>
449
+ );
450
+ }
451
+
452
+ /** The narrow-viewport sheet's link stack: flat links plus labelled sections. */
453
+ function NavShellSheetNav({
454
+ items,
455
+ className,
456
+ currentPath,
457
+ onNavigate,
458
+ }: {
459
+ items: NavItem[];
460
+ className?: string;
461
+ currentPath?: string;
462
+ onNavigate?: () => void;
463
+ }) {
464
+ return (
465
+ <nav className={className} aria-label="Primary">
466
+ {items.map((item, i) =>
467
+ isNavGroup(item) ? (
468
+ <NavShellSheetSection
469
+ key={itemKey(item, i)}
470
+ group={item}
471
+ currentPath={currentPath}
472
+ onNavigate={onNavigate}
473
+ />
474
+ ) : (
475
+ <NavShellLink
476
+ key={itemKey(item, i)}
477
+ link={item}
478
+ current={hrefIsCurrent(item.href, currentPath)}
479
+ onNavigate={onNavigate}
480
+ />
481
+ ),
482
+ )}
483
+ </nav>
484
+ );
485
+ }
486
+
487
+ export function NavShell({
488
+ brand,
489
+ links,
490
+ cta,
491
+ actions,
492
+ showThemeToggle = true,
493
+ homeHref = "/",
494
+ homeLabel = "home",
495
+ currentPath,
496
+ skipTo,
497
+ autoHide = "scroll",
498
+ startHidden = false,
499
+ scrollRevealDelta = 0,
500
+ hoverRevealZone,
501
+ clipToFrame = false,
502
+ }: NavShellProps) {
503
+ const navRef = useRef<HTMLElement>(null);
504
+ /** Last pointer Y, so a scroll-down does not hide the bar while the cursor is in the top rescue zone. */
505
+ const pointerY = useRef<number | null>(null);
506
+ const toggleRef = useRef<HTMLButtonElement>(null);
507
+ const [menuOpen, setMenuOpen] = useState(false);
508
+ // Which dropdown is open, by itemKey — one slot, so opening a second group
509
+ // closes the first without any cross-group bookkeeping.
510
+ const [openGroup, setOpenGroup] = useState<string | null>(null);
511
+ const sheetId = useId();
512
+ const mounted = useSyncExternalStore(
513
+ emptySubscribe,
514
+ () => true,
515
+ () => false,
516
+ );
517
+
518
+ // Navigation and the hamburger's own toggle leave focus where it is; the
519
+ // two dismissal paths that would otherwise strand focus on a hidden sheet
520
+ // (Escape, scrim) return it to the control that opened the sheet.
521
+ const closeMenu = useCallback(() => setMenuOpen(false), []);
522
+ const closeMenuAndReturn = useCallback(() => {
523
+ setMenuOpen(false);
524
+ toggleRef.current?.focus();
525
+ }, []);
526
+
527
+ // Scroll grammar (canon: settle, then yield). Class flips outside React
528
+ // state: scroll fires per frame and the bar's dress is pure presentation.
529
+ // "pinned" settles like "scroll" but never yields — the bar stays fixed
530
+ // to the top. Only for autoHide="hover" is this skipped below — a surface
531
+ // using "hover" typically doesn't scroll at all (window.scrollY would just
532
+ // stay pinned at 0), and even if it did, the two grammars shouldn't fight
533
+ // over the same class.
534
+ useEffect(() => {
535
+ const nav = navRef.current;
536
+ if (!nav || (autoHide !== "scroll" && autoHide !== "pinned")) return;
537
+ let last = 0;
538
+ // Cumulative upward scroll since the bar was last hidden (or since the
539
+ // top). Reset on any downward movement so a down/up wobble around the
540
+ // same y doesn't slowly creep past the threshold.
541
+ let upAccum = 0;
542
+ const onScroll = () => {
543
+ const y = window.scrollY;
544
+ nav.classList.toggle("is-scrolled", y > 8);
545
+ if (autoHide !== "scroll") return;
546
+ const delta = y - last;
547
+ const hoveringTop =
548
+ hoverRevealZone != null &&
549
+ hoverRevealZone > 0 &&
550
+ pointerY.current != null &&
551
+ pointerY.current <= hoverRevealZone;
552
+ if (delta > 0 && y > 180 && !hoveringTop) {
553
+ upAccum = 0;
554
+ nav.classList.add("is-hidden");
555
+ } else if (delta < 0) {
556
+ upAccum += -delta;
557
+ if (y <= 180 || upAccum >= scrollRevealDelta) {
558
+ nav.classList.remove("is-hidden");
559
+ }
560
+ }
561
+ last = y;
562
+ };
563
+ onScroll();
564
+ window.addEventListener("scroll", onScroll, { passive: true });
565
+ return () => window.removeEventListener("scroll", onScroll);
566
+ }, [autoHide, scrollRevealDelta, hoverRevealZone]);
567
+
568
+ // Rescue reveal: in "scroll" mode, a hidden bar also reappears while the
569
+ // cursor dwells near the very top edge, regardless of accumulated upward
570
+ // scroll. Only attached when a caller opts in via `hoverRevealZone` — the
571
+ // canon "scroll" grammar otherwise has no hover behavior at all. Mouse-only
572
+ // by design, same as the "hover" grammar's own reveal; keyboard reach is
573
+ // handled by `:focus-within` in nav-shell.css regardless of mode.
574
+ useEffect(() => {
575
+ const nav = navRef.current;
576
+ if (!nav || autoHide !== "scroll" || !hoverRevealZone) return;
577
+ const onMove = (e: MouseEvent) => {
578
+ pointerY.current = e.clientY;
579
+ if (e.clientY <= hoverRevealZone) nav.classList.remove("is-hidden");
580
+ };
581
+ const onLeave = () => {
582
+ pointerY.current = null;
583
+ };
584
+ window.addEventListener("mousemove", onMove, { passive: true });
585
+ document.documentElement.addEventListener("mouseleave", onLeave);
586
+ return () => {
587
+ window.removeEventListener("mousemove", onMove);
588
+ document.documentElement.removeEventListener("mouseleave", onLeave);
589
+ };
590
+ }, [autoHide, hoverRevealZone]);
591
+
592
+ // Hover grammar: starts hidden, reveals while the cursor sits in the top
593
+ // strip. RELEASE_ZONE is deliberately wider than the bar itself (a flat
594
+ // HOT_ZONE-only release would flicker right at the reveal boundary, since
595
+ // the reveal transition itself moves the cursor's target out from under
596
+ // it). Focus reach for keyboard users is handled in CSS
597
+ // (.nav-shell.is-hidden:focus-within), not here — a transformed-offscreen
598
+ // element is still in tab order, so no JS is needed for that path.
599
+ //
600
+ // REVEAL_DELAY requires the cursor to *dwell* in HOT_ZONE for a beat
601
+ // before revealing, rather than firing the instant it crosses the line.
602
+ // A "hover" surface has no reserved top padding by design (it overlays on
603
+ // reveal instead of pushing content down), so a consumer's own top-of-
604
+ // viewport content can end up only a few px below HOT_ZONE — e.g.
605
+ // digithings-web's /chat, /chat/occ, whose iframed header (incl. the
606
+ // language selector) sits ~12-28px down. Without a delay, simply moving
607
+ // the cursor *toward* that control — not toward the bar at all — grazes
608
+ // HOT_ZONE en route and reveals the bar over the very control the user
609
+ // was reaching for. A fast pass-through clears the zone before the timer
610
+ // fires; only a genuine paused hover at
611
+ // the top edge reveals it. Mirrors the existing close-delay debounce
612
+ // pattern below (150ms) rather than inventing a new timing idiom.
613
+ //
614
+ // is-scrolled rides along with the reveal (not just is-hidden): in scroll
615
+ // mode that class is what gives the bar its blurred backdrop once there's
616
+ // page content to read it over, gated on actually having scrolled past
617
+ // it. A hover-revealed bar overlays content unconditionally — there's no
618
+ // "haven't scrolled yet" moment where see-through is correct — so it
619
+ // needs that same backdrop every time it's shown, not on a scroll gate
620
+ // that a non-scrolling surface would never trip.
621
+ useEffect(() => {
622
+ const nav = navRef.current;
623
+ if (!nav || autoHide !== "hover") return;
624
+ const HOT_ZONE = 24;
625
+ const RELEASE_ZONE = nav.getBoundingClientRect().height + 64;
626
+ const REVEAL_DELAY = 120;
627
+ nav.classList.add("is-hidden");
628
+ let revealed = false;
629
+ let revealTimer: ReturnType<typeof setTimeout> | null = null;
630
+ const clearRevealTimer = () => {
631
+ if (revealTimer == null) return;
632
+ clearTimeout(revealTimer);
633
+ revealTimer = null;
634
+ };
635
+ const onMove = (e: MouseEvent) => {
636
+ if (!revealed && e.clientY <= HOT_ZONE) {
637
+ if (revealTimer == null) {
638
+ revealTimer = setTimeout(() => {
639
+ revealTimer = null;
640
+ revealed = true;
641
+ nav.classList.remove("is-hidden");
642
+ nav.classList.add("is-scrolled");
643
+ }, REVEAL_DELAY);
644
+ }
645
+ } else if (!revealed) {
646
+ clearRevealTimer();
647
+ } else if (e.clientY > RELEASE_ZONE) {
648
+ revealed = false;
649
+ nav.classList.add("is-hidden");
650
+ nav.classList.remove("is-scrolled");
651
+ }
652
+ };
653
+ // A pending reveal timer only gets cancelled by a later mousemove landing
654
+ // outside HOT_ZONE — but a fast flick off the *top* edge (toward browser
655
+ // chrome, another display, alt-tab) leaves the viewport entirely, so no
656
+ // further mousemove ever fires there to cancel it. Without this, the bar
657
+ // still reveals 120ms later even though the cursor is no longer anywhere
658
+ // near it, defeating the dwell delay via the one path it doesn't cover.
659
+ // documentElement (not window) is what actually receives "left the
660
+ // viewport": mouseleave doesn't bubble, and it's the html element's
661
+ // geometry — filling the viewport — that the pointer exits.
662
+ const onLeaveViewport = () => {
663
+ if (!revealed) clearRevealTimer();
664
+ };
665
+ window.addEventListener("mousemove", onMove, { passive: true });
666
+ document.documentElement.addEventListener("mouseleave", onLeaveViewport);
667
+ return () => {
668
+ window.removeEventListener("mousemove", onMove);
669
+ document.documentElement.removeEventListener("mouseleave", onLeaveViewport);
670
+ clearRevealTimer();
671
+ };
672
+ }, [autoHide]);
673
+
674
+ // Reference-counted (see useBodyScrollLock) so this coordinates safely
675
+ // with any other overlay locked at the same time (e.g. CommandPalette),
676
+ // regardless of open/close order — a plain per-component overflow toggle
677
+ // here previously clobbered any other overlay's lock the moment either
678
+ // one closed (confirmed in the in-session review on PR #2269).
679
+ useBodyScrollLock(menuOpen);
680
+
681
+ useEffect(() => {
682
+ if (!menuOpen) return;
683
+ const onKey = (e: KeyboardEvent) => {
684
+ if (e.key === "Escape") closeMenuAndReturn();
685
+ };
686
+ window.addEventListener("keydown", onKey);
687
+ return () => window.removeEventListener("keydown", onKey);
688
+ }, [menuOpen, closeMenuAndReturn]);
689
+
690
+ const menuOverlay =
691
+ mounted &&
692
+ createPortal(
693
+ <>
694
+ <Button
695
+ type="button"
696
+ className={`nav-shell-backdrop h-auto${menuOpen ? " is-open" : ""}`}
697
+ aria-label="Close menu"
698
+ aria-hidden={!menuOpen}
699
+ tabIndex={menuOpen ? 0 : -1}
700
+ onClick={closeMenuAndReturn}
701
+ />
702
+ <div
703
+ id={sheetId}
704
+ className={`nav-shell-sheet${menuOpen ? " is-open" : ""}`}
705
+ aria-hidden={!menuOpen}
706
+ >
707
+ <NavShellSheetNav
708
+ items={links}
709
+ className="nav-shell-sheet-links"
710
+ currentPath={currentPath}
711
+ onNavigate={closeMenu}
712
+ />
713
+ {cta && <div className="nav-shell-sheet-cta">{cta}</div>}
714
+ </div>
715
+ </>,
716
+ document.body,
717
+ );
718
+
719
+ return (
720
+ <>
721
+ {skipTo ? <SkipLink href={skipTo} /> : null}
722
+ {/* data-* rather than a class: the scroll listener owns .is-scrolled /
723
+ .is-hidden on this same element via classList, and a React className
724
+ rewrite would wipe them. An attribute React alone controls can't
725
+ collide — and it lets CSS pin the bar in place while a menu is open. */}
726
+ <header
727
+ ref={navRef}
728
+ className={`nav-shell${menuOpen ? " is-menu-open" : ""}${clipToFrame ? " is-clipped" : ""}${startHidden ? " is-hidden" : ""}`}
729
+ data-autohide={autoHide}
730
+ data-group-open={openGroup !== null}
731
+ >
732
+ <div className="nav-shell-row relative z-[56] mx-auto flex w-full max-w-[var(--wrap,1180px)] items-center justify-between gap-[1.5rem] px-[var(--gutter,1.5rem)] max-[880px]:gap-[1rem]">
733
+ <div className="nav-shell-lead flex min-w-0 items-center">
734
+ <a
735
+ className="nav-shell-brand inline-flex items-center gap-[0.6rem] font-semibold lowercase tracking-[-0.02em] text-ink"
736
+ href={homeHref}
737
+ aria-label={homeLabel}
738
+ onClick={closeMenu}
739
+ >
740
+ {brand}
741
+ </a>
742
+ </div>
743
+ <NavShellStrip
744
+ items={links}
745
+ className="nav-shell-links flex gap-[1.8rem] text-[0.9rem] text-ink-soft max-[880px]:hidden"
746
+ openKey={openGroup}
747
+ setOpenKey={setOpenGroup}
748
+ currentPath={currentPath}
749
+ />
750
+ <div className="nav-shell-tail flex items-center gap-[0.9rem] max-[560px]:shrink-0 max-[560px]:gap-[0.5rem]">
751
+ {showThemeToggle && <ThemeToggle />}
752
+ {actions}
753
+ <Button
754
+ type="button"
755
+ ref={toggleRef}
756
+ className="nav-shell-toggle h-auto"
757
+ aria-label={menuOpen ? "Close menu" : "Open menu"}
758
+ aria-expanded={menuOpen}
759
+ aria-controls={sheetId}
760
+ onClick={() => {
761
+ // Never leave a dropdown open behind the sheet (possible after
762
+ // a resize across the 880px breakpoint).
763
+ setOpenGroup(null);
764
+ setMenuOpen((v) => !v);
765
+ }}
766
+ >
767
+ <span aria-hidden="true" />
768
+ <span aria-hidden="true" />
769
+ </Button>
770
+ </div>
771
+ </div>
772
+ </header>
773
+ {menuOverlay}
774
+ </>
775
+ );
776
+ }