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