@anthusai/papyrus 1.0.0-next.9 → 1.1.0

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 (222) hide show
  1. package/amplify/auth/prospectus-google-client.js +66 -0
  2. package/amplify/auth/prospectus-google-client.ts +71 -0
  3. package/amplify/auth/redirect-urls.js +54 -0
  4. package/amplify/auth/redirect-urls.ts +66 -0
  5. package/amplify/auth/resource.js +23 -5
  6. package/amplify/auth/resource.ts +31 -5
  7. package/amplify/data/resource.js +42 -80
  8. package/amplify/data/resource.ts +42 -80
  9. package/amplify/data/schema.js +45 -62
  10. package/amplify/data/schema.ts +47 -62
  11. package/amplify/functions/category-action/handler.ts +5 -3
  12. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
  13. package/amplify/functions/console-chat-responder/resource.js +0 -7
  14. package/amplify/functions/console-chat-responder/resource.ts +0 -8
  15. package/amplify/functions/console-chat-responder/src/main.rs +0 -9
  16. package/amplify/functions/content-actions/handler.py +143 -1
  17. package/amplify/functions/content-actions/requirements.txt +2 -1
  18. package/amplify/functions/content-actions/resource.js +6 -1
  19. package/amplify/functions/content-actions/resource.ts +9 -2
  20. package/amplify/functions/email-submission-processor/handler.py +3 -1
  21. package/amplify/functions/renditions/handler.py +120 -0
  22. package/amplify/functions/renditions/requirements.txt +4 -0
  23. package/amplify/functions/ses-inbound-receive/handler.ts +32 -4
  24. package/amplify/functions/shared/email-submission.ts +109 -1
  25. package/amplify/functions/shared/python-bundle.js +42 -2
  26. package/amplify/functions/shared/python-bundle.ts +47 -2
  27. package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
  28. package/amplify/inbound-email/ses-intake.js +86 -0
  29. package/amplify/inbound-email/ses-intake.ts +110 -0
  30. package/amplify/plugins/registry.js +19 -0
  31. package/amplify/plugins/registry.ts +23 -0
  32. package/amplify/plugins/signup/function/handler.py +200 -0
  33. package/amplify/plugins/signup/function/resource.js +27 -0
  34. package/amplify/plugins/signup/function/resource.ts +35 -0
  35. package/amplify/plugins/signup/papyrus.plugin.js +59 -0
  36. package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
  37. package/amplify/plugins/signup/schema.js +40 -0
  38. package/amplify/plugins/signup/schema.ts +41 -0
  39. package/amplify/plugins/signup/waf.js +48 -0
  40. package/amplify/plugins/signup/waf.ts +51 -0
  41. package/amplify/plugins/types.js +1 -0
  42. package/amplify/plugins/types.ts +39 -0
  43. package/amplify/plugins/wire.js +24 -0
  44. package/amplify/plugins/wire.ts +26 -0
  45. package/amplify/publication/default-publication.js +18 -0
  46. package/amplify/publication/default-publication.ts +20 -0
  47. package/amplify/publication/invalidation-alarm.js +26 -0
  48. package/amplify/publication/invalidation-alarm.ts +30 -0
  49. package/amplify/publication/redirect-and-index.js +37 -0
  50. package/amplify/publication/renditions-function.js +74 -0
  51. package/amplify/publication/renditions-function.ts +112 -0
  52. package/amplify/publication/renditions-settings.js +51 -0
  53. package/amplify/publication/renditions-settings.ts +58 -0
  54. package/amplify/publication/reproject-on-deploy.js +73 -0
  55. package/amplify/publication/reproject-on-deploy.ts +78 -0
  56. package/amplify/publication/s3-projection-settings.js +67 -0
  57. package/amplify/publication/s3-projection-settings.ts +101 -0
  58. package/amplify/publication/s3-projection.js +151 -0
  59. package/amplify/publication/s3-projection.ts +201 -0
  60. package/amplify/resolver-waves.js +26 -0
  61. package/amplify/resolver-waves.ts +34 -0
  62. package/amplify/revalidate-invalidation-settings.js +30 -0
  63. package/amplify/revalidate-invalidation-settings.ts +35 -0
  64. package/amplify/revalidate-secret-policy.js +13 -0
  65. package/amplify/revalidate-secret-policy.ts +19 -0
  66. package/amplify/site-backend-config.js +65 -1
  67. package/amplify/site-backend-config.ts +153 -2
  68. package/amplify/site-backend-names.js +103 -0
  69. package/amplify/site-backend-names.ts +166 -0
  70. package/amplify/site-backend.js +150 -176
  71. package/amplify/site-backend.ts +189 -201
  72. package/amplify/storage-backups/construct.js +31 -0
  73. package/amplify/storage-backups/construct.ts +47 -0
  74. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
  75. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
  76. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
  77. package/app/[year]/[month]/[day]/page.tsx +1 -1
  78. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
  79. package/app/api/archive/editions/route.ts +1 -1
  80. package/app/api/auth/prospectus/callback/route.ts +9 -0
  81. package/app/api/auth/prospectus/selection/route.ts +9 -0
  82. package/app/api/auth/prospectus/session/route.ts +9 -0
  83. package/app/api/auth/prospectus/sign-in/route.ts +9 -0
  84. package/app/api/auth/prospectus/sign-out/route.ts +9 -0
  85. package/app/api/cyclotron/priming-proof/route.ts +11 -0
  86. package/app/api/media/[...path]/route.ts +5 -16
  87. package/app/api/revalidate/route.ts +15 -7
  88. package/app/archive/page.tsx +1 -1
  89. package/app/articles/[slug]/page.tsx +7 -5
  90. package/app/blog-rhythm.css +278 -0
  91. package/app/cyclotron/page.tsx +13 -0
  92. package/app/globals.css +2 -0
  93. package/app/layout.tsx +16 -0
  94. package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
  95. package/app/newsroom/[section]/page.tsx +6 -3
  96. package/app/newsroom/layout.tsx +4 -9
  97. package/app/newsroom/settings/page.tsx +6 -0
  98. package/app/newsroom/work/page.tsx +10 -0
  99. package/app/page.tsx +22 -8
  100. package/app/robots.ts +2 -5
  101. package/app/tailwind.css +1 -6
  102. package/components/amplify-client-provider.tsx +5 -22
  103. package/components/archive-shell.tsx +2 -22
  104. package/components/article-video.tsx +152 -5
  105. package/components/blog-page-background.tsx +23 -6
  106. package/components/news-desk-page.tsx +4 -15
  107. package/components/news-desk-taxonomy-client.ts +90 -3
  108. package/components/newspaper.tsx +6 -2
  109. package/components/newsroom-access-gate.tsx +2 -1
  110. package/components/newsroom-board-view.tsx +99 -0
  111. package/components/newsroom-client-shell.tsx +12 -0
  112. package/components/newsroom-curation-provider.tsx +17 -0
  113. package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
  114. package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
  115. package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
  116. package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
  117. package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
  118. package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
  119. package/components/newsroom-newsprint-board.css +48 -0
  120. package/components/newsroom-ops-overview.tsx +28 -11
  121. package/components/newsroom-ops-shell.tsx +7 -1
  122. package/components/newsroom-organizer-shell.tsx +130 -0
  123. package/components/newsroom-organizer.css +63 -0
  124. package/components/newsroom-references-view.tsx +92 -8
  125. package/components/papyrus-console-shell.tsx +4 -2
  126. package/components/pictogram-figure.tsx +9 -2
  127. package/components/presentation-header.tsx +135 -0
  128. package/components/presentation-rhythm-hrule.tsx +17 -0
  129. package/components/priming-proof-page.tsx +94 -0
  130. package/components/publication-cyclotron-panel.tsx +57 -0
  131. package/components/reader-auth-control.tsx +8 -5
  132. package/components/reference-curation-controls.tsx +77 -0
  133. package/components/reference-cyclotron-status.tsx +109 -0
  134. package/components/reference-priming.tsx +18 -0
  135. package/components/reference-relevance-review.tsx +75 -0
  136. package/components/settings-page.tsx +2 -1
  137. package/components/signup-entries-panel.tsx +94 -0
  138. package/components/topic-steering-workspace.tsx +78 -8
  139. package/components/use-blog-header-obstacles.ts +81 -0
  140. package/components/use-brand-fonts-ready.ts +28 -0
  141. package/components/use-near-viewport.ts +30 -0
  142. package/components/use-rhythm-overlay.ts +29 -0
  143. package/infra/amplify-app-shell.js +166 -27
  144. package/infra/amplify-app-shell.ts +180 -31
  145. package/infra/build-specs.js +23 -7
  146. package/infra/build-specs.ts +27 -10
  147. package/infra/site-config.js +248 -36
  148. package/infra/site-config.ts +313 -51
  149. package/lib/amplify-outputs-path.ts +5 -1
  150. package/lib/amplify-server-runtime.ts +36 -1
  151. package/lib/approved-priming-proof-reports.ts +6 -0
  152. package/lib/blog-feature-solver.ts +459 -0
  153. package/lib/blog-rhythm.ts +134 -0
  154. package/lib/brand-fonts-watch.ts +43 -0
  155. package/lib/brand-fonts.ts +112 -0
  156. package/lib/cached-content-repository.ts +12 -0
  157. package/lib/category-repository.ts +43 -93
  158. package/lib/content-item-lifecycle.ts +55 -0
  159. package/lib/content-repository.ts +3 -0
  160. package/lib/content-types.ts +4 -0
  161. package/lib/curation-client.ts +61 -0
  162. package/lib/edition-routes.ts +31 -29
  163. package/lib/graphql-content-repository.ts +56 -22
  164. package/lib/media-route-response.ts +45 -0
  165. package/lib/newspaper-layout.ts +3 -2
  166. package/lib/newsroom-base-path.ts +12 -3
  167. package/lib/newsroom-board.ts +99 -0
  168. package/lib/newsroom-category-drilldown.ts +3 -2
  169. package/lib/newsroom-forum-routes.ts +6 -4
  170. package/lib/newsroom-index-filters.ts +14 -10
  171. package/lib/newsroom-nav.ts +3 -1
  172. package/lib/newsroom-organizer-nav.ts +37 -0
  173. package/lib/newsroom-section-routing.ts +20 -0
  174. package/lib/oauth-redirect-priority.ts +40 -0
  175. package/lib/papyrus-web-locations.ts +7 -6
  176. package/lib/plugin-settings.ts +11 -0
  177. package/lib/presentation-footer.ts +3 -2
  178. package/lib/pretext-layout.ts +12 -3
  179. package/lib/priming-proof-report.ts +90 -0
  180. package/lib/prospectus-publication-access.ts +53 -0
  181. package/lib/prospectus-session.ts +196 -0
  182. package/lib/prospectus-sign-in-routes.ts +284 -0
  183. package/lib/prospectus-site-identity.ts +303 -0
  184. package/lib/publication-cyclotron.ts +74 -0
  185. package/lib/rate-limit-retry.ts +55 -0
  186. package/lib/reader-base-path.ts +24 -0
  187. package/lib/reader-cache-lifetime.ts +5 -0
  188. package/lib/reader-static-params.ts +85 -4
  189. package/lib/reader-storage-url.ts +9 -12
  190. package/lib/reference-policy.ts +5 -0
  191. package/lib/relevance-decisions.ts +103 -0
  192. package/lib/relevance-status.ts +63 -0
  193. package/lib/renderer.ts +7 -0
  194. package/lib/revalidate-prewarm.ts +53 -0
  195. package/lib/revalidate-secret.ts +59 -0
  196. package/lib/robots-policy.ts +14 -0
  197. package/lib/root-route-redirect.ts +16 -0
  198. package/lib/semantic-graph.ts +7 -0
  199. package/lib/shared-credentials-provider.ts +61 -0
  200. package/lib/signup-csv.ts +24 -0
  201. package/lib/site-brand.ts +59 -0
  202. package/lib/staging-gated-path.ts +23 -0
  203. package/lib/staging-preview-object.ts +21 -5
  204. package/lib/test-editor-override.ts +4 -0
  205. package/lib/video-loading.ts +22 -0
  206. package/lib/video-script.ts +14 -0
  207. package/middleware.ts +11 -19
  208. package/package.json +73 -8
  209. package/renderers/pretext/article-page.tsx +39 -3
  210. package/renderers/pretext/presentation-shared.tsx +113 -0
  211. package/renderers/pretext/presentation-shell.tsx +15 -102
  212. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  213. package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
  214. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  215. package/routes.manifest.json +123 -12
  216. package/src/sync.mjs +30 -6
  217. package/src/with-papyrus.d.mts +9 -0
  218. package/src/with-papyrus.mjs +48 -3
  219. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  220. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  221. package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
  222. package/components/ai-elements/message.tsx +0 -360
@@ -0,0 +1,279 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ import { solveFeaturedItem } from "../../lib/blog-feature-solver";
6
+ import { createDefaultBlogTextStyle, getMeasuredTextHeight, reserveRhythmRows } from "../../lib/blog-rhythm";
7
+ import type { ArticleVideoAsset } from "../../lib/articles";
8
+ import type { EditionContent, EditionSection } from "../../lib/content-types";
9
+ import { getEditionSectionItems } from "../../lib/edition-sections";
10
+ import { layoutAllTextLines, prepareWithSegments } from "../../lib/pretext-layout";
11
+ import { buildPresentationFooterEntries } from "../../lib/presentation-footer";
12
+ import {
13
+ getPublicationItemImageAssets,
14
+ getPublicationItemVideoAsset,
15
+ type PublicationItem,
16
+ } from "../../lib/publication-items";
17
+ import { SITE_BRAND } from "../../lib/site-brand";
18
+ import { EDITION_OVERVIEW_VIDEO_KEY, type VideoScriptRef } from "../../lib/video-script";
19
+ import { ArticleVideoFigure } from "../../components/article-video";
20
+ import { BlogPageBackground as GenericBlogPageBackground } from "../../components/blog-page-background";
21
+ import { PictogramFigure as GenericPictogramFigure } from "../../components/pictogram-figure";
22
+ import { PresentationFooter } from "../../components/presentation-footer";
23
+ import { getSectionAnchorId, PresentationHeader } from "../../components/presentation-header";
24
+ import { PresentationRhythmHRule } from "../../components/presentation-rhythm-hrule";
25
+ import { useBlogHeaderObstacles } from "../../components/use-blog-header-obstacles";
26
+ import { useBrandFontsReadyVersion } from "../../components/use-brand-fonts-ready";
27
+ import { useRhythmOverlay } from "../../components/use-rhythm-overlay";
28
+ import {
29
+ getBlogFooterSectionHref,
30
+ getPresentationBodyText,
31
+ getPresentationItemRole,
32
+ getPresentationTitle,
33
+ handleBlogFooterSectionClick,
34
+ MeasuredPresentationLines,
35
+ useMeasuredWidth,
36
+ usePresentationTargetScroll,
37
+ } from "./presentation-shared";
38
+ import { BLOG_RHYTHM, getFeaturedLayoutStyle, getRhythmShellStyle } from "./rhythm-shell-style";
39
+
40
+ const BlogPageBackground = SITE_BRAND.components?.BlogPageBackground ?? GenericBlogPageBackground;
41
+ const PictogramFigure = SITE_BRAND.components?.PictogramFigure ?? GenericPictogramFigure;
42
+ const BLOG_TEXT_STYLE = createDefaultBlogTextStyle(SITE_BRAND.textFont);
43
+ const SECONDARY_PAIR_ITEM_INDEX = 2;
44
+
45
+ type PapyrusTestWindow = Window & typeof globalThis & {
46
+ __PAPYRUS_SCENARIO__?: string;
47
+ };
48
+
49
+ export function RhythmBlogPresentation({
50
+ content,
51
+ editionBasePath,
52
+ targetSection,
53
+ }: {
54
+ content: EditionContent;
55
+ editionBasePath?: string;
56
+ targetSection?: EditionSection;
57
+ }) {
58
+ const footerEntries = useMemo(() => buildPresentationFooterEntries(content), [content]);
59
+ const footerSubtitle = SITE_BRAND.id === "papyrus" ? (content.description?.trim() || "Inside Papyrus") : SITE_BRAND.mastheadSubtitle;
60
+ const pageRef = useRef<HTMLElement | null>(null);
61
+ const showRhythmOverlay = useRhythmOverlay();
62
+ const headerObstacles = useBlogHeaderObstacles(pageRef, BLOG_RHYTHM.paintBuffer);
63
+ usePresentationTargetScroll(targetSection);
64
+ useEffect(() => {
65
+ (window as PapyrusTestWindow).__PAPYRUS_SCENARIO__ = content.scenarioId;
66
+ return () => {
67
+ delete (window as PapyrusTestWindow).__PAPYRUS_SCENARIO__;
68
+ };
69
+ }, [content.scenarioId]);
70
+
71
+ return (
72
+ <main
73
+ className="presentation-page presentation-page--blog blog-rhythm-shell"
74
+ data-presentation-engine="blog"
75
+ data-rhythm-overlay={showRhythmOverlay ? "true" : "false"}
76
+ ref={pageRef}
77
+ style={getRhythmShellStyle(BLOG_RHYTHM)}
78
+ >
79
+ <BlogPageBackground headerObstacles={headerObstacles} pageRef={pageRef} rhythm={BLOG_RHYTHM} />
80
+ <PresentationHeader
81
+ description={content.description}
82
+ editionBasePath={editionBasePath}
83
+ editionDate={content.editionDate}
84
+ sections={content.sections}
85
+ title={content.title}
86
+ />
87
+ <div className="blog-sections">
88
+ {content.sections.map((section, sectionIndex) => (
89
+ <section className="blog-section" data-edition-section={section.key} id={getSectionAnchorId(section.key)} key={section.key}>
90
+ {sectionIndex === 0 && content.editionVideo ? (
91
+ <EditionOverviewVideo
92
+ editionVideo={content.editionVideo}
93
+ videoScript={content.videoScripts?.[EDITION_OVERVIEW_VIDEO_KEY] ?? null}
94
+ />
95
+ ) : null}
96
+ <header className="presentation-section-header">
97
+ <div className="presentation-section-header__band">
98
+ <p>{section.label}</p>
99
+ </div>
100
+ {section.description ? <span>{section.description}</span> : null}
101
+ </header>
102
+ {getEditionSectionItems(section, content.items).flatMap((item, index) => [
103
+ ...(index > 0
104
+ ? [
105
+ <PresentationRhythmHRule
106
+ hideInSecondaryPair={index === SECONDARY_PAIR_ITEM_INDEX}
107
+ key={`hrule-before-${item.slug}`}
108
+ />,
109
+ ]
110
+ : []),
111
+ <RhythmPresentationItem
112
+ editionBasePath={editionBasePath}
113
+ index={index}
114
+ item={item}
115
+ key={item.slug}
116
+ videoScript={content.videoScripts?.[item.slug] ?? null}
117
+ />,
118
+ ])}
119
+ </section>
120
+ ))}
121
+ </div>
122
+ <PresentationFooter
123
+ editionBasePath={editionBasePath}
124
+ entries={footerEntries}
125
+ onSectionClick={handleBlogFooterSectionClick}
126
+ resolveSectionHref={(entry) => getBlogFooterSectionHref(entry, editionBasePath)}
127
+ subtitle={SITE_BRAND.footerSubtitleOverride ?? footerSubtitle}
128
+ title={SITE_BRAND.footerTitle}
129
+ />
130
+ </main>
131
+ );
132
+ }
133
+
134
+ function EditionOverviewVideo({
135
+ editionVideo,
136
+ videoScript,
137
+ }: {
138
+ editionVideo: ArticleVideoAsset;
139
+ videoScript: VideoScriptRef | null;
140
+ }) {
141
+ return (
142
+ <section className="presentation-edition-video" aria-label={editionVideo.alt}>
143
+ <ArticleVideoFigure
144
+ figureClassName="presentation-edition-video__figure article-video"
145
+ slug={EDITION_OVERVIEW_VIDEO_KEY}
146
+ video={editionVideo}
147
+ videoScript={videoScript}
148
+ />
149
+ </section>
150
+ );
151
+ }
152
+
153
+ function RhythmPresentationItem({
154
+ editionBasePath,
155
+ index,
156
+ item,
157
+ videoScript,
158
+ }: {
159
+ editionBasePath?: string;
160
+ index: number;
161
+ item: PublicationItem;
162
+ videoScript: VideoScriptRef | null;
163
+ }) {
164
+ const articleRef = useRef<HTMLElement | null>(null);
165
+ const frameRef = useRef<HTMLDivElement | null>(null);
166
+ const containerWidth = useMeasuredWidth(articleRef);
167
+ const frameWidth = useMeasuredWidth(frameRef);
168
+ const viewportWidth = useViewportWidth();
169
+ const fontsReadyVersion = useBrandFontsReadyVersion();
170
+ const textStyle = useMemo(() => ({ ...BLOG_TEXT_STYLE, fontsReadyVersion }), [fontsReadyVersion]);
171
+ const image = getPublicationItemImageAssets(item)[0];
172
+ const video = getPublicationItemVideoAsset(item);
173
+ const text = getPresentationBodyText(item, "blog");
174
+ const hasImage = Boolean(image);
175
+ const featuredLayout = useMemo(() => {
176
+ if (!hasImage || !containerWidth || !image) return null;
177
+ return solveFeaturedItem({
178
+ text,
179
+ containerWidth,
180
+ viewportWidth,
181
+ rhythm: BLOG_RHYTHM,
182
+ textStyle,
183
+ imageAsset: image,
184
+ itemIndex: index,
185
+ });
186
+ }, [containerWidth, hasImage, image, index, text, textStyle, viewportWidth]);
187
+ const lines = useMemo(() => {
188
+ if (featuredLayout) return featuredLayout.textLines;
189
+ const maxWidth = frameWidth || containerWidth;
190
+ if (!maxWidth || !text.trim()) return [];
191
+ return layoutAllTextLines({
192
+ prepared: prepareWithSegments(text, `${textStyle.fontSize}px ${textStyle.fontFamily}`, { whiteSpace: "pre-wrap" }),
193
+ maxWidth,
194
+ ...textStyle,
195
+ });
196
+ }, [containerWidth, featuredLayout, frameWidth, text, textStyle]);
197
+ const textHeight = featuredLayout?.textFrameHeight ?? reserveRhythmRows(getMeasuredTextHeight(lines), BLOG_RHYTHM);
198
+ const layoutMode = featuredLayout?.mode ?? "stacked";
199
+ const directHref = editionBasePath ? `${editionBasePath}/${encodeURIComponent(item.slug)}` : `/articles/${encodeURIComponent(item.slug)}`;
200
+
201
+ return (
202
+ <article
203
+ className="presentation-item presentation-item--blog"
204
+ data-feature-layout={hasImage ? layoutMode : undefined}
205
+ data-has-image={hasImage ? "true" : "false"}
206
+ data-has-video={video ? "true" : "false"}
207
+ data-item-id={item.slug}
208
+ data-item-index={index}
209
+ data-item-role={getPresentationItemRole("blog", index)}
210
+ data-item-type={item.type}
211
+ id={item.slug}
212
+ ref={articleRef}
213
+ style={featuredLayout ? getFeaturedLayoutStyle(featuredLayout) : undefined}
214
+ >
215
+ <div className="presentation-item__copy">
216
+ <header className="presentation-item__header">
217
+ <p>{item.section ?? "General"}</p>
218
+ <h2>
219
+ <Link href={directHref}>{getPresentationTitle(item)}</Link>
220
+ </h2>
221
+ {item.deck ? <span>{item.deck}</span> : null}
222
+ </header>
223
+ <div className="presentation-item__body">
224
+ {image ? (
225
+ <div className="presentation-item__media">
226
+ <PictogramFigure
227
+ alt={image.alt}
228
+ caption={image.caption}
229
+ credit={image.credit}
230
+ figureClassName="presentation-item__image"
231
+ frameHeight={featuredLayout?.imageHeight}
232
+ frameWidth={featuredLayout?.imageWidth}
233
+ height={760}
234
+ layout={image.layout}
235
+ sizes="(max-width: 900px) 100vw, 760px"
236
+ slug={item.slug}
237
+ src={image.src}
238
+ themeVariants={image.themeVariants}
239
+ width={1200}
240
+ />
241
+ </div>
242
+ ) : null}
243
+ <div
244
+ className="presentation-item__text-frame"
245
+ data-layout-mode={layoutMode}
246
+ ref={frameRef}
247
+ style={{ height: textHeight }}
248
+ >
249
+ <MeasuredPresentationLines lines={lines} />
250
+ </div>
251
+ <Link className="presentation-item__cta" href={directHref}>
252
+ Read Article
253
+ </Link>
254
+ </div>
255
+ </div>
256
+ {video ? (
257
+ <div className="presentation-item__video">
258
+ <ArticleVideoFigure
259
+ figureClassName="presentation-item__video-figure article-video"
260
+ slug={item.slug}
261
+ video={video}
262
+ videoScript={videoScript}
263
+ />
264
+ </div>
265
+ ) : null}
266
+ </article>
267
+ );
268
+ }
269
+
270
+ function useViewportWidth(): number {
271
+ const [width, setWidth] = useState(0);
272
+ useEffect(() => {
273
+ const update = () => setWidth(Math.max(1, Math.floor(window.innerWidth)));
274
+ update();
275
+ window.addEventListener("resize", update);
276
+ return () => window.removeEventListener("resize", update);
277
+ }, []);
278
+ return width;
279
+ }
@@ -0,0 +1,35 @@
1
+ import type { CSSProperties } from "react";
2
+ import {
3
+ BLOG_COPY_ROW_MULTIPLE,
4
+ createDefaultBlogVerticalRhythm,
5
+ type VerticalRhythm,
6
+ } from "../../lib/blog-rhythm";
7
+
8
+ export const BLOG_RHYTHM: VerticalRhythm = createDefaultBlogVerticalRhythm();
9
+
10
+ export function getRhythmShellStyle(rhythm: VerticalRhythm): CSSProperties {
11
+ return {
12
+ "--blog-rhythm": `${rhythm.rowHeight / BLOG_COPY_ROW_MULTIPLE}px`,
13
+ "--blog-row-height": `${rhythm.rowHeight}px`,
14
+ "--blog-paint-buffer": `${rhythm.paintBuffer}px`,
15
+ "--blog-paint-height": `${rhythm.paintHeight}px`,
16
+ } as CSSProperties;
17
+ }
18
+
19
+ export function getFeaturedLayoutStyle(layout: {
20
+ copyWidth: number;
21
+ imageWidth: number;
22
+ imageHeight: number;
23
+ gap: number;
24
+ mediaHeight?: number;
25
+ textFrameHeight?: number;
26
+ }): CSSProperties {
27
+ return {
28
+ "--feature-copy-width": `${layout.copyWidth}px`,
29
+ "--feature-image-width": `${layout.imageWidth}px`,
30
+ "--feature-image-height": `${layout.imageHeight}px`,
31
+ "--feature-layout-gap": `${layout.gap}px`,
32
+ ...(layout.mediaHeight === undefined ? {} : { "--feature-media-height": `${layout.mediaHeight}px` }),
33
+ ...(layout.textFrameHeight === undefined ? {} : { "--feature-text-frame-height": `${layout.textFrameHeight}px` }),
34
+ } as CSSProperties;
35
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.0-next.9",
2
+ "version": "1.1.0",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",
@@ -13,38 +13,47 @@
13
13
  },
14
14
  {
15
15
  "file": "app/[year]/[month]/[day]/[articleSlug]/page.tsx",
16
+ "reader": true,
16
17
  "hasDefault": true,
17
18
  "exports": [
18
- "generateMetadata"
19
+ "generateMetadata",
20
+ "generateStaticParams"
19
21
  ],
20
22
  "config": {
21
- "revalidate": "3600"
23
+ "revalidate": "60"
22
24
  }
23
25
  },
24
26
  {
25
27
  "file": "app/[year]/[month]/[day]/page.tsx",
28
+ "reader": true,
26
29
  "hasDefault": true,
27
30
  "exports": [
28
31
  "generateStaticParams"
29
32
  ],
30
33
  "config": {
31
- "revalidate": "3600"
34
+ "revalidate": "60"
32
35
  }
33
36
  },
34
37
  {
35
38
  "file": "app/[year]/[month]/[day]/page/[pageNumber]/page.tsx",
39
+ "reader": true,
36
40
  "hasDefault": true,
37
- "exports": [],
41
+ "exports": [
42
+ "generateStaticParams"
43
+ ],
38
44
  "config": {
39
- "revalidate": "3600"
45
+ "revalidate": "60"
40
46
  }
41
47
  },
42
48
  {
43
49
  "file": "app/[year]/[month]/[day]/section/[sectionKey]/page.tsx",
50
+ "reader": true,
44
51
  "hasDefault": true,
45
- "exports": [],
52
+ "exports": [
53
+ "generateStaticParams"
54
+ ],
46
55
  "config": {
47
- "revalidate": "3600"
56
+ "revalidate": "60"
48
57
  }
49
58
  },
50
59
  {
@@ -54,9 +63,72 @@
54
63
  "GET"
55
64
  ],
56
65
  "config": {
57
- "revalidate": "3600"
66
+ "revalidate": "60"
67
+ }
68
+ },
69
+ {
70
+ "file": "app/api/auth/prospectus/callback/route.ts",
71
+ "hasDefault": false,
72
+ "exports": [
73
+ "GET"
74
+ ],
75
+ "config": {
76
+ "dynamic": "\"force-dynamic\"",
77
+ "runtime": "\"nodejs\""
78
+ }
79
+ },
80
+ {
81
+ "file": "app/api/auth/prospectus/selection/route.ts",
82
+ "hasDefault": false,
83
+ "exports": [
84
+ "POST"
85
+ ],
86
+ "config": {
87
+ "dynamic": "\"force-dynamic\"",
88
+ "runtime": "\"nodejs\""
89
+ }
90
+ },
91
+ {
92
+ "file": "app/api/auth/prospectus/session/route.ts",
93
+ "hasDefault": false,
94
+ "exports": [
95
+ "GET"
96
+ ],
97
+ "config": {
98
+ "dynamic": "\"force-dynamic\"",
99
+ "runtime": "\"nodejs\""
100
+ }
101
+ },
102
+ {
103
+ "file": "app/api/auth/prospectus/sign-in/route.ts",
104
+ "hasDefault": false,
105
+ "exports": [
106
+ "GET"
107
+ ],
108
+ "config": {
109
+ "dynamic": "\"force-dynamic\"",
110
+ "runtime": "\"nodejs\""
111
+ }
112
+ },
113
+ {
114
+ "file": "app/api/auth/prospectus/sign-out/route.ts",
115
+ "hasDefault": false,
116
+ "exports": [
117
+ "POST"
118
+ ],
119
+ "config": {
120
+ "dynamic": "\"force-dynamic\"",
121
+ "runtime": "\"nodejs\""
58
122
  }
59
123
  },
124
+ {
125
+ "file": "app/api/cyclotron/priming-proof/route.ts",
126
+ "hasDefault": false,
127
+ "exports": [
128
+ "GET"
129
+ ],
130
+ "config": {}
131
+ },
60
132
  {
61
133
  "file": "app/api/media/[...path]/route.ts",
62
134
  "hasDefault": false,
@@ -79,23 +151,33 @@
79
151
  },
80
152
  {
81
153
  "file": "app/archive/page.tsx",
154
+ "reader": true,
82
155
  "hasDefault": true,
83
156
  "exports": [],
84
157
  "config": {
85
- "revalidate": "3600"
158
+ "revalidate": "60"
86
159
  }
87
160
  },
88
161
  {
89
162
  "file": "app/articles/[slug]/page.tsx",
163
+ "reader": true,
90
164
  "hasDefault": true,
91
165
  "exports": [
92
166
  "generateMetadata",
93
167
  "generateStaticParams"
94
168
  ],
95
169
  "config": {
96
- "revalidate": "3600"
170
+ "revalidate": "60"
97
171
  }
98
172
  },
173
+ {
174
+ "file": "app/cyclotron/page.tsx",
175
+ "hasDefault": true,
176
+ "exports": [
177
+ "metadata"
178
+ ],
179
+ "config": {}
180
+ },
99
181
  {
100
182
  "file": "app/layout.tsx",
101
183
  "hasDefault": true,
@@ -182,8 +264,35 @@
182
264
  "dynamic": "\"force-dynamic\""
183
265
  }
184
266
  },
267
+ {
268
+ "file": "app/newsroom/settings/page.tsx",
269
+ "hasDefault": true,
270
+ "exports": [
271
+ "metadata"
272
+ ],
273
+ "config": {
274
+ "dynamic": "\"force-dynamic\""
275
+ }
276
+ },
277
+ {
278
+ "file": "app/newsroom/work/page.tsx",
279
+ "hasDefault": true,
280
+ "exports": [],
281
+ "config": {
282
+ "dynamic": "\"force-dynamic\""
283
+ }
284
+ },
185
285
  {
186
286
  "file": "app/page.tsx",
287
+ "reader": true,
288
+ "hasDefault": true,
289
+ "exports": [],
290
+ "config": {
291
+ "dynamic": "\"force-dynamic\""
292
+ }
293
+ },
294
+ {
295
+ "file": "app/robots.ts",
187
296
  "hasDefault": true,
188
297
  "exports": [],
189
298
  "config": {
@@ -192,6 +301,7 @@
192
301
  },
193
302
  {
194
303
  "file": "app/settings/page.tsx",
304
+ "reader": true,
195
305
  "hasDefault": true,
196
306
  "exports": [
197
307
  "metadata"
@@ -203,6 +313,7 @@
203
313
  ],
204
314
  "middleware": {
205
315
  "file": "middleware.ts",
206
- "matcher": "[\"/((?!_next/static|_next/image|favicon.ico|icon).*)\"]"
316
+ "matcher": "[\"/((?!_next/static|_next/image|favicon.ico|icon).*)\"]",
317
+ "runtime": "nodejs"
207
318
  }
208
319
  }
package/src/sync.mjs CHANGED
@@ -37,18 +37,42 @@ function shimFor(route) {
37
37
  return lines.join("\n") + "\n";
38
38
  }
39
39
 
40
- function middlewareShim(manifest) {
40
+ export function middlewareShim(manifest) {
41
41
  const matcher = manifest.middleware.matcher ?? '["/((?!_next/static|_next/image|favicon.ico|icon).*)"]';
42
42
  return [
43
43
  MARKER,
44
44
  `export { middleware } from "${PKG}/middleware";`,
45
- `export const config = { matcher: ${matcher} };`,
45
+ `export const config = { ${manifest.middleware.runtime ? `runtime: ${JSON.stringify(manifest.middleware.runtime)}, ` : ""}matcher: ${matcher} };`,
46
46
  "",
47
47
  ].join("\n");
48
48
  }
49
49
 
50
- export function sync({ root = process.cwd(), log = console.log } = {}) {
51
- const manifest = JSON.parse(fs.readFileSync(path.join(pkgRoot, "routes.manifest.json"), "utf8"));
50
+ const RESERVED_FIRST_SEGMENTS = new Set(["newsroom", "api", "_next", "_preview", "%5F_preview"]);
51
+
52
+ export function normalizeReaderBasePath(value) {
53
+ const trimmed = (value ?? "").trim().replace(/\/+$/, "");
54
+ if (!trimmed) return "";
55
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
56
+ }
57
+
58
+ export function readReaderBasePath(root) {
59
+ let text;
60
+ try {
61
+ text = fs.readFileSync(path.join(root, "papyrus.config.ts"), "utf8");
62
+ } catch {
63
+ return "";
64
+ }
65
+ const match = text.match(/\breaderBasePath\s*:\s*(["'`])([^"'`]*)\1/);
66
+ return normalizeReaderBasePath(match ? match[2] : "");
67
+ }
68
+
69
+ export function sync({ root = process.cwd(), log = console.log, manifest: manifestOverride, readerBasePath: readerBasePathOverride } = {}) {
70
+ const manifest = manifestOverride ?? JSON.parse(fs.readFileSync(path.join(pkgRoot, "routes.manifest.json"), "utf8"));
71
+ const readerBasePath = normalizeReaderBasePath(readerBasePathOverride ?? readReaderBasePath(root));
72
+ if (readerBasePath && RESERVED_FIRST_SEGMENTS.has(readerBasePath.split("/")[1])) {
73
+ throw new Error(`readerBasePath "${readerBasePath}" collides with a Papyrus-owned route (newsroom, api, _preview)`);
74
+ }
75
+ const readerRouteFile = (file) => (readerBasePath ? file.replace(/^app\//, `app${readerBasePath}/`) : file);
52
76
  const generated = [];
53
77
  const skipped = [];
54
78
  const write = (relPath, content) => {
@@ -61,7 +85,7 @@ export function sync({ root = process.cwd(), log = console.log } = {}) {
61
85
  fs.writeFileSync(dest, content);
62
86
  generated.push(relPath);
63
87
  };
64
- for (const route of manifest.routes) write(route.file, shimFor(route));
88
+ for (const route of manifest.routes) write(route.reader ? readerRouteFile(route.file) : route.file, shimFor(route));
65
89
  write("middleware.ts", middlewareShim(manifest));
66
90
 
67
91
  // Remove shims that are no longer in the manifest (e.g. after an upgrade).
@@ -81,7 +105,7 @@ export function sync({ root = process.cwd(), log = console.log } = {}) {
81
105
  const re = new RegExp(`${GITIGNORE_BEGIN.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${GITIGNORE_END}`);
82
106
  fs.writeFileSync(giFile, re.test(gi) ? gi.replace(re, block) : `${gi}${gi.endsWith("\n") || !gi ? "" : "\n"}${block}\n`);
83
107
 
84
- log(`papyrus-app sync: ${generated.length} shim(s) written, ${skipped.length} site-owned file(s) kept (${PKG}@${manifest.version})`);
108
+ log(`papyrus-app sync: ${generated.length} shim(s) written, ${skipped.length} site-owned file(s) kept (${PKG}@${manifest.version})${readerBasePath ? `, reader mounted at ${readerBasePath}` : ""}`);
85
109
  for (const rel of skipped) log(` site-owned: ${rel}`);
86
110
  return { generated, skipped };
87
111
  }
@@ -1,2 +1,11 @@
1
1
  import type { NextConfig } from "next";
2
2
  export function withPapyrus(nextConfig?: NextConfig, options?: { root?: string }): NextConfig;
3
+ export const READER_EXPIRE_TIME_SECONDS: number;
4
+ export const DEFAULT_STATIC_GENERATION_WORKERS: number;
5
+ export const DEFAULT_STATIC_GENERATION_CONCURRENCY: number;
6
+ export const DEFAULT_STATIC_GENERATION_RETRY_COUNT: number;
7
+ export function staticGenerationSettings(): {
8
+ cpus: number;
9
+ staticGenerationMaxConcurrency: number;
10
+ staticGenerationRetryCount: number;
11
+ };