@takazudo/zudo-doc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +18 -0
- package/dist/body-foot-util/body-foot-util-area.d.ts +55 -0
- package/dist/body-foot-util/body-foot-util-area.js +71 -0
- package/dist/body-foot-util/edit-link.d.ts +45 -0
- package/dist/body-foot-util/edit-link.js +42 -0
- package/dist/body-foot-util/index.d.ts +3 -0
- package/dist/body-foot-util/index.js +11 -0
- package/dist/breadcrumb/breadcrumb.d.ts +57 -0
- package/dist/breadcrumb/breadcrumb.js +113 -0
- package/dist/breadcrumb/find-path.d.ts +16 -0
- package/dist/breadcrumb/find-path.js +13 -0
- package/dist/breadcrumb/index.d.ts +4 -0
- package/dist/breadcrumb/index.js +7 -0
- package/dist/breadcrumb/types.d.ts +32 -0
- package/dist/breadcrumb/types.js +0 -0
- package/dist/code-syntax/code-block-enhancer-script.d.ts +3 -0
- package/dist/code-syntax/code-block-enhancer-script.js +165 -0
- package/dist/code-syntax/code-block-enhancer.d.ts +22 -0
- package/dist/code-syntax/code-block-enhancer.js +13 -0
- package/dist/code-syntax/index.d.ts +8 -0
- package/dist/code-syntax/index.js +29 -0
- package/dist/code-syntax/mermaid-init-script.d.ts +51 -0
- package/dist/code-syntax/mermaid-init-script.js +225 -0
- package/dist/code-syntax/mermaid-init.d.ts +48 -0
- package/dist/code-syntax/mermaid-init.js +14 -0
- package/dist/code-syntax/tabs-init-script.d.ts +3 -0
- package/dist/code-syntax/tabs-init-script.js +106 -0
- package/dist/code-syntax/tabs-init.d.ts +24 -0
- package/dist/code-syntax/tabs-init.js +10 -0
- package/dist/code-syntax/tabs.d.ts +36 -0
- package/dist/code-syntax/tabs.js +62 -0
- package/dist/content/component-map.d.ts +51 -0
- package/dist/content/component-map.js +29 -0
- package/dist/content/content-blockquote.d.ts +11 -0
- package/dist/content/content-blockquote.js +14 -0
- package/dist/content/content-code.d.ts +26 -0
- package/dist/content/content-code.js +39 -0
- package/dist/content/content-link.d.ts +12 -0
- package/dist/content/content-link.js +43 -0
- package/dist/content/content-ol.d.ts +11 -0
- package/dist/content/content-ol.js +15 -0
- package/dist/content/content-paragraph.d.ts +10 -0
- package/dist/content/content-paragraph.js +7 -0
- package/dist/content/content-strong.d.ts +11 -0
- package/dist/content/content-strong.js +14 -0
- package/dist/content/content-table.d.ts +11 -0
- package/dist/content/content-table.js +14 -0
- package/dist/content/content-ul.d.ts +11 -0
- package/dist/content/content-ul.js +15 -0
- package/dist/content/heading-h2.d.ts +14 -0
- package/dist/content/heading-h2.js +18 -0
- package/dist/content/heading-h3.d.ts +14 -0
- package/dist/content/heading-h3.js +18 -0
- package/dist/content/heading-h4.d.ts +14 -0
- package/dist/content/heading-h4.js +18 -0
- package/dist/content/index.d.ts +13 -0
- package/dist/content/index.js +27 -0
- package/dist/details/details.d.ts +26 -0
- package/dist/details/details.js +10 -0
- package/dist/details/index.d.ts +2 -0
- package/dist/details/index.js +4 -0
- package/dist/doclayout/anchors.d.ts +55 -0
- package/dist/doclayout/anchors.js +42 -0
- package/dist/doclayout/doc-layout-with-defaults.d.ts +83 -0
- package/dist/doclayout/doc-layout-with-defaults.js +159 -0
- package/dist/doclayout/doc-layout.d.ts +121 -0
- package/dist/doclayout/doc-layout.js +89 -0
- package/dist/doclayout/index.d.ts +6 -0
- package/dist/doclayout/index.js +17 -0
- package/dist/footer/footer.d.ts +42 -0
- package/dist/footer/footer.js +79 -0
- package/dist/footer/index.d.ts +3 -0
- package/dist/footer/index.js +4 -0
- package/dist/footer/types.d.ts +58 -0
- package/dist/footer/types.js +0 -0
- package/dist/head/doc-head.d.ts +33 -0
- package/dist/head/doc-head.js +90 -0
- package/dist/head/index.d.ts +5 -0
- package/dist/head/index.js +8 -0
- package/dist/head/og-tags.d.ts +25 -0
- package/dist/head/og-tags.js +16 -0
- package/dist/head/twitter-card.d.ts +21 -0
- package/dist/head/twitter-card.js +14 -0
- package/dist/head/types.d.ts +83 -0
- package/dist/head/types.js +0 -0
- package/dist/header/header.d.ts +158 -0
- package/dist/header/header.js +367 -0
- package/dist/header/index.d.ts +6 -0
- package/dist/header/index.js +18 -0
- package/dist/header/nav-active.d.ts +39 -0
- package/dist/header/nav-active.js +24 -0
- package/dist/header/nav-overflow-script.d.ts +3 -0
- package/dist/header/nav-overflow-script.js +169 -0
- package/dist/header/right-items.d.ts +38 -0
- package/dist/header/right-items.js +30 -0
- package/dist/header/types.d.ts +40 -0
- package/dist/header/types.js +0 -0
- package/dist/html-preview-wrapper/dedent.d.ts +10 -0
- package/dist/html-preview-wrapper/dedent.js +16 -0
- package/dist/html-preview-wrapper/highlighted-code.d.ts +18 -0
- package/dist/html-preview-wrapper/highlighted-code.js +53 -0
- package/dist/html-preview-wrapper/html-preview-wrapper.d.ts +60 -0
- package/dist/html-preview-wrapper/html-preview-wrapper.js +36 -0
- package/dist/html-preview-wrapper/html-preview.d.ts +35 -0
- package/dist/html-preview-wrapper/html-preview.js +75 -0
- package/dist/html-preview-wrapper/index.d.ts +5 -0
- package/dist/html-preview-wrapper/index.js +12 -0
- package/dist/html-preview-wrapper/preflight.d.ts +13 -0
- package/dist/html-preview-wrapper/preflight.js +108 -0
- package/dist/html-preview-wrapper/preview-base.d.ts +29 -0
- package/dist/html-preview-wrapper/preview-base.js +127 -0
- package/dist/i18n-version/index.d.ts +5 -0
- package/dist/i18n-version/index.js +14 -0
- package/dist/i18n-version/language-switcher.d.ts +24 -0
- package/dist/i18n-version/language-switcher.js +25 -0
- package/dist/i18n-version/types.d.ts +43 -0
- package/dist/i18n-version/types.js +0 -0
- package/dist/i18n-version/version-banner.d.ts +30 -0
- package/dist/i18n-version/version-banner.js +21 -0
- package/dist/i18n-version/version-switcher.d.ts +112 -0
- package/dist/i18n-version/version-switcher.js +157 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +0 -0
- package/dist/integrations/claude-resources/escape-for-mdx.d.ts +8 -0
- package/dist/integrations/claude-resources/escape-for-mdx.js +131 -0
- package/dist/integrations/claude-resources/generate.d.ts +13 -0
- package/dist/integrations/claude-resources/generate.js +404 -0
- package/dist/integrations/claude-resources/index.d.ts +46 -0
- package/dist/integrations/claude-resources/index.js +24 -0
- package/dist/integrations/doc-history/index.d.ts +137 -0
- package/dist/integrations/doc-history/index.js +138 -0
- package/dist/integrations/doc-history/pre-build.d.ts +58 -0
- package/dist/integrations/doc-history/pre-build.js +67 -0
- package/dist/integrations/llms-txt/dev-middleware.d.ts +40 -0
- package/dist/integrations/llms-txt/dev-middleware.js +76 -0
- package/dist/integrations/llms-txt/emit.d.ts +22 -0
- package/dist/integrations/llms-txt/emit.js +55 -0
- package/dist/integrations/llms-txt/generate.d.ts +26 -0
- package/dist/integrations/llms-txt/generate.js +36 -0
- package/dist/integrations/llms-txt/index.d.ts +7 -0
- package/dist/integrations/llms-txt/index.js +27 -0
- package/dist/integrations/llms-txt/load.d.ts +51 -0
- package/dist/integrations/llms-txt/load.js +83 -0
- package/dist/integrations/llms-txt/strip.d.ts +22 -0
- package/dist/integrations/llms-txt/strip.js +10 -0
- package/dist/integrations/llms-txt/types.d.ts +145 -0
- package/dist/integrations/llms-txt/types.js +0 -0
- package/dist/integrations/search-index/build-emitter.d.ts +30 -0
- package/dist/integrations/search-index/build-emitter.js +15 -0
- package/dist/integrations/search-index/collect.d.ts +12 -0
- package/dist/integrations/search-index/collect.js +48 -0
- package/dist/integrations/search-index/content-files.d.ts +32 -0
- package/dist/integrations/search-index/content-files.js +50 -0
- package/dist/integrations/search-index/dev-middleware.d.ts +16 -0
- package/dist/integrations/search-index/dev-middleware.js +30 -0
- package/dist/integrations/search-index/index.d.ts +5 -0
- package/dist/integrations/search-index/index.js +14 -0
- package/dist/integrations/search-index/types.d.ts +38 -0
- package/dist/integrations/search-index/types.js +6 -0
- package/dist/metainfo/doc-metainfo.d.ts +50 -0
- package/dist/metainfo/doc-metainfo.js +107 -0
- package/dist/metainfo/doc-tags.d.ts +66 -0
- package/dist/metainfo/doc-tags.js +53 -0
- package/dist/metainfo/frontmatter-preview.d.ts +95 -0
- package/dist/metainfo/frontmatter-preview.js +55 -0
- package/dist/metainfo/index.d.ts +4 -0
- package/dist/metainfo/index.js +28 -0
- package/dist/nav-indexing/category-nav.d.ts +28 -0
- package/dist/nav-indexing/category-nav.js +41 -0
- package/dist/nav-indexing/category-tree-nav.d.ts +27 -0
- package/dist/nav-indexing/category-tree-nav.js +49 -0
- package/dist/nav-indexing/doc-card-grid.d.ts +38 -0
- package/dist/nav-indexing/doc-card-grid.js +40 -0
- package/dist/nav-indexing/docs-sitemap.d.ts +25 -0
- package/dist/nav-indexing/docs-sitemap.js +40 -0
- package/dist/nav-indexing/index.d.ts +10 -0
- package/dist/nav-indexing/index.js +18 -0
- package/dist/nav-indexing/nav-card-grid.d.ts +31 -0
- package/dist/nav-indexing/nav-card-grid.js +43 -0
- package/dist/nav-indexing/site-tree-nav-demo.d.ts +39 -0
- package/dist/nav-indexing/site-tree-nav-demo.js +65 -0
- package/dist/nav-indexing/tag-nav.d.ts +38 -0
- package/dist/nav-indexing/tag-nav.js +96 -0
- package/dist/nav-indexing/types.d.ts +95 -0
- package/dist/nav-indexing/types.js +0 -0
- package/dist/nav-indexing/versions-page-content.d.ts +29 -0
- package/dist/nav-indexing/versions-page-content.js +76 -0
- package/dist/page-loading/index.d.ts +2 -0
- package/dist/page-loading/index.js +10 -0
- package/dist/page-loading/page-loading-overlay.d.ts +35 -0
- package/dist/page-loading/page-loading-overlay.js +50 -0
- package/dist/sidebar/index.d.ts +3 -0
- package/dist/sidebar/index.js +4 -0
- package/dist/sidebar/sidebar.d.ts +54 -0
- package/dist/sidebar/sidebar.js +35 -0
- package/dist/sidebar/types.d.ts +71 -0
- package/dist/sidebar/types.js +0 -0
- package/dist/sidebar-resizer/index.d.ts +14 -0
- package/dist/sidebar-resizer/index.js +165 -0
- package/dist/sidebar-resizer/sidebar-resizer-init.d.ts +20 -0
- package/dist/sidebar-resizer/sidebar-resizer-init.js +119 -0
- package/dist/sidebar-tree/build-tree.d.ts +43 -0
- package/dist/sidebar-tree/build-tree.js +127 -0
- package/dist/sidebar-tree/category-meta.d.ts +19 -0
- package/dist/sidebar-tree/category-meta.js +63 -0
- package/dist/sidebar-tree/index.d.ts +3 -0
- package/dist/sidebar-tree/index.js +13 -0
- package/dist/sidebar-tree/types.d.ts +120 -0
- package/dist/sidebar-tree/types.js +0 -0
- package/dist/tab-item/index.d.ts +2 -0
- package/dist/tab-item/index.js +5 -0
- package/dist/tab-item/tab-item.d.ts +38 -0
- package/dist/tab-item/tab-item.js +25 -0
- package/dist/theme/color-scheme-provider.d.ts +32 -0
- package/dist/theme/color-scheme-provider.js +43 -0
- package/dist/theme/color-tweak-export-modal.d.ts +20 -0
- package/dist/theme/color-tweak-export-modal.js +138 -0
- package/dist/theme/design-token-serde.d.ts +136 -0
- package/dist/theme/design-token-serde.js +254 -0
- package/dist/theme/design-token-tweak-panel.d.ts +11 -0
- package/dist/theme/design-token-tweak-panel.js +10 -0
- package/dist/theme/design-token-types.d.ts +19 -0
- package/dist/theme/design-token-types.js +6 -0
- package/dist/theme/iframe-bridge.d.ts +44 -0
- package/dist/theme/iframe-bridge.js +70 -0
- package/dist/theme/index.d.ts +9 -0
- package/dist/theme/index.js +42 -0
- package/dist/theme/theme-toggle.d.ts +17 -0
- package/dist/theme/theme-toggle.js +91 -0
- package/dist/toc/cx.d.ts +16 -0
- package/dist/toc/cx.js +22 -0
- package/dist/toc/index.d.ts +5 -0
- package/dist/toc/index.js +9 -0
- package/dist/toc/mobile-toc.d.ts +46 -0
- package/dist/toc/mobile-toc.js +88 -0
- package/dist/toc/smart-break.d.ts +27 -0
- package/dist/toc/smart-break.js +36 -0
- package/dist/toc/toc-title.d.ts +10 -0
- package/dist/toc/toc-title.js +14 -0
- package/dist/toc/toc.d.ts +60 -0
- package/dist/toc/toc.js +56 -0
- package/dist/toc/types.d.ts +23 -0
- package/dist/toc/types.js +0 -0
- package/dist/toc/use-active-heading.d.ts +30 -0
- package/dist/toc/use-active-heading.js +106 -0
- package/dist/transitions/index.d.ts +1 -0
- package/dist/transitions/index.js +12 -0
- package/dist/transitions/page-events.d.ts +41 -0
- package/dist/transitions/page-events.js +20 -0
- package/dist/url-normalizer/index.d.ts +1 -0
- package/dist/url-normalizer/index.js +6 -0
- package/dist/url-normalizer/normalize.d.ts +104 -0
- package/dist/url-normalizer/normalize.js +22 -0
- package/package.json +186 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "preact/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from "preact/hooks";
|
|
4
|
+
import { Island } from "@takazudo/zfb";
|
|
5
|
+
const STORAGE_KEY = "zudo-doc-theme";
|
|
6
|
+
function SunIcon() {
|
|
7
|
+
return /* @__PURE__ */ jsxs(
|
|
8
|
+
"svg",
|
|
9
|
+
{
|
|
10
|
+
"aria-hidden": "true",
|
|
11
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
12
|
+
width: "20",
|
|
13
|
+
height: "20",
|
|
14
|
+
viewBox: "0 0 24 24",
|
|
15
|
+
fill: "none",
|
|
16
|
+
stroke: "currentColor",
|
|
17
|
+
strokeWidth: "2",
|
|
18
|
+
strokeLinecap: "round",
|
|
19
|
+
strokeLinejoin: "round",
|
|
20
|
+
children: [
|
|
21
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "5" }),
|
|
22
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "1", x2: "12", y2: "3" }),
|
|
23
|
+
/* @__PURE__ */ jsx("line", { x1: "12", y1: "21", x2: "12", y2: "23" }),
|
|
24
|
+
/* @__PURE__ */ jsx("line", { x1: "4.22", y1: "4.22", x2: "5.64", y2: "5.64" }),
|
|
25
|
+
/* @__PURE__ */ jsx("line", { x1: "18.36", y1: "18.36", x2: "19.78", y2: "19.78" }),
|
|
26
|
+
/* @__PURE__ */ jsx("line", { x1: "1", y1: "12", x2: "3", y2: "12" }),
|
|
27
|
+
/* @__PURE__ */ jsx("line", { x1: "21", y1: "12", x2: "23", y2: "12" }),
|
|
28
|
+
/* @__PURE__ */ jsx("line", { x1: "4.22", y1: "19.78", x2: "5.64", y2: "18.36" }),
|
|
29
|
+
/* @__PURE__ */ jsx("line", { x1: "18.36", y1: "5.64", x2: "19.78", y2: "4.22" })
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
function MoonIcon() {
|
|
35
|
+
return /* @__PURE__ */ jsx(
|
|
36
|
+
"svg",
|
|
37
|
+
{
|
|
38
|
+
"aria-hidden": "true",
|
|
39
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
40
|
+
width: "20",
|
|
41
|
+
height: "20",
|
|
42
|
+
viewBox: "0 0 24 24",
|
|
43
|
+
fill: "none",
|
|
44
|
+
stroke: "currentColor",
|
|
45
|
+
strokeWidth: "2",
|
|
46
|
+
strokeLinecap: "round",
|
|
47
|
+
strokeLinejoin: "round",
|
|
48
|
+
children: /* @__PURE__ */ jsx("path", { d: "M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" })
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
function ThemeToggleInner({ defaultMode = "dark" }) {
|
|
53
|
+
const [mode, setMode] = useState(defaultMode);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const actual = document.documentElement.getAttribute("data-theme") || defaultMode;
|
|
56
|
+
if (actual !== mode) {
|
|
57
|
+
setMode(actual);
|
|
58
|
+
}
|
|
59
|
+
}, []);
|
|
60
|
+
function toggle() {
|
|
61
|
+
const next = mode === "dark" ? "light" : "dark";
|
|
62
|
+
setMode(next);
|
|
63
|
+
document.documentElement.setAttribute("data-theme", next);
|
|
64
|
+
document.documentElement.style.colorScheme = next;
|
|
65
|
+
localStorage.setItem(STORAGE_KEY, next);
|
|
66
|
+
localStorage.removeItem("zudo-doc-tweak-state");
|
|
67
|
+
localStorage.removeItem("zudo-doc-tweak-state-v2");
|
|
68
|
+
window.dispatchEvent(new CustomEvent("color-scheme-changed"));
|
|
69
|
+
}
|
|
70
|
+
const nextMode = mode === "dark" ? "light" : "dark";
|
|
71
|
+
return /* @__PURE__ */ jsx(
|
|
72
|
+
"button",
|
|
73
|
+
{
|
|
74
|
+
onClick: toggle,
|
|
75
|
+
"aria-label": `Switch to ${nextMode} mode`,
|
|
76
|
+
className: "text-muted hover:text-fg transition-colors p-hsp-sm focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2",
|
|
77
|
+
children: mode === "dark" ? /* @__PURE__ */ jsx(SunIcon, {}) : /* @__PURE__ */ jsx(MoonIcon, {})
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
ThemeToggleInner.displayName = "ThemeToggle";
|
|
82
|
+
function ThemeToggle(props = {}) {
|
|
83
|
+
const rendered = Island({
|
|
84
|
+
when: "load",
|
|
85
|
+
children: /* @__PURE__ */ jsx(ThemeToggleInner, { ...props })
|
|
86
|
+
});
|
|
87
|
+
return rendered;
|
|
88
|
+
}
|
|
89
|
+
export {
|
|
90
|
+
ThemeToggle as default
|
|
91
|
+
};
|
package/dist/toc/cx.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny `clsx`-style class name joiner. Accepts strings, falsy values,
|
|
3
|
+
* and arrays/objects of the same — only truthy strings survive.
|
|
4
|
+
*
|
|
5
|
+
* The host project depends on `clsx`, but framework primitives keep
|
|
6
|
+
* their dependency surface minimal so that a downstream consumer of
|
|
7
|
+
* `@takazudo/zudo-doc/toc` does not need an extra runtime dep just
|
|
8
|
+
* for class name composition. The behavior here is the subset the TOC
|
|
9
|
+
* primitives use; see clsx for the full feature set.
|
|
10
|
+
*/
|
|
11
|
+
type ClassValue = string | number | bigint | boolean | null | undefined | ClassValue[] | {
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
};
|
|
14
|
+
declare function cx(...args: ClassValue[]): string;
|
|
15
|
+
|
|
16
|
+
export { cx };
|
package/dist/toc/cx.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function cx(...args) {
|
|
2
|
+
const out = [];
|
|
3
|
+
for (const arg of args) {
|
|
4
|
+
if (!arg) continue;
|
|
5
|
+
if (typeof arg === "string") {
|
|
6
|
+
out.push(arg);
|
|
7
|
+
} else if (typeof arg === "number" || typeof arg === "bigint") {
|
|
8
|
+
out.push(String(arg));
|
|
9
|
+
} else if (Array.isArray(arg)) {
|
|
10
|
+
const inner = cx(...arg);
|
|
11
|
+
if (inner) out.push(inner);
|
|
12
|
+
} else if (typeof arg === "object") {
|
|
13
|
+
for (const key of Object.keys(arg)) {
|
|
14
|
+
if (arg[key]) out.push(key);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return out.join(" ");
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
cx
|
|
22
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { Toc, TocProps } from './toc.js';
|
|
2
|
+
export { MobileToc, MobileTocProps } from './mobile-toc.js';
|
|
3
|
+
export { UseActiveHeadingResult, getActiveHeadingId, useActiveHeading } from './use-active-heading.js';
|
|
4
|
+
export { HeadingItem, TocItem } from './types.js';
|
|
5
|
+
import 'preact';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { VNode } from 'preact';
|
|
2
|
+
import { HeadingItem } from './types.js';
|
|
3
|
+
|
|
4
|
+
/** @jsxRuntime automatic */
|
|
5
|
+
/** @jsxImportSource preact */
|
|
6
|
+
|
|
7
|
+
interface MobileTocProps {
|
|
8
|
+
headings: readonly HeadingItem[];
|
|
9
|
+
title?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Collapsible TOC for narrow viewports (`xl:hidden`) — a Preact island
|
|
13
|
+
* component. Closed by default; tapping the header toggles, and tapping
|
|
14
|
+
* any entry closes the panel after navigation.
|
|
15
|
+
*
|
|
16
|
+
* Renders the `<div class="xl:hidden …">` panel directly. **The caller
|
|
17
|
+
* is responsible for wrapping this in `<Island when="load">`** so the
|
|
18
|
+
* SSG output emits the `data-zfb-island="MobileToc"` hydration marker
|
|
19
|
+
* around the panel. `<DocLayoutWithDefaults>` does this for you;
|
|
20
|
+
* consumers who render `<MobileToc>` outside the default layout (e.g.
|
|
21
|
+
* via the `mobileTocOverride` prop or in a custom layout) must apply
|
|
22
|
+
* the wrapper themselves — otherwise the open/close toggle never
|
|
23
|
+
* hydrates on the client and the panel stays in its initial closed
|
|
24
|
+
* state.
|
|
25
|
+
*
|
|
26
|
+
* Wave 13 (zudolab/zudo-doc#1355): previously this module exported a
|
|
27
|
+
* `MobileToc` wrapper that called `Island(...)` itself; on hydration
|
|
28
|
+
* the runtime ran `hydrate(<MobileToc/>, dataIslandDiv)` and the
|
|
29
|
+
* wrapper re-emitted *another* `<div data-zfb-island="MobileToc">…</div>`
|
|
30
|
+
* inside the existing one, leaving two `xl:hidden …` panels in the
|
|
31
|
+
* post-hydration DOM. See `./toc.tsx` for the full diagnosis. Moving
|
|
32
|
+
* the `<Island>` wrapper to the call site lets the bundle hydrate the
|
|
33
|
+
* bare panel against the existing DOM in-place.
|
|
34
|
+
*
|
|
35
|
+
* Like `Toc`, this always includes the `title` text in the SSG HTML
|
|
36
|
+
* even when no headings qualify. When no headings are present, a
|
|
37
|
+
* CSS-hidden container carries the title string so no-JS users and
|
|
38
|
+
* crawlers can still find the locale label in the static markup. When
|
|
39
|
+
* headings are present the full interactive toggle UI is rendered.
|
|
40
|
+
*/
|
|
41
|
+
declare function MobileToc({ headings, title, }: MobileTocProps): VNode;
|
|
42
|
+
declare namespace MobileToc {
|
|
43
|
+
var displayName: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { MobileToc, type MobileTocProps };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "preact/jsx-runtime";
|
|
3
|
+
import { useMemo, useState } from "preact/hooks";
|
|
4
|
+
import { SmartBreak } from "./smart-break.js";
|
|
5
|
+
import { cx } from "./cx.js";
|
|
6
|
+
function MobileToc({
|
|
7
|
+
headings,
|
|
8
|
+
title = "On this page"
|
|
9
|
+
}) {
|
|
10
|
+
const filtered = useMemo(
|
|
11
|
+
() => headings.filter((h) => h.depth >= 2 && h.depth <= 4),
|
|
12
|
+
[headings]
|
|
13
|
+
);
|
|
14
|
+
const [open, setOpen] = useState(false);
|
|
15
|
+
if (filtered.length === 0) {
|
|
16
|
+
return /* @__PURE__ */ jsx("div", { className: "hidden", "aria-hidden": "true", children: title });
|
|
17
|
+
}
|
|
18
|
+
return /* @__PURE__ */ jsxs("div", { className: "xl:hidden border border-muted mb-vsp-lg", children: [
|
|
19
|
+
/* @__PURE__ */ jsxs(
|
|
20
|
+
"button",
|
|
21
|
+
{
|
|
22
|
+
type: "button",
|
|
23
|
+
onClick: () => setOpen((prev) => !prev),
|
|
24
|
+
"aria-expanded": open,
|
|
25
|
+
className: "flex w-full items-center justify-between px-hsp-lg py-vsp-xs text-small font-medium text-fg",
|
|
26
|
+
children: [
|
|
27
|
+
/* @__PURE__ */ jsx("span", { children: title }),
|
|
28
|
+
/* @__PURE__ */ jsx(
|
|
29
|
+
"svg",
|
|
30
|
+
{
|
|
31
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
32
|
+
"aria-hidden": "true",
|
|
33
|
+
className: cx(
|
|
34
|
+
"h-icon-sm w-icon-sm text-muted transition-transform duration-150",
|
|
35
|
+
open && "rotate-180"
|
|
36
|
+
),
|
|
37
|
+
fill: "none",
|
|
38
|
+
viewBox: "0 0 24 24",
|
|
39
|
+
stroke: "currentColor",
|
|
40
|
+
strokeWidth: 2,
|
|
41
|
+
children: /* @__PURE__ */ jsx(
|
|
42
|
+
"path",
|
|
43
|
+
{
|
|
44
|
+
strokeLinecap: "round",
|
|
45
|
+
strokeLinejoin: "round",
|
|
46
|
+
d: "M19 9l-7 7-7-7"
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
),
|
|
54
|
+
/* @__PURE__ */ jsx(
|
|
55
|
+
"ul",
|
|
56
|
+
{
|
|
57
|
+
className: cx(
|
|
58
|
+
"border-t border-muted px-hsp-lg py-vsp-xs space-y-vsp-2xs",
|
|
59
|
+
!open && "hidden"
|
|
60
|
+
),
|
|
61
|
+
"aria-hidden": !open,
|
|
62
|
+
children: filtered.map((heading, index) => /* @__PURE__ */ jsx(
|
|
63
|
+
"li",
|
|
64
|
+
{
|
|
65
|
+
className: cx(
|
|
66
|
+
heading.depth === 3 && "ml-hsp-lg",
|
|
67
|
+
heading.depth === 4 && "ml-hsp-2xl"
|
|
68
|
+
),
|
|
69
|
+
children: /* @__PURE__ */ jsx(
|
|
70
|
+
"a",
|
|
71
|
+
{
|
|
72
|
+
href: `#${heading.slug}`,
|
|
73
|
+
onClick: () => setOpen(false),
|
|
74
|
+
className: "block py-vsp-2xs text-small text-muted hover:text-fg hover:underline focus-visible:underline",
|
|
75
|
+
children: /* @__PURE__ */ jsx(SmartBreak, { children: heading.text })
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
},
|
|
79
|
+
`${heading.slug}-${index}`
|
|
80
|
+
))
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
] });
|
|
84
|
+
}
|
|
85
|
+
MobileToc.displayName = "MobileToc";
|
|
86
|
+
export {
|
|
87
|
+
MobileToc
|
|
88
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { VNode } from 'preact';
|
|
2
|
+
|
|
3
|
+
/** @jsxRuntime automatic */
|
|
4
|
+
/** @jsxImportSource preact */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Local copy of the SmartBreak helper used by TOC entries. Kept inside
|
|
8
|
+
* the toc primitive folder so the package is self-contained and does
|
|
9
|
+
* not reach back into the host application's `src/utils/smart-break`.
|
|
10
|
+
*
|
|
11
|
+
* The host's full smart-break util exposes additional helpers
|
|
12
|
+
* (`smartBreakToHtml`, `escapeAndInjectWbr`) that the TOC does not
|
|
13
|
+
* need; this file ports only `isPathLike`, `smartBreak`, and the
|
|
14
|
+
* `SmartBreak` Preact wrapper. Keep the heuristic byte-identical to
|
|
15
|
+
* the host implementation so wbr placement matches everywhere.
|
|
16
|
+
*/
|
|
17
|
+
declare function isPathLike(text: string): boolean;
|
|
18
|
+
declare function smartBreak(text: string): VNode | string;
|
|
19
|
+
/**
|
|
20
|
+
* Preact function component wrapper — pure, server-renderable.
|
|
21
|
+
* Stringifies children and defers to smartBreak.
|
|
22
|
+
*/
|
|
23
|
+
declare function SmartBreak({ children }: {
|
|
24
|
+
children?: unknown;
|
|
25
|
+
}): any;
|
|
26
|
+
|
|
27
|
+
export { SmartBreak, isPathLike, smartBreak };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Fragment, jsx } from "preact/jsx-runtime";
|
|
2
|
+
function isPathLike(text) {
|
|
3
|
+
if (!text) return false;
|
|
4
|
+
if (text.includes("://")) return true;
|
|
5
|
+
if (/^\.{0,2}\//.test(text)) return true;
|
|
6
|
+
if (/^[A-Za-z]:[\\/]/.test(text)) return true;
|
|
7
|
+
const forwardMatches = text.match(/[A-Za-z0-9]\/[A-Za-z0-9]/g);
|
|
8
|
+
if (forwardMatches && forwardMatches.length >= 2) return true;
|
|
9
|
+
const backMatches = text.match(/[A-Za-z0-9]\\[A-Za-z0-9]/g);
|
|
10
|
+
if (backMatches && backMatches.length >= 2) return true;
|
|
11
|
+
const hasDomainDot = /[A-Za-z0-9]\.[A-Za-z0-9]/.test(text);
|
|
12
|
+
const hasSlash = /[\\/]/.test(text);
|
|
13
|
+
if (hasDomainDot && hasSlash) return true;
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const DELIM_SPLIT = /([/\\\-_.:?#&=])/;
|
|
17
|
+
function smartBreak(text) {
|
|
18
|
+
if (!isPathLike(text)) return text;
|
|
19
|
+
const parts = text.split(DELIM_SPLIT);
|
|
20
|
+
const nodes = [];
|
|
21
|
+
for (let i = 0; i < parts.length; i++) {
|
|
22
|
+
const part = parts[i];
|
|
23
|
+
if (part === "") continue;
|
|
24
|
+
nodes.push(part);
|
|
25
|
+
if (i % 2 === 1) nodes.push(/* @__PURE__ */ jsx("wbr", {}, `wbr-${i}`));
|
|
26
|
+
}
|
|
27
|
+
return /* @__PURE__ */ jsx(Fragment, { children: nodes });
|
|
28
|
+
}
|
|
29
|
+
function SmartBreak({ children }) {
|
|
30
|
+
return /* @__PURE__ */ jsx(Fragment, { children: smartBreak(String(children ?? "")) });
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
SmartBreak,
|
|
34
|
+
isPathLike,
|
|
35
|
+
smartBreak
|
|
36
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the TOC section label for the given BCP-47 language tag.
|
|
3
|
+
*
|
|
4
|
+
* Accepts full tags ("en-US", "ja-JP") by checking the primary subtag
|
|
5
|
+
* first, then falls back to the full tag, then falls back to the English
|
|
6
|
+
* default so callers always receive a non-empty string.
|
|
7
|
+
*/
|
|
8
|
+
declare function getTocTitle(lang?: string): string;
|
|
9
|
+
|
|
10
|
+
export { getTocTitle };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const TOC_TITLES = {
|
|
2
|
+
en: "On this page",
|
|
3
|
+
ja: "\u76EE\u6B21",
|
|
4
|
+
de: "Auf dieser Seite"
|
|
5
|
+
};
|
|
6
|
+
const DEFAULT_TOC_TITLE = "On this page";
|
|
7
|
+
function getTocTitle(lang) {
|
|
8
|
+
if (!lang) return DEFAULT_TOC_TITLE;
|
|
9
|
+
const primary = lang.split("-")[0];
|
|
10
|
+
return TOC_TITLES[primary] ?? TOC_TITLES[lang] ?? DEFAULT_TOC_TITLE;
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
getTocTitle
|
|
14
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { VNode } from 'preact';
|
|
2
|
+
import { HeadingItem } from './types.js';
|
|
3
|
+
|
|
4
|
+
/** @jsxRuntime automatic */
|
|
5
|
+
/** @jsxImportSource preact */
|
|
6
|
+
|
|
7
|
+
interface TocProps {
|
|
8
|
+
headings: readonly HeadingItem[];
|
|
9
|
+
/**
|
|
10
|
+
* Section label used by MobileToc. The desktop Toc no longer renders
|
|
11
|
+
* a visible heading (issue #1655 / T1) but the prop is kept so callers
|
|
12
|
+
* can share a single title value for both desktop and mobile surfaces.
|
|
13
|
+
* Defaults to "On this page" (English).
|
|
14
|
+
*/
|
|
15
|
+
title?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Desktop right-rail Table of Contents — a Preact island component.
|
|
19
|
+
*
|
|
20
|
+
* Renders the `<nav aria-label="Table of contents">` directly. **The
|
|
21
|
+
* caller is responsible for wrapping this in `<Island when="load">`**
|
|
22
|
+
* so the SSG output emits the `data-zfb-island="Toc"` hydration marker
|
|
23
|
+
* around the nav. `<DocLayoutWithDefaults>` does this for you; consumers
|
|
24
|
+
* who render `<Toc>` outside the default layout (e.g. via the
|
|
25
|
+
* `tocOverride` prop or in a custom layout) must apply the wrapper
|
|
26
|
+
* themselves — otherwise the active-section scroll-spy never hydrates
|
|
27
|
+
* on the client and the nav stays static.
|
|
28
|
+
*
|
|
29
|
+
* Wave 13 ("smoke-toc duplicate-nav regression", zudolab/zudo-doc#1355):
|
|
30
|
+
* before this refactor, this module exported a `Toc` wrapper that itself
|
|
31
|
+
* called `Island(...)` internally and an inner `TocInner` component that
|
|
32
|
+
* carried `displayName = "Toc"`. The zfb island scanner picked the
|
|
33
|
+
* exported `Toc` (the wrapper) as the hydration target, so on the
|
|
34
|
+
* client `mountIslands` ran `hydrate(<Toc/>, dataIslandDiv)` where the
|
|
35
|
+
* vnode itself rendered to *another* `<div data-zfb-island="Toc">…<nav>…</nav></div>`
|
|
36
|
+
* — Preact appended the new wrapper-div as a child of the existing
|
|
37
|
+
* data-zfb-island element instead of in-place hydrating the inner nav,
|
|
38
|
+
* leaving two `<nav aria-label="Table of contents">` elements in the
|
|
39
|
+
* post-hydration DOM. The host's Wave 12 prop-serialisation pin
|
|
40
|
+
* (`data-props`) made hydration succeed where it previously crashed
|
|
41
|
+
* silently on the missing `headings` prop, which is why the duplicate
|
|
42
|
+
* only became visible in CI smoke runs after Wave 12. Moving the
|
|
43
|
+
* `<Island>` wrapper to the call site lets the bundle hydrate the bare
|
|
44
|
+
* `<nav>` against the existing DOM in-place, eliminating the duplicate.
|
|
45
|
+
*
|
|
46
|
+
* Renders only depth 2–4 headings (h2/h3/h4), matching zudo-doc's
|
|
47
|
+
* historical behavior — the page title (h1) is rendered separately by
|
|
48
|
+
* the layout and h5+ are deemed too granular for the TOC.
|
|
49
|
+
*
|
|
50
|
+
* The `title` prop is kept on the signature (MobileToc and callers still
|
|
51
|
+
* use it) but the desktop Toc no longer renders a visible heading — the
|
|
52
|
+
* `<nav aria-label="Table of contents">` landmark provides the section
|
|
53
|
+
* semantics for screen readers. Issue #1655 / T1.
|
|
54
|
+
*/
|
|
55
|
+
declare function Toc({ headings, title }: TocProps): VNode;
|
|
56
|
+
declare namespace Toc {
|
|
57
|
+
var displayName: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { Toc, type TocProps };
|
package/dist/toc/toc.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "preact/jsx-runtime";
|
|
3
|
+
import { useMemo } from "preact/hooks";
|
|
4
|
+
import { useActiveHeading } from "./use-active-heading.js";
|
|
5
|
+
import { SmartBreak } from "./smart-break.js";
|
|
6
|
+
import { cx } from "./cx.js";
|
|
7
|
+
function Toc({ headings, title = "On this page" }) {
|
|
8
|
+
const filtered = useMemo(
|
|
9
|
+
() => headings.filter((h) => h.depth >= 2 && h.depth <= 4),
|
|
10
|
+
[headings]
|
|
11
|
+
);
|
|
12
|
+
const { activeId, activate } = useActiveHeading(filtered);
|
|
13
|
+
return /* @__PURE__ */ jsx(
|
|
14
|
+
"nav",
|
|
15
|
+
{
|
|
16
|
+
"aria-label": "Table of contents",
|
|
17
|
+
className: cx(
|
|
18
|
+
"hidden xl:flex flex-col",
|
|
19
|
+
"w-[280px] shrink-0",
|
|
20
|
+
"sticky top-[3.5rem] self-start z-10",
|
|
21
|
+
"pt-vsp-xl lg:pt-vsp-2xl",
|
|
22
|
+
"h-[calc(100vh-3.5rem)]"
|
|
23
|
+
),
|
|
24
|
+
children: filtered.length > 0 && /* @__PURE__ */ jsx("ul", { className: "border-l border-muted pl-hsp-lg overflow-y-auto", children: filtered.map((heading, index) => {
|
|
25
|
+
const isActive = heading.slug === activeId;
|
|
26
|
+
return /* @__PURE__ */ jsx(
|
|
27
|
+
"li",
|
|
28
|
+
{
|
|
29
|
+
className: cx(
|
|
30
|
+
heading.depth === 3 && "ml-hsp-lg",
|
|
31
|
+
heading.depth === 4 && "ml-hsp-2xl"
|
|
32
|
+
),
|
|
33
|
+
children: /* @__PURE__ */ jsx(
|
|
34
|
+
"a",
|
|
35
|
+
{
|
|
36
|
+
href: `#${heading.slug}`,
|
|
37
|
+
onClick: () => activate(heading.slug),
|
|
38
|
+
"aria-current": isActive ? "true" : void 0,
|
|
39
|
+
className: cx(
|
|
40
|
+
"block py-vsp-2xs text-small leading-snug transition-colors",
|
|
41
|
+
isActive ? "bg-fg text-bg font-medium" : "text-muted hover:underline focus:underline"
|
|
42
|
+
),
|
|
43
|
+
children: /* @__PURE__ */ jsx(SmartBreak, { children: heading.text })
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
},
|
|
47
|
+
`${heading.slug}-${index}`
|
|
48
|
+
);
|
|
49
|
+
}) })
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
Toc.displayName = "Toc";
|
|
54
|
+
export {
|
|
55
|
+
Toc
|
|
56
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heading metadata consumed by the TOC primitives.
|
|
3
|
+
*
|
|
4
|
+
* Shape mirrors the `headings` array exported from MDX modules by the
|
|
5
|
+
* underlying engine — `depth` (1–6), DOM `slug` (matching the rendered
|
|
6
|
+
* heading element's id), and the rendered `text`. Keep field names and
|
|
7
|
+
* types byte-aligned with the engine so a page module's `headings`
|
|
8
|
+
* export drops in directly.
|
|
9
|
+
*/
|
|
10
|
+
interface HeadingItem {
|
|
11
|
+
readonly depth: number;
|
|
12
|
+
readonly slug: string;
|
|
13
|
+
readonly text: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Alias kept for ergonomic imports — `TocItem` is the same shape the
|
|
17
|
+
* TOC components consume after filtering by depth. Filtering does not
|
|
18
|
+
* change the per-item shape, so the alias documents intent without
|
|
19
|
+
* introducing a structural difference.
|
|
20
|
+
*/
|
|
21
|
+
type TocItem = HeadingItem;
|
|
22
|
+
|
|
23
|
+
export type { HeadingItem, TocItem };
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { HeadingItem } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure helper that picks the active heading id given the heading slug
|
|
5
|
+
* order and a slug→element map. Extracted from the hook so it is unit-
|
|
6
|
+
* testable without DOM event plumbing.
|
|
7
|
+
*
|
|
8
|
+
* Algorithm: find the first heading whose top is at or below the
|
|
9
|
+
* SCROLL_MARGIN_TOP threshold ("first visible"). If none, the user has
|
|
10
|
+
* scrolled past every heading — return the last one. If the very first
|
|
11
|
+
* heading is the first visible AND its top is above the viewport
|
|
12
|
+
* midline, treat it as already active. Otherwise activate the
|
|
13
|
+
* predecessor of the first visible heading.
|
|
14
|
+
*/
|
|
15
|
+
declare function getActiveHeadingId(headingIds: readonly string[], elementMap: ReadonlyMap<string, HTMLElement>, viewportHeight: number): string | null;
|
|
16
|
+
interface UseActiveHeadingResult {
|
|
17
|
+
activeId: string | null;
|
|
18
|
+
activate: (id: string) => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Scroll-spy hook. Tracks which heading slug should be highlighted
|
|
22
|
+
* based on the current scroll position. Provides an `activate(id)`
|
|
23
|
+
* imperatively-callable helper used by click handlers — sets the
|
|
24
|
+
* active id immediately and suppresses the scroll-driven update until
|
|
25
|
+
* the smooth-scroll completes (`scrollend`, with a timeout fallback
|
|
26
|
+
* for older Safari).
|
|
27
|
+
*/
|
|
28
|
+
declare function useActiveHeading(headings: readonly HeadingItem[]): UseActiveHeadingResult;
|
|
29
|
+
|
|
30
|
+
export { type UseActiveHeadingResult, getActiveHeadingId, useActiveHeading };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
|
2
|
+
const SCROLL_MARGIN_TOP = 80;
|
|
3
|
+
const DEBOUNCE_MS = 200;
|
|
4
|
+
function getActiveHeadingId(headingIds, elementMap, viewportHeight) {
|
|
5
|
+
if (headingIds.length === 0) return null;
|
|
6
|
+
let firstVisibleIndex = -1;
|
|
7
|
+
for (let i = 0; i < headingIds.length; i++) {
|
|
8
|
+
const el2 = elementMap.get(headingIds[i]);
|
|
9
|
+
if (!el2) continue;
|
|
10
|
+
const { top } = el2.getBoundingClientRect();
|
|
11
|
+
if (top >= SCROLL_MARGIN_TOP) {
|
|
12
|
+
firstVisibleIndex = i;
|
|
13
|
+
break;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (firstVisibleIndex === -1) {
|
|
17
|
+
return headingIds[headingIds.length - 1] ?? null;
|
|
18
|
+
}
|
|
19
|
+
if (firstVisibleIndex === 0) {
|
|
20
|
+
const el2 = elementMap.get(headingIds[0]);
|
|
21
|
+
if (el2) {
|
|
22
|
+
const { top } = el2.getBoundingClientRect();
|
|
23
|
+
if (top < viewportHeight / 2) {
|
|
24
|
+
return headingIds[0] ?? null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const el = elementMap.get(headingIds[firstVisibleIndex]);
|
|
30
|
+
if (el) {
|
|
31
|
+
const { top } = el.getBoundingClientRect();
|
|
32
|
+
if (top < viewportHeight / 2) {
|
|
33
|
+
return headingIds[firstVisibleIndex] ?? null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return headingIds[firstVisibleIndex - 1] ?? null;
|
|
37
|
+
}
|
|
38
|
+
function useActiveHeading(headings) {
|
|
39
|
+
const [activeId, setActiveId] = useState(null);
|
|
40
|
+
const headingIdsRef = useRef([]);
|
|
41
|
+
const elementMapRef = useRef(/* @__PURE__ */ new Map());
|
|
42
|
+
const suppressedRef = useRef(false);
|
|
43
|
+
const activate = useCallback((id) => {
|
|
44
|
+
setActiveId(id);
|
|
45
|
+
suppressedRef.current = true;
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
suppressedRef.current = false;
|
|
48
|
+
}, 2e3);
|
|
49
|
+
}, []);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const ids = headings.map((h) => h.slug);
|
|
52
|
+
headingIdsRef.current = ids;
|
|
53
|
+
const map = /* @__PURE__ */ new Map();
|
|
54
|
+
for (const id of ids) {
|
|
55
|
+
const el = document.getElementById(id);
|
|
56
|
+
if (el) map.set(id, el);
|
|
57
|
+
}
|
|
58
|
+
elementMapRef.current = map;
|
|
59
|
+
let timerId = null;
|
|
60
|
+
function update() {
|
|
61
|
+
timerId = null;
|
|
62
|
+
setActiveId(
|
|
63
|
+
getActiveHeadingId(
|
|
64
|
+
headingIdsRef.current,
|
|
65
|
+
elementMapRef.current,
|
|
66
|
+
window.innerHeight
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
let fallbackTimerId = null;
|
|
71
|
+
function onScroll() {
|
|
72
|
+
if (suppressedRef.current) {
|
|
73
|
+
if (fallbackTimerId !== null) clearTimeout(fallbackTimerId);
|
|
74
|
+
fallbackTimerId = setTimeout(onScrollEnd, 1500);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (timerId !== null) clearTimeout(timerId);
|
|
78
|
+
timerId = setTimeout(update, DEBOUNCE_MS);
|
|
79
|
+
}
|
|
80
|
+
function onScrollEnd() {
|
|
81
|
+
suppressedRef.current = false;
|
|
82
|
+
if (fallbackTimerId !== null) {
|
|
83
|
+
clearTimeout(fallbackTimerId);
|
|
84
|
+
fallbackTimerId = null;
|
|
85
|
+
}
|
|
86
|
+
if (timerId !== null) clearTimeout(timerId);
|
|
87
|
+
update();
|
|
88
|
+
}
|
|
89
|
+
update();
|
|
90
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
91
|
+
window.addEventListener("resize", onScroll, { passive: true });
|
|
92
|
+
window.addEventListener("scrollend", onScrollEnd, { passive: true });
|
|
93
|
+
return () => {
|
|
94
|
+
window.removeEventListener("scroll", onScroll);
|
|
95
|
+
window.removeEventListener("resize", onScroll);
|
|
96
|
+
window.removeEventListener("scrollend", onScrollEnd);
|
|
97
|
+
if (timerId !== null) clearTimeout(timerId);
|
|
98
|
+
if (fallbackTimerId !== null) clearTimeout(fallbackTimerId);
|
|
99
|
+
};
|
|
100
|
+
}, [headings]);
|
|
101
|
+
return { activeId, activate };
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
getActiveHeadingId,
|
|
105
|
+
useActiveHeading
|
|
106
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AFTER_NAVIGATE_EVENT, BEFORE_NAVIGATE_EVENT, onAfterNavigate, onBeforeNavigate } from './page-events.js';
|