@anthusai/papyrus 1.3.0 → 1.4.0-next.10
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/amplify/data/resource.js +134 -3
- package/amplify/data/resource.ts +145 -3
- package/amplify/data/schema.js +131 -0
- package/amplify/data/schema.ts +142 -0
- package/amplify/functions/category-action/handler.ts +272 -20
- package/amplify/functions/console-chat-responder/src/main.rs +1 -1
- package/amplify/functions/content-actions/handler.py +23 -0
- package/amplify/functions/content-actions/requirements.txt +1 -1
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/amplify/functions/model-attachment-upload/handler.ts +1 -0
- package/amplify/functions/renditions/requirements.txt +1 -1
- package/amplify/functions/shared/curation-delegation-flow.ts +147 -0
- package/amplify/functions/shared/curation-delegation.ts +115 -0
- package/amplify/functions/shared/python-bundle.js +3 -3
- package/amplify/functions/shared/python-bundle.ts +3 -3
- package/amplify/functions/shared/reference-curation-metadata.ts +51 -0
- package/amplify/functions/shared/slack-events.ts +2 -2
- package/amplify/publication/renditions-function.js +1 -1
- package/amplify/publication/renditions-function.ts +1 -1
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -1
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -1
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +2 -1
- package/app/[year]/[month]/[day]/page.tsx +3 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +2 -0
- package/app/api/archive/editions/route.ts +2 -0
- package/app/api/auth/prospectus/accounts/route.ts +9 -0
- package/app/api/local-preview/derive/route.ts +28 -0
- package/app/api/local-preview/media/route.ts +25 -0
- package/app/archive/page.tsx +3 -0
- package/app/globals.css +28 -28
- package/app/layout.tsx +8 -5
- package/app/newsroom/[section]/[...selection]/page.tsx +0 -1
- package/app/newsroom/[section]/page.tsx +8 -1
- package/app/newsroom/articles/page.tsx +10 -0
- package/app/newsroom/insights/[threadId]/page.tsx +5 -16
- package/app/newsroom/insights/page.tsx +4 -12
- package/app/newsroom/sections/[sectionId]/page.tsx +3 -0
- package/app/page.tsx +30 -1
- package/components/administration-delegations-panel.tsx +156 -0
- package/components/curation-delegation-client.ts +104 -0
- package/components/item-stream-view.tsx +63 -0
- package/components/markus-ir-preview.tsx +47 -2
- package/components/news-desk-client-provider.tsx +17 -2
- package/components/news-desk-page.tsx +0 -1
- package/components/news-desk-taxonomy-client.ts +154 -240
- package/components/newspaper.tsx +4 -4
- package/components/newsroom-article-editor.tsx +138 -40
- package/components/newsroom-assignment-detail.tsx +79 -0
- package/components/newsroom-assignments-view.tsx +1 -1
- package/components/newsroom-board-view.tsx +153 -43
- package/components/newsroom-client-shell.tsx +8 -1
- package/components/newsroom-newsprint-board.css +5 -46
- package/components/newsroom-ops-overview.tsx +27 -5
- package/components/newsroom-organizer-shell.tsx +36 -13
- package/components/newsroom-organizer.css +269 -13
- package/components/newsroom-references-view.tsx +118 -23
- package/components/newsroom-topics-view.tsx +1 -1
- package/components/priming-proof-page.tsx +17 -8
- package/components/prospectus-account-chooser.tsx +131 -0
- package/components/reference-cyclotron-priming.tsx +85 -0
- package/components/reference-cyclotron-status.tsx +5 -5
- package/components/reference-cyclotron-suggestion.tsx +143 -0
- package/components/reference-live-agreement.tsx +62 -0
- package/components/reference-section-suggestion.tsx +104 -0
- package/components/story-reporting-trail-client.ts +91 -0
- package/components/story-reporting-trail.tsx +115 -0
- package/components/topic-steering-workspace.tsx +280 -679
- package/components/ui/sheet.tsx +10 -4
- package/components/use-paged-content-item-versions.ts +151 -0
- package/components/wiki-concepts-view.tsx +153 -0
- package/components/wiki-index-client.ts +51 -0
- package/infra/amplify-app-shell.js +17 -3
- package/infra/amplify-app-shell.ts +18 -3
- package/infra/site-config.js +6 -1
- package/infra/site-config.ts +7 -1
- package/lib/approved-priming-proof-reports.ts +6 -3
- package/lib/approved-priming-proofs/papyrus.json +90 -0
- package/lib/article-flow.ts +95 -0
- package/lib/article-frame-solver.ts +519 -0
- package/lib/blog-feature-solver.ts +1 -109
- package/lib/category-repository.ts +1 -1
- package/lib/chrome-text.ts +504 -0
- package/lib/content-actions-client.ts +3 -0
- package/lib/content-actions-demo.ts +55 -0
- package/lib/content-item-lifecycle.ts +34 -5
- package/lib/content-source-context.ts +9 -1
- package/lib/curation-delegation-helpers.ts +69 -0
- package/lib/cyclotron-suggestions.ts +263 -0
- package/lib/front-matter-fields.ts +42 -0
- package/lib/front-mosaic.ts +1133 -0
- package/lib/furniture.ts +385 -0
- package/lib/graphql-content-repository.ts +42 -7
- package/lib/item-stream.ts +47 -0
- package/lib/labels.ts +37 -0
- package/lib/layout-config.ts +237 -0
- package/lib/layout-plan.ts +40 -11
- package/lib/layout-scenarios.ts +21 -11
- package/lib/layout-types.ts +457 -0
- package/lib/layout-warnings.ts +8 -0
- package/lib/local-preview-section-fixtures.ts +72 -0
- package/lib/local-preview-seed.ts +286 -0
- package/lib/local-preview-tenant.ts +27 -0
- package/lib/local-preview.ts +3 -0
- package/lib/newspaper-layout.ts +17 -3410
- package/lib/newsroom-articles.ts +2 -0
- package/lib/newsroom-assignments.ts +14 -1
- package/lib/newsroom-board.ts +99 -47
- package/lib/newsroom-demo-dashboard.ts +1 -1
- package/lib/newsroom-forum-routes.ts +2 -43
- package/lib/newsroom-index-filters.ts +3 -40
- package/lib/newsroom-media-backend.ts +5 -0
- package/lib/newsroom-nav.ts +1 -3
- package/lib/newsroom-organizer-nav.ts +9 -16
- package/lib/newsroom-section-routing.ts +1 -1
- package/lib/newsroom-sections.ts +12 -0
- package/lib/papyrus-web-locations.ts +3 -24
- package/lib/pretext-layout.ts +22 -0
- package/lib/priming-proof-report.ts +1 -0
- package/lib/prospectus-account-client.ts +90 -0
- package/lib/prospectus-sign-in-routes.ts +45 -0
- package/lib/prospectus-site-identity.ts +13 -1
- package/lib/prospectus-tenant.ts +22 -0
- package/lib/reader-static-params.ts +2 -1
- package/lib/relevance-decisions.ts +29 -0
- package/lib/relevance-status.ts +73 -0
- package/lib/reporting-story-budget.ts +2 -2
- package/lib/semantic-graph.ts +5 -5
- package/lib/site-brand.ts +28 -1
- package/lib/site-env.ts +5 -0
- package/lib/site-section-suggestions.ts +120 -0
- package/lib/site-stack.ts +1 -1
- package/lib/story-reporting-trail.ts +106 -0
- package/lib/tenant-keys.ts +32 -0
- package/lib/text-columns.ts +251 -0
- package/lib/wiki.ts +56 -0
- package/package.json +46 -3
- package/renderers/pretext/presentation-shell.tsx +1 -1
- package/renderers/pretext/rhythm-article-page.tsx +1 -18
- package/routes.manifest.json +34 -15
- package/app/newsroom/work/page.tsx +0 -10
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { type LayoutCursor } from "./pretext-layout";
|
|
2
|
+
import {
|
|
3
|
+
createVerticalRhythm,
|
|
4
|
+
reserveRhythmRows,
|
|
5
|
+
snapDownToRhythm,
|
|
6
|
+
type VerticalRhythm,
|
|
7
|
+
} from "./blog-rhythm";
|
|
8
|
+
import { type ResponsiveSpanPolicy } from "./layout-plan";
|
|
9
|
+
import { type LayoutConfig, type PageChromeMetrics } from "./layout-types";
|
|
10
|
+
import { getFrontGridHeight, getFrontRows } from "./front-mosaic";
|
|
11
|
+
|
|
12
|
+
export const EMPTY_CURSOR: LayoutCursor = { segmentIndex: 0, graphemeIndex: 0 };
|
|
13
|
+
|
|
14
|
+
export const SERIF_TEXT_FONT = 'Georgia, "Times New Roman", serif';
|
|
15
|
+
|
|
16
|
+
export const SANS_TEXT_FONT = "Arial, Helvetica, sans-serif";
|
|
17
|
+
|
|
18
|
+
export const IMAGE_CAPTION_FONT_SIZE = 12;
|
|
19
|
+
|
|
20
|
+
export const IMAGE_CAPTION_HORIZONTAL_PADDING_EM = 0.3;
|
|
21
|
+
|
|
22
|
+
const MASTHEAD_TITLE_GLYPH_HEIGHT_RATIO = 0.736;
|
|
23
|
+
|
|
24
|
+
const MASTHEAD_RULE_HEIGHT = 0;
|
|
25
|
+
|
|
26
|
+
const MASTHEAD_RULE_MARGIN_BOTTOM = 0;
|
|
27
|
+
|
|
28
|
+
const MASTHEAD_TITLE_MARGIN_TOP = 0;
|
|
29
|
+
|
|
30
|
+
const MASTHEAD_PADDING_BOTTOM = 0;
|
|
31
|
+
|
|
32
|
+
const MASTHEAD_BORDER_BOTTOM = 0;
|
|
33
|
+
|
|
34
|
+
const MASTHEAD_MARGIN_BOTTOM = 0;
|
|
35
|
+
|
|
36
|
+
const MASTHEAD_META_BORDER_TOP = 0;
|
|
37
|
+
|
|
38
|
+
const MASTHEAD_META_PADDING_TOP = 0;
|
|
39
|
+
|
|
40
|
+
const MASTHEAD_META_PADDING_BOTTOM = 0;
|
|
41
|
+
|
|
42
|
+
const MASTHEAD_META_BORDER_BOTTOM = 0;
|
|
43
|
+
|
|
44
|
+
const INSIDE_HEADER_PADDING_BOTTOM = 8;
|
|
45
|
+
|
|
46
|
+
const INSIDE_HEADER_BORDER_BOTTOM = 2;
|
|
47
|
+
|
|
48
|
+
const INSIDE_HEADER_MARGIN_BOTTOM = 14;
|
|
49
|
+
|
|
50
|
+
const CONTINUED_TITLE_KICKER_LINE_HEIGHT = 14;
|
|
51
|
+
|
|
52
|
+
export const CONTINUED_TITLE_HEADING_MARGIN_TOP = 5;
|
|
53
|
+
|
|
54
|
+
export const CONTINUED_TITLE_PADDING_BOTTOM = 0;
|
|
55
|
+
|
|
56
|
+
export const CONTINUED_TITLE_BORDER_BOTTOM = 0;
|
|
57
|
+
|
|
58
|
+
export const CONTINUED_TITLE_MARGIN_BOTTOM = 0;
|
|
59
|
+
|
|
60
|
+
export const FURNITURE_COLLISION_GUTTER = 14;
|
|
61
|
+
|
|
62
|
+
export const PULL_QUOTE_VERTICAL_PADDING = 24;
|
|
63
|
+
|
|
64
|
+
const NEWSPAPER_PAINT_BUFFER_PX = 4;
|
|
65
|
+
|
|
66
|
+
export function getMinimumTextFrameHeight(config: LayoutConfig): number {
|
|
67
|
+
return reserveRhythmRows(config.linePaintHeight, config.rhythm);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function pageIdFor(pageNumber: number): string {
|
|
71
|
+
return `page-${pageNumber}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getLayoutConfig(pageWidth: number, viewportHeight: number): LayoutConfig {
|
|
75
|
+
const narrow = pageWidth < 560;
|
|
76
|
+
const medium = pageWidth >= 560 && pageWidth < 1040;
|
|
77
|
+
const bodyFontSize = narrow ? 14 : 15;
|
|
78
|
+
const bodyLineHeight = Math.ceil(bodyFontSize * 1.4);
|
|
79
|
+
const rhythm = createVerticalRhythm(bodyLineHeight, NEWSPAPER_PAINT_BUFFER_PX);
|
|
80
|
+
const gap = narrow ? 14 : 18;
|
|
81
|
+
const rowGap = rhythm.rowHeight;
|
|
82
|
+
const sideMargin = narrow ? 18 : 30;
|
|
83
|
+
const contentWidth = Math.max(280, pageWidth - sideMargin * 2);
|
|
84
|
+
const columnCount = getResponsiveColumnCount(pageWidth, contentWidth, gap);
|
|
85
|
+
const pageChrome = getPageChromeMetrics(pageWidth, narrow, rhythm, columnCount);
|
|
86
|
+
const targetPageHeight = reserveRhythmRows(getTargetPageHeight(pageWidth, viewportHeight), rhythm);
|
|
87
|
+
const frontGridHeight = snapDownToRhythm(getFrontGridHeight(targetPageHeight, narrow, medium), rhythm);
|
|
88
|
+
const frontRowMaxHeight = snapDownToRhythm(narrow ? 520 : medium ? 560 : 620, rhythm);
|
|
89
|
+
const continuationChrome =
|
|
90
|
+
pageChrome.pagePaddingTop +
|
|
91
|
+
pageChrome.insideHeaderHeight +
|
|
92
|
+
pageChrome.continuedTitleHeight +
|
|
93
|
+
pageChrome.pagePaddingBottom;
|
|
94
|
+
const continuationHeight = reserveRhythmRows(Math.max(760, targetPageHeight - continuationChrome), rhythm);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
columnCount,
|
|
98
|
+
contentWidth,
|
|
99
|
+
gap,
|
|
100
|
+
rowGap,
|
|
101
|
+
rhythm,
|
|
102
|
+
pageChrome,
|
|
103
|
+
lineHeight: rhythm.rowHeight,
|
|
104
|
+
linePaintHeight: rhythm.paintHeight,
|
|
105
|
+
bodyFontSize,
|
|
106
|
+
bodyFont: `${bodyFontSize}px ${SERIF_TEXT_FONT}`,
|
|
107
|
+
frontRows: getFrontRows(columnCount, rowGap, frontGridHeight, frontRowMaxHeight, rhythm),
|
|
108
|
+
continuationHeight,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getResponsiveColumnCount(pageWidth: number, contentWidth: number, gap: number): number {
|
|
113
|
+
if (pageWidth < 560) return 1;
|
|
114
|
+
const minimumColumnWidth = 168;
|
|
115
|
+
for (const columnCount of [6, 5, 4, 3, 2] as const) {
|
|
116
|
+
const columnWidth = (contentWidth - gap * (columnCount - 1)) / columnCount;
|
|
117
|
+
if (columnWidth >= minimumColumnWidth) return columnCount;
|
|
118
|
+
}
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getTargetPageHeight(pageWidth: number, viewportHeight: number): number {
|
|
123
|
+
if (pageWidth < 720) return Math.max(3000, viewportHeight * 3.25);
|
|
124
|
+
if (pageWidth < 1040) return Math.max(1900, viewportHeight * 2.1);
|
|
125
|
+
return Math.max(1320, viewportHeight * 1.72);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getMastheadRowPolicy(columnCount: number): { totalRows: number; titleRows: number } {
|
|
129
|
+
if (columnCount <= 1) return { totalRows: 4, titleRows: 2 };
|
|
130
|
+
if (columnCount <= 3) return { totalRows: 5, titleRows: 3 };
|
|
131
|
+
return { totalRows: 6, titleRows: 4 };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getPageChromeMetrics(pageWidth: number, narrow: boolean, rhythm: VerticalRhythm, columnCount: number): PageChromeMetrics {
|
|
135
|
+
const pagePaddingTop = rhythm.rowHeight;
|
|
136
|
+
const pagePaddingX = narrow ? 18 : 30;
|
|
137
|
+
const pagePaddingBottom = reserveRhythmRows(narrow ? 18 : 30, rhythm);
|
|
138
|
+
const mastheadRows = getMastheadRowPolicy(columnCount);
|
|
139
|
+
const mastheadKickerLineHeight = 14;
|
|
140
|
+
const mastheadTitleLineHeight = rhythm.rowHeight * mastheadRows.titleRows;
|
|
141
|
+
const mastheadTitleFontSize = mastheadTitleLineHeight / MASTHEAD_TITLE_GLYPH_HEIGHT_RATIO;
|
|
142
|
+
const mastheadTitleMarginTop = rhythm.rowHeight / 2;
|
|
143
|
+
const mastheadTitleMarginBottom = rhythm.rowHeight / 2;
|
|
144
|
+
const mastheadTitleOpticalShift = -3;
|
|
145
|
+
const mastheadMetaLineHeight = rhythm.rowHeight - MASTHEAD_META_BORDER_TOP - MASTHEAD_META_BORDER_BOTTOM;
|
|
146
|
+
const mastheadMetaGap = narrow ? 5 : 0;
|
|
147
|
+
const mastheadMetaContentHeight = mastheadMetaLineHeight;
|
|
148
|
+
const rawMastheadHeight =
|
|
149
|
+
MASTHEAD_RULE_HEIGHT +
|
|
150
|
+
MASTHEAD_RULE_MARGIN_BOTTOM +
|
|
151
|
+
MASTHEAD_TITLE_MARGIN_TOP +
|
|
152
|
+
mastheadTitleMarginTop +
|
|
153
|
+
mastheadTitleLineHeight +
|
|
154
|
+
mastheadTitleMarginBottom +
|
|
155
|
+
MASTHEAD_META_BORDER_TOP +
|
|
156
|
+
MASTHEAD_META_PADDING_TOP +
|
|
157
|
+
mastheadMetaContentHeight +
|
|
158
|
+
MASTHEAD_META_PADDING_BOTTOM +
|
|
159
|
+
MASTHEAD_META_BORDER_BOTTOM +
|
|
160
|
+
MASTHEAD_PADDING_BOTTOM +
|
|
161
|
+
MASTHEAD_BORDER_BOTTOM +
|
|
162
|
+
MASTHEAD_MARGIN_BOTTOM;
|
|
163
|
+
const mastheadHeight = rhythm.rowHeight * mastheadRows.totalRows;
|
|
164
|
+
const mastheadMetaPaddingTop = MASTHEAD_META_PADDING_TOP + Math.max(0, mastheadHeight - rawMastheadHeight);
|
|
165
|
+
const frontGridMarginTop = rhythm.rowHeight;
|
|
166
|
+
const insideHeaderHeight = INSIDE_HEADER_PADDING_BOTTOM + INSIDE_HEADER_BORDER_BOTTOM + INSIDE_HEADER_MARGIN_BOTTOM + 18;
|
|
167
|
+
const continuedTitleFontSize = narrow ? clamp(pageWidth * 0.075, 28, 42) : clamp(pageWidth * 0.035, 32, 56);
|
|
168
|
+
const continuedTitleLineHeight = continuedTitleFontSize * 0.94;
|
|
169
|
+
const continuedTitleHeight = Math.ceil(
|
|
170
|
+
CONTINUED_TITLE_KICKER_LINE_HEIGHT +
|
|
171
|
+
CONTINUED_TITLE_HEADING_MARGIN_TOP +
|
|
172
|
+
continuedTitleLineHeight * 2 +
|
|
173
|
+
CONTINUED_TITLE_PADDING_BOTTOM +
|
|
174
|
+
CONTINUED_TITLE_BORDER_BOTTOM +
|
|
175
|
+
CONTINUED_TITLE_MARGIN_BOTTOM,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
pagePaddingTop,
|
|
180
|
+
pagePaddingX,
|
|
181
|
+
pagePaddingBottom,
|
|
182
|
+
mastheadHeight,
|
|
183
|
+
mastheadKickerLineHeight,
|
|
184
|
+
mastheadTitleFontSize,
|
|
185
|
+
mastheadTitleLineHeight,
|
|
186
|
+
mastheadTitleMarginTop,
|
|
187
|
+
mastheadTitleMarginBottom,
|
|
188
|
+
mastheadTitleOpticalShift,
|
|
189
|
+
mastheadMetaLineHeight,
|
|
190
|
+
mastheadMetaGap,
|
|
191
|
+
mastheadMetaPaddingTop,
|
|
192
|
+
frontGridMarginTop,
|
|
193
|
+
insideHeaderHeight: reserveRhythmRows(insideHeaderHeight, rhythm),
|
|
194
|
+
continuedTitleHeight: reserveRhythmRows(continuedTitleHeight, rhythm),
|
|
195
|
+
continuedTitleChromeHeight: reserveRhythmRows(
|
|
196
|
+
CONTINUED_TITLE_KICKER_LINE_HEIGHT +
|
|
197
|
+
CONTINUED_TITLE_HEADING_MARGIN_TOP +
|
|
198
|
+
CONTINUED_TITLE_PADDING_BOTTOM +
|
|
199
|
+
CONTINUED_TITLE_BORDER_BOTTOM +
|
|
200
|
+
CONTINUED_TITLE_MARGIN_BOTTOM,
|
|
201
|
+
rhythm,
|
|
202
|
+
),
|
|
203
|
+
continuedTitleFontSize,
|
|
204
|
+
continuedTitleLineHeight,
|
|
205
|
+
continuationSectionSeparatorHeight: rhythm.rowHeight * 2,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function getBlockSpan(config: LayoutConfig, span: ResponsiveSpanPolicy | undefined): number {
|
|
210
|
+
if (config.columnCount === 1) return 1;
|
|
211
|
+
return clamp(span?.preferred ?? config.columnCount, span?.min ?? 1, Math.min(span?.max ?? config.columnCount, config.columnCount));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function getBlockWidth(config: LayoutConfig, span: ResponsiveSpanPolicy | undefined): number {
|
|
215
|
+
return getSpanWidth(config, getBlockSpan(config, span));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function getSpanWidth(config: LayoutConfig, span: number): number {
|
|
219
|
+
const safeSpan = clamp(span, 1, config.columnCount);
|
|
220
|
+
const singleColumn = (config.contentWidth - config.gap * (config.columnCount - 1)) / config.columnCount;
|
|
221
|
+
return singleColumn * safeSpan + config.gap * (safeSpan - 1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function getSpanRect(config: LayoutConfig, columnStart: number, columnSpan: number): { x: number; width: number } {
|
|
225
|
+
const safeStart = clamp(columnStart, 0, Math.max(0, config.columnCount - 1));
|
|
226
|
+
const safeSpan = clamp(columnSpan, 1, config.columnCount - safeStart);
|
|
227
|
+
const columnWidth = getSpanWidth(config, 1);
|
|
228
|
+
const rawX = safeStart * (columnWidth + config.gap);
|
|
229
|
+
const rawRight = rawX + getSpanWidth(config, safeSpan);
|
|
230
|
+
const x = Math.round(rawX);
|
|
231
|
+
const right = Math.min(Math.round(rawRight), Math.floor(config.contentWidth));
|
|
232
|
+
return { x, width: Math.max(1, right - x) };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
236
|
+
return Math.min(max, Math.max(min, value));
|
|
237
|
+
}
|
package/lib/layout-plan.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { warnAboutMissingMedia } from "./layout-warnings";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import {
|
|
3
4
|
type PublicationItem,
|
|
@@ -469,7 +470,12 @@ export type LayoutRegionSpec = z.infer<typeof LayoutRegionSchema>;
|
|
|
469
470
|
export type LayoutPageSpec = z.infer<typeof LayoutPageSchema>;
|
|
470
471
|
export type EditionLayoutPlan = z.infer<typeof EditionLayoutPlanSchema>;
|
|
471
472
|
|
|
472
|
-
export
|
|
473
|
+
export type ArticleCutPolicy = ArticleFrameBlockSpec["cutPolicy"];
|
|
474
|
+
|
|
475
|
+
export function createDefaultEditionLayoutPlan(
|
|
476
|
+
itemIds: string[],
|
|
477
|
+
cutPoliciesByItemId: Record<string, ArticleCutPolicy> = {},
|
|
478
|
+
): EditionLayoutPlan {
|
|
473
479
|
const frontItemIds = getDefaultFrontItemOrder(itemIds);
|
|
474
480
|
const frontBlocks = frontItemIds.map((itemId, index) => ({
|
|
475
481
|
id: `front-${itemId}`,
|
|
@@ -563,7 +569,7 @@ export function createDefaultEditionLayoutPlan(itemIds: string[]): EditionLayout
|
|
|
563
569
|
],
|
|
564
570
|
}
|
|
565
571
|
: undefined,
|
|
566
|
-
cutPolicy:
|
|
572
|
+
cutPolicy: cutPoliciesByItemId[itemId],
|
|
567
573
|
}));
|
|
568
574
|
|
|
569
575
|
const plan: EditionLayoutPlan = normalizeEditionLayoutPlan({
|
|
@@ -812,9 +818,39 @@ export function validateEditionLayoutPlanForItems(
|
|
|
812
818
|
}
|
|
813
819
|
}
|
|
814
820
|
|
|
821
|
+
validateJumpTargetsHaveContinuationFrames(layoutPlan, label);
|
|
822
|
+
|
|
815
823
|
return layoutPlan;
|
|
816
824
|
}
|
|
817
825
|
|
|
826
|
+
function validateJumpTargetsHaveContinuationFrames(layoutPlan: EditionLayoutPlan, label: string): void {
|
|
827
|
+
const continuationFlowKeysByPageNumber = new Map<number, Set<string>>();
|
|
828
|
+
for (const page of layoutPlan.pages) {
|
|
829
|
+
const flowKeys = new Set<string>();
|
|
830
|
+
for (const region of page.regions) {
|
|
831
|
+
for (const block of region.blocks) {
|
|
832
|
+
if (block.type === "articleFrame" && block.startCursor === "current") flowKeys.add(block.flowKey ?? block.itemId);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
continuationFlowKeysByPageNumber.set(page.pageNumber, flowKeys);
|
|
836
|
+
}
|
|
837
|
+
for (const page of layoutPlan.pages) {
|
|
838
|
+
for (const region of page.regions) {
|
|
839
|
+
for (const block of region.blocks) {
|
|
840
|
+
if (block.type !== "articleFrame") continue;
|
|
841
|
+
const jumpTargetPage = block.cutPolicy?.jumpTargetPage;
|
|
842
|
+
if (!jumpTargetPage) continue;
|
|
843
|
+
const flowKey = block.flowKey ?? block.itemId;
|
|
844
|
+
if (!continuationFlowKeysByPageNumber.get(jumpTargetPage)?.has(flowKey)) {
|
|
845
|
+
throw new Error(
|
|
846
|
+
`${label}.pages[${page.pageNumber}].regions.${region.id}.${block.id} advertises page ${jumpTargetPage} but that page has no startCursor "current" frame for flow ${flowKey}`,
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
818
854
|
function validateBlock(
|
|
819
855
|
block: LayoutBlockSpec,
|
|
820
856
|
itemsBySlug: Map<string, PublicationItem>,
|
|
@@ -859,7 +895,7 @@ function validateBlock(
|
|
|
859
895
|
if (block.type === "mediaCluster") {
|
|
860
896
|
const assets = findAssetsByRole(item, block.assetRole);
|
|
861
897
|
if (block.required && assets.length < block.count.min) {
|
|
862
|
-
|
|
898
|
+
warnAboutMissingMedia(`${label}.${block.id} requires at least ${block.count.min} media assets`);
|
|
863
899
|
}
|
|
864
900
|
}
|
|
865
901
|
}
|
|
@@ -867,7 +903,7 @@ function validateBlock(
|
|
|
867
903
|
function validateMediaSpec(item: PublicationItem, media: LayoutMediaSpec, label: string): void {
|
|
868
904
|
const matchingAssets = findAssetsByRole(item, media.assetRole);
|
|
869
905
|
if (media.required && matchingAssets.length === 0) {
|
|
870
|
-
|
|
906
|
+
warnAboutMissingMedia(`${label} requires an image asset${media.assetRole ? ` with role ${media.assetRole}` : ""} and renders without it`);
|
|
871
907
|
}
|
|
872
908
|
}
|
|
873
909
|
|
|
@@ -954,13 +990,6 @@ function getDefaultFrontItemOrder(itemIds: string[]): string[] {
|
|
|
954
990
|
return [itemIds[1], itemIds[0], itemIds[2], ...itemIds.slice(3)];
|
|
955
991
|
}
|
|
956
992
|
|
|
957
|
-
function getDefaultCutPolicy(itemId: string): ArticleFrameBlockSpec["cutPolicy"] | undefined {
|
|
958
|
-
if (itemId === "agent-procedure-patterns") return { maxBodyLines: 22, jumpTargetPage: 2 };
|
|
959
|
-
if (itemId === "schools-reading-lab") return { maxBodyLines: 16, jumpTargetPage: 3 };
|
|
960
|
-
if (itemId === "market-hall") return { maxBodyLines: 14, jumpTargetPage: 3 };
|
|
961
|
-
return undefined;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
993
|
function parseMaybeJson(value: unknown, label: string): unknown {
|
|
965
994
|
if (typeof value !== "string") {
|
|
966
995
|
if (value === undefined || value === null) throw new Error(`${label} is required`);
|
package/lib/layout-scenarios.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Article, type ArticleVideoAsset, articles, editionDate } from "./articles";
|
|
2
2
|
import type { EditionContent, NewsDeskAppendix } from "./content-types";
|
|
3
3
|
import { createEditionSectionPlan } from "./edition-sections";
|
|
4
|
-
import { createDefaultEditionLayoutPlan, type EditionLayoutPlan } from "./layout-plan";
|
|
4
|
+
import { createDefaultEditionLayoutPlan, type ArticleCutPolicy, type EditionLayoutPlan } from "./layout-plan";
|
|
5
5
|
import { articleToPublicationItem, cloneArticle } from "./publication-items";
|
|
6
6
|
import { PILOBOL_SAMPLE_SCENARIO_ID, createPilobolSampleEditionContent } from "./pilobol-sample";
|
|
7
7
|
import { SITE_BRAND } from "./site-brand";
|
|
@@ -17,6 +17,16 @@ type RawLayoutScenario = Omit<LayoutScenario, "sections"> & {
|
|
|
17
17
|
sections?: LayoutScenario["sections"];
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
const fixtureCutPoliciesByItemId: Record<string, ArticleCutPolicy> = {
|
|
21
|
+
"agent-procedure-patterns": { maxBodyLines: 22, jumpTargetPage: 2 },
|
|
22
|
+
"schools-reading-lab": { maxBodyLines: 16, jumpTargetPage: 3 },
|
|
23
|
+
"market-hall": { maxBodyLines: 14, jumpTargetPage: 3 },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function createFixtureEditionLayoutPlan(itemIds: string[]): EditionLayoutPlan {
|
|
27
|
+
return createDefaultEditionLayoutPlan(itemIds, fixtureCutPoliciesByItemId);
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
const defaultScenarioSeed = getDefaultScenarioSeed();
|
|
21
31
|
|
|
22
32
|
const rawLayoutScenarios: RawLayoutScenario[] = [
|
|
@@ -27,7 +37,7 @@ const rawLayoutScenarios: RawLayoutScenario[] = [
|
|
|
27
37
|
editionDate: defaultScenarioSeed.editionDate,
|
|
28
38
|
scenarioId: DEFAULT_LAYOUT_SCENARIO_ID,
|
|
29
39
|
description: defaultScenarioSeed.description,
|
|
30
|
-
layoutPlan:
|
|
40
|
+
layoutPlan: createFixtureEditionLayoutPlan(defaultScenarioSeed.articles.map((article) => article.slug)),
|
|
31
41
|
items: cloneArticles(defaultScenarioSeed.articles).map(articleToPublicationItem),
|
|
32
42
|
suppressNewsDeskAppendix: defaultScenarioSeed.suppressNewsDeskAppendix,
|
|
33
43
|
editionVideo: defaultScenarioSeed.editionVideo,
|
|
@@ -40,7 +50,7 @@ const rawLayoutScenarios: RawLayoutScenario[] = [
|
|
|
40
50
|
editionDate,
|
|
41
51
|
scenarioId: "blank-edition-title",
|
|
42
52
|
description: "A fixture edition with a blank title used to verify masthead fallback labeling.",
|
|
43
|
-
layoutPlan:
|
|
53
|
+
layoutPlan: createFixtureEditionLayoutPlan(articles.map((article) => article.slug)),
|
|
44
54
|
items: cloneArticles(articles).map(articleToPublicationItem),
|
|
45
55
|
},
|
|
46
56
|
{
|
|
@@ -51,7 +61,7 @@ const rawLayoutScenarios: RawLayoutScenario[] = [
|
|
|
51
61
|
scenarioId: "shared-blank-column-pressure",
|
|
52
62
|
description:
|
|
53
63
|
"Shorter shared continuation tails with images and pull quotes, used to prove that empty columns are repaired with solved furniture.",
|
|
54
|
-
layoutPlan:
|
|
64
|
+
layoutPlan: createFixtureEditionLayoutPlan(articles.map((article) => article.slug)),
|
|
55
65
|
items: createSharedBlankColumnPressureArticles().map(articleToPublicationItem),
|
|
56
66
|
},
|
|
57
67
|
{
|
|
@@ -62,7 +72,7 @@ const rawLayoutScenarios: RawLayoutScenario[] = [
|
|
|
62
72
|
scenarioId: "shared-continuation-no-pull-quotes",
|
|
63
73
|
description:
|
|
64
74
|
"The shared continuation page with editorial pull quotes removed, used to prove pull quotes are optional display furniture.",
|
|
65
|
-
layoutPlan:
|
|
75
|
+
layoutPlan: createFixtureEditionLayoutPlan(articles.map((article) => article.slug)),
|
|
66
76
|
items: createNoPullQuoteArticles().map(articleToPublicationItem),
|
|
67
77
|
},
|
|
68
78
|
{
|
|
@@ -106,7 +116,7 @@ const rawLayoutScenarios: RawLayoutScenario[] = [
|
|
|
106
116
|
scenarioId: "long-image-caption",
|
|
107
117
|
description:
|
|
108
118
|
"A continuation image with a long caption proves image furniture reserves enough rhythm rows for complete caption text.",
|
|
109
|
-
layoutPlan:
|
|
119
|
+
layoutPlan: createFixtureEditionLayoutPlan(articles.map((article) => article.slug)),
|
|
110
120
|
items: createLongImageCaptionArticles().map(articleToPublicationItem),
|
|
111
121
|
},
|
|
112
122
|
{
|
|
@@ -116,7 +126,7 @@ const rawLayoutScenarios: RawLayoutScenario[] = [
|
|
|
116
126
|
editionDate,
|
|
117
127
|
scenarioId: "themed-ink-study",
|
|
118
128
|
description: "A raster fixture verifies dark-mode image variant switching in solved newspaper image furniture.",
|
|
119
|
-
layoutPlan:
|
|
129
|
+
layoutPlan: createFixtureEditionLayoutPlan(articles.map((article) => article.slug)),
|
|
120
130
|
items: createThemedInkStudyArticles().map(articleToPublicationItem),
|
|
121
131
|
},
|
|
122
132
|
{
|
|
@@ -377,7 +387,7 @@ function createHeightPolicyLayoutPlan({
|
|
|
377
387
|
defaultRows?: number;
|
|
378
388
|
blockShrinkToContent?: boolean;
|
|
379
389
|
}): EditionLayoutPlan {
|
|
380
|
-
const plan = cloneLayoutPlan(
|
|
390
|
+
const plan = cloneLayoutPlan(createFixtureEditionLayoutPlan(articles.map((article) => article.slug)));
|
|
381
391
|
const region = findPlanRegion(plan, "harbor-continuation");
|
|
382
392
|
if (region) {
|
|
383
393
|
if (regionShrinkToContent === undefined) {
|
|
@@ -472,7 +482,7 @@ function cloneArticles(source: Article[]): Article[] {
|
|
|
472
482
|
}
|
|
473
483
|
|
|
474
484
|
function createCompactChromeLayoutPlan(): EditionLayoutPlan {
|
|
475
|
-
const plan = cloneLayoutPlan(
|
|
485
|
+
const plan = cloneLayoutPlan(createFixtureEditionLayoutPlan(articles.map((article) => article.slug)));
|
|
476
486
|
const frontPage = plan.pages.find((page) => page.pageNumber === 1);
|
|
477
487
|
const frontBlocks = frontPage?.regions.flatMap((region) => region.blocks) ?? [];
|
|
478
488
|
for (const block of frontBlocks) {
|
|
@@ -498,7 +508,7 @@ function createCompactChromeLayoutPlan(): EditionLayoutPlan {
|
|
|
498
508
|
}
|
|
499
509
|
|
|
500
510
|
function createFrontBodyDepthLayoutPlan(): EditionLayoutPlan {
|
|
501
|
-
const plan = cloneLayoutPlan(
|
|
511
|
+
const plan = cloneLayoutPlan(createFixtureEditionLayoutPlan(articles.map((article) => article.slug)));
|
|
502
512
|
for (const blockId of ["front-agent-procedure-patterns", "front-schools-reading-lab", "front-market-hall"]) {
|
|
503
513
|
const block = findPlanBlock(plan, blockId);
|
|
504
514
|
if (block?.type !== "articleFrame") continue;
|
|
@@ -511,7 +521,7 @@ function createFrontBodyDepthLayoutPlan(): EditionLayoutPlan {
|
|
|
511
521
|
}
|
|
512
522
|
|
|
513
523
|
function createFurnitureSufficiencyLayoutPlan(): EditionLayoutPlan {
|
|
514
|
-
const plan = cloneLayoutPlan(
|
|
524
|
+
const plan = cloneLayoutPlan(createFixtureEditionLayoutPlan(articles.map((article) => article.slug)));
|
|
515
525
|
const frontSchoolsBlock = findPlanBlock(plan, "front-schools-reading-lab");
|
|
516
526
|
if (frontSchoolsBlock?.type === "articleFrame") {
|
|
517
527
|
frontSchoolsBlock.cutPolicy = { maxBodyLines: 4, jumpTargetPage: 3 };
|