@anthusai/papyrus 1.3.0 → 1.4.0-next.1

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.
Files changed (36) hide show
  1. package/amplify/functions/model-attachment-upload/handler.ts +1 -0
  2. package/app/newsroom/[section]/page.tsx +6 -0
  3. package/app/newsroom/articles/page.tsx +3 -0
  4. package/components/news-desk-client-provider.tsx +17 -2
  5. package/components/news-desk-taxonomy-client.ts +74 -0
  6. package/components/newspaper.tsx +4 -4
  7. package/components/newsroom-board-view.tsx +59 -26
  8. package/components/newsroom-client-shell.tsx +8 -1
  9. package/components/newsroom-organizer-shell.tsx +12 -7
  10. package/components/newsroom-organizer.css +2 -1
  11. package/components/newsroom-references-view.tsx +18 -0
  12. package/components/reference-cyclotron-priming.tsx +85 -0
  13. package/components/topic-steering-workspace.tsx +35 -1
  14. package/lib/article-flow.ts +95 -0
  15. package/lib/article-frame-solver.ts +519 -0
  16. package/lib/chrome-text.ts +504 -0
  17. package/lib/front-mosaic.ts +1132 -0
  18. package/lib/furniture.ts +385 -0
  19. package/lib/labels.ts +37 -0
  20. package/lib/layout-config.ts +237 -0
  21. package/lib/layout-plan.ts +37 -9
  22. package/lib/layout-scenarios.ts +21 -11
  23. package/lib/layout-types.ts +457 -0
  24. package/lib/local-preview-seed.ts +97 -0
  25. package/lib/local-preview.ts +3 -0
  26. package/lib/newspaper-layout.ts +17 -3410
  27. package/lib/newsroom-board.ts +52 -45
  28. package/lib/newsroom-organizer-nav.ts +3 -4
  29. package/lib/pretext-layout.ts +22 -0
  30. package/lib/relevance-status.ts +73 -0
  31. package/lib/site-brand.ts +8 -1
  32. package/lib/text-columns.ts +251 -0
  33. package/package.json +13 -2
  34. package/renderers/pretext/presentation-shell.tsx +1 -1
  35. package/routes.manifest.json +1 -9
  36. package/app/newsroom/work/page.tsx +0 -10
@@ -469,7 +469,12 @@ export type LayoutRegionSpec = z.infer<typeof LayoutRegionSchema>;
469
469
  export type LayoutPageSpec = z.infer<typeof LayoutPageSchema>;
470
470
  export type EditionLayoutPlan = z.infer<typeof EditionLayoutPlanSchema>;
471
471
 
472
- export function createDefaultEditionLayoutPlan(itemIds: string[]): EditionLayoutPlan {
472
+ export type ArticleCutPolicy = ArticleFrameBlockSpec["cutPolicy"];
473
+
474
+ export function createDefaultEditionLayoutPlan(
475
+ itemIds: string[],
476
+ cutPoliciesByItemId: Record<string, ArticleCutPolicy> = {},
477
+ ): EditionLayoutPlan {
473
478
  const frontItemIds = getDefaultFrontItemOrder(itemIds);
474
479
  const frontBlocks = frontItemIds.map((itemId, index) => ({
475
480
  id: `front-${itemId}`,
@@ -563,7 +568,7 @@ export function createDefaultEditionLayoutPlan(itemIds: string[]): EditionLayout
563
568
  ],
564
569
  }
565
570
  : undefined,
566
- cutPolicy: getDefaultCutPolicy(itemId),
571
+ cutPolicy: cutPoliciesByItemId[itemId],
567
572
  }));
568
573
 
569
574
  const plan: EditionLayoutPlan = normalizeEditionLayoutPlan({
@@ -812,9 +817,39 @@ export function validateEditionLayoutPlanForItems(
812
817
  }
813
818
  }
814
819
 
820
+ validateJumpTargetsHaveContinuationFrames(layoutPlan, label);
821
+
815
822
  return layoutPlan;
816
823
  }
817
824
 
825
+ function validateJumpTargetsHaveContinuationFrames(layoutPlan: EditionLayoutPlan, label: string): void {
826
+ const continuationFlowKeysByPageNumber = new Map<number, Set<string>>();
827
+ for (const page of layoutPlan.pages) {
828
+ const flowKeys = new Set<string>();
829
+ for (const region of page.regions) {
830
+ for (const block of region.blocks) {
831
+ if (block.type === "articleFrame" && block.startCursor === "current") flowKeys.add(block.flowKey ?? block.itemId);
832
+ }
833
+ }
834
+ continuationFlowKeysByPageNumber.set(page.pageNumber, flowKeys);
835
+ }
836
+ for (const page of layoutPlan.pages) {
837
+ for (const region of page.regions) {
838
+ for (const block of region.blocks) {
839
+ if (block.type !== "articleFrame") continue;
840
+ const jumpTargetPage = block.cutPolicy?.jumpTargetPage;
841
+ if (!jumpTargetPage) continue;
842
+ const flowKey = block.flowKey ?? block.itemId;
843
+ if (!continuationFlowKeysByPageNumber.get(jumpTargetPage)?.has(flowKey)) {
844
+ throw new Error(
845
+ `${label}.pages[${page.pageNumber}].regions.${region.id}.${block.id} advertises page ${jumpTargetPage} but that page has no startCursor "current" frame for flow ${flowKey}`,
846
+ );
847
+ }
848
+ }
849
+ }
850
+ }
851
+ }
852
+
818
853
  function validateBlock(
819
854
  block: LayoutBlockSpec,
820
855
  itemsBySlug: Map<string, PublicationItem>,
@@ -954,13 +989,6 @@ function getDefaultFrontItemOrder(itemIds: string[]): string[] {
954
989
  return [itemIds[1], itemIds[0], itemIds[2], ...itemIds.slice(3)];
955
990
  }
956
991
 
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
992
  function parseMaybeJson(value: unknown, label: string): unknown {
965
993
  if (typeof value !== "string") {
966
994
  if (value === undefined || value === null) throw new Error(`${label} is required`);
@@ -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: createDefaultEditionLayoutPlan(defaultScenarioSeed.articles.map((article) => article.slug)),
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: createDefaultEditionLayoutPlan(articles.map((article) => article.slug)),
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: createDefaultEditionLayoutPlan(articles.map((article) => article.slug)),
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: createDefaultEditionLayoutPlan(articles.map((article) => article.slug)),
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: createDefaultEditionLayoutPlan(articles.map((article) => article.slug)),
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: createDefaultEditionLayoutPlan(articles.map((article) => article.slug)),
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(createDefaultEditionLayoutPlan(articles.map((article) => article.slug)));
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(createDefaultEditionLayoutPlan(articles.map((article) => article.slug)));
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(createDefaultEditionLayoutPlan(articles.map((article) => article.slug)));
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(createDefaultEditionLayoutPlan(articles.map((article) => article.slug)));
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 };
@@ -0,0 +1,457 @@
1
+ import { type LayoutCursor, type TextLine } from "./pretext-layout";
2
+ import { type ArticleImageThemeVariants } from "./articles";
3
+ import {
4
+ type ArticleFrameBlockSpec,
5
+ type EditorialPriorityId,
6
+ type HeadlineScaleId,
7
+ type LayoutBlockSpec,
8
+ type LayoutPageSpec,
9
+ type LayoutRegionSpec,
10
+ type ResponsivePlacementSpec,
11
+ } from "./layout-plan";
12
+ import { type ArticlePublicationItem, type PublicationItem } from "./publication-items";
13
+ import { type VerticalRhythm } from "./blog-rhythm";
14
+
15
+ export type SolvedTextLine = TextLine;
16
+
17
+ export type SolvedFurniture = SolvedImageFurniture | SolvedPullQuoteFurniture | SolvedMediaClusterFurniture | SolvedAdFurniture;
18
+
19
+ export type SolvedImageFurniture = {
20
+ kind: "image";
21
+ id: string;
22
+ assetId: string;
23
+ src: string;
24
+ themeVariants?: ArticleImageThemeVariants;
25
+ alt: string;
26
+ caption: string;
27
+ credit: string;
28
+ templateId: string;
29
+ columnStart: number;
30
+ columnSpan: number;
31
+ x: number;
32
+ y: number;
33
+ width: number;
34
+ height: number;
35
+ imageHeight: number;
36
+ captionHeight: number;
37
+ captionFontSize: number;
38
+ captionLineHeight: number;
39
+ aspectRatio: number;
40
+ objectFit: "contain" | "cover";
41
+ objectPosition: string;
42
+ wrapsText: boolean;
43
+ preferredHeight: number;
44
+ };
45
+
46
+ export type SolvedPullQuoteFurniture = {
47
+ kind: "pullQuote";
48
+ id: string;
49
+ text: string;
50
+ templateId: string;
51
+ columnStart: number;
52
+ columnSpan: number;
53
+ x: number;
54
+ y: number;
55
+ width: number;
56
+ height: number;
57
+ fontSize: number;
58
+ lineHeight: number;
59
+ wrapsText: boolean;
60
+ };
61
+
62
+ export type SolvedMediaClusterFurniture = {
63
+ kind: "mediaCluster";
64
+ id: string;
65
+ templateId: string;
66
+ images: SolvedImageFurniture[];
67
+ caption: string;
68
+ x: number;
69
+ y: number;
70
+ width: number;
71
+ height: number;
72
+ wrapsText: false;
73
+ };
74
+
75
+ export type SolvedAdFurniture = {
76
+ kind: "ad";
77
+ id: string;
78
+ src?: string;
79
+ label: string;
80
+ x: number;
81
+ y: number;
82
+ width: number;
83
+ height: number;
84
+ wrapsText: false;
85
+ };
86
+
87
+ export type SolvedChromeBox = {
88
+ id: string;
89
+ slot: "label" | "headline" | "deck" | "byline";
90
+ text: string;
91
+ columnStart: number;
92
+ columnSpan: number;
93
+ x: number;
94
+ y: number;
95
+ width: number;
96
+ height: number;
97
+ fontSize: number;
98
+ lineHeight: number;
99
+ paintBuffer: number;
100
+ paddingTop?: number;
101
+ paddingBottom?: number;
102
+ ruleTopHeight?: number;
103
+ ruleBottomHeight?: number;
104
+ fontFamily: "serif" | "sans";
105
+ fontWeight?: number | string;
106
+ fontStyle?: "normal" | "italic";
107
+ textTransform?: "uppercase";
108
+ };
109
+
110
+ export type SolvedBlock = {
111
+ id: string;
112
+ type: LayoutBlockSpec["type"];
113
+ presetId?: string;
114
+ headlineScale?: HeadlineScaleId;
115
+ editorialPriority?: EditorialPriorityId;
116
+ item?: PublicationItem;
117
+ items?: PublicationItem[];
118
+ article?: ArticlePublicationItem;
119
+ pageNumber: number;
120
+ jumpTargetPage?: number;
121
+ jumpLabel?: string;
122
+ label?: string;
123
+ title?: string;
124
+ deck?: string;
125
+ byline?: string;
126
+ dateline?: string;
127
+ section?: string;
128
+ x: number;
129
+ y: number;
130
+ width: number;
131
+ height: number;
132
+ span: number;
133
+ columnCount: number;
134
+ columns: TextLine[][];
135
+ chromeBoxes?: SolvedChromeBox[];
136
+ furniture: SolvedFurniture[];
137
+ textRange?: PlacedTextRange;
138
+ hasMore?: boolean;
139
+ front?: SolvedFrontStoryMetrics;
140
+ titleChrome?: ContinuationTitleMetrics;
141
+ titleHeight?: number;
142
+ bodyHeight?: number;
143
+ furnitureSufficiency?: FurnitureSufficiencyReport;
144
+ };
145
+
146
+ export type SolvedFrontStoryMetrics = {
147
+ rowHeight: number;
148
+ bodySlotHeight: number;
149
+ chromeHeight: number;
150
+ jumpReserveHeight: number;
151
+ chrome: StoryChromeMetrics;
152
+ gridPlacement?: SolvedFrontGridPlacement;
153
+ composition?: {
154
+ mode: FrontCompositionFlowMode;
155
+ bodyTop: number;
156
+ bodyHeight: number;
157
+ columnCount: number;
158
+ copyBandTop: number;
159
+ copyBandTops: number[];
160
+ };
161
+ };
162
+
163
+ export type SolvedFrontGridPlacement = {
164
+ columnStart: number;
165
+ columnSpan: number;
166
+ rowStart: number;
167
+ rowSpan: number;
168
+ };
169
+
170
+ export type SolvedRegion = {
171
+ id: string;
172
+ type: LayoutRegionSpec["type"];
173
+ role?: string;
174
+ x: number;
175
+ y: number;
176
+ width: number;
177
+ height: number;
178
+ rowHeights?: number[];
179
+ columnCount: number;
180
+ blocks: SolvedBlock[];
181
+ };
182
+
183
+ export type SolvedFrontFooterEntry = {
184
+ section: string;
185
+ articleSlug: string;
186
+ articleTitle: string;
187
+ blockId: string;
188
+ pageNumber: number;
189
+ };
190
+
191
+ export type SolvedFrontFooterUtilityEntry =
192
+ | {
193
+ id: "archive";
194
+ label: string;
195
+ href: string;
196
+ disabled: false;
197
+ }
198
+ | {
199
+ id: "newsDesk";
200
+ label: string;
201
+ href: string;
202
+ disabled: false;
203
+ }
204
+ | {
205
+ id: "settings";
206
+ label: string;
207
+ href: string;
208
+ disabled: false;
209
+ }
210
+ | {
211
+ id: "login";
212
+ label: string;
213
+ disabled: true;
214
+ };
215
+
216
+ export type SolvedFrontFooter = {
217
+ rowHeight: number;
218
+ marginTop: number;
219
+ height: number;
220
+ sectionRows: number;
221
+ sectionColumns: number;
222
+ entries: SolvedFrontFooterEntry[];
223
+ utilityEntries: SolvedFrontFooterUtilityEntry[];
224
+ };
225
+
226
+ export type SolvedPage = {
227
+ id: string;
228
+ pageNumber: number;
229
+ presetId: LayoutPageSpec["presetId"];
230
+ kind: string;
231
+ height: number;
232
+ columnCount: number;
233
+ regions: SolvedRegion[];
234
+ frontFooter?: SolvedFrontFooter;
235
+ };
236
+
237
+ export type NewspaperLayout = {
238
+ columnCount: number;
239
+ contentWidth: number;
240
+ gap: number;
241
+ rowGap: number;
242
+ rhythm: VerticalRhythm;
243
+ pageHeight: number;
244
+ frontPageHeight: number;
245
+ pageHeights: Record<number, number>;
246
+ pageChrome: PageChromeMetrics;
247
+ pages: SolvedPage[];
248
+ textRanges: PlacedTextRange[];
249
+ };
250
+
251
+ export type PageChromeMetrics = {
252
+ pagePaddingTop: number;
253
+ pagePaddingX: number;
254
+ pagePaddingBottom: number;
255
+ mastheadHeight: number;
256
+ mastheadKickerLineHeight: number;
257
+ mastheadTitleFontSize: number;
258
+ mastheadTitleLineHeight: number;
259
+ mastheadTitleMarginTop: number;
260
+ mastheadTitleMarginBottom: number;
261
+ mastheadTitleOpticalShift: number;
262
+ mastheadMetaLineHeight: number;
263
+ mastheadMetaGap: number;
264
+ mastheadMetaPaddingTop: number;
265
+ frontGridMarginTop: number;
266
+ insideHeaderHeight: number;
267
+ continuedTitleHeight: number;
268
+ continuedTitleChromeHeight: number;
269
+ continuedTitleFontSize: number;
270
+ continuedTitleLineHeight: number;
271
+ continuationSectionSeparatorHeight: number;
272
+ };
273
+
274
+ export type ChromeTextBoxMetrics = {
275
+ fontSize: number;
276
+ lineHeight: number;
277
+ paintHeight: number;
278
+ paintBuffer: number;
279
+ contentHeight: number;
280
+ paddingTop: number;
281
+ paddingBottom: number;
282
+ ruleTopHeight: number;
283
+ ruleBottomHeight: number;
284
+ height: number;
285
+ lineCount: number;
286
+ marginBefore: number;
287
+ marginAfter: number;
288
+ totalHeight: number;
289
+ };
290
+
291
+ export type StoryChromeMetrics = {
292
+ borderTopHeight: number;
293
+ paddingTop: number;
294
+ mediaPreludeHeight: number;
295
+ mediaPreludeMarginBottom: number;
296
+ labelFontSize: number;
297
+ labelLineHeight: number;
298
+ labelPaintHeight: number;
299
+ labelPaintBuffer: number;
300
+ labelHeight: number;
301
+ headlineFontSize: number;
302
+ headlineLineHeight: number;
303
+ headlinePaintHeight: number;
304
+ headlinePaintBuffer: number;
305
+ headlineHeight: number;
306
+ headlineLineCount: number;
307
+ headlineMarginTop: number;
308
+ headlineMarginBottom: number;
309
+ deckFontSize: number;
310
+ deckLineHeight: number;
311
+ deckPaintHeight: number;
312
+ deckPaintBuffer: number;
313
+ deckPaddingTop: number;
314
+ deckPaddingBottom: number;
315
+ deckRuleTopHeight: number;
316
+ deckRuleBottomHeight: number;
317
+ deckHeight: number;
318
+ deckLineCount: number;
319
+ deckMarginBottom: number;
320
+ bylineFontSize: number;
321
+ bylineLineHeight: number;
322
+ bylinePaintHeight: number;
323
+ bylinePaintBuffer: number;
324
+ bylinePaddingTop: number;
325
+ bylinePaddingBottom: number;
326
+ bylineHeight: number;
327
+ bylineLineCount: number;
328
+ bylineMarginBottom: number;
329
+ measureChromeHeight: number;
330
+ jumpFontSize: number;
331
+ jumpLineHeight: number;
332
+ jumpPaintHeight: number;
333
+ jumpPaintBuffer: number;
334
+ jumpHeight: number;
335
+ jumpPaddingTop: number;
336
+ jumpPaddingBottom: number;
337
+ jumpBorderTopHeight: number;
338
+ };
339
+
340
+ export type ContinuationTitleMetrics = {
341
+ label: ChromeTextBoxMetrics;
342
+ heading: ChromeTextBoxMetrics;
343
+ chromeHeight: number;
344
+ totalHeight: number;
345
+ };
346
+
347
+ export type FrontStoryRole = "feature" | "rail" | "standard";
348
+
349
+ export type ChromeTextStyle = {
350
+ fontSize: number;
351
+ fontFamily: string;
352
+ fontWeight?: number | string;
353
+ fontStyle?: "normal" | "italic";
354
+ lineHeightEm: number;
355
+ paintHeightEm: number;
356
+ marginBeforeEm?: number;
357
+ marginAfterEm?: number;
358
+ minHeightEm?: number;
359
+ };
360
+
361
+ export type ChromeBoxTreatment = {
362
+ paddingTop: number;
363
+ paddingBottom: number;
364
+ ruleTopHeight?: number;
365
+ ruleBottomHeight?: number;
366
+ };
367
+
368
+ export type PlacedTextRange = {
369
+ articleId: string;
370
+ pageId: string;
371
+ blockId: string;
372
+ startCursor: LayoutCursor;
373
+ endCursor: LayoutCursor;
374
+ exhausted: boolean;
375
+ };
376
+
377
+ export type ArticleFlow = {
378
+ article: ArticlePublicationItem;
379
+ currentCursor: LayoutCursor;
380
+ placedRanges: PlacedTextRange[];
381
+ usedImageAssetIds: Set<string>;
382
+ };
383
+
384
+ export type LayoutConfig = {
385
+ columnCount: number;
386
+ contentWidth: number;
387
+ gap: number;
388
+ rowGap: number;
389
+ rhythm: VerticalRhythm;
390
+ pageChrome: PageChromeMetrics;
391
+ lineHeight: number;
392
+ linePaintHeight: number;
393
+ bodyFontSize: number;
394
+ bodyFont: string;
395
+ frontRows: FrontRow[];
396
+ continuationHeight: number;
397
+ };
398
+
399
+ export type FrontRow = {
400
+ startIndex: number;
401
+ endIndex: number;
402
+ height: number;
403
+ };
404
+
405
+ export type FrontArticlePlacement = {
406
+ block: ArticleFrameBlockSpec;
407
+ originalIndex: number;
408
+ solveIndex: number;
409
+ gridPlacement?: SolvedFrontGridPlacement;
410
+ rowHeight?: number;
411
+ };
412
+
413
+ export type FrontArticleSolveOptions = {
414
+ enforcePlacementHeight?: boolean;
415
+ };
416
+
417
+ export type FrontCompositionFlowMode = "offsetBody" | "integrated" | "titleStackedMedia" | "stackedMedia";
418
+
419
+ export type ArticleFrameCandidate = {
420
+ block: SolvedBlock;
421
+ range: PlacedTextRange;
422
+ score: number;
423
+ whitespace: number;
424
+ furnitureSufficiency?: FurnitureSufficiencyReport;
425
+ };
426
+
427
+ export type FurnitureSufficiencyReport = {
428
+ accepted: boolean;
429
+ reason?: string;
430
+ visibleRows: number;
431
+ furnitureRows: number;
432
+ };
433
+
434
+ export type MediaVariant = {
435
+ id: string;
436
+ placement: ResponsivePlacementSpec | null;
437
+ assetRole?: string;
438
+ columnStart: number;
439
+ columnSpan: number;
440
+ fallbackPenalty: number;
441
+ };
442
+
443
+ export type PlacementVariant = {
444
+ id: string;
445
+ placement: ResponsivePlacementSpec;
446
+ columnStart: number;
447
+ columnSpan: number;
448
+ fallbackPenalty: number;
449
+ };
450
+
451
+ export type PullQuoteVariant = {
452
+ id: string;
453
+ placement: ResponsivePlacementSpec | null;
454
+ columnStart: number;
455
+ columnSpan: number;
456
+ fallbackPenalty: number;
457
+ };