@drawnagency/primitives 0.1.118 → 0.1.119
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/{chunk-O37AV27P.js → chunk-5JYHTHVR.js} +28 -0
- package/dist/{chunk-W72BPGR3.js → chunk-BZKHHUIN.js} +44 -8
- package/dist/{chunk-CEFFP3CH.js → chunk-G6O3TYPJ.js} +297 -83
- package/dist/{chunk-4NSOKEA7.js → chunk-HFHM4MNK.js} +12 -7
- package/dist/{chunk-RIITGTV3.js → chunk-LQHBC7C6.js} +37 -2
- package/dist/{chunk-VY3IFGE3.js → chunk-QAMAIDXZ.js} +1 -1
- package/dist/{chunk-HRX3XJBU.js → chunk-RIN2QCYJ.js} +9 -11
- package/dist/{chunk-ZOTRHHUB.js → chunk-UNFPATDE.js} +8 -10
- package/dist/{chunk-ERLIL4RU.js → chunk-YKKGVSJH.js} +35 -37
- package/dist/{chunk-67P34WNW.js → chunk-ZKVQMIMJ.js} +3 -20
- package/dist/collab/rebase.js +4 -5
- package/dist/components/chat/ChatComposer.d.ts +0 -4
- package/dist/components/chat/ChatComposer.d.ts.map +1 -1
- package/dist/components/chat/ChatPanel.d.ts +2 -3
- package/dist/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/components/chat/markdown.d.ts +2 -3
- package/dist/components/chat/markdown.d.ts.map +1 -1
- package/dist/components/editor/PagesModal.d.ts +0 -2
- package/dist/components/editor/PagesModal.d.ts.map +1 -1
- package/dist/components/editor/index.js +15 -19
- package/dist/components/primitives/index.js +11 -15
- package/dist/components/sections/brand-guide.js +10 -13
- package/dist/components/sections/register-schemas.js +16 -21
- package/dist/components/sections/register.js +16 -21
- package/dist/components/shell/AudienceChips.d.ts +1 -3
- package/dist/components/shell/AudienceChips.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts +0 -34
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsModal.d.ts.map +1 -1
- package/dist/hooks/useSortableNavRow.d.ts +1 -1
- package/dist/hooks/useSortableRow.d.ts +1 -1
- package/dist/index.js +83 -90
- package/dist/lib/index-ops.js +5 -6
- package/dist/lib/index.js +27 -32
- package/dist/lib/pages.d.ts +0 -2
- package/dist/lib/pages.d.ts.map +1 -1
- package/dist/lib/sidecars.js +2 -2
- package/dist/lib/site-validation.js +13 -14
- package/dist/lib/storage-tier.js +4 -4
- package/dist/schemas/index.js +42 -45
- package/dist/schemas/site-config.d.ts +1 -277
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/package.json +2 -42
- package/src/components/chat/ChatComposer.tsx +0 -4
- package/src/components/chat/ChatPanel.tsx +2 -3
- package/src/components/chat/markdown.ts +2 -3
- package/src/components/editor/PagesModal.tsx +1 -15
- package/src/components/editor/StatusIndicator.tsx +4 -4
- package/src/components/shell/AudienceChips.tsx +1 -3
- package/src/components/shell/EditorShell.tsx +5 -113
- package/src/components/shell/SiteSettingsModal.tsx +0 -11
- package/src/lib/pages.ts +0 -22
- package/src/schemas/index.ts +0 -4
- package/src/schemas/site-config.ts +4 -32
- package/dist/chunk-4TZJZU2T.js +0 -92
- package/dist/chunk-DIMDRBFK.js +0 -11
- package/dist/chunk-FV7JQRCN.js +0 -94
- package/dist/chunk-IIDKW7AV.js +0 -41
- package/dist/chunk-JCDZAFX2.js +0 -224
- package/dist/chunk-MM7KZS5C.js +0 -30
- package/dist/chunk-QQ3S3P5P.js +0 -38
- package/dist/chunk-TEOAKGZA.js +0 -784
- package/dist/chunk-VAYZJKVR.js +0 -209
- package/dist/components/shell/DocInspector.d.ts +0 -18
- package/dist/components/shell/DocInspector.d.ts.map +0 -1
- package/dist/components/shell/DocumentEditorOverlay.d.ts +0 -29
- package/dist/components/shell/DocumentEditorOverlay.d.ts.map +0 -1
- package/dist/components/shell/DocumentPresentOverlay.d.ts +0 -30
- package/dist/components/shell/DocumentPresentOverlay.d.ts.map +0 -1
- package/dist/components/shell/SiteSettingsDecks.d.ts +0 -13
- package/dist/components/shell/SiteSettingsDecks.d.ts.map +0 -1
- package/dist/deck/DeckComponentView.d.ts +0 -13
- package/dist/deck/DeckComponentView.d.ts.map +0 -1
- package/dist/deck/DeckPlayer.d.ts +0 -26
- package/dist/deck/DeckPlayer.d.ts.map +0 -1
- package/dist/deck/DeckSlideView.d.ts +0 -17
- package/dist/deck/DeckSlideView.d.ts.map +0 -1
- package/dist/deck/DeckStage.d.ts +0 -16
- package/dist/deck/DeckStage.d.ts.map +0 -1
- package/dist/deck/components/DeckImage.d.ts +0 -14
- package/dist/deck/components/DeckImage.d.ts.map +0 -1
- package/dist/deck/components/DeckImageGrid.d.ts +0 -27
- package/dist/deck/components/DeckImageGrid.d.ts.map +0 -1
- package/dist/deck/components/DeckRichText.d.ts +0 -22
- package/dist/deck/components/DeckRichText.d.ts.map +0 -1
- package/dist/deck/components/DeckVideo.d.ts +0 -15
- package/dist/deck/components/DeckVideo.d.ts.map +0 -1
- package/dist/deck/editing.d.ts +0 -50
- package/dist/deck/editing.d.ts.map +0 -1
- package/dist/deck/flip.d.ts +0 -11
- package/dist/deck/flip.d.ts.map +0 -1
- package/dist/deck/index.d.ts +0 -24
- package/dist/deck/index.d.ts.map +0 -1
- package/dist/deck/index.js +0 -51
- package/dist/deck/layouts/CenteredTextLayout.d.ts +0 -12
- package/dist/deck/layouts/CenteredTextLayout.d.ts.map +0 -1
- package/dist/deck/layouts/ColumnSplitLayout.d.ts +0 -14
- package/dist/deck/layouts/ColumnSplitLayout.d.ts.map +0 -1
- package/dist/deck/layouts/FullBleedImageLayout.d.ts +0 -14
- package/dist/deck/layouts/FullBleedImageLayout.d.ts.map +0 -1
- package/dist/deck/media.d.ts +0 -33
- package/dist/deck/media.d.ts.map +0 -1
- package/dist/deck/tokens.d.ts +0 -22
- package/dist/deck/tokens.d.ts.map +0 -1
- package/dist/deck/useStageScale.d.ts +0 -10
- package/dist/deck/useStageScale.d.ts.map +0 -1
- package/dist/deck-builder/DeckBuilderApp.d.ts +0 -30
- package/dist/deck-builder/DeckBuilderApp.d.ts.map +0 -1
- package/dist/deck-builder/DeckBuilderTopBar.d.ts +0 -24
- package/dist/deck-builder/DeckBuilderTopBar.d.ts.map +0 -1
- package/dist/deck-builder/DeckChatPanel.d.ts +0 -45
- package/dist/deck-builder/DeckChatPanel.d.ts.map +0 -1
- package/dist/deck-builder/DeckEditor.d.ts +0 -37
- package/dist/deck-builder/DeckEditor.d.ts.map +0 -1
- package/dist/deck-builder/DeckLibrary.d.ts +0 -22
- package/dist/deck-builder/DeckLibrary.d.ts.map +0 -1
- package/dist/deck-builder/DeckSlideSurface.d.ts +0 -31
- package/dist/deck-builder/DeckSlideSurface.d.ts.map +0 -1
- package/dist/deck-builder/deck-api.d.ts +0 -76
- package/dist/deck-builder/deck-api.d.ts.map +0 -1
- package/dist/deck-builder/deck-chat-client.d.ts +0 -44
- package/dist/deck-builder/deck-chat-client.d.ts.map +0 -1
- package/dist/deck-builder/deck-chat-history.d.ts +0 -10
- package/dist/deck-builder/deck-chat-history.d.ts.map +0 -1
- package/dist/deck-builder/index.d.ts +0 -13
- package/dist/deck-builder/index.d.ts.map +0 -1
- package/dist/deck-builder/index.js +0 -1498
- package/dist/deck-builder/starter-deck.d.ts +0 -23
- package/dist/deck-builder/starter-deck.d.ts.map +0 -1
- package/dist/deck-builder/types.d.ts +0 -29
- package/dist/deck-builder/types.d.ts.map +0 -1
- package/dist/deck-builder/useSortableSlide.d.ts +0 -29
- package/dist/deck-builder/useSortableSlide.d.ts.map +0 -1
- package/dist/document/assets/fonts/albra-grotesk-bold.woff +0 -0
- package/dist/document/assets/fonts/albra-grotesk-regular.woff +0 -0
- package/dist/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
- package/dist/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
- package/dist/document/components/DocCoverPage.d.ts +0 -11
- package/dist/document/components/DocCoverPage.d.ts.map +0 -1
- package/dist/document/components/DocPage.d.ts +0 -10
- package/dist/document/components/DocPage.d.ts.map +0 -1
- package/dist/document/components/DocRail.d.ts +0 -28
- package/dist/document/components/DocRail.d.ts.map +0 -1
- package/dist/document/components/DocSectionPage.d.ts +0 -12
- package/dist/document/components/DocSectionPage.d.ts.map +0 -1
- package/dist/document/components/DocViewer.d.ts +0 -21
- package/dist/document/components/DocViewer.d.ts.map +0 -1
- package/dist/document/components/Wordmark.d.ts +0 -4
- package/dist/document/components/Wordmark.d.ts.map +0 -1
- package/dist/document/components/measure.d.ts +0 -7
- package/dist/document/components/measure.d.ts.map +0 -1
- package/dist/document/components/useDocPages.d.ts +0 -12
- package/dist/document/components/useDocPages.d.ts.map +0 -1
- package/dist/document/engine/blocks.d.ts +0 -2
- package/dist/document/engine/blocks.d.ts.map +0 -1
- package/dist/document/engine/harvest.d.ts +0 -3
- package/dist/document/engine/harvest.d.ts.map +0 -1
- package/dist/document/engine/masonry.d.ts +0 -26
- package/dist/document/engine/masonry.d.ts.map +0 -1
- package/dist/document/engine/paginate.d.ts +0 -43
- package/dist/document/engine/paginate.d.ts.map +0 -1
- package/dist/document/engine/settings.d.ts +0 -91
- package/dist/document/engine/settings.d.ts.map +0 -1
- package/dist/document/engine/types.d.ts +0 -44
- package/dist/document/engine/types.d.ts.map +0 -1
- package/dist/document/index.d.ts +0 -13
- package/dist/document/index.d.ts.map +0 -1
- package/dist/document/index.js +0 -1030
- package/dist/document/styles/document.css +0 -217
- package/dist/lib/deck-access.d.ts +0 -86
- package/dist/lib/deck-access.d.ts.map +0 -1
- package/dist/lib/deck-access.js +0 -58
- package/dist/lib/deck-dexie.d.ts +0 -88
- package/dist/lib/deck-dexie.d.ts.map +0 -1
- package/dist/lib/deck-dexie.js +0 -22
- package/dist/lib/deck-edit.d.ts +0 -67
- package/dist/lib/deck-edit.d.ts.map +0 -1
- package/dist/lib/deck-media-assets.d.ts +0 -27
- package/dist/lib/deck-media-assets.d.ts.map +0 -1
- package/dist/lib/deck-media-assets.js +0 -25
- package/dist/lib/deck-media-refs.d.ts +0 -35
- package/dist/lib/deck-media-refs.d.ts.map +0 -1
- package/dist/lib/deck-media-refs.js +0 -36
- package/dist/lib/deck-tool.d.ts +0 -25
- package/dist/lib/deck-tool.d.ts.map +0 -1
- package/dist/lib/deck-tool.js +0 -58
- package/dist/lib/document-input.d.ts +0 -25
- package/dist/lib/document-input.d.ts.map +0 -1
- package/dist/schemas/deck-meta.d.ts +0 -85
- package/dist/schemas/deck-meta.d.ts.map +0 -1
- package/dist/schemas/deck-meta.js +0 -64
- package/dist/schemas/deck.d.ts +0 -954
- package/dist/schemas/deck.d.ts.map +0 -1
- package/dist/schemas/deck.js +0 -65
- package/src/components/shell/DocInspector.tsx +0 -314
- package/src/components/shell/DocumentEditorOverlay.tsx +0 -179
- package/src/components/shell/DocumentPresentOverlay.tsx +0 -87
- package/src/components/shell/SiteSettingsDecks.tsx +0 -41
- package/src/deck/DeckComponentView.tsx +0 -28
- package/src/deck/DeckPlayer.tsx +0 -166
- package/src/deck/DeckSlideView.tsx +0 -38
- package/src/deck/DeckStage.tsx +0 -39
- package/src/deck/components/DeckImage.tsx +0 -38
- package/src/deck/components/DeckImageGrid.tsx +0 -254
- package/src/deck/components/DeckRichText.tsx +0 -111
- package/src/deck/components/DeckVideo.tsx +0 -81
- package/src/deck/editing.tsx +0 -201
- package/src/deck/flip.ts +0 -70
- package/src/deck/index.ts +0 -30
- package/src/deck/layouts/CenteredTextLayout.tsx +0 -29
- package/src/deck/layouts/ColumnSplitLayout.tsx +0 -69
- package/src/deck/layouts/FullBleedImageLayout.tsx +0 -58
- package/src/deck/media.tsx +0 -46
- package/src/deck/tokens.ts +0 -57
- package/src/deck/useStageScale.ts +0 -44
- package/src/deck-builder/DeckBuilderApp.tsx +0 -572
- package/src/deck-builder/DeckBuilderTopBar.tsx +0 -62
- package/src/deck-builder/DeckChatPanel.tsx +0 -334
- package/src/deck-builder/DeckEditor.tsx +0 -258
- package/src/deck-builder/DeckLibrary.tsx +0 -195
- package/src/deck-builder/DeckSlideSurface.tsx +0 -55
- package/src/deck-builder/deck-api.ts +0 -177
- package/src/deck-builder/deck-chat-client.ts +0 -129
- package/src/deck-builder/deck-chat-history.ts +0 -62
- package/src/deck-builder/index.ts +0 -12
- package/src/deck-builder/starter-deck.ts +0 -86
- package/src/deck-builder/types.ts +0 -24
- package/src/deck-builder/useSortableSlide.ts +0 -102
- package/src/document/assets/fonts/albra-grotesk-bold.woff +0 -0
- package/src/document/assets/fonts/albra-grotesk-regular.woff +0 -0
- package/src/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
- package/src/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
- package/src/document/components/DocCoverPage.tsx +0 -45
- package/src/document/components/DocPage.tsx +0 -40
- package/src/document/components/DocRail.tsx +0 -189
- package/src/document/components/DocSectionPage.tsx +0 -48
- package/src/document/components/DocViewer.tsx +0 -313
- package/src/document/components/Wordmark.tsx +0 -18
- package/src/document/components/measure.tsx +0 -133
- package/src/document/components/useDocPages.ts +0 -95
- package/src/document/engine/blocks.ts +0 -116
- package/src/document/engine/harvest.ts +0 -95
- package/src/document/engine/masonry.ts +0 -158
- package/src/document/engine/paginate.ts +0 -232
- package/src/document/engine/settings.ts +0 -125
- package/src/document/engine/types.ts +0 -64
- package/src/document/index.ts +0 -18
- package/src/document/styles/document.css +0 -217
- package/src/lib/deck-access.ts +0 -132
- package/src/lib/deck-dexie.ts +0 -190
- package/src/lib/deck-edit.ts +0 -196
- package/src/lib/deck-media-assets.ts +0 -57
- package/src/lib/deck-media-refs.ts +0 -67
- package/src/lib/deck-tool.ts +0 -95
- package/src/lib/document-input.ts +0 -57
- package/src/schemas/deck-meta.ts +0 -88
- package/src/schemas/deck.ts +0 -478
- package/dist/{chunk-DJABADOO.js → chunk-AJ2QOAZI.js} +3 -3
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
2
|
-
import type { DocumentOptions } from "../engine/settings";
|
|
3
|
-
import { settingsFor } from "../engine/settings";
|
|
4
|
-
import type { Chunk, DocImage, DocSourceContent } from "../engine/types";
|
|
5
|
-
import { DocCoverPage } from "./DocCoverPage";
|
|
6
|
-
import { DocRail, entryId, type RailEntry } from "./DocRail";
|
|
7
|
-
import { DocSectionPage, type DocHost } from "./DocSectionPage";
|
|
8
|
-
import { useDocPages } from "./useDocPages";
|
|
9
|
-
|
|
10
|
-
// 17in @96dpi = 1632×1056; the canvas scales the real page to fit its width.
|
|
11
|
-
const PAGE_W = 1632;
|
|
12
|
-
const PAGE_H = 1056;
|
|
13
|
-
|
|
14
|
-
export interface DocViewerProps {
|
|
15
|
-
source: DocSourceContent | null;
|
|
16
|
-
document: DocumentOptions; // { enabled, meta, cover, sections } — PageSchema.document shape
|
|
17
|
-
host: DocHost;
|
|
18
|
-
mode: "present" | "edit";
|
|
19
|
-
// edit mode only:
|
|
20
|
-
selectedChunkId?: string | null;
|
|
21
|
-
onSelectChunk?: (chunkId: string | null) => void; // null = cover selected
|
|
22
|
-
onClose?: () => void; // present mode: close control
|
|
23
|
-
/** Present mode only: when set, the toolbar shows a "Download PDF" link (the /present
|
|
24
|
-
* route passes the gated /api/document-pdf href when a PDF exists for this viewer). */
|
|
25
|
-
downloadHref?: string | null;
|
|
26
|
-
// Optional pagination-state hook (present mode unaffected — the present route
|
|
27
|
-
// never passes it). Fires, identity-guarded, whenever the internal useDocPages
|
|
28
|
-
// output changes, so a host (the edit overlay) can reuse this single pass for
|
|
29
|
-
// its inspector titles/warnings instead of running a second useDocPages.
|
|
30
|
-
onDocState?: (state: { chunks: Chunk[]; warningsByChunk: Record<string, string[]> }) => void;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Rail-entry id → the chunk it belongs to (null for the cover).
|
|
34
|
-
function chunkIdOf(id: string): string | null {
|
|
35
|
-
if (id === "cover") return null;
|
|
36
|
-
if (id.startsWith("ghost:")) return id.slice("ghost:".length);
|
|
37
|
-
if (id.includes("#")) return id.slice(0, id.lastIndexOf("#"));
|
|
38
|
-
return id;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function DocViewer({
|
|
42
|
-
source,
|
|
43
|
-
document: doc,
|
|
44
|
-
host,
|
|
45
|
-
mode,
|
|
46
|
-
selectedChunkId,
|
|
47
|
-
onSelectChunk,
|
|
48
|
-
onClose,
|
|
49
|
-
downloadHref,
|
|
50
|
-
onDocState,
|
|
51
|
-
}: DocViewerProps) {
|
|
52
|
-
const { chunks, pages, measuring, warningsByChunk } = useDocPages({ source, sections: doc.sections });
|
|
53
|
-
const [activeId, setActiveId] = useState<string>("cover");
|
|
54
|
-
const [railOpen, setRailOpen] = useState(true);
|
|
55
|
-
|
|
56
|
-
// Resolve the cover's media-library image once here; missing id or missing
|
|
57
|
-
// source entry (deleted image) both degrade to null → solid cover.
|
|
58
|
-
const coverImage: DocImage | null = doc.cover.imageId ? (source?.images[doc.cover.imageId] ?? null) : null;
|
|
59
|
-
|
|
60
|
-
// Publish the pagination output to an optional host, guarded so it only fires
|
|
61
|
-
// when the memoized chunks/warnings identities actually change (and reading
|
|
62
|
-
// onDocState via a ref so an unstable callback identity can't cause a loop).
|
|
63
|
-
const onDocStateRef = useRef(onDocState);
|
|
64
|
-
onDocStateRef.current = onDocState;
|
|
65
|
-
const lastEmitRef = useRef<{ chunks: Chunk[]; warningsByChunk: Record<string, string[]> } | null>(null);
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
if (lastEmitRef.current?.chunks === chunks && lastEmitRef.current?.warningsByChunk === warningsByChunk) return;
|
|
68
|
-
lastEmitRef.current = { chunks, warningsByChunk };
|
|
69
|
-
onDocStateRef.current?.({ chunks, warningsByChunk });
|
|
70
|
-
}, [chunks, warningsByChunk]);
|
|
71
|
-
|
|
72
|
-
// Present mode: excluded/empty chunks are simply absent. Edit mode: they
|
|
73
|
-
// surface as ghost entries (excluded takes priority over empty).
|
|
74
|
-
const entries: RailEntry[] = useMemo(() => {
|
|
75
|
-
const list: RailEntry[] = [{ kind: "cover" }];
|
|
76
|
-
for (const chunk of chunks) {
|
|
77
|
-
const s = settingsFor(doc.sections, chunk.id);
|
|
78
|
-
const empty = chunk.textBlocks.length === 0 && chunk.images.length === 0;
|
|
79
|
-
if (s.excluded || empty) {
|
|
80
|
-
if (mode === "edit") {
|
|
81
|
-
list.push({ kind: "ghost", chunkId: chunk.id, title: chunk.title, reason: s.excluded ? "excluded" : "empty", depth: chunk.depth });
|
|
82
|
-
}
|
|
83
|
-
} else {
|
|
84
|
-
for (const page of pages.filter((p) => p.chunkId === chunk.id)) list.push({ kind: "page", page });
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
return list;
|
|
88
|
-
}, [chunks, pages, doc.sections, mode]);
|
|
89
|
-
|
|
90
|
-
const selectEntry = useCallback(
|
|
91
|
-
(id: string) => {
|
|
92
|
-
setActiveId(id);
|
|
93
|
-
if (mode === "edit") onSelectChunk?.(chunkIdOf(id));
|
|
94
|
-
},
|
|
95
|
-
[mode, onSelectChunk],
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
// Stale active-entry guard: if the active entry vanished (chunk excluded, page
|
|
99
|
-
// count shrank), re-point to the matching ghost or the cover. Guarded on
|
|
100
|
-
// !measuring so transient empty-`pages` frames during layout don't churn it.
|
|
101
|
-
useEffect(() => {
|
|
102
|
-
if (measuring) return;
|
|
103
|
-
if (entries.some((e) => entryId(e) === activeId)) return;
|
|
104
|
-
const chunkId = chunkIdOf(activeId);
|
|
105
|
-
const ghost = entries.find((e) => e.kind === "ghost" && e.chunkId === chunkId);
|
|
106
|
-
setActiveId(ghost ? entryId(ghost) : "cover");
|
|
107
|
-
}, [entries, activeId, measuring]);
|
|
108
|
-
|
|
109
|
-
// Controlled selection (edit): follow an externally-driven selectedChunkId.
|
|
110
|
-
// Undefined means uncontrolled — the viewer owns selection outright.
|
|
111
|
-
useEffect(() => {
|
|
112
|
-
if (mode !== "edit" || selectedChunkId === undefined) return;
|
|
113
|
-
if (chunkIdOf(activeId) === selectedChunkId) return;
|
|
114
|
-
if (selectedChunkId === null) {
|
|
115
|
-
setActiveId("cover");
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const match = entries.find(
|
|
119
|
-
(e) =>
|
|
120
|
-
(e.kind === "page" && e.page.chunkId === selectedChunkId) ||
|
|
121
|
-
(e.kind === "ghost" && e.chunkId === selectedChunkId),
|
|
122
|
-
);
|
|
123
|
-
if (match) setActiveId(entryId(match));
|
|
124
|
-
}, [selectedChunkId, mode, entries, activeId]);
|
|
125
|
-
|
|
126
|
-
// Arrow-key paging on `window`. Ignores events targeting form fields, clamps
|
|
127
|
-
// at the ends, preventDefault only when it consumes the key, listener removed
|
|
128
|
-
// on unmount. A ref keeps the handler reading the live active id without
|
|
129
|
-
// resubscribing on every step.
|
|
130
|
-
const activeIdRef = useRef(activeId);
|
|
131
|
-
activeIdRef.current = activeId;
|
|
132
|
-
useEffect(() => {
|
|
133
|
-
// Present mode only. In the editor overlay the same keys belong to the
|
|
134
|
-
// surface the author is working in, and a window-level listener stole them
|
|
135
|
-
// to flip the previewed page instead.
|
|
136
|
-
if (mode !== "present") return;
|
|
137
|
-
const onKey = (e: KeyboardEvent) => {
|
|
138
|
-
const t = e.target as HTMLElement | null;
|
|
139
|
-
if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT" || t.isContentEditable)) return;
|
|
140
|
-
let dir = 0;
|
|
141
|
-
if (e.key === "ArrowRight" || e.key === "ArrowDown") dir = 1;
|
|
142
|
-
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") dir = -1;
|
|
143
|
-
else return;
|
|
144
|
-
e.preventDefault();
|
|
145
|
-
const ids = entries.map(entryId);
|
|
146
|
-
const cur = Math.max(0, ids.indexOf(activeIdRef.current));
|
|
147
|
-
const next = Math.min(ids.length - 1, Math.max(0, cur + dir));
|
|
148
|
-
if (ids[next] !== activeIdRef.current) selectEntry(ids[next]);
|
|
149
|
-
};
|
|
150
|
-
window.addEventListener("keydown", onKey);
|
|
151
|
-
return () => window.removeEventListener("keydown", onKey);
|
|
152
|
-
}, [entries, selectEntry, mode]);
|
|
153
|
-
|
|
154
|
-
const active = entries.find((e) => entryId(e) === activeId) ?? null;
|
|
155
|
-
|
|
156
|
-
return (
|
|
157
|
-
// min-w-0 (here and on the rail/canvas row): without it these flex items
|
|
158
|
-
// refuse to shrink below their content width, and opening the rail shoves
|
|
159
|
-
// the overlay's inspector off the right edge instead of narrowing the
|
|
160
|
-
// canvas.
|
|
161
|
-
<div className="flex h-full min-h-0 min-w-0 flex-1 flex-col">
|
|
162
|
-
{mode === "present" ? (
|
|
163
|
-
<div className="flex items-center justify-between border-b border-base-200 bg-base px-4 py-2">
|
|
164
|
-
<span className="text-sm font-medium text-base-contrast">{source?.pageTitle ?? "Document"}</span>
|
|
165
|
-
<div className="flex items-center gap-2">
|
|
166
|
-
{downloadHref ? (
|
|
167
|
-
<a
|
|
168
|
-
href={downloadHref}
|
|
169
|
-
download
|
|
170
|
-
aria-label="Download PDF"
|
|
171
|
-
className="rounded border border-base-200 px-3 py-1 text-xs font-medium text-base-contrast hover:bg-base-accent/30"
|
|
172
|
-
>
|
|
173
|
-
Download PDF
|
|
174
|
-
</a>
|
|
175
|
-
) : null}
|
|
176
|
-
{onClose ? (
|
|
177
|
-
<button
|
|
178
|
-
type="button"
|
|
179
|
-
aria-label="Close"
|
|
180
|
-
onClick={onClose}
|
|
181
|
-
className="rounded border border-base-200 px-3 py-1 text-xs font-medium text-base-contrast hover:bg-base-accent/30"
|
|
182
|
-
>
|
|
183
|
-
✕
|
|
184
|
-
</button>
|
|
185
|
-
) : null}
|
|
186
|
-
</div>
|
|
187
|
-
</div>
|
|
188
|
-
) : null}
|
|
189
|
-
<div className="flex min-h-0 min-w-0 flex-1">
|
|
190
|
-
{railOpen ? (
|
|
191
|
-
<DocRail
|
|
192
|
-
entries={entries}
|
|
193
|
-
selectedId={activeId}
|
|
194
|
-
meta={doc.meta}
|
|
195
|
-
cover={doc.cover}
|
|
196
|
-
coverImage={coverImage}
|
|
197
|
-
host={host}
|
|
198
|
-
onSelect={selectEntry}
|
|
199
|
-
onCollapse={() => setRailOpen(false)}
|
|
200
|
-
/>
|
|
201
|
-
) : (
|
|
202
|
-
<div className="flex w-10 shrink-0 flex-col border-r border-neutral-200 bg-neutral-50">
|
|
203
|
-
<button
|
|
204
|
-
type="button"
|
|
205
|
-
aria-label="Expand page rail"
|
|
206
|
-
onClick={() => setRailOpen(true)}
|
|
207
|
-
className="px-2 py-3 text-lg leading-none text-neutral-500 hover:text-neutral-800"
|
|
208
|
-
>
|
|
209
|
-
›
|
|
210
|
-
</button>
|
|
211
|
-
</div>
|
|
212
|
-
)}
|
|
213
|
-
<DocCanvas
|
|
214
|
-
active={active}
|
|
215
|
-
cover={doc.cover}
|
|
216
|
-
coverImage={coverImage}
|
|
217
|
-
meta={doc.meta}
|
|
218
|
-
host={host}
|
|
219
|
-
measuring={measuring}
|
|
220
|
-
noContent={chunks.length === 0}
|
|
221
|
-
/>
|
|
222
|
-
</div>
|
|
223
|
-
</div>
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function DocCanvas({
|
|
228
|
-
active,
|
|
229
|
-
cover,
|
|
230
|
-
coverImage,
|
|
231
|
-
meta,
|
|
232
|
-
host,
|
|
233
|
-
measuring,
|
|
234
|
-
noContent,
|
|
235
|
-
}: {
|
|
236
|
-
active: RailEntry | null;
|
|
237
|
-
cover: DocumentOptions["cover"];
|
|
238
|
-
coverImage: DocImage | null;
|
|
239
|
-
meta: DocumentOptions["meta"];
|
|
240
|
-
host: DocHost;
|
|
241
|
-
measuring: boolean;
|
|
242
|
-
noContent: boolean;
|
|
243
|
-
}) {
|
|
244
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
245
|
-
const [scale, setScale] = useState(0.4);
|
|
246
|
-
useEffect(() => {
|
|
247
|
-
const el = containerRef.current;
|
|
248
|
-
if (!el || typeof ResizeObserver === "undefined") return;
|
|
249
|
-
const observer = new ResizeObserver(() => {
|
|
250
|
-
// Fill the canvas: fit both dimensions (clientWidth/Height include the
|
|
251
|
-
// p-6 padding — subtract it) and take the limiting axis. The page keeps
|
|
252
|
-
// its true 1632×1056 layout and is transform-scaled, so what's shown is
|
|
253
|
-
// byte-identical to the print layout at any size.
|
|
254
|
-
const availW = el.clientWidth - 48;
|
|
255
|
-
const availH = el.clientHeight - 48;
|
|
256
|
-
setScale(Math.max(0.15, Math.min(availW / PAGE_W, availH / PAGE_H)));
|
|
257
|
-
});
|
|
258
|
-
observer.observe(el);
|
|
259
|
-
return () => observer.disconnect();
|
|
260
|
-
}, []);
|
|
261
|
-
|
|
262
|
-
const note = (text: string) => <p className="text-sm text-neutral-400">{text}</p>;
|
|
263
|
-
|
|
264
|
-
let body: ReactNode;
|
|
265
|
-
if (active?.kind === "cover") {
|
|
266
|
-
body = noContent ? (
|
|
267
|
-
<div className="flex flex-col items-center gap-4">
|
|
268
|
-
<Scaled scale={scale}>
|
|
269
|
-
<DocCoverPage cover={cover} meta={meta} image={coverImage} host={host} />
|
|
270
|
-
</Scaled>
|
|
271
|
-
<p className="text-sm text-neutral-400">This page has no displayable content.</p>
|
|
272
|
-
</div>
|
|
273
|
-
) : (
|
|
274
|
-
<Scaled scale={scale}>
|
|
275
|
-
<DocCoverPage cover={cover} meta={meta} image={coverImage} host={host} />
|
|
276
|
-
</Scaled>
|
|
277
|
-
);
|
|
278
|
-
} else if (active?.kind === "page") {
|
|
279
|
-
body = (
|
|
280
|
-
<Scaled scale={scale}>
|
|
281
|
-
<DocSectionPage page={active.page} meta={meta} host={host} />
|
|
282
|
-
</Scaled>
|
|
283
|
-
);
|
|
284
|
-
} else if (active?.kind === "ghost") {
|
|
285
|
-
body = note(active.reason === "excluded" ? "Excluded — re-include from the inspector" : "No displayable content");
|
|
286
|
-
} else {
|
|
287
|
-
// Active id resolved to nothing — its page is still being measured.
|
|
288
|
-
body = measuring ? note("Laying out…") : note("This page has no displayable content.");
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return (
|
|
292
|
-
<div
|
|
293
|
-
ref={containerRef}
|
|
294
|
-
data-testid="doc-canvas"
|
|
295
|
-
className="flex min-h-0 min-w-0 flex-1 overflow-auto bg-neutral-100 p-6"
|
|
296
|
-
>
|
|
297
|
-
{/* m-auto instead of items/justify-center: centers both axes when the
|
|
298
|
-
page fits, but keeps the top/left edge reachable when it overflows
|
|
299
|
-
(flex centering clips overflow irretrievably; auto margins don't). */}
|
|
300
|
-
<div className="m-auto">{body}</div>
|
|
301
|
-
</div>
|
|
302
|
-
);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function Scaled({ scale, children }: { scale: number; children: ReactNode }) {
|
|
306
|
-
return (
|
|
307
|
-
<div className="shrink-0 overflow-hidden shadow-lg" style={{ width: PAGE_W * scale, height: PAGE_H * scale }}>
|
|
308
|
-
<div className="origin-top-left" style={{ transform: `scale(${scale})`, width: PAGE_W, height: PAGE_H }}>
|
|
309
|
-
{children}
|
|
310
|
-
</div>
|
|
311
|
-
</div>
|
|
312
|
-
);
|
|
313
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// Drawn script wordmark. Path data copied verbatim from
|
|
2
|
-
// ~/drawn/frontend/src/components/shell/Logo.astro — do not edit paths by hand.
|
|
3
|
-
export function Wordmark({ className }: { className?: string }) {
|
|
4
|
-
return (
|
|
5
|
-
<svg
|
|
6
|
-
viewBox="0 0 360 154.474"
|
|
7
|
-
fill="currentColor"
|
|
8
|
-
role="img"
|
|
9
|
-
aria-label="Drawn"
|
|
10
|
-
className={className}
|
|
11
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
12
|
-
>
|
|
13
|
-
<path d="M93.211 18.436c-2.177.868 2.024 1.375 6.374 5.14 3.913 3.392 5.542 6.526 8.126 5.794l.084-.019c15.002-4.018 30.605-4.343 45.642-3.985 37.577.895 76.5 13.868 127.052 10.736 52.074-3.226 64.063-33.293 64.423-35.34.335-1.903-4.473.223-6.71 1.341-2.461 1.23-12.375 22.056-56.817 24.605-54.58 3.13-76.949-13.42-127.948-16.105-24.157-1.272-44.308 1.912-60.225 7.832ZM353.301 104.05c-3.497-.813-8.22-.393-14.355 3.083a42.817 42.817 0 0 0-1.464.863l-.646.396c-.153.094-.302.192-.452.286-.185.121-.375.239-.56.36-.158.1-.315.202-.47.305-.18.119-.36.234-.539.354-.177.118-.353.235-.53.35l-.557.373c-.113.076-.23.15-.344.225-2.209 1.475-4.596 3.046-7.903 4.822l-.002-.001c-.178.093-.354.189-.534.285-.25.132-.504.264-.764.398-.28.145-.574.292-.87.442-.268.133-.537.269-.817.403-.32.16-.653.319-.988.477-.282.136-.561.27-.857.407a124.187 124.187 0 0 1-4.258 1.882c-22.372 9.426-46.637 12.894-83.75 9.349-37.286-3.562-76.22-13.87-126.593-10.74-51.887 3.228-63.837 33.296-64.195 35.346-.336 1.898 4.455-.225 6.685-1.345 2.45-1.228 12.332-22.055 56.612-24.603 54.385-3.132 84.076 13.469 131.52 17.447 34.494 2.89 59.914-3.453 77.117-14.422 3.42-2.183 6.945-4.758 10.342-7.353.093-.067.176-.134.264-.202.111-.085.217-.168.329-.25.563-.434 1.122-.87 1.68-1.302.098-.077.2-.152.3-.23 1.027-.8 2.038-1.593 3.021-2.37 2.518-1.98 4.866-3.835 6.939-5.395l.823-.614.382-.28c.344-.251.675-.49.999-.718a42.728 42.728 0 0 1 1.547-1.058c.07-.047.14-.084.206-.13.31-.197.608-.379.89-.544.094-.054.189-.11.281-.16a12.3 12.3 0 0 1 .933-.486c4.427-2.017 8.968-2.27 9.22 1.837.124 2.074-1.999 4.029-5.691 4.449-2.565.292-6.454.442-9.699-.803-.026-.012-.048-.012-.077-.023-.5-.17-1.841 1.036-2.762 2-.922.966-2.05 1.673-1.084 2.163 4.29 2.29 8.832 3.056 11.611 3.137 8.07.221 14.336-1.887 15.567-8.04.93-4.653-1.559-9.227-6.507-10.371Z" />
|
|
14
|
-
<path d="M340.048 55.675c-3.094-2.81-8.125-2.34-11.568-2.069-5.92.466-11.54 3.713-15.45 6.582-.477-3.394-1.684-5.828-3.908-6.64-6.393-2.34-11.45 2.035-16.42 5.387-1.95 1.254-4.322-4.012-5.878-4.895-1.55-.882-4.995-1.347-7.803.126-4.404 2.305-7.465 5.91-7.21 8.136.257 2.297 4.149 4.28 4.065 6.879-.13 4.019-2.04 7.87-4.668 13.225-1.998 4.068-4.108 7.762-6.38 10.711-1.537 1.954-3.631 4.144-6.278 5.748-.514.277-1.04.496-1.57.685-3.299.82-4.712-3.26-3.843-8.882 1.006-6.444 9.235-29.756 9.738-30.966.502-1.21-.382-1.121-2.396-1.49-2.015-.37-6.724-.041-8.45.241-1.727.279-5.21 15.477-7.354 22.253-2.143 6.778-5.96 14.898-8.942 17.284-2.568 2.058-4.896 3.11-7.137 2.31-6.262-2.243-2.25-19.315 1.117-28.63 3.801-10.515 9.353-20.009 6.934-19.91-5.368.224-11.976 2.01-13.28 4.058-2.33 3.673-8.074 21.308-9.999 26.677-1.072 2.989-1.506 6.316-1.412 9.579-1.124.544-6.17 6.036-12.888 5.767-7.49-.303 4.697-29.98 5.369-31.32.67-1.342-11.354-1.567-12.026-.222-.668 1.34-6.037 31.487-20.409 31.487-10.457 0-5.145-25.45 7.604-34.614 10.985-7.9 25.36-2.188 26.924-2.862 1.565-.668 4.17-9.668 4.17-10.786s-4.698-1.119-18.792-.222c-14.09.893-21.92 7.38-30.195 20.69-8.278 13.31-10.068 26.73-19.412 26.73-9.22 0-1.169-18.788 3.082-27.738 4.248-8.946 6.937-17.895 5.592-19.907-1.343-2.014-16.332-.46-17.673-1.58-1.344-1.119 1.121-3.12.45-4.913-.67-1.787-10.515-1.787-14.205 0-1.887.916-2.185 3.287-2.021 5.893 1.254 7.163 1.232 14.979-.318 22.603.064.302.197.384.438.138 2.586-2.63 8.876-11.227 9.55-12.12.669-.897 8.836-.71 9.239.043.645 1.203-6.796 17.224-8.723 22.59-3.61 10.057-2.573 16.894 2.973 20.234 4.967 2.996 9.777 2.584 16.73-1.624 6.977-4.222 9.599-6.526 9.599-6.526s0 14.087 15.437 14.087c11.406 0 14.089-8.048 14.089-8.048s1.797 10.698 12.414 9.087c6.176-.937 11.652-9.47 13.41-12.763 1.045 6.066 3.97 11.25 8.174 12.625 5.887 1.922 13.06.388 19.818-4.645a15.195 15.195 0 0 0 2.495-2.362c2.356 5.703 5.865 9.538 9.681 10.008 19.478 2.399 28.458-19.807 33.299-34.217.993-2.955 1.71-5.846 2.654-9.364.268-1.007 1.183-2.504 2.994-2.939 4.188-.933 7.848-1.282 8.583.371 3.784 8.503-13.171 43.36-10.747 43.36 3.3 0 10.047-1.037 12.48-4.353 4.145-5.64 10.948-25.812 11.45-39.149 2.636-2.101 5.458-3.664 8.179-3.883 9.395-.757 6.167 14.162 3.22 22.537-4.906 13.966-3.351 22.982.632 28.735.709.9 1.775-.014 3.49-.917 1.718-.906 2.7-1.413 2.527-2.26-.011-.062-.019-.12-.029-.183-1.323-5.592 3.407-13.519 7.318-24.085 3.762-10.158 7.001-24.749 1.465-29.782ZM46.09 89.63c1.282-2.736 2.616-5.56 4.002-8.468C56.528 67.684 66.09 53.946 75.049 46.32a80.636 80.636 0 0 1 14.747-9.98c.07-.044.148-.092.221-.132 1.173-.631 1.17-1.394-.013-3.301-1.354-2.185-3.87-4.955-8.296-7.144-1.703-.842-3.224-.362-4.618.396-4.817 2.932-9.012 6.161-12.569 9.6C46.448 53.23 37.23 76.32 33.231 88.318l-1.407 4.22c-.406 1.22-.8 2.397-1.185 3.546l-.149.437c-.38 1.13-.748 2.214-1.11 3.27-.008.02-.014.043-.022.061-.993 2.89-1.932 5.525-2.846 7.885h-.01c-.22.568-.44 1.153-.66 1.689l-.006.015a79.329 79.329 0 0 1-1.105 2.609 51.44 51.44 0 0 1-1.022 2.16l-.106.22c-2.926 5.805-5.963 8.77-10.193 8.77-8.5 0-6.377-11.156 6.71-15.516a18.611 18.611 0 0 1 3.791-.83c.064-.01.118-.024.188-.035 1.761-.268 1.54-1.49 2.059-3.246.376-1.273 1.282-2.543-.193-2.404a57.899 57.899 0 0 0-6.293 1.147c-23.152 5.477-24.366 25.123-11.446 28.413 9.656 2.457 17.26-3.099 23.006-11.475.24-.356.493-.743.75-1.154.084-.132.173-.28.26-.42.177-.29.354-.584.54-.901.102-.175.208-.362.313-.545.176-.305.355-.623.537-.95.11-.198.22-.393.33-.598.198-.364.4-.74.604-1.122.098-.188.197-.368.294-.557.304-.578.614-1.173.93-1.796.028-.052.055-.112.083-.164l.337-.668c1.406-2.791 2.94-5.988 4.603-9.504 1.64-3.475 3.4-7.248 5.277-11.245Z" />
|
|
15
|
-
<path d="M79.206 111.375c35.531-11.52 39.87-52.054 28.828-74.15a34.694 34.694 0 0 0-2.573-4.302c-4.379-6.306-10.605-10.865-17.802-14.027-5.346-2.35-11.22-3.94-17.254-4.914C43.94 9.708 6.14 18.749 5.74 39.95 5.354 60.374 30.548 57.9 32.533 58.025c2.247.14 1.678-1.678-.67-4.028-2.35-2.347-7.946-7.147-7.382-15.433.844-12.415 17.78-22.482 44.612-18.03 3.758.625 6.956 1.438 9.75 2.425 8.204 2.907 12.703 7.418 16.072 13.73a55.714 55.714 0 0 1 2.154 4.559c8.11 19.313-4.926 52.567-24.438 59.737-14.466 5.317-21.625 3.08-29.713 1.281-1.304-.287-1.495 1.185-2.543 3.446-1.047 2.264-2.092 3.906-1.052 4.293 9.395 3.44 20.77 7.563 39.883 1.37Z" />
|
|
16
|
-
</svg>
|
|
17
|
-
);
|
|
18
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { useEffect, useLayoutEffect, useMemo, useState } from "react";
|
|
2
|
-
import { DOC_GEOM } from "../engine/paginate";
|
|
3
|
-
import type { SectionSettings } from "../engine/settings";
|
|
4
|
-
import type { Chunk, TextBlock } from "../engine/types";
|
|
5
|
-
|
|
6
|
-
const PX_PER_IN = 96;
|
|
7
|
-
|
|
8
|
-
function widthInFor(settings: SectionSettings | undefined): number {
|
|
9
|
-
return settings?.textPlacement === "leftColumn" ? DOC_GEOM.leftColWIn : DOC_GEOM.bodyWIn;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function blockClassName(block: TextBlock): string {
|
|
13
|
-
if (block.kind === "heading") return block.level === 2 ? "pdoc-h2" : "pdoc-h3";
|
|
14
|
-
return "pdoc-prose";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Content that affects a block's measured height, serialized so the
|
|
18
|
-
// re-measure effect can key on structural equality rather than object
|
|
19
|
-
// identity (chunks are re-derived on every harvest() call). JSON-encoded —
|
|
20
|
-
// hand-joined delimiters can occur verbatim in content, letting two different
|
|
21
|
-
// block lists collide and the re-measure effect never fire.
|
|
22
|
-
function signatureFor(chunks: Chunk[], settingsMap: Record<string, SectionSettings>): string {
|
|
23
|
-
return JSON.stringify(
|
|
24
|
-
chunks.map((c) => [
|
|
25
|
-
c.id,
|
|
26
|
-
widthInFor(settingsMap[c.id]),
|
|
27
|
-
c.textBlocks.map((b) => (b.kind === "heading" ? ["h", b.level, b.text] : ["p", b.html])),
|
|
28
|
-
]),
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Measures every text block of every chunk by rendering it into a hidden,
|
|
33
|
-
// fixed-position container at the chunk's real text width with the real
|
|
34
|
-
// pdoc-* classes (see document.css `.pdoc-measure`) — plain DOM nodes, not a
|
|
35
|
-
// React subtree, since measurement needs no reconciliation or events, only
|
|
36
|
-
// the browser's own layout of the real markup shape. Blocks get no CSS
|
|
37
|
-
// margins (pagination owns inter-block spacing via `gapIn`), so a wrapper's
|
|
38
|
-
// offsetHeight is the whole truth.
|
|
39
|
-
export function useTextMeasurements(
|
|
40
|
-
chunks: Chunk[],
|
|
41
|
-
settingsMap: Record<string, SectionSettings>,
|
|
42
|
-
): { heights: Record<string, number[]> | null; textWidths: Record<string, number> | null } {
|
|
43
|
-
const [heights, setHeights] = useState<Record<string, number[]> | null>(null);
|
|
44
|
-
const [textWidths, setTextWidths] = useState<Record<string, number> | null>(null);
|
|
45
|
-
const [fontsReady, setFontsReady] = useState(false);
|
|
46
|
-
|
|
47
|
-
// jsdom (tests) has no document.fonts — optional-chain so this resolves
|
|
48
|
-
// immediately there instead of hanging measurement forever.
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
let cancelled = false;
|
|
51
|
-
const markReady = () => {
|
|
52
|
-
if (!cancelled) setFontsReady(true);
|
|
53
|
-
};
|
|
54
|
-
// A rejected `fonts.ready` (e.g. a font failing to load) must not wedge
|
|
55
|
-
// `measuring` true forever — fall back to treating fonts as ready so
|
|
56
|
-
// measurement still runs with whatever's loaded.
|
|
57
|
-
Promise.resolve(document.fonts?.ready).then(markReady).catch(markReady);
|
|
58
|
-
return () => {
|
|
59
|
-
cancelled = true;
|
|
60
|
-
};
|
|
61
|
-
}, []);
|
|
62
|
-
|
|
63
|
-
// Memoized: this walks every chunk's full prose HTML through JSON.stringify,
|
|
64
|
-
// and it ran on every DocViewer render. Both inputs are memoized upstream
|
|
65
|
-
// (useDocPages), so the identity check actually holds.
|
|
66
|
-
const signature = useMemo(() => signatureFor(chunks, settingsMap), [chunks, settingsMap]);
|
|
67
|
-
|
|
68
|
-
useLayoutEffect(() => {
|
|
69
|
-
if (!fontsReady) return;
|
|
70
|
-
const containers: HTMLDivElement[] = [];
|
|
71
|
-
const next: Record<string, number[]> = {};
|
|
72
|
-
const nextW: Record<string, number> = {};
|
|
73
|
-
|
|
74
|
-
// WRITE PASS. Every wrapper is appended before anything is read: interleaving
|
|
75
|
-
// the two forced a synchronous reflow per text block, so a 200-block document
|
|
76
|
-
// flushed layout ~400 times inside this one layout effect.
|
|
77
|
-
const pending: { chunkId: string; wrappers: HTMLDivElement[] }[] = [];
|
|
78
|
-
for (const chunk of chunks) {
|
|
79
|
-
if (chunk.textBlocks.length === 0) {
|
|
80
|
-
next[chunk.id] = [];
|
|
81
|
-
nextW[chunk.id] = 0;
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
const container = document.createElement("div");
|
|
85
|
-
container.className = "pdoc-measure";
|
|
86
|
-
container.style.width = `${widthInFor(settingsMap[chunk.id])}in`;
|
|
87
|
-
document.body.appendChild(container);
|
|
88
|
-
containers.push(container);
|
|
89
|
-
|
|
90
|
-
const wrappers: HTMLDivElement[] = [];
|
|
91
|
-
for (const block of chunk.textBlocks) {
|
|
92
|
-
const wrapper = document.createElement("div");
|
|
93
|
-
wrapper.className = blockClassName(block);
|
|
94
|
-
if (block.kind === "heading") {
|
|
95
|
-
wrapper.textContent = block.text;
|
|
96
|
-
} else {
|
|
97
|
-
wrapper.innerHTML = block.html;
|
|
98
|
-
}
|
|
99
|
-
// max-content + max-width:100% = shrink-to-fit capped at the column:
|
|
100
|
-
// wrapping blocks report the full cap (same wrap as before -> heights
|
|
101
|
-
// unchanged); short blocks report their natural width.
|
|
102
|
-
wrapper.style.width = "max-content";
|
|
103
|
-
wrapper.style.maxWidth = "100%";
|
|
104
|
-
container.appendChild(wrapper);
|
|
105
|
-
wrappers.push(wrapper);
|
|
106
|
-
}
|
|
107
|
-
pending.push({ chunkId: chunk.id, wrappers });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// READ PASS. One layout for the whole document.
|
|
111
|
-
for (const { chunkId, wrappers } of pending) {
|
|
112
|
-
const measured: number[] = [];
|
|
113
|
-
let wMax = 0;
|
|
114
|
-
for (const wrapper of wrappers) {
|
|
115
|
-
measured.push(wrapper.offsetHeight / PX_PER_IN);
|
|
116
|
-
wMax = Math.max(wMax, wrapper.getBoundingClientRect().width / PX_PER_IN);
|
|
117
|
-
}
|
|
118
|
-
next[chunkId] = measured;
|
|
119
|
-
nextW[chunkId] = wMax;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
setHeights(next);
|
|
123
|
-
setTextWidths(nextW);
|
|
124
|
-
return () => {
|
|
125
|
-
for (const container of containers) container.remove();
|
|
126
|
-
};
|
|
127
|
-
// Re-measure only when chunk/width content actually changes, or once
|
|
128
|
-
// fonts finish loading (font swap changes heights).
|
|
129
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
130
|
-
}, [signature, fontsReady]);
|
|
131
|
-
|
|
132
|
-
return { heights, textWidths };
|
|
133
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
2
|
-
import { harvest } from "../engine/harvest";
|
|
3
|
-
import { DOC_GEOM, paginateChunk, type DerivedPage } from "../engine/paginate";
|
|
4
|
-
import { settingsFor, settingsIssuesFor, type SectionSettings } from "../engine/settings";
|
|
5
|
-
import type { Chunk, DocSourceContent, SplitAt } from "../engine/types";
|
|
6
|
-
import { useTextMeasurements } from "./measure";
|
|
7
|
-
|
|
8
|
-
export function useDocPages(input: {
|
|
9
|
-
source: DocSourceContent | null;
|
|
10
|
-
sections: Record<string, unknown>;
|
|
11
|
-
}): { chunks: Chunk[]; pages: DerivedPage[]; measuring: boolean; warningsByChunk: Record<string, string[]> } {
|
|
12
|
-
const { source, sections } = input;
|
|
13
|
-
|
|
14
|
-
// splitAt drives harvesting, so chunks must re-derive when it changes — but
|
|
15
|
-
// ONLY then. Keying on a JSON signature of just the non-default splitAt
|
|
16
|
-
// entries keeps chunk identities stable across unrelated settings edits
|
|
17
|
-
// (exclude, layout), which would otherwise churn measurement and onDocState.
|
|
18
|
-
const splitSig = useMemo(() => {
|
|
19
|
-
const m: Record<string, SplitAt> = {};
|
|
20
|
-
for (const id of Object.keys(sections ?? {})) {
|
|
21
|
-
const v = settingsFor(sections, id).splitAt;
|
|
22
|
-
if (v !== "none") m[id] = v;
|
|
23
|
-
}
|
|
24
|
-
return JSON.stringify(m);
|
|
25
|
-
}, [sections]);
|
|
26
|
-
|
|
27
|
-
const splitAtFor = useMemo(() => {
|
|
28
|
-
const m = JSON.parse(splitSig) as Record<string, SplitAt>;
|
|
29
|
-
return (id: string) => m[id] ?? "none";
|
|
30
|
-
}, [splitSig]);
|
|
31
|
-
|
|
32
|
-
const chunks = useMemo(
|
|
33
|
-
() => (source ? harvest(source.sections, source.pageTitle, source.images, splitAtFor) : []),
|
|
34
|
-
[source, splitAtFor],
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
const settingsMap = useMemo(() => {
|
|
38
|
-
const map: Record<string, SectionSettings> = {};
|
|
39
|
-
for (const c of chunks) map[c.id] = settingsFor(sections, c.id);
|
|
40
|
-
return map;
|
|
41
|
-
}, [chunks, sections]);
|
|
42
|
-
|
|
43
|
-
const { heights, textWidths } = useTextMeasurements(chunks, settingsMap);
|
|
44
|
-
|
|
45
|
-
// `heights` is state set by an effect, so it always lags one render behind
|
|
46
|
-
// `chunks`: when `source` changes shape (a block added/removed, a new
|
|
47
|
-
// section), this memo can run with heights measured for the PREVIOUS chunk
|
|
48
|
-
// shape. Calling paginateChunk with a stale (mismatched-length) heights
|
|
49
|
-
// array is its documented throw condition, so each chunk is guarded
|
|
50
|
-
// independently — a chunk whose current heights aren't in yet contributes
|
|
51
|
-
// no pages (rather than crash) until the measurement effect catches up.
|
|
52
|
-
// Zero-text chunks have nothing to measure, so they always paginate.
|
|
53
|
-
const pages = useMemo(() => {
|
|
54
|
-
return chunks.flatMap((c) => {
|
|
55
|
-
const h = heights?.[c.id];
|
|
56
|
-
if (c.textBlocks.length > 0 && (!h || h.length !== c.textBlocks.length)) return [];
|
|
57
|
-
return paginateChunk(c, settingsMap[c.id], h ?? [], DOC_GEOM, textWidths?.[c.id]);
|
|
58
|
-
});
|
|
59
|
-
}, [chunks, settingsMap, heights, textWidths]);
|
|
60
|
-
|
|
61
|
-
// Mirrors the pages memo's per-chunk guard: still "measuring" until every
|
|
62
|
-
// non-empty chunk has current heights, not just until `heights` is non-null.
|
|
63
|
-
const measuring = useMemo(
|
|
64
|
-
() =>
|
|
65
|
-
chunks.some((c) => {
|
|
66
|
-
if (c.textBlocks.length === 0) return false;
|
|
67
|
-
const h = heights?.[c.id];
|
|
68
|
-
return !h || h.length !== c.textBlocks.length;
|
|
69
|
-
}),
|
|
70
|
-
[chunks, heights],
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
const warningsByChunk = useMemo(() => {
|
|
74
|
-
const map: Record<string, string[]> = {};
|
|
75
|
-
for (const c of chunks) {
|
|
76
|
-
const noContent = c.textBlocks.length === 0 && c.images.length === 0;
|
|
77
|
-
const pageWarnings = pages.filter((p) => p.chunkId === c.id).flatMap((p) => p.warnings);
|
|
78
|
-
// A stored setting the schema no longer recognises is reset to its
|
|
79
|
-
// default. Silent, that reads as the document ignoring an explicit
|
|
80
|
-
// choice — most visibly `excluded`, whose default puts the section back
|
|
81
|
-
// in the output.
|
|
82
|
-
const rejected = settingsIssuesFor(sections, c.id);
|
|
83
|
-
const settingWarnings = rejected.length > 0
|
|
84
|
-
? [`Unrecognised value reset to default: ${rejected.join(", ")}`]
|
|
85
|
-
: [];
|
|
86
|
-
const warnings = noContent
|
|
87
|
-
? ["Section has no content", ...settingWarnings, ...pageWarnings]
|
|
88
|
-
: [...settingWarnings, ...pageWarnings];
|
|
89
|
-
if (warnings.length > 0) map[c.id] = warnings;
|
|
90
|
-
}
|
|
91
|
-
return map;
|
|
92
|
-
}, [chunks, pages, sections]);
|
|
93
|
-
|
|
94
|
-
return { chunks, pages, measuring, warningsByChunk };
|
|
95
|
-
}
|