@drawnagency/primitives 0.1.80 → 0.1.81
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-KVQFMVXE.js → chunk-DKXISNJ2.js} +2 -2
- package/dist/{chunk-RSITF24B.js → chunk-EVNEMJZ6.js} +9 -3
- package/dist/{chunk-VI26O4E3.js → chunk-FDBZZ4AT.js} +1 -1
- package/dist/{chunk-QB5Q23G2.js → chunk-FN4NKUZU.js} +1 -1
- package/dist/{chunk-EMGLZQCM.js → chunk-MBJUWOM2.js} +1 -1
- package/dist/{chunk-4TVODHD2.js → chunk-P4ZNHZMP.js} +6 -6
- package/dist/{chunk-UNUJPLZV.js → chunk-PJOHAWNV.js} +1 -1
- package/dist/{chunk-2DDWWTRX.js → chunk-Q6LZOH5M.js} +1 -1
- package/dist/chunk-WP5UX4MI.js +63 -0
- package/dist/{chunk-3LNKOLZP.js → chunk-YKOLOZB2.js} +1 -1
- package/dist/{chunk-7XWER6YR.js → chunk-ZEXR7JYQ.js} +3 -3
- package/dist/components/editor/PagesModal.d.ts +2 -0
- package/dist/components/editor/PagesModal.d.ts.map +1 -1
- package/dist/components/editor/index.js +6 -5
- package/dist/components/primitives/index.js +7 -6
- package/dist/components/sections/brand-guide.js +7 -6
- package/dist/components/sections/register-schemas.js +10 -9
- package/dist/components/sections/register.js +10 -9
- package/dist/components/shell/DocInspector.d.ts +17 -0
- package/dist/components/shell/DocInspector.d.ts.map +1 -0
- package/dist/components/shell/DocumentEditorOverlay.d.ts +29 -0
- package/dist/components/shell/DocumentEditorOverlay.d.ts.map +1 -0
- package/dist/components/shell/DocumentPresentOverlay.d.ts +30 -0
- package/dist/components/shell/DocumentPresentOverlay.d.ts.map +1 -0
- package/dist/components/shell/EditorShell.d.ts +31 -0
- package/dist/components/shell/EditorShell.d.ts.map +1 -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 +11 -0
- package/dist/document/components/DocCoverPage.d.ts.map +1 -0
- package/dist/document/components/DocPage.d.ts +10 -0
- package/dist/document/components/DocPage.d.ts.map +1 -0
- package/dist/document/components/DocRail.d.ts +27 -0
- package/dist/document/components/DocRail.d.ts.map +1 -0
- package/dist/document/components/DocSectionPage.d.ts +12 -0
- package/dist/document/components/DocSectionPage.d.ts.map +1 -0
- package/dist/document/components/DocViewer.d.ts +18 -0
- package/dist/document/components/DocViewer.d.ts.map +1 -0
- package/dist/document/components/Wordmark.d.ts +4 -0
- package/dist/document/components/Wordmark.d.ts.map +1 -0
- package/dist/document/components/measure.d.ts +6 -0
- package/dist/document/components/measure.d.ts.map +1 -0
- package/dist/document/components/useDocPages.d.ts +12 -0
- package/dist/document/components/useDocPages.d.ts.map +1 -0
- package/dist/document/engine/blocks.d.ts +2 -0
- package/dist/document/engine/blocks.d.ts.map +1 -0
- package/dist/document/engine/harvest.d.ts +3 -0
- package/dist/document/engine/harvest.d.ts.map +1 -0
- package/dist/document/engine/masonry.d.ts +25 -0
- package/dist/document/engine/masonry.d.ts.map +1 -0
- package/dist/document/engine/paginate.d.ts +40 -0
- package/dist/document/engine/paginate.d.ts.map +1 -0
- package/dist/document/engine/settings.d.ts +70 -0
- package/dist/document/engine/settings.d.ts.map +1 -0
- package/dist/document/engine/types.d.ts +40 -0
- package/dist/document/engine/types.d.ts.map +1 -0
- package/dist/document/index.d.ts +13 -0
- package/dist/document/index.d.ts.map +1 -0
- package/dist/document/index.js +881 -0
- package/dist/document/styles/document.css +208 -0
- package/dist/index.js +29 -28
- package/dist/lib/document-input.d.ts +25 -0
- package/dist/lib/document-input.d.ts.map +1 -0
- package/dist/lib/index.js +10 -9
- package/dist/lib/pages.d.ts +2 -0
- package/dist/lib/pages.d.ts.map +1 -1
- package/dist/media/index.js +4 -3
- package/dist/schemas/index.js +3 -2
- package/dist/schemas/site-config.d.ts +223 -1
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/components/editor/PagesModal.tsx +16 -2
- package/src/components/shell/DocInspector.tsx +278 -0
- package/src/components/shell/DocumentEditorOverlay.tsx +169 -0
- package/src/components/shell/DocumentPresentOverlay.tsx +87 -0
- package/src/components/shell/EditorShell.tsx +128 -5
- 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 +45 -0
- package/src/document/components/DocPage.tsx +40 -0
- package/src/document/components/DocRail.tsx +160 -0
- package/src/document/components/DocSectionPage.tsx +45 -0
- package/src/document/components/DocViewer.tsx +285 -0
- package/src/document/components/Wordmark.tsx +18 -0
- package/src/document/components/measure.tsx +105 -0
- package/src/document/components/useDocPages.ts +67 -0
- package/src/document/engine/blocks.ts +87 -0
- package/src/document/engine/harvest.ts +80 -0
- package/src/document/engine/masonry.ts +89 -0
- package/src/document/engine/paginate.ts +209 -0
- package/src/document/engine/settings.ts +77 -0
- package/src/document/engine/types.ts +54 -0
- package/src/document/index.ts +18 -0
- package/src/document/styles/document.css +208 -0
- package/src/lib/document-input.ts +57 -0
- package/src/lib/pages.ts +22 -0
- package/src/schemas/site-config.ts +22 -7
- /package/dist/{chunk-4YEOTFVU.js → chunk-YBVK254P.js} +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import type { Page } from "../../schemas/site-config";
|
|
3
|
+
import type { MediaManifest } from "../../media/types";
|
|
4
|
+
import type { LoadedSection } from "../../lib/loader";
|
|
5
|
+
import {
|
|
6
|
+
DocViewer,
|
|
7
|
+
documentOptionsSchema,
|
|
8
|
+
sectionSettingsSchema,
|
|
9
|
+
settingsFor,
|
|
10
|
+
type Chunk,
|
|
11
|
+
type DocHost,
|
|
12
|
+
type DocumentOptions,
|
|
13
|
+
} from "../../document";
|
|
14
|
+
import { DocInspector, type DocInspectorProps } from "./DocInspector";
|
|
15
|
+
import { buildDraftDocumentInput } from "../../lib/document-input";
|
|
16
|
+
import { Button } from "../shared/Button";
|
|
17
|
+
// The sealed document stylesheet ships with its editor consumer (Task 6 decided
|
|
18
|
+
// the present route imports it from its island; this is the editor's island).
|
|
19
|
+
// Vitest stubs CSS imports, so it stays out of the jsdom test graph regardless.
|
|
20
|
+
import "../../document/styles/document.css";
|
|
21
|
+
|
|
22
|
+
export interface DocumentEditorOverlayProps {
|
|
23
|
+
/** The active page — its `document` block must be enabled for the FAB to mount. */
|
|
24
|
+
page: Page;
|
|
25
|
+
/**
|
|
26
|
+
* The page's draft sections, in `page.order` order. The overlay drops
|
|
27
|
+
* archived ones itself — /present renders only live sections, so a chunk the
|
|
28
|
+
* published document will never show must not influence the proofed layout.
|
|
29
|
+
*/
|
|
30
|
+
sections: LoadedSection[];
|
|
31
|
+
manifest: MediaManifest;
|
|
32
|
+
/** Wired by the shell to `setPageDocument` + the local-changes dirty flag. */
|
|
33
|
+
onUpdateDocument: (patch: Partial<DocumentOptions>) => void;
|
|
34
|
+
onClose: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Full-viewport editing surface: DocViewer (edit mode, ghosts + selection) on
|
|
39
|
+
* the left, DocInspector (settings for the current selection) on the right.
|
|
40
|
+
*
|
|
41
|
+
* The engine input is rebuilt from the live draft (page + in-memory sections +
|
|
42
|
+
* manifest) on every prop change, so unsaved edits repaginate immediately —
|
|
43
|
+
* the pagination hook's stale-heights guard absorbs the shape changes.
|
|
44
|
+
*/
|
|
45
|
+
export function DocumentEditorOverlay({
|
|
46
|
+
page,
|
|
47
|
+
sections,
|
|
48
|
+
manifest,
|
|
49
|
+
onUpdateDocument,
|
|
50
|
+
onClose,
|
|
51
|
+
}: DocumentEditorOverlayProps) {
|
|
52
|
+
// page.document is optional on the type; the caller only mounts this when it
|
|
53
|
+
// is enabled, but parse-with-defaults keeps us total either way.
|
|
54
|
+
const doc = useMemo(() => documentOptionsSchema.parse(page.document ?? {}), [page.document]);
|
|
55
|
+
|
|
56
|
+
const source = useMemo(
|
|
57
|
+
() =>
|
|
58
|
+
buildDraftDocumentInput({
|
|
59
|
+
page,
|
|
60
|
+
sections: sections.filter((s) => s.meta.status !== "archived"),
|
|
61
|
+
manifest,
|
|
62
|
+
}),
|
|
63
|
+
[page, sections, manifest],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Titles (for the inspector header) + per-chunk warnings come straight from
|
|
67
|
+
// DocViewer's single pagination pass via onDocState — no second useDocPages.
|
|
68
|
+
const [docState, setDocState] = useState<{ chunks: Chunk[]; warningsByChunk: Record<string, string[]> }>({
|
|
69
|
+
chunks: [],
|
|
70
|
+
warningsByChunk: {},
|
|
71
|
+
});
|
|
72
|
+
const { chunks, warningsByChunk } = docState;
|
|
73
|
+
|
|
74
|
+
// null = cover selected.
|
|
75
|
+
const [selectedChunkId, setSelectedChunkId] = useState<string | null>(null);
|
|
76
|
+
|
|
77
|
+
// All media bytes come from /api/media (draft branch bytes are served to
|
|
78
|
+
// editor sessions) — the same host shape the present route uses.
|
|
79
|
+
const host: DocHost = useMemo(
|
|
80
|
+
() => ({
|
|
81
|
+
variant: "preview",
|
|
82
|
+
imageSrc: (image, variant) =>
|
|
83
|
+
`/api/media/${image.imageId}/${variant === "print" ? image.printFile : image.previewFile}`,
|
|
84
|
+
}),
|
|
85
|
+
[],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Escape closes (the close button is the other exit). DocViewer owns arrow
|
|
89
|
+
// paging on its own window listener; Escape doesn't collide with it. An
|
|
90
|
+
// Escape aimed at an inspector control is the cancel-the-field gesture, not
|
|
91
|
+
// "leave the editor": text fields blur (so the next Escape closes), and a
|
|
92
|
+
// select is left alone entirely — the browser uses Escape to dismiss its
|
|
93
|
+
// popup and there's no way to tell whether one is open.
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const onKey = (e: KeyboardEvent) => {
|
|
96
|
+
if (e.key !== "Escape") return;
|
|
97
|
+
const t = e.target as HTMLElement | null;
|
|
98
|
+
if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) {
|
|
99
|
+
t.blur();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (t && t.tagName === "SELECT") return;
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
onClose();
|
|
105
|
+
};
|
|
106
|
+
window.addEventListener("keydown", onKey);
|
|
107
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
108
|
+
}, [onClose]);
|
|
109
|
+
|
|
110
|
+
const selection: DocInspectorProps["selection"] = useMemo(() => {
|
|
111
|
+
if (selectedChunkId === null) return { kind: "cover" };
|
|
112
|
+
const chunk = chunks.find((c) => c.id === selectedChunkId);
|
|
113
|
+
return { kind: "chunk", chunkId: selectedChunkId, title: chunk?.title ?? selectedChunkId };
|
|
114
|
+
}, [selectedChunkId, chunks]);
|
|
115
|
+
|
|
116
|
+
const warnings = selectedChunkId ? warningsByChunk[selectedChunkId] ?? [] : [];
|
|
117
|
+
|
|
118
|
+
// Merge the inspector's partial patch onto the chunk's CURRENT settings and
|
|
119
|
+
// ship the whole validated block — setPageDocument merges sections shallowly
|
|
120
|
+
// by key, so we must hand it a complete SectionSettings, not a fragment.
|
|
121
|
+
const handleUpdateSection = useCallback(
|
|
122
|
+
(chunkId: string, patch: Record<string, unknown>) => {
|
|
123
|
+
const merged = sectionSettingsSchema.parse({ ...settingsFor(doc.sections, chunkId), ...patch });
|
|
124
|
+
onUpdateDocument({ sections: { [chunkId]: merged } });
|
|
125
|
+
},
|
|
126
|
+
[doc.sections, onUpdateDocument],
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Cover/meta are single objects on the document; merge onto the current value
|
|
130
|
+
// so a one-field edit doesn't blank the siblings before setPageDocument runs.
|
|
131
|
+
const handleUpdateCover = useCallback(
|
|
132
|
+
(patch: Partial<DocumentOptions["cover"]>) => onUpdateDocument({ cover: { ...doc.cover, ...patch } }),
|
|
133
|
+
[doc.cover, onUpdateDocument],
|
|
134
|
+
);
|
|
135
|
+
const handleUpdateMeta = useCallback(
|
|
136
|
+
(patch: Partial<DocumentOptions["meta"]>) => onUpdateDocument({ meta: { ...doc.meta, ...patch } }),
|
|
137
|
+
[doc.meta, onUpdateDocument],
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className="fixed inset-0 z-50 flex flex-col bg-base" role="dialog" aria-modal="true" aria-label="Document editor">
|
|
142
|
+
<div className="flex items-center justify-between border-b border-base-200 bg-base px-4 py-2">
|
|
143
|
+
<span className="text-sm font-medium text-base-contrast">Document · {page.title}</span>
|
|
144
|
+
<Button variant="secondary" size="sm" aria-label="Close document editor" onClick={onClose}>
|
|
145
|
+
Close
|
|
146
|
+
</Button>
|
|
147
|
+
</div>
|
|
148
|
+
<div className="flex min-h-0 flex-1">
|
|
149
|
+
<DocViewer
|
|
150
|
+
source={source}
|
|
151
|
+
document={doc}
|
|
152
|
+
host={host}
|
|
153
|
+
mode="edit"
|
|
154
|
+
selectedChunkId={selectedChunkId}
|
|
155
|
+
onSelectChunk={setSelectedChunkId}
|
|
156
|
+
onDocState={setDocState}
|
|
157
|
+
/>
|
|
158
|
+
<DocInspector
|
|
159
|
+
document={doc}
|
|
160
|
+
selection={selection}
|
|
161
|
+
warnings={warnings}
|
|
162
|
+
onUpdateCover={handleUpdateCover}
|
|
163
|
+
onUpdateMeta={handleUpdateMeta}
|
|
164
|
+
onUpdateSection={handleUpdateSection}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useEffect, useMemo } from "react";
|
|
2
|
+
import type { Page } from "../../schemas/site-config";
|
|
3
|
+
import type { MediaManifest } from "../../media/types";
|
|
4
|
+
import type { LoadedSection } from "../../lib/loader";
|
|
5
|
+
import { DocViewer, documentOptionsSchema, type DocHost } from "../../document";
|
|
6
|
+
import { buildDraftDocumentInput } from "../../lib/document-input";
|
|
7
|
+
// Same sealed document stylesheet the editor overlay ships (see
|
|
8
|
+
// DocumentEditorOverlay). Vitest stubs CSS imports, so it stays out of the jsdom
|
|
9
|
+
// test graph regardless.
|
|
10
|
+
import "../../document/styles/document.css";
|
|
11
|
+
|
|
12
|
+
export interface DocumentPresentOverlayProps {
|
|
13
|
+
/** The active page — its `document` block supplies cover/meta/section settings. */
|
|
14
|
+
page: Page;
|
|
15
|
+
/**
|
|
16
|
+
* The sections currently on the canvas (already page-ordered and filtered to
|
|
17
|
+
* the branch being previewed). The shell derives these from the same
|
|
18
|
+
* `computeDisplaySections` call the history/branch canvas renders.
|
|
19
|
+
*/
|
|
20
|
+
sections: LoadedSection[];
|
|
21
|
+
manifest: MediaManifest;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read-only, full-viewport document PRESENTATION of the content currently being
|
|
27
|
+
* viewed — used when the shell is previewing a non-draft version (the published
|
|
28
|
+
* branch). It is `DocViewer` in `present` mode: no inspector, no update path.
|
|
29
|
+
*
|
|
30
|
+
* Product decision (Drew): history/branch views are the record — we present them
|
|
31
|
+
* but never open the editor over them. Document SETTINGS come from the current
|
|
32
|
+
* draft's `page.document`; the previewed branch's own document index is not
|
|
33
|
+
* carried in the shell's `viewSections`, so cover/meta/exclusions here reflect
|
|
34
|
+
* the draft, not the historical commit. This is an accepted approximation: we
|
|
35
|
+
* are rendering historical *content* with the draft's presentation settings.
|
|
36
|
+
*/
|
|
37
|
+
export function DocumentPresentOverlay({
|
|
38
|
+
page,
|
|
39
|
+
sections,
|
|
40
|
+
manifest,
|
|
41
|
+
onClose,
|
|
42
|
+
}: DocumentPresentOverlayProps) {
|
|
43
|
+
// page.document is optional on the type; the caller only mounts this when it
|
|
44
|
+
// is enabled, but parse-with-defaults keeps us total either way.
|
|
45
|
+
const doc = useMemo(() => documentOptionsSchema.parse(page.document ?? {}), [page.document]);
|
|
46
|
+
|
|
47
|
+
const source = useMemo(
|
|
48
|
+
() => buildDraftDocumentInput({ page, sections, manifest }),
|
|
49
|
+
[page, sections, manifest],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// All media bytes come from /api/media — the same host shape the present route
|
|
53
|
+
// and the editor overlay use.
|
|
54
|
+
const host: DocHost = useMemo(
|
|
55
|
+
() => ({
|
|
56
|
+
variant: "preview",
|
|
57
|
+
imageSrc: (image, variant) =>
|
|
58
|
+
`/api/media/${image.imageId}/${variant === "print" ? image.printFile : image.previewFile}`,
|
|
59
|
+
}),
|
|
60
|
+
[],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Escape closes, matching the editor overlay. DocViewer owns arrow paging on
|
|
64
|
+
// its own window listener; Escape doesn't collide with it.
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const onKey = (e: KeyboardEvent) => {
|
|
67
|
+
if (e.key !== "Escape") return;
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
onClose();
|
|
70
|
+
};
|
|
71
|
+
window.addEventListener("keydown", onKey);
|
|
72
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
73
|
+
}, [onClose]);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
className="fixed inset-0 z-50 flex flex-col bg-base"
|
|
78
|
+
role="dialog"
|
|
79
|
+
aria-modal="true"
|
|
80
|
+
aria-label="Document presentation"
|
|
81
|
+
>
|
|
82
|
+
<div className="flex min-h-0 flex-1">
|
|
83
|
+
<DocViewer source={source} document={doc} host={host} mode="present" onClose={onClose} />
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fragment, useState, useCallback, useEffect, useRef, useMemo, type ReactNode } from "react";
|
|
1
|
+
import { Fragment, Suspense, lazy, useState, useCallback, useEffect, useRef, useMemo, type ReactNode } from "react";
|
|
2
2
|
|
|
3
3
|
import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
4
4
|
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|
@@ -10,6 +10,10 @@ import type { SiteIndex, SiteConfig, Page } from "../../schemas/site-config";
|
|
|
10
10
|
import type { Audience } from "../../auth/types";
|
|
11
11
|
import type { MediaManifest } from "../../media/types";
|
|
12
12
|
import type { QueueItem } from "../../media/queue";
|
|
13
|
+
// Sanctioned outside->inside import: document/ is a sealed subtree, but the
|
|
14
|
+
// Document mode toggle wiring needs the patch shape it edits (mirrors the same
|
|
15
|
+
// sanctioned import in schemas/site-config.ts and lib/pages.ts).
|
|
16
|
+
import type { DocumentOptions } from "../../document/engine/settings";
|
|
13
17
|
import { SiteConfigSchema, IndexSchema } from "../../schemas/site-config";
|
|
14
18
|
import { ensureSanitizer } from "../../lib/sanitize";
|
|
15
19
|
import { EditorProvider, useEditorContext, type HistoryState } from "./EditorContext";
|
|
@@ -34,6 +38,17 @@ import { buildInsertMenuOptions, resolveInsertedContent } from "../../lib/insert
|
|
|
34
38
|
|
|
35
39
|
ensureSectionsRegistered();
|
|
36
40
|
import { BugReportFAB } from "./BugReportFAB";
|
|
41
|
+
// Lazy boundary: the document editor/presentation overlays (and the whole
|
|
42
|
+
// document engine they pull in) must not weigh /edit on sites that never use
|
|
43
|
+
// document mode — they load only when a document-enabled page opens the overlay.
|
|
44
|
+
// Named exports → adapt to the default-export shape React.lazy expects (mirrors
|
|
45
|
+
// the ChatPanel lazy import convention in ChatWidget).
|
|
46
|
+
const DocumentEditorOverlay = lazy(() =>
|
|
47
|
+
import("./DocumentEditorOverlay").then((m) => ({ default: m.DocumentEditorOverlay })),
|
|
48
|
+
);
|
|
49
|
+
const DocumentPresentOverlay = lazy(() =>
|
|
50
|
+
import("./DocumentPresentOverlay").then((m) => ({ default: m.DocumentPresentOverlay })),
|
|
51
|
+
);
|
|
37
52
|
import { SectionWrapper } from "../editor/SectionWrapper";
|
|
38
53
|
import { SectionOrderingModal } from "../editor/SectionOrderingModal";
|
|
39
54
|
import { MoveSectionModal } from "../editor/MoveSectionModal";
|
|
@@ -72,7 +87,7 @@ import { siteNavChangeEvent, pageSelectEvent, darkModeEvent, siteNameChangeEvent
|
|
|
72
87
|
import {
|
|
73
88
|
homePage, pageById, pageBySlug, pageDisplayTitle, addSectionToPage, removeSectionFromPages,
|
|
74
89
|
reorderSectionInPage,
|
|
75
|
-
setHomePage, setPageArchived, setPageFields, setPageAudience, deletePage, addPage, reorderPages,
|
|
90
|
+
setHomePage, setPageArchived, setPageFields, setPageAudience, setPageDocument, deletePage, addPage, reorderPages,
|
|
76
91
|
moveSection as moveSectionReducer,
|
|
77
92
|
} from "../../lib/pages";
|
|
78
93
|
import { PagesContext } from "../shared/PagesContext";
|
|
@@ -187,6 +202,7 @@ export default function EditorShell({
|
|
|
187
202
|
const [showOrderingModal, setShowOrderingModal] = useState(false);
|
|
188
203
|
const [showPagesModal, setShowPagesModal] = useState(false);
|
|
189
204
|
const [showChatbotConfig, setShowChatbotConfig] = useState(false);
|
|
205
|
+
const [showDocumentEditor, setShowDocumentEditor] = useState(false);
|
|
190
206
|
const [isRestoring, setIsRestoring] = useState(false);
|
|
191
207
|
const [rejectedUploads, setRejectedUploads] = useState<{ name: string; reason: "size" | "type" }[]>([]);
|
|
192
208
|
// Surfaces useMediaPipeline's onUploadError (oversized/disallowed files,
|
|
@@ -208,6 +224,13 @@ export default function EditorShell({
|
|
|
208
224
|
// takeover confirmation is pending for.
|
|
209
225
|
const [remoteNotice, setRemoteNotice] = useState<string | null>(null);
|
|
210
226
|
const [pendingTakeoverId, setPendingTakeoverId] = useState<string | null>(null);
|
|
227
|
+
// Lifted from EditorContext.historyState by HistoryWatcher (mirrors
|
|
228
|
+
// ViewBranchWatcher's setViewSections lift) — the shell body renders the
|
|
229
|
+
// EditorProvider, so it can't read historyState via useEditorContext directly.
|
|
230
|
+
// Drives the Document FAB gate below: history is presentation only, never
|
|
231
|
+
// editing (branch-preview already routes to DocumentPresentOverlay; this
|
|
232
|
+
// covers the separate historySelectEvent commit view).
|
|
233
|
+
const [historyActive, setHistoryActive] = useState(false);
|
|
211
234
|
|
|
212
235
|
const siteIndexRef = useRef<SiteIndex>({ siteId, pages: [], sections: {} });
|
|
213
236
|
// Ids of sections that exist on the remote (loaded from server, or persisted by
|
|
@@ -285,6 +308,26 @@ export default function EditorShell({
|
|
|
285
308
|
return audiences.filter((a) => resolvedActivePage.access.includes(a.name));
|
|
286
309
|
}, [resolvedActivePage, audiences]);
|
|
287
310
|
|
|
311
|
+
// The sections shown on the canvas when previewing the published ("live")
|
|
312
|
+
// branch — the SAME list computeDisplaySections renders for viewBranch "live".
|
|
313
|
+
// Only meaningful while viewSections !== null (branch view); the read-only
|
|
314
|
+
// document presentation overlay renders exactly these so it mirrors the canvas.
|
|
315
|
+
// (viewSections is set only for the live branch, so its non-null-ness IS the
|
|
316
|
+
// "branch view" signal at this level.)
|
|
317
|
+
const branchDisplaySections = useMemo(
|
|
318
|
+
() =>
|
|
319
|
+
viewSections && resolvedActivePage
|
|
320
|
+
? computeDisplaySections({
|
|
321
|
+
isEditMode: false,
|
|
322
|
+
viewBranch: "live",
|
|
323
|
+
sections,
|
|
324
|
+
viewSections,
|
|
325
|
+
activePageSectionIds: resolvedActivePage.order,
|
|
326
|
+
})
|
|
327
|
+
: [],
|
|
328
|
+
[viewSections, resolvedActivePage, sections],
|
|
329
|
+
);
|
|
330
|
+
|
|
288
331
|
const getPageHeadings = useCallback(
|
|
289
332
|
(pageId: string) => {
|
|
290
333
|
const page = pageById(siteIndex, pageId);
|
|
@@ -1218,6 +1261,11 @@ export default function EditorShell({
|
|
|
1218
1261
|
persistence.markIndexDirty(); setLocalChangesExist(true);
|
|
1219
1262
|
}, [persistence]);
|
|
1220
1263
|
|
|
1264
|
+
const handleSetPageDocument = useCallback((id: string, patch: Partial<DocumentOptions>) => {
|
|
1265
|
+
setSiteIndex((prev) => setPageDocument(prev, id, patch));
|
|
1266
|
+
persistence.markIndexDirty(); setLocalChangesExist(true);
|
|
1267
|
+
}, [persistence]);
|
|
1268
|
+
|
|
1221
1269
|
const handleSetPageAudience = useCallback((id: string, access: string[]) => {
|
|
1222
1270
|
const { index: next, resetSectionIds } = setPageAudience(siteIndexRef.current, id, access);
|
|
1223
1271
|
setSiteIndex(next);
|
|
@@ -1363,7 +1411,7 @@ export default function EditorShell({
|
|
|
1363
1411
|
<ViewBranchWatcher
|
|
1364
1412
|
setViewSections={setViewSections}
|
|
1365
1413
|
/>
|
|
1366
|
-
<HistoryWatcher />
|
|
1414
|
+
<HistoryWatcher onHistoryActiveChange={setHistoryActive} />
|
|
1367
1415
|
<EditorModalProvider>
|
|
1368
1416
|
<MediaLibraryContext.Provider value={{
|
|
1369
1417
|
...mediaPipeline.contextValue,
|
|
@@ -1408,6 +1456,59 @@ export default function EditorShell({
|
|
|
1408
1456
|
the button would always 401 — hide it there. */}
|
|
1409
1457
|
{!capabilities.passwordOnly && <BugReportFAB />}
|
|
1410
1458
|
|
|
1459
|
+
{/* Document editor: opens the full-screen editing surface for the
|
|
1460
|
+
active page when its document mode is enabled. Mirrors the viewer
|
|
1461
|
+
document FAB's icon; sits above BugReportFAB (bottom-40 clears the
|
|
1462
|
+
bottom-28 bug button + gap) so the two never collide.
|
|
1463
|
+
!historyActive: history is presentation only, never editing — while
|
|
1464
|
+
viewing a specific historical commit (historySelectEvent →
|
|
1465
|
+
EditorContext.historyState, lifted here by HistoryWatcher since this
|
|
1466
|
+
scope renders the provider and can't read the context directly),
|
|
1467
|
+
hide the FAB so it can't open the draft editor over a history view. */}
|
|
1468
|
+
{resolvedActivePage?.document?.enabled && !historyActive && (
|
|
1469
|
+
<button
|
|
1470
|
+
type="button"
|
|
1471
|
+
onClick={() => setShowDocumentEditor(true)}
|
|
1472
|
+
aria-label="Edit as document"
|
|
1473
|
+
className="cursor-pointer fixed bottom-40 right-4 lg:right-auto z-50 flex h-10 w-10 items-center justify-center rounded-full bg-primary text-primary-contrast shadow-lg hover:opacity-90 transition-opacity fab-container-right"
|
|
1474
|
+
>
|
|
1475
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
|
1476
|
+
<rect x="3" y="4" width="18" height="14" rx="2" />
|
|
1477
|
+
<path d="M8 21h8" />
|
|
1478
|
+
</svg>
|
|
1479
|
+
</button>
|
|
1480
|
+
)}
|
|
1481
|
+
{showDocumentEditor && resolvedActivePage?.document?.enabled && (
|
|
1482
|
+
<Suspense fallback={<DocumentOverlayFallback />}>
|
|
1483
|
+
{/* Branch/history view (viewSections !== null → previewing the
|
|
1484
|
+
published branch): presentation only, never the editor. Drew's
|
|
1485
|
+
product decision — old versions are the record, and we have no
|
|
1486
|
+
way to edit them, so opening the editor over one would be a lie.
|
|
1487
|
+
The draft view (viewSections === null) still opens the real
|
|
1488
|
+
editor. NOTE: viewSections drives the "Published" branch preview
|
|
1489
|
+
only; the separate historySelectEvent commit view
|
|
1490
|
+
(EditorContext.historyState) is the OTHER read-only case — the
|
|
1491
|
+
historyActive-gated FAB above is what keeps this overlay from
|
|
1492
|
+
ever being opened onto it in the first place. */}
|
|
1493
|
+
{viewSections !== null ? (
|
|
1494
|
+
<DocumentPresentOverlay
|
|
1495
|
+
page={resolvedActivePage}
|
|
1496
|
+
sections={branchDisplaySections}
|
|
1497
|
+
manifest={mediaManifest}
|
|
1498
|
+
onClose={() => setShowDocumentEditor(false)}
|
|
1499
|
+
/>
|
|
1500
|
+
) : (
|
|
1501
|
+
<DocumentEditorOverlay
|
|
1502
|
+
page={resolvedActivePage}
|
|
1503
|
+
sections={activeSections}
|
|
1504
|
+
manifest={mediaManifest}
|
|
1505
|
+
onUpdateDocument={(patch) => handleSetPageDocument(resolvedActivePage.id, patch)}
|
|
1506
|
+
onClose={() => setShowDocumentEditor(false)}
|
|
1507
|
+
/>
|
|
1508
|
+
)}
|
|
1509
|
+
</Suspense>
|
|
1510
|
+
)}
|
|
1511
|
+
|
|
1411
1512
|
{/* Editor chat FAB: chat while editing, against the DRAFT chatbot
|
|
1412
1513
|
config (tasks + prompt reflect unsaved changes; grounding is
|
|
1413
1514
|
published content). Gated only on the platform switch — editors
|
|
@@ -1589,6 +1690,7 @@ export default function EditorShell({
|
|
|
1589
1690
|
onSetArchived={handleSetArchived}
|
|
1590
1691
|
onSetFields={handleSetPageFields}
|
|
1591
1692
|
onSetAudience={handleSetPageAudience}
|
|
1693
|
+
onSetDocument={handleSetPageDocument}
|
|
1592
1694
|
onConfirmDelete={handleDeletePage}
|
|
1593
1695
|
/>
|
|
1594
1696
|
</EditorModal>
|
|
@@ -1665,8 +1767,16 @@ function ViewBranchWatcher({
|
|
|
1665
1767
|
return null;
|
|
1666
1768
|
}
|
|
1667
1769
|
|
|
1668
|
-
function HistoryWatcher(
|
|
1669
|
-
|
|
1770
|
+
function HistoryWatcher({
|
|
1771
|
+
onHistoryActiveChange,
|
|
1772
|
+
}: {
|
|
1773
|
+
// Lifts historyState's active/inactive status to the shell body (mirrors
|
|
1774
|
+
// ViewBranchWatcher's setViewSections lift) — the body renders the
|
|
1775
|
+
// EditorProvider this component reads from, so it can't call
|
|
1776
|
+
// useEditorContext() itself to get at historyState.
|
|
1777
|
+
onHistoryActiveChange?: (active: boolean) => void;
|
|
1778
|
+
}) {
|
|
1779
|
+
const { historyState, setHistoryState } = useEditorContext();
|
|
1670
1780
|
|
|
1671
1781
|
useEffect(() => {
|
|
1672
1782
|
const unlisten = historySelectEvent.listen(async ({ sha, date }) => {
|
|
@@ -1691,6 +1801,10 @@ function HistoryWatcher() {
|
|
|
1691
1801
|
return unlisten;
|
|
1692
1802
|
}, [setHistoryState]);
|
|
1693
1803
|
|
|
1804
|
+
useEffect(() => {
|
|
1805
|
+
onHistoryActiveChange?.(historyState !== null);
|
|
1806
|
+
}, [historyState, onHistoryActiveChange]);
|
|
1807
|
+
|
|
1694
1808
|
return null;
|
|
1695
1809
|
}
|
|
1696
1810
|
|
|
@@ -2039,6 +2153,15 @@ function EditorContent({
|
|
|
2039
2153
|
);
|
|
2040
2154
|
}
|
|
2041
2155
|
|
|
2156
|
+
// Suspense fallback while the lazy document overlay chunk loads.
|
|
2157
|
+
function DocumentOverlayFallback() {
|
|
2158
|
+
return (
|
|
2159
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-base" role="status" aria-live="polite">
|
|
2160
|
+
<span className="text-sm text-base-contrast-light">Loading document editor…</span>
|
|
2161
|
+
</div>
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2042
2165
|
function GlobalModal() {
|
|
2043
2166
|
const { modalState, closeModal } = useEditorModal();
|
|
2044
2167
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { DocCover } from "../engine/settings";
|
|
2
|
+
import type { DocImage, DocMeta } from "../engine/types";
|
|
3
|
+
import { DocPage } from "./DocPage";
|
|
4
|
+
import type { DocHost } from "./DocSectionPage";
|
|
5
|
+
import { Wordmark } from "./Wordmark";
|
|
6
|
+
|
|
7
|
+
// Absorbs the harness's TitleCardLayout inline instead of wrapping it: the
|
|
8
|
+
// portal-native cover has no client-logo slot (the harness only ever passed
|
|
9
|
+
// null) and cover text is edited in the inspector, never on-canvas, so there
|
|
10
|
+
// is no EditableText/deck-context machinery here — this always renders the
|
|
11
|
+
// harness's non-editable, null-logo branch.
|
|
12
|
+
export function DocCoverPage({
|
|
13
|
+
cover,
|
|
14
|
+
meta,
|
|
15
|
+
image,
|
|
16
|
+
host,
|
|
17
|
+
}: {
|
|
18
|
+
cover: DocCover;
|
|
19
|
+
meta: DocMeta;
|
|
20
|
+
/** Resolved from source.images[cover.imageId] by the caller; null/undefined = solid cover. */
|
|
21
|
+
image?: DocImage | null;
|
|
22
|
+
host?: DocHost;
|
|
23
|
+
}) {
|
|
24
|
+
return (
|
|
25
|
+
<DocPage meta={meta} bleed chrome={false}>
|
|
26
|
+
<div className="layout-titlecard" style={{ background: cover.bgColor }}>
|
|
27
|
+
<header className="layout-titlecard__masthead">
|
|
28
|
+
<Wordmark className="layout-titlecard__wordmark" />
|
|
29
|
+
<span className="layout-titlecard__rule" />
|
|
30
|
+
</header>
|
|
31
|
+
{image && host ? (
|
|
32
|
+
<div className="layout-titlecard__image">
|
|
33
|
+
<img src={host.imageSrc(image, host.variant)} alt={image.alt} />
|
|
34
|
+
</div>
|
|
35
|
+
) : null}
|
|
36
|
+
<div className="layout-titlecard__lockup" style={{ color: cover.accentColor }}>
|
|
37
|
+
<h1 className="layout-titlecard__title">{cover.title}</h1>
|
|
38
|
+
<span className="layout-titlecard__titlerule" style={{ background: cover.accentColor }} />
|
|
39
|
+
<p className="layout-titlecard__label">{cover.label}</p>
|
|
40
|
+
<p className="layout-titlecard__date">{cover.date || meta.dateLabel}</p>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</DocPage>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { DocMeta } from "../engine/types";
|
|
3
|
+
import { Wordmark } from "./Wordmark";
|
|
4
|
+
|
|
5
|
+
// Re-exported so index.ts's `export { DocPage, type DocMeta } from
|
|
6
|
+
// "./components/DocPage"` has a local name — the single definition still
|
|
7
|
+
// lives in engine/types.ts.
|
|
8
|
+
export type { DocMeta };
|
|
9
|
+
|
|
10
|
+
export function DocPage({
|
|
11
|
+
meta,
|
|
12
|
+
bleed = false,
|
|
13
|
+
chrome = true,
|
|
14
|
+
children,
|
|
15
|
+
}: {
|
|
16
|
+
meta: DocMeta;
|
|
17
|
+
bleed?: boolean;
|
|
18
|
+
chrome?: boolean;
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
}) {
|
|
21
|
+
const crumb = [meta.client, meta.project, meta.dateLabel]
|
|
22
|
+
.map((s) => s.trim().toUpperCase())
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.join(" :: ");
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<section className="deck-page">
|
|
28
|
+
{bleed ? <div className="deck-bleed">{children}</div> : null}
|
|
29
|
+
{chrome ? (
|
|
30
|
+
<header className="deck-chrome">
|
|
31
|
+
<span className="deck-crumb">{crumb}</span>
|
|
32
|
+
<div className="deck-wordmark-clip">
|
|
33
|
+
<Wordmark className="deck-wordmark" />
|
|
34
|
+
</div>
|
|
35
|
+
</header>
|
|
36
|
+
) : null}
|
|
37
|
+
{!bleed ? <div className="deck-body">{children}</div> : null}
|
|
38
|
+
</section>
|
|
39
|
+
);
|
|
40
|
+
}
|