@drawnagency/primitives 0.1.80 → 0.1.82
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-7XWER6YR.js → chunk-22GU6GNP.js} +3 -3
- package/dist/{chunk-4TVODHD2.js → chunk-5UDIQLGQ.js} +6 -6
- package/dist/{chunk-UNUJPLZV.js → chunk-6NRVX6NQ.js} +1 -1
- package/dist/{chunk-KVQFMVXE.js → chunk-DKXISNJ2.js} +2 -2
- package/dist/{chunk-RSITF24B.js → chunk-ETJYYUQX.js} +9 -3
- package/dist/{chunk-VI26O4E3.js → chunk-FDBZZ4AT.js} +1 -1
- package/dist/chunk-FM4M4Q2W.js +72 -0
- package/dist/{chunk-EMGLZQCM.js → chunk-MBJUWOM2.js} +1 -1
- package/dist/{chunk-2DDWWTRX.js → chunk-Q6LZOH5M.js} +1 -1
- package/dist/{chunk-QB5Q23G2.js → chunk-Y54G2XSC.js} +1 -1
- package/dist/{chunk-3LNKOLZP.js → chunk-YKOLOZB2.js} +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/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 +18 -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 +34 -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 +28 -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 +7 -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 +26 -0
- package/dist/document/engine/masonry.d.ts.map +1 -0
- package/dist/document/engine/paginate.d.ts +43 -0
- package/dist/document/engine/paginate.d.ts.map +1 -0
- package/dist/document/engine/settings.d.ts +88 -0
- package/dist/document/engine/settings.d.ts.map +1 -0
- package/dist/document/engine/types.d.ts +44 -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 +965 -0
- package/dist/document/styles/document.css +217 -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 +265 -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 +323 -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 +164 -0
- package/src/document/components/DocSectionPage.tsx +48 -0
- package/src/document/components/DocViewer.tsx +285 -0
- package/src/document/components/Wordmark.tsx +18 -0
- package/src/document/components/measure.tsx +117 -0
- package/src/document/components/useDocPages.ts +85 -0
- package/src/document/engine/blocks.ts +87 -0
- package/src/document/engine/harvest.ts +95 -0
- package/src/document/engine/masonry.ts +152 -0
- package/src/document/engine/paginate.ts +229 -0
- package/src/document/engine/settings.ts +88 -0
- package/src/document/engine/types.ts +64 -0
- package/src/document/index.ts +18 -0
- package/src/document/styles/document.css +217 -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,217 @@
|
|
|
1
|
+
/* Document artifact rendering — physical units only. This file styles the
|
|
2
|
+
artifact itself (editor canvas AND print route), so no Tailwind utilities
|
|
3
|
+
and no viewport-relative units. 11×17 tabloid landscape = 17in × 11in. */
|
|
4
|
+
|
|
5
|
+
/* ---- Drawn brand fonts ---------------------------------------------- */
|
|
6
|
+
/* Albra Grotesk (sans body) · Kapra Neue Expanded (uppercase labels).
|
|
7
|
+
Self-hosted, resolved relative to this file so bundlers can locate them
|
|
8
|
+
without a public root. */
|
|
9
|
+
@font-face {
|
|
10
|
+
font-family: "Albra Grotesk";
|
|
11
|
+
src: url("../assets/fonts/albra-grotesk-regular.woff") format("woff");
|
|
12
|
+
font-weight: 400;
|
|
13
|
+
font-style: normal;
|
|
14
|
+
font-display: swap;
|
|
15
|
+
}
|
|
16
|
+
@font-face {
|
|
17
|
+
font-family: "Albra Grotesk";
|
|
18
|
+
src: url("../assets/fonts/albra-grotesk-bold.woff") format("woff");
|
|
19
|
+
font-weight: 600;
|
|
20
|
+
font-style: normal;
|
|
21
|
+
font-display: swap;
|
|
22
|
+
}
|
|
23
|
+
@font-face {
|
|
24
|
+
font-family: "Kapra Neue Expanded";
|
|
25
|
+
src: url("../assets/fonts/kapra-neue-expanded-regular.woff") format("woff");
|
|
26
|
+
font-weight: 400;
|
|
27
|
+
font-style: normal;
|
|
28
|
+
font-display: swap;
|
|
29
|
+
}
|
|
30
|
+
@font-face {
|
|
31
|
+
font-family: "Kapra Neue Expanded";
|
|
32
|
+
src: url("../assets/fonts/kapra-neue-expanded-medium.woff") format("woff");
|
|
33
|
+
font-weight: 700;
|
|
34
|
+
font-style: normal;
|
|
35
|
+
font-display: swap;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* ---- page chrome ------------------------------------------------------ */
|
|
39
|
+
|
|
40
|
+
.deck-page {
|
|
41
|
+
position: relative;
|
|
42
|
+
width: 17in;
|
|
43
|
+
height: 11in;
|
|
44
|
+
background: #fff;
|
|
45
|
+
color: #1a1a1a;
|
|
46
|
+
overflow: hidden;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.deck-chrome {
|
|
50
|
+
position: absolute;
|
|
51
|
+
top: 0.5in;
|
|
52
|
+
left: 0.6in;
|
|
53
|
+
right: 0.6in;
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: flex-end;
|
|
56
|
+
justify-content: space-between;
|
|
57
|
+
border-bottom: 1px solid #1a1a1a;
|
|
58
|
+
padding-bottom: 0.1in;
|
|
59
|
+
z-index: 10;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.deck-crumb {
|
|
63
|
+
font-family: "Kapra Neue Expanded", Helvetica, Arial, sans-serif;
|
|
64
|
+
font-size: 9pt;
|
|
65
|
+
letter-spacing: 0.12em;
|
|
66
|
+
text-transform: uppercase;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* The wordmark SVG's viewBox includes an under-swash flourish below the
|
|
70
|
+
letters' baseline. The clip wrapper truncates the glyph so the hairline
|
|
71
|
+
rule slices INTO the letter bottoms (per the reference deck): the cut
|
|
72
|
+
falls at ~viewBox y=100.5 of 154.474 (0.407 / 0.625), removing the
|
|
73
|
+
under-swash, the D's descender loop, and the rounded letter bottoms.
|
|
74
|
+
The negative margin cancels .deck-chrome's padding-bottom so the cut
|
|
75
|
+
edge lands exactly on the rule, while the breadcrumb keeps its normal
|
|
76
|
+
gap above the rule. */
|
|
77
|
+
.deck-wordmark-clip {
|
|
78
|
+
height: 0.407in;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
margin-bottom: -0.1in;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.deck-wordmark {
|
|
84
|
+
height: 0.625in;
|
|
85
|
+
width: auto;
|
|
86
|
+
display: block;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.deck-body {
|
|
90
|
+
position: absolute;
|
|
91
|
+
top: 1.35in;
|
|
92
|
+
left: 0.6in;
|
|
93
|
+
right: 0.6in;
|
|
94
|
+
bottom: 0.6in;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Bleed layouts (cover) paint the whole page; chrome layers above via
|
|
98
|
+
.deck-chrome's z-index — layering only, chrome treatment is untouched. */
|
|
99
|
+
.deck-bleed {
|
|
100
|
+
position: absolute;
|
|
101
|
+
inset: 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* ---- title card (document cover) --------------------------------------- */
|
|
105
|
+
|
|
106
|
+
.layout-titlecard {
|
|
107
|
+
position: absolute;
|
|
108
|
+
inset: 0;
|
|
109
|
+
color: #fff;
|
|
110
|
+
}
|
|
111
|
+
.layout-titlecard__masthead {
|
|
112
|
+
position: absolute;
|
|
113
|
+
top: 0.5in;
|
|
114
|
+
left: 0.6in;
|
|
115
|
+
right: 0.6in;
|
|
116
|
+
display: flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
gap: 0.3in;
|
|
119
|
+
}
|
|
120
|
+
.layout-titlecard__wordmark { height: 0.4in; width: auto; color: #fff; flex: none; }
|
|
121
|
+
.layout-titlecard__rule { flex: 1; height: 1px; background: #fff; }
|
|
122
|
+
/* Optional cover image: the band between the masthead rule (~1.1in) and the
|
|
123
|
+
title lockup (~3.2in from the page bottom). Image is 50% of the band's
|
|
124
|
+
height, natural aspect ratio, left-aligned to the content margin, its
|
|
125
|
+
center anchored at 45% of the band (top 20% + half of 50%). */
|
|
126
|
+
.layout-titlecard__image {
|
|
127
|
+
position: absolute;
|
|
128
|
+
top: 1.1in;
|
|
129
|
+
bottom: 3.2in;
|
|
130
|
+
left: 0.6in;
|
|
131
|
+
right: 0.6in;
|
|
132
|
+
}
|
|
133
|
+
.layout-titlecard__image img {
|
|
134
|
+
position: absolute;
|
|
135
|
+
top: 20%;
|
|
136
|
+
left: 0;
|
|
137
|
+
height: 50%;
|
|
138
|
+
width: auto;
|
|
139
|
+
max-width: 100%;
|
|
140
|
+
object-fit: contain;
|
|
141
|
+
}
|
|
142
|
+
.layout-titlecard__lockup {
|
|
143
|
+
position: absolute;
|
|
144
|
+
left: 0.6in;
|
|
145
|
+
bottom: 1.1in;
|
|
146
|
+
right: 0.6in;
|
|
147
|
+
}
|
|
148
|
+
.layout-titlecard__title {
|
|
149
|
+
font-family: "Kapra Neue Expanded", Helvetica, Arial, sans-serif;
|
|
150
|
+
font-size: 40pt;
|
|
151
|
+
letter-spacing: 0.14em;
|
|
152
|
+
text-transform: uppercase;
|
|
153
|
+
margin: 0 0 0.25in;
|
|
154
|
+
}
|
|
155
|
+
.layout-titlecard__titlerule { display: block; width: 100%; height: 1px; margin-bottom: 0.18in; }
|
|
156
|
+
.layout-titlecard__label {
|
|
157
|
+
font-family: "Kapra Neue Expanded", Helvetica, Arial, sans-serif;
|
|
158
|
+
font-size: 10pt;
|
|
159
|
+
letter-spacing: 0.12em;
|
|
160
|
+
text-transform: uppercase;
|
|
161
|
+
margin: 0;
|
|
162
|
+
}
|
|
163
|
+
.layout-titlecard__date {
|
|
164
|
+
font-family: "Kapra Neue Expanded", Helvetica, Arial, sans-serif;
|
|
165
|
+
font-size: 10pt;
|
|
166
|
+
letter-spacing: 0.12em;
|
|
167
|
+
text-transform: uppercase;
|
|
168
|
+
margin: 0.04in 0 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* ---- document pages ----------------------------------------------------- */
|
|
172
|
+
|
|
173
|
+
.pdoc-title {
|
|
174
|
+
font-family: "Kapra Neue Expanded", Helvetica, Arial, sans-serif;
|
|
175
|
+
font-size: 22pt;
|
|
176
|
+
letter-spacing: 0.04em;
|
|
177
|
+
text-transform: uppercase;
|
|
178
|
+
margin: 0;
|
|
179
|
+
height: 0.9in; /* = DOC_GEOM.titleBandIn; keep in sync */
|
|
180
|
+
}
|
|
181
|
+
/* Sub-page context line inside the title band (uppercase inherits from
|
|
182
|
+
.pdoc-title). Sized to sit with the title within the fixed 0.9in band. */
|
|
183
|
+
.pdoc-kicker {
|
|
184
|
+
display: block;
|
|
185
|
+
font-size: 9pt;
|
|
186
|
+
letter-spacing: 0.12em;
|
|
187
|
+
color: #999999;
|
|
188
|
+
margin-bottom: 0.05in;
|
|
189
|
+
}
|
|
190
|
+
.pdoc-text { position: absolute; }
|
|
191
|
+
.pdoc-h2, .pdoc-h3 {
|
|
192
|
+
font-family: "Kapra Neue Expanded", Helvetica, Arial, sans-serif;
|
|
193
|
+
text-transform: uppercase;
|
|
194
|
+
letter-spacing: 0.04em;
|
|
195
|
+
line-height: 1.3;
|
|
196
|
+
margin: 0;
|
|
197
|
+
}
|
|
198
|
+
.pdoc-h2 { font-size: 14pt; }
|
|
199
|
+
.pdoc-h3 { font-size: 11pt; letter-spacing: 0.08em; }
|
|
200
|
+
.pdoc-prose {
|
|
201
|
+
font-family: "Albra Grotesk", Helvetica, Arial, sans-serif;
|
|
202
|
+
font-size: 12.5pt;
|
|
203
|
+
line-height: 1.55;
|
|
204
|
+
margin: 0;
|
|
205
|
+
}
|
|
206
|
+
/* Self-contained prose: renders identically with or without Tailwind
|
|
207
|
+
preflight. The editor loads preflight but the /print route and the
|
|
208
|
+
hidden measurer do not, so pin every element the upstream sanitizer
|
|
209
|
+
allows to zero margins and give lists explicit markers — otherwise
|
|
210
|
+
measured heights (and page counts) diverge across surfaces. */
|
|
211
|
+
.pdoc-prose :where(p, ul, ol, li, blockquote, h1, h2, h3, h4, h5, h6, pre, hr) { margin: 0; }
|
|
212
|
+
.pdoc-prose ul { list-style: disc; }
|
|
213
|
+
.pdoc-prose ol { list-style: decimal; }
|
|
214
|
+
.pdoc-prose ul, .pdoc-prose ol { padding-left: 0.25in; }
|
|
215
|
+
.pdoc-img { position: absolute; overflow: hidden; }
|
|
216
|
+
.pdoc-img img { width: 100%; height: 100%; display: block; }
|
|
217
|
+
.pdoc-measure { position: fixed; left: -10000px; top: 0; visibility: hidden; pointer-events: none; }
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Content adapters that turn portal content into the document engine's input
|
|
2
|
+
// shape. These live in the primitives lib (outside the sealed document/ subtree)
|
|
3
|
+
// so the editor (EditorShell) can reach them; primitives cannot import core, so
|
|
4
|
+
// the audience-filtered viewer/presentation adapter lives in core's
|
|
5
|
+
// document-route.ts and calls into this module for image mapping.
|
|
6
|
+
import type { MediaManifest } from "../schemas/media";
|
|
7
|
+
import type { Page } from "../schemas/site-config";
|
|
8
|
+
import type { LoadedSection } from "./loader";
|
|
9
|
+
import type { DocImage, DocSectionInput, DocSourceContent } from "../document";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Map a media manifest to the engine's pre-resolved image dictionary. Port of
|
|
13
|
+
* the harness's `portalImagesFrom` against the real MediaManifest type:
|
|
14
|
+
* - only `kind === "image"` entries (videos/animated skipped);
|
|
15
|
+
* - SVGs → `original.svg` for both preview and print;
|
|
16
|
+
* - other images: previewFile = largest variant ≤ 1080 (else the smallest),
|
|
17
|
+
* printFile = largest; images with zero variants are skipped.
|
|
18
|
+
* The manifest is already schema-parsed upstream, so no tolerant re-parse here.
|
|
19
|
+
*/
|
|
20
|
+
export function docImagesFrom(manifest: MediaManifest): Record<string, DocImage> {
|
|
21
|
+
const out: Record<string, DocImage> = {};
|
|
22
|
+
for (const [imageId, item] of Object.entries(manifest.images)) {
|
|
23
|
+
if (item.kind !== "image") continue;
|
|
24
|
+
if (item.mimeType === "image/svg+xml") {
|
|
25
|
+
out[imageId] = { imageId, width: item.width, height: item.height, alt: item.alt, previewFile: "original.svg", printFile: "original.svg" };
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const widths = item.variants.map((v) => v.width).sort((a, b) => a - b);
|
|
29
|
+
if (widths.length === 0) continue;
|
|
30
|
+
const preview = [...widths].reverse().find((w) => w <= 1080) ?? widths[0];
|
|
31
|
+
const print = widths[widths.length - 1];
|
|
32
|
+
out[imageId] = { imageId, width: item.width, height: item.height, alt: item.alt, previewFile: `${preview}.webp`, printFile: `${print}.webp` };
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toSectionInput(s: LoadedSection): DocSectionInput {
|
|
38
|
+
return { id: s.section.id, type: s.section.type, content: s.section.content, options: s.section.options };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Editor path: build the engine's source content from the caller's already-
|
|
43
|
+
* selected page + its draft sections, verbatim. No status/audience filtering —
|
|
44
|
+
* editors see everything (the shell passes in-memory draft content).
|
|
45
|
+
*/
|
|
46
|
+
export function buildDraftDocumentInput(opts: {
|
|
47
|
+
page: Page;
|
|
48
|
+
sections: LoadedSection[];
|
|
49
|
+
manifest: MediaManifest;
|
|
50
|
+
}): DocSourceContent {
|
|
51
|
+
const { page, sections, manifest } = opts;
|
|
52
|
+
return {
|
|
53
|
+
pageTitle: page.title,
|
|
54
|
+
sections: sections.map(toSectionInput),
|
|
55
|
+
images: docImagesFrom(manifest),
|
|
56
|
+
};
|
|
57
|
+
}
|
package/src/lib/pages.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { Page, SiteIndex, SectionMeta } from "../schemas/site-config";
|
|
2
2
|
import { RESERVED_SLUGS } from "../schemas/site-config";
|
|
3
|
+
// Sanctioned outside->inside import: document/ is a sealed subtree, but pages.ts
|
|
4
|
+
// is allowed to reach into its engine to write the page-level document options
|
|
5
|
+
// block (mirrors the same sanctioned import in schemas/site-config.ts).
|
|
6
|
+
import { documentOptionsSchema, type DocumentOptions } from "../document/engine/settings";
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* Display fallback for blank page titles. addPage creates pages with an empty
|
|
@@ -96,6 +100,24 @@ export function setPageFields(
|
|
|
96
100
|
return mapPages(index, (p) => (p.id === pageId ? { ...p, ...patch } : p));
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
export function setPageDocument(index: SiteIndex, pageId: string, patch: Partial<DocumentOptions>): SiteIndex {
|
|
104
|
+
return {
|
|
105
|
+
...index,
|
|
106
|
+
pages: index.pages.map((p) => {
|
|
107
|
+
if (p.id !== pageId) return p;
|
|
108
|
+
const current = p.document ?? documentOptionsSchema.parse({});
|
|
109
|
+
return {
|
|
110
|
+
...p,
|
|
111
|
+
document: documentOptionsSchema.parse({
|
|
112
|
+
...current,
|
|
113
|
+
...patch,
|
|
114
|
+
sections: { ...current.sections, ...(patch.sections ?? {}) },
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
99
121
|
export function setPageAudience(
|
|
100
122
|
index: SiteIndex,
|
|
101
123
|
pageId: string,
|
|
@@ -2,6 +2,10 @@ import { z } from "zod";
|
|
|
2
2
|
import { MediaConfigSchema } from "./media";
|
|
3
3
|
import { HexColorSchema, FontNameSchema } from "./shared";
|
|
4
4
|
import { CORNER_RADIUS_OPTIONS } from "../lib/corner-radius";
|
|
5
|
+
// Sanctioned outside->inside import: document/ is a sealed subtree, but
|
|
6
|
+
// site-config.ts is allowed to reach into its engine to wire the page-level
|
|
7
|
+
// document options block onto PageSchema.
|
|
8
|
+
import { documentOptionsSchema } from "../document/engine/settings";
|
|
5
9
|
|
|
6
10
|
const StatusSchema = z.enum(["draft", "live", "archived", "published"]).transform(
|
|
7
11
|
(val) => val === "published" ? "live" as const : val,
|
|
@@ -15,12 +19,22 @@ export const SectionMetaSchema = z.object({
|
|
|
15
19
|
|
|
16
20
|
export type SectionMeta = z.infer<typeof SectionMetaSchema>;
|
|
17
21
|
|
|
18
|
-
// Page slugs that would shadow a platform route. KEEP IN SYNC with the root
|
|
19
|
-
// /edit/* route patterns injected in
|
|
20
|
-
// root-level route is added there,
|
|
21
|
-
// guards the known set). This is
|
|
22
|
-
// the schema, uniquePageSlug, and
|
|
23
|
-
|
|
22
|
+
// Page slugs that would shadow a platform route. KEEP IN SYNC with the root,
|
|
23
|
+
// /edit/*, and /present/* route patterns injected in
|
|
24
|
+
// packages/core/src/integration.ts — if a new root-level route is added there,
|
|
25
|
+
// add its first segment here (the test in Step 1 guards the known set). This is
|
|
26
|
+
// the single source of truth for reserved slugs; the schema, uniquePageSlug, and
|
|
27
|
+
// /api/save all import it.
|
|
28
|
+
export const RESERVED_SLUGS = ["edit", "api", "login", "set-password", "404", "present"] as const;
|
|
29
|
+
|
|
30
|
+
// Subset of RESERVED_SLUGS the index schema hard-rejects at parse time. Only
|
|
31
|
+
// slugs reserved since before client sites existed may hard-fail here: parse-
|
|
32
|
+
// rejecting a newly-reserved slug would brick an already-deployed index that
|
|
33
|
+
// uses it (viewer AND editor refuse to load, leaving no in-product rename
|
|
34
|
+
// path). Newly reserved slugs ("present") are enforced at authoring time only —
|
|
35
|
+
// PagesModal and uniquePageSlug block new uses via RESERVED_SLUGS, and the
|
|
36
|
+
// injected platform route simply shadows a grandfathered page until renamed.
|
|
37
|
+
const PARSE_REJECTED_SLUGS = ["edit", "api", "login", "set-password", "404"] as const;
|
|
24
38
|
|
|
25
39
|
export const PageStatusSchema = z.enum(["live", "archived"]);
|
|
26
40
|
|
|
@@ -33,6 +47,7 @@ export const PageSchema = z.object({
|
|
|
33
47
|
status: PageStatusSchema,
|
|
34
48
|
access: z.array(z.string()),
|
|
35
49
|
order: z.array(z.string()),
|
|
50
|
+
document: documentOptionsSchema.optional(),
|
|
36
51
|
});
|
|
37
52
|
|
|
38
53
|
export type Page = z.infer<typeof PageSchema>;
|
|
@@ -102,7 +117,7 @@ const CanonicalIndexSchema = z
|
|
|
102
117
|
if (!SLUG_RE.test(p.slug)) {
|
|
103
118
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Invalid slug "${p.slug}"` });
|
|
104
119
|
}
|
|
105
|
-
if ((
|
|
120
|
+
if ((PARSE_REJECTED_SLUGS as readonly string[]).includes(p.slug)) {
|
|
106
121
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Slug "${p.slug}" is reserved` });
|
|
107
122
|
}
|
|
108
123
|
}
|
|
File without changes
|