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