@takazudo/zudo-doc 2.4.1 → 2.5.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.
@@ -1,4 +1,6 @@
1
1
  export declare const SIDEBAR_STORAGE_KEY = "zudo-doc-sidebar-visible";
2
+ export declare function readState(): boolean;
3
+ export declare function setDataAttribute(isVisible: boolean): void;
2
4
  export declare function DesktopSidebarToggle(): import("preact").JSX.Element;
3
5
  export declare namespace DesktopSidebarToggle {
4
6
  var displayName: string;
@@ -58,5 +58,7 @@ function DesktopSidebarToggle() {
58
58
  DesktopSidebarToggle.displayName = "DesktopSidebarToggle";
59
59
  export {
60
60
  DesktopSidebarToggle,
61
- SIDEBAR_STORAGE_KEY
61
+ SIDEBAR_STORAGE_KEY,
62
+ readState,
63
+ setDataAttribute
62
64
  };
@@ -4,7 +4,10 @@ import { DocLayoutWithDefaults } from "../doclayout/index.js";
4
4
  import { Toc, MobileToc, getTocTitle } from "../toc/index.js";
5
5
  import { Breadcrumb } from "../breadcrumb/index.js";
6
6
  import { NavCardGrid } from "../nav-indexing/index.js";
7
- import { createSidebarPrepaint } from "../sidebar-prepaint/index.js";
7
+ import {
8
+ createSidebarPrepaint,
9
+ createSidebarVisibilityPrepaint
10
+ } from "../sidebar-prepaint/index.js";
8
11
  import { createHeadWithDefaults } from "../head-with-defaults/index.js";
9
12
  import { createSidebarWithDefaults } from "../sidebar-with-defaults/index.js";
10
13
  import { createHeaderWithDefaults } from "../header-with-defaults/index.js";
@@ -21,8 +24,14 @@ function createDocPageShell(ctx) {
21
24
  const SidebarWithDefaults = createSidebarWithDefaults(ctx);
22
25
  const HeaderWithDefaults = createHeaderWithDefaults(ctx);
23
26
  const FooterWithDefaults = createFooterWithDefaults(ctx);
27
+ const sidebarToggleEnabled = Boolean(
28
+ ctx.settings.sidebarToggle
29
+ );
24
30
  const SidebarPrepaint = createSidebarPrepaint({
25
- sidebarToggle: Boolean(ctx.settings.sidebarToggle)
31
+ sidebarToggle: sidebarToggleEnabled
32
+ });
33
+ const SidebarVisibilityPrepaint = createSidebarVisibilityPrepaint({
34
+ sidebarToggle: sidebarToggleEnabled
26
35
  });
27
36
  const DocBodyEnd = createDocBodyEnd(ctx);
28
37
  const DocPager = createDocPager(ctx);
@@ -69,7 +78,10 @@ function createDocPageShell(ctx) {
69
78
  {
70
79
  title: composeMetaTitle(title),
71
80
  description: settings.metaTags.description ? description : void 0,
72
- head: /* @__PURE__ */ jsx(HeadWithDefaults, { title, description, canonical }),
81
+ head: /* @__PURE__ */ jsxs(Fragment, { children: [
82
+ /* @__PURE__ */ jsx(HeadWithDefaults, { title, description, canonical }),
83
+ /* @__PURE__ */ jsx(SidebarVisibilityPrepaint, { hideSidebar })
84
+ ] }),
73
85
  lang: locale,
74
86
  noindex: settings.noindex,
75
87
  hideSidebar,
@@ -97,11 +97,14 @@ function downgradeRepoRelativeLinks(content) {
97
97
  function findClaudeMdFiles(dir, excludeDirs) {
98
98
  const results = [];
99
99
  if (!fs.existsSync(dir)) return results;
100
+ const excludes = excludeDirs.map(
101
+ (d) => d.endsWith(path.sep) ? d.slice(0, -path.sep.length) : d
102
+ );
100
103
  for (const item of fs.readdirSync(dir)) {
101
104
  if (item === "node_modules") continue;
102
105
  if (item.startsWith(".")) continue;
103
106
  const itemPath = path.join(dir, item);
104
- if (excludeDirs.some((d) => itemPath.startsWith(d))) continue;
107
+ if (excludes.some((d) => itemPath === d || itemPath.startsWith(d + path.sep))) continue;
105
108
  let stat;
106
109
  try {
107
110
  stat = fs.lstatSync(itemPath);
@@ -109,7 +112,7 @@ function findClaudeMdFiles(dir, excludeDirs) {
109
112
  continue;
110
113
  }
111
114
  if (stat.isDirectory()) {
112
- results.push(...findClaudeMdFiles(itemPath, excludeDirs));
115
+ results.push(...findClaudeMdFiles(itemPath, excludes));
113
116
  } else if (stat.isFile() && item === "CLAUDE.md") {
114
117
  results.push(itemPath);
115
118
  }
@@ -441,8 +444,6 @@ sidebar_position: 899
441
444
  generated: true
442
445
  ---
443
446
 
444
- Claude Code configuration reference.
445
-
446
447
  ## Resources
447
448
 
448
449
  <CategoryNav categories={${categoriesAttr}} />
@@ -12,10 +12,27 @@ export interface ClaudeResourcesPluginOptions {
12
12
  */
13
13
  claudeDir: string;
14
14
  /**
15
- * Project root used both as the search root for `CLAUDE.md` discovery
16
- * and to anchor relative paths. Defaults to `process.cwd()`.
15
+ * Anchor for resolving the relative `claudeDir`, `docsDir`, and `scanRoot`
16
+ * paths, and the default value of `scanRoot`. Defaults to `process.cwd()`.
17
+ *
18
+ * Note: this does NOT itself decide where `CLAUDE.md` discovery walks — that
19
+ * is `scanRoot` (which defaults to this). Set `scanRoot` to widen discovery
20
+ * (e.g. a subdirectory doc site scanning its repo root) without moving the
21
+ * output base, which stays anchored here.
17
22
  */
18
23
  projectRoot?: string;
24
+ /**
25
+ * Root for `CLAUDE.md` discovery and the base for the generated pages'
26
+ * relative-path titles/slugs. Defaults to `projectRoot`. Resolved against
27
+ * `projectRoot` when relative (absolute allowed).
28
+ *
29
+ * Scope: governs `CLAUDE.md` discovery ONLY. Commands, skills, and agents
30
+ * always come from `claudeDir` and are unaffected by `scanRoot`. Decoupling
31
+ * this from `projectRoot` lets a doc site in a repo subdirectory scan
32
+ * repo-wide `CLAUDE.md` files while still writing generated pages into its
33
+ * own content collection (see #2558).
34
+ */
35
+ scanRoot?: string;
19
36
  /**
20
37
  * Output directory for generated MDX pages, resolved against
21
38
  * `projectRoot` when relative. Defaults to `src/content/docs` to
@@ -12,9 +12,10 @@ function claudeResourcesPlugin(options) {
12
12
  function runClaudeResourcesPreStep(options) {
13
13
  const projectRoot = path.resolve(options.projectRoot ?? process.cwd());
14
14
  const claudeDir = path.isAbsolute(options.claudeDir) ? options.claudeDir : path.resolve(projectRoot, options.claudeDir);
15
+ const scanRoot = options.scanRoot === void 0 ? projectRoot : path.isAbsolute(options.scanRoot) ? options.scanRoot : path.resolve(projectRoot, options.scanRoot);
15
16
  const docsDirInput = options.docsDir ?? "src/content/docs";
16
17
  const docsDir = path.isAbsolute(docsDirInput) ? docsDirInput : path.resolve(projectRoot, docsDirInput);
17
- return generateClaudeResourcesDocs({ claudeDir, projectRoot, docsDir });
18
+ return generateClaudeResourcesDocs({ claudeDir, projectRoot: scanRoot, docsDir });
18
19
  }
19
20
  export {
20
21
  CLAUDE_RESOURCES_PLUGIN_NAME,
@@ -10,10 +10,12 @@ const plugin = {
10
10
  );
11
11
  }
12
12
  const projectRootOpt = ctx.options["projectRoot"];
13
+ const scanRootOpt = ctx.options["scanRoot"];
13
14
  const docsDirOpt = ctx.options["docsDir"];
14
15
  const result = await runClaudeResourcesPreStep({
15
16
  claudeDir,
16
17
  projectRoot: typeof projectRootOpt === "string" ? projectRootOpt : ctx.projectRoot,
18
+ scanRoot: typeof scanRootOpt === "string" ? scanRootOpt : void 0,
17
19
  docsDir: typeof docsDirOpt === "string" ? docsDirOpt : "src/content/docs"
18
20
  });
19
21
  ctx.logger.info(
package/dist/preset.d.ts CHANGED
@@ -50,6 +50,11 @@ export interface PresetVersionConfig {
50
50
  export interface PresetClaudeResourcesConfig {
51
51
  claudeDir: string;
52
52
  projectRoot?: string;
53
+ /**
54
+ * Root for `CLAUDE.md` discovery; defaults to `projectRoot`. Decouples
55
+ * repo-wide scanning from the output base for subdirectory doc sites (#2558).
56
+ */
57
+ scanRoot?: string;
53
58
  }
54
59
  /**
55
60
  * The subset of `settings` the preset reads. Any concrete `typeof settings`
package/dist/preset.js CHANGED
@@ -171,6 +171,7 @@ function buildPlugins(settings, routeContext) {
171
171
  options: {
172
172
  claudeDir: settings.claudeResources.claudeDir,
173
173
  projectRoot: settings.claudeResources.projectRoot,
174
+ scanRoot: settings.claudeResources.scanRoot,
174
175
  docsDir: settings.docsDir
175
176
  }
176
177
  }
@@ -301,6 +301,7 @@ export interface Settings {
301
301
  claudeResources: {
302
302
  claudeDir: string;
303
303
  projectRoot?: string;
304
+ scanRoot?: string;
304
305
  } | false;
305
306
  defaultLocaleOnlyPrefixes: string[];
306
307
  footer: FooterConfig | false;
@@ -8,10 +8,36 @@ export interface SidebarPrepaintProps {
8
8
  */
9
9
  hideSidebar?: boolean;
10
10
  }
11
- /** Settings subset read by {@link createSidebarPrepaint}. */
11
+ /** Settings subset read by the sidebar-prepaint factories. */
12
12
  export interface SidebarPrepaintSettings {
13
13
  sidebarToggle?: boolean;
14
14
  }
15
+ /**
16
+ * Pre-paint inline script body: restore persisted sidebar visibility to
17
+ * `<html data-sidebar-hidden>` before first paint to avoid a hard-reload flash.
18
+ *
19
+ * A tiny synchronous IIFE that reads `localStorage[SIDEBAR_STORAGE_KEY]` and
20
+ * sets `data-sidebar-hidden` only when the stored value is exactly "false"
21
+ * (the collapsed preference) — the default (visible) needs no attribute and
22
+ * causes no layout shift. Silently no-ops in privacy / disabled-storage modes.
23
+ *
24
+ * The storage key is interpolated from `SIDEBAR_STORAGE_KEY` so it stays in
25
+ * sync with the island's reader; the `data-sidebar-hidden` attribute name is a
26
+ * literal here (the island's `setDataAttribute` hardcodes the same literal —
27
+ * keep the two in sync). Intended for `<head>` placement so it executes before
28
+ * the `<aside>` sidebar is painted.
29
+ */
30
+ export declare const SIDEBAR_VISIBILITY_PREPAINT_SCRIPT: string;
31
+ /**
32
+ * Create a `SidebarVisibilityPrepaint` component bound to the host's settings.
33
+ *
34
+ * Returns the pre-paint `<script>` (for the page `<head>`) when
35
+ * `settings.sidebarToggle` is enabled AND the page actually shows a sidebar;
36
+ * returns `undefined` when the toggle is disabled or the page hides the sidebar
37
+ * — the SAME gating as {@link createSidebarPrepaint}, so the head script and
38
+ * the afterSidebar toggle button always appear together.
39
+ */
40
+ export declare function createSidebarVisibilityPrepaint(settings: SidebarPrepaintSettings): (props: SidebarPrepaintProps) => JSX.Element | undefined;
15
41
  /**
16
42
  * Create a `SidebarPrepaint` component bound to the host's settings.
17
43
  *
@@ -1,23 +1,43 @@
1
- import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
1
+ import { Fragment, jsx } from "preact/jsx-runtime";
2
2
  import { Island } from "@takazudo/zfb";
3
- import { DesktopSidebarToggle } from "../desktop-sidebar-toggle-island/index.js";
3
+ import {
4
+ DesktopSidebarToggle,
5
+ SIDEBAR_STORAGE_KEY
6
+ } from "../desktop-sidebar-toggle-island/index.js";
7
+ function sidebarPrepaintActive(settings, hideSidebar) {
8
+ return Boolean(settings.sidebarToggle) && !hideSidebar;
9
+ }
10
+ const SIDEBAR_VISIBILITY_PREPAINT_SCRIPT = `(function(){try{if(localStorage.getItem(${JSON.stringify(
11
+ SIDEBAR_STORAGE_KEY
12
+ )})==='false'){document.documentElement.setAttribute('data-sidebar-hidden','');}}catch(e){}})();`;
13
+ function createSidebarVisibilityPrepaint(settings) {
14
+ function SidebarVisibilityPrepaint({
15
+ hideSidebar
16
+ }) {
17
+ if (!sidebarPrepaintActive(settings, hideSidebar)) return void 0;
18
+ return /* @__PURE__ */ jsx(
19
+ "script",
20
+ {
21
+ dangerouslySetInnerHTML: { __html: SIDEBAR_VISIBILITY_PREPAINT_SCRIPT }
22
+ }
23
+ );
24
+ }
25
+ return SidebarVisibilityPrepaint;
26
+ }
4
27
  function createSidebarPrepaint(settings) {
5
28
  function SidebarPrepaint({
6
29
  hideSidebar
7
30
  }) {
8
- if (!settings.sidebarToggle || hideSidebar) return void 0;
9
- return /* @__PURE__ */ jsxs(Fragment, { children: [
10
- /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: {
11
- __html: `(function(){try{if(localStorage.getItem('zudo-doc-sidebar-visible')==='false'){document.documentElement.setAttribute('data-sidebar-hidden','');}}catch(e){}})();`
12
- } }),
13
- Island({
14
- when: "load",
15
- children: /* @__PURE__ */ jsx(DesktopSidebarToggle, {})
16
- })
17
- ] });
31
+ if (!sidebarPrepaintActive(settings, hideSidebar)) return void 0;
32
+ return /* @__PURE__ */ jsx(Fragment, { children: Island({
33
+ when: "load",
34
+ children: /* @__PURE__ */ jsx(DesktopSidebarToggle, {})
35
+ }) });
18
36
  }
19
37
  return SidebarPrepaint;
20
38
  }
21
39
  export {
22
- createSidebarPrepaint
40
+ SIDEBAR_VISIBILITY_PREPAINT_SCRIPT,
41
+ createSidebarPrepaint,
42
+ createSidebarVisibilityPrepaint
23
43
  };
@@ -25,7 +25,8 @@ function makeUrlHelpers(settings, i18n) {
25
25
  return settings.siteUrl ? settings.siteUrl.replace(/\/$/, "") + pageUrl : void 0;
26
26
  }
27
27
  function docsUrl(slug, lang = defaultLocale) {
28
- const path = lang === defaultLocale ? `/docs/${slug}` : `/${lang}/docs/${slug}`;
28
+ const defaultPath = `/docs/${slug}`;
29
+ const path = lang === defaultLocale || isDefaultLocaleOnlyPath(defaultPath) ? defaultPath : `/${lang}/docs/${slug}`;
29
30
  return withBase(path);
30
31
  }
31
32
  function isExternal(href) {
@@ -35,7 +36,7 @@ function makeUrlHelpers(settings, i18n) {
35
36
  return isExternal(href) ? href : withBase(href);
36
37
  }
37
38
  function navHref(path, lang, currentVersion) {
38
- const isNonDefaultLocale = lang != null && lang !== defaultLocale;
39
+ const isNonDefaultLocale = lang != null && lang !== defaultLocale && !isDefaultLocaleOnlyPath(path);
39
40
  const versionPrefix = currentVersion ? `/v/${currentVersion}` : "";
40
41
  return withBase(
41
42
  isNonDefaultLocale ? `${versionPrefix}/${lang}${path}` : `${versionPrefix}${path}`
@@ -82,7 +83,8 @@ function makeUrlHelpers(settings, i18n) {
82
83
  }));
83
84
  }
84
85
  function versionedDocsUrl(slug, versionSlug, lang = defaultLocale) {
85
- const path = lang === defaultLocale ? `/v/${versionSlug}/docs/${slug}` : `/v/${versionSlug}/${lang}/docs/${slug}`;
86
+ const localePrefixed = lang !== defaultLocale && !isDefaultLocaleOnlyPath(`/docs/${slug}`);
87
+ const path = localePrefixed ? `/v/${versionSlug}/${lang}/docs/${slug}` : `/v/${versionSlug}/docs/${slug}`;
86
88
  return withBase(path);
87
89
  }
88
90
  return {
@@ -8,7 +8,12 @@ import { AFTER_NAVIGATE_EVENT } from "../transitions/index.js";
8
8
 
9
9
  export const SIDEBAR_STORAGE_KEY = "zudo-doc-sidebar-visible";
10
10
 
11
- function readState(): boolean {
11
+ // Exported for unit testing the mount-reconcile logic in a plain Node env
12
+ // (no jsdom in the package vitest config) — the same convention the sibling
13
+ // ThemeToggle uses via color-scheme-sync.ts. `readState` is the localStorage
14
+ // reader the mount effect uses to reconcile `visible` on initial load;
15
+ // `setDataAttribute` is the `<html data-sidebar-hidden>` writer.
16
+ export function readState(): boolean {
12
17
  if (typeof window === "undefined") return true;
13
18
  try {
14
19
  return localStorage.getItem(SIDEBAR_STORAGE_KEY) !== "false";
@@ -17,7 +22,7 @@ function readState(): boolean {
17
22
  }
18
23
  }
19
24
 
20
- function setDataAttribute(isVisible: boolean) {
25
+ export function setDataAttribute(isVisible: boolean) {
21
26
  if (isVisible) {
22
27
  document.documentElement.removeAttribute("data-sidebar-hidden");
23
28
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takazudo/zudo-doc",
3
- "version": "2.4.1",
3
+ "version": "2.5.1",
4
4
  "type": "module",
5
5
  "description": "zudo-doc framework primitives layer that sits on top of zfb's engine — sidebar, theme, TOC, breadcrumb, layouts, head injection, View Transitions, SSR-skip wrappers (per ADR-003).",
6
6
  "license": "MIT",
@@ -586,7 +586,7 @@
586
586
  "zod": "^4.3.6",
587
587
  "@takazudo/zfb": "0.1.0-next.76",
588
588
  "@takazudo/zfb-runtime": "0.1.0-next.76",
589
- "@takazudo/zudo-doc-history-server": "2.4.1"
589
+ "@takazudo/zudo-doc-history-server": "2.5.1"
590
590
  },
591
591
  "scripts": {
592
592
  "build": "tsup && tsc -p tsconfig.build.json",