create-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 +146 -0
- package/bin/create-zudo-doc.js +2 -0
- package/dist/api.d.ts +20 -0
- package/dist/api.js +13 -0
- package/dist/claude-md-gen.d.ts +2 -0
- package/dist/claude-md-gen.js +113 -0
- package/dist/cli.d.ts +39 -0
- package/dist/cli.js +157 -0
- package/dist/compose.d.ts +95 -0
- package/dist/compose.js +206 -0
- package/dist/constants.d.ts +20 -0
- package/dist/constants.js +224 -0
- package/dist/features/body-foot-util.d.ts +10 -0
- package/dist/features/body-foot-util.js +12 -0
- package/dist/features/claude-resources.d.ts +2 -0
- package/dist/features/claude-resources.js +6 -0
- package/dist/features/design-token-panel.d.ts +14 -0
- package/dist/features/design-token-panel.js +27 -0
- package/dist/features/doc-history.d.ts +9 -0
- package/dist/features/doc-history.js +11 -0
- package/dist/features/doc-tags.d.ts +19 -0
- package/dist/features/doc-tags.js +33 -0
- package/dist/features/footer-taglist.d.ts +14 -0
- package/dist/features/footer-taglist.js +17 -0
- package/dist/features/footer.d.ts +8 -0
- package/dist/features/footer.js +10 -0
- package/dist/features/i18n.d.ts +22 -0
- package/dist/features/i18n.js +41 -0
- package/dist/features/image-enlarge.d.ts +11 -0
- package/dist/features/image-enlarge.js +13 -0
- package/dist/features/index.d.ts +15 -0
- package/dist/features/index.js +53 -0
- package/dist/features/llms-txt.d.ts +11 -0
- package/dist/features/llms-txt.js +13 -0
- package/dist/features/search.d.ts +9 -0
- package/dist/features/search.js +11 -0
- package/dist/features/sidebar-resizer.d.ts +14 -0
- package/dist/features/sidebar-resizer.js +16 -0
- package/dist/features/sidebar-toggle.d.ts +13 -0
- package/dist/features/sidebar-toggle.js +15 -0
- package/dist/features/tag-governance.d.ts +14 -0
- package/dist/features/tag-governance.js +16 -0
- package/dist/features/tauri-dev.d.ts +2 -0
- package/dist/features/tauri-dev.js +25 -0
- package/dist/features/tauri.d.ts +11 -0
- package/dist/features/tauri.js +52 -0
- package/dist/features/versioning.d.ts +27 -0
- package/dist/features/versioning.js +43 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +150 -0
- package/dist/preset.d.ts +37 -0
- package/dist/preset.js +156 -0
- package/dist/prompts.d.ts +32 -0
- package/dist/prompts.js +248 -0
- package/dist/scaffold.d.ts +4 -0
- package/dist/scaffold.js +344 -0
- package/dist/settings-gen.d.ts +2 -0
- package/dist/settings-gen.js +237 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +34 -0
- package/dist/zfb-config-gen.d.ts +19 -0
- package/dist/zfb-config-gen.js +222 -0
- package/package.json +65 -0
- package/templates/base/.htmlvalidate.json +5 -0
- package/templates/base/.zfb/doc-history-meta.json +1 -0
- package/templates/base/pages/404.tsx +55 -0
- package/templates/base/pages/_data.ts +179 -0
- package/templates/base/pages/_mdx-components.ts +249 -0
- package/templates/base/pages/docs/[...slug].tsx +448 -0
- package/templates/base/pages/index.tsx +158 -0
- package/templates/base/pages/lib/_body-end-islands.tsx +201 -0
- package/templates/base/pages/lib/_category-nav.tsx +148 -0
- package/templates/base/pages/lib/_category-tree-nav.tsx +104 -0
- package/templates/base/pages/lib/_compose-meta-title.ts +29 -0
- package/templates/base/pages/lib/_details.tsx +30 -0
- package/templates/base/pages/lib/_doc-history-area.tsx +178 -0
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +100 -0
- package/templates/base/pages/lib/_doc-tags-area.tsx +89 -0
- package/templates/base/pages/lib/_extract-headings.ts +81 -0
- package/templates/base/pages/lib/_footer-with-defaults.tsx +234 -0
- package/templates/base/pages/lib/_frontmatter-preview-data.ts +53 -0
- package/templates/base/pages/lib/_head-with-defaults.tsx +113 -0
- package/templates/base/pages/lib/_header-with-defaults.tsx +386 -0
- package/templates/base/pages/lib/_inline-version-switcher.tsx +84 -0
- package/templates/base/pages/lib/_math-block.tsx +63 -0
- package/templates/base/pages/lib/_nav-source-docs.ts +68 -0
- package/templates/base/pages/lib/_preset-generator.tsx +81 -0
- package/templates/base/pages/lib/_search-widget-script.ts +388 -0
- package/templates/base/pages/lib/_search-widget.tsx +196 -0
- package/templates/base/pages/lib/_sidebar-with-defaults.tsx +176 -0
- package/templates/base/pages/lib/_site-tree-nav.tsx +128 -0
- package/templates/base/pages/lib/locale-merge.ts +58 -0
- package/templates/base/pages/lib/route-enumerators.ts +302 -0
- package/templates/base/pages/sitemap.xml.tsx +51 -0
- package/templates/base/plugins/connect-adapter.mjs +144 -0
- package/templates/base/plugins/copy-public-plugin.mjs +50 -0
- package/templates/base/plugins/search-index-plugin.mjs +54 -0
- package/templates/base/scripts/run-b4push.sh +102 -0
- package/templates/base/src/components/ai-chat-modal.tsx +15 -0
- package/templates/base/src/components/client-router-bootstrap.tsx +14 -0
- package/templates/base/src/components/content/component-map.ts +25 -0
- package/templates/base/src/components/content/content-blockquote.tsx +16 -0
- package/templates/base/src/components/content/content-code.tsx +117 -0
- package/templates/base/src/components/content/content-link.tsx +83 -0
- package/templates/base/src/components/content/content-ol.tsx +19 -0
- package/templates/base/src/components/content/content-paragraph.tsx +10 -0
- package/templates/base/src/components/content/content-strong.tsx +16 -0
- package/templates/base/src/components/content/content-table.tsx +18 -0
- package/templates/base/src/components/content/content-ul.tsx +18 -0
- package/templates/base/src/components/content/heading-h2.tsx +26 -0
- package/templates/base/src/components/content/heading-h3.tsx +26 -0
- package/templates/base/src/components/content/heading-h4.tsx +26 -0
- package/templates/base/src/components/design-token-panel-bootstrap.tsx +15 -0
- package/templates/base/src/components/desktop-sidebar-toggle.tsx +15 -0
- package/templates/base/src/components/doc-history.tsx +18 -0
- package/templates/base/src/components/html-preview/highlighted-code.tsx +74 -0
- package/templates/base/src/components/html-preview/html-preview.tsx +108 -0
- package/templates/base/src/components/html-preview/preflight.ts +112 -0
- package/templates/base/src/components/html-preview/preview-base.tsx +159 -0
- package/templates/base/src/components/image-enlarge.tsx +19 -0
- package/templates/base/src/components/mobile-toc.tsx +94 -0
- package/templates/base/src/components/preset-generator.tsx +14 -0
- package/templates/base/src/components/sidebar-toggle.tsx +98 -0
- package/templates/base/src/components/sidebar-tree.tsx +543 -0
- package/templates/base/src/components/site-tree-nav.tsx +233 -0
- package/templates/base/src/components/theme-toggle.tsx +93 -0
- package/templates/base/src/components/toc.tsx +63 -0
- package/templates/base/src/components/tree-nav-shared.tsx +71 -0
- package/templates/base/src/config/color-scheme-utils.ts +182 -0
- package/templates/base/src/config/color-schemes.ts +128 -0
- package/templates/base/src/config/frontmatter-preview-defaults.ts +24 -0
- package/templates/base/src/config/frontmatter-preview-renderers.tsx +46 -0
- package/templates/base/src/config/i18n.ts +225 -0
- package/templates/base/src/config/settings-types.ts +162 -0
- package/templates/base/src/config/sidebars.ts +66 -0
- package/templates/base/src/config/tag-vocabulary-types.ts +39 -0
- package/templates/base/src/config/tag-vocabulary.ts +20 -0
- package/templates/base/src/hooks/use-active-heading.ts +133 -0
- package/templates/base/src/plugins/docs-source-map.ts +103 -0
- package/templates/base/src/plugins/hast-utils.ts +10 -0
- package/templates/base/src/plugins/rehype-code-title.ts +50 -0
- package/templates/base/src/plugins/rehype-heading-links.ts +53 -0
- package/templates/base/src/plugins/rehype-image-enlarge.ts +113 -0
- package/templates/base/src/plugins/rehype-mermaid.ts +41 -0
- package/templates/base/src/plugins/rehype-strip-md-extension.ts +58 -0
- package/templates/base/src/plugins/remark-admonitions.ts +99 -0
- package/templates/base/src/plugins/remark-resolve-markdown-links.ts +127 -0
- package/templates/base/src/plugins/url-utils.ts +4 -0
- package/templates/base/src/styles/global.css +1066 -0
- package/templates/base/src/types/docs-entry.ts +39 -0
- package/templates/base/src/types/heading.ts +5 -0
- package/templates/base/src/types/locale.ts +10 -0
- package/templates/base/src/utils/base.ts +139 -0
- package/templates/base/src/utils/content-files.ts +106 -0
- package/templates/base/src/utils/dedent.ts +24 -0
- package/templates/base/src/utils/docs.ts +335 -0
- package/templates/base/src/utils/git-info.ts +70 -0
- package/templates/base/src/utils/github.ts +19 -0
- package/templates/base/src/utils/header-right-items.ts +38 -0
- package/templates/base/src/utils/nav-scope.ts +63 -0
- package/templates/base/src/utils/sidebar.ts +104 -0
- package/templates/base/src/utils/slug.ts +10 -0
- package/templates/base/src/utils/smart-break.tsx +126 -0
- package/templates/base/src/utils/tags.ts +126 -0
- package/templates/base/tsconfig.json +36 -0
- package/templates/features/bodyFootUtil/files/src/utils/github.ts +19 -0
- package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +137 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/escape-for-mdx.test.ts +34 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +376 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/escape-for-mdx.ts +93 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +586 -0
- package/templates/features/designTokenPanel/files/src/components/design-token-panel-bootstrap.tsx +15 -0
- package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +99 -0
- package/templates/features/designTokenPanel/files/src/config/design-tokens-manifest.ts +177 -0
- package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +50 -0
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +99 -0
- package/templates/features/docHistory/files/src/components/doc-history.tsx +598 -0
- package/templates/features/docHistory/files/src/types/doc-history.ts +23 -0
- package/templates/features/docHistory/files/src/utils/doc-history.ts +180 -0
- package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +116 -0
- package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +99 -0
- package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +101 -0
- package/templates/features/docTags/files/pages/docs/tags/index.tsx +86 -0
- package/templates/features/i18n/files/pages/[locale]/docs/[...slug].tsx +467 -0
- package/templates/features/i18n/files/pages/[locale]/index.tsx +213 -0
- package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +248 -0
- package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +74 -0
- package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +185 -0
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +126 -0
- package/templates/features/tagGovernance/files/scripts/tags-audit.ts +576 -0
- package/templates/features/tagGovernance/files/scripts/tags-suggest.ts +428 -0
- package/templates/features/tauri/files/src/components/find-bar.tsx +122 -0
- package/templates/features/tauri/files/src/components/find-in-page-init.tsx +53 -0
- package/templates/features/tauri/files/src/utils/find-in-page.ts +175 -0
- package/templates/features/tauri/files/src-tauri/Cargo.toml +14 -0
- package/templates/features/tauri/files/src-tauri/build.rs +3 -0
- package/templates/features/tauri/files/src-tauri/capabilities/default.json +11 -0
- package/templates/features/tauri/files/src-tauri/src/main.rs +250 -0
- package/templates/features/tauri/files/src-tauri/tauri.conf.json +25 -0
- package/templates/features/tauriDev/files/src-tauri-dev/Cargo.toml +15 -0
- package/templates/features/tauriDev/files/src-tauri-dev/build.rs +3 -0
- package/templates/features/tauriDev/files/src-tauri-dev/capabilities/default.json +7 -0
- package/templates/features/tauriDev/files/src-tauri-dev/frontend/index.html +187 -0
- package/templates/features/tauriDev/files/src-tauri-dev/icons/icon.png +0 -0
- package/templates/features/tauriDev/files/src-tauri-dev/src/main.rs +995 -0
- package/templates/features/tauriDev/files/src-tauri-dev/tauri.conf.json +22 -0
- package/templates/features/tauriDev/files/src-tauri-dev/test-launch.sh +65 -0
- package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +100 -0
- package/templates/features/versioning/files/pages/docs/versions.tsx +78 -0
- package/templates/features/versioning/files/pages/v/[version]/docs/[...slug].tsx +451 -0
- package/templates/features/versioning/files/pages/v/[version]/ja/docs/[...slug].tsx +490 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo, useRef } from "preact/compat";
|
|
2
|
+
import { diffLines } from "diff";
|
|
3
|
+
import type { DocHistoryData, DocHistoryEntry } from "@/types/doc-history";
|
|
4
|
+
import { SmartBreak } from "@/utils/smart-break";
|
|
5
|
+
|
|
6
|
+
interface DocHistoryProps {
|
|
7
|
+
slug: string;
|
|
8
|
+
locale?: string;
|
|
9
|
+
basePath?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type PanelView = "closed" | "revisions" | "diff";
|
|
13
|
+
|
|
14
|
+
interface DiffSelection {
|
|
15
|
+
older: DocHistoryEntry;
|
|
16
|
+
newer: DocHistoryEntry;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* ────────────────────────────────────────────
|
|
20
|
+
* Icons
|
|
21
|
+
* ──────────────────────────────────────────── */
|
|
22
|
+
|
|
23
|
+
function HistoryIcon() {
|
|
24
|
+
return (
|
|
25
|
+
<svg
|
|
26
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
27
|
+
viewBox="0 0 24 24"
|
|
28
|
+
fill="none"
|
|
29
|
+
stroke="currentColor"
|
|
30
|
+
strokeWidth={2}
|
|
31
|
+
strokeLinecap="round"
|
|
32
|
+
strokeLinejoin="round"
|
|
33
|
+
className="h-icon-md w-icon-md"
|
|
34
|
+
>
|
|
35
|
+
<circle cx="12" cy="12" r="10" />
|
|
36
|
+
<polyline points="12 6 12 12 16 14" />
|
|
37
|
+
</svg>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function CloseIcon() {
|
|
42
|
+
return (
|
|
43
|
+
<svg
|
|
44
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
45
|
+
viewBox="0 0 24 24"
|
|
46
|
+
fill="none"
|
|
47
|
+
stroke="currentColor"
|
|
48
|
+
strokeWidth={2}
|
|
49
|
+
strokeLinecap="round"
|
|
50
|
+
strokeLinejoin="round"
|
|
51
|
+
className="h-icon-md w-icon-md"
|
|
52
|
+
>
|
|
53
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
54
|
+
</svg>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ArrowLeftIcon() {
|
|
59
|
+
return (
|
|
60
|
+
<svg
|
|
61
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
62
|
+
viewBox="0 0 24 24"
|
|
63
|
+
fill="none"
|
|
64
|
+
stroke="currentColor"
|
|
65
|
+
strokeWidth={2}
|
|
66
|
+
strokeLinecap="round"
|
|
67
|
+
strokeLinejoin="round"
|
|
68
|
+
className="h-icon-sm w-icon-sm"
|
|
69
|
+
>
|
|
70
|
+
<path d="M19 12H5M12 19l-7-7 7-7" />
|
|
71
|
+
</svg>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ────────────────────────────────────────────
|
|
76
|
+
* Spinner (matches page-loading-overlay style)
|
|
77
|
+
* ──────────────────────────────────────────── */
|
|
78
|
+
|
|
79
|
+
function Spinner() {
|
|
80
|
+
return (
|
|
81
|
+
<div className="flex items-center justify-center py-vsp-xl">
|
|
82
|
+
<span
|
|
83
|
+
className="inline-block box-border rounded-full animate-spin"
|
|
84
|
+
style={{
|
|
85
|
+
width: 48,
|
|
86
|
+
height: 48,
|
|
87
|
+
border: "5px solid var(--color-fg, #fff)",
|
|
88
|
+
borderBottomColor: "transparent",
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ────────────────────────────────────────────
|
|
96
|
+
* Side-by-side diff row types and builder
|
|
97
|
+
* ──────────────────────────────────────────── */
|
|
98
|
+
|
|
99
|
+
interface DiffRow {
|
|
100
|
+
leftLine: string | null; // null = empty (added-only row)
|
|
101
|
+
rightLine: string | null; // null = empty (removed-only row)
|
|
102
|
+
leftNum: number | null;
|
|
103
|
+
rightNum: number | null;
|
|
104
|
+
type: "context" | "removed" | "added" | "changed";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildSideBySideRows(
|
|
108
|
+
changes: ReturnType<typeof diffLines>,
|
|
109
|
+
): DiffRow[] {
|
|
110
|
+
const rows: DiffRow[] = [];
|
|
111
|
+
let leftNum = 0;
|
|
112
|
+
let rightNum = 0;
|
|
113
|
+
|
|
114
|
+
let i = 0;
|
|
115
|
+
while (i < changes.length) {
|
|
116
|
+
const change = changes[i];
|
|
117
|
+
|
|
118
|
+
if (!change.added && !change.removed) {
|
|
119
|
+
// Context lines — show on both sides
|
|
120
|
+
const lines = change.value.replace(/\n$/, "").split("\n");
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
leftNum++;
|
|
123
|
+
rightNum++;
|
|
124
|
+
rows.push({ leftLine: line, rightLine: line, leftNum, rightNum, type: "context" });
|
|
125
|
+
}
|
|
126
|
+
i++;
|
|
127
|
+
} else if (change.removed && i + 1 < changes.length && changes[i + 1].added) {
|
|
128
|
+
// Paired remove+add — show side by side
|
|
129
|
+
const removedLines = change.value.replace(/\n$/, "").split("\n");
|
|
130
|
+
const addedLines = changes[i + 1].value.replace(/\n$/, "").split("\n");
|
|
131
|
+
const maxLen = Math.max(removedLines.length, addedLines.length);
|
|
132
|
+
for (let j = 0; j < maxLen; j++) {
|
|
133
|
+
const left = j < removedLines.length ? removedLines[j] : null;
|
|
134
|
+
const right = j < addedLines.length ? addedLines[j] : null;
|
|
135
|
+
if (left !== null) leftNum++;
|
|
136
|
+
if (right !== null) rightNum++;
|
|
137
|
+
rows.push({
|
|
138
|
+
leftLine: left,
|
|
139
|
+
rightLine: right,
|
|
140
|
+
leftNum: left !== null ? leftNum : null,
|
|
141
|
+
rightNum: right !== null ? rightNum : null,
|
|
142
|
+
type: "changed",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
i += 2;
|
|
146
|
+
} else if (change.removed) {
|
|
147
|
+
const lines = change.value.replace(/\n$/, "").split("\n");
|
|
148
|
+
for (const line of lines) {
|
|
149
|
+
leftNum++;
|
|
150
|
+
rows.push({ leftLine: line, rightLine: null, leftNum, rightNum: null, type: "removed" });
|
|
151
|
+
}
|
|
152
|
+
i++;
|
|
153
|
+
} else {
|
|
154
|
+
// added
|
|
155
|
+
const lines = change.value.replace(/\n$/, "").split("\n");
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
rightNum++;
|
|
158
|
+
rows.push({ leftLine: null, rightLine: line, leftNum: null, rightNum, type: "added" });
|
|
159
|
+
}
|
|
160
|
+
i++;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return rows;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ────────────────────────────────────────────
|
|
168
|
+
* DiffViewer sub-component (side-by-side)
|
|
169
|
+
* ──────────────────────────────────────────── */
|
|
170
|
+
|
|
171
|
+
function DiffViewer({
|
|
172
|
+
selection,
|
|
173
|
+
onBack,
|
|
174
|
+
showBackButton,
|
|
175
|
+
}: {
|
|
176
|
+
selection: DiffSelection;
|
|
177
|
+
onBack: () => void;
|
|
178
|
+
showBackButton: boolean;
|
|
179
|
+
}) {
|
|
180
|
+
const changes = useMemo(
|
|
181
|
+
() => diffLines(selection.older.content, selection.newer.content),
|
|
182
|
+
[selection.older.content, selection.newer.content],
|
|
183
|
+
);
|
|
184
|
+
const rows = useMemo(() => buildSideBySideRows(changes), [changes]);
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<div className="flex flex-col h-full">
|
|
188
|
+
{/* Header */}
|
|
189
|
+
<div className="flex items-center gap-hsp-sm px-hsp-lg py-vsp-xs border-b border-muted">
|
|
190
|
+
{showBackButton && (
|
|
191
|
+
<button
|
|
192
|
+
type="button"
|
|
193
|
+
onClick={onBack}
|
|
194
|
+
className="text-muted hover:text-fg lg:hidden"
|
|
195
|
+
aria-label="Back to revisions"
|
|
196
|
+
>
|
|
197
|
+
<ArrowLeftIcon />
|
|
198
|
+
</button>
|
|
199
|
+
)}
|
|
200
|
+
<div className="flex-1 min-w-0 flex">
|
|
201
|
+
<div className="w-1/2 text-small text-muted font-mono truncate pr-hsp-sm">
|
|
202
|
+
{selection.older.hash.slice(0, 7)}
|
|
203
|
+
</div>
|
|
204
|
+
<div className="w-1/2 text-small text-muted font-mono truncate pl-hsp-sm">
|
|
205
|
+
{selection.newer.hash.slice(0, 7)}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{/* Side-by-side diff */}
|
|
211
|
+
<div className="flex-1 overflow-auto">
|
|
212
|
+
<table className="w-full border-collapse" style={{ tableLayout: "fixed" }}>
|
|
213
|
+
<colgroup>
|
|
214
|
+
<col style={{ width: "2.5rem" }} />
|
|
215
|
+
<col />
|
|
216
|
+
<col style={{ width: "2.5rem" }} />
|
|
217
|
+
<col />
|
|
218
|
+
</colgroup>
|
|
219
|
+
<tbody>
|
|
220
|
+
{rows.map((row, idx) => {
|
|
221
|
+
const leftBg =
|
|
222
|
+
row.type === "removed" || row.type === "changed"
|
|
223
|
+
? "diff-line-removed"
|
|
224
|
+
: "";
|
|
225
|
+
const rightBg =
|
|
226
|
+
row.type === "added" || row.type === "changed"
|
|
227
|
+
? "diff-line-added"
|
|
228
|
+
: "";
|
|
229
|
+
const leftEmpty = row.leftLine === null;
|
|
230
|
+
const rightEmpty = row.rightLine === null;
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<tr key={idx} className="diff-row">
|
|
234
|
+
{/* Left line number */}
|
|
235
|
+
<td className={`diff-line-num ${leftBg}`}>
|
|
236
|
+
{row.leftNum ?? ""}
|
|
237
|
+
</td>
|
|
238
|
+
{/* Left content */}
|
|
239
|
+
<td className={`diff-line-content ${leftBg}${leftEmpty ? " diff-line-empty" : ""}`}>
|
|
240
|
+
{row.leftLine ?? ""}
|
|
241
|
+
</td>
|
|
242
|
+
{/* Right line number */}
|
|
243
|
+
<td className={`diff-line-num ${rightBg}`}>
|
|
244
|
+
{row.rightNum ?? ""}
|
|
245
|
+
</td>
|
|
246
|
+
{/* Right content */}
|
|
247
|
+
<td className={`diff-line-content ${rightBg}${rightEmpty ? " diff-line-empty" : ""}`}>
|
|
248
|
+
{row.rightLine ?? ""}
|
|
249
|
+
</td>
|
|
250
|
+
</tr>
|
|
251
|
+
);
|
|
252
|
+
})}
|
|
253
|
+
</tbody>
|
|
254
|
+
</table>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* ────────────────────────────────────────────
|
|
261
|
+
* RevisionList sub-component
|
|
262
|
+
* ──────────────────────────────────────────── */
|
|
263
|
+
|
|
264
|
+
function RevisionList({
|
|
265
|
+
entries,
|
|
266
|
+
onSelectDiff,
|
|
267
|
+
}: {
|
|
268
|
+
entries: DocHistoryEntry[];
|
|
269
|
+
onSelectDiff: (selection: DiffSelection) => void;
|
|
270
|
+
}) {
|
|
271
|
+
const [selectedA, setSelectedA] = useState<number>(1); // older (default: second entry)
|
|
272
|
+
const [selectedB, setSelectedB] = useState<number>(0); // newer (default: first entry)
|
|
273
|
+
|
|
274
|
+
if (entries.length === 0) {
|
|
275
|
+
return (
|
|
276
|
+
<div className="px-hsp-lg py-vsp-lg text-muted text-small">
|
|
277
|
+
No revision history available.
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const canCompare =
|
|
283
|
+
selectedA !== selectedB &&
|
|
284
|
+
selectedA >= 0 &&
|
|
285
|
+
selectedB >= 0 &&
|
|
286
|
+
selectedA < entries.length &&
|
|
287
|
+
selectedB < entries.length;
|
|
288
|
+
|
|
289
|
+
function handleCompare() {
|
|
290
|
+
if (!canCompare) return;
|
|
291
|
+
const idxOlder = Math.max(selectedA, selectedB);
|
|
292
|
+
const idxNewer = Math.min(selectedA, selectedB);
|
|
293
|
+
onSelectDiff({
|
|
294
|
+
older: entries[idxOlder],
|
|
295
|
+
newer: entries[idxNewer],
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<div className="flex flex-col h-full">
|
|
301
|
+
{/* Compare bar */}
|
|
302
|
+
{entries.length >= 2 && (
|
|
303
|
+
<div className="px-hsp-lg py-vsp-xs border-b border-muted flex items-center gap-hsp-sm">
|
|
304
|
+
<button
|
|
305
|
+
type="button"
|
|
306
|
+
disabled={!canCompare}
|
|
307
|
+
onClick={handleCompare}
|
|
308
|
+
className={
|
|
309
|
+
canCompare
|
|
310
|
+
? "px-hsp-md py-vsp-2xs text-small rounded bg-accent text-bg hover:bg-accent-hover"
|
|
311
|
+
: "px-hsp-md py-vsp-2xs text-small rounded bg-surface text-muted cursor-not-allowed"
|
|
312
|
+
}
|
|
313
|
+
>
|
|
314
|
+
Compare
|
|
315
|
+
</button>
|
|
316
|
+
<span className="text-caption text-muted">
|
|
317
|
+
Select two revisions (A / B)
|
|
318
|
+
</span>
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
|
|
322
|
+
{/* Revision entries */}
|
|
323
|
+
<div className="flex-1 overflow-auto">
|
|
324
|
+
{entries.map((entry, idx) => {
|
|
325
|
+
const isA = selectedA === idx;
|
|
326
|
+
const isB = selectedB === idx;
|
|
327
|
+
const dateStr = formatDate(entry.date);
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<div
|
|
331
|
+
key={entry.hash}
|
|
332
|
+
className={
|
|
333
|
+
isA || isB
|
|
334
|
+
? "px-hsp-lg py-vsp-xs border-b border-muted bg-surface"
|
|
335
|
+
: "px-hsp-lg py-vsp-xs border-b border-muted hover:bg-surface"
|
|
336
|
+
}
|
|
337
|
+
>
|
|
338
|
+
<div className="flex items-start gap-hsp-sm">
|
|
339
|
+
{/* Selection badges */}
|
|
340
|
+
{entries.length >= 2 && (
|
|
341
|
+
<div className="flex flex-col gap-vsp-2xs pt-[2px] shrink-0">
|
|
342
|
+
<button
|
|
343
|
+
type="button"
|
|
344
|
+
onClick={() => setSelectedA(idx)}
|
|
345
|
+
className={
|
|
346
|
+
isA
|
|
347
|
+
? "w-[1.5rem] h-[1.25rem] text-caption rounded flex items-center justify-center bg-accent text-bg"
|
|
348
|
+
: "w-[1.5rem] h-[1.25rem] text-caption rounded flex items-center justify-center border border-muted text-muted hover:border-fg hover:text-fg"
|
|
349
|
+
}
|
|
350
|
+
aria-label={`Select revision ${entry.hash.slice(0, 7)} as A`}
|
|
351
|
+
>
|
|
352
|
+
A
|
|
353
|
+
</button>
|
|
354
|
+
<button
|
|
355
|
+
type="button"
|
|
356
|
+
onClick={() => setSelectedB(idx)}
|
|
357
|
+
className={
|
|
358
|
+
isB
|
|
359
|
+
? "w-[1.5rem] h-[1.25rem] text-caption rounded flex items-center justify-center bg-accent text-bg"
|
|
360
|
+
: "w-[1.5rem] h-[1.25rem] text-caption rounded flex items-center justify-center border border-muted text-muted hover:border-fg hover:text-fg"
|
|
361
|
+
}
|
|
362
|
+
aria-label={`Select revision ${entry.hash.slice(0, 7)} as B`}
|
|
363
|
+
>
|
|
364
|
+
B
|
|
365
|
+
</button>
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
|
|
369
|
+
{/* Revision info */}
|
|
370
|
+
<div className="min-w-0 flex-1">
|
|
371
|
+
<div className="flex items-baseline gap-hsp-sm">
|
|
372
|
+
<code className="text-caption text-accent font-mono">
|
|
373
|
+
{entry.hash.slice(0, 7)}
|
|
374
|
+
</code>
|
|
375
|
+
<span className="text-caption text-muted">{dateStr}</span>
|
|
376
|
+
</div>
|
|
377
|
+
<div className="text-small text-fg mt-vsp-2xs truncate">
|
|
378
|
+
<SmartBreak>{entry.message}</SmartBreak>
|
|
379
|
+
</div>
|
|
380
|
+
<div className="text-caption text-muted">{entry.author}</div>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
);
|
|
385
|
+
})}
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* ────────────────────────────────────────────
|
|
392
|
+
* Date formatter
|
|
393
|
+
* ──────────────────────────────────────────── */
|
|
394
|
+
|
|
395
|
+
function formatDate(dateStr: string): string {
|
|
396
|
+
const d = new Date(dateStr);
|
|
397
|
+
if (isNaN(d.getTime())) return dateStr;
|
|
398
|
+
return d.toLocaleDateString(undefined, {
|
|
399
|
+
year: "numeric",
|
|
400
|
+
month: "short",
|
|
401
|
+
day: "numeric",
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* ────────────────────────────────────────────
|
|
406
|
+
* Main DocHistory component
|
|
407
|
+
* ──────────────────────────────────────────── */
|
|
408
|
+
|
|
409
|
+
export function DocHistory({ slug, locale, basePath = "/" }: DocHistoryProps) {
|
|
410
|
+
const [view, setView] = useState<PanelView>("closed");
|
|
411
|
+
const [data, setData] = useState<DocHistoryData | null>(null);
|
|
412
|
+
const [loading, setLoading] = useState(false);
|
|
413
|
+
const [error, setError] = useState<string | null>(null);
|
|
414
|
+
const [diffSelection, setDiffSelection] = useState<DiffSelection | null>(
|
|
415
|
+
null,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const base = basePath.replace(/\/+$/, "");
|
|
419
|
+
const fetchPath = locale
|
|
420
|
+
? `${base}/doc-history/${locale}/${slug}.json`
|
|
421
|
+
: `${base}/doc-history/${slug}.json`;
|
|
422
|
+
|
|
423
|
+
const fetchHistory = useCallback(async () => {
|
|
424
|
+
if (data) return; // already loaded
|
|
425
|
+
setLoading(true);
|
|
426
|
+
setError(null);
|
|
427
|
+
try {
|
|
428
|
+
const res = await fetch(fetchPath);
|
|
429
|
+
if (!res.ok) {
|
|
430
|
+
throw new Error(`Failed to load history (${res.status})`);
|
|
431
|
+
}
|
|
432
|
+
const json: DocHistoryData = await res.json();
|
|
433
|
+
setData(json);
|
|
434
|
+
} catch (e) {
|
|
435
|
+
setError(e instanceof Error ? e.message : "Failed to load history");
|
|
436
|
+
} finally {
|
|
437
|
+
setLoading(false);
|
|
438
|
+
}
|
|
439
|
+
}, [data, fetchPath]);
|
|
440
|
+
|
|
441
|
+
function handleOpen() {
|
|
442
|
+
setView("revisions");
|
|
443
|
+
fetchHistory();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const handleClose = useCallback(() => {
|
|
447
|
+
setView("closed");
|
|
448
|
+
setDiffSelection(null);
|
|
449
|
+
}, []);
|
|
450
|
+
|
|
451
|
+
function handleSelectDiff(selection: DiffSelection) {
|
|
452
|
+
setDiffSelection(selection);
|
|
453
|
+
setView("diff");
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function handleBackToRevisions() {
|
|
457
|
+
setDiffSelection(null);
|
|
458
|
+
setView("revisions");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Lock body scroll when panel is open
|
|
462
|
+
useEffect(() => {
|
|
463
|
+
if (view !== "closed") {
|
|
464
|
+
document.body.style.overflow = "hidden";
|
|
465
|
+
} else {
|
|
466
|
+
document.body.style.overflow = "";
|
|
467
|
+
}
|
|
468
|
+
return () => {
|
|
469
|
+
document.body.style.overflow = "";
|
|
470
|
+
};
|
|
471
|
+
}, [view]);
|
|
472
|
+
|
|
473
|
+
// Close on Escape key
|
|
474
|
+
useEffect(() => {
|
|
475
|
+
if (view === "closed") return;
|
|
476
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
477
|
+
if (e.key === "Escape") handleClose();
|
|
478
|
+
}
|
|
479
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
480
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
481
|
+
}, [view, handleClose]);
|
|
482
|
+
|
|
483
|
+
// Close on View Transition navigation
|
|
484
|
+
useEffect(() => {
|
|
485
|
+
document.addEventListener("DOMContentLoaded", handleClose);
|
|
486
|
+
return () => document.removeEventListener("DOMContentLoaded", handleClose);
|
|
487
|
+
}, [handleClose]);
|
|
488
|
+
|
|
489
|
+
const isOpen = view !== "closed";
|
|
490
|
+
const hasDiff = view === "diff" && diffSelection;
|
|
491
|
+
|
|
492
|
+
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
493
|
+
|
|
494
|
+
// Sync dialog open/close with React state
|
|
495
|
+
useEffect(() => {
|
|
496
|
+
const dialog = dialogRef.current;
|
|
497
|
+
if (!dialog) return;
|
|
498
|
+
if (isOpen && !dialog.open) {
|
|
499
|
+
dialog.showModal();
|
|
500
|
+
} else if (!isOpen && dialog.open) {
|
|
501
|
+
dialog.close();
|
|
502
|
+
}
|
|
503
|
+
}, [isOpen]);
|
|
504
|
+
|
|
505
|
+
// Close React state when dialog is closed natively (Escape key)
|
|
506
|
+
useEffect(() => {
|
|
507
|
+
const dialog = dialogRef.current;
|
|
508
|
+
if (!dialog) return;
|
|
509
|
+
function onClose() {
|
|
510
|
+
if (isOpen) handleClose();
|
|
511
|
+
}
|
|
512
|
+
dialog.addEventListener("close", onClose);
|
|
513
|
+
return () => dialog.removeEventListener("close", onClose);
|
|
514
|
+
}, [isOpen, handleClose]);
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
<>
|
|
518
|
+
{/* History button */}
|
|
519
|
+
{!isOpen && (
|
|
520
|
+
<div className="flex justify-end mt-vsp-xl">
|
|
521
|
+
<button
|
|
522
|
+
type="button"
|
|
523
|
+
onClick={handleOpen}
|
|
524
|
+
className="doc-history-trigger flex items-center gap-hsp-xs px-hsp-md py-vsp-xs rounded-lg bg-surface border border-muted text-muted hover:text-fg hover:border-fg transition-colors"
|
|
525
|
+
aria-label="View document history"
|
|
526
|
+
>
|
|
527
|
+
<HistoryIcon />
|
|
528
|
+
<span className="text-small">History</span>
|
|
529
|
+
</button>
|
|
530
|
+
</div>
|
|
531
|
+
)}
|
|
532
|
+
|
|
533
|
+
{/* Full-screen dialog — renders in top layer, above all stacking contexts */}
|
|
534
|
+
<dialog
|
|
535
|
+
ref={dialogRef}
|
|
536
|
+
aria-label="Document revision history"
|
|
537
|
+
className="doc-history-panel fixed inset-0 m-0 h-full w-full max-h-full max-w-full bg-bg border-none p-0 backdrop:bg-bg/30"
|
|
538
|
+
style={{ color: "var(--color-fg)" }}
|
|
539
|
+
>
|
|
540
|
+
{/* Panel header */}
|
|
541
|
+
<div className="flex items-center justify-between px-hsp-lg py-vsp-xs border-b border-muted">
|
|
542
|
+
<h2 className="text-body font-semibold text-fg">
|
|
543
|
+
{view === "diff" ? "Diff" : "Revision History"}
|
|
544
|
+
</h2>
|
|
545
|
+
<button
|
|
546
|
+
type="button"
|
|
547
|
+
onClick={handleClose}
|
|
548
|
+
className="text-muted hover:text-fg"
|
|
549
|
+
aria-label="Close history panel"
|
|
550
|
+
>
|
|
551
|
+
<CloseIcon />
|
|
552
|
+
</button>
|
|
553
|
+
</div>
|
|
554
|
+
|
|
555
|
+
{/* Panel body */}
|
|
556
|
+
<div className="h-[calc(100%-3rem)] overflow-hidden">
|
|
557
|
+
{loading && <Spinner />}
|
|
558
|
+
|
|
559
|
+
{error && (
|
|
560
|
+
<div className="px-hsp-lg py-vsp-lg text-danger text-small">
|
|
561
|
+
{error}
|
|
562
|
+
</div>
|
|
563
|
+
)}
|
|
564
|
+
|
|
565
|
+
{/* Difit-style LR split: revision sidebar | diff area */}
|
|
566
|
+
{!loading && !error && data && (
|
|
567
|
+
<div className="flex h-full">
|
|
568
|
+
{/* Left sidebar: revision list — always visible on lg */}
|
|
569
|
+
<div
|
|
570
|
+
className={
|
|
571
|
+
hasDiff
|
|
572
|
+
? "hidden lg:flex lg:flex-col lg:w-[clamp(16rem,25%,22rem)] shrink-0 border-r border-muted h-full"
|
|
573
|
+
: "flex flex-col w-full h-full"
|
|
574
|
+
}
|
|
575
|
+
>
|
|
576
|
+
<RevisionList
|
|
577
|
+
entries={data.entries}
|
|
578
|
+
onSelectDiff={handleSelectDiff}
|
|
579
|
+
/>
|
|
580
|
+
</div>
|
|
581
|
+
|
|
582
|
+
{/* Right: diff viewer (on mobile, replaces the sidebar) */}
|
|
583
|
+
{hasDiff && (
|
|
584
|
+
<div className="flex-1 min-w-0 h-full">
|
|
585
|
+
<DiffViewer
|
|
586
|
+
selection={diffSelection}
|
|
587
|
+
onBack={handleBackToRevisions}
|
|
588
|
+
showBackButton={true}
|
|
589
|
+
/>
|
|
590
|
+
</div>
|
|
591
|
+
)}
|
|
592
|
+
</div>
|
|
593
|
+
)}
|
|
594
|
+
</div>
|
|
595
|
+
</dialog>
|
|
596
|
+
</>
|
|
597
|
+
);
|
|
598
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** A single git revision entry for a document */
|
|
2
|
+
export interface DocHistoryEntry {
|
|
3
|
+
/** Full commit hash (use .slice(0, 7) for display) */
|
|
4
|
+
hash: string;
|
|
5
|
+
/** ISO 8601 date string */
|
|
6
|
+
date: string;
|
|
7
|
+
/** Commit author name */
|
|
8
|
+
author: string;
|
|
9
|
+
/** First line of commit message */
|
|
10
|
+
message: string;
|
|
11
|
+
/** Full file content at this revision */
|
|
12
|
+
content: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Complete history data for a single document */
|
|
16
|
+
export interface DocHistoryData {
|
|
17
|
+
/** Document slug (route path) */
|
|
18
|
+
slug: string;
|
|
19
|
+
/** Relative file path in the repository */
|
|
20
|
+
filePath: string;
|
|
21
|
+
/** Git revision entries, newest first */
|
|
22
|
+
entries: DocHistoryEntry[];
|
|
23
|
+
}
|