create-zudo-doc 0.2.20 → 0.2.22

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.
Files changed (74) hide show
  1. package/dist/features/image-enlarge.d.ts +10 -2
  2. package/dist/features/image-enlarge.js +10 -2
  3. package/dist/scaffold.js +22 -22
  4. package/dist/zfb-config-gen.d.ts +16 -10
  5. package/dist/zfb-config-gen.js +34 -239
  6. package/package.json +1 -1
  7. package/templates/base/pages/lib/_body-end-islands.tsx +6 -13
  8. package/templates/base/pages/lib/_category-nav.tsx +30 -115
  9. package/templates/base/pages/lib/_category-tree-nav.tsx +38 -65
  10. package/templates/base/pages/lib/_compose-meta-title.ts +2 -6
  11. package/templates/base/pages/lib/_doc-body-end.tsx +3 -35
  12. package/templates/base/pages/lib/_doc-content-header.tsx +10 -111
  13. package/templates/base/pages/lib/_doc-history-area.tsx +19 -211
  14. package/templates/base/pages/lib/_doc-metainfo-area.tsx +10 -113
  15. package/templates/base/pages/lib/_doc-page-renderer.tsx +22 -183
  16. package/templates/base/pages/lib/_doc-page-shell.tsx +17 -251
  17. package/templates/base/pages/lib/_doc-pager.tsx +4 -74
  18. package/templates/base/pages/lib/_doc-route-entries.ts +43 -177
  19. package/templates/base/pages/lib/_doc-route-paths.ts +16 -101
  20. package/templates/base/pages/lib/_doc-tags-area.tsx +14 -77
  21. package/templates/base/pages/lib/_footer-with-defaults.tsx +37 -225
  22. package/templates/base/pages/lib/_frontmatter-preview-data.ts +5 -31
  23. package/templates/base/pages/lib/_head-with-defaults.tsx +13 -138
  24. package/templates/base/pages/lib/_header-with-defaults.tsx +24 -324
  25. package/templates/base/pages/lib/_inline-version-switcher.tsx +16 -78
  26. package/templates/base/pages/lib/_nav-data-prep.ts +32 -51
  27. package/templates/base/pages/lib/_nav-source-cache.ts +12 -57
  28. package/templates/base/pages/lib/_nav-source-docs.ts +33 -233
  29. package/templates/base/pages/lib/_search-widget-script.ts +2 -470
  30. package/templates/base/pages/lib/_search-widget.tsx +9 -195
  31. package/templates/base/pages/lib/_sidebar-prepaint.tsx +6 -47
  32. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +16 -118
  33. package/templates/base/pages/lib/_site-tree-nav.tsx +29 -80
  34. package/templates/base/pages/lib/doc-page-props.ts +26 -44
  35. package/templates/base/pages/lib/locale-merge.ts +32 -145
  36. package/templates/base/pages/lib/route-enumerators.ts +52 -286
  37. package/templates/base/pages/robots.txt.tsx +2 -26
  38. package/templates/base/src/components/ai-chat-modal.tsx +9 -8
  39. package/templates/base/src/components/doc-history.tsx +9 -8
  40. package/templates/base/src/components/image-enlarge.tsx +11 -8
  41. package/templates/base/src/components/sidebar-toggle.tsx +6 -170
  42. package/templates/base/src/components/sidebar-tree.tsx +6 -548
  43. package/templates/base/src/components/site-tree-nav.tsx +6 -220
  44. package/templates/base/src/config/color-scheme-utils.ts +34 -158
  45. package/templates/base/src/config/i18n.ts +9 -0
  46. package/templates/base/src/config/z-index-tokens.ts +5 -4
  47. package/templates/base/src/styles/global.css +5 -579
  48. package/templates/base/src/utils/base.ts +1 -1
  49. package/templates/base/src/utils/docs.ts +47 -16
  50. package/templates/base/src/utils/github.ts +12 -9
  51. package/templates/base/src/utils/nav-scope.ts +13 -42
  52. package/templates/base/src/utils/sidebar.ts +18 -86
  53. package/templates/base/src/utils/slug.ts +10 -53
  54. package/templates/base/src/utils/smart-break.tsx +12 -120
  55. package/templates/base/src/utils/tags.ts +25 -68
  56. package/templates/features/bodyFootUtil/files/src/utils/github.ts +12 -9
  57. package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +13 -39
  58. package/templates/features/docHistory/files/src/components/doc-history.tsx +8 -636
  59. package/templates/features/docHistory/files/src/types/doc-history.ts +7 -23
  60. package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +35 -201
  61. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +8 -269
  62. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +6 -99
  63. package/templates/features/tagGovernance/files/scripts/tags-audit.ts +67 -515
  64. package/templates/features/tauri/files/src-tauri/capabilities/default.json +1 -4
  65. package/templates/features/tauri/files/src-tauri/tauri.conf.json +1 -1
  66. package/templates/features/tauriDev/files/src-tauri-dev/capabilities/default.json +1 -1
  67. package/templates/features/versioning/files/pages/lib/_versions-page.tsx +21 -73
  68. package/templates/base/plugins/connect-adapter.mjs +0 -169
  69. package/templates/base/plugins/search-index-plugin.mjs +0 -66
  70. package/templates/base/scripts/gen-z-index.mjs +0 -157
  71. package/templates/base/src/components/mermaid-enlarge.tsx +0 -490
  72. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +0 -47
  73. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +0 -111
  74. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +0 -93
@@ -1,213 +1,47 @@
1
1
  /** @jsxRuntime automatic */
2
2
  /** @jsxImportSource preact */
3
- // Shared data + renderers for the doc-tags pages (#2010).
4
- //
5
- // Collapses the per-locale page pairs:
6
- // pages/docs/tags/[tag].tsx + pages/[locale]/docs/tags/[tag].tsx
7
- // pages/docs/tags/index.tsx + pages/[locale]/docs/tags/index.tsx
8
- // into locale-parameterized helpers. The page files stay thin shells that own
9
- // only their paths() param shapes; URL prefixes and the default-vs-locale
10
- // data path branch live here.
11
- //
12
- // Data-path branch (kept exactly as the original pages had it):
13
- // - Default locale: the "docs" collection directly, filtered
14
- // unlisted/draft/category_no_page before tag collection.
15
- // - Non-default locale: locale-first merge with base fallback
16
- // (see locale-merge.ts) — draft-filtered inputs, unlisted dropped by the
17
- // merge default, category_no_page filtered AFTER the merge so a locale
18
- // override carrying the flag first wins the merge (suppressing the base
19
- // doc); pre-merge filtering would drop it from localeSlugSet and the
20
- // unflagged base doc would resurface as a card linking to a locale route
21
- // the docs route never builds.
3
+ // Thin stub tag-pages moved to the package (epic #2344, S8).
4
+ // Calls `createTagPages(deps)` from @takazudo/zudo-doc/tag-pages with host
5
+ // singletons injected, then re-exports the resulting functions so all existing
6
+ // call sites continue to work unchanged.
22
7
 
8
+ import { createTagPages } from "@takazudo/zudo-doc/tag-pages";
9
+ import type { TagInfo } from "@takazudo/zudo-doc/tag-pages";
23
10
  import { collectTags } from "@/utils/tags";
24
- import type { TagInfo } from "@/utils/tags";
25
- import { toRouteSlug } from "@/utils/slug";
26
- import { t, defaultLocale } from "@/config/i18n";
11
+ import { defaultLocale, t } from "@/config/i18n";
27
12
  import { settings } from "@/config/settings";
28
13
  import { withBase, docsUrl } from "@/utils/base";
29
- import { DocLayoutWithDefaults } from "@takazudo/zudo-doc/doclayout";
30
- import { Breadcrumb } from "@takazudo/zudo-doc/breadcrumb";
31
- import type { BreadcrumbItem } from "@takazudo/zudo-doc/breadcrumb";
32
- import { DocCardGrid, TagNav } from "@takazudo/zudo-doc/nav-indexing";
33
- import type { TagItem, TagNavLabels } from "@takazudo/zudo-doc/nav-indexing";
34
- import type { JSX } from "preact";
14
+ import { isDefaultLocaleOnlyPath } from "@/utils/base";
35
15
  import { FooterWithDefaults } from "./_footer-with-defaults";
36
16
  import { HeaderWithDefaults } from "./_header-with-defaults";
37
17
  import { HeadWithDefaults } from "./_head-with-defaults";
38
18
  import { composeMetaTitle } from "./_compose-meta-title";
39
19
  import { DocHistoryArea } from "./_doc-history-area";
40
20
  import { BodyEndIslands } from "./_body-end-islands";
41
- import { stableDocs, memoizeDerived } from "./_nav-source-cache";
42
- import { mergeLocaleDocs } from "./locale-merge";
43
-
44
- // ---------------------------------------------------------------------------
45
- // Tag collection
46
- // ---------------------------------------------------------------------------
47
-
48
- /**
49
- * Build the { tag → TagInfo } map for one locale, preserving each original
50
- * page's exact data path (see the module-header branch notes).
51
- *
52
- * Memoized on the snapshot-anchored stableDocs identity (same pattern as
53
- * _nav-source-cache) so the collection bridging and tag aggregation run once
54
- * per locale per build rather than once per built tag page.
55
- */
56
- export function collectTagMapForLocale(locale: string): Map<string, TagInfo> {
57
- if (locale === defaultLocale) {
58
- const baseDocs = stableDocs("docs");
59
- return memoizeDerived([baseDocs], "tagMap;default", () => {
60
- // category_no_page index.mdx builds no route — drop it so a tag it carries
61
- // doesn't render a DocCard linking to a non-existent /docs/<cat>/ page.
62
- const docs = baseDocs.filter(
63
- (doc) =>
64
- !doc.data.unlisted && !doc.data.draft && !doc.data.category_no_page,
65
- );
66
- return collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
67
- });
68
- }
69
-
70
- const baseDocs = stableDocs("docs");
71
- const localeDocs = stableDocs(`docs-${locale}`);
72
- return memoizeDerived([baseDocs, localeDocs], `tagMap;${locale}`, () => {
73
- const { docs: mergedDocs } = mergeLocaleDocs({
74
- baseDocs: baseDocs.filter((d) => !d.data.draft),
75
- localeDocs: localeDocs.filter((d) => !d.data.draft),
76
- applyDefaultLocaleOnlyFilter: true,
77
- });
78
- const docs = mergedDocs.filter((d) => !d.data.category_no_page);
79
- return collectTags(docs, (id, data) => data.slug ?? toRouteSlug(id));
80
- });
81
- }
82
-
83
- // ---------------------------------------------------------------------------
84
- // Shared renderers
85
- // ---------------------------------------------------------------------------
86
-
87
- /** URL prefix for a locale: "" for the default locale, "/{locale}" otherwise. */
88
- function localePrefix(locale: string): string {
89
- return locale === defaultLocale ? "" : `/${locale}`;
90
- }
91
-
92
- /** Per-tag detail page (chip target). */
93
- export function TagDetailPageView({
94
- locale,
95
- tag,
96
- tagInfo,
97
- }: {
98
- locale: string;
99
- tag: string;
100
- tagInfo: TagInfo;
101
- }): JSX.Element {
102
- const isDefault = locale === defaultLocale;
103
- const prefix = localePrefix(locale);
104
-
105
- const countText =
106
- tagInfo.count === 1
107
- ? t("doc.pageCountSingle", locale).replace("{count}", String(tagInfo.count))
108
- : t("doc.pageCount", locale).replace("{count}", String(tagInfo.count));
109
-
110
- const pageTitle = `${t("doc.taggedWith", locale)}: ${tag}`;
111
-
112
- const breadcrumbItems: BreadcrumbItem[] = [
113
- { label: "Docs" },
114
- { label: t("doc.allTags", locale), href: withBase(`${prefix}/docs/tags`) },
115
- { label: tag },
116
- ];
117
-
118
- const cardItems = tagInfo.docs.map((doc) => ({
119
- href: docsUrl(doc.slug, locale),
120
- title: doc.title,
121
- description: doc.description,
122
- }));
123
-
124
- return (
125
- <DocLayoutWithDefaults
126
- title={composeMetaTitle(pageTitle)}
127
- head={<HeadWithDefaults title={pageTitle} />}
128
- // The original default-locale page omitted `lang` entirely; passing
129
- // undefined relies on Preact treating an undefined prop as absent.
130
- lang={isDefault ? undefined : locale}
131
- noindex={settings.noindex}
132
- hideSidebar={true}
133
- hideToc={true}
134
- // Empty fragment suppresses DocLayoutWithDefaults' empty-data default
135
- // Sidebar island — its marker never hydrates for published-package
136
- // consumers (zfb#999) and zfb >= next.38 warns about it; the sidebar is
137
- // hidden on this page anyway (zudolab/zudo-doc#2057).
138
- sidebarOverride={<></>}
139
- // Tag segment URL-encoded — emitted href/path sites only; route params
140
- // stay raw (e.g. "type:guide" → "type%3Aguide").
141
- headerOverride={<HeaderWithDefaults lang={locale} currentPath={withBase(`${prefix}/docs/tags/${encodeURIComponent(tag)}`)} />}
142
- breadcrumbOverride={<Breadcrumb items={breadcrumbItems} />}
143
- footerOverride={<FooterWithDefaults lang={locale} />}
144
- bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
145
- enableClientRouter={settings.dynamicPageTransition}
146
- >
147
- <h1 class="text-heading font-bold mb-vsp-xs">{pageTitle}</h1>
148
- <p class="text-muted mb-vsp-lg">{countText}</p>
149
- <DocCardGrid ariaLabel={pageTitle} items={cardItems} />
150
- <DocHistoryArea slug={`tags/${tag}`} locale={locale} />
151
- </DocLayoutWithDefaults>
152
- );
153
- }
154
-
155
- /** "All Tags" index page — computes the tag map at render time (matching the
156
- * original pages, which had no props from paths()). */
157
- export function TagsIndexPageView({ locale }: { locale: string }): JSX.Element {
158
- const isDefault = locale === defaultLocale;
159
- const prefix = localePrefix(locale);
160
- const pageTitle = t("doc.allTags", locale);
161
-
162
- const tagMap = collectTagMapForLocale(locale);
163
-
164
- const labels: TagNavLabels = {
165
- tags: t("doc.tags", locale),
166
- taggedWith: t("doc.taggedWith", locale),
167
- };
168
-
169
- // Sort alphabetically using the page locale — matches documented tag-nav sort order.
170
- const tags: TagItem[] = [...tagMap.values()]
171
- .sort((a, b) => a.tag.localeCompare(b.tag, locale))
172
- .map((info) => ({
173
- tag: info.tag,
174
- count: info.count,
175
- // Tag segment URL-encoded — href sites only; route params stay raw.
176
- href: withBase(`${prefix}/docs/tags/${encodeURIComponent(info.tag)}`),
177
- }));
178
-
179
- const breadcrumbItems: BreadcrumbItem[] = [
180
- { label: "Docs" },
181
- { label: pageTitle },
182
- ];
183
-
184
- return (
185
- <DocLayoutWithDefaults
186
- title={composeMetaTitle(pageTitle)}
187
- head={<HeadWithDefaults title={pageTitle} />}
188
- // Same undefined-≡-absent reliance as TagDetailPageView above.
189
- lang={isDefault ? undefined : locale}
190
- noindex={settings.noindex}
191
- hideSidebar={true}
192
- hideToc={true}
193
- // Empty fragment suppresses DocLayoutWithDefaults' empty-data default
194
- // Sidebar island — its marker never hydrates for published-package
195
- // consumers (zfb#999) and zfb >= next.38 warns about it; the sidebar is
196
- // hidden on this page anyway (zudolab/zudo-doc#2057).
197
- sidebarOverride={<></>}
198
- headerOverride={<HeaderWithDefaults lang={locale} currentPath={withBase(`${prefix}/docs/tags`)} />}
199
- breadcrumbOverride={<Breadcrumb items={breadcrumbItems} />}
200
- footerOverride={<FooterWithDefaults lang={locale} />}
201
- bodyEndComponents={<BodyEndIslands basePath={settings.base ?? "/"} />}
202
- enableClientRouter={settings.dynamicPageTransition}
203
- >
204
- <h1 class="text-heading font-bold mb-vsp-lg">{pageTitle}</h1>
205
- {!settings.docTags || tags.length === 0 ? (
206
- <p class="text-muted">{t("doc.noTags", locale)}</p>
207
- ) : (
208
- <TagNav variant="all" tags={tags} labels={labels} />
209
- )}
210
- <DocHistoryArea slug="tags" locale={locale} />
211
- </DocLayoutWithDefaults>
212
- );
213
- }
21
+ import { stableDocs } from "./_nav-source-cache";
22
+
23
+ export type { TagInfo };
24
+
25
+ const { collectTagMapForLocale, TagDetailPageView, TagsIndexPageView } = createTagPages({
26
+ settings,
27
+ defaultLocale,
28
+ t,
29
+ withBase,
30
+ docsUrl,
31
+ composeMetaTitle,
32
+ collectTags: collectTags as (
33
+ entries: import("@takazudo/zudo-doc/tag-pages").TagPagesDocsEntry[],
34
+ slugFn: (id: string, data: { slug?: string }) => string,
35
+ ) => Map<string, TagInfo>,
36
+ stableDocs: stableDocs as (collectionName: string) => import("@takazudo/zudo-doc/tag-pages").TagPagesDocsEntry[],
37
+ isDefaultLocaleOnlyPath,
38
+ components: {
39
+ HeadWithDefaults,
40
+ HeaderWithDefaults,
41
+ FooterWithDefaults,
42
+ BodyEndIslands,
43
+ DocHistoryArea,
44
+ },
45
+ });
46
+
47
+ export { collectTagMapForLocale, TagDetailPageView, TagsIndexPageView };
@@ -1,272 +1,11 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect, useRef } from "preact/compat";
4
- import type { JSX } from "preact";
5
-
6
- interface ImageData {
7
- src: string;
8
- currentSrc: string;
9
- srcset?: string;
10
- sizes?: string;
11
- alt: string;
12
- naturalWidth: number;
13
- naturalHeight: number;
14
- }
15
-
16
- // Shared shell for the enlarge `<dialog>`. The hydrated component and the
17
- // SSR fallback (below) render into the same Island container, so they MUST
18
- // agree on class string and inline style — otherwise the dist HTML and the
19
- // post-hydration DOM disagree on size / position and the first interaction
20
- // flashes. Sourcing both from the same constants closes the drift gap.
3
+ // Re-export shim for the ImageEnlarge island.
21
4
  //
22
- // z-modal / backdrop:z-modal-backdrop are defense-in-depth for the SPA-swap
23
- // window: if this dialog is still open while the page body is swapped, a native
24
- // showModal() dialog can lose top-layer promotion and fall back to z-index:auto,
25
- // flashing behind the header/sidebar. `backdrop:z-modal-backdrop` targets the
26
- // native `::backdrop` (present for every showModal() dialog even with no backdrop
27
- // tint). The explicit modal-tier z-index keeps it above all chrome during that
28
- // window. Intentionally redundant in the normal (top-layer) case — do not remove
29
- // as "redundant".
30
- const DIALOG_CLASS =
31
- "zd-enlarge-dialog z-modal mx-auto max-h-[90vh] max-w-[90vw] overflow-hidden border border-muted bg-surface p-0 backdrop:z-modal-backdrop";
32
- // Center the modal with `inset: 0; margin: auto` rather than a transform.
33
- // A `transform` on the dialog would establish a containing block for its
34
- // `position: fixed` descendants, which would trap the `.zd-enlarge-dialog-close`
35
- // button at the dialog's corner instead of the viewport's — see the close
36
- // button's fixed positioning in global.css. Auto-margin centering keeps the
37
- // dialog transform-free so the close button anchors to the page top-right.
38
- const DIALOG_STYLE = {
39
- position: "fixed",
40
- inset: "0",
41
- margin: "auto",
42
- } as const;
43
-
44
- export default function ImageEnlarge() {
45
- const [imgData, setImgData] = useState<ImageData | null>(null);
46
- const dialogRef = useRef<HTMLDialogElement>(null);
47
-
48
- // Eligibility detection: toggle .zd-enlarge-btn[hidden] per image
49
- useEffect(() => {
50
- // Single shared ResizeObserver watching all observed images.
51
- // One observer for N images is more efficient than N observers.
52
- // The callback iterates entries so each image's eligibility is
53
- // re-evaluated independently when its size changes.
54
- const observedImages = new Set<HTMLImageElement>();
55
- const sharedResizeObserver = new ResizeObserver((entries) => {
56
- for (const entry of entries) {
57
- evaluateEligibility(entry.target as HTMLImageElement);
58
- }
59
- });
60
- let mutationObserver: MutationObserver | null = null;
61
- let resizeTimer = 0;
62
-
63
- function evaluateEligibility(img: HTMLImageElement) {
64
- const container = img.closest(".zd-enlargeable");
65
- if (!container) return;
66
- const btn = container.querySelector(".zd-enlarge-btn") as HTMLElement | null;
67
- if (!btn) return;
68
- const eligible = img.naturalWidth > img.clientWidth * window.devicePixelRatio;
69
- if (eligible) {
70
- btn.removeAttribute("hidden");
71
- } else {
72
- btn.setAttribute("hidden", "");
73
- }
74
- }
75
-
76
- function observeImage(img: HTMLImageElement) {
77
- if (observedImages.has(img)) return;
78
- observedImages.add(img);
79
- sharedResizeObserver.observe(img);
80
- if (img.complete) {
81
- evaluateEligibility(img);
82
- } else {
83
- img.addEventListener("load", () => evaluateEligibility(img), { once: true });
84
- }
85
- }
86
-
87
- function scanContent() {
88
- const scope = document.querySelector("main .zd-content");
89
- if (!scope) return;
90
- scope.querySelectorAll<HTMLImageElement>(".zd-enlargeable img").forEach(observeImage);
91
- }
92
-
93
- function startObserving() {
94
- const scope = document.querySelector("main .zd-content");
95
- if (scope) {
96
- mutationObserver = new MutationObserver((mutations) => {
97
- for (const mutation of mutations) {
98
- for (const node of mutation.addedNodes) {
99
- if (!(node instanceof Element)) continue;
100
- if (node.matches(".zd-enlargeable")) {
101
- node.querySelectorAll<HTMLImageElement>("img").forEach(observeImage);
102
- }
103
- node.querySelectorAll<HTMLImageElement>(".zd-enlargeable img").forEach(observeImage);
104
- }
105
- }
106
- });
107
- mutationObserver.observe(scope, { childList: true, subtree: true });
108
- }
109
- scanContent();
110
- }
111
-
112
- function handleWindowResize() {
113
- clearTimeout(resizeTimer);
114
- resizeTimer = window.setTimeout(() => {
115
- observedImages.forEach((img) => evaluateEligibility(img));
116
- }, 150);
117
- }
118
-
119
- function handleAfterSwap() {
120
- sharedResizeObserver.disconnect();
121
- observedImages.clear();
122
- mutationObserver?.disconnect();
123
- mutationObserver = null;
124
- startObserving();
125
- }
126
-
127
- startObserving();
128
- window.addEventListener("resize", handleWindowResize);
129
- document.addEventListener("DOMContentLoaded", handleAfterSwap);
130
-
131
- return () => {
132
- sharedResizeObserver.disconnect();
133
- observedImages.clear();
134
- mutationObserver?.disconnect();
135
- window.removeEventListener("resize", handleWindowResize);
136
- document.removeEventListener("DOMContentLoaded", handleAfterSwap);
137
- clearTimeout(resizeTimer);
138
- };
139
- }, []);
140
-
141
- useEffect(() => {
142
- function handleDocumentClick(e: MouseEvent) {
143
- const target = e.target as Element;
144
- const container = target.closest(".zd-enlargeable");
145
- if (!container) return;
146
- const btn = container.querySelector(".zd-enlarge-btn") as HTMLElement | null;
147
- // Eligibility gate: only open when the expand button is visible (image is large enough).
148
- if (!btn || btn.hasAttribute("hidden")) return;
149
- const img = container.querySelector("img") as HTMLImageElement | null;
150
- if (!img) return;
151
- setImgData({
152
- src: img.src,
153
- currentSrc: img.currentSrc,
154
- srcset: img.srcset || undefined,
155
- sizes: img.sizes || undefined,
156
- alt: img.alt,
157
- naturalWidth: img.naturalWidth,
158
- naturalHeight: img.naturalHeight,
159
- });
160
- }
161
- document.addEventListener("click", handleDocumentClick);
162
- return () => document.removeEventListener("click", handleDocumentClick);
163
- }, []);
164
-
165
- // Open dialog when imgData is set
166
- useEffect(() => {
167
- if (!imgData) return;
168
- const dialog = dialogRef.current;
169
- if (!dialog) return;
170
- dialog.showModal();
171
- }, [imgData]);
172
-
173
- // Handle cancel event (ESC key)
174
- useEffect(() => {
175
- const dialog = dialogRef.current;
176
- if (!dialog) return;
177
- function handleCancel() {
178
- setImgData(null);
179
- }
180
- dialog.addEventListener("cancel", handleCancel);
181
- return () => dialog.removeEventListener("cancel", handleCancel);
182
- }, []);
183
-
184
- // Reset state when dialog closes
185
- useEffect(() => {
186
- const dialog = dialogRef.current;
187
- if (!dialog) return;
188
- function handleClose() {
189
- setImgData(null);
190
- }
191
- dialog.addEventListener("close", handleClose);
192
- return () => dialog.removeEventListener("close", handleClose);
193
- }, []);
194
-
195
- // Close and reset on ClientRouter navigation
196
- useEffect(() => {
197
- function handleAfterSwap() {
198
- const dialog = dialogRef.current;
199
- if (dialog?.open) dialog.close();
200
- setImgData(null);
201
- }
202
- document.addEventListener("DOMContentLoaded", handleAfterSwap);
203
- return () => document.removeEventListener("DOMContentLoaded", handleAfterSwap);
204
- }, []);
205
-
206
- function handleBackdropClick(e: JSX.TargetedMouseEvent<HTMLDialogElement>) {
207
- const dialog = dialogRef.current;
208
- if (!dialog) return;
209
- const rect = dialog.getBoundingClientRect();
210
- if (
211
- e.clientX < rect.left ||
212
- e.clientX > rect.right ||
213
- e.clientY < rect.top ||
214
- e.clientY > rect.bottom
215
- ) {
216
- dialog.close();
217
- }
218
- }
219
-
220
- return (
221
- <dialog
222
- ref={dialogRef}
223
- onClick={handleBackdropClick}
224
- className={DIALOG_CLASS}
225
- style={DIALOG_STYLE}
226
- >
227
- {imgData && (
228
- <>
229
- <div className="relative">
230
- <img
231
- src={imgData.currentSrc || imgData.src}
232
- srcSet={imgData.srcset}
233
- sizes={imgData.srcset ? "100vw" : undefined}
234
- alt={imgData.alt}
235
- className="block max-h-[85vh] max-w-[85vw] object-contain"
236
- />
237
- </div>
238
- <button
239
- type="button"
240
- onClick={() => dialogRef.current?.close()}
241
- className="zd-enlarge-dialog-close"
242
- aria-label="Close enlarged image"
243
- >
244
- <svg viewBox="0 0 161.03 161.03" fill="currentColor" aria-hidden="true" focusable="false">
245
- <polygon points="161.03 10.27 150.76 0 80.51 70.24 10.27 0 0 10.27 70.24 80.51 0 150.76 10.27 161.03 80.51 90.78 150.76 161.03 161.03 150.76 90.78 80.51 161.03 10.27" />
246
- </svg>
247
- </button>
248
- </>
249
- )}
250
- </dialog>
251
- );
252
- }
253
-
254
- /**
255
- * Static SSR fallback for the {@link ImageEnlarge} island.
256
- *
257
- * Renders an empty, closed `<dialog class="zd-enlarge-dialog ...">` so the
258
- * dist HTML carries the dialog shell even before hydration. A `<dialog>`
259
- * without `open` is `display:none` per UA stylesheet, so screen readers
260
- * and crawlers see the same shape they would post-hydration. Sources its
261
- * class and inline style from the shared `DIALOG_CLASS` / `DIALOG_STYLE`
262
- * constants above so the SSR shell cannot drift from the hydrated
263
- * dialog (a drift would surface as a cosmetic flash on first interaction).
264
- */
265
- export function ImageEnlargeSsrFallback() {
266
- return (
267
- <dialog
268
- className={DIALOG_CLASS}
269
- style={DIALOG_STYLE}
270
- />
271
- );
272
- }
5
+ // The real component and its SSR fallback now ship in the package at
6
+ // @takazudo/zudo-doc/image-enlarge (epic #2344, S3).
7
+ // pages/lib/_body-end-islands.tsx imports them directly from there; this
8
+ // overlay overwrites the base W6A no-op stub so that any project-local
9
+ // code referencing the @/components/image-enlarge path resolves to the
10
+ // real island when the imageEnlarge feature is enabled.
11
+ export { ImageEnlarge, ImageEnlargeSsrFallback } from "@takazudo/zudo-doc/image-enlarge";
@@ -1,99 +1,6 @@
1
- "use client";
2
-
3
- import { useState, useEffect, useRef } from 'preact/hooks';
4
-
5
- export const SIDEBAR_STORAGE_KEY = 'zudo-doc-sidebar-visible';
6
-
7
- function readState(): boolean {
8
- if (typeof window === 'undefined') return true;
9
- try {
10
- return localStorage.getItem(SIDEBAR_STORAGE_KEY) !== 'false';
11
- } catch {
12
- return true;
13
- }
14
- }
15
-
16
- function setDataAttribute(isVisible: boolean) {
17
- if (isVisible) {
18
- document.documentElement.removeAttribute('data-sidebar-hidden');
19
- } else {
20
- document.documentElement.setAttribute('data-sidebar-hidden', '');
21
- }
22
- }
23
-
24
- export default function DesktopSidebarToggle() {
25
- // Initial state must match server render (always `true`) to avoid a
26
- // hydration mismatch when the persisted preference is "hidden". The
27
- // doc-layout's pre-paint inline script applies `data-sidebar-hidden`
28
- // to <html> from localStorage *before* this island mounts, so the
29
- // visual state stays correct; we only need to sync this island's
30
- // React state to the persisted preference after hydration.
31
- const [visible, setVisible] = useState<boolean>(true);
32
- // Tracks whether the hydration sync (below) has run. The persistence
33
- // effect below skips the very first mount so we don't overwrite the
34
- // user's persisted "hidden" preference with the SSR-safe default
35
- // `true` before the hydration sync gets a chance to fire.
36
- const hydrated = useRef(false);
37
-
38
- // Persist state changes to localStorage and the <html> data-attribute.
39
- // The `hydrated.current` guard is the real protection: it is still
40
- // `false` on the very first effect run (the hydration-sync effect
41
- // below sets it to `true` only after this one fires, since effects
42
- // run in declaration order on mount), so the first run bails out
43
- // and we don't clobber the user's persisted "hidden" preference
44
- // with the SSR-safe default `true`.
45
- useEffect(() => {
46
- if (!hydrated.current) return;
47
- setDataAttribute(visible);
48
- try {
49
- localStorage.setItem(SIDEBAR_STORAGE_KEY, String(visible));
50
- } catch {
51
- // ignore storage errors
52
- }
53
- }, [visible]);
54
-
55
- // After mount, read the persisted preference and reconcile state
56
- // with the SSR default. Sets the ref so subsequent runs of the
57
- // persistence effect above start syncing normally.
58
- useEffect(() => {
59
- hydrated.current = true;
60
- const actual = readState();
61
- if (actual !== visible) {
62
- setVisible(actual);
63
- }
64
- // eslint-disable-next-line react-hooks/exhaustive-deps
65
- }, []);
66
-
67
- // The SPA-navigation flash (a collapsed sidebar briefly painting open on
68
- // every soft swap) is handled upstream: doc-layout mounts
69
- // <ClientRouter preserveHtmlAttrs={["data-sidebar-hidden", ...]} /> so
70
- // zfb-runtime (>= 0.1.0-next.52) re-applies the runtime attribute across the
71
- // swap before paint. No host-side capture/restore guard is needed.
72
-
73
- return (
74
- <button
75
- type="button"
76
- onClick={() => setVisible((v) => !v)}
77
- className="zd-desktop-sidebar-toggle hidden lg:flex fixed bottom-vsp-xl z-sidebar items-center justify-center w-[1.5rem] h-[3rem] bg-surface border border-muted border-l-0 rounded-r-DEFAULT text-muted cursor-pointer transition-[left,color] duration-200 ease-in-out hover:text-fg"
78
- aria-label={visible ? 'Hide sidebar' : 'Show sidebar'}
79
- aria-pressed={visible}
80
- data-zfb-transition-persist="desktop-sidebar-toggle"
81
- >
82
- <svg
83
- xmlns="http://www.w3.org/2000/svg"
84
- className="h-icon-sm w-icon-sm"
85
- aria-hidden="true"
86
- fill="none"
87
- viewBox="0 0 24 24"
88
- stroke="currentColor"
89
- strokeWidth={2}
90
- >
91
- <path
92
- strokeLinecap="round"
93
- strokeLinejoin="round"
94
- d={visible ? 'M15 19l-7-7 7-7' : 'M9 5l7 7-7 7'}
95
- />
96
- </svg>
97
- </button>
98
- );
99
- }
1
+ // Thin re-export shim. DesktopSidebarToggle moved into the package
2
+ // (`@takazudo/zudo-doc/desktop-sidebar-toggle-island`) as part of the
3
+ // package-first migration (#2344, S2). Host code using
4
+ // `@/components/desktop-sidebar-toggle` still resolves correctly;
5
+ // the implementation lives in the package.
6
+ export { DesktopSidebarToggle, SIDEBAR_STORAGE_KEY } from "@takazudo/zudo-doc/desktop-sidebar-toggle-island";