@takazudo/zudo-doc 0.2.21 → 1.0.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/bin/gen-z-index.mjs +164 -0
- package/bin/tags-audit-runner.ts +126 -0
- package/bin/tags-audit.mjs +64 -0
- package/bin/zudo-doc.mjs +83 -0
- package/dist/ai-chat-modal/index.d.ts +8 -0
- package/dist/ai-chat-modal/index.js +243 -0
- package/dist/body-foot-util/body-foot-util-area.d.ts +4 -8
- package/dist/body-foot-util/edit-link.d.ts +4 -8
- package/dist/body-foot-util/index.d.ts +4 -3
- package/dist/breadcrumb/breadcrumb.d.ts +5 -9
- package/dist/breadcrumb/find-path.d.ts +2 -5
- package/dist/breadcrumb/index.d.ts +4 -4
- package/dist/breadcrumb/types.d.ts +3 -5
- package/dist/category-nav/index.d.ts +86 -0
- package/dist/category-nav/index.js +44 -0
- package/dist/category-tree-nav/index.d.ts +65 -0
- package/dist/category-tree-nav/index.js +24 -0
- package/dist/code-group/index.d.ts +10 -0
- package/dist/code-group/index.js +37 -0
- package/dist/code-syntax/code-block-enhancer-script.d.ts +1 -3
- package/dist/code-syntax/code-block-enhancer.d.ts +3 -6
- package/dist/code-syntax/index.d.ts +9 -8
- package/dist/code-syntax/mermaid-init-script.d.ts +3 -5
- package/dist/code-syntax/mermaid-init.d.ts +4 -7
- package/dist/code-syntax/tabs-init-script.d.ts +1 -3
- package/dist/code-syntax/tabs-init.d.ts +3 -6
- package/dist/code-syntax/tabs.d.ts +4 -7
- package/dist/color-scheme-utils.d.ts +125 -0
- package/dist/color-scheme-utils.js +151 -0
- package/dist/compose-meta-title/index.d.ts +7 -0
- package/dist/compose-meta-title/index.js +10 -0
- package/dist/content/component-map.d.ts +13 -17
- package/dist/content/content-blockquote.d.ts +3 -6
- package/dist/content/content-code.d.ts +3 -6
- package/dist/content/content-code.js +1 -1
- package/dist/content/content-link.d.ts +3 -6
- package/dist/content/content-link.js +1 -1
- package/dist/content/content-ol.d.ts +3 -6
- package/dist/content/content-paragraph.d.ts +3 -6
- package/dist/content/content-strong.d.ts +3 -6
- package/dist/content/content-table.d.ts +3 -6
- package/dist/content/content-ul.d.ts +3 -6
- package/dist/content/heading-h2.d.ts +3 -6
- package/dist/content/heading-h3.d.ts +3 -6
- package/dist/content/heading-h4.d.ts +3 -6
- package/dist/content/index.d.ts +29 -13
- package/dist/content-admonition/index.d.ts +15 -0
- package/dist/content-admonition/index.js +14 -0
- package/dist/design-token-panel-bootstrap.d.ts +38 -0
- package/dist/design-token-panel-bootstrap.js +29 -0
- package/dist/desktop-sidebar-toggle-island/index.d.ts +5 -0
- package/dist/desktop-sidebar-toggle-island/index.js +62 -0
- package/dist/details/details.d.ts +3 -7
- package/dist/details/index.d.ts +2 -2
- package/dist/doc-body-end/index.d.ts +21 -0
- package/dist/doc-body-end/index.js +15 -0
- package/dist/doc-content-header/index.d.ts +69 -0
- package/dist/doc-content-header/index.js +49 -0
- package/dist/doc-history/index.d.ts +10 -0
- package/dist/doc-history/index.js +407 -0
- package/dist/doc-history-area/index.d.ts +84 -0
- package/dist/doc-history-area/index.js +76 -0
- package/dist/doc-metainfo-area/index.d.ts +46 -0
- package/dist/doc-metainfo-area/index.js +41 -0
- package/dist/doc-page-props/index.d.ts +93 -0
- package/dist/doc-page-props/index.js +0 -0
- package/dist/doc-page-renderer/index.d.ts +128 -0
- package/dist/doc-page-renderer/index.js +103 -0
- package/dist/doc-page-shell/index.d.ts +136 -0
- package/dist/doc-page-shell/index.js +127 -0
- package/dist/doc-pager/index.d.ts +25 -0
- package/dist/doc-pager/index.js +41 -0
- package/dist/doc-route-entries/index.d.ts +125 -0
- package/dist/doc-route-entries/index.js +76 -0
- package/dist/doc-route-paths/index.d.ts +54 -0
- package/dist/doc-route-paths/index.js +61 -0
- package/dist/doc-tags-area/index.d.ts +38 -0
- package/dist/doc-tags-area/index.js +30 -0
- package/dist/doclayout/anchors.d.ts +7 -9
- package/dist/doclayout/doc-layout-with-defaults.d.ts +16 -12
- package/dist/doclayout/doc-layout.d.ts +5 -9
- package/dist/doclayout/index.d.ts +6 -6
- package/dist/eject/index.d.ts +33 -0
- package/dist/eject/index.js +229 -0
- package/dist/extract-headings/index.d.ts +43 -0
- package/dist/extract-headings/index.js +111 -0
- package/dist/factory-context/index.d.ts +80 -0
- package/dist/factory-context/index.js +0 -0
- package/dist/features.css +882 -0
- package/dist/footer/footer.d.ts +4 -8
- package/dist/footer/index.d.ts +13 -3
- package/dist/footer/types.d.ts +4 -6
- package/dist/footer-with-defaults/index.d.ts +63 -0
- package/dist/footer-with-defaults/index.js +128 -0
- package/dist/frontmatter-preview-data/index.d.ts +26 -0
- package/dist/frontmatter-preview-data/index.js +23 -0
- package/dist/github-helpers/index.d.ts +17 -0
- package/dist/github-helpers/index.js +16 -0
- package/dist/head/doc-head.d.ts +3 -6
- package/dist/head/index.d.ts +9 -5
- package/dist/head/og-tags.d.ts +4 -7
- package/dist/head/twitter-card.d.ts +3 -6
- package/dist/head/types.d.ts +1 -3
- package/dist/head-with-defaults/index.d.ts +49 -0
- package/dist/head-with-defaults/index.js +62 -0
- package/dist/header/header.d.ts +6 -10
- package/dist/header/index.d.ts +16 -6
- package/dist/header/nav-active.d.ts +5 -7
- package/dist/header/nav-overflow-script.d.ts +1 -3
- package/dist/header/right-items.d.ts +3 -6
- package/dist/header/types.d.ts +10 -12
- package/dist/header-with-defaults/index.d.ts +77 -0
- package/dist/header-with-defaults/index.js +163 -0
- package/dist/html-preview-wrapper/dedent.d.ts +1 -3
- package/dist/html-preview-wrapper/highlighted-code.d.ts +3 -7
- package/dist/html-preview-wrapper/html-preview-wrapper.d.ts +6 -10
- package/dist/html-preview-wrapper/html-preview.d.ts +5 -9
- package/dist/html-preview-wrapper/index.d.ts +8 -5
- package/dist/html-preview-wrapper/preflight.d.ts +1 -3
- package/dist/html-preview-wrapper/preview-base.d.ts +4 -8
- package/dist/i18n-version/index.d.ts +7 -5
- package/dist/i18n-version/language-switcher.d.ts +5 -8
- package/dist/i18n-version/types.d.ts +3 -5
- package/dist/i18n-version/version-banner.d.ts +4 -8
- package/dist/i18n-version/version-switcher.d.ts +7 -10
- package/dist/icons/index.d.ts +9 -13
- package/dist/image-enlarge/index.d.ts +13 -0
- package/dist/image-enlarge/index.js +176 -0
- package/dist/index.d.ts +26 -2
- package/dist/inline-version-switcher/index.d.ts +31 -0
- package/dist/inline-version-switcher/index.js +44 -0
- package/dist/integrations/claude-resources/escape-for-mdx.d.ts +1 -3
- package/dist/integrations/claude-resources/generate.d.ts +2 -4
- package/dist/integrations/claude-resources/index.d.ts +7 -10
- package/dist/integrations/doc-history/index.d.ts +21 -24
- package/dist/integrations/doc-history/pre-build.d.ts +7 -9
- package/dist/integrations/llms-txt/dev-middleware.d.ts +7 -11
- package/dist/integrations/llms-txt/emit.d.ts +2 -7
- package/dist/integrations/llms-txt/generate.d.ts +3 -8
- package/dist/integrations/llms-txt/index.d.ts +33 -8
- package/dist/integrations/llms-txt/load.d.ts +3 -7
- package/dist/integrations/llms-txt/strip.d.ts +2 -6
- package/dist/integrations/llms-txt/types.d.ts +9 -13
- package/dist/integrations/search-index/build-emitter.d.ts +4 -7
- package/dist/integrations/search-index/collect.d.ts +2 -5
- package/dist/integrations/search-index/dev-middleware.d.ts +5 -8
- package/dist/integrations/search-index/index.d.ts +7 -5
- package/dist/integrations/search-index/types.d.ts +5 -7
- package/dist/island-types/index.d.ts +44 -0
- package/dist/island-types/index.js +12 -0
- package/dist/locale-merge/index.d.ts +114 -0
- package/dist/locale-merge/index.js +27 -0
- package/dist/math-block/index.d.ts +18 -0
- package/dist/math-block/index.js +29 -0
- package/dist/md-utils/index.d.ts +6 -8
- package/dist/md-utils/index.js +2 -1
- package/dist/mdx-components/index.d.ts +46 -0
- package/dist/mdx-components/index.js +161 -0
- package/dist/mermaid-enlarge/index.d.ts +11 -0
- package/dist/mermaid-enlarge/index.js +290 -0
- package/dist/metainfo/doc-metainfo.d.ts +5 -9
- package/dist/metainfo/doc-tags.d.ts +7 -11
- package/dist/metainfo/frontmatter-preview.d.ts +8 -12
- package/dist/metainfo/index.d.ts +6 -4
- package/dist/nav-data-prep/index.d.ts +72 -0
- package/dist/nav-data-prep/index.js +34 -0
- package/dist/nav-indexing/category-nav.d.ts +4 -8
- package/dist/nav-indexing/category-tree-nav.d.ts +4 -8
- package/dist/nav-indexing/doc-card-grid.d.ts +4 -8
- package/dist/nav-indexing/docs-sitemap.d.ts +4 -8
- package/dist/nav-indexing/index.d.ts +37 -10
- package/dist/nav-indexing/nav-card-grid.d.ts +4 -8
- package/dist/nav-indexing/site-tree-nav-demo.d.ts +4 -8
- package/dist/nav-indexing/tag-nav.d.ts +6 -10
- package/dist/nav-indexing/types.d.ts +6 -8
- package/dist/nav-indexing/versions-page-content.d.ts +4 -8
- package/dist/nav-scope/index.d.ts +41 -0
- package/dist/nav-scope/index.js +41 -0
- package/dist/nav-source-cache/index.d.ts +13 -0
- package/dist/nav-source-cache/index.js +28 -0
- package/dist/nav-source-docs/index.d.ts +169 -0
- package/dist/nav-source-docs/index.js +123 -0
- package/dist/page-loading/index.d.ts +1 -2
- package/dist/page-loading/page-loading-overlay.d.ts +4 -8
- package/dist/plugins/claude-resources.d.ts +3 -0
- package/dist/plugins/claude-resources.js +27 -0
- package/dist/plugins/connect-adapter.d.ts +31 -0
- package/dist/plugins/connect-adapter.js +78 -0
- package/dist/plugins/doc-history.d.ts +3 -0
- package/dist/plugins/doc-history.js +54 -0
- package/dist/plugins/llms-txt.d.ts +3 -0
- package/dist/plugins/llms-txt.js +48 -0
- package/dist/plugins/plugin-utils.d.ts +17 -0
- package/dist/plugins/plugin-utils.js +11 -0
- package/dist/plugins/routes.d.ts +2 -0
- package/dist/plugins/routes.js +100 -0
- package/dist/plugins/search-index.d.ts +3 -0
- package/dist/plugins/search-index.js +24 -0
- package/dist/preset.d.ts +198 -0
- package/dist/preset.js +185 -0
- package/dist/render-markdown/index.d.ts +5 -0
- package/dist/render-markdown/index.js +115 -0
- package/dist/robots.d.ts +36 -0
- package/dist/robots.js +17 -0
- package/dist/route-enumerators/index.d.ts +204 -0
- package/dist/route-enumerators/index.js +156 -0
- package/dist/routes/404.d.ts +7 -0
- package/dist/routes/404.js +47 -0
- package/dist/routes/_chrome.d.ts +32 -0
- package/dist/routes/_chrome.js +365 -0
- package/dist/routes/_context.d.ts +56 -0
- package/dist/routes/_context.js +222 -0
- package/dist/routes/_docs-helpers.d.ts +43 -0
- package/dist/routes/_docs-helpers.js +173 -0
- package/dist/routes/_virtual.d.js +0 -0
- package/dist/routes/api-ai-chat.d.ts +5 -0
- package/dist/routes/api-ai-chat.js +24 -0
- package/dist/routes/docs-slug.d.ts +21 -0
- package/dist/routes/docs-slug.js +26 -0
- package/dist/routes/docs-tags-index.d.ts +7 -0
- package/dist/routes/docs-tags-index.js +11 -0
- package/dist/routes/docs-tags-tag.d.ts +23 -0
- package/dist/routes/docs-tags-tag.js +19 -0
- package/dist/routes/docs-versions.d.ts +7 -0
- package/dist/routes/docs-versions.js +11 -0
- package/dist/routes/index.d.ts +7 -0
- package/dist/routes/index.js +117 -0
- package/dist/routes/locale-docs-slug.d.ts +27 -0
- package/dist/routes/locale-docs-slug.js +40 -0
- package/dist/routes/locale-docs-tags-index.d.ts +18 -0
- package/dist/routes/locale-docs-tags-index.js +15 -0
- package/dist/routes/locale-docs-tags-tag.d.ts +25 -0
- package/dist/routes/locale-docs-tags-tag.js +22 -0
- package/dist/routes/locale-docs-versions.d.ts +18 -0
- package/dist/routes/locale-docs-versions.js +15 -0
- package/dist/routes/locale-index.d.ts +24 -0
- package/dist/routes/locale-index.js +132 -0
- package/dist/routes/robots.txt.d.ts +3 -0
- package/dist/routes/robots.txt.js +8 -0
- package/dist/routes/sitemap.xml.d.ts +5 -0
- package/dist/routes/sitemap.xml.js +31 -0
- package/dist/routes/v-docs-slug.d.ts +27 -0
- package/dist/routes/v-docs-slug.js +34 -0
- package/dist/routes/v-locale-docs-slug.d.ts +31 -0
- package/dist/routes/v-locale-docs-slug.js +50 -0
- package/dist/safelist.css +1 -1
- package/dist/search-widget/index.d.ts +38 -0
- package/dist/search-widget/index.js +169 -0
- package/dist/search-widget-script/index.d.ts +18 -0
- package/dist/search-widget-script/index.js +465 -0
- package/dist/settings.d.ts +270 -0
- package/dist/settings.js +0 -0
- package/dist/sidebar/index.d.ts +21 -3
- package/dist/sidebar/sidebar.d.ts +5 -9
- package/dist/sidebar/types.d.ts +4 -6
- package/dist/sidebar-prepaint/index.d.ts +21 -0
- package/dist/sidebar-prepaint/index.js +23 -0
- package/dist/sidebar-resizer/index.d.ts +2 -6
- package/dist/sidebar-resizer/sidebar-resizer-init.d.ts +6 -10
- package/dist/sidebar-toggle-island/index.d.ts +13 -0
- package/dist/sidebar-toggle-island/index.js +122 -0
- package/dist/sidebar-tree/build-tree.d.ts +4 -8
- package/dist/sidebar-tree/category-meta.d.ts +3 -6
- package/dist/sidebar-tree/index.d.ts +18 -3
- package/dist/sidebar-tree/types.d.ts +6 -8
- package/dist/sidebar-tree-island/index.d.ts +13 -0
- package/dist/sidebar-tree-island/index.js +454 -0
- package/dist/sidebar-utils/index.d.ts +64 -0
- package/dist/sidebar-utils/index.js +88 -0
- package/dist/sidebar-with-defaults/index.d.ts +38 -0
- package/dist/sidebar-with-defaults/index.js +45 -0
- package/dist/site-tree-nav/index.d.ts +71 -0
- package/dist/site-tree-nav/index.js +36 -0
- package/dist/site-tree-nav-island/index.d.ts +11 -0
- package/dist/site-tree-nav-island/index.js +176 -0
- package/dist/slug/index.d.ts +4 -0
- package/dist/slug/index.js +19 -0
- package/dist/smart-break/index.d.ts +55 -0
- package/dist/{toc/smart-break.js → smart-break/index.js} +30 -3
- package/dist/tab-item/index.d.ts +2 -2
- package/dist/tab-item/tab-item.d.ts +6 -7
- package/dist/tag-helpers/index.d.ts +40 -0
- package/dist/tag-helpers/index.js +54 -0
- package/dist/tag-pages/index.d.ts +114 -0
- package/dist/tag-pages/index.js +146 -0
- package/dist/tags-audit.d.ts +107 -0
- package/dist/tags-audit.js +321 -0
- package/dist/theme/color-scheme-provider.d.ts +4 -8
- package/dist/theme/color-tweak-export-modal.d.ts +4 -7
- package/dist/theme/design-token-serde.d.ts +14 -17
- package/dist/theme/design-token-types.d.ts +4 -6
- package/dist/theme/iframe-bridge.d.ts +12 -14
- package/dist/theme/index.d.ts +7 -9
- package/dist/theme/theme-toggle.d.ts +3 -8
- package/dist/theme-toggle/color-scheme-sync.d.ts +5 -7
- package/dist/theme-toggle/index.d.ts +4 -8
- package/dist/toc/cx.d.ts +2 -3
- package/dist/toc/index.d.ts +8 -6
- package/dist/toc/mobile-toc.d.ts +5 -9
- package/dist/toc/mobile-toc.js +1 -1
- package/dist/toc/toc-title.d.ts +1 -3
- package/dist/toc/toc.d.ts +5 -9
- package/dist/toc/toc.js +1 -1
- package/dist/toc/types.d.ts +1 -3
- package/dist/toc/use-active-heading.d.ts +4 -7
- package/dist/transitions/index.d.ts +1 -1
- package/dist/transitions/page-events.d.ts +4 -6
- package/dist/tree-nav-shared/index.d.ts +22 -0
- package/dist/tree-nav-shared/index.js +66 -0
- package/dist/url-helpers/index.d.ts +37 -0
- package/dist/url-helpers/index.js +106 -0
- package/dist/url-normalizer/index.d.ts +39 -1
- package/dist/url-normalizer/normalize.d.ts +3 -5
- package/dist/use-modal-dialog/index.d.ts +70 -0
- package/dist/use-modal-dialog/index.js +73 -0
- package/dist/versions-page/index.d.ts +55 -0
- package/dist/versions-page/index.js +73 -0
- package/eject/breadcrumb/breadcrumb.tsx +194 -0
- package/eject/breadcrumb/find-path.ts +26 -0
- package/eject/breadcrumb/index.ts +4 -0
- package/eject/breadcrumb/types.ts +32 -0
- package/eject/code-group/index.tsx +78 -0
- package/eject/content-admonition/index.tsx +50 -0
- package/eject/details/details.tsx +34 -0
- package/eject/details/index.ts +4 -0
- package/eject/doc-pager/index.tsx +91 -0
- package/eject/footer/footer.tsx +167 -0
- package/eject/footer/index.ts +19 -0
- package/eject/footer/types.ts +60 -0
- package/eject/header/header.tsx +725 -0
- package/eject/header/index.ts +36 -0
- package/eject/header/nav-active.ts +122 -0
- package/eject/header/nav-overflow-script.ts +302 -0
- package/eject/header/right-items.ts +78 -0
- package/eject/header/types.ts +69 -0
- package/eject/page-loading/index.ts +10 -0
- package/eject/page-loading/page-loading-overlay.tsx +103 -0
- package/eject/sidebar/index.ts +27 -0
- package/eject/sidebar/sidebar.tsx +137 -0
- package/eject/sidebar/types.ts +73 -0
- package/eject/tab-item/index.ts +7 -0
- package/eject/tab-item/tab-item.tsx +70 -0
- package/eject/theme-toggle/color-scheme-sync.ts +73 -0
- package/eject/theme-toggle/index.tsx +120 -0
- package/eject/toc/cx.ts +38 -0
- package/eject/toc/index.ts +11 -0
- package/eject/toc/mobile-toc.tsx +133 -0
- package/eject/toc/toc-title.ts +33 -0
- package/eject/toc/toc.tsx +119 -0
- package/eject/toc/types.ts +14 -0
- package/eject/toc/use-active-heading.ts +162 -0
- package/package.json +371 -11
- package/routes-src/404.tsx +49 -0
- package/routes-src/_chrome.tsx +470 -0
- package/routes-src/_context.ts +314 -0
- package/routes-src/_docs-helpers.ts +284 -0
- package/routes-src/_virtual.d.ts +15 -0
- package/routes-src/api-ai-chat.tsx +46 -0
- package/routes-src/docs-slug.tsx +43 -0
- package/routes-src/docs-tags-index.tsx +14 -0
- package/routes-src/docs-tags-tag.tsx +29 -0
- package/routes-src/docs-versions.tsx +15 -0
- package/routes-src/index.tsx +134 -0
- package/routes-src/locale-docs-slug.tsx +69 -0
- package/routes-src/locale-docs-tags-index.tsx +23 -0
- package/routes-src/locale-docs-tags-tag.tsx +39 -0
- package/routes-src/locale-docs-versions.tsx +23 -0
- package/routes-src/locale-index.tsx +150 -0
- package/routes-src/robots.txt.tsx +13 -0
- package/routes-src/sitemap.xml.tsx +48 -0
- package/routes-src/v-docs-slug.tsx +64 -0
- package/routes-src/v-locale-docs-slug.tsx +85 -0
- package/dist/toc/smart-break.d.ts +0 -27
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
function makeUrlHelpers(settings, i18n) {
|
|
2
|
+
const { defaultLocale, locales, getLocaleLabel } = i18n;
|
|
3
|
+
const normalizedBase = settings.base.replace(/\/+$/, "");
|
|
4
|
+
function applyTrailingSlash(url) {
|
|
5
|
+
if (!settings.trailingSlash) return url;
|
|
6
|
+
if (url.endsWith("/")) return url;
|
|
7
|
+
const suffixIdx = url.search(/[?#]/);
|
|
8
|
+
const pathPart = suffixIdx >= 0 ? url.slice(0, suffixIdx) : url;
|
|
9
|
+
const suffix = suffixIdx >= 0 ? url.slice(suffixIdx) : "";
|
|
10
|
+
if (pathPart.endsWith("/")) return url;
|
|
11
|
+
const lastSegment = pathPart.split("/").pop() ?? "";
|
|
12
|
+
if (/\.[a-zA-Z]\w*$/.test(lastSegment)) return url;
|
|
13
|
+
return pathPart + "/" + suffix;
|
|
14
|
+
}
|
|
15
|
+
function withBase(path) {
|
|
16
|
+
const raw = normalizedBase === "" ? path : `${normalizedBase}${path.startsWith("/") ? path : `/${path}`}`;
|
|
17
|
+
return applyTrailingSlash(raw);
|
|
18
|
+
}
|
|
19
|
+
function stripBase(path) {
|
|
20
|
+
if (normalizedBase === "") return path;
|
|
21
|
+
if (path === normalizedBase) return "/";
|
|
22
|
+
return path.startsWith(`${normalizedBase}/`) ? path.slice(normalizedBase.length) : path;
|
|
23
|
+
}
|
|
24
|
+
function absoluteUrl(pageUrl) {
|
|
25
|
+
return settings.siteUrl ? settings.siteUrl.replace(/\/$/, "") + pageUrl : void 0;
|
|
26
|
+
}
|
|
27
|
+
function docsUrl(slug, lang = defaultLocale) {
|
|
28
|
+
const path = lang === defaultLocale ? `/docs/${slug}` : `/${lang}/docs/${slug}`;
|
|
29
|
+
return withBase(path);
|
|
30
|
+
}
|
|
31
|
+
function isExternal(href) {
|
|
32
|
+
return href.startsWith("http://") || href.startsWith("https://");
|
|
33
|
+
}
|
|
34
|
+
function resolveHref(href) {
|
|
35
|
+
return isExternal(href) ? href : withBase(href);
|
|
36
|
+
}
|
|
37
|
+
function navHref(path, lang, currentVersion) {
|
|
38
|
+
const isNonDefaultLocale = lang != null && lang !== defaultLocale;
|
|
39
|
+
const versionPrefix = currentVersion ? `/v/${currentVersion}` : "";
|
|
40
|
+
return withBase(
|
|
41
|
+
isNonDefaultLocale ? `${versionPrefix}/${lang}${path}` : `${versionPrefix}${path}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
function splitVersionPrefix(path) {
|
|
45
|
+
const m = path.match(/^(\/v\/[^/]+)(\/.*|$)/);
|
|
46
|
+
return m ? { versionPrefix: m[1] ?? "", rest: m[2] ?? "/" } : { versionPrefix: "", rest: path };
|
|
47
|
+
}
|
|
48
|
+
function getPathForLocale(path, currentLang, targetLang) {
|
|
49
|
+
const { versionPrefix, rest } = splitVersionPrefix(stripBase(path));
|
|
50
|
+
let relativePath = rest;
|
|
51
|
+
if (currentLang !== defaultLocale) {
|
|
52
|
+
relativePath = relativePath.replace(new RegExp(`^/${currentLang}(?:/|$)`), "/");
|
|
53
|
+
}
|
|
54
|
+
if (targetLang !== defaultLocale) {
|
|
55
|
+
relativePath = `/${targetLang}${relativePath}`;
|
|
56
|
+
}
|
|
57
|
+
return withBase(`${versionPrefix}${relativePath}`);
|
|
58
|
+
}
|
|
59
|
+
function isDefaultLocaleOnlyPath(path) {
|
|
60
|
+
const stripped = stripBase(path);
|
|
61
|
+
const normalized = stripped.endsWith("/") ? stripped : `${stripped}/`;
|
|
62
|
+
return settings.defaultLocaleOnlyPrefixes.some((prefix) => normalized.startsWith(prefix));
|
|
63
|
+
}
|
|
64
|
+
function buildLocaleLinks(currentPath, currentLang) {
|
|
65
|
+
let defaultLocalePath = splitVersionPrefix(stripBase(currentPath)).rest;
|
|
66
|
+
if (currentLang !== defaultLocale) {
|
|
67
|
+
defaultLocalePath = defaultLocalePath.replace(new RegExp(`^/${currentLang}(?:/|$)`), "/");
|
|
68
|
+
}
|
|
69
|
+
if (isDefaultLocaleOnlyPath(defaultLocalePath)) {
|
|
70
|
+
return [{
|
|
71
|
+
code: currentLang,
|
|
72
|
+
label: getLocaleLabel(currentLang),
|
|
73
|
+
href: getPathForLocale(currentPath, currentLang, currentLang),
|
|
74
|
+
active: true
|
|
75
|
+
}];
|
|
76
|
+
}
|
|
77
|
+
return locales.map((code) => ({
|
|
78
|
+
code,
|
|
79
|
+
label: getLocaleLabel(code),
|
|
80
|
+
href: getPathForLocale(currentPath, currentLang, code),
|
|
81
|
+
active: code === currentLang
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
function versionedDocsUrl(slug, versionSlug, lang = defaultLocale) {
|
|
85
|
+
const path = lang === defaultLocale ? `/v/${versionSlug}/docs/${slug}` : `/v/${versionSlug}/${lang}/docs/${slug}`;
|
|
86
|
+
return withBase(path);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
normalizedBase,
|
|
90
|
+
applyTrailingSlash,
|
|
91
|
+
withBase,
|
|
92
|
+
stripBase,
|
|
93
|
+
absoluteUrl,
|
|
94
|
+
docsUrl,
|
|
95
|
+
isExternal,
|
|
96
|
+
resolveHref,
|
|
97
|
+
navHref,
|
|
98
|
+
getPathForLocale,
|
|
99
|
+
buildLocaleLinks,
|
|
100
|
+
isDefaultLocaleOnlyPath,
|
|
101
|
+
versionedDocsUrl
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export {
|
|
105
|
+
makeUrlHelpers
|
|
106
|
+
};
|
|
@@ -1 +1,39 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Public entry for `@takazudo/zudo-doc/url-normalizer`.
|
|
3
|
+
*
|
|
4
|
+
* Build-time URL normalizer that ensures every emitted route URL ends with
|
|
5
|
+
* a trailing slash, matching the `trailingSlash: "always"` policy of the
|
|
6
|
+
* legacy Astro build.
|
|
7
|
+
*
|
|
8
|
+
* ## Why build-time only?
|
|
9
|
+
*
|
|
10
|
+
* zfb is a purely static site builder — there is no server or dev middleware.
|
|
11
|
+
* The Astro `src/middleware-handler.ts` ran at request time; the equivalent
|
|
12
|
+
* guarantee is now split:
|
|
13
|
+
*
|
|
14
|
+
* - **Build time** (this module): `buildUrl()` constructs URLs that always
|
|
15
|
+
* end with `/`, and `normalizePathname()` corrects any bare pathname passed
|
|
16
|
+
* in from external data.
|
|
17
|
+
* - **Runtime**: the deployment host (e.g. Cloudflare Pages `_redirects`) is
|
|
18
|
+
* responsible for redirecting requests that arrive without a trailing slash.
|
|
19
|
+
*
|
|
20
|
+
* See the "Trailing-Slash Policy" concept doc in the showcase docs for the
|
|
21
|
+
* full design rationale and an example `_redirects` rule.
|
|
22
|
+
*
|
|
23
|
+
* ## Usage
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* import {
|
|
27
|
+
* buildUrl,
|
|
28
|
+
* normalizePathname,
|
|
29
|
+
* shouldSkipNormalization,
|
|
30
|
+
* } from "@takazudo/zudo-doc/url-normalizer";
|
|
31
|
+
*
|
|
32
|
+
* // In a paths() implementation:
|
|
33
|
+
* const url = buildUrl("docs", slug); // "/docs/my-slug/"
|
|
34
|
+
*
|
|
35
|
+
* // In an integration that receives raw pathnames:
|
|
36
|
+
* const canonical = normalizePathname(rawPathname);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export { buildUrl, normalizePathname, shouldSkipNormalization } from "./normalize.js";
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
* buildUrl() // "/"
|
|
59
59
|
* buildUrl("", "docs", "") // "/docs/"
|
|
60
60
|
*/
|
|
61
|
-
declare function buildUrl(...segments: string[]): string;
|
|
61
|
+
export declare function buildUrl(...segments: string[]): string;
|
|
62
62
|
/**
|
|
63
63
|
* Return `true` when `pathname` should NOT receive a trailing slash.
|
|
64
64
|
*
|
|
@@ -78,7 +78,7 @@ declare function buildUrl(...segments: string[]): string;
|
|
|
78
78
|
* shouldSkipNormalization("/docs/guide") // false (needs trailing slash)
|
|
79
79
|
* shouldSkipNormalization("/docs/v2.0") // false (number after dot → not an extension)
|
|
80
80
|
*/
|
|
81
|
-
declare function shouldSkipNormalization(pathname: string): boolean;
|
|
81
|
+
export declare function shouldSkipNormalization(pathname: string): boolean;
|
|
82
82
|
/**
|
|
83
83
|
* Return `pathname` with a trailing slash appended if needed.
|
|
84
84
|
*
|
|
@@ -99,6 +99,4 @@ declare function shouldSkipNormalization(pathname: string): boolean;
|
|
|
99
99
|
* normalizePathname("/docs/guide/") // "/docs/guide/" (no-op)
|
|
100
100
|
* normalizePathname("/assets/app.js") // "/assets/app.js" (skipped)
|
|
101
101
|
*/
|
|
102
|
-
declare function normalizePathname(pathname: string): string;
|
|
103
|
-
|
|
104
|
-
export { buildUrl, normalizePathname, shouldSkipNormalization };
|
|
102
|
+
export declare function normalizePathname(pathname: string): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
interface UseModalDialogOptions {
|
|
2
|
+
/** True when the dialog should be open. */
|
|
3
|
+
isOpen: boolean;
|
|
4
|
+
/** Called when the dialog is closed for any reason (Escape, backdrop, navigation). */
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
/**
|
|
7
|
+
* DOM event name that triggers a close-and-reset. Pass the host's SPA
|
|
8
|
+
* after-navigate event name (e.g. `AFTER_NAVIGATE_EVENT`); pass `undefined`
|
|
9
|
+
* to skip navigation-close wiring entirely.
|
|
10
|
+
*/
|
|
11
|
+
navigateEvent?: string;
|
|
12
|
+
/**
|
|
13
|
+
* When `true`, enable backdrop-click-to-close. The caller must forward the
|
|
14
|
+
* click handler returned by this hook to the `<dialog>` element's `onClick`.
|
|
15
|
+
* Defaults to `false` so opt-in only.
|
|
16
|
+
*/
|
|
17
|
+
backdropClickClose?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* When `true`, enable focus management:
|
|
20
|
+
* - On open: capture `document.activeElement` as the return target (unless
|
|
21
|
+
* `returnFocusRef` is provided), then move focus to the first focusable
|
|
22
|
+
* element inside the dialog (or the dialog itself as a fallback).
|
|
23
|
+
* - On close: restore focus to the captured element.
|
|
24
|
+
*
|
|
25
|
+
* When a caller already manages focus internally (e.g. ai-chat-modal focuses
|
|
26
|
+
* its input in its own effect), pass `restoreFocusOnly: true` instead to get
|
|
27
|
+
* trigger-capture + restore without the "move into dialog" step.
|
|
28
|
+
*/
|
|
29
|
+
manageFocus?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Like `manageFocus` but skips the "focus first element in dialog" step —
|
|
32
|
+
* use when the caller has its own focus-on-open logic and only needs
|
|
33
|
+
* trigger-capture + focus restore on close.
|
|
34
|
+
*/
|
|
35
|
+
restoreFocusOnly?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Optional ref holding the element to return focus to on close. When
|
|
38
|
+
* provided and non-null, this takes precedence over the internally captured
|
|
39
|
+
* `document.activeElement`. Useful when the trigger element is conditionally
|
|
40
|
+
* rendered and therefore removed from the DOM before the open effect fires
|
|
41
|
+
* (e.g. the doc-history trigger button, which is unmounted when `isOpen`
|
|
42
|
+
* is true). The caller should set `returnFocusRef.current` to the trigger
|
|
43
|
+
* element in its click handler, before calling `setState`.
|
|
44
|
+
*/
|
|
45
|
+
returnFocusRef?: React.RefObject<HTMLElement | null>;
|
|
46
|
+
}
|
|
47
|
+
interface UseModalDialogResult {
|
|
48
|
+
dialogRef: React.RefObject<HTMLDialogElement>;
|
|
49
|
+
/**
|
|
50
|
+
* Pass this to `<dialog onClick={handleBackdropClick}>`.
|
|
51
|
+
* Only wired when `backdropClickClose` is true; otherwise a no-op.
|
|
52
|
+
*/
|
|
53
|
+
handleBackdropClick: (e: React.MouseEvent<HTMLDialogElement>) => void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Shared hook for `<dialog>`-based modal components.
|
|
57
|
+
*
|
|
58
|
+
* Usage:
|
|
59
|
+
* ```tsx
|
|
60
|
+
* const { dialogRef, handleBackdropClick } = useModalDialog({
|
|
61
|
+
* isOpen,
|
|
62
|
+
* onClose: handleClose,
|
|
63
|
+
* navigateEvent: AFTER_NAVIGATE_EVENT,
|
|
64
|
+
* backdropClickClose: true,
|
|
65
|
+
* });
|
|
66
|
+
* return <dialog ref={dialogRef} onClick={handleBackdropClick} ... />;
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare function useModalDialog({ isOpen, onClose, navigateEvent, backdropClickClose, manageFocus, restoreFocusOnly, returnFocusRef, }: UseModalDialogOptions): UseModalDialogResult;
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef } from "preact/compat";
|
|
3
|
+
function useModalDialog({
|
|
4
|
+
isOpen,
|
|
5
|
+
onClose,
|
|
6
|
+
navigateEvent,
|
|
7
|
+
backdropClickClose = false,
|
|
8
|
+
manageFocus = false,
|
|
9
|
+
restoreFocusOnly = false,
|
|
10
|
+
returnFocusRef
|
|
11
|
+
}) {
|
|
12
|
+
const dialogRef = useRef(null);
|
|
13
|
+
const internalTriggerRef = useRef(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const dialog = dialogRef.current;
|
|
16
|
+
if (!dialog) return;
|
|
17
|
+
if (isOpen && !dialog.open) {
|
|
18
|
+
if ((manageFocus || restoreFocusOnly) && !returnFocusRef) {
|
|
19
|
+
internalTriggerRef.current = document.activeElement;
|
|
20
|
+
}
|
|
21
|
+
dialog.showModal();
|
|
22
|
+
if (manageFocus && !restoreFocusOnly) {
|
|
23
|
+
const FOCUSABLE = 'button:not([disabled]),a[href],[tabindex]:not([tabindex="-1"]),input:not([disabled]),select:not([disabled]),textarea:not([disabled])';
|
|
24
|
+
const first = dialog.querySelector(FOCUSABLE);
|
|
25
|
+
if (first) {
|
|
26
|
+
first.focus();
|
|
27
|
+
} else {
|
|
28
|
+
dialog.focus();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} else if (!isOpen && dialog.open) {
|
|
32
|
+
dialog.close();
|
|
33
|
+
}
|
|
34
|
+
}, [isOpen, manageFocus, restoreFocusOnly, returnFocusRef]);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const dialog = dialogRef.current;
|
|
37
|
+
if (!dialog) return;
|
|
38
|
+
function onDialogClose() {
|
|
39
|
+
if (manageFocus || restoreFocusOnly) {
|
|
40
|
+
const trigger = returnFocusRef ? returnFocusRef.current : internalTriggerRef.current;
|
|
41
|
+
if (trigger instanceof HTMLElement) {
|
|
42
|
+
trigger.focus();
|
|
43
|
+
}
|
|
44
|
+
internalTriggerRef.current = null;
|
|
45
|
+
}
|
|
46
|
+
if (isOpen) onClose();
|
|
47
|
+
}
|
|
48
|
+
dialog.addEventListener("close", onDialogClose);
|
|
49
|
+
return () => dialog.removeEventListener("close", onDialogClose);
|
|
50
|
+
}, [isOpen, onClose, manageFocus, restoreFocusOnly, returnFocusRef]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!navigateEvent) return;
|
|
53
|
+
function handleNavigation() {
|
|
54
|
+
const dialog = dialogRef.current;
|
|
55
|
+
if (dialog?.open) {
|
|
56
|
+
dialog.close();
|
|
57
|
+
onClose();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
document.addEventListener(navigateEvent, handleNavigation);
|
|
61
|
+
return () => document.removeEventListener(navigateEvent, handleNavigation);
|
|
62
|
+
}, [navigateEvent, onClose]);
|
|
63
|
+
function handleBackdropClick(e) {
|
|
64
|
+
if (!backdropClickClose) return;
|
|
65
|
+
const dialog = dialogRef.current;
|
|
66
|
+
if (!dialog) return;
|
|
67
|
+
if (e.target === dialog) dialog.close();
|
|
68
|
+
}
|
|
69
|
+
return { dialogRef, handleBackdropClick };
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
useModalDialog
|
|
73
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** @jsxRuntime automatic */
|
|
2
|
+
/** @jsxImportSource preact */
|
|
3
|
+
import type { ComponentChildren, JSX } from "preact";
|
|
4
|
+
/** Minimal version config entry needed by the versions page. */
|
|
5
|
+
export interface VersionsPageVersionEntry {
|
|
6
|
+
slug: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
banner?: "unmaintained" | "unreleased" | false;
|
|
9
|
+
}
|
|
10
|
+
/** Settings subset read by the versions page factory. */
|
|
11
|
+
export interface VersionsPageSettings {
|
|
12
|
+
noindex?: boolean;
|
|
13
|
+
dynamicPageTransition?: boolean;
|
|
14
|
+
versions: VersionsPageVersionEntry[] | false;
|
|
15
|
+
base: string;
|
|
16
|
+
}
|
|
17
|
+
/** Host-supplied component bindings injected into the versions page factory. */
|
|
18
|
+
export interface VersionsPageComponents {
|
|
19
|
+
HeadWithDefaults: (props: {
|
|
20
|
+
title: string;
|
|
21
|
+
}) => JSX.Element;
|
|
22
|
+
HeaderWithDefaults: (props: {
|
|
23
|
+
lang?: string;
|
|
24
|
+
currentPath?: string;
|
|
25
|
+
}) => JSX.Element;
|
|
26
|
+
FooterWithDefaults: (props: {
|
|
27
|
+
lang?: string;
|
|
28
|
+
}) => JSX.Element;
|
|
29
|
+
BodyEndIslands: (props: {
|
|
30
|
+
basePath: string;
|
|
31
|
+
}) => JSX.Element;
|
|
32
|
+
}
|
|
33
|
+
/** Injected dependencies for {@link createVersionsPageView}. */
|
|
34
|
+
export interface VersionsPageDeps {
|
|
35
|
+
settings: VersionsPageSettings;
|
|
36
|
+
/** Default locale code (e.g. `"en"`). */
|
|
37
|
+
defaultLocale: string;
|
|
38
|
+
/** Translate a UI string key for a locale. */
|
|
39
|
+
t: (key: string, locale: string) => string;
|
|
40
|
+
/** Prefix a path with the configured base directory. */
|
|
41
|
+
withBase: (path: string) => string;
|
|
42
|
+
/** Compose the meta `<title>` value. */
|
|
43
|
+
composeMetaTitle: (title: string) => string;
|
|
44
|
+
components: VersionsPageComponents;
|
|
45
|
+
}
|
|
46
|
+
/** Props for the VersionsPageView component. */
|
|
47
|
+
export interface VersionsPageViewProps {
|
|
48
|
+
locale: string;
|
|
49
|
+
children?: ComponentChildren;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create a `VersionsPageView` component bound to the host's settings and
|
|
53
|
+
* component dependencies.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createVersionsPageView(deps: VersionsPageDeps): (props: VersionsPageViewProps) => JSX.Element;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Fragment, jsx } from "preact/jsx-runtime";
|
|
2
|
+
import { DocLayoutWithDefaults } from "../doclayout/index.js";
|
|
3
|
+
import { VersionsPageContent } from "../nav-indexing/index.js";
|
|
4
|
+
function createVersionsPageView(deps) {
|
|
5
|
+
const {
|
|
6
|
+
settings,
|
|
7
|
+
defaultLocale,
|
|
8
|
+
t,
|
|
9
|
+
withBase,
|
|
10
|
+
composeMetaTitle,
|
|
11
|
+
components: {
|
|
12
|
+
HeadWithDefaults,
|
|
13
|
+
HeaderWithDefaults,
|
|
14
|
+
FooterWithDefaults,
|
|
15
|
+
BodyEndIslands
|
|
16
|
+
}
|
|
17
|
+
} = deps;
|
|
18
|
+
function VersionsPageView({ locale }) {
|
|
19
|
+
const isDefault = locale === defaultLocale;
|
|
20
|
+
const prefix = isDefault ? "" : `/${locale}`;
|
|
21
|
+
const pageTitle = t("version.page.title", locale);
|
|
22
|
+
const labels = {
|
|
23
|
+
pageTitle,
|
|
24
|
+
latestTitle: t("version.page.latest.title", locale),
|
|
25
|
+
latestDescription: t("version.page.latest.description", locale),
|
|
26
|
+
latestLink: t("version.page.latest.link", locale),
|
|
27
|
+
pastTitle: t("version.page.past.title", locale),
|
|
28
|
+
pastDescription: t("version.page.past.description", locale),
|
|
29
|
+
unmaintained: t("version.page.unmaintained", locale),
|
|
30
|
+
unreleased: t("version.page.unreleased", locale),
|
|
31
|
+
versionCol: t("version.switcher.label", locale),
|
|
32
|
+
statusCol: t("version.page.status", locale),
|
|
33
|
+
docsCol: t("version.page.docs", locale)
|
|
34
|
+
};
|
|
35
|
+
const latestHref = withBase(`${prefix}/docs/getting-started`);
|
|
36
|
+
const versions = settings.versions ? settings.versions.map((v) => ({
|
|
37
|
+
slug: v.slug,
|
|
38
|
+
label: v.label ?? v.slug,
|
|
39
|
+
// Version prefix comes BEFORE the locale — the only routed shape is
|
|
40
|
+
// pages/v/[version]/{locale}/docs/...; /{locale}/v/... has no route.
|
|
41
|
+
docsHref: withBase(`/v/${v.slug}${prefix}/docs/getting-started/`),
|
|
42
|
+
banner: v.banner
|
|
43
|
+
})) : [];
|
|
44
|
+
return /* @__PURE__ */ jsx(
|
|
45
|
+
DocLayoutWithDefaults,
|
|
46
|
+
{
|
|
47
|
+
title: composeMetaTitle(pageTitle),
|
|
48
|
+
head: /* @__PURE__ */ jsx(HeadWithDefaults, { title: pageTitle }),
|
|
49
|
+
lang: locale,
|
|
50
|
+
noindex: settings.noindex,
|
|
51
|
+
hideSidebar: true,
|
|
52
|
+
hideToc: true,
|
|
53
|
+
sidebarOverride: /* @__PURE__ */ jsx(Fragment, {}),
|
|
54
|
+
headerOverride: /* @__PURE__ */ jsx(HeaderWithDefaults, { lang: locale, currentPath: withBase(`${prefix}/docs/versions`) }),
|
|
55
|
+
footerOverride: /* @__PURE__ */ jsx(FooterWithDefaults, { lang: locale }),
|
|
56
|
+
bodyEndComponents: /* @__PURE__ */ jsx(BodyEndIslands, { basePath: settings.base ?? "/" }),
|
|
57
|
+
enableClientRouter: settings.dynamicPageTransition,
|
|
58
|
+
children: /* @__PURE__ */ jsx(
|
|
59
|
+
VersionsPageContent,
|
|
60
|
+
{
|
|
61
|
+
latestHref,
|
|
62
|
+
versions,
|
|
63
|
+
labels
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return VersionsPageView;
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
createVersionsPageView
|
|
73
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/** @jsxRuntime automatic */
|
|
2
|
+
/** @jsxImportSource preact */
|
|
3
|
+
|
|
4
|
+
import type { ComponentChildren, VNode } from "preact";
|
|
5
|
+
import { findPath } from "./find-path.js";
|
|
6
|
+
import type { BreadcrumbItem, SidebarNode } from "./types.js";
|
|
7
|
+
import { ChevronRight } from "../icons/index.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert a sidebar tree + the current page's id into the ordered list
|
|
11
|
+
* of crumbs the renderer walks. The first crumb is the home rung
|
|
12
|
+
* (icon-only, label is the empty string), followed by each ancestor in
|
|
13
|
+
* the tree, ending with the current page (no `href` so the renderer
|
|
14
|
+
* paints it as plain text).
|
|
15
|
+
*
|
|
16
|
+
* Mirrors the legacy buildBreadcrumbs() behaviour from
|
|
17
|
+
* src/utils/docs.ts so the rendered output stays parity-equivalent
|
|
18
|
+
* with the original Astro breadcrumb component.
|
|
19
|
+
*/
|
|
20
|
+
export function buildBreadcrumbItems(
|
|
21
|
+
tree: SidebarNode[],
|
|
22
|
+
currentId: string,
|
|
23
|
+
homeHref: string,
|
|
24
|
+
): BreadcrumbItem[] {
|
|
25
|
+
const items: BreadcrumbItem[] = [{ label: "", href: homeHref }];
|
|
26
|
+
const path = findPath(tree, currentId);
|
|
27
|
+
for (let i = 0; i < path.length; i++) {
|
|
28
|
+
const node = path[i];
|
|
29
|
+
if (!node) continue;
|
|
30
|
+
const isLast = i === path.length - 1;
|
|
31
|
+
items.push({
|
|
32
|
+
label: node.label,
|
|
33
|
+
href: isLast ? undefined : node.href,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return items;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SmartLabelProps {
|
|
40
|
+
label: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Inline replacement for the Astro template's `set:html` /
|
|
45
|
+
* smartBreakToHtml call. Keeps the breadcrumb self-contained inside the
|
|
46
|
+
* v2 package — the legacy smart-break util lives in the host project's
|
|
47
|
+
* src/utils/, which v2 must not reach into.
|
|
48
|
+
*
|
|
49
|
+
* Inserts a Preact <wbr/> after each delimiter character when the label
|
|
50
|
+
* looks "path-like" (URLs, slash-separated paths, etc.). Prose labels
|
|
51
|
+
* pass through unchanged.
|
|
52
|
+
*/
|
|
53
|
+
function SmartLabel({ label }: SmartLabelProps): VNode | string {
|
|
54
|
+
if (!isPathLike(label)) return label;
|
|
55
|
+
const parts = label.split(DELIM_SPLIT);
|
|
56
|
+
const nodes: (string | VNode)[] = [];
|
|
57
|
+
for (let i = 0; i < parts.length; i++) {
|
|
58
|
+
const part = parts[i];
|
|
59
|
+
if (!part) continue;
|
|
60
|
+
nodes.push(part);
|
|
61
|
+
if (i % 2 === 1) nodes.push(<wbr key={`wbr-${i}`} />);
|
|
62
|
+
}
|
|
63
|
+
return <>{nodes}</>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const DELIM_SPLIT = /([/\\\-_.:?#&=])/;
|
|
67
|
+
|
|
68
|
+
/** Heuristic copied from src/utils/smart-break.tsx — kept inline so v2
|
|
69
|
+
* has no upward dependency. Returns true when delimiter-aware
|
|
70
|
+
* line-break hints are useful (paths, URLs), false for prose. */
|
|
71
|
+
function isPathLike(text: string): boolean {
|
|
72
|
+
if (!text) return false;
|
|
73
|
+
if (text.includes("://")) return true;
|
|
74
|
+
if (/^\.{0,2}\//.test(text)) return true;
|
|
75
|
+
if (/^[A-Za-z]:[\\/]/.test(text)) return true;
|
|
76
|
+
const forwardMatches = text.match(/[A-Za-z0-9]\/[A-Za-z0-9]/g);
|
|
77
|
+
if (forwardMatches && forwardMatches.length >= 2) return true;
|
|
78
|
+
const backMatches = text.match(/[A-Za-z0-9]\\[A-Za-z0-9]/g);
|
|
79
|
+
if (backMatches && backMatches.length >= 2) return true;
|
|
80
|
+
const hasDomainDot = /[A-Za-z0-9]\.[A-Za-z0-9]/.test(text);
|
|
81
|
+
const hasSlash = /[\\/]/.test(text);
|
|
82
|
+
if (hasDomainDot && hasSlash) return true;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function ChevronIcon(): VNode {
|
|
87
|
+
return <ChevronRight className="h-icon-xs w-icon-xs text-muted shrink-0" />;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function HomeIcon(): VNode {
|
|
91
|
+
return (
|
|
92
|
+
<svg
|
|
93
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
94
|
+
class="h-[1.575rem] w-[1.575rem] shrink-0"
|
|
95
|
+
fill="none"
|
|
96
|
+
viewBox="0 0 24 24"
|
|
97
|
+
stroke="currentColor"
|
|
98
|
+
stroke-width="2"
|
|
99
|
+
aria-hidden="true"
|
|
100
|
+
>
|
|
101
|
+
<path
|
|
102
|
+
stroke-linecap="round"
|
|
103
|
+
stroke-linejoin="round"
|
|
104
|
+
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0a1 1 0 01-1-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 01-1 1h-2z"
|
|
105
|
+
/>
|
|
106
|
+
</svg>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface BreadcrumbProps {
|
|
111
|
+
/** Pre-built items, or omit and pass `tree` + `currentId` instead. */
|
|
112
|
+
items?: BreadcrumbItem[];
|
|
113
|
+
/** Sidebar tree to derive the trail from. Required when `items` is omitted. */
|
|
114
|
+
tree?: SidebarNode[];
|
|
115
|
+
/** Id of the current page (matches SidebarNode.id). Required when `items` is omitted. */
|
|
116
|
+
currentId?: string;
|
|
117
|
+
/** Href for the leading home rung. Defaults to "/". */
|
|
118
|
+
homeHref?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Optional slot rendered at the right end of the breadcrumb row.
|
|
121
|
+
* When provided, the breadcrumb nav and this slot are wrapped in a
|
|
122
|
+
* flex row that matches the original Astro doc-layout's combined
|
|
123
|
+
* breadcrumb + version-switcher row:
|
|
124
|
+
*
|
|
125
|
+
* class="mb-vsp-sm flex flex-col items-start gap-vsp-xs
|
|
126
|
+
* sm:flex-row sm:items-center sm:justify-between [&_nav]:mb-0"
|
|
127
|
+
*
|
|
128
|
+
* The `[&_nav]:mb-0` rule strips the nav's own `mb-vsp-md` so the
|
|
129
|
+
* wrapper margin controls spacing instead. Use this to place the
|
|
130
|
+
* VersionSwitcher pill inline at the right of the breadcrumb row,
|
|
131
|
+
* matching the reference site's layout on category index pages.
|
|
132
|
+
*/
|
|
133
|
+
rightSlot?: ComponentChildren;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Breadcrumb trail component.
|
|
138
|
+
*
|
|
139
|
+
* Two usage shapes:
|
|
140
|
+
* 1. Pass pre-built `items` (existing call-sites that already
|
|
141
|
+
* compute crumbs upstream).
|
|
142
|
+
* 2. Pass `tree` + `currentId` and the component derives the trail.
|
|
143
|
+
*
|
|
144
|
+
* Returns null when no items resolve, matching the documented
|
|
145
|
+
* `items.length > 0 &&` guard.
|
|
146
|
+
*/
|
|
147
|
+
export function Breadcrumb(props: BreadcrumbProps): VNode | null {
|
|
148
|
+
const items =
|
|
149
|
+
props.items ??
|
|
150
|
+
(props.tree && props.currentId !== undefined
|
|
151
|
+
? buildBreadcrumbItems(props.tree, props.currentId, props.homeHref ?? "/")
|
|
152
|
+
: []);
|
|
153
|
+
|
|
154
|
+
if (items.length === 0) return null;
|
|
155
|
+
|
|
156
|
+
const nav = (
|
|
157
|
+
<nav class="mb-vsp-md text-small" aria-label="Breadcrumb">
|
|
158
|
+
<ol class="flex flex-wrap items-center gap-x-hsp-xs">
|
|
159
|
+
{items.map((item, i) => (
|
|
160
|
+
<li key={`crumb-${i}`} class="flex items-center gap-x-hsp-xs">
|
|
161
|
+
{i > 0 && <ChevronIcon />}
|
|
162
|
+
{item.href ? (
|
|
163
|
+
<a
|
|
164
|
+
href={item.href}
|
|
165
|
+
class="text-muted underline hover:text-fg flex items-center gap-x-hsp-2xs"
|
|
166
|
+
>
|
|
167
|
+
{i === 0 && <HomeIcon />}
|
|
168
|
+
<SmartLabel label={item.label} />
|
|
169
|
+
</a>
|
|
170
|
+
) : (
|
|
171
|
+
<span class="text-fg">
|
|
172
|
+
<SmartLabel label={item.label} />
|
|
173
|
+
</span>
|
|
174
|
+
)}
|
|
175
|
+
</li>
|
|
176
|
+
))}
|
|
177
|
+
</ol>
|
|
178
|
+
</nav>
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (props.rightSlot === undefined) return nav;
|
|
182
|
+
|
|
183
|
+
// When a rightSlot is provided, wrap both elements in a flex row that
|
|
184
|
+
// matches the original Astro doc-layout's breadcrumb + version-switcher
|
|
185
|
+
// row. The `[&_nav]:mb-0` rule cancels the nav's own bottom margin so
|
|
186
|
+
// the wrapper margin controls spacing instead (matching the Astro
|
|
187
|
+
// reference which used `mb-vsp-sm` on the outer div).
|
|
188
|
+
return (
|
|
189
|
+
<div class="mb-vsp-sm flex flex-col items-start gap-vsp-xs sm:flex-row sm:items-center sm:justify-between [&_nav]:mb-0">
|
|
190
|
+
{nav}
|
|
191
|
+
{props.rightSlot}
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BreadcrumbNode, SidebarNode } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Walk the sidebar tree and return the chain of nodes from the root
|
|
5
|
+
* level down to (and including) the node whose `id` matches `targetId`.
|
|
6
|
+
*
|
|
7
|
+
* Returns an empty array when no node matches. Pure: never mutates the
|
|
8
|
+
* input tree, never reads anything outside of it.
|
|
9
|
+
*
|
|
10
|
+
* Used by the breadcrumb component to derive the current page's
|
|
11
|
+
* ancestry. The home/root crumb is the renderer's responsibility — this
|
|
12
|
+
* helper deals strictly with the tree itself.
|
|
13
|
+
*/
|
|
14
|
+
export function findPath<T extends BreadcrumbNode = SidebarNode>(
|
|
15
|
+
nodes: readonly T[],
|
|
16
|
+
targetId: string,
|
|
17
|
+
): T[] {
|
|
18
|
+
for (const node of nodes) {
|
|
19
|
+
if (node.id === targetId) return [node];
|
|
20
|
+
if (node.children && node.children.length > 0) {
|
|
21
|
+
const sub = findPath(node.children as readonly T[], targetId);
|
|
22
|
+
if (sub.length > 0) return [node, ...sub];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
26
|
+
}
|