@ciderpress/ui 1.0.0-rc.1
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/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/0~142.js +2 -0
- package/dist/0~164.js +2 -0
- package/dist/0~2.js +2 -0
- package/dist/0~225.js +2 -0
- package/dist/0~288.js +2 -0
- package/dist/0~299.js +2 -0
- package/dist/0~3.js +2 -0
- package/dist/0~301.js +2 -0
- package/dist/0~311.js +2 -0
- package/dist/0~326.js +2 -0
- package/dist/0~40.js +2 -0
- package/dist/0~43.js +2 -0
- package/dist/0~433.js +2 -0
- package/dist/0~453.js +2 -0
- package/dist/0~457.js +2 -0
- package/dist/0~497.js +2 -0
- package/dist/0~511.js +2 -0
- package/dist/0~603.js +2 -0
- package/dist/0~616.js +2 -0
- package/dist/0~659.js +2 -0
- package/dist/0~670.js +2 -0
- package/dist/0~687.js +2 -0
- package/dist/0~706.js +2 -0
- package/dist/0~729.js +2 -0
- package/dist/0~768.js +2 -0
- package/dist/0~819.js +2 -0
- package/dist/0~830.js +2 -0
- package/dist/0~854.js +2 -0
- package/dist/0~863.js +2 -0
- package/dist/0~879.js +2 -0
- package/dist/0~96.js +2 -0
- package/dist/0~97.js +2 -0
- package/dist/components/FileTree/FileTree.css +90 -0
- package/dist/components/FileTree/FileTree.d.ts +7 -0
- package/dist/components/FileTree/FileTree.js +531 -0
- package/dist/components/FileTree/FileTreeItem.d.ts +8 -0
- package/dist/components/FileTree/RemoteSvgIcon.d.ts +11 -0
- package/dist/components/FileTree/index.d.ts +4 -0
- package/dist/components/folder-icons.d.ts +32 -0
- package/dist/components/languages.d.ts +7 -0
- package/dist/components/tree-parser/tree-parser.d.ts +12 -0
- package/dist/components/tree-parser/types.d.ts +11 -0
- package/dist/head/css/loader-apple.css +1 -0
- package/dist/head/css/loader-backdrop.css +1 -0
- package/dist/head/css/loader-dots.css +1 -0
- package/dist/head/css/themes/amber.css +1 -0
- package/dist/head/css/themes/arcade.css +1 -0
- package/dist/head/css/themes/grannysmith.css +1 -0
- package/dist/head/css/themes/honeycrisp.css +1 -0
- package/dist/head/css/themes/midnight.css +1 -0
- package/dist/head/css/themes/mulled.css +1 -0
- package/dist/head/js/loader-dots.js +1 -0
- package/dist/head/js/vscode-nav.js +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.mjs +40 -0
- package/dist/node.d.ts +81 -0
- package/dist/node.mjs +17762 -0
- package/dist/plugins/mermaid/MermaidRenderer.tsx +480 -0
- package/dist/plugins/mermaid/mermaid.css +199 -0
- package/dist/theme/components/announcement/announcement-bar.css +103 -0
- package/dist/theme/components/announcement/announcement-bar.tsx +173 -0
- package/dist/theme/components/ask-ai/ask-ai-button.css +52 -0
- package/dist/theme/components/ask-ai/ask-ai-button.tsx +56 -0
- package/dist/theme/components/content-footer/content-footer-portal.tsx +70 -0
- package/dist/theme/components/content-footer/feedback.css +47 -0
- package/dist/theme/components/content-footer/feedback.tsx +96 -0
- package/dist/theme/components/content-footer/meta-actions.css +25 -0
- package/dist/theme/components/content-footer/meta-actions.tsx +57 -0
- package/dist/theme/components/content-footer/page-pager.css +61 -0
- package/dist/theme/components/content-footer/page-pager.tsx +110 -0
- package/dist/theme/components/edit-source-button.tsx +97 -0
- package/dist/theme/components/footer/site-footer.css +179 -0
- package/dist/theme/components/footer/site-footer.tsx +115 -0
- package/dist/theme/components/home/cta.css +67 -0
- package/dist/theme/components/home/cta.tsx +77 -0
- package/dist/theme/components/home/feature-card.css +52 -0
- package/dist/theme/components/home/feature-card.tsx +131 -0
- package/dist/theme/components/home/feature.tsx +79 -0
- package/dist/theme/components/home/hero-demo.css +66 -0
- package/dist/theme/components/home/hero-demo.tsx +46 -0
- package/dist/theme/components/home/hero.css +96 -0
- package/dist/theme/components/home/hero.tsx +111 -0
- package/dist/theme/components/home/layout.tsx +183 -0
- package/dist/theme/components/home/page-rail.css +21 -0
- package/dist/theme/components/home/page-rail.tsx +19 -0
- package/dist/theme/components/home/split.css +148 -0
- package/dist/theme/components/home/split.tsx +120 -0
- package/dist/theme/components/home/trust-strip.css +33 -0
- package/dist/theme/components/home/trust-strip.tsx +43 -0
- package/dist/theme/components/home/workspaces.tsx +71 -0
- package/dist/theme/components/nav/branch-tag.css +54 -0
- package/dist/theme/components/nav/branch-tag.tsx +101 -0
- package/dist/theme/components/nav/ciderpress-doc-layout.tsx +247 -0
- package/dist/theme/components/nav/ciderpress-docs-bar.css +505 -0
- package/dist/theme/components/nav/ciderpress-docs-bar.tsx +426 -0
- package/dist/theme/components/nav/ciderpress-header.css +172 -0
- package/dist/theme/components/nav/ciderpress-header.tsx +113 -0
- package/dist/theme/components/nav/ciderpress-layout-context.tsx +49 -0
- package/dist/theme/components/nav/ciderpress-nav-hamburger.css +193 -0
- package/dist/theme/components/nav/ciderpress-nav-hamburger.tsx +112 -0
- package/dist/theme/components/nav/ciderpress-nav-menu.css +130 -0
- package/dist/theme/components/nav/ciderpress-nav-menu.tsx +372 -0
- package/dist/theme/components/nav/ciderpress-nav-search.css +73 -0
- package/dist/theme/components/nav/ciderpress-nav-search.tsx +55 -0
- package/dist/theme/components/nav/ciderpress-nav-social-links.css +35 -0
- package/dist/theme/components/nav/ciderpress-nav-social-links.tsx +87 -0
- package/dist/theme/components/nav/floating-branch-indicator.css +106 -0
- package/dist/theme/components/nav/floating-branch-indicator.tsx +109 -0
- package/dist/theme/components/nav/layout.tsx +268 -0
- package/dist/theme/components/nav/mobile-nav-cta.tsx +79 -0
- package/dist/theme/components/nav/nav-divider.css +15 -0
- package/dist/theme/components/nav/nav-divider.tsx +19 -0
- package/dist/theme/components/nav/nav-logo.css +42 -0
- package/dist/theme/components/nav/nav-logo.tsx +262 -0
- package/dist/theme/components/nav/theme-switcher.css +92 -0
- package/dist/theme/components/nav/theme-switcher.tsx +298 -0
- package/dist/theme/components/nav/topbar-cta.css +45 -0
- package/dist/theme/components/nav/topbar-cta.tsx +39 -0
- package/dist/theme/components/nav/variant-toggle.css +70 -0
- package/dist/theme/components/nav/variant-toggle.tsx +83 -0
- package/dist/theme/components/nav/version-chip.css +31 -0
- package/dist/theme/components/nav/version-chip.tsx +45 -0
- package/dist/theme/components/nav/vscode-tag.css +32 -0
- package/dist/theme/components/nav/vscode-tag.tsx +20 -0
- package/dist/theme/components/openapi/code-sample.tsx +341 -0
- package/dist/theme/components/openapi/copy-markdown-button.tsx +41 -0
- package/dist/theme/components/openapi/icons.tsx +45 -0
- package/dist/theme/components/openapi/index.ts +6 -0
- package/dist/theme/components/openapi/method-badge.tsx +24 -0
- package/dist/theme/components/openapi/openapi.css +665 -0
- package/dist/theme/components/openapi/operation.tsx +200 -0
- package/dist/theme/components/openapi/overview.tsx +207 -0
- package/dist/theme/components/openapi/parameters-table.tsx +149 -0
- package/dist/theme/components/openapi/request-body.tsx +90 -0
- package/dist/theme/components/openapi/response-list.tsx +112 -0
- package/dist/theme/components/openapi/schema-viewer.tsx +277 -0
- package/dist/theme/components/openapi/security-badges.tsx +63 -0
- package/dist/theme/components/openapi/spec-utils.ts +122 -0
- package/dist/theme/components/shared/accordion.css +118 -0
- package/dist/theme/components/shared/accordion.stories.tsx +96 -0
- package/dist/theme/components/shared/accordion.tsx +191 -0
- package/dist/theme/components/shared/brand-logo.stories.tsx +49 -0
- package/dist/theme/components/shared/brand-mark.stories.tsx +53 -0
- package/dist/theme/components/shared/browser-window.css +1 -0
- package/dist/theme/components/shared/browser-window.stories.tsx +100 -0
- package/dist/theme/components/shared/browser-window.tsx +2 -0
- package/dist/theme/components/shared/card.stories.tsx +50 -0
- package/dist/theme/components/shared/card.tsx +35 -0
- package/dist/theme/components/shared/ciderpress-logo.tsx +93 -0
- package/dist/theme/components/shared/ciderpress-mark.tsx +77 -0
- package/dist/theme/components/shared/color.css +56 -0
- package/dist/theme/components/shared/color.stories.tsx +47 -0
- package/dist/theme/components/shared/color.tsx +69 -0
- package/dist/theme/components/shared/columns.css +37 -0
- package/dist/theme/components/shared/columns.stories.tsx +91 -0
- package/dist/theme/components/shared/columns.tsx +50 -0
- package/dist/theme/components/shared/desktop-window.css +339 -0
- package/dist/theme/components/shared/desktop-window.tsx +535 -0
- package/dist/theme/components/shared/field.css +158 -0
- package/dist/theme/components/shared/field.stories.tsx +100 -0
- package/dist/theme/components/shared/field.tsx +186 -0
- package/dist/theme/components/shared/frame.css +39 -0
- package/dist/theme/components/shared/frame.stories.tsx +49 -0
- package/dist/theme/components/shared/frame.tsx +76 -0
- package/dist/theme/components/shared/icon.stories.tsx +90 -0
- package/dist/theme/components/shared/icon.tsx +41 -0
- package/dist/theme/components/shared/prompt.css +178 -0
- package/dist/theme/components/shared/prompt.stories.tsx +57 -0
- package/dist/theme/components/shared/prompt.tsx +369 -0
- package/dist/theme/components/shared/resolve-card-icon.ts +28 -0
- package/dist/theme/components/shared/section-card.stories.tsx +51 -0
- package/dist/theme/components/shared/section-card.tsx +44 -0
- package/dist/theme/components/shared/section-grid.stories.tsx +46 -0
- package/dist/theme/components/shared/section-grid.tsx +15 -0
- package/dist/theme/components/shared/status-badge.css +37 -0
- package/dist/theme/components/shared/status-badge.stories.tsx +61 -0
- package/dist/theme/components/shared/status-badge.tsx +74 -0
- package/dist/theme/components/shared/steps.css +88 -0
- package/dist/theme/components/shared/steps.stories.tsx +68 -0
- package/dist/theme/components/shared/steps.tsx +100 -0
- package/dist/theme/components/shared/tech-icon-table.stories.tsx +29 -0
- package/dist/theme/components/shared/tech-icon-table.tsx +71 -0
- package/dist/theme/components/shared/tech-tag.stories.tsx +40 -0
- package/dist/theme/components/shared/tech-tag.tsx +31 -0
- package/dist/theme/components/shared/theme-color-palette.css +80 -0
- package/dist/theme/components/shared/theme-color-palette.stories.tsx +52 -0
- package/dist/theme/components/shared/theme-color-palette.tsx +275 -0
- package/dist/theme/components/shared/tooltip.css +69 -0
- package/dist/theme/components/shared/tooltip.stories.tsx +72 -0
- package/dist/theme/components/shared/tooltip.tsx +77 -0
- package/dist/theme/components/sidebar/framework-picker.css +77 -0
- package/dist/theme/components/sidebar/framework-picker.tsx +78 -0
- package/dist/theme/components/sidebar/sidebar-filter.test.ts +140 -0
- package/dist/theme/components/sidebar/sidebar-filter.ts +79 -0
- package/dist/theme/components/sidebar/sidebar-links.css +102 -0
- package/dist/theme/components/sidebar/sidebar-links.tsx +122 -0
- package/dist/theme/components/sidebar/sidebar-promo.css +42 -0
- package/dist/theme/components/sidebar/sidebar-promo.tsx +52 -0
- package/dist/theme/components/sidebar/sidebar-scope.tsx +151 -0
- package/dist/theme/components/sidebar/sidebar-toggle.css +181 -0
- package/dist/theme/components/sidebar/sidebar-toggle.tsx +127 -0
- package/dist/theme/components/theme-provider.tsx +637 -0
- package/dist/theme/components/workspaces/card.css +151 -0
- package/dist/theme/components/workspaces/card.tsx +166 -0
- package/dist/theme/components/workspaces/grid.tsx +62 -0
- package/dist/theme/css.d.ts +1 -0
- package/dist/theme/env.d.ts +12 -0
- package/dist/theme/fonts/GeistMono-Variable.woff2 +0 -0
- package/dist/theme/fonts/GeistPixel-Square.woff2 +0 -0
- package/dist/theme/fonts/GeistSans-Variable.woff2 +0 -0
- package/dist/theme/hooks/use-ciderpress.ts +61 -0
- package/dist/theme/hooks/use-nav-items.ts +76 -0
- package/dist/theme/icons/index.ts +2 -0
- package/dist/theme/icons/tech-map.ts +228 -0
- package/dist/theme/index.tsx +180 -0
- package/dist/theme/lib/read-social-links.ts +27 -0
- package/dist/theme/lib/route-link.tsx +126 -0
- package/dist/theme/lib/safe-url.test.ts +0 -0
- package/dist/theme/lib/safe-url.ts +68 -0
- package/dist/theme/lib/theme-favicon.ts +125 -0
- package/dist/theme/lib/user-config-stub.ts +12 -0
- package/dist/theme/styles/layers.css +32 -0
- package/dist/theme/styles/overrides/badge.css +26 -0
- package/dist/theme/styles/overrides/content-footer.css +80 -0
- package/dist/theme/styles/overrides/details.css +61 -0
- package/dist/theme/styles/overrides/fonts.css +32 -0
- package/dist/theme/styles/overrides/home-card.css +133 -0
- package/dist/theme/styles/overrides/home.css +243 -0
- package/dist/theme/styles/overrides/rail.css +337 -0
- package/dist/theme/styles/overrides/rspress.css +62 -0
- package/dist/theme/styles/overrides/scrollbar.css +25 -0
- package/dist/theme/styles/overrides/section-card.css +114 -0
- package/dist/theme/styles/overrides/sidebar.css +144 -0
- package/dist/theme/styles/overrides/tokens.css +288 -0
- package/dist/theme/styles/overrides/vscode.css +88 -0
- package/dist/theme/styles/themes/amber.css +545 -0
- package/dist/theme/styles/themes/arcade-fx.css +366 -0
- package/dist/theme/styles/themes/arcade.css +273 -0
- package/dist/theme/styles/themes/grannysmith.css +545 -0
- package/dist/theme/styles/themes/honeycrisp.css +545 -0
- package/dist/theme/styles/themes/midnight.css +273 -0
- package/dist/theme/styles/themes/mulled.css +817 -0
- package/package.json +103 -0
- package/src/theme/components/announcement/announcement-bar.css +103 -0
- package/src/theme/components/announcement/announcement-bar.tsx +173 -0
- package/src/theme/components/ask-ai/ask-ai-button.css +52 -0
- package/src/theme/components/ask-ai/ask-ai-button.tsx +56 -0
- package/src/theme/components/content-footer/content-footer-portal.tsx +70 -0
- package/src/theme/components/content-footer/feedback.css +47 -0
- package/src/theme/components/content-footer/feedback.tsx +96 -0
- package/src/theme/components/content-footer/meta-actions.css +25 -0
- package/src/theme/components/content-footer/meta-actions.tsx +57 -0
- package/src/theme/components/content-footer/page-pager.css +61 -0
- package/src/theme/components/content-footer/page-pager.tsx +110 -0
- package/src/theme/components/edit-source-button.tsx +97 -0
- package/src/theme/components/footer/site-footer.css +179 -0
- package/src/theme/components/footer/site-footer.tsx +115 -0
- package/src/theme/components/home/cta.css +67 -0
- package/src/theme/components/home/cta.tsx +77 -0
- package/src/theme/components/home/feature-card.css +52 -0
- package/src/theme/components/home/feature-card.tsx +131 -0
- package/src/theme/components/home/feature.tsx +79 -0
- package/src/theme/components/home/hero-demo.css +66 -0
- package/src/theme/components/home/hero-demo.tsx +46 -0
- package/src/theme/components/home/hero.css +96 -0
- package/src/theme/components/home/hero.tsx +111 -0
- package/src/theme/components/home/layout.tsx +183 -0
- package/src/theme/components/home/page-rail.css +21 -0
- package/src/theme/components/home/page-rail.tsx +19 -0
- package/src/theme/components/home/split.css +148 -0
- package/src/theme/components/home/split.tsx +120 -0
- package/src/theme/components/home/trust-strip.css +33 -0
- package/src/theme/components/home/trust-strip.tsx +43 -0
- package/src/theme/components/home/workspaces.tsx +71 -0
- package/src/theme/components/nav/branch-tag.css +54 -0
- package/src/theme/components/nav/branch-tag.tsx +101 -0
- package/src/theme/components/nav/ciderpress-doc-layout.tsx +247 -0
- package/src/theme/components/nav/ciderpress-docs-bar.css +505 -0
- package/src/theme/components/nav/ciderpress-docs-bar.tsx +426 -0
- package/src/theme/components/nav/ciderpress-header.css +172 -0
- package/src/theme/components/nav/ciderpress-header.tsx +113 -0
- package/src/theme/components/nav/ciderpress-layout-context.tsx +49 -0
- package/src/theme/components/nav/ciderpress-nav-hamburger.css +193 -0
- package/src/theme/components/nav/ciderpress-nav-hamburger.tsx +112 -0
- package/src/theme/components/nav/ciderpress-nav-menu.css +130 -0
- package/src/theme/components/nav/ciderpress-nav-menu.tsx +372 -0
- package/src/theme/components/nav/ciderpress-nav-search.css +73 -0
- package/src/theme/components/nav/ciderpress-nav-search.tsx +55 -0
- package/src/theme/components/nav/ciderpress-nav-social-links.css +35 -0
- package/src/theme/components/nav/ciderpress-nav-social-links.tsx +87 -0
- package/src/theme/components/nav/floating-branch-indicator.css +106 -0
- package/src/theme/components/nav/floating-branch-indicator.tsx +109 -0
- package/src/theme/components/nav/layout.tsx +268 -0
- package/src/theme/components/nav/mobile-nav-cta.tsx +79 -0
- package/src/theme/components/nav/nav-divider.css +15 -0
- package/src/theme/components/nav/nav-divider.tsx +19 -0
- package/src/theme/components/nav/nav-logo.css +42 -0
- package/src/theme/components/nav/nav-logo.tsx +262 -0
- package/src/theme/components/nav/theme-switcher.css +92 -0
- package/src/theme/components/nav/theme-switcher.tsx +298 -0
- package/src/theme/components/nav/topbar-cta.css +45 -0
- package/src/theme/components/nav/topbar-cta.tsx +39 -0
- package/src/theme/components/nav/variant-toggle.css +70 -0
- package/src/theme/components/nav/variant-toggle.tsx +83 -0
- package/src/theme/components/nav/version-chip.css +31 -0
- package/src/theme/components/nav/version-chip.tsx +45 -0
- package/src/theme/components/nav/vscode-tag.css +32 -0
- package/src/theme/components/nav/vscode-tag.tsx +20 -0
- package/src/theme/components/openapi/code-sample.tsx +341 -0
- package/src/theme/components/openapi/copy-markdown-button.tsx +41 -0
- package/src/theme/components/openapi/icons.tsx +45 -0
- package/src/theme/components/openapi/index.ts +6 -0
- package/src/theme/components/openapi/method-badge.tsx +24 -0
- package/src/theme/components/openapi/openapi.css +665 -0
- package/src/theme/components/openapi/operation.tsx +200 -0
- package/src/theme/components/openapi/overview.tsx +207 -0
- package/src/theme/components/openapi/parameters-table.tsx +149 -0
- package/src/theme/components/openapi/request-body.tsx +90 -0
- package/src/theme/components/openapi/response-list.tsx +112 -0
- package/src/theme/components/openapi/schema-viewer.tsx +277 -0
- package/src/theme/components/openapi/security-badges.tsx +63 -0
- package/src/theme/components/openapi/spec-utils.ts +122 -0
- package/src/theme/components/shared/accordion.css +118 -0
- package/src/theme/components/shared/accordion.stories.tsx +96 -0
- package/src/theme/components/shared/accordion.tsx +191 -0
- package/src/theme/components/shared/brand-logo.stories.tsx +49 -0
- package/src/theme/components/shared/brand-mark.stories.tsx +53 -0
- package/src/theme/components/shared/browser-window.css +1 -0
- package/src/theme/components/shared/browser-window.stories.tsx +100 -0
- package/src/theme/components/shared/browser-window.tsx +2 -0
- package/src/theme/components/shared/card.stories.tsx +50 -0
- package/src/theme/components/shared/card.tsx +35 -0
- package/src/theme/components/shared/ciderpress-logo.tsx +93 -0
- package/src/theme/components/shared/ciderpress-mark.tsx +77 -0
- package/src/theme/components/shared/color.css +56 -0
- package/src/theme/components/shared/color.stories.tsx +47 -0
- package/src/theme/components/shared/color.tsx +69 -0
- package/src/theme/components/shared/columns.css +37 -0
- package/src/theme/components/shared/columns.stories.tsx +91 -0
- package/src/theme/components/shared/columns.tsx +50 -0
- package/src/theme/components/shared/desktop-window.css +339 -0
- package/src/theme/components/shared/desktop-window.tsx +535 -0
- package/src/theme/components/shared/field.css +158 -0
- package/src/theme/components/shared/field.stories.tsx +100 -0
- package/src/theme/components/shared/field.tsx +186 -0
- package/src/theme/components/shared/frame.css +39 -0
- package/src/theme/components/shared/frame.stories.tsx +49 -0
- package/src/theme/components/shared/frame.tsx +76 -0
- package/src/theme/components/shared/icon.stories.tsx +90 -0
- package/src/theme/components/shared/icon.tsx +41 -0
- package/src/theme/components/shared/prompt.css +178 -0
- package/src/theme/components/shared/prompt.stories.tsx +57 -0
- package/src/theme/components/shared/prompt.tsx +369 -0
- package/src/theme/components/shared/resolve-card-icon.ts +28 -0
- package/src/theme/components/shared/section-card.stories.tsx +51 -0
- package/src/theme/components/shared/section-card.tsx +44 -0
- package/src/theme/components/shared/section-grid.stories.tsx +46 -0
- package/src/theme/components/shared/section-grid.tsx +15 -0
- package/src/theme/components/shared/status-badge.css +37 -0
- package/src/theme/components/shared/status-badge.stories.tsx +61 -0
- package/src/theme/components/shared/status-badge.tsx +74 -0
- package/src/theme/components/shared/steps.css +88 -0
- package/src/theme/components/shared/steps.stories.tsx +68 -0
- package/src/theme/components/shared/steps.tsx +100 -0
- package/src/theme/components/shared/tech-icon-table.stories.tsx +29 -0
- package/src/theme/components/shared/tech-icon-table.tsx +71 -0
- package/src/theme/components/shared/tech-tag.stories.tsx +40 -0
- package/src/theme/components/shared/tech-tag.tsx +31 -0
- package/src/theme/components/shared/theme-color-palette.css +80 -0
- package/src/theme/components/shared/theme-color-palette.stories.tsx +52 -0
- package/src/theme/components/shared/theme-color-palette.tsx +275 -0
- package/src/theme/components/shared/tooltip.css +69 -0
- package/src/theme/components/shared/tooltip.stories.tsx +72 -0
- package/src/theme/components/shared/tooltip.tsx +77 -0
- package/src/theme/components/sidebar/framework-picker.css +77 -0
- package/src/theme/components/sidebar/framework-picker.tsx +78 -0
- package/src/theme/components/sidebar/sidebar-filter.test.ts +140 -0
- package/src/theme/components/sidebar/sidebar-filter.ts +79 -0
- package/src/theme/components/sidebar/sidebar-links.css +102 -0
- package/src/theme/components/sidebar/sidebar-links.tsx +122 -0
- package/src/theme/components/sidebar/sidebar-promo.css +42 -0
- package/src/theme/components/sidebar/sidebar-promo.tsx +52 -0
- package/src/theme/components/sidebar/sidebar-scope.tsx +151 -0
- package/src/theme/components/sidebar/sidebar-toggle.css +181 -0
- package/src/theme/components/sidebar/sidebar-toggle.tsx +127 -0
- package/src/theme/components/theme-provider.tsx +637 -0
- package/src/theme/components/workspaces/card.css +151 -0
- package/src/theme/components/workspaces/card.tsx +166 -0
- package/src/theme/components/workspaces/grid.tsx +62 -0
- package/src/theme/css.d.ts +1 -0
- package/src/theme/env.d.ts +12 -0
- package/src/theme/fonts/GeistMono-Variable.woff2 +0 -0
- package/src/theme/fonts/GeistPixel-Square.woff2 +0 -0
- package/src/theme/fonts/GeistSans-Variable.woff2 +0 -0
- package/src/theme/hooks/use-ciderpress.ts +61 -0
- package/src/theme/hooks/use-nav-items.ts +76 -0
- package/src/theme/icons/index.ts +2 -0
- package/src/theme/icons/tech-map.ts +228 -0
- package/src/theme/index.tsx +180 -0
- package/src/theme/lib/read-social-links.ts +27 -0
- package/src/theme/lib/route-link.tsx +126 -0
- package/src/theme/lib/safe-url.test.ts +0 -0
- package/src/theme/lib/safe-url.ts +68 -0
- package/src/theme/lib/theme-favicon.ts +125 -0
- package/src/theme/lib/user-config-stub.ts +12 -0
- package/src/theme/styles/layers.css +32 -0
- package/src/theme/styles/overrides/badge.css +26 -0
- package/src/theme/styles/overrides/content-footer.css +80 -0
- package/src/theme/styles/overrides/details.css +61 -0
- package/src/theme/styles/overrides/fonts.css +32 -0
- package/src/theme/styles/overrides/home-card.css +133 -0
- package/src/theme/styles/overrides/home.css +243 -0
- package/src/theme/styles/overrides/rail.css +337 -0
- package/src/theme/styles/overrides/rspress.css +62 -0
- package/src/theme/styles/overrides/scrollbar.css +25 -0
- package/src/theme/styles/overrides/section-card.css +114 -0
- package/src/theme/styles/overrides/sidebar.css +144 -0
- package/src/theme/styles/overrides/tokens.css +288 -0
- package/src/theme/styles/overrides/vscode.css +88 -0
- package/src/theme/styles/themes/amber.css +545 -0
- package/src/theme/styles/themes/arcade-fx.css +366 -0
- package/src/theme/styles/themes/arcade.css +273 -0
- package/src/theme/styles/themes/grannysmith.css +545 -0
- package/src/theme/styles/themes/honeycrisp.css +545 -0
- package/src/theme/styles/themes/midnight.css +273 -0
- package/src/theme/styles/themes/mulled.css +817 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/* oxlint-disable no-ternary -- raw-copied file; relaxed rules per packages/ui/CLAUDE.md */
|
|
2
|
+
import type { LogoContext, LogoFn, LogoImage, CiderpressConfig } from '@ciderpress/config'
|
|
3
|
+
// oxlint-disable-next-line import/no-unresolved -- alias provided by createRspressConfig's resolve.alias
|
|
4
|
+
import userConfigModule from '@ciderpress/internal/user-config'
|
|
5
|
+
import { BRAND_COLORS, DEFAULT_THEME_NAME } from '@ciderpress/theme'
|
|
6
|
+
import React, { useEffect, useState } from 'react'
|
|
7
|
+
import { createPortal } from 'react-dom'
|
|
8
|
+
|
|
9
|
+
import { CiderpressLogo } from '../shared/ciderpress-logo'
|
|
10
|
+
|
|
11
|
+
import './nav-logo.css'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* CSS custom property names read from `<html>` to populate `LogoContext.colors`.
|
|
15
|
+
* Kept in sync with the `--rp-c-*` variables emitted by the active theme's
|
|
16
|
+
* stylesheet.
|
|
17
|
+
*/
|
|
18
|
+
const COLOR_VARS = Object.freeze({
|
|
19
|
+
brand: '--rp-c-brand',
|
|
20
|
+
brandHover: '--rp-c-brand-dark',
|
|
21
|
+
brandSoft: '--rp-c-brand-tint',
|
|
22
|
+
bg: '--rp-c-bg',
|
|
23
|
+
text: '--rp-c-text-1',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fallback colors used during SSR / before the first paint, where
|
|
28
|
+
* `getComputedStyle` is unavailable or returns empty strings.
|
|
29
|
+
*/
|
|
30
|
+
const DEFAULT_BRAND = BRAND_COLORS[DEFAULT_THEME_NAME]
|
|
31
|
+
const FALLBACK_COLORS = Object.freeze({
|
|
32
|
+
brand: DEFAULT_BRAND.primary,
|
|
33
|
+
brandHover: DEFAULT_BRAND.hover,
|
|
34
|
+
brandSoft: DEFAULT_BRAND.soft,
|
|
35
|
+
bg: '#ffffff',
|
|
36
|
+
text: '#1f2937',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* `NavLogo` — Rspress-aware logo slot that handles all three logo configs.
|
|
41
|
+
*
|
|
42
|
+
* - Reads `userConfig.logo` from the bundled user config via the
|
|
43
|
+
* `@ciderpress/internal/user-config` alias.
|
|
44
|
+
* - Subscribes to `data-cp-theme` / `data-cp-variant` mutations on `<html>`
|
|
45
|
+
* so a function-form `logo` re-runs when the theme switches.
|
|
46
|
+
* - Portals into Rspress's `.rp-nav__title__link` element so the rendered
|
|
47
|
+
* logo sits inside the same wrapper as Rspress's stock title/logo.
|
|
48
|
+
*
|
|
49
|
+
* Branching:
|
|
50
|
+
* - `logo` is a `string` → Rspress already renders an `<img>` for it via
|
|
51
|
+
* its native `logo` config field; this slot renders `null`.
|
|
52
|
+
* - `logo` is a function → call with the live `LogoContext`. If the return
|
|
53
|
+
* value is a `LogoImage`-shaped object, spread onto `<img>`. Otherwise
|
|
54
|
+
* render as a React node.
|
|
55
|
+
* - `logo` is missing → render the default themed `<CiderpressLogo />`.
|
|
56
|
+
*
|
|
57
|
+
* @returns Portaled logo element or null
|
|
58
|
+
*/
|
|
59
|
+
export function NavLogo(): React.ReactElement | null {
|
|
60
|
+
const [target, setTarget] = useState<HTMLElement | null>(null)
|
|
61
|
+
const [themeContext, setThemeContext] = useState<LogoContext | null>(null)
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const html = globalThis.document.documentElement
|
|
65
|
+
setThemeContext(readThemeContext(html))
|
|
66
|
+
|
|
67
|
+
function findTarget() {
|
|
68
|
+
return globalThis.document.querySelector('.rp-nav__title__link') as HTMLElement | null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Try immediately, then poll on requestAnimationFrame ticks until the
|
|
72
|
+
// nav renders. Rspress's CSR nav can mount AFTER this effect fires,
|
|
73
|
+
// so a single querySelector at effect time misses the element. We
|
|
74
|
+
// belt-and-suspenders with both rAF polling AND MutationObserver in
|
|
75
|
+
// case the polling stops before the nav appears (e.g. tab inactive).
|
|
76
|
+
// oxlint-disable-next-line functional/no-let -- raf id retained for cleanup
|
|
77
|
+
let rafId: number | null = null
|
|
78
|
+
// oxlint-disable-next-line functional/no-let -- captured below for closure equality
|
|
79
|
+
let resolved = false
|
|
80
|
+
|
|
81
|
+
function tryResolve() {
|
|
82
|
+
if (resolved) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
const link = findTarget()
|
|
86
|
+
if (link !== null) {
|
|
87
|
+
// oxlint-disable-next-line functional/immutable-data -- flag flip to stop polling
|
|
88
|
+
resolved = true
|
|
89
|
+
setTarget(link)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
rafId = globalThis.requestAnimationFrame(tryResolve)
|
|
93
|
+
}
|
|
94
|
+
tryResolve()
|
|
95
|
+
|
|
96
|
+
const observer = new MutationObserver(() => {
|
|
97
|
+
setThemeContext(readThemeContext(html))
|
|
98
|
+
if (!resolved) {
|
|
99
|
+
tryResolve()
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
observer.observe(html, {
|
|
103
|
+
attributes: true,
|
|
104
|
+
attributeFilter: ['data-cp-theme', 'data-cp-variant', 'class', 'style'],
|
|
105
|
+
childList: true,
|
|
106
|
+
subtree: true,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
return () => {
|
|
110
|
+
observer.disconnect()
|
|
111
|
+
if (rafId !== null) {
|
|
112
|
+
globalThis.cancelAnimationFrame(rafId)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}, [])
|
|
116
|
+
|
|
117
|
+
const logoConfig = readLogoConfig(userConfigModule)
|
|
118
|
+
|
|
119
|
+
// String-form user logos are handled by Rspress's native <img>. NavLogo
|
|
120
|
+
// takes over for both the default case (theme-aware CiderpressLogo SVG) and
|
|
121
|
+
// function-form logos (user-defined render). The static `/logo.svg` from
|
|
122
|
+
// Rspress's <img> renders immediately on first paint; once NavLogo's
|
|
123
|
+
// portal mounts, CSS hides the static img so only the theme-aware version
|
|
124
|
+
// shows. Theme color flips via `currentColor` reading from `--rp-c-brand`.
|
|
125
|
+
if (typeof logoConfig === 'string') {
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (target === null || themeContext === null) {
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Raw-copied file — plain conditional, no ts-pattern (see packages/ui/CLAUDE.md).
|
|
134
|
+
const rendered =
|
|
135
|
+
typeof logoConfig === 'function' ? (
|
|
136
|
+
renderLogoFn({ fn: logoConfig, theme: themeContext })
|
|
137
|
+
) : (
|
|
138
|
+
<CiderpressLogo />
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return createPortal(<span className="cp-nav-logo">{rendered}</span>, target)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export { NavLogo as default }
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Extract the `logo` field from the bundled user config module. Tolerates
|
|
148
|
+
* default-exports, named-exports, and the empty stub fallback.
|
|
149
|
+
*
|
|
150
|
+
* @private
|
|
151
|
+
* @param mod - Module imported from `@ciderpress/internal/user-config`
|
|
152
|
+
* @returns The `logo` value or `undefined` when none is configured
|
|
153
|
+
*/
|
|
154
|
+
function readLogoConfig(mod: unknown): string | LogoFn | undefined {
|
|
155
|
+
if (mod === null || mod === undefined) {
|
|
156
|
+
return undefined
|
|
157
|
+
}
|
|
158
|
+
const asRecord = mod as Record<string, unknown>
|
|
159
|
+
const candidate =
|
|
160
|
+
asRecord.default !== null && asRecord.default !== undefined
|
|
161
|
+
? (asRecord.default as Partial<CiderpressConfig>)
|
|
162
|
+
: (mod as Partial<CiderpressConfig>)
|
|
163
|
+
const { logo } = candidate
|
|
164
|
+
if (typeof logo === 'string') {
|
|
165
|
+
return logo
|
|
166
|
+
}
|
|
167
|
+
if (typeof logo === 'function') {
|
|
168
|
+
return logo
|
|
169
|
+
}
|
|
170
|
+
return undefined
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface RenderLogoFnParams {
|
|
174
|
+
readonly fn: LogoFn
|
|
175
|
+
readonly theme: LogoContext
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Invoke the user's logo function and turn its return value into a
|
|
180
|
+
* renderable React node.
|
|
181
|
+
*
|
|
182
|
+
* `LogoImage` returns are detected structurally (object with a `src` key
|
|
183
|
+
* that isn't already a React element) and spread onto an `<img>` element
|
|
184
|
+
* matching Rspress's logo classes so styling stays consistent. Anything
|
|
185
|
+
* else is rendered as a React node directly.
|
|
186
|
+
*
|
|
187
|
+
* @private
|
|
188
|
+
* @param params - User function + live theme context
|
|
189
|
+
* @returns React node or null
|
|
190
|
+
*/
|
|
191
|
+
function renderLogoFn(params: RenderLogoFnParams): React.ReactNode {
|
|
192
|
+
const result = params.fn({ theme: params.theme })
|
|
193
|
+
if (isLogoImage(result)) {
|
|
194
|
+
return (
|
|
195
|
+
<img
|
|
196
|
+
src={result.src}
|
|
197
|
+
alt={result.alt ?? 'logo'}
|
|
198
|
+
width={result.width}
|
|
199
|
+
height={result.height}
|
|
200
|
+
className="rspress-logo rp-nav__title__logo-image"
|
|
201
|
+
/>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
return result as React.ReactNode
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Type guard distinguishing a `LogoImage` object from a React element.
|
|
209
|
+
* React elements are objects too, but they carry a `$$typeof` symbol key —
|
|
210
|
+
* any plain object with a string `src` and without `$$typeof` is treated
|
|
211
|
+
* as a `LogoImage`.
|
|
212
|
+
*
|
|
213
|
+
* @private
|
|
214
|
+
* @param value - Return value from a `LogoFn`
|
|
215
|
+
* @returns True when `value` should be spread onto an `<img>` element
|
|
216
|
+
*/
|
|
217
|
+
function isLogoImage(value: unknown): value is LogoImage {
|
|
218
|
+
if (value === null || typeof value !== 'object') {
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
const obj = value as Record<string, unknown>
|
|
222
|
+
if ('$$typeof' in obj) {
|
|
223
|
+
return false
|
|
224
|
+
}
|
|
225
|
+
return typeof obj.src === 'string'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Build the `LogoContext` passed to the user's logo function. Reads the
|
|
230
|
+
* active theme name and variant from `<html>` data attributes and the
|
|
231
|
+
* resolved brand colors from CSS custom properties.
|
|
232
|
+
*
|
|
233
|
+
* @private
|
|
234
|
+
* @param html - Document root element
|
|
235
|
+
* @returns Live theme context snapshot
|
|
236
|
+
*/
|
|
237
|
+
function readThemeContext(html: HTMLElement): LogoContext {
|
|
238
|
+
const variant: 'light' | 'dark' = html.dataset.cpVariant === 'light' ? 'light' : 'dark'
|
|
239
|
+
const name = typeof html.dataset.cpTheme === 'string' ? html.dataset.cpTheme : DEFAULT_THEME_NAME
|
|
240
|
+
|
|
241
|
+
const styles = globalThis.window.getComputedStyle(html)
|
|
242
|
+
function read(cssVar: string, fallback: string): string {
|
|
243
|
+
const raw = styles.getPropertyValue(cssVar).trim()
|
|
244
|
+
if (raw.length === 0) {
|
|
245
|
+
return fallback
|
|
246
|
+
}
|
|
247
|
+
return raw
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
name,
|
|
252
|
+
variant,
|
|
253
|
+
isDark: variant === 'dark',
|
|
254
|
+
colors: {
|
|
255
|
+
brand: read(COLOR_VARS.brand, FALLBACK_COLORS.brand),
|
|
256
|
+
brandHover: read(COLOR_VARS.brandHover, FALLBACK_COLORS.brandHover),
|
|
257
|
+
brandSoft: read(COLOR_VARS.brandSoft, FALLBACK_COLORS.brandSoft),
|
|
258
|
+
bg: read(COLOR_VARS.bg, FALLBACK_COLORS.bg),
|
|
259
|
+
text: read(COLOR_VARS.text, FALLBACK_COLORS.text),
|
|
260
|
+
},
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme switcher dropdown styles.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
.cp-theme-switcher {
|
|
6
|
+
position: relative;
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
margin-left: 8px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.cp-theme-switcher__btn {
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
justify-content: center;
|
|
16
|
+
width: 32px;
|
|
17
|
+
height: 32px;
|
|
18
|
+
padding: 0;
|
|
19
|
+
border: 1px solid var(--cp-c-border);
|
|
20
|
+
border-radius: var(--rp-radius);
|
|
21
|
+
background: var(--cp-c-bg);
|
|
22
|
+
color: var(--cp-c-text-2);
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
transition:
|
|
25
|
+
color 0.2s,
|
|
26
|
+
border-color 0.2s;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.cp-theme-switcher__btn:hover {
|
|
30
|
+
color: var(--cp-c-text-1);
|
|
31
|
+
border-color: var(--cp-c-brand-1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.cp-theme-switcher__dropdown {
|
|
35
|
+
position: absolute;
|
|
36
|
+
bottom: calc(100% + 8px);
|
|
37
|
+
right: 0;
|
|
38
|
+
z-index: 100;
|
|
39
|
+
min-width: 160px;
|
|
40
|
+
padding: 4px;
|
|
41
|
+
border: 1px solid var(--cp-c-border);
|
|
42
|
+
border-radius: var(--rp-radius);
|
|
43
|
+
background: var(--cp-c-bg-elv);
|
|
44
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.cp-theme-switcher__option {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: 8px;
|
|
51
|
+
width: 100%;
|
|
52
|
+
padding: 8px 12px;
|
|
53
|
+
border: none;
|
|
54
|
+
border-radius: var(--rp-radius-small);
|
|
55
|
+
background: none;
|
|
56
|
+
color: var(--cp-c-text-2);
|
|
57
|
+
font-size: 13px;
|
|
58
|
+
font-family: var(--cp-font-family-base);
|
|
59
|
+
cursor: pointer;
|
|
60
|
+
transition:
|
|
61
|
+
background 0.15s,
|
|
62
|
+
color 0.15s;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.cp-theme-switcher__option:hover {
|
|
66
|
+
background: var(--cp-c-bg-soft);
|
|
67
|
+
color: var(--cp-c-text-1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.cp-theme-switcher__option--active {
|
|
71
|
+
color: var(--cp-c-brand-1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.cp-theme-switcher__swatch {
|
|
75
|
+
display: inline-block;
|
|
76
|
+
width: 12px;
|
|
77
|
+
height: 12px;
|
|
78
|
+
border-radius: 50%;
|
|
79
|
+
border: 1px solid var(--cp-c-border);
|
|
80
|
+
flex-shrink: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.cp-theme-switcher__check {
|
|
84
|
+
margin-left: auto;
|
|
85
|
+
font-size: 11px;
|
|
86
|
+
opacity: 0.7;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.cp-theme-switcher__name {
|
|
90
|
+
flex: 1;
|
|
91
|
+
text-align: left;
|
|
92
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { DEFAULT_THEME_NAME } from '@ciderpress/theme'
|
|
2
|
+
import { match, P } from 'massaman/match'
|
|
3
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
import { Icon } from '../shared/icon.tsx'
|
|
6
|
+
|
|
7
|
+
import './theme-switcher.css'
|
|
8
|
+
|
|
9
|
+
declare const __CIDERPRESS_THEME_SWITCHER__: boolean
|
|
10
|
+
declare const __CIDERPRESS_THEME_REGISTRY__: string
|
|
11
|
+
|
|
12
|
+
type Variant = 'dark' | 'light'
|
|
13
|
+
|
|
14
|
+
interface ThemeOption {
|
|
15
|
+
readonly name: string
|
|
16
|
+
readonly label: string
|
|
17
|
+
readonly swatch: string
|
|
18
|
+
readonly variants: readonly Variant[]
|
|
19
|
+
readonly defaultVariant: Variant
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const THEME_OPTIONS: readonly ThemeOption[] = parseThemeRegistry(__CIDERPRESS_THEME_REGISTRY__)
|
|
23
|
+
|
|
24
|
+
const VALID_THEME_NAMES = new Set(THEME_OPTIONS.map((t) => t.name))
|
|
25
|
+
|
|
26
|
+
const FALLBACK_THEME = match(THEME_OPTIONS[0])
|
|
27
|
+
.with(P.nullish, () => DEFAULT_THEME_NAME as string)
|
|
28
|
+
.otherwise((entry) => entry.name)
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ThemeSwitcher — dropdown button for switching between registered themes.
|
|
32
|
+
* Only renders when `__CIDERPRESS_THEME_SWITCHER__` build-time define is true.
|
|
33
|
+
*
|
|
34
|
+
* Switching a theme updates `data-cp-theme` and `data-cp-variants` on
|
|
35
|
+
* `<html>`. When the newly active theme does not declare the user's
|
|
36
|
+
* previously selected variant, falls back to the new theme's
|
|
37
|
+
* `defaultVariant` and mirrors that to Rspress's `.rp-dark` class.
|
|
38
|
+
*
|
|
39
|
+
* @returns React element or null when theme switching is disabled
|
|
40
|
+
*/
|
|
41
|
+
export function ThemeSwitcher(): React.ReactElement | null {
|
|
42
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
43
|
+
const [activeTheme, setActiveTheme] = useState(initialThemeName)
|
|
44
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
45
|
+
|
|
46
|
+
const handleToggle = useCallback(() => {
|
|
47
|
+
setIsOpen((prev) => !prev)
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
50
|
+
const handleSelect = useCallback((theme: ThemeOption) => {
|
|
51
|
+
setActiveTheme(theme.name)
|
|
52
|
+
applyTheme(theme)
|
|
53
|
+
setIsOpen(false)
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
function handleClickOutside(event: MouseEvent): void {
|
|
58
|
+
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
59
|
+
setIsOpen(false)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function handleEscape(event: KeyboardEvent): void {
|
|
64
|
+
if (event.key === 'Escape') {
|
|
65
|
+
setIsOpen(false)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
70
|
+
document.addEventListener('keydown', handleEscape)
|
|
71
|
+
|
|
72
|
+
return () => {
|
|
73
|
+
document.removeEventListener('mousedown', handleClickOutside)
|
|
74
|
+
document.removeEventListener('keydown', handleEscape)
|
|
75
|
+
}
|
|
76
|
+
}, [])
|
|
77
|
+
|
|
78
|
+
if (!__CIDERPRESS_THEME_SWITCHER__) {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="cp-theme-switcher" ref={containerRef}>
|
|
84
|
+
<button
|
|
85
|
+
className="cp-theme-switcher__btn"
|
|
86
|
+
onClick={handleToggle}
|
|
87
|
+
aria-label="Switch theme"
|
|
88
|
+
type="button"
|
|
89
|
+
>
|
|
90
|
+
<Icon icon="mdi:palette-outline" width={16} height={16} />
|
|
91
|
+
</button>
|
|
92
|
+
{isOpen && (
|
|
93
|
+
<div className="cp-theme-switcher__dropdown">
|
|
94
|
+
{THEME_OPTIONS.map((theme) => (
|
|
95
|
+
<button
|
|
96
|
+
key={theme.name}
|
|
97
|
+
className={optionClassName(activeTheme === theme.name)}
|
|
98
|
+
onClick={() => handleSelect(theme)}
|
|
99
|
+
type="button"
|
|
100
|
+
>
|
|
101
|
+
<span
|
|
102
|
+
className="cp-theme-switcher__swatch"
|
|
103
|
+
style={{ backgroundColor: theme.swatch }}
|
|
104
|
+
/>
|
|
105
|
+
<span className="cp-theme-switcher__name">{theme.label}</span>
|
|
106
|
+
{activeTheme === theme.name && (
|
|
107
|
+
<span className="cp-theme-switcher__check">{'✓'}</span>
|
|
108
|
+
)}
|
|
109
|
+
</button>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve the active theme name for first render. Reads
|
|
119
|
+
* `localStorage['ciderpress-theme']` when available; falls back to the first
|
|
120
|
+
* registered theme when the value is missing or stale.
|
|
121
|
+
*
|
|
122
|
+
* @private
|
|
123
|
+
* @returns Resolved theme name
|
|
124
|
+
*/
|
|
125
|
+
function initialThemeName(): string {
|
|
126
|
+
if (globalThis.window === undefined) {
|
|
127
|
+
return FALLBACK_THEME
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
return sanitizeThemeName(globalThis.localStorage.getItem('ciderpress-theme'))
|
|
131
|
+
} catch {
|
|
132
|
+
return FALLBACK_THEME
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validate a stored theme name, rejecting unknown values.
|
|
138
|
+
*
|
|
139
|
+
* @private
|
|
140
|
+
* @param raw - Raw theme name from localStorage
|
|
141
|
+
* @returns Sanitized theme name, defaulting to the first registered theme
|
|
142
|
+
*/
|
|
143
|
+
function sanitizeThemeName(raw: string | null): string {
|
|
144
|
+
if (!raw) {
|
|
145
|
+
return FALLBACK_THEME
|
|
146
|
+
}
|
|
147
|
+
if (VALID_THEME_NAMES.has(raw)) {
|
|
148
|
+
return raw
|
|
149
|
+
}
|
|
150
|
+
return FALLBACK_THEME
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Build the className string for a theme option button.
|
|
155
|
+
*
|
|
156
|
+
* @private
|
|
157
|
+
* @param isActive - Whether this option is the currently active theme
|
|
158
|
+
* @returns CSS class name string
|
|
159
|
+
*/
|
|
160
|
+
function optionClassName(isActive: boolean): string {
|
|
161
|
+
if (isActive) {
|
|
162
|
+
return 'cp-theme-switcher__option cp-theme-switcher__option--active'
|
|
163
|
+
}
|
|
164
|
+
return 'cp-theme-switcher__option'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Apply a theme by updating the DOM and persisting to localStorage.
|
|
169
|
+
*
|
|
170
|
+
* When the user's current variant is supported by the new theme, that
|
|
171
|
+
* variant is preserved. Otherwise the new theme's `defaultVariant` is
|
|
172
|
+
* applied — and Rspress's `.rp-dark` class is mirrored accordingly so
|
|
173
|
+
* its built-in components stay aligned.
|
|
174
|
+
*
|
|
175
|
+
* Keep the persistence keys (`ciderpress-theme`, `ciderpress-variant`,
|
|
176
|
+
* `rspress-theme-appearance`) and the `.rp-dark` / `data-cp-*` mirror
|
|
177
|
+
* logic in sync with:
|
|
178
|
+
* - `packages/ui/src/config.ts` → `buildHeadScriptBody` (head IIFE)
|
|
179
|
+
* - `theme-provider.tsx` → `resolveActiveVariant` / `applyVariant`
|
|
180
|
+
*
|
|
181
|
+
* @private
|
|
182
|
+
* @param theme - Theme option to apply
|
|
183
|
+
*/
|
|
184
|
+
function applyTheme(theme: ThemeOption): void {
|
|
185
|
+
const html = document.documentElement
|
|
186
|
+
html.dataset.cpTheme = theme.name
|
|
187
|
+
html.dataset.cpVariants = theme.variants.join(' ')
|
|
188
|
+
try {
|
|
189
|
+
localStorage.setItem('ciderpress-theme', theme.name)
|
|
190
|
+
} catch {
|
|
191
|
+
// storage unavailable
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const currentVariant = readCurrentVariant(html)
|
|
195
|
+
const nextVariant = resolveNextVariant(currentVariant, theme)
|
|
196
|
+
|
|
197
|
+
html.dataset.cpVariant = nextVariant
|
|
198
|
+
if (nextVariant === 'dark') {
|
|
199
|
+
html.classList.add('rp-dark', 'dark')
|
|
200
|
+
html.dataset.dark = 'true'
|
|
201
|
+
} else {
|
|
202
|
+
html.classList.remove('rp-dark', 'dark')
|
|
203
|
+
html.dataset.dark = 'false'
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
localStorage.setItem('rspress-theme-appearance', nextVariant)
|
|
207
|
+
localStorage.setItem('ciderpress-variant', nextVariant)
|
|
208
|
+
} catch {
|
|
209
|
+
// storage unavailable
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Pick the variant to apply after a theme switch — preserves the user's
|
|
215
|
+
* current variant when the new theme supports it, otherwise falls back
|
|
216
|
+
* to the new theme's `defaultVariant`.
|
|
217
|
+
*
|
|
218
|
+
* @private
|
|
219
|
+
* @param current - Currently active variant (or `null` when none is set)
|
|
220
|
+
* @param theme - Theme being switched to
|
|
221
|
+
* @returns Resolved variant for the new theme
|
|
222
|
+
*/
|
|
223
|
+
function resolveNextVariant(current: Variant | null, theme: ThemeOption): Variant {
|
|
224
|
+
if (current === null) {
|
|
225
|
+
return theme.defaultVariant
|
|
226
|
+
}
|
|
227
|
+
if (theme.variants.includes(current)) {
|
|
228
|
+
return current
|
|
229
|
+
}
|
|
230
|
+
return theme.defaultVariant
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Read the current `data-cp-variant` from `<html>` as a typed value.
|
|
235
|
+
* Returns `null` when no attribute is present or the value isn't a
|
|
236
|
+
* known variant.
|
|
237
|
+
*
|
|
238
|
+
* @private
|
|
239
|
+
* @param html - HTML document element
|
|
240
|
+
* @returns Current variant or `null`
|
|
241
|
+
*/
|
|
242
|
+
function readCurrentVariant(html: HTMLElement): Variant | null {
|
|
243
|
+
const raw = html.dataset.cpVariant
|
|
244
|
+
if (raw === 'dark' || raw === 'light') {
|
|
245
|
+
return raw
|
|
246
|
+
}
|
|
247
|
+
return null
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Parse the `__CIDERPRESS_THEME_REGISTRY__` build-time define into a typed
|
|
252
|
+
* list of theme options. Falls back to an empty list when the define is
|
|
253
|
+
* missing or malformed — the switcher then renders no options.
|
|
254
|
+
*
|
|
255
|
+
* @private
|
|
256
|
+
* @param raw - Raw JSON string from the build-time define
|
|
257
|
+
* @returns Validated list of theme options
|
|
258
|
+
*/
|
|
259
|
+
function parseThemeRegistry(raw: string): readonly ThemeOption[] {
|
|
260
|
+
if (!raw || raw === '""' || raw === 'undefined') {
|
|
261
|
+
return []
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
const parsed: unknown = JSON.parse(raw)
|
|
265
|
+
if (!Array.isArray(parsed)) {
|
|
266
|
+
return []
|
|
267
|
+
}
|
|
268
|
+
return parsed.filter(isThemeOption)
|
|
269
|
+
} catch {
|
|
270
|
+
return []
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Type-guard that validates a registry entry has the shape expected by
|
|
276
|
+
* the switcher. Any entry missing required fields or carrying the wrong
|
|
277
|
+
* runtime types is dropped silently.
|
|
278
|
+
*
|
|
279
|
+
* @private
|
|
280
|
+
* @param value - Candidate registry entry
|
|
281
|
+
* @returns True when the entry matches the `ThemeOption` shape
|
|
282
|
+
*/
|
|
283
|
+
function isThemeOption(value: unknown): value is ThemeOption {
|
|
284
|
+
if (value === null || typeof value !== 'object') {
|
|
285
|
+
return false
|
|
286
|
+
}
|
|
287
|
+
const v = value as Record<string, unknown>
|
|
288
|
+
if (typeof v.name !== 'string' || typeof v.label !== 'string' || typeof v.swatch !== 'string') {
|
|
289
|
+
return false
|
|
290
|
+
}
|
|
291
|
+
if (v.defaultVariant !== 'dark' && v.defaultVariant !== 'light') {
|
|
292
|
+
return false
|
|
293
|
+
}
|
|
294
|
+
if (!Array.isArray(v.variants)) {
|
|
295
|
+
return false
|
|
296
|
+
}
|
|
297
|
+
return v.variants.every((m) => m === 'dark' || m === 'light')
|
|
298
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
.cp-topbar-cta {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
gap: 6px;
|
|
6
|
+
padding: 6px 14px;
|
|
7
|
+
font-size: 12.5px;
|
|
8
|
+
font-weight: 600;
|
|
9
|
+
border-radius: var(--cp-radius-pill);
|
|
10
|
+
background: var(--cp-c-brand-1);
|
|
11
|
+
color: var(--cp-c-brand-fg);
|
|
12
|
+
border: 1px solid var(--cp-c-brand-1);
|
|
13
|
+
text-decoration: none;
|
|
14
|
+
white-space: nowrap;
|
|
15
|
+
transition: all 0.15s ease;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.cp-topbar-cta:hover {
|
|
19
|
+
background: var(--cp-c-brand-2);
|
|
20
|
+
border-color: var(--cp-c-brand-2);
|
|
21
|
+
color: var(--cp-c-brand-fg);
|
|
22
|
+
text-decoration: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Hide on narrow viewports — the navbar gets too tight and the CTA
|
|
26
|
+
gets clipped against the right edge. The mobile slide-over menu
|
|
27
|
+
surfaces the same action via `MobileNavCTA` (portalled into the
|
|
28
|
+
nav-screen). */
|
|
29
|
+
@media (max-width: 720px) {
|
|
30
|
+
html .rp-nav .cp-topbar-cta {
|
|
31
|
+
display: none;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Portalled CTA inside the mobile nav-screen — matches the slide-over's
|
|
36
|
+
padding and stretches full-width like other menu rows. */
|
|
37
|
+
html .cp-mobile-nav-cta {
|
|
38
|
+
padding: 16px 24px 8px;
|
|
39
|
+
}
|
|
40
|
+
html .cp-mobile-nav-cta .cp-topbar-cta {
|
|
41
|
+
display: flex;
|
|
42
|
+
width: 100%;
|
|
43
|
+
padding: 12px 16px;
|
|
44
|
+
font-size: 13px;
|
|
45
|
+
}
|