@drawnagency/primitives 0.1.79 → 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-RWMSDDQM.js → chunk-DKXISNJ2.js} +79 -26
- package/dist/{chunk-RSITF24B.js → chunk-EVNEMJZ6.js} +9 -3
- package/dist/{chunk-DR22HM44.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-FRTJNS5F.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-4XXLH5CL.js → chunk-ZEXR7JYQ.js} +19 -12
- package/dist/components/brandguide/Colors.d.ts +2 -1
- package/dist/components/brandguide/Colors.d.ts.map +1 -1
- 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/EditableGrid.d.ts +11 -1
- package/dist/components/primitives/EditableGrid.d.ts.map +1 -1
- package/dist/components/primitives/gridDrop.d.ts +18 -0
- package/dist/components/primitives/gridDrop.d.ts.map +1 -0
- package/dist/components/primitives/index.js +7 -6
- package/dist/components/primitives/useEditableCollection.d.ts +1 -0
- package/dist/components/primitives/useEditableCollection.d.ts.map +1 -1
- package/dist/components/sections/Colors/index.d.ts.map +1 -1
- 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/EditorContext.d.ts +2 -0
- package/dist/components/shell/EditorContext.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts +44 -0
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/HistoryToolbar.d.ts +8 -1
- package/dist/components/shell/HistoryToolbar.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/lib/registry.d.ts +3 -0
- package/dist/lib/registry.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/brandguide/Colors.tsx +15 -5
- package/src/components/editor/PagesModal.tsx +16 -2
- package/src/components/primitives/EditableGrid.tsx +86 -24
- package/src/components/primitives/gridDrop.ts +26 -0
- package/src/components/primitives/useEditableCollection.ts +16 -1
- package/src/components/sections/Colors/index.tsx +2 -1
- 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/EditorContext.tsx +6 -1
- package/src/components/shell/EditorShell.tsx +171 -10
- package/src/components/shell/HistoryToolbar.tsx +24 -2
- 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/lib/registry.ts +3 -0
- package/src/schemas/site-config.ts +22 -7
- /package/dist/{chunk-4YEOTFVU.js → chunk-YBVK254P.js} +0 -0
|
@@ -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,9 +10,13 @@ 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
|
-
import { EditorProvider, useEditorContext } from "./EditorContext";
|
|
19
|
+
import { EditorProvider, useEditorContext, type HistoryState } from "./EditorContext";
|
|
16
20
|
import { makeBlockMoveDispatch } from "./blockMoveDispatch";
|
|
17
21
|
import { ROOT_CONTAINER_ID, resolveDropIndex } from "../../lib/block-dnd";
|
|
18
22
|
import type { BlockMove } from "../../lib/block-move";
|
|
@@ -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
|
|
@@ -1454,6 +1555,7 @@ export default function EditorShell({
|
|
|
1454
1555
|
onSectionChange={onSectionChange}
|
|
1455
1556
|
onAddSection={onAddSection}
|
|
1456
1557
|
onDeleteSection={setPendingDeleteSectionId}
|
|
1558
|
+
onDeleteSectionImmediate={performDeleteSection}
|
|
1457
1559
|
onReorderSections={onReorderSections}
|
|
1458
1560
|
onBlockMove={onBlockMove}
|
|
1459
1561
|
onMoveSection={siteIndex.pages.length > 1 ? setMovingSectionId : undefined}
|
|
@@ -1588,6 +1690,7 @@ export default function EditorShell({
|
|
|
1588
1690
|
onSetArchived={handleSetArchived}
|
|
1589
1691
|
onSetFields={handleSetPageFields}
|
|
1590
1692
|
onSetAudience={handleSetPageAudience}
|
|
1693
|
+
onSetDocument={handleSetPageDocument}
|
|
1591
1694
|
onConfirmDelete={handleDeletePage}
|
|
1592
1695
|
/>
|
|
1593
1696
|
</EditorModal>
|
|
@@ -1664,8 +1767,16 @@ function ViewBranchWatcher({
|
|
|
1664
1767
|
return null;
|
|
1665
1768
|
}
|
|
1666
1769
|
|
|
1667
|
-
function HistoryWatcher(
|
|
1668
|
-
|
|
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();
|
|
1669
1780
|
|
|
1670
1781
|
useEffect(() => {
|
|
1671
1782
|
const unlisten = historySelectEvent.listen(async ({ sha, date }) => {
|
|
@@ -1690,9 +1801,25 @@ function HistoryWatcher() {
|
|
|
1690
1801
|
return unlisten;
|
|
1691
1802
|
}, [setHistoryState]);
|
|
1692
1803
|
|
|
1804
|
+
useEffect(() => {
|
|
1805
|
+
onHistoryActiveChange?.(historyState !== null);
|
|
1806
|
+
}, [historyState, onHistoryActiveChange]);
|
|
1807
|
+
|
|
1693
1808
|
return null;
|
|
1694
1809
|
}
|
|
1695
1810
|
|
|
1811
|
+
/** Resolve which historical page to show: explicit selection → the page the
|
|
1812
|
+
* editor is currently on (if it existed at that version) → home → first. */
|
|
1813
|
+
export function resolveHistoryPage(
|
|
1814
|
+
historyState: NonNullable<HistoryState>,
|
|
1815
|
+
historyPageId: string | null,
|
|
1816
|
+
preferredPageId: string | null,
|
|
1817
|
+
) {
|
|
1818
|
+
const pages = historyState.index.pages;
|
|
1819
|
+
const byId = (id: string | null) => (id ? pages.find((p) => p.id === id) : undefined);
|
|
1820
|
+
return byId(historyPageId) ?? byId(preferredPageId) ?? pages.find((p) => p.isHome) ?? pages[0];
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1696
1823
|
function HistoryOrEditorContent({
|
|
1697
1824
|
children,
|
|
1698
1825
|
index,
|
|
@@ -1704,9 +1831,11 @@ function HistoryOrEditorContent({
|
|
|
1704
1831
|
activeSections: LoadedSection[];
|
|
1705
1832
|
activePageId: string | null;
|
|
1706
1833
|
}) {
|
|
1707
|
-
const { historyState } = useEditorContext();
|
|
1834
|
+
const { historyState, historyPageId, setHistoryPageId } = useEditorContext();
|
|
1708
1835
|
const wasInHistory = useRef(false);
|
|
1709
1836
|
|
|
1837
|
+
const historyPage = historyState ? resolveHistoryPage(historyState, historyPageId, activePageId) : null;
|
|
1838
|
+
|
|
1710
1839
|
useEffect(() => {
|
|
1711
1840
|
if (historyState) {
|
|
1712
1841
|
wasInHistory.current = true;
|
|
@@ -1720,8 +1849,22 @@ function HistoryOrEditorContent({
|
|
|
1720
1849
|
}
|
|
1721
1850
|
}, [historyState, index, activeSections, activePageId]);
|
|
1722
1851
|
|
|
1723
|
-
|
|
1724
|
-
|
|
1852
|
+
// Pin the auto-resolved page into context so the toolbar selector agrees,
|
|
1853
|
+
// and keep the nav highlighting in sync with the page being viewed.
|
|
1854
|
+
useEffect(() => {
|
|
1855
|
+
if (!historyState || !historyPage) return;
|
|
1856
|
+
if (!historyPageId) setHistoryPageId(historyPage.id);
|
|
1857
|
+
siteNavChangeEvent.dispatch(
|
|
1858
|
+
generateSiteNav({ index: historyState.index, activeSections: [], activePageId: historyPage.id, mode: "editor", audience: null }),
|
|
1859
|
+
);
|
|
1860
|
+
}, [historyState, historyPage?.id]);
|
|
1861
|
+
|
|
1862
|
+
if (historyState && historyPage) {
|
|
1863
|
+
const position = new Map(historyPage.order.map((id, i) => [id, i]));
|
|
1864
|
+
const pageSections = historyState.sections
|
|
1865
|
+
.filter((s) => position.has(s.section.id))
|
|
1866
|
+
.sort((a, b) => (position.get(a.section.id) ?? 0) - (position.get(b.section.id) ?? 0));
|
|
1867
|
+
return <ViewRenderer sections={pageSections} />;
|
|
1725
1868
|
}
|
|
1726
1869
|
return <>{children}</>;
|
|
1727
1870
|
}
|
|
@@ -1764,6 +1907,7 @@ function EditorContent({
|
|
|
1764
1907
|
onSectionChange,
|
|
1765
1908
|
onAddSection,
|
|
1766
1909
|
onDeleteSection,
|
|
1910
|
+
onDeleteSectionImmediate,
|
|
1767
1911
|
onReorderSections,
|
|
1768
1912
|
onBlockMove,
|
|
1769
1913
|
onMoveSection,
|
|
@@ -1784,6 +1928,7 @@ function EditorContent({
|
|
|
1784
1928
|
onSectionChange: (sectionId: string, content: SectionContent) => void;
|
|
1785
1929
|
onAddSection: (insertIndex: number, selection: string) => void;
|
|
1786
1930
|
onDeleteSection: (sectionId: string) => void;
|
|
1931
|
+
onDeleteSectionImmediate: (sectionId: string) => void;
|
|
1787
1932
|
onReorderSections: (fromIndex: number, toIndex: number) => void;
|
|
1788
1933
|
onBlockMove: (move: BlockMove) => void;
|
|
1789
1934
|
onMoveSection?: (sectionId: string) => void;
|
|
@@ -1978,6 +2123,9 @@ function EditorContent({
|
|
|
1978
2123
|
onChange={editingEnabled ? (newContent) => onSectionChange(section.id, newContent as SectionContent) : undefined}
|
|
1979
2124
|
isEditMode={isEditMode}
|
|
1980
2125
|
openModal={editingEnabled ? openModal : undefined}
|
|
2126
|
+
// Immediate (no confirm modal): used when a colors section's
|
|
2127
|
+
// last swatch is dragged out, auto-removing the empty section.
|
|
2128
|
+
onDelete={editingEnabled ? () => onDeleteSectionImmediate(section.id) : undefined}
|
|
1981
2129
|
/>
|
|
1982
2130
|
</SectionWrapper>
|
|
1983
2131
|
</SectionLayout>
|
|
@@ -2005,6 +2153,15 @@ function EditorContent({
|
|
|
2005
2153
|
);
|
|
2006
2154
|
}
|
|
2007
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
|
+
|
|
2008
2165
|
function GlobalModal() {
|
|
2009
2166
|
const { modalState, closeModal } = useEditorModal();
|
|
2010
2167
|
|
|
@@ -2150,12 +2307,16 @@ function EditorToolbar({
|
|
|
2150
2307
|
collabSelfId: string | null;
|
|
2151
2308
|
collabStatus: CollabStatus;
|
|
2152
2309
|
}) {
|
|
2153
|
-
const { isEditMode, viewBranch, setViewBranch, toggleEditMode, historyState, setHistoryState } = useEditorContext();
|
|
2310
|
+
const { isEditMode, viewBranch, setViewBranch, toggleEditMode, historyState, setHistoryState, historyPageId, setHistoryPageId } = useEditorContext();
|
|
2154
2311
|
|
|
2155
2312
|
if (historyState) {
|
|
2313
|
+
const page = resolveHistoryPage(historyState, historyPageId, null);
|
|
2156
2314
|
return (
|
|
2157
2315
|
<HistoryToolbar
|
|
2158
2316
|
date={historyState.date}
|
|
2317
|
+
pages={historyState.index.pages.map((p) => ({ id: p.id, title: p.title }))}
|
|
2318
|
+
activePageId={page?.id ?? ""}
|
|
2319
|
+
onPageChange={setHistoryPageId}
|
|
2159
2320
|
onBackToCurrent={() => setHistoryState(null)}
|
|
2160
2321
|
onRestore={onRestoreClick}
|
|
2161
2322
|
/>
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { formatDateTime } from "../../lib/timestamp";
|
|
2
2
|
import { Button } from "../shared/Button";
|
|
3
3
|
|
|
4
|
+
interface HistoryPageOption {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
4
9
|
interface HistoryToolbarProps {
|
|
5
10
|
date: string;
|
|
11
|
+
pages: HistoryPageOption[];
|
|
12
|
+
activePageId: string;
|
|
13
|
+
onPageChange: (pageId: string) => void;
|
|
6
14
|
onBackToCurrent: () => void;
|
|
7
15
|
onRestore: () => void;
|
|
8
16
|
}
|
|
9
17
|
|
|
10
|
-
export function HistoryToolbar({ date, onBackToCurrent, onRestore }: HistoryToolbarProps) {
|
|
18
|
+
export function HistoryToolbar({ date, pages, activePageId, onPageChange, onBackToCurrent, onRestore }: HistoryToolbarProps) {
|
|
11
19
|
return (
|
|
12
20
|
<div className="fixed top-0 right-0 left-0 z-50 border-b border-base-200 bg-base">
|
|
13
21
|
<div className="mx-auto max-w-screen-xl grid grid-cols-3 items-center px-4 py-2">
|
|
@@ -16,10 +24,24 @@ export function HistoryToolbar({ date, onBackToCurrent, onRestore }: HistoryTool
|
|
|
16
24
|
Back to current
|
|
17
25
|
</Button>
|
|
18
26
|
</div>
|
|
19
|
-
<div className="flex items-center justify-center">
|
|
27
|
+
<div className="flex items-center justify-center gap-2">
|
|
20
28
|
<span className="text-xs font-medium text-base-contrast-light">
|
|
21
29
|
Viewing {formatDateTime(date)}
|
|
22
30
|
</span>
|
|
31
|
+
{pages.length > 1 && (
|
|
32
|
+
<select
|
|
33
|
+
value={activePageId}
|
|
34
|
+
onChange={(e) => onPageChange(e.target.value)}
|
|
35
|
+
className="rounded border border-base-200 bg-base px-1.5 py-0.5 text-xs font-medium text-base-contrast"
|
|
36
|
+
aria-label="Page at this version"
|
|
37
|
+
>
|
|
38
|
+
{pages.map((p) => (
|
|
39
|
+
<option key={p.id} value={p.id}>
|
|
40
|
+
{p.title}
|
|
41
|
+
</option>
|
|
42
|
+
))}
|
|
43
|
+
</select>
|
|
44
|
+
)}
|
|
23
45
|
</div>
|
|
24
46
|
<div className="flex items-center justify-end">
|
|
25
47
|
<Button variant="primary" size="sm" onClick={onRestore}>
|
|
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
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
|
+
import type { DerivedPage } from "../engine/paginate";
|
|
3
|
+
import type { DocCover } from "../engine/settings";
|
|
4
|
+
import type { DocImage, DocMeta } from "../engine/types";
|
|
5
|
+
import { DocCoverPage } from "./DocCoverPage";
|
|
6
|
+
import { DocSectionPage, type DocHost } from "./DocSectionPage";
|
|
7
|
+
|
|
8
|
+
// 17in @96dpi = 1632px wide; thumbnails render the real page scaled to fill the
|
|
9
|
+
// rail width at a locked 17:11 aspect ratio (mirrors the harness PageRail).
|
|
10
|
+
const PAGE_W = 1632;
|
|
11
|
+
const PAGE_H = 1056;
|
|
12
|
+
|
|
13
|
+
export type RailEntry =
|
|
14
|
+
| { kind: "cover" }
|
|
15
|
+
| { kind: "page"; page: DerivedPage }
|
|
16
|
+
| { kind: "ghost"; chunkId: string; title: string; reason: "excluded" | "empty" };
|
|
17
|
+
|
|
18
|
+
export function entryId(entry: RailEntry): string {
|
|
19
|
+
if (entry.kind === "cover") return "cover";
|
|
20
|
+
if (entry.kind === "page") return entry.page.id;
|
|
21
|
+
return `ghost:${entry.chunkId}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function ThumbButton({
|
|
25
|
+
id,
|
|
26
|
+
selectedId,
|
|
27
|
+
scale,
|
|
28
|
+
label,
|
|
29
|
+
indent,
|
|
30
|
+
onSelect,
|
|
31
|
+
children,
|
|
32
|
+
}: {
|
|
33
|
+
id: string;
|
|
34
|
+
selectedId: string;
|
|
35
|
+
scale: number;
|
|
36
|
+
label: string;
|
|
37
|
+
indent?: boolean;
|
|
38
|
+
onSelect: (id: string) => void;
|
|
39
|
+
children: ReactNode;
|
|
40
|
+
}) {
|
|
41
|
+
return (
|
|
42
|
+
<li className={indent ? "pl-4" : undefined}>
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
onClick={() => onSelect(id)}
|
|
46
|
+
className={`relative block w-full rounded border-2 bg-white ${
|
|
47
|
+
id === selectedId ? "border-teal-600" : "border-neutral-200 hover:border-neutral-300"
|
|
48
|
+
}`}
|
|
49
|
+
>
|
|
50
|
+
<span className="pointer-events-none block w-full overflow-hidden aspect-[17/11]">
|
|
51
|
+
<span
|
|
52
|
+
className="block origin-top-left"
|
|
53
|
+
style={{ transform: `scale(${scale})`, width: PAGE_W, height: PAGE_H }}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</span>
|
|
57
|
+
</span>
|
|
58
|
+
<span className="absolute bottom-0.5 left-1 max-w-[90%] truncate text-[10px] text-neutral-400">
|
|
59
|
+
{label}
|
|
60
|
+
</span>
|
|
61
|
+
</button>
|
|
62
|
+
</li>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function DocRail({
|
|
67
|
+
entries,
|
|
68
|
+
selectedId,
|
|
69
|
+
meta,
|
|
70
|
+
cover,
|
|
71
|
+
coverImage,
|
|
72
|
+
host,
|
|
73
|
+
onSelect,
|
|
74
|
+
onCollapse,
|
|
75
|
+
}: {
|
|
76
|
+
entries: RailEntry[];
|
|
77
|
+
selectedId: string;
|
|
78
|
+
meta: DocMeta;
|
|
79
|
+
cover: DocCover;
|
|
80
|
+
coverImage?: DocImage | null;
|
|
81
|
+
host: DocHost;
|
|
82
|
+
onSelect: (id: string) => void;
|
|
83
|
+
onCollapse: () => void;
|
|
84
|
+
}) {
|
|
85
|
+
const listRef = useRef<HTMLOListElement>(null);
|
|
86
|
+
const [scale, setScale] = useState(0.11);
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const el = listRef.current;
|
|
89
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
90
|
+
const obs = new ResizeObserver(() => {
|
|
91
|
+
// content width = list width minus the button's 2px border on each side
|
|
92
|
+
const w = el.clientWidth - 4;
|
|
93
|
+
if (w > 0) setScale(w / PAGE_W);
|
|
94
|
+
});
|
|
95
|
+
obs.observe(el);
|
|
96
|
+
return () => obs.disconnect();
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<nav
|
|
101
|
+
data-testid="doc-rail"
|
|
102
|
+
className="flex w-52 shrink-0 flex-col overflow-y-auto border-r border-neutral-200 bg-neutral-50 p-3"
|
|
103
|
+
>
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
aria-label="Collapse page rail"
|
|
107
|
+
onClick={onCollapse}
|
|
108
|
+
className="mb-2 flex w-full items-center justify-between rounded px-2 py-1.5 text-sm font-medium text-neutral-600 hover:bg-neutral-100 hover:text-neutral-900"
|
|
109
|
+
>
|
|
110
|
+
<span>Pages</span>
|
|
111
|
+
<span aria-hidden="true" className="text-lg leading-none">‹</span>
|
|
112
|
+
</button>
|
|
113
|
+
<ol ref={listRef} className="flex flex-col gap-2">
|
|
114
|
+
{entries.map((entry) => {
|
|
115
|
+
const id = entryId(entry);
|
|
116
|
+
if (entry.kind === "cover") {
|
|
117
|
+
return (
|
|
118
|
+
<ThumbButton key={id} id={id} selectedId={selectedId} scale={scale} label="Cover" onSelect={onSelect}>
|
|
119
|
+
<DocCoverPage cover={cover} meta={meta} image={coverImage} host={host} />
|
|
120
|
+
</ThumbButton>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (entry.kind === "page") {
|
|
124
|
+
return (
|
|
125
|
+
<ThumbButton
|
|
126
|
+
key={id}
|
|
127
|
+
id={id}
|
|
128
|
+
selectedId={selectedId}
|
|
129
|
+
scale={scale}
|
|
130
|
+
label={entry.page.title}
|
|
131
|
+
indent={entry.page.cont}
|
|
132
|
+
onSelect={onSelect}
|
|
133
|
+
>
|
|
134
|
+
<DocSectionPage page={entry.page} meta={meta} host={host} />
|
|
135
|
+
</ThumbButton>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return (
|
|
139
|
+
<li key={id}>
|
|
140
|
+
<button
|
|
141
|
+
type="button"
|
|
142
|
+
onClick={() => onSelect(id)}
|
|
143
|
+
className={`flex w-full items-center justify-between gap-2 rounded border px-2 py-2 text-left text-xs ${
|
|
144
|
+
id === selectedId
|
|
145
|
+
? "border-teal-600 bg-white"
|
|
146
|
+
: "border-dashed border-neutral-300 bg-neutral-50 hover:border-neutral-400"
|
|
147
|
+
}`}
|
|
148
|
+
>
|
|
149
|
+
<span className="truncate text-neutral-400">{entry.title}</span>
|
|
150
|
+
<span className="shrink-0 rounded bg-neutral-200 px-1 text-[10px] uppercase tracking-wide text-neutral-500">
|
|
151
|
+
{entry.reason}
|
|
152
|
+
</span>
|
|
153
|
+
</button>
|
|
154
|
+
</li>
|
|
155
|
+
);
|
|
156
|
+
})}
|
|
157
|
+
</ol>
|
|
158
|
+
</nav>
|
|
159
|
+
);
|
|
160
|
+
}
|