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