@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
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "./blog-rhythm";
|
|
13
13
|
import { layoutAllTextLines, layoutTextLines, prepareWithSegments, type TextLine, type TextObstacle } from "./pretext-layout";
|
|
14
14
|
|
|
15
|
-
export type FeaturedLayoutMode = "
|
|
15
|
+
export type FeaturedLayoutMode = "float" | "stacked";
|
|
16
16
|
|
|
17
17
|
export type SolvedFeaturedItem = {
|
|
18
18
|
mode: FeaturedLayoutMode;
|
|
@@ -92,19 +92,6 @@ export function solveFeaturedItem(input: SolveFeaturedItemInput): SolvedFeatured
|
|
|
92
92
|
});
|
|
93
93
|
const aspectRatio = getImageAspectRatio(input.imageAsset);
|
|
94
94
|
|
|
95
|
-
if (mode === "obstacle") {
|
|
96
|
-
return solveObstacleFeaturedItem({
|
|
97
|
-
prepared,
|
|
98
|
-
containerWidth: input.containerWidth,
|
|
99
|
-
viewportWidth: input.viewportWidth,
|
|
100
|
-
rhythm,
|
|
101
|
-
textStyle: input.textStyle,
|
|
102
|
-
aspectRatio,
|
|
103
|
-
gap,
|
|
104
|
-
imageAsset: input.imageAsset,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
95
|
if (mode === "float") {
|
|
109
96
|
return solveFloatFeaturedItem({
|
|
110
97
|
prepared,
|
|
@@ -151,96 +138,6 @@ type PreparedSolveInput = {
|
|
|
151
138
|
imageAsset: ArticleImageAsset;
|
|
152
139
|
};
|
|
153
140
|
|
|
154
|
-
function solveObstacleFeaturedItem(input: PreparedSolveInput): SolvedFeaturedItem {
|
|
155
|
-
const maxImageWidth = getFloatImageWidth(input);
|
|
156
|
-
let best: {
|
|
157
|
-
score: number;
|
|
158
|
-
textLines: TextLine[];
|
|
159
|
-
imageHeight: number;
|
|
160
|
-
imageWidth: number;
|
|
161
|
-
captionHeight: number;
|
|
162
|
-
mediaHeight: number;
|
|
163
|
-
copyWidth: number;
|
|
164
|
-
textFrameHeight: number;
|
|
165
|
-
hasMore: boolean;
|
|
166
|
-
} | null = null;
|
|
167
|
-
|
|
168
|
-
for (const imageWidth of getObstacleImageWidths(maxImageWidth, input.rhythm)) {
|
|
169
|
-
const copyWidth = Math.max(
|
|
170
|
-
getFloatCopyMinimum(input.viewportWidth, input.rhythm),
|
|
171
|
-
input.containerWidth - imageWidth - input.gap,
|
|
172
|
-
);
|
|
173
|
-
const imageHeight = snapImageHeight(imageWidth / input.aspectRatio, input.rhythm, input.imageAsset.layout);
|
|
174
|
-
const captionHeight = getFeatureCaptionHeight(input.imageAsset, imageWidth, input.rhythm);
|
|
175
|
-
const mediaHeight = imageHeight + captionHeight;
|
|
176
|
-
const obstacle: TextObstacle = {
|
|
177
|
-
x: Math.max(0, copyWidth - imageWidth),
|
|
178
|
-
y: 0,
|
|
179
|
-
width: imageWidth,
|
|
180
|
-
height: mediaHeight,
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
let candidateHeight = mediaHeight;
|
|
184
|
-
let result = layoutTextLines({
|
|
185
|
-
prepared: input.prepared,
|
|
186
|
-
cursor: { segmentIndex: 0, graphemeIndex: 0 },
|
|
187
|
-
maxHeight: candidateHeight,
|
|
188
|
-
maxWidth: copyWidth,
|
|
189
|
-
lineHeight: input.textStyle.lineHeight,
|
|
190
|
-
linePaintHeight: input.textStyle.linePaintHeight,
|
|
191
|
-
fontSize: input.textStyle.fontSize,
|
|
192
|
-
fontFamily: input.textStyle.fontFamily,
|
|
193
|
-
obstacles: [obstacle],
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
const textBottom = getMeasuredTextHeight(result.lines);
|
|
197
|
-
if (textBottom > 0 && textBottom < candidateHeight) {
|
|
198
|
-
candidateHeight = reserveRhythmRows(textBottom, input.rhythm);
|
|
199
|
-
obstacle.height = candidateHeight;
|
|
200
|
-
result = layoutTextLines({
|
|
201
|
-
prepared: input.prepared,
|
|
202
|
-
cursor: { segmentIndex: 0, graphemeIndex: 0 },
|
|
203
|
-
maxHeight: candidateHeight,
|
|
204
|
-
maxWidth: copyWidth,
|
|
205
|
-
lineHeight: input.textStyle.lineHeight,
|
|
206
|
-
linePaintHeight: input.textStyle.linePaintHeight,
|
|
207
|
-
fontSize: input.textStyle.fontSize,
|
|
208
|
-
fontFamily: input.textStyle.fontFamily,
|
|
209
|
-
obstacles: [obstacle],
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const whitespace = Math.max(0, candidateHeight - getMeasuredTextHeight(result.lines));
|
|
214
|
-
const score = 50_000 - whitespace * 1.4 - Math.abs(candidateHeight - mediaHeight) * 0.35 - Math.abs(maxImageWidth - imageWidth) * 0.2;
|
|
215
|
-
const candidate = {
|
|
216
|
-
score,
|
|
217
|
-
textLines: result.lines,
|
|
218
|
-
imageHeight: Math.min(imageHeight, candidateHeight),
|
|
219
|
-
imageWidth,
|
|
220
|
-
captionHeight,
|
|
221
|
-
mediaHeight: candidateHeight,
|
|
222
|
-
copyWidth,
|
|
223
|
-
textFrameHeight: reserveRhythmRows(getMeasuredTextHeight(result.lines), input.rhythm),
|
|
224
|
-
hasMore: result.hasMore,
|
|
225
|
-
};
|
|
226
|
-
if (!best || candidate.score > best.score) best = candidate;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const fallbackImageHeight = input.rhythm.rowHeight * 6;
|
|
230
|
-
return {
|
|
231
|
-
mode: "obstacle",
|
|
232
|
-
textLines: best?.textLines ?? [],
|
|
233
|
-
imageHeight: best?.imageHeight ?? fallbackImageHeight,
|
|
234
|
-
imageWidth: best?.imageWidth ?? maxImageWidth,
|
|
235
|
-
captionHeight: best?.captionHeight ?? 0,
|
|
236
|
-
mediaHeight: best?.mediaHeight ?? fallbackImageHeight,
|
|
237
|
-
copyWidth: best?.copyWidth ?? input.containerWidth,
|
|
238
|
-
textFrameHeight: best?.textFrameHeight ?? 0,
|
|
239
|
-
hasMore: best?.hasMore ?? false,
|
|
240
|
-
gap: input.gap,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
141
|
function solveFloatFeaturedItem(input: PreparedSolveInput): SolvedFeaturedItem {
|
|
245
142
|
const geometry = computeFloatGeometry({
|
|
246
143
|
containerWidth: input.containerWidth,
|
|
@@ -417,11 +314,6 @@ function getFloatCopyMinimum(viewportWidth: number, rhythm: VerticalRhythm): num
|
|
|
417
314
|
return rhythmLengthRows(viewportWidth <= 600 ? FLOAT_COPY_MIN_PHONE_ROWS : FLOAT_COPY_MIN_DESKTOP_ROWS, rhythm);
|
|
418
315
|
}
|
|
419
316
|
|
|
420
|
-
function getObstacleImageWidths(maximum: number, rhythm: VerticalRhythm): number[] {
|
|
421
|
-
const candidateRows = [0, 1, 2].map((offset) => Math.max(rhythm.rowHeight * 5, maximum - (offset * rhythm.rowHeight)));
|
|
422
|
-
return Array.from(new Set(candidateRows));
|
|
423
|
-
}
|
|
424
|
-
|
|
425
317
|
function snapImageHeight(
|
|
426
318
|
naturalHeight: number,
|
|
427
319
|
rhythm: VerticalRhythm,
|
|
@@ -1510,7 +1510,7 @@ export function createDemoCategorySteeringDashboard(
|
|
|
1510
1510
|
assignments: [
|
|
1511
1511
|
{
|
|
1512
1512
|
id: assignmentReportingId,
|
|
1513
|
-
assignmentTypeKey: "reporting.
|
|
1513
|
+
assignmentTypeKey: "reporting.candidate",
|
|
1514
1514
|
queueKey: "edition:edition-demo:section:news:lane:reporting",
|
|
1515
1515
|
queueStatusKey: "edition:edition-demo:section:news:lane:reporting#open",
|
|
1516
1516
|
status: "open",
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ArticleFrameBlockSpec,
|
|
3
|
+
type ArticleFrameChromeSpec,
|
|
4
|
+
type ArticleFrameCompositionSlotSpec,
|
|
5
|
+
type ChromeTextSlotSpec,
|
|
6
|
+
type HeadlineScaleId,
|
|
7
|
+
} from "./layout-plan";
|
|
8
|
+
import { type ArticlePublicationItem } from "./publication-items";
|
|
9
|
+
import { getLineStackHeight, reserveRhythmRows, snapUpToRhythm, type VerticalRhythm } from "./blog-rhythm";
|
|
10
|
+
import { layoutNextLine, prepareWithSegmentsCached } from "./pretext-layout";
|
|
11
|
+
import { resolvePreferredPlacementVariant } from "./furniture";
|
|
12
|
+
import {
|
|
13
|
+
type ChromeBoxTreatment,
|
|
14
|
+
type ChromeTextBoxMetrics,
|
|
15
|
+
type ChromeTextStyle,
|
|
16
|
+
type ContinuationTitleMetrics,
|
|
17
|
+
type FrontStoryRole,
|
|
18
|
+
type LayoutConfig,
|
|
19
|
+
type PlacementVariant,
|
|
20
|
+
type SolvedChromeBox,
|
|
21
|
+
type StoryChromeMetrics,
|
|
22
|
+
} from "./layout-types";
|
|
23
|
+
import {
|
|
24
|
+
CONTINUED_TITLE_BORDER_BOTTOM,
|
|
25
|
+
CONTINUED_TITLE_HEADING_MARGIN_TOP,
|
|
26
|
+
CONTINUED_TITLE_MARGIN_BOTTOM,
|
|
27
|
+
CONTINUED_TITLE_PADDING_BOTTOM,
|
|
28
|
+
EMPTY_CURSOR,
|
|
29
|
+
SANS_TEXT_FONT,
|
|
30
|
+
SERIF_TEXT_FONT,
|
|
31
|
+
clamp,
|
|
32
|
+
getSpanRect,
|
|
33
|
+
} from "./layout-config";
|
|
34
|
+
import { formatByline } from "./labels";
|
|
35
|
+
|
|
36
|
+
const KICKER_BASE_FONT_SIZE = 11.5;
|
|
37
|
+
|
|
38
|
+
const KICKER_BASE_ROW_HEIGHT = 19;
|
|
39
|
+
|
|
40
|
+
function getKickerFontSize(rhythmRowHeight: number): number {
|
|
41
|
+
return Math.round((KICKER_BASE_FONT_SIZE * rhythmRowHeight / KICKER_BASE_ROW_HEIGHT) * 10) / 10;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function solveCompositionChromeBox({
|
|
45
|
+
slot,
|
|
46
|
+
item,
|
|
47
|
+
storyRole,
|
|
48
|
+
headlineScale,
|
|
49
|
+
localConfig,
|
|
50
|
+
y,
|
|
51
|
+
globalYOffset,
|
|
52
|
+
chromeOverrides,
|
|
53
|
+
variant = resolvePreferredPlacementVariant(slot.placement, localConfig),
|
|
54
|
+
}: {
|
|
55
|
+
slot: ArticleFrameCompositionSlotSpec;
|
|
56
|
+
item: ArticlePublicationItem;
|
|
57
|
+
storyRole: FrontStoryRole;
|
|
58
|
+
headlineScale: HeadlineScaleId;
|
|
59
|
+
localConfig: LayoutConfig;
|
|
60
|
+
y: number;
|
|
61
|
+
globalYOffset: number;
|
|
62
|
+
chromeOverrides?: ArticleFrameChromeSpec;
|
|
63
|
+
variant?: PlacementVariant | null;
|
|
64
|
+
}): { localBox: SolvedChromeBox; globalBox: SolvedChromeBox; localReserveBottom: number } | null {
|
|
65
|
+
if (!variant || !isTextCompositionSlot(slot.slot)) return null;
|
|
66
|
+
const text = getCompositionSlotText(slot.slot, item);
|
|
67
|
+
if (!text) return null;
|
|
68
|
+
const { x, width } = getSpanRect(localConfig, variant.columnStart, variant.columnSpan);
|
|
69
|
+
const style = getCompositionSlotStyle(slot.slot, localConfig, storyRole, headlineScale, width);
|
|
70
|
+
const metrics = solveChromeTextBox(text, width, style, localConfig.rhythm, chromeOverrides?.[slot.slot]);
|
|
71
|
+
const treatedMetrics = applyFrontChromeTreatment(slot.slot, metrics, localConfig, storyRole);
|
|
72
|
+
const boxY = snapUpToRhythm(y + treatedMetrics.marginBefore, localConfig.rhythm);
|
|
73
|
+
const localBox: SolvedChromeBox = {
|
|
74
|
+
id: `${item.slug}-${slot.slot}-${variant.id}`,
|
|
75
|
+
slot: slot.slot,
|
|
76
|
+
text,
|
|
77
|
+
columnStart: variant.columnStart,
|
|
78
|
+
columnSpan: variant.columnSpan,
|
|
79
|
+
x,
|
|
80
|
+
y: boxY,
|
|
81
|
+
width,
|
|
82
|
+
height: treatedMetrics.height,
|
|
83
|
+
fontSize: treatedMetrics.fontSize,
|
|
84
|
+
lineHeight: treatedMetrics.lineHeight,
|
|
85
|
+
paintBuffer: treatedMetrics.paintBuffer,
|
|
86
|
+
paddingTop: treatedMetrics.paddingTop,
|
|
87
|
+
paddingBottom: treatedMetrics.paddingBottom,
|
|
88
|
+
ruleTopHeight: treatedMetrics.ruleTopHeight,
|
|
89
|
+
ruleBottomHeight: treatedMetrics.ruleBottomHeight,
|
|
90
|
+
fontFamily: style.fontFamily === SANS_TEXT_FONT ? "sans" : "serif",
|
|
91
|
+
fontWeight: style.fontWeight,
|
|
92
|
+
fontStyle: style.fontStyle,
|
|
93
|
+
textTransform: slot.slot === "label" || slot.slot === "byline" ? "uppercase" : undefined,
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
localBox,
|
|
97
|
+
globalBox: { ...localBox, y: localBox.y + globalYOffset },
|
|
98
|
+
localReserveBottom: reserveRhythmRows(boxY + treatedMetrics.height + treatedMetrics.marginAfter, localConfig.rhythm),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function isTextCompositionSlot(slot: ArticleFrameCompositionSlotSpec["slot"]): slot is SolvedChromeBox["slot"] {
|
|
103
|
+
return slot === "label" || slot === "headline" || slot === "deck" || slot === "byline";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getCompositionSlotText(slot: SolvedChromeBox["slot"], article: ArticlePublicationItem): string {
|
|
107
|
+
if (slot === "label") return article.section;
|
|
108
|
+
if (slot === "headline") return article.headline;
|
|
109
|
+
if (slot === "deck") return article.deck;
|
|
110
|
+
return formatByline(article);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getCompositionSlotStyle(
|
|
114
|
+
slot: SolvedChromeBox["slot"],
|
|
115
|
+
config: LayoutConfig,
|
|
116
|
+
storyRole: FrontStoryRole,
|
|
117
|
+
headlineScale: HeadlineScaleId,
|
|
118
|
+
slotWidth: number,
|
|
119
|
+
): ChromeTextStyle {
|
|
120
|
+
if (slot === "label") {
|
|
121
|
+
const labelFontSize = getKickerFontSize(config.rhythm.rowHeight);
|
|
122
|
+
return {
|
|
123
|
+
fontSize: labelFontSize,
|
|
124
|
+
fontFamily: SANS_TEXT_FONT,
|
|
125
|
+
fontWeight: 800,
|
|
126
|
+
lineHeightEm: config.rhythm.rowHeight / labelFontSize,
|
|
127
|
+
paintHeightEm: config.rhythm.rowHeight / labelFontSize,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (slot === "headline") {
|
|
131
|
+
const displayHeadline = isDisplayHeadlineScale(headlineScale);
|
|
132
|
+
const fontSize = getHeadlineFontSize(config, headlineScale, slotWidth);
|
|
133
|
+
return {
|
|
134
|
+
fontSize,
|
|
135
|
+
fontFamily: SERIF_TEXT_FONT,
|
|
136
|
+
fontWeight: 900,
|
|
137
|
+
lineHeightEm: displayHeadline ? 0.96 : 1,
|
|
138
|
+
paintHeightEm: displayHeadline ? 1.1 : 1.12,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (slot === "deck") {
|
|
142
|
+
const feature = storyRole === "feature";
|
|
143
|
+
const fontSize = config.columnCount === 1 ? 17 : feature ? 19 : 16;
|
|
144
|
+
const lineHeight = feature ? Math.round(fontSize * 1.16) : config.rhythm.rowHeight;
|
|
145
|
+
return {
|
|
146
|
+
fontSize,
|
|
147
|
+
fontFamily: SERIF_TEXT_FONT,
|
|
148
|
+
fontStyle: "italic",
|
|
149
|
+
lineHeightEm: lineHeight / fontSize,
|
|
150
|
+
paintHeightEm: lineHeight / fontSize,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
fontSize: 12,
|
|
155
|
+
fontFamily: SANS_TEXT_FONT,
|
|
156
|
+
fontWeight: 800,
|
|
157
|
+
lineHeightEm: config.rhythm.rowHeight / 12,
|
|
158
|
+
paintHeightEm: config.rhythm.rowHeight / 12,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function applyFrontChromeTreatment(
|
|
163
|
+
slot: SolvedChromeBox["slot"],
|
|
164
|
+
metrics: ChromeTextBoxMetrics,
|
|
165
|
+
config: LayoutConfig,
|
|
166
|
+
storyRole: FrontStoryRole,
|
|
167
|
+
): ChromeTextBoxMetrics {
|
|
168
|
+
if (slot === "deck") {
|
|
169
|
+
const padding = Math.round(config.rhythm.rowHeight * (storyRole === "feature" ? 0.48 : 0.34));
|
|
170
|
+
return applyChromeBoxTreatment(metrics, {
|
|
171
|
+
paddingTop: padding,
|
|
172
|
+
paddingBottom: padding,
|
|
173
|
+
ruleTopHeight: 1,
|
|
174
|
+
ruleBottomHeight: 1,
|
|
175
|
+
}, config.rhythm);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (slot === "byline") {
|
|
179
|
+
return applyChromeBoxTreatment(metrics, {
|
|
180
|
+
paddingTop: config.rhythm.rowHeight,
|
|
181
|
+
paddingBottom: config.rhythm.rowHeight,
|
|
182
|
+
}, config.rhythm);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return metrics;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function applyChromeBoxTreatment(
|
|
189
|
+
metrics: ChromeTextBoxMetrics,
|
|
190
|
+
treatment: ChromeBoxTreatment,
|
|
191
|
+
rhythm: VerticalRhythm,
|
|
192
|
+
): ChromeTextBoxMetrics {
|
|
193
|
+
const ruleTopHeight = treatment.ruleTopHeight ?? 0;
|
|
194
|
+
const ruleBottomHeight = treatment.ruleBottomHeight ?? 0;
|
|
195
|
+
const rawHeight =
|
|
196
|
+
ruleTopHeight +
|
|
197
|
+
treatment.paddingTop +
|
|
198
|
+
metrics.contentHeight +
|
|
199
|
+
treatment.paddingBottom +
|
|
200
|
+
ruleBottomHeight;
|
|
201
|
+
const height = reserveRhythmRows(rawHeight, rhythm);
|
|
202
|
+
const paddingBottom = treatment.paddingBottom + Math.max(0, height - rawHeight);
|
|
203
|
+
const rawTotalHeight = metrics.marginBefore + height + metrics.marginAfter;
|
|
204
|
+
const totalHeight = reserveRhythmRows(rawTotalHeight, rhythm);
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
...metrics,
|
|
208
|
+
height,
|
|
209
|
+
paddingTop: treatment.paddingTop,
|
|
210
|
+
paddingBottom,
|
|
211
|
+
ruleTopHeight,
|
|
212
|
+
ruleBottomHeight,
|
|
213
|
+
totalHeight,
|
|
214
|
+
marginAfter: metrics.marginAfter + (totalHeight - rawTotalHeight),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function getStoryChromeMetrics(
|
|
219
|
+
config: LayoutConfig,
|
|
220
|
+
article: ArticlePublicationItem,
|
|
221
|
+
storyRole: FrontStoryRole,
|
|
222
|
+
headlineScale: HeadlineScaleId,
|
|
223
|
+
blockWidth: number,
|
|
224
|
+
mediaPreludeHeight = 0,
|
|
225
|
+
overrides?: ArticleFrameChromeSpec,
|
|
226
|
+
): StoryChromeMetrics {
|
|
227
|
+
const feature = storyRole === "feature";
|
|
228
|
+
const displayHeadline = isDisplayHeadlineScale(headlineScale);
|
|
229
|
+
const headlineFontSize = getHeadlineFontSize(config, headlineScale, blockWidth);
|
|
230
|
+
const labelFontSize = getKickerFontSize(config.rhythm.rowHeight);
|
|
231
|
+
const label = solveChromeTextBox(article.section, blockWidth, {
|
|
232
|
+
fontSize: labelFontSize,
|
|
233
|
+
fontFamily: SANS_TEXT_FONT,
|
|
234
|
+
fontWeight: 800,
|
|
235
|
+
lineHeightEm: config.rhythm.rowHeight / labelFontSize,
|
|
236
|
+
paintHeightEm: config.rhythm.rowHeight / labelFontSize,
|
|
237
|
+
}, config.rhythm, overrides?.label);
|
|
238
|
+
const headline = solveChromeTextBox(article.headline, blockWidth, {
|
|
239
|
+
fontSize: headlineFontSize,
|
|
240
|
+
fontFamily: SERIF_TEXT_FONT,
|
|
241
|
+
fontWeight: 900,
|
|
242
|
+
lineHeightEm: displayHeadline ? 0.96 : 1,
|
|
243
|
+
paintHeightEm: displayHeadline ? 1.1 : 1.12,
|
|
244
|
+
}, config.rhythm, overrides?.headline);
|
|
245
|
+
const deckFontSize = config.columnCount === 1 ? 17 : feature ? 19 : 16;
|
|
246
|
+
const deckLineHeight = feature ? Math.round(deckFontSize * 1.16) : config.rhythm.rowHeight;
|
|
247
|
+
const deck = applyFrontChromeTreatment("deck", solveChromeTextBox(article.deck, blockWidth, {
|
|
248
|
+
fontSize: deckFontSize,
|
|
249
|
+
fontFamily: SERIF_TEXT_FONT,
|
|
250
|
+
fontStyle: "italic",
|
|
251
|
+
lineHeightEm: deckLineHeight / deckFontSize,
|
|
252
|
+
paintHeightEm: deckLineHeight / deckFontSize,
|
|
253
|
+
}, config.rhythm, overrides?.deck), config, storyRole);
|
|
254
|
+
const bylineFontSize = 12;
|
|
255
|
+
const byline = applyFrontChromeTreatment("byline", solveChromeTextBox(formatByline(article), blockWidth, {
|
|
256
|
+
fontSize: bylineFontSize,
|
|
257
|
+
fontFamily: SANS_TEXT_FONT,
|
|
258
|
+
fontWeight: 800,
|
|
259
|
+
lineHeightEm: config.rhythm.rowHeight / bylineFontSize,
|
|
260
|
+
paintHeightEm: config.rhythm.rowHeight / bylineFontSize,
|
|
261
|
+
}, config.rhythm, overrides?.byline), config, storyRole);
|
|
262
|
+
const jump = solveChromeTextBox("SEE MORE", blockWidth, {
|
|
263
|
+
fontSize: 12.2,
|
|
264
|
+
fontFamily: SANS_TEXT_FONT,
|
|
265
|
+
fontWeight: 800,
|
|
266
|
+
lineHeightEm: config.rhythm.rowHeight / 12.2,
|
|
267
|
+
paintHeightEm: config.rhythm.rowHeight / 12.2,
|
|
268
|
+
}, config.rhythm, overrides?.jumpLine);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
borderTopHeight: 0,
|
|
272
|
+
paddingTop: 0,
|
|
273
|
+
mediaPreludeHeight,
|
|
274
|
+
mediaPreludeMarginBottom: mediaPreludeHeight > 0 ? config.rhythm.rowHeight : 0,
|
|
275
|
+
labelFontSize: label.fontSize,
|
|
276
|
+
labelLineHeight: label.lineHeight,
|
|
277
|
+
labelPaintHeight: label.paintHeight,
|
|
278
|
+
labelPaintBuffer: label.paintBuffer,
|
|
279
|
+
labelHeight: label.height,
|
|
280
|
+
headlineFontSize: headline.fontSize,
|
|
281
|
+
headlineLineHeight: headline.lineHeight,
|
|
282
|
+
headlinePaintHeight: headline.paintHeight,
|
|
283
|
+
headlinePaintBuffer: headline.paintBuffer,
|
|
284
|
+
headlineHeight: headline.height,
|
|
285
|
+
headlineLineCount: headline.lineCount,
|
|
286
|
+
headlineMarginTop: headline.marginBefore,
|
|
287
|
+
headlineMarginBottom: headline.marginAfter,
|
|
288
|
+
deckFontSize: deck.fontSize,
|
|
289
|
+
deckLineHeight: deck.lineHeight,
|
|
290
|
+
deckPaintHeight: deck.paintHeight,
|
|
291
|
+
deckPaintBuffer: deck.paintBuffer,
|
|
292
|
+
deckPaddingTop: deck.paddingTop,
|
|
293
|
+
deckPaddingBottom: deck.paddingBottom,
|
|
294
|
+
deckRuleTopHeight: deck.ruleTopHeight,
|
|
295
|
+
deckRuleBottomHeight: deck.ruleBottomHeight,
|
|
296
|
+
deckHeight: deck.height,
|
|
297
|
+
deckLineCount: deck.lineCount,
|
|
298
|
+
deckMarginBottom: deck.marginAfter,
|
|
299
|
+
bylineFontSize: byline.fontSize,
|
|
300
|
+
bylineLineHeight: byline.lineHeight,
|
|
301
|
+
bylinePaintHeight: byline.paintHeight,
|
|
302
|
+
bylinePaintBuffer: byline.paintBuffer,
|
|
303
|
+
bylinePaddingTop: byline.paddingTop,
|
|
304
|
+
bylinePaddingBottom: byline.paddingBottom,
|
|
305
|
+
bylineHeight: byline.height,
|
|
306
|
+
bylineLineCount: byline.lineCount,
|
|
307
|
+
bylineMarginBottom: byline.marginAfter,
|
|
308
|
+
measureChromeHeight: 0,
|
|
309
|
+
jumpFontSize: jump.fontSize,
|
|
310
|
+
jumpLineHeight: jump.lineHeight,
|
|
311
|
+
jumpPaintHeight: jump.paintHeight,
|
|
312
|
+
jumpPaintBuffer: jump.paintBuffer,
|
|
313
|
+
jumpHeight: jump.height,
|
|
314
|
+
jumpPaddingTop: 0,
|
|
315
|
+
jumpPaddingBottom: config.rhythm.rowHeight,
|
|
316
|
+
jumpBorderTopHeight: 0,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function resolveFrontStoryRole(blockSpec: ArticleFrameBlockSpec, span: number, hasFeatureCue: boolean): FrontStoryRole {
|
|
321
|
+
if (blockSpec.role === "feature" || blockSpec.role === "primary") return "feature";
|
|
322
|
+
if (blockSpec.role === "rail") return "rail";
|
|
323
|
+
if (span >= 4 || hasFeatureCue) return "feature";
|
|
324
|
+
return "standard";
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function resolveHeadlineScale(blockSpec: ArticleFrameBlockSpec, storyRole: FrontStoryRole): HeadlineScaleId {
|
|
328
|
+
if (blockSpec.typography?.headlineScale) return blockSpec.typography.headlineScale;
|
|
329
|
+
if (storyRole === "feature") return "feature";
|
|
330
|
+
if (storyRole === "rail") return "rail";
|
|
331
|
+
return "standard";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function isDisplayHeadlineScale(headlineScale: HeadlineScaleId): boolean {
|
|
335
|
+
return headlineScale === "banner" || headlineScale === "feature";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function getHeadlineFontSize(config: LayoutConfig, headlineScale: HeadlineScaleId, blockWidth: number): number {
|
|
339
|
+
const width = Math.max(blockWidth, 1);
|
|
340
|
+
if (config.columnCount === 1) {
|
|
341
|
+
if (headlineScale === "banner") return clamp(width * 0.13, 42, 82);
|
|
342
|
+
if (headlineScale === "feature") return clamp(width * 0.11, 34, 54);
|
|
343
|
+
if (headlineScale === "standard") return clamp(width * 0.095, 29, 46);
|
|
344
|
+
if (headlineScale === "rail") return clamp(width * 0.09, 27, 42);
|
|
345
|
+
return clamp(width * 0.075, 22, 32);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (headlineScale === "banner") return clamp(width * 0.105, 54, 112);
|
|
349
|
+
if (headlineScale === "feature") return clamp(width * 0.052, 34, 58);
|
|
350
|
+
if (headlineScale === "standard") return clamp(width * 0.07, 27, 40);
|
|
351
|
+
if (headlineScale === "rail") return clamp(width * 0.15, 24, 34);
|
|
352
|
+
return clamp(width * 0.055, 18, 26);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function solveChromeTextBox(
|
|
356
|
+
text: string,
|
|
357
|
+
maxWidth: number,
|
|
358
|
+
defaults: ChromeTextStyle,
|
|
359
|
+
rhythm: VerticalRhythm,
|
|
360
|
+
overrides?: ChromeTextSlotSpec,
|
|
361
|
+
): ChromeTextBoxMetrics {
|
|
362
|
+
const fontSize = defaults.fontSize;
|
|
363
|
+
const lineHeight = Math.ceil(emToPx(parseEmOverride(overrides?.lineHeight, defaults.lineHeightEm), fontSize));
|
|
364
|
+
const paintHeight = Math.ceil(emToPx(parseEmOverride(overrides?.paintHeight, defaults.paintHeightEm), fontSize));
|
|
365
|
+
const marginBefore = Math.round(emToPx(parseEmOverride(overrides?.marginBefore, defaults.marginBeforeEm ?? 0), fontSize));
|
|
366
|
+
const rawMarginAfter = Math.round(emToPx(parseEmOverride(overrides?.marginAfter, defaults.marginAfterEm ?? 0), fontSize));
|
|
367
|
+
const minHeight = Math.ceil(emToPx(parseEmOverride(overrides?.minHeight, defaults.minHeightEm ?? 0), fontSize));
|
|
368
|
+
const measured = measureWrappedTextBlock(text, buildChromeFont(defaults), maxWidth, lineHeight, paintHeight);
|
|
369
|
+
const height = reserveRhythmRows(Math.max(measured.height, minHeight), rhythm);
|
|
370
|
+
const rawTotalHeight = marginBefore + height + rawMarginAfter;
|
|
371
|
+
const totalHeight = reserveRhythmRows(rawTotalHeight, rhythm);
|
|
372
|
+
const marginAfter = rawMarginAfter + (totalHeight - rawTotalHeight);
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
fontSize,
|
|
376
|
+
lineHeight,
|
|
377
|
+
paintHeight,
|
|
378
|
+
paintBuffer: Math.max(0, paintHeight - lineHeight),
|
|
379
|
+
contentHeight: measured.height,
|
|
380
|
+
paddingTop: 0,
|
|
381
|
+
paddingBottom: 0,
|
|
382
|
+
ruleTopHeight: 0,
|
|
383
|
+
ruleBottomHeight: 0,
|
|
384
|
+
height,
|
|
385
|
+
lineCount: measured.lineCount,
|
|
386
|
+
marginBefore,
|
|
387
|
+
marginAfter,
|
|
388
|
+
totalHeight,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function buildChromeFont(style: ChromeTextStyle): string {
|
|
393
|
+
const pieces: string[] = [];
|
|
394
|
+
if (style.fontStyle && style.fontStyle !== "normal") pieces.push(style.fontStyle);
|
|
395
|
+
if (style.fontWeight) pieces.push(String(style.fontWeight));
|
|
396
|
+
pieces.push(`${style.fontSize}px`);
|
|
397
|
+
pieces.push(style.fontFamily);
|
|
398
|
+
return pieces.join(" ");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function parseEmOverride(value: ChromeTextSlotSpec[keyof ChromeTextSlotSpec] | undefined, fallback: number): number {
|
|
402
|
+
if (!value) return fallback;
|
|
403
|
+
return parseEmValue(value);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function parseEmValue(value: string): number {
|
|
407
|
+
return Number(value.slice(0, -2));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function emToPx(value: number, fontSize: number): number {
|
|
411
|
+
return value * fontSize;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function getStoryChromeHeight(chrome: StoryChromeMetrics): number {
|
|
415
|
+
return (
|
|
416
|
+
chrome.borderTopHeight +
|
|
417
|
+
chrome.paddingTop +
|
|
418
|
+
chrome.mediaPreludeHeight +
|
|
419
|
+
chrome.mediaPreludeMarginBottom +
|
|
420
|
+
chrome.labelHeight +
|
|
421
|
+
chrome.headlineMarginTop +
|
|
422
|
+
chrome.headlineHeight +
|
|
423
|
+
chrome.headlineMarginBottom +
|
|
424
|
+
chrome.deckHeight +
|
|
425
|
+
chrome.deckMarginBottom +
|
|
426
|
+
chrome.bylineHeight +
|
|
427
|
+
chrome.bylineMarginBottom +
|
|
428
|
+
chrome.measureChromeHeight
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function getStoryJumpReserveHeight(chrome: StoryChromeMetrics): number {
|
|
433
|
+
return chrome.jumpBorderTopHeight + chrome.jumpPaddingTop + chrome.jumpHeight + chrome.jumpPaddingBottom;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function getContinuationTitleMetrics(
|
|
437
|
+
config: LayoutConfig,
|
|
438
|
+
article: ArticlePublicationItem,
|
|
439
|
+
blockWidth: number,
|
|
440
|
+
labelText: string,
|
|
441
|
+
headlineScale: HeadlineScaleId,
|
|
442
|
+
explicitHeadlineScale: boolean,
|
|
443
|
+
overrides?: ArticleFrameChromeSpec,
|
|
444
|
+
): ContinuationTitleMetrics {
|
|
445
|
+
const labelFontSize = getKickerFontSize(config.rhythm.rowHeight);
|
|
446
|
+
const label = solveChromeTextBox(labelText, blockWidth, {
|
|
447
|
+
fontSize: labelFontSize,
|
|
448
|
+
fontFamily: SANS_TEXT_FONT,
|
|
449
|
+
fontWeight: 800,
|
|
450
|
+
lineHeightEm: config.rhythm.rowHeight / labelFontSize,
|
|
451
|
+
paintHeightEm: config.rhythm.rowHeight / labelFontSize,
|
|
452
|
+
}, config.rhythm, overrides?.label);
|
|
453
|
+
const headingWidth = Math.min(blockWidth, 980);
|
|
454
|
+
const headingFontSize = explicitHeadlineScale
|
|
455
|
+
? getHeadlineFontSize(config, headlineScale, headingWidth)
|
|
456
|
+
: config.pageChrome.continuedTitleFontSize;
|
|
457
|
+
const headingLineHeight = explicitHeadlineScale
|
|
458
|
+
? headingFontSize * (isDisplayHeadlineScale(headlineScale) ? 0.94 : 1)
|
|
459
|
+
: config.pageChrome.continuedTitleLineHeight;
|
|
460
|
+
const heading = solveChromeTextBox(article.headline, headingWidth, {
|
|
461
|
+
fontSize: headingFontSize,
|
|
462
|
+
fontFamily: SERIF_TEXT_FONT,
|
|
463
|
+
fontWeight: 900,
|
|
464
|
+
lineHeightEm: headingLineHeight / headingFontSize,
|
|
465
|
+
paintHeightEm: Math.max(1.08, headingLineHeight / headingFontSize),
|
|
466
|
+
marginBeforeEm: CONTINUED_TITLE_HEADING_MARGIN_TOP / headingFontSize,
|
|
467
|
+
}, config.rhythm, overrides?.headline);
|
|
468
|
+
const chromeHeight = reserveRhythmRows(
|
|
469
|
+
label.totalHeight +
|
|
470
|
+
heading.totalHeight +
|
|
471
|
+
CONTINUED_TITLE_PADDING_BOTTOM +
|
|
472
|
+
CONTINUED_TITLE_BORDER_BOTTOM +
|
|
473
|
+
CONTINUED_TITLE_MARGIN_BOTTOM,
|
|
474
|
+
config.rhythm,
|
|
475
|
+
);
|
|
476
|
+
return {
|
|
477
|
+
label,
|
|
478
|
+
heading,
|
|
479
|
+
chromeHeight,
|
|
480
|
+
totalHeight: chromeHeight,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function measureWrappedTextBlock(
|
|
485
|
+
text: string,
|
|
486
|
+
font: string,
|
|
487
|
+
maxWidth: number,
|
|
488
|
+
lineHeight: number,
|
|
489
|
+
paintHeight = lineHeight,
|
|
490
|
+
): { lineCount: number; height: number } {
|
|
491
|
+
const prepared = prepareWithSegmentsCached(text, font);
|
|
492
|
+
let cursor = { ...EMPTY_CURSOR };
|
|
493
|
+
let lineCount = 0;
|
|
494
|
+
while (lineCount < 20) {
|
|
495
|
+
const line = layoutNextLine(prepared, cursor, maxWidth);
|
|
496
|
+
if (!line) break;
|
|
497
|
+
cursor = line.end;
|
|
498
|
+
lineCount += 1;
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
lineCount,
|
|
502
|
+
height: getLineStackHeight(lineCount, lineHeight, paintHeight),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
@@ -13,6 +13,9 @@ export type SaveItemDraftInput = {
|
|
|
13
13
|
bodyMarkus: string;
|
|
14
14
|
aliases: string[];
|
|
15
15
|
expectedContentHash: string | null;
|
|
16
|
+
/** Records a new draft version of the story instead of correcting the current one in place. */
|
|
17
|
+
newVersion?: boolean;
|
|
18
|
+
changeReason?: string | null;
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
export type DeriveMarkusResult = { ok: true; bodyIrBytes: number; bodyIr?: unknown; errors: [] };
|