@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,912 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* Architecture tour — the same container diagram, walked, and then swapped.
|
|
4
|
+
*
|
|
5
|
+
* The owner, after seeing the first cut: "i still want to have that slide where
|
|
6
|
+
* i do the guided box rings or actually the element that's selected or talked
|
|
7
|
+
* about should just glow and then i still want that that swipe to the digi
|
|
8
|
+
* things so we start with the diagram for the off-the-shelf solution and then we
|
|
9
|
+
* swipe over to the digi things solution the guided camera while it'd be much
|
|
10
|
+
* cooler just to guide the camera and make the elements glow it's currently
|
|
11
|
+
* broken if you could make it work".
|
|
12
|
+
*
|
|
13
|
+
* So the tour takes PAIRS of sides — `[{ rented spec + steps }, { owned spec +
|
|
14
|
+
* steps }]` — and runs them as ONE scroll timeline:
|
|
15
|
+
*
|
|
16
|
+
* - the first side's steps hold the frame still and light its boxes (a glow,
|
|
17
|
+
* not a ring, with a spotlight that drops everything outside the box back
|
|
18
|
+
* into the page);
|
|
19
|
+
* - the last fraction of the final step runs the SWAP. Each side is ONE group
|
|
20
|
+
* — its diagram and its own step list — and the groups swap by sliding, not
|
|
21
|
+
* by fading: the rented group (diagram left, list right) leaves to the LEFT
|
|
22
|
+
* as a unit, then the owned group arrives from the RIGHT (list left and
|
|
23
|
+
* bottom-aligned, diagram right). The rail does not cross the page on its
|
|
24
|
+
* own; it travels with its diagram. The owner asked for exactly this: "the
|
|
25
|
+
* diagram slides to the left, the rented text also goes left with it. And
|
|
26
|
+
* then from the right side of the screen appears the owned text with its
|
|
27
|
+
* diagram … they don't continue one another. One replaces the other so that
|
|
28
|
+
* you should see them slide on and off the screen." So neither group is ever
|
|
29
|
+
* faded — no opacity anywhere on the swap — and the two never share the
|
|
30
|
+
* frame: the exit runs over the first half of the window and the entrance
|
|
31
|
+
* over the second;
|
|
32
|
+
* - the second side's steps then drive the camera as well as the glow.
|
|
33
|
+
*
|
|
34
|
+
* The camera follows whichever side is active and can only ever zoom IN on
|
|
35
|
+
* a step's lit boxes (a step lighting the whole diagram computes ~1 and
|
|
36
|
+
* switches off), so a walk never zooms in and then has to pull back out —
|
|
37
|
+
* which is what made the earlier version read as empty space and clipping.
|
|
38
|
+
*
|
|
39
|
+
* HOW IT KNOWS WHERE ANYTHING IS. It does not. mermaid's layout is its own, so
|
|
40
|
+
* the tour measures the rendered SVG after the fact: each service is emitted as
|
|
41
|
+
* `<g id="arch-…-service-<id>">` and each boundary as `<g id="arch-…-group-<id>">`,
|
|
42
|
+
* which makes every box addressable by id without touching mermaid internals or
|
|
43
|
+
* hard-coding coordinates. Measurements are taken once each SVG appears (a
|
|
44
|
+
* MutationObserver on the stage) and again on resize, always while that pane's
|
|
45
|
+
* camera is untransformed so the numbers are in unscaled pane space.
|
|
46
|
+
*
|
|
47
|
+
* MOTION BUDGET. One rAF-throttled passive scroll listener writing custom
|
|
48
|
+
* properties (`--arch-out` / `--arch-in` on the grid; `--arch-p` on the pin);
|
|
49
|
+
* React state changes only when the coarse step index changes. The wheel is
|
|
50
|
+
* never captured: a flick skips through with the page, and once scroll rests
|
|
51
|
+
* the camera eases onto the narrated boxes (`scroll-glide`). Reduced motion
|
|
52
|
+
* gets the static treatment, and so does anything narrower than the breakpoint
|
|
53
|
+
* at which the pinned layout fits.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
57
|
+
|
|
58
|
+
import {
|
|
59
|
+
REST_GLIDE_MS,
|
|
60
|
+
REST_QUIET_MS,
|
|
61
|
+
applyRestBlend,
|
|
62
|
+
restBlendAmount,
|
|
63
|
+
shouldStartRestGlide,
|
|
64
|
+
} from "../../motion/scroll-glide";
|
|
65
|
+
import { ArchitectureDiagram, type ArchSpec } from "./ArchitectureDiagram";
|
|
66
|
+
import {
|
|
67
|
+
CAM_IDENTITY,
|
|
68
|
+
camTransform,
|
|
69
|
+
fitCamera,
|
|
70
|
+
glideAmount,
|
|
71
|
+
lerpCam,
|
|
72
|
+
mixSpot,
|
|
73
|
+
unionSpot,
|
|
74
|
+
walkCursor,
|
|
75
|
+
type CamFrame,
|
|
76
|
+
} from "./tour-camera";
|
|
77
|
+
|
|
78
|
+
export interface TourStep {
|
|
79
|
+
id: string;
|
|
80
|
+
label: string;
|
|
81
|
+
line: string;
|
|
82
|
+
/** Box ids this step is about. Empty means "the whole diagram". */
|
|
83
|
+
ids: string[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface TourSide {
|
|
87
|
+
spec: ArchSpec;
|
|
88
|
+
/**
|
|
89
|
+
* Per-step specs for a side that redraws as it is walked (the morph: the
|
|
90
|
+
* same topology with labels swapped step by step). Entry i renders at that
|
|
91
|
+
* side's local step i; missing entries and the static fallback use `spec`,
|
|
92
|
+
* which should be the side's end state. Box ids must be stable across
|
|
93
|
+
* entries so measurement, glow and camera keep working.
|
|
94
|
+
*/
|
|
95
|
+
specs?: ArchSpec[];
|
|
96
|
+
steps: TourStep[];
|
|
97
|
+
/** Short marker for the rail, e.g. "rented" / "owned". */
|
|
98
|
+
tag?: string;
|
|
99
|
+
caption?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Which edge this side's step column shows its progress rule on, and with it
|
|
102
|
+
* the step text alignment: "start" (default, rule left) or "end" (rule right,
|
|
103
|
+
* text right-aligned to it). Per side, so the rented column can sit right
|
|
104
|
+
* while the owned one keeps the left.
|
|
105
|
+
*/
|
|
106
|
+
rail?: "start" | "end";
|
|
107
|
+
/** Extra content under the step column, bottom-aligned with the frame. */
|
|
108
|
+
aside?: ReactNode;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type TourVariant = "static" | "highlight" | "camera";
|
|
112
|
+
|
|
113
|
+
export interface ArchitectureTourProps {
|
|
114
|
+
/** One side is a plain walk; two add the swipe between them. */
|
|
115
|
+
sides: TourSide[];
|
|
116
|
+
variant?: TourVariant;
|
|
117
|
+
/** Scroll spent per step, in viewport heights. The track is this times the step count. */
|
|
118
|
+
vhPerStep?: number;
|
|
119
|
+
/**
|
|
120
|
+
* Controlled step index into the flattened walk. When omitted the tour
|
|
121
|
+
* owns its step internally. Either way every step change is reported
|
|
122
|
+
* through `onStepChange`, so a parent can key side content (an invoice,
|
|
123
|
+
* a caption) off the walk without reaching into the tour.
|
|
124
|
+
*/
|
|
125
|
+
step?: number;
|
|
126
|
+
onStepChange?: (index: number) => void;
|
|
127
|
+
/** Reports the resolved treatment ("static" under reduced motion or narrow
|
|
128
|
+
viewports); a parent uses it to pick the matching fallback content. */
|
|
129
|
+
onModeChange?: (mode: TourVariant) => void;
|
|
130
|
+
/**
|
|
131
|
+
* Held inside the pin, above the diagram, so a title/lede stays on screen for
|
|
132
|
+
* the whole walk instead of scrolling away when the walk begins.
|
|
133
|
+
*/
|
|
134
|
+
header?: ReactNode;
|
|
135
|
+
className?: string;
|
|
136
|
+
/**
|
|
137
|
+
* How much of the stage a step's boxes should fill, and the zoom cap.
|
|
138
|
+
* Defaults keep a whole-graph beat at identity. A denser drawing needs a
|
|
139
|
+
* higher fill before the camera will travel between clusters.
|
|
140
|
+
*/
|
|
141
|
+
cameraFill?: number;
|
|
142
|
+
cameraMaxScale?: number;
|
|
143
|
+
/** Zoom a wide short row into view. Default fits the whole cluster. */
|
|
144
|
+
cameraCover?: boolean;
|
|
145
|
+
/**
|
|
146
|
+
* Stretch each drawing's gutters to its frame's shape in the pinned view, so
|
|
147
|
+
* a graph fills the box instead of letterboxing. Static mode sizes the frame
|
|
148
|
+
* from the drawing, so it is left alone there.
|
|
149
|
+
*/
|
|
150
|
+
fitFrame?: boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface Box {
|
|
154
|
+
x: number;
|
|
155
|
+
y: number;
|
|
156
|
+
w: number;
|
|
157
|
+
h: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface SideMeasure {
|
|
161
|
+
boxes: Record<string, Box>;
|
|
162
|
+
w: number;
|
|
163
|
+
h: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const NO_MOTION = "(prefers-reduced-motion: reduce)";
|
|
167
|
+
// The pinned treatment runs at every width. Narrow viewports keep the same
|
|
168
|
+
// single-stage scroll walk but collapse it to one column (diagrams.css), so the
|
|
169
|
+
// two stacks share one pane and the scroll swaps them.
|
|
170
|
+
const PIN_MEDIA = "(min-width: 1px)";
|
|
171
|
+
|
|
172
|
+
const clamp01 = (n: number): number => Math.max(0, Math.min(1, n));
|
|
173
|
+
const smooth = (n: number): number => {
|
|
174
|
+
const t = clamp01(n);
|
|
175
|
+
return t * t * (3 - 2 * t);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
function sameMeasure(prev: SideMeasure[], next: SideMeasure[]): boolean {
|
|
179
|
+
if (prev.length !== next.length) return false;
|
|
180
|
+
return next.every((side, si) => {
|
|
181
|
+
const before = prev[si];
|
|
182
|
+
if (!before || Math.abs(before.w - side.w) > 0.5 || Math.abs(before.h - side.h) > 0.5) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
const ids = Object.keys(side.boxes);
|
|
186
|
+
if (ids.length !== Object.keys(before.boxes).length) return false;
|
|
187
|
+
return ids.every((id) => {
|
|
188
|
+
const a = before.boxes[id];
|
|
189
|
+
const b = side.boxes[id];
|
|
190
|
+
return (
|
|
191
|
+
a !== undefined &&
|
|
192
|
+
b !== undefined &&
|
|
193
|
+
Math.abs(a.x - b.x) < 0.5 &&
|
|
194
|
+
Math.abs(a.y - b.y) < 0.5 &&
|
|
195
|
+
Math.abs(a.w - b.w) < 0.5 &&
|
|
196
|
+
Math.abs(a.h - b.h) < 0.5
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Where the pin parks: its own sticky `top`, so a consumer that moves the pin
|
|
203
|
+
in CSS moves the scroll math with it. */
|
|
204
|
+
function pinOffset(pin: HTMLElement): number {
|
|
205
|
+
const top = Number.parseFloat(getComputedStyle(pin).top);
|
|
206
|
+
return Number.isFinite(top) ? top : 88;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function usePrefersReducedMotion(): boolean {
|
|
210
|
+
const [reduced, setReduced] = useState(false);
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (typeof matchMedia !== "function") return;
|
|
213
|
+
const mq = matchMedia(NO_MOTION);
|
|
214
|
+
const sync = () => setReduced(mq.matches);
|
|
215
|
+
sync();
|
|
216
|
+
mq.addEventListener("change", sync);
|
|
217
|
+
return () => mq.removeEventListener("change", sync);
|
|
218
|
+
}, []);
|
|
219
|
+
return reduced;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** True while the viewport is wide enough for the pinned two-column layout. */
|
|
223
|
+
function usePinned(): boolean {
|
|
224
|
+
const [pinned, setPinned] = useState(false);
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (typeof matchMedia !== "function") return;
|
|
227
|
+
const mq = matchMedia(PIN_MEDIA);
|
|
228
|
+
const sync = () => setPinned(mq.matches);
|
|
229
|
+
sync();
|
|
230
|
+
mq.addEventListener("change", sync);
|
|
231
|
+
return () => mq.removeEventListener("change", sync);
|
|
232
|
+
}, []);
|
|
233
|
+
return pinned;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function ArchitectureTour({
|
|
237
|
+
sides,
|
|
238
|
+
variant = "static",
|
|
239
|
+
vhPerStep = 0.62,
|
|
240
|
+
header,
|
|
241
|
+
className,
|
|
242
|
+
step: controlledStep,
|
|
243
|
+
onStepChange,
|
|
244
|
+
onModeChange,
|
|
245
|
+
cameraFill = 0.34,
|
|
246
|
+
cameraMaxScale = 1.45,
|
|
247
|
+
cameraCover = false,
|
|
248
|
+
fitFrame = false,
|
|
249
|
+
}: ArchitectureTourProps) {
|
|
250
|
+
const reduced = usePrefersReducedMotion();
|
|
251
|
+
const wide = usePinned();
|
|
252
|
+
// The animated treatments need a pin to exist; without one they read as the
|
|
253
|
+
// static treatment rather than pinning to nothing.
|
|
254
|
+
const mode: TourVariant = reduced || !wide ? "static" : variant;
|
|
255
|
+
|
|
256
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
257
|
+
const trackRef = useRef<HTMLDivElement | null>(null);
|
|
258
|
+
const pinRef = useRef<HTMLDivElement | null>(null);
|
|
259
|
+
const gridRef = useRef<HTMLDivElement | null>(null);
|
|
260
|
+
const paneRefs = useRef<Array<HTMLDivElement | null>>([]);
|
|
261
|
+
const cameraRefs = useRef<Array<HTMLDivElement | null>>([]);
|
|
262
|
+
const spotRefs = useRef<Array<HTMLSpanElement | null>>([]);
|
|
263
|
+
const onStepChangeRef = useRef(onStepChange);
|
|
264
|
+
onStepChangeRef.current = onStepChange;
|
|
265
|
+
const controlledRef = useRef(controlledStep);
|
|
266
|
+
controlledRef.current = controlledStep;
|
|
267
|
+
/** Last index reported to the parent. -1 so the opening beat still fires. */
|
|
268
|
+
const reportedStep = useRef(-1);
|
|
269
|
+
/** Set by the scroll effect so a late-arriving SVG can resize the track. */
|
|
270
|
+
const remeasureRef = useRef<() => void>(() => {});
|
|
271
|
+
|
|
272
|
+
const [measures, setMeasures] = useState<SideMeasure[]>([]);
|
|
273
|
+
const [internalStep, setInternalStep] = useState(0);
|
|
274
|
+
const step = controlledStep ?? internalStep;
|
|
275
|
+
|
|
276
|
+
/* Report the walk outward: the flattened step (fires on mount too, so a
|
|
277
|
+
parent keying content off the walk starts in sync) and the resolved
|
|
278
|
+
treatment (so the parent can match the static fallback). Both are
|
|
279
|
+
parent-owned setters or undefined — no feedback loop either way. */
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
onStepChange?.(step);
|
|
282
|
+
}, [step, onStepChange]);
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
onModeChange?.(mode);
|
|
285
|
+
}, [mode, onModeChange]);
|
|
286
|
+
|
|
287
|
+
const flat = useMemo(
|
|
288
|
+
() => sides.flatMap((side, si) => side.steps.map((entry) => ({ ...entry, side: si }))),
|
|
289
|
+
[sides],
|
|
290
|
+
);
|
|
291
|
+
const count = flat.length;
|
|
292
|
+
const flatRef = useRef(flat);
|
|
293
|
+
flatRef.current = flat;
|
|
294
|
+
const measuresRef = useRef(measures);
|
|
295
|
+
measuresRef.current = measures;
|
|
296
|
+
/** The flat index at which the swipe has finished and the last side is live. */
|
|
297
|
+
const swapAt = sides.length > 1 ? sides[0].steps.length : count;
|
|
298
|
+
|
|
299
|
+
/* ── measure each rendered diagram ─────────────────────────────────── */
|
|
300
|
+
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
// The media queries resolve after mount, so the FIRST render is always the
|
|
303
|
+
// static branch — there is no stage to measure yet. Depending on `mode` is
|
|
304
|
+
// what re-runs this once the pinned layout exists; without it the effect
|
|
305
|
+
// returned early forever and nothing was ever measured.
|
|
306
|
+
if (mode === "static") return;
|
|
307
|
+
const pin = pinRef.current;
|
|
308
|
+
if (!pin) return;
|
|
309
|
+
|
|
310
|
+
/** Returns true once every side has at least one measured box. */
|
|
311
|
+
const measure = (): boolean => {
|
|
312
|
+
// Snapshots are in unscaled pane space. Clear the camera for the read,
|
|
313
|
+
// then put it back in the same turn so a ResizeObserver does not see a
|
|
314
|
+
// size change and re-enter.
|
|
315
|
+
// The camera eases its transform, so clearing it alone leaves the zoom
|
|
316
|
+
// on screen and every box reads scaled. Drop the transition for the read.
|
|
317
|
+
const saved = cameraRefs.current.map((camera) => camera?.style.transform ?? "");
|
|
318
|
+
cameraRefs.current.forEach((camera) => {
|
|
319
|
+
if (!camera) return;
|
|
320
|
+
camera.style.transition = "none";
|
|
321
|
+
camera.style.transform = "";
|
|
322
|
+
});
|
|
323
|
+
let done = true;
|
|
324
|
+
const next = sides.map((side, si) => {
|
|
325
|
+
const empty: SideMeasure = { boxes: {}, w: 0, h: 0 };
|
|
326
|
+
const pane = paneRefs.current[si];
|
|
327
|
+
if (!pane) {
|
|
328
|
+
done = false;
|
|
329
|
+
return empty;
|
|
330
|
+
}
|
|
331
|
+
const svg = pane.querySelector("svg");
|
|
332
|
+
if (!svg) {
|
|
333
|
+
done = false;
|
|
334
|
+
return empty;
|
|
335
|
+
}
|
|
336
|
+
const origin = svg.getBoundingClientRect();
|
|
337
|
+
if (origin.width === 0) {
|
|
338
|
+
done = false;
|
|
339
|
+
return empty;
|
|
340
|
+
}
|
|
341
|
+
const ids = [
|
|
342
|
+
...side.spec.services.map((s) => s.id),
|
|
343
|
+
...(side.spec.groups ?? []).map((g) => g.id),
|
|
344
|
+
];
|
|
345
|
+
const boxes: Record<string, Box> = {};
|
|
346
|
+
for (const id of ids) {
|
|
347
|
+
const el = svg.querySelector(`[id$="-service-${id}"], [id$="-group-${id}"]`);
|
|
348
|
+
if (!el) continue;
|
|
349
|
+
const r = el.getBoundingClientRect();
|
|
350
|
+
if (r.width === 0) continue;
|
|
351
|
+
boxes[id] = {
|
|
352
|
+
x: r.left - origin.left,
|
|
353
|
+
y: r.top - origin.top,
|
|
354
|
+
w: r.width,
|
|
355
|
+
h: r.height,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
if (Object.keys(boxes).length === 0) done = false;
|
|
359
|
+
return { boxes, w: origin.width, h: origin.height };
|
|
360
|
+
});
|
|
361
|
+
cameraRefs.current.forEach((camera, si) => {
|
|
362
|
+
if (!camera) return;
|
|
363
|
+
camera.style.transform = saved[si] ?? "";
|
|
364
|
+
void camera.offsetWidth;
|
|
365
|
+
camera.style.transition = "";
|
|
366
|
+
});
|
|
367
|
+
setMeasures((prev) => (sameMeasure(prev, next) ? prev : next));
|
|
368
|
+
return done;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// mermaid is a lazy ~2.9MB chunk that draws after this mounts, and there is
|
|
372
|
+
// no event to await — the figure only flips to `data-state="diagram"` when
|
|
373
|
+
// it is done. So rather than trust a MutationObserver to catch the draw, poll
|
|
374
|
+
// until every pane has measurable boxes and then stop, with a hard ceiling so
|
|
375
|
+
// a diagram that never renders cannot spin forever.
|
|
376
|
+
let timer = 0;
|
|
377
|
+
const tick = (): void => {
|
|
378
|
+
if (measure() && timer) {
|
|
379
|
+
window.clearInterval(timer);
|
|
380
|
+
timer = 0;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
const frame = requestAnimationFrame(tick);
|
|
384
|
+
timer = window.setInterval(tick, 250);
|
|
385
|
+
const ceiling = window.setTimeout(() => {
|
|
386
|
+
if (timer) window.clearInterval(timer);
|
|
387
|
+
timer = 0;
|
|
388
|
+
}, 12000);
|
|
389
|
+
|
|
390
|
+
const onResize = (): void => {
|
|
391
|
+
measure();
|
|
392
|
+
};
|
|
393
|
+
window.addEventListener("resize", onResize);
|
|
394
|
+
|
|
395
|
+
// A browser/page ZOOM (ctrl +/-, pinch, or a devicePixelRatio change) scales
|
|
396
|
+
// every CSS-pixel measurement but does NOT reliably raise a `resize` event, so
|
|
397
|
+
// the box coordinates kept describing the pre-zoom layout while the camera read
|
|
398
|
+
// `stage.clientWidth` live — the two frames no longer agreed and the focus
|
|
399
|
+
// drifted or clipped. A ResizeObserver on each pane's SVG fires whenever that
|
|
400
|
+
// element's own box changes, which is exactly what a zoom does, so the snapshot
|
|
401
|
+
// is retaken and the camera recomputes against matching geometry.
|
|
402
|
+
const panes = paneRefs.current.filter(Boolean) as HTMLDivElement[];
|
|
403
|
+
let observer: ResizeObserver | null = null;
|
|
404
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
405
|
+
observer = new ResizeObserver(() => {
|
|
406
|
+
measure();
|
|
407
|
+
// The SVG resizing also means the pin/track geometry moved, so anchor the
|
|
408
|
+
// scroll to the same step as well as retaking the box snapshot.
|
|
409
|
+
remeasureRef.current?.();
|
|
410
|
+
});
|
|
411
|
+
for (const pane of panes) {
|
|
412
|
+
const svg = pane.querySelector("svg");
|
|
413
|
+
if (svg) observer.observe(svg);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// A fitted drawing (`fitFrame`) re-lays its gutters once the frame is
|
|
418
|
+
// measured: every box moves but the SVG element keeps its size, so the
|
|
419
|
+
// ResizeObserver above never fires. The viewBox is what changes.
|
|
420
|
+
const relayout = new MutationObserver(() => {
|
|
421
|
+
measure();
|
|
422
|
+
remeasureRef.current?.();
|
|
423
|
+
});
|
|
424
|
+
for (const pane of panes) {
|
|
425
|
+
relayout.observe(pane, { subtree: true, attributes: true, attributeFilter: ["viewBox"] });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// devicePixelRatio is the other half: a zoom can change it (and a bare
|
|
429
|
+
// resolution change can change it without moving any element), and there is
|
|
430
|
+
// no event for it, so watch the media query that describes it.
|
|
431
|
+
const dpr = window.devicePixelRatio || 1;
|
|
432
|
+
const dprQuery =
|
|
433
|
+
typeof window.matchMedia === "function"
|
|
434
|
+
? window.matchMedia(`(resolution: ${dpr}dppx)`)
|
|
435
|
+
: null;
|
|
436
|
+
const onDpr = (): void => {
|
|
437
|
+
measure();
|
|
438
|
+
remeasureRef.current?.();
|
|
439
|
+
};
|
|
440
|
+
dprQuery?.addEventListener("change", onDpr);
|
|
441
|
+
|
|
442
|
+
return () => {
|
|
443
|
+
cancelAnimationFrame(frame);
|
|
444
|
+
if (timer) window.clearInterval(timer);
|
|
445
|
+
window.clearTimeout(ceiling);
|
|
446
|
+
window.removeEventListener("resize", onResize);
|
|
447
|
+
observer?.disconnect();
|
|
448
|
+
relayout.disconnect();
|
|
449
|
+
dprQuery?.removeEventListener("change", onDpr);
|
|
450
|
+
};
|
|
451
|
+
}, [sides, mode]);
|
|
452
|
+
|
|
453
|
+
/* ── the scroll binding ────────────────────────────────────────────── */
|
|
454
|
+
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
if (mode === "static") return;
|
|
457
|
+
const track = trackRef.current;
|
|
458
|
+
const pin = pinRef.current;
|
|
459
|
+
if (!track || !pin) return;
|
|
460
|
+
|
|
461
|
+
let distance = 0;
|
|
462
|
+
let frame = 0;
|
|
463
|
+
|
|
464
|
+
const measureTrack = () => {
|
|
465
|
+
/* Cap the scroll budget: at extreme zoom-out a viewport-relative track
|
|
466
|
+
grows without bound and the stack section reads as an endless page. */
|
|
467
|
+
distance = Math.min(Math.round(window.innerHeight * vhPerStep * Math.max(1, count)), 3000);
|
|
468
|
+
/* Cap the whole section too: the pin's own content grows with the
|
|
469
|
+
viewport, so capping only the scroll distance still leaves an
|
|
470
|
+
unbounded section at extreme zoom-out. */
|
|
471
|
+
track.style.height = `${Math.max(pin.offsetHeight, Math.min(pin.offsetHeight + distance, 4800))}px`;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const boxesFor = (entry: { side: number; ids: string[] } | undefined, si: number): Box[] => {
|
|
475
|
+
if (!entry || entry.side !== si || entry.ids.length === 0) return [];
|
|
476
|
+
const m = measuresRef.current[si];
|
|
477
|
+
if (!m) return [];
|
|
478
|
+
return entry.ids.map((id) => m.boxes[id]).filter((box): box is Box => Boolean(box));
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const frameFor = (entry: { side: number; ids: string[] } | undefined, si: number): CamFrame => {
|
|
482
|
+
const camera = cameraRefs.current[si];
|
|
483
|
+
const m = measuresRef.current[si];
|
|
484
|
+
if (!camera || !m?.w || !m.h) return CAM_IDENTITY;
|
|
485
|
+
if (!entry || entry.side !== si || entry.ids.length === 0) return CAM_IDENTITY;
|
|
486
|
+
const stage = camera.closest(".arch-tour__stage") as HTMLElement | null;
|
|
487
|
+
const stageW = stage?.clientWidth ?? 0;
|
|
488
|
+
const stageH = stage?.clientHeight ?? 0;
|
|
489
|
+
if (!stageW || !stageH) return CAM_IDENTITY;
|
|
490
|
+
const svg = paneRefs.current[si]?.querySelector("svg");
|
|
491
|
+
const liveW = svg?.clientWidth ?? 0;
|
|
492
|
+
let scale = 1;
|
|
493
|
+
let contentW = m.w;
|
|
494
|
+
let contentH = m.h;
|
|
495
|
+
if (svg && liveW > 0 && Math.abs(liveW - m.w) > 0.5) {
|
|
496
|
+
scale = liveW / m.w;
|
|
497
|
+
contentW = m.w * scale;
|
|
498
|
+
contentH = m.h * scale;
|
|
499
|
+
}
|
|
500
|
+
const picked = boxesFor(entry, si).map((b) => ({
|
|
501
|
+
x: b.x * scale,
|
|
502
|
+
y: b.y * scale,
|
|
503
|
+
w: b.w * scale,
|
|
504
|
+
h: b.h * scale,
|
|
505
|
+
}));
|
|
506
|
+
return fitCamera({
|
|
507
|
+
boxes: picked,
|
|
508
|
+
stageW,
|
|
509
|
+
stageH,
|
|
510
|
+
contentW,
|
|
511
|
+
contentH,
|
|
512
|
+
fill: cameraFill,
|
|
513
|
+
maxScale: cameraMaxScale,
|
|
514
|
+
cover: cameraCover,
|
|
515
|
+
});
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
/* The camera follows scroll, not the integer step. It rests on the
|
|
519
|
+
narrated boxes, then glides to the next ones across the tail of the
|
|
520
|
+
beat so the move and the text rail arrive together. `restBlend` eases
|
|
521
|
+
that glide back to the parked boxes after the page stops; it stays 0
|
|
522
|
+
while the reader is skipping, so the picture never lags the wheel.
|
|
523
|
+
A controlled parent (the why band) only hears the integer index —
|
|
524
|
+
without this report the rail and the glow stay on the opening beat
|
|
525
|
+
forever. */
|
|
526
|
+
let restBlend = 0;
|
|
527
|
+
const pose = (progress: number) => {
|
|
528
|
+
const cursor = walkCursor(progress, count);
|
|
529
|
+
const glide = applyRestBlend(glideAmount(cursor.frac), restBlend);
|
|
530
|
+
const steps = flatRef.current;
|
|
531
|
+
const current = steps[cursor.index];
|
|
532
|
+
const upcoming = steps[Math.min(count - 1, cursor.index + 1)];
|
|
533
|
+
cameraRefs.current.forEach((camera, si) => {
|
|
534
|
+
if (!camera) return;
|
|
535
|
+
const frame = lerpCam(frameFor(current, si), frameFor(upcoming, si), glide);
|
|
536
|
+
camera.style.transform = camTransform(frame);
|
|
537
|
+
const spot = spotRefs.current[si];
|
|
538
|
+
if (!spot) return;
|
|
539
|
+
const mixed = mixSpot(
|
|
540
|
+
unionSpot(
|
|
541
|
+
boxesFor(current, si),
|
|
542
|
+
measuresRef.current[si]?.w ?? 0,
|
|
543
|
+
measuresRef.current[si]?.h ?? 0,
|
|
544
|
+
),
|
|
545
|
+
unionSpot(
|
|
546
|
+
boxesFor(upcoming, si),
|
|
547
|
+
measuresRef.current[si]?.w ?? 0,
|
|
548
|
+
measuresRef.current[si]?.h ?? 0,
|
|
549
|
+
),
|
|
550
|
+
glide,
|
|
551
|
+
);
|
|
552
|
+
if (!mixed || mixed.opacity < 0.02) {
|
|
553
|
+
spot.style.opacity = "0";
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
spot.style.opacity = mixed.opacity.toFixed(3);
|
|
557
|
+
spot.style.transform = `translate(${mixed.rect.left}px, ${mixed.rect.top}px)`;
|
|
558
|
+
spot.style.width = `${mixed.rect.width}px`;
|
|
559
|
+
spot.style.height = `${mixed.rect.height}px`;
|
|
560
|
+
});
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const report = (index: number) => {
|
|
564
|
+
if (reportedStep.current === index) return;
|
|
565
|
+
reportedStep.current = index;
|
|
566
|
+
if (controlledRef.current === undefined) setInternalStep(index);
|
|
567
|
+
onStepChangeRef.current?.(index);
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Re-measure the track WITHOUT moving the reader.
|
|
572
|
+
*
|
|
573
|
+
* The track's height depends on `window.innerHeight`, so a viewport or zoom
|
|
574
|
+
* change resizes it — and because the track sits at a fixed document offset,
|
|
575
|
+
* its top edge and every step's scroll position move with it. Left alone that
|
|
576
|
+
* lands the reader on a different step of the walk, which is what "it moves
|
|
577
|
+
* around as I zoom" was. So read the current step first, resize, then put the
|
|
578
|
+
* scroll position back on the same step.
|
|
579
|
+
*/
|
|
580
|
+
const remeasure = () => {
|
|
581
|
+
const pinEl = pinRef.current;
|
|
582
|
+
const trackEl = trackRef.current;
|
|
583
|
+
if (!pinEl || !trackEl) {
|
|
584
|
+
measureTrack();
|
|
585
|
+
update();
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
// The fraction of the walk the reader is currently on, before the
|
|
589
|
+
// resize — UNCLAMPED. At or past either edge the walk does not hold
|
|
590
|
+
// the pin, so the reader is just scrolling the page and must not be
|
|
591
|
+
// moved at all. That bail is what stops the scroll-jump-on-load: on a
|
|
592
|
+
// fresh load the pin sits far below the viewport (fraction 0) while
|
|
593
|
+
// late layout (fonts, mermaid draw, the chat iframe) fires the pane
|
|
594
|
+
// observers, and without it every one of those callbacks scrollTo'd
|
|
595
|
+
// the tour start.
|
|
596
|
+
const beforeAvail = Math.max(1, trackEl.offsetHeight - pinEl.offsetHeight);
|
|
597
|
+
const beforeTop = trackEl.getBoundingClientRect().top + window.scrollY;
|
|
598
|
+
const beforeRaw = (pinOffset(pin) - beforeTop) / beforeAvail;
|
|
599
|
+
|
|
600
|
+
measureTrack();
|
|
601
|
+
|
|
602
|
+
if (!(beforeRaw > 0 && beforeRaw < 1)) {
|
|
603
|
+
update();
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const beforeP = clamp01(beforeRaw);
|
|
607
|
+
|
|
608
|
+
// After the resize, put the scroll back so the same fraction sits under the
|
|
609
|
+
// pin. Only do it while the walk actually holds the pin — outside it the
|
|
610
|
+
// reader is just scrolling the page and must not be moved at all.
|
|
611
|
+
const afterAvail = Math.max(1, trackEl.offsetHeight - pinEl.offsetHeight);
|
|
612
|
+
const afterTop = trackEl.getBoundingClientRect().top + window.scrollY;
|
|
613
|
+
const target = afterTop - pinOffset(pin) + beforeP * afterAvail;
|
|
614
|
+
// A zoom keeps the anchor scrollY ratio, so the browser may already be at
|
|
615
|
+
// the right place; only nudge when the step would actually change.
|
|
616
|
+
if (Math.abs(window.scrollY - target) > 2) {
|
|
617
|
+
window.scrollTo({ top: Math.max(0, Math.round(target)), behavior: "auto" });
|
|
618
|
+
}
|
|
619
|
+
update();
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const update = () => {
|
|
623
|
+
// The pinned scroll IS the track minus the pin, so progress is measured
|
|
624
|
+
// against that rather than against the requested budget — otherwise the
|
|
625
|
+
// pin releases with the walk only partly finished and the last steps are
|
|
626
|
+
// crammed into the tail.
|
|
627
|
+
const avail = Math.max(1, track.offsetHeight - pin.offsetHeight);
|
|
628
|
+
const p = clamp01((pinOffset(pin) - track.getBoundingClientRect().top) / avail);
|
|
629
|
+
pin.style.setProperty("--arch-p", p.toFixed(4));
|
|
630
|
+
|
|
631
|
+
// Each rail's own progress bar would otherwise read the whole-walk value:
|
|
632
|
+
// on the rented rail that fills only its share then the side slides away,
|
|
633
|
+
// and on the owned rail it is already full before that side starts. So
|
|
634
|
+
// each side gets a local 0→1 across its own steps.
|
|
635
|
+
let seen = 0;
|
|
636
|
+
for (let si = 0; si < sides.length; si += 1) {
|
|
637
|
+
const total = sides[si].steps.length;
|
|
638
|
+
const local = clamp01((p * count - seen) / Math.max(1, total));
|
|
639
|
+
pin.style.setProperty(`--arch-r${si}`, local.toFixed(4));
|
|
640
|
+
seen += total;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// The swipe lives in the tail of the last step of the earlier side, so the
|
|
644
|
+
// reader finishes that walk before the page changes diagram under them.
|
|
645
|
+
const v = p * count;
|
|
646
|
+
const index = Math.min(count - 1, Math.floor(v));
|
|
647
|
+
const frac = clamp01(v - index);
|
|
648
|
+
let swap = 0;
|
|
649
|
+
if (sides.length > 1) {
|
|
650
|
+
if (index >= swapAt) swap = 1;
|
|
651
|
+
else if (index === swapAt - 1) swap = smooth((frac - 0.35) / 0.65);
|
|
652
|
+
}
|
|
653
|
+
const grid = gridRef.current;
|
|
654
|
+
if (grid) {
|
|
655
|
+
// "get rid of the old, in with the new": on a wide two-column stage the
|
|
656
|
+
// rented group leaves over the first half of the window and the owned
|
|
657
|
+
// group arrives over the second, so the two are never in the frame
|
|
658
|
+
// together. On a narrow single-pane stage that hand-off would leave a
|
|
659
|
+
// blank frame at p=0.5, so the same swap value drives a crossfade.
|
|
660
|
+
const narrow = window.innerWidth < 1024;
|
|
661
|
+
grid.style.setProperty("--arch-out", clamp01(narrow ? swap : swap * 2).toFixed(4));
|
|
662
|
+
grid.style.setProperty("--arch-in", clamp01(narrow ? swap : swap * 2 - 1).toFixed(4));
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
pose(p);
|
|
666
|
+
report(index);
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
/* Skip-through / rest. Scroll position is never rewritten and the wheel
|
|
670
|
+
is never captured, so a flick crosses the walk with the page. While
|
|
671
|
+
scrollY is changing, `data-skipping` drops the time-based CSS follows
|
|
672
|
+
(they lag a fast skip). After the page has been still, the camera
|
|
673
|
+
eases onto the narrated boxes — unless that glide is already committed,
|
|
674
|
+
in which case the frame stays where the reader left it. */
|
|
675
|
+
let restRaf = 0;
|
|
676
|
+
let restTimer = 0;
|
|
677
|
+
let resting = false;
|
|
678
|
+
let lastScrollY = window.scrollY;
|
|
679
|
+
let lastMoveAt = performance.now();
|
|
680
|
+
|
|
681
|
+
const setSkipping = (on: boolean) => {
|
|
682
|
+
const root = rootRef.current;
|
|
683
|
+
if (!root) return;
|
|
684
|
+
if (on) root.setAttribute("data-skipping", "");
|
|
685
|
+
else root.removeAttribute("data-skipping");
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
const cancelRest = () => {
|
|
689
|
+
resting = false;
|
|
690
|
+
restBlend = 0;
|
|
691
|
+
if (restRaf) cancelAnimationFrame(restRaf);
|
|
692
|
+
restRaf = 0;
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
const beginRest = () => {
|
|
696
|
+
if (resting) return;
|
|
697
|
+
/* Nothing to glide while the pin is off screen: update() reads
|
|
698
|
+
getBoundingClientRect twice and writes transforms every frame, and a
|
|
699
|
+
reader who stopped at the top of the page is nowhere near the tour. */
|
|
700
|
+
const pinBox = pin.getBoundingClientRect();
|
|
701
|
+
if (pinBox.bottom < 0 || pinBox.top > window.innerHeight) {
|
|
702
|
+
setSkipping(false);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const availNow = Math.max(1, track.offsetHeight - pin.offsetHeight);
|
|
706
|
+
const progress = clamp01((pinOffset(pin) - track.getBoundingClientRect().top) / availNow);
|
|
707
|
+
const live = glideAmount(walkCursor(progress, count).frac);
|
|
708
|
+
if (!shouldStartRestGlide({ quietMs: performance.now() - lastMoveAt, liveGlide: live })) {
|
|
709
|
+
setSkipping(false);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const start = performance.now();
|
|
713
|
+
resting = true;
|
|
714
|
+
const step = (now: number) => {
|
|
715
|
+
if (!resting) return;
|
|
716
|
+
restBlend = restBlendAmount(now - start, REST_GLIDE_MS);
|
|
717
|
+
update();
|
|
718
|
+
if (restBlend < 1) {
|
|
719
|
+
restRaf = requestAnimationFrame(step);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
resting = false;
|
|
723
|
+
restRaf = 0;
|
|
724
|
+
setSkipping(false);
|
|
725
|
+
};
|
|
726
|
+
restRaf = requestAnimationFrame(step);
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const noteMove = () => {
|
|
730
|
+
const y = window.scrollY;
|
|
731
|
+
if (Math.abs(y - lastScrollY) < 0.5) return;
|
|
732
|
+
lastScrollY = y;
|
|
733
|
+
lastMoveAt = performance.now();
|
|
734
|
+
if (resting || restBlend !== 0) cancelRest();
|
|
735
|
+
setSkipping(true);
|
|
736
|
+
window.clearTimeout(restTimer);
|
|
737
|
+
restTimer = window.setTimeout(beginRest, REST_QUIET_MS);
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
const onScroll = () => {
|
|
741
|
+
noteMove();
|
|
742
|
+
cancelAnimationFrame(frame);
|
|
743
|
+
frame = requestAnimationFrame(update);
|
|
744
|
+
};
|
|
745
|
+
const onResize = () => {
|
|
746
|
+
remeasure();
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
remeasureRef.current = remeasure;
|
|
750
|
+
measureTrack();
|
|
751
|
+
update();
|
|
752
|
+
/* The sides and camera ease their transforms, so a fresh mount would
|
|
753
|
+
otherwise tween from the CSS defaults to the first pose — a remount
|
|
754
|
+
(a parent swapping content by `key`) slid the old side across the new
|
|
755
|
+
one. Motion switches on only once that pose has painted. */
|
|
756
|
+
let settle = requestAnimationFrame(() => {
|
|
757
|
+
settle = requestAnimationFrame(() => rootRef.current?.setAttribute("data-settled", ""));
|
|
758
|
+
});
|
|
759
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
760
|
+
window.addEventListener("resize", onResize);
|
|
761
|
+
|
|
762
|
+
return () => {
|
|
763
|
+
resting = false;
|
|
764
|
+
cancelAnimationFrame(frame);
|
|
765
|
+
cancelAnimationFrame(settle);
|
|
766
|
+
cancelAnimationFrame(restRaf);
|
|
767
|
+
window.clearTimeout(restTimer);
|
|
768
|
+
window.removeEventListener("scroll", onScroll);
|
|
769
|
+
window.removeEventListener("resize", onResize);
|
|
770
|
+
rootRef.current?.removeAttribute("data-skipping");
|
|
771
|
+
track.style.height = "";
|
|
772
|
+
};
|
|
773
|
+
}, [mode, count, swapAt, sides.length, vhPerStep, cameraFill, cameraMaxScale, cameraCover]);
|
|
774
|
+
|
|
775
|
+
// The diagrams arrive after mermaid draws, so the pin is taller than it was
|
|
776
|
+
// when the track was first measured — remeasure once the boxes land.
|
|
777
|
+
useEffect(() => {
|
|
778
|
+
if (measures.length === 0) return;
|
|
779
|
+
remeasureRef.current();
|
|
780
|
+
}, [measures]);
|
|
781
|
+
|
|
782
|
+
/* ── the camera ────────────────────────────────────────────────────── */
|
|
783
|
+
|
|
784
|
+
const active = flat[Math.min(step, count - 1)];
|
|
785
|
+
const activeSide = active?.side ?? 0;
|
|
786
|
+
const activeIds = mode === "static" ? [] : (active?.ids ?? []);
|
|
787
|
+
|
|
788
|
+
/* ── render ────────────────────────────────────────────────────────── */
|
|
789
|
+
|
|
790
|
+
if (mode === "static") {
|
|
791
|
+
return (
|
|
792
|
+
<div className={`arch-tour${className ? ` ${className}` : ""}`} data-variant="static">
|
|
793
|
+
{header ? <div className="arch-tour__head">{header}</div> : null}
|
|
794
|
+
{sides.map((side, si) => (
|
|
795
|
+
<div className="arch-tour__static" key={si}>
|
|
796
|
+
{side.tag ? (
|
|
797
|
+
<span className="arch-tour__tag" data-tag={side.tag}>
|
|
798
|
+
{side.tag}
|
|
799
|
+
</span>
|
|
800
|
+
) : null}
|
|
801
|
+
<ArchitectureDiagram spec={side.spec} caption={side.caption} />
|
|
802
|
+
<ol className="arch-tour__rail">
|
|
803
|
+
{side.steps.map((entry, index) => (
|
|
804
|
+
<li className="arch-tour__step" key={entry.id}>
|
|
805
|
+
<span className="arch-tour__index">{String(index + 1).padStart(2, "0")}</span>
|
|
806
|
+
<span className="arch-tour__label">{entry.label}</span>
|
|
807
|
+
<span className="arch-tour__line">{entry.line}</span>
|
|
808
|
+
</li>
|
|
809
|
+
))}
|
|
810
|
+
</ol>
|
|
811
|
+
{side.aside ? <div className="arch-tour__aside">{side.aside}</div> : null}
|
|
812
|
+
</div>
|
|
813
|
+
))}
|
|
814
|
+
</div>
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return (
|
|
819
|
+
<div
|
|
820
|
+
ref={rootRef}
|
|
821
|
+
className={`arch-tour${className ? ` ${className}` : ""}`}
|
|
822
|
+
data-variant={mode}
|
|
823
|
+
/* The walked side, so CSS can light it bright white and subdue the
|
|
824
|
+
other (static fallback omits it — both sides read full bright). */
|
|
825
|
+
data-active-side={activeSide}
|
|
826
|
+
>
|
|
827
|
+
<div className="arch-tour__track" ref={trackRef}>
|
|
828
|
+
<div className="arch-tour__pin" ref={pinRef}>
|
|
829
|
+
{header ? <div className="arch-tour__head">{header}</div> : null}
|
|
830
|
+
<div className="arch-tour__grid" ref={gridRef}>
|
|
831
|
+
{sides.map((side, si) => {
|
|
832
|
+
const base = sides.slice(0, si).reduce((n, s) => n + s.steps.length, 0);
|
|
833
|
+
/* The side's local step, clamped: ahead of its range it rests on
|
|
834
|
+
its first frame (the morph waits as the provider copy), past
|
|
835
|
+
it on its last (the provider rests full-lit for comparison). */
|
|
836
|
+
const local = Math.max(0, Math.min(side.steps.length - 1, step - base));
|
|
837
|
+
const effSpec = side.specs?.[local] ?? side.spec;
|
|
838
|
+
const mod =
|
|
839
|
+
sides.length > 1
|
|
840
|
+
? si === 0
|
|
841
|
+
? " arch-tour__side--leaving"
|
|
842
|
+
: " arch-tour__side--entering"
|
|
843
|
+
: "";
|
|
844
|
+
return (
|
|
845
|
+
<div
|
|
846
|
+
className={`arch-tour__side${mod}`}
|
|
847
|
+
key={si}
|
|
848
|
+
data-rail={side.rail ?? "start"}
|
|
849
|
+
data-active={si === activeSide}
|
|
850
|
+
>
|
|
851
|
+
<div className="arch-tour__frame">
|
|
852
|
+
<div className="arch-tour__stage">
|
|
853
|
+
<div
|
|
854
|
+
className="arch-tour__pane"
|
|
855
|
+
ref={(el) => {
|
|
856
|
+
paneRefs.current[si] = el;
|
|
857
|
+
}}
|
|
858
|
+
>
|
|
859
|
+
<div
|
|
860
|
+
className="arch-tour__camera"
|
|
861
|
+
ref={(el) => {
|
|
862
|
+
cameraRefs.current[si] = el;
|
|
863
|
+
}}
|
|
864
|
+
>
|
|
865
|
+
<ArchitectureDiagram
|
|
866
|
+
spec={effSpec}
|
|
867
|
+
lit={si === activeSide ? activeIds : undefined}
|
|
868
|
+
fit={fitFrame}
|
|
869
|
+
/>
|
|
870
|
+
{/* One spotlight, positioned from the scroll pose. A second
|
|
871
|
+
spot would stack the 9999px dim and black out the frame. */}
|
|
872
|
+
<span
|
|
873
|
+
className="arch-tour__spot"
|
|
874
|
+
aria-hidden="true"
|
|
875
|
+
ref={(el) => {
|
|
876
|
+
spotRefs.current[si] = el;
|
|
877
|
+
}}
|
|
878
|
+
/>
|
|
879
|
+
</div>
|
|
880
|
+
</div>
|
|
881
|
+
</div>
|
|
882
|
+
</div>
|
|
883
|
+
|
|
884
|
+
{side.tag ? (
|
|
885
|
+
<span className="arch-tour__side-tag" data-tag={side.tag}>
|
|
886
|
+
{side.tag}
|
|
887
|
+
</span>
|
|
888
|
+
) : null}
|
|
889
|
+
<ol className="arch-tour__rail">
|
|
890
|
+
{side.steps.map((entry, index) => {
|
|
891
|
+
const on = base + index === step;
|
|
892
|
+
return (
|
|
893
|
+
<li className={`arch-tour__step${on ? " on" : ""}`} key={entry.id}>
|
|
894
|
+
<span className="arch-tour__index">
|
|
895
|
+
{String(index + 1).padStart(2, "0")}
|
|
896
|
+
</span>
|
|
897
|
+
<span className="arch-tour__label">{entry.label}</span>
|
|
898
|
+
<span className="arch-tour__line">{entry.line}</span>
|
|
899
|
+
</li>
|
|
900
|
+
);
|
|
901
|
+
})}
|
|
902
|
+
</ol>
|
|
903
|
+
{side.aside ? <div className="arch-tour__aside">{side.aside}</div> : null}
|
|
904
|
+
</div>
|
|
905
|
+
);
|
|
906
|
+
})}
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
</div>
|
|
910
|
+
</div>
|
|
911
|
+
);
|
|
912
|
+
}
|