create-zudo-doc 0.2.21 → 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.
- package/dist/features/image-enlarge.d.ts +10 -2
- package/dist/features/image-enlarge.js +10 -2
- package/dist/scaffold.js +20 -22
- package/dist/zfb-config-gen.d.ts +16 -10
- package/dist/zfb-config-gen.js +34 -239
- package/package.json +1 -1
- package/templates/base/pages/lib/_body-end-islands.tsx +6 -13
- package/templates/base/pages/lib/_category-nav.tsx +30 -115
- package/templates/base/pages/lib/_category-tree-nav.tsx +38 -65
- package/templates/base/pages/lib/_compose-meta-title.ts +2 -6
- package/templates/base/pages/lib/_doc-body-end.tsx +3 -35
- package/templates/base/pages/lib/_doc-content-header.tsx +10 -111
- package/templates/base/pages/lib/_doc-history-area.tsx +19 -211
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +10 -113
- package/templates/base/pages/lib/_doc-page-renderer.tsx +22 -183
- package/templates/base/pages/lib/_doc-page-shell.tsx +17 -251
- package/templates/base/pages/lib/_doc-pager.tsx +4 -74
- package/templates/base/pages/lib/_doc-route-entries.ts +43 -177
- package/templates/base/pages/lib/_doc-route-paths.ts +16 -101
- package/templates/base/pages/lib/_doc-tags-area.tsx +14 -77
- package/templates/base/pages/lib/_footer-with-defaults.tsx +37 -225
- package/templates/base/pages/lib/_frontmatter-preview-data.ts +5 -31
- package/templates/base/pages/lib/_head-with-defaults.tsx +13 -138
- package/templates/base/pages/lib/_header-with-defaults.tsx +24 -324
- package/templates/base/pages/lib/_inline-version-switcher.tsx +16 -78
- package/templates/base/pages/lib/_nav-data-prep.ts +32 -51
- package/templates/base/pages/lib/_nav-source-cache.ts +12 -57
- package/templates/base/pages/lib/_nav-source-docs.ts +33 -233
- package/templates/base/pages/lib/_search-widget-script.ts +2 -470
- package/templates/base/pages/lib/_search-widget.tsx +9 -195
- package/templates/base/pages/lib/_sidebar-prepaint.tsx +6 -59
- package/templates/base/pages/lib/_sidebar-with-defaults.tsx +16 -118
- package/templates/base/pages/lib/_site-tree-nav.tsx +29 -80
- package/templates/base/pages/lib/doc-page-props.ts +26 -44
- package/templates/base/pages/lib/locale-merge.ts +32 -145
- package/templates/base/pages/lib/route-enumerators.ts +52 -286
- package/templates/base/pages/robots.txt.tsx +2 -26
- package/templates/base/src/components/ai-chat-modal.tsx +9 -8
- package/templates/base/src/components/doc-history.tsx +9 -8
- package/templates/base/src/components/image-enlarge.tsx +11 -8
- package/templates/base/src/components/sidebar-toggle.tsx +6 -170
- package/templates/base/src/components/sidebar-tree.tsx +6 -548
- package/templates/base/src/components/site-tree-nav.tsx +6 -220
- package/templates/base/src/config/color-scheme-utils.ts +34 -158
- package/templates/base/src/config/i18n.ts +9 -0
- package/templates/base/src/config/z-index-tokens.ts +5 -4
- package/templates/base/src/styles/global.css +5 -579
- package/templates/base/src/utils/base.ts +1 -1
- package/templates/base/src/utils/docs.ts +47 -16
- package/templates/base/src/utils/github.ts +12 -9
- package/templates/base/src/utils/nav-scope.ts +13 -42
- package/templates/base/src/utils/sidebar.ts +18 -86
- package/templates/base/src/utils/slug.ts +10 -53
- package/templates/base/src/utils/smart-break.tsx +12 -120
- package/templates/base/src/utils/tags.ts +25 -68
- package/templates/features/bodyFootUtil/files/src/utils/github.ts +12 -9
- package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +13 -39
- package/templates/features/docHistory/files/src/components/doc-history.tsx +8 -636
- package/templates/features/docHistory/files/src/types/doc-history.ts +7 -23
- package/templates/features/docTags/files/pages/lib/_tag-pages.tsx +35 -201
- package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +8 -269
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +6 -99
- package/templates/features/tagGovernance/files/scripts/tags-audit.ts +67 -515
- package/templates/features/versioning/files/pages/lib/_versions-page.tsx +21 -73
- package/templates/base/plugins/connect-adapter.mjs +0 -169
- package/templates/base/plugins/search-index-plugin.mjs +0 -66
- package/templates/base/scripts/gen-z-index.mjs +0 -157
- package/templates/base/src/components/mermaid-enlarge.tsx +0 -490
- package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +0 -47
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +0 -111
- 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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
|
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 {
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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";
|