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