@anthusai/papyrus 1.0.0-next.9 → 1.0.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 (208) hide show
  1. package/amplify/auth/redirect-urls.js +54 -0
  2. package/amplify/auth/redirect-urls.ts +66 -0
  3. package/amplify/auth/resource.js +20 -5
  4. package/amplify/auth/resource.ts +27 -5
  5. package/amplify/data/resource.js +42 -80
  6. package/amplify/data/resource.ts +42 -80
  7. package/amplify/data/schema.js +45 -60
  8. package/amplify/data/schema.ts +47 -60
  9. package/amplify/functions/category-action/handler.ts +5 -3
  10. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
  11. package/amplify/functions/console-chat-responder/resource.js +0 -7
  12. package/amplify/functions/console-chat-responder/resource.ts +0 -8
  13. package/amplify/functions/console-chat-responder/src/main.rs +0 -9
  14. package/amplify/functions/content-actions/handler.py +143 -1
  15. package/amplify/functions/content-actions/requirements.txt +2 -1
  16. package/amplify/functions/content-actions/resource.js +6 -1
  17. package/amplify/functions/content-actions/resource.ts +9 -2
  18. package/amplify/functions/renditions/handler.py +120 -0
  19. package/amplify/functions/renditions/requirements.txt +4 -0
  20. package/amplify/functions/shared/python-bundle.js +42 -2
  21. package/amplify/functions/shared/python-bundle.ts +47 -2
  22. package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
  23. package/amplify/inbound-email/ses-intake.js +86 -0
  24. package/amplify/inbound-email/ses-intake.ts +110 -0
  25. package/amplify/plugins/registry.js +19 -0
  26. package/amplify/plugins/registry.ts +23 -0
  27. package/amplify/plugins/signup/function/handler.py +200 -0
  28. package/amplify/plugins/signup/function/resource.js +27 -0
  29. package/amplify/plugins/signup/function/resource.ts +35 -0
  30. package/amplify/plugins/signup/papyrus.plugin.js +59 -0
  31. package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
  32. package/amplify/plugins/signup/schema.js +40 -0
  33. package/amplify/plugins/signup/schema.ts +41 -0
  34. package/amplify/plugins/signup/waf.js +48 -0
  35. package/amplify/plugins/signup/waf.ts +51 -0
  36. package/amplify/plugins/types.js +1 -0
  37. package/amplify/plugins/types.ts +39 -0
  38. package/amplify/plugins/wire.js +24 -0
  39. package/amplify/plugins/wire.ts +26 -0
  40. package/amplify/publication/default-publication.js +18 -0
  41. package/amplify/publication/default-publication.ts +20 -0
  42. package/amplify/publication/invalidation-alarm.js +26 -0
  43. package/amplify/publication/invalidation-alarm.ts +30 -0
  44. package/amplify/publication/redirect-and-index.js +37 -0
  45. package/amplify/publication/renditions-function.js +74 -0
  46. package/amplify/publication/renditions-function.ts +112 -0
  47. package/amplify/publication/renditions-settings.js +51 -0
  48. package/amplify/publication/renditions-settings.ts +58 -0
  49. package/amplify/publication/reproject-on-deploy.js +73 -0
  50. package/amplify/publication/reproject-on-deploy.ts +78 -0
  51. package/amplify/publication/s3-projection-settings.js +67 -0
  52. package/amplify/publication/s3-projection-settings.ts +101 -0
  53. package/amplify/publication/s3-projection.js +151 -0
  54. package/amplify/publication/s3-projection.ts +201 -0
  55. package/amplify/resolver-waves.js +26 -0
  56. package/amplify/resolver-waves.ts +34 -0
  57. package/amplify/revalidate-invalidation-settings.js +30 -0
  58. package/amplify/revalidate-invalidation-settings.ts +35 -0
  59. package/amplify/revalidate-secret-policy.js +13 -0
  60. package/amplify/revalidate-secret-policy.ts +19 -0
  61. package/amplify/site-backend-config.js +65 -1
  62. package/amplify/site-backend-config.ts +153 -2
  63. package/amplify/site-backend-names.js +103 -0
  64. package/amplify/site-backend-names.ts +166 -0
  65. package/amplify/site-backend.js +143 -176
  66. package/amplify/site-backend.ts +181 -201
  67. package/amplify/storage-backups/construct.js +31 -0
  68. package/amplify/storage-backups/construct.ts +47 -0
  69. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
  70. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
  71. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
  72. package/app/[year]/[month]/[day]/page.tsx +1 -1
  73. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
  74. package/app/api/archive/editions/route.ts +1 -1
  75. package/app/api/cyclotron/priming-proof/route.ts +11 -0
  76. package/app/api/media/[...path]/route.ts +5 -16
  77. package/app/api/revalidate/route.ts +15 -7
  78. package/app/archive/page.tsx +1 -1
  79. package/app/articles/[slug]/page.tsx +7 -5
  80. package/app/blog-rhythm.css +278 -0
  81. package/app/cyclotron/page.tsx +13 -0
  82. package/app/globals.css +2 -0
  83. package/app/layout.tsx +16 -0
  84. package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
  85. package/app/newsroom/[section]/page.tsx +6 -3
  86. package/app/newsroom/layout.tsx +4 -9
  87. package/app/newsroom/settings/page.tsx +6 -0
  88. package/app/newsroom/work/page.tsx +10 -0
  89. package/app/page.tsx +22 -8
  90. package/app/robots.ts +2 -5
  91. package/app/tailwind.css +1 -0
  92. package/components/amplify-client-provider.tsx +5 -22
  93. package/components/archive-shell.tsx +2 -22
  94. package/components/article-video.tsx +152 -5
  95. package/components/blog-page-background.tsx +23 -6
  96. package/components/news-desk-page.tsx +4 -15
  97. package/components/news-desk-taxonomy-client.ts +90 -3
  98. package/components/newspaper.tsx +6 -2
  99. package/components/newsroom-access-gate.tsx +2 -1
  100. package/components/newsroom-board-view.tsx +99 -0
  101. package/components/newsroom-client-shell.tsx +12 -0
  102. package/components/newsroom-curation-provider.tsx +17 -0
  103. package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
  104. package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
  105. package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
  106. package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
  107. package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
  108. package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
  109. package/components/newsroom-newsprint-board.css +48 -0
  110. package/components/newsroom-ops-overview.tsx +28 -11
  111. package/components/newsroom-ops-shell.tsx +7 -1
  112. package/components/newsroom-organizer-shell.tsx +130 -0
  113. package/components/newsroom-organizer.css +63 -0
  114. package/components/newsroom-references-view.tsx +92 -8
  115. package/components/papyrus-console-shell.tsx +4 -2
  116. package/components/pictogram-figure.tsx +9 -2
  117. package/components/presentation-header.tsx +135 -0
  118. package/components/presentation-rhythm-hrule.tsx +17 -0
  119. package/components/priming-proof-page.tsx +92 -0
  120. package/components/publication-cyclotron-panel.tsx +57 -0
  121. package/components/reader-auth-control.tsx +8 -5
  122. package/components/reference-curation-controls.tsx +77 -0
  123. package/components/reference-cyclotron-status.tsx +109 -0
  124. package/components/reference-priming.tsx +18 -0
  125. package/components/reference-relevance-review.tsx +75 -0
  126. package/components/settings-page.tsx +2 -1
  127. package/components/signup-entries-panel.tsx +94 -0
  128. package/components/topic-steering-workspace.tsx +78 -6
  129. package/components/use-blog-header-obstacles.ts +81 -0
  130. package/components/use-brand-fonts-ready.ts +28 -0
  131. package/components/use-near-viewport.ts +30 -0
  132. package/components/use-rhythm-overlay.ts +29 -0
  133. package/infra/amplify-app-shell.js +148 -27
  134. package/infra/amplify-app-shell.ts +160 -31
  135. package/infra/build-specs.js +23 -7
  136. package/infra/build-specs.ts +27 -10
  137. package/infra/site-config.js +248 -36
  138. package/infra/site-config.ts +313 -51
  139. package/lib/amplify-outputs-path.ts +5 -1
  140. package/lib/amplify-server-runtime.ts +36 -1
  141. package/lib/approved-priming-proof-reports.ts +6 -0
  142. package/lib/blog-feature-solver.ts +459 -0
  143. package/lib/blog-rhythm.ts +134 -0
  144. package/lib/brand-fonts-watch.ts +43 -0
  145. package/lib/brand-fonts.ts +112 -0
  146. package/lib/cached-content-repository.ts +12 -0
  147. package/lib/category-repository.ts +43 -93
  148. package/lib/content-item-lifecycle.ts +55 -0
  149. package/lib/content-repository.ts +3 -0
  150. package/lib/content-types.ts +4 -0
  151. package/lib/curation-client.ts +61 -0
  152. package/lib/edition-routes.ts +31 -29
  153. package/lib/graphql-content-repository.ts +56 -22
  154. package/lib/media-route-response.ts +45 -0
  155. package/lib/newspaper-layout.ts +3 -2
  156. package/lib/newsroom-base-path.ts +12 -3
  157. package/lib/newsroom-board.ts +99 -0
  158. package/lib/newsroom-category-drilldown.ts +3 -2
  159. package/lib/newsroom-forum-routes.ts +6 -4
  160. package/lib/newsroom-index-filters.ts +14 -10
  161. package/lib/newsroom-nav.ts +3 -1
  162. package/lib/newsroom-organizer-nav.ts +37 -0
  163. package/lib/newsroom-section-routing.ts +20 -0
  164. package/lib/oauth-redirect-priority.ts +40 -0
  165. package/lib/papyrus-web-locations.ts +7 -6
  166. package/lib/plugin-settings.ts +11 -0
  167. package/lib/presentation-footer.ts +3 -2
  168. package/lib/pretext-layout.ts +12 -3
  169. package/lib/priming-proof-report.ts +89 -0
  170. package/lib/prospectus-site-identity.ts +303 -0
  171. package/lib/publication-cyclotron.ts +74 -0
  172. package/lib/rate-limit-retry.ts +55 -0
  173. package/lib/reader-base-path.ts +24 -0
  174. package/lib/reader-cache-lifetime.ts +5 -0
  175. package/lib/reader-static-params.ts +85 -4
  176. package/lib/reader-storage-url.ts +9 -12
  177. package/lib/reference-policy.ts +5 -0
  178. package/lib/relevance-decisions.ts +103 -0
  179. package/lib/relevance-status.ts +63 -0
  180. package/lib/renderer.ts +7 -0
  181. package/lib/revalidate-prewarm.ts +53 -0
  182. package/lib/revalidate-secret.ts +59 -0
  183. package/lib/robots-policy.ts +14 -0
  184. package/lib/root-route-redirect.ts +16 -0
  185. package/lib/semantic-graph.ts +7 -0
  186. package/lib/shared-credentials-provider.ts +61 -0
  187. package/lib/signup-csv.ts +24 -0
  188. package/lib/site-brand.ts +59 -0
  189. package/lib/staging-gated-path.ts +23 -0
  190. package/lib/staging-preview-object.ts +21 -5
  191. package/lib/test-editor-override.ts +4 -0
  192. package/lib/video-loading.ts +22 -0
  193. package/lib/video-script.ts +14 -0
  194. package/middleware.ts +11 -19
  195. package/package.json +64 -1
  196. package/renderers/pretext/article-page.tsx +39 -3
  197. package/renderers/pretext/presentation-shared.tsx +113 -0
  198. package/renderers/pretext/presentation-shell.tsx +15 -102
  199. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  200. package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
  201. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  202. package/routes.manifest.json +68 -12
  203. package/src/sync.mjs +30 -6
  204. package/src/with-papyrus.d.mts +9 -0
  205. package/src/with-papyrus.mjs +48 -3
  206. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  207. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  208. package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
@@ -3,7 +3,7 @@ import { loadArchiveEditionPreviews, clampArchiveLimit } from "../../../../lib/a
3
3
  import { ARCHIVE_BATCH_SIZE } from "../../../../lib/archive-types";
4
4
 
5
5
  // Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
6
- export const revalidate = 3600;
6
+ export const revalidate = 60;
7
7
 
8
8
  export async function GET(request: Request) {
9
9
  const url = new URL(request.url);
@@ -0,0 +1,11 @@
1
+ import { NextResponse } from "next/server";
2
+ import { approvedPrimingProofReport } from "../../../../lib/priming-proof-report";
3
+ import { SITE_BRAND } from "../../../../lib/site-brand";
4
+
5
+ export function GET() {
6
+ const report = approvedPrimingProofReport(SITE_BRAND.id);
7
+ if (report === null) {
8
+ return NextResponse.json({ error: "No approved priming proof report for this publication yet." }, { status: 404 });
9
+ }
10
+ return NextResponse.json(report);
11
+ }
@@ -1,4 +1,5 @@
1
1
  import { NextResponse } from "next/server";
2
+ import { buildMediaRouteResponse } from "../../../../lib/media-route-response";
2
3
  import { isGuestReadableStoragePath, signStorageUrl } from "../../../../lib/reader-storage-url";
3
4
 
4
5
  export const dynamic = "force-dynamic";
@@ -16,20 +17,8 @@ export async function GET(_request: Request, context: MediaRouteContext) {
16
17
  return NextResponse.json({ error: "Forbidden storage path." }, { status: 403 });
17
18
  }
18
19
 
19
- const signedUrl = await signStorageUrl(storagePath);
20
- const upstream = await fetch(signedUrl, { cache: "no-store" });
21
- if (!upstream.ok) {
22
- return NextResponse.json(
23
- { error: `Upstream media fetch failed (${upstream.status}).` },
24
- { status: upstream.status === 404 ? 404 : 502 },
25
- );
26
- }
27
-
28
- const headers = new Headers();
29
- const contentType = upstream.headers.get("content-type");
30
- if (contentType) {
31
- headers.set("content-type", contentType);
32
- }
33
- headers.set("cache-control", "public, max-age=3600, stale-while-revalidate=86400");
34
- return new NextResponse(upstream.body, { status: 200, headers });
20
+ return buildMediaRouteResponse(storagePath, {
21
+ signStorageUrl,
22
+ fetchUpstream: (signedUrl) => fetch(signedUrl, { cache: "no-store" }),
23
+ });
35
24
  }
@@ -1,3 +1,4 @@
1
+ import { toPublicReaderPath } from "../../../lib/reader-base-path";
1
2
  import { revalidatePath, revalidateTag } from "next/cache";
2
3
  import { NextResponse } from "next/server";
3
4
  import {
@@ -8,6 +9,8 @@ import {
8
9
  editionContentCacheTag,
9
10
  editionItemCacheTag,
10
11
  } from "../../../lib/cached-content-repository";
12
+ import { isAuthorizedRevalidateRequest } from "../../../lib/revalidate-secret";
13
+ import { prewarmPaths, resolvePrewarmOrigin } from "../../../lib/revalidate-prewarm";
11
14
  import { getEditionArticlePath, getEditionDatePath } from "../../../lib/edition-routes";
12
15
 
13
16
  export const dynamic = "force-dynamic";
@@ -18,12 +21,12 @@ type RevalidateRequestBody = {
18
21
  itemSlugs?: string[];
19
22
  paths?: string[];
20
23
  tags?: string[];
24
+ prewarm?: boolean;
21
25
  };
22
26
 
23
27
  export async function POST(request: Request) {
24
- const configuredSecret = process.env.PAPYRUS_REVALIDATE_SECRET?.trim();
25
- const providedSecret = request.headers.get("x-papyrus-revalidate-secret")?.trim();
26
- if (!configuredSecret || providedSecret !== configuredSecret) {
28
+ const providedSecret = request.headers.get("x-papyrus-revalidate-secret");
29
+ if (!(await isAuthorizedRevalidateRequest(providedSecret))) {
27
30
  return NextResponse.json({ ok: false, error: "Unauthorized." }, { status: 401 });
28
31
  }
29
32
 
@@ -44,8 +47,8 @@ export async function POST(request: Request) {
44
47
  if (!normalized) continue;
45
48
  revalidateTag(articleCacheTag(normalized));
46
49
  revalidatedTags.add(articleCacheTag(normalized));
47
- revalidatePath(`/articles/${encodeURIComponent(normalized)}`);
48
- revalidatedPaths.add(`/articles/${encodeURIComponent(normalized)}`);
50
+ revalidatePath(toPublicReaderPath(`/articles/${encodeURIComponent(normalized)}`));
51
+ revalidatedPaths.add(toPublicReaderPath(`/articles/${encodeURIComponent(normalized)}`));
49
52
  if (editionDate) {
50
53
  revalidateTag(editionItemCacheTag(editionDate, normalized));
51
54
  revalidatedTags.add(editionItemCacheTag(editionDate, normalized));
@@ -76,8 +79,8 @@ export async function POST(request: Request) {
76
79
  revalidatedTags.add(ARCHIVE_CACHE_TAG);
77
80
  revalidateTag(ARTICLES_CACHE_TAG);
78
81
  revalidatedTags.add(ARTICLES_CACHE_TAG);
79
- revalidatePath("/archive");
80
- revalidatedPaths.add("/archive");
82
+ revalidatePath(toPublicReaderPath("/archive"));
83
+ revalidatedPaths.add(toPublicReaderPath("/archive"));
81
84
 
82
85
  for (const path of body.paths ?? []) {
83
86
  const normalized = path.trim();
@@ -86,9 +89,14 @@ export async function POST(request: Request) {
86
89
  revalidatedPaths.add(normalized);
87
90
  }
88
91
 
92
+ const prewarmed = body.prewarm === false
93
+ ? []
94
+ : await prewarmPaths(resolvePrewarmOrigin(request.url), [...revalidatedPaths]);
95
+
89
96
  return NextResponse.json({
90
97
  ok: true,
91
98
  revalidatedTags: [...revalidatedTags],
92
99
  revalidatedPaths: [...revalidatedPaths],
100
+ prewarmed,
93
101
  });
94
102
  }
@@ -4,7 +4,7 @@ import { loadArchiveEditionPreviews } from "../../lib/archive-data";
4
4
  import { ARCHIVE_BATCH_SIZE } from "../../lib/archive-types";
5
5
 
6
6
  // Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
7
- export const revalidate = 3600;
7
+ export const revalidate = 60;
8
8
 
9
9
  export default async function ArchivePage() {
10
10
  const initialBatch = await loadArchiveEditionPreviews({ limit: ARCHIVE_BATCH_SIZE });
@@ -1,11 +1,11 @@
1
1
  import { notFound } from "next/navigation";
2
2
  import { getSiteRenderer } from "../../../lib/site-renderer";
3
- import { getCachedArticle } from "../../../lib/cached-content-repository";
4
- import { generateArticleStaticParams } from "../../../lib/reader-static-params";
3
+ import { getCachedArticle, getCachedVideoScript } from "../../../lib/cached-content-repository";
4
+ import { generateArticleStaticParams, siteServesNextReaderRoutes } from "../../../lib/reader-static-params";
5
5
  import { SITE_BRAND } from "../../../lib/site-brand";
6
6
 
7
7
  // Keep in sync with READER_REVALIDATE_SECONDS in lib/reader-route-config.ts
8
- export const revalidate = 3600;
8
+ export const revalidate = 60;
9
9
 
10
10
  export async function generateStaticParams() {
11
11
  return generateArticleStaticParams();
@@ -16,6 +16,7 @@ type ArticlePageProps = {
16
16
  };
17
17
 
18
18
  export async function generateMetadata({ params }: ArticlePageProps) {
19
+ if (!siteServesNextReaderRoutes()) return {};
19
20
  const { slug } = await params;
20
21
  const article = await getCachedArticle(slug);
21
22
  if (!article) return {};
@@ -26,10 +27,11 @@ export async function generateMetadata({ params }: ArticlePageProps) {
26
27
  }
27
28
 
28
29
  export default async function ArticlePage({ params }: ArticlePageProps) {
30
+ if (!siteServesNextReaderRoutes()) notFound();
29
31
  const { slug } = await params;
30
- const article = await getCachedArticle(slug);
32
+ const [article, videoScript] = await Promise.all([getCachedArticle(slug), getCachedVideoScript(slug)]);
31
33
  if (!article) notFound();
32
34
 
33
35
  const siteRenderer = getSiteRenderer();
34
- return <siteRenderer.renderArticle article={article} backHref="/" />;
36
+ return <siteRenderer.renderArticle article={article} videoScript={videoScript} backHref="/" />;
35
37
  }
@@ -0,0 +1,278 @@
1
+ /*
2
+ Structural CSS for the rhythm blog layout (blogLayout: "rhythm").
3
+ Every rule is scoped to .blog-rhythm-shell, which only the rhythm layout
4
+ renders, so classic layouts are unaffected. The shell sets --blog-rhythm,
5
+ --blog-row-height, --blog-paint-buffer and --blog-paint-height inline.
6
+ Typography, colors and the header look belong to the publication's theme.css.
7
+ Optional tokens a publication may set:
8
+ --blog-hrule-foreground, --blog-section-rule, --blog-section-band-height,
9
+ --blog-page-padding-x, --blog-secondary-pair-min-width.
10
+ */
11
+
12
+ .blog-rhythm-shell {
13
+ --blog-hrule-foreground: var(--border);
14
+ --blog-section-rule: var(--border);
15
+ --blog-section-band-height: calc(var(--blog-row-height) * 2);
16
+ --blog-page-padding-x: calc(var(--blog-rhythm) * 3);
17
+ --paper-rhythm: var(--blog-row-height);
18
+
19
+ isolation: isolate;
20
+ position: relative;
21
+ }
22
+
23
+ .blog-rhythm-shell.presentation-page--blog {
24
+ padding: calc(var(--blog-row-height) * 4) var(--blog-page-padding-x) 0;
25
+ }
26
+
27
+ .blog-rhythm-shell > :not(.blog-page-background) {
28
+ position: relative;
29
+ z-index: 1;
30
+ }
31
+
32
+ .blog-rhythm-shell[data-rhythm-overlay="true"]::before {
33
+ background-image:
34
+ repeating-linear-gradient(
35
+ to bottom,
36
+ rgb(var(--rhythm-overlay-rule-rgb) / var(--rhythm-overlay-rule-alpha)) 0,
37
+ rgb(var(--rhythm-overlay-rule-rgb) / var(--rhythm-overlay-rule-alpha)) 1px,
38
+ transparent 1px,
39
+ transparent var(--blog-row-height)
40
+ ),
41
+ repeating-linear-gradient(
42
+ to bottom,
43
+ rgb(var(--rhythm-overlay-blue-rgb) / var(--rhythm-overlay-blue-alpha)) 0,
44
+ rgb(var(--rhythm-overlay-blue-rgb) / var(--rhythm-overlay-blue-alpha)) var(--blog-row-height),
45
+ rgb(var(--rhythm-overlay-yellow-rgb) / var(--rhythm-overlay-yellow-alpha)) var(--blog-row-height),
46
+ rgb(var(--rhythm-overlay-yellow-rgb) / var(--rhythm-overlay-yellow-alpha)) calc(var(--blog-row-height) * 2)
47
+ );
48
+ content: "";
49
+ inset: 0;
50
+ pointer-events: none;
51
+ position: absolute;
52
+ z-index: 900;
53
+ }
54
+
55
+ .blog-rhythm-shell .presentation-header {
56
+ max-width: 100%;
57
+ min-width: 0;
58
+ width: 100%;
59
+ }
60
+
61
+ .blog-rhythm-shell .presentation-section-header,
62
+ .blog-rhythm-shell .presentation-item,
63
+ .blog-rhythm-shell .presentation-item__copy {
64
+ max-width: 100%;
65
+ min-width: 0;
66
+ }
67
+
68
+ .blog-rhythm-shell .presentation-section-header {
69
+ align-items: stretch;
70
+ border-bottom: 0;
71
+ border-top: 0;
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: var(--blog-row-height);
75
+ margin: 0;
76
+ min-height: 0;
77
+ padding: calc(var(--blog-row-height) * 4) 0 calc(var(--blog-row-height) * 2);
78
+ width: 100%;
79
+ }
80
+
81
+ .blog-rhythm-shell .presentation-section-header__band {
82
+ align-items: center;
83
+ display: grid;
84
+ gap: 0 var(--blog-rhythm);
85
+ grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
86
+ grid-template-rows: var(--blog-section-band-height);
87
+ width: 100%;
88
+ }
89
+
90
+ .blog-rhythm-shell .presentation-section-header__band::before,
91
+ .blog-rhythm-shell .presentation-section-header__band::after {
92
+ background: var(--blog-section-rule);
93
+ content: "";
94
+ grid-row: 1;
95
+ height: var(--blog-section-band-height);
96
+ }
97
+
98
+ .blog-rhythm-shell .presentation-section-header__band::before {
99
+ grid-column: 1;
100
+ }
101
+
102
+ .blog-rhythm-shell .presentation-section-header__band::after {
103
+ grid-column: 3;
104
+ }
105
+
106
+ .blog-rhythm-shell .presentation-section-header__band p {
107
+ grid-column: 2;
108
+ grid-row: 1;
109
+ height: var(--blog-section-band-height);
110
+ justify-self: center;
111
+ line-height: var(--blog-section-band-height);
112
+ margin: 0;
113
+ padding: 0 var(--blog-rhythm);
114
+ width: max-content;
115
+ }
116
+
117
+ .blog-rhythm-shell .presentation-item {
118
+ border-bottom: 0;
119
+ gap: var(--blog-row-height) calc(var(--blog-rhythm) * 4);
120
+ padding-bottom: 0;
121
+ }
122
+
123
+ .blog-rhythm-shell .presentation-item--blog {
124
+ grid-template-columns: minmax(0, 1fr);
125
+ }
126
+
127
+ .blog-rhythm-shell .presentation-rhythm-hrule {
128
+ display: grid;
129
+ grid-column: 1 / -1;
130
+ grid-template-rows: repeat(5, var(--blog-row-height));
131
+ height: calc(var(--blog-row-height) * 5);
132
+ width: 100%;
133
+ }
134
+
135
+ .blog-rhythm-shell .presentation-rhythm-hrule__line {
136
+ background: var(--blog-hrule-foreground);
137
+ grid-row: 3;
138
+ height: var(--blog-row-height);
139
+ }
140
+
141
+ @media (min-width: 1100px) {
142
+ .blog-rhythm-shell .blog-section:has(> .presentation-item[data-item-index="2"]) {
143
+ column-gap: calc(var(--blog-row-height) * 2);
144
+ grid-template-columns: repeat(2, minmax(0, 1fr));
145
+ }
146
+
147
+ .blog-rhythm-shell .blog-section:has(> .presentation-item[data-item-index="2"]) > .presentation-section-header,
148
+ .blog-rhythm-shell .blog-section:has(> .presentation-item[data-item-index="2"]) > .presentation-item[data-item-index="0"] {
149
+ grid-column: 1 / -1;
150
+ }
151
+
152
+ .blog-rhythm-shell .blog-section:has(> .presentation-item[data-item-index="2"]) > .presentation-rhythm-hrule[data-hide-in-secondary-pair="true"] {
153
+ display: none;
154
+ }
155
+ }
156
+
157
+ .blog-rhythm-shell .presentation-item[data-feature-layout][data-has-image="true"] {
158
+ display: flex;
159
+ flex-direction: column;
160
+ gap: 0;
161
+ grid-template-columns: minmax(0, 1fr);
162
+ }
163
+
164
+ .blog-rhythm-shell .presentation-item[data-feature-layout] .presentation-item__text-frame {
165
+ height: var(--feature-text-frame-height);
166
+ max-width: none;
167
+ overflow: hidden;
168
+ position: relative;
169
+ width: 100%;
170
+ }
171
+
172
+ .blog-rhythm-shell .presentation-item[data-feature-layout] .presentation-item__media {
173
+ height: var(--feature-media-height);
174
+ width: var(--feature-image-width);
175
+ }
176
+
177
+ .blog-rhythm-shell .presentation-item[data-feature-layout] .pictogram-figure__frame {
178
+ aspect-ratio: auto;
179
+ height: var(--feature-image-height);
180
+ width: var(--feature-image-width);
181
+ }
182
+
183
+ .blog-rhythm-shell .presentation-item--blog[data-feature-layout][data-has-image="true"] .presentation-item__copy {
184
+ display: contents;
185
+ }
186
+
187
+ .blog-rhythm-shell .presentation-item--blog[data-feature-layout][data-has-image="true"] .presentation-item__header {
188
+ width: 100%;
189
+ }
190
+
191
+ .blog-rhythm-shell .presentation-item--blog[data-feature-layout][data-has-image="true"] .presentation-item__body {
192
+ min-width: 0;
193
+ position: relative;
194
+ width: 100%;
195
+ }
196
+
197
+ .blog-rhythm-shell .presentation-item--blog[data-feature-layout][data-has-image="true"] .presentation-item__body .presentation-item__text-frame {
198
+ margin-top: 0;
199
+ }
200
+
201
+ .blog-rhythm-shell .presentation-item--blog[data-feature-layout][data-has-image="true"] .presentation-item__media {
202
+ margin: 0;
203
+ position: absolute;
204
+ right: 0;
205
+ top: 0;
206
+ z-index: 1;
207
+ }
208
+
209
+ .blog-rhythm-shell .presentation-item__image img {
210
+ height: 100%;
211
+ object-fit: contain;
212
+ width: 100%;
213
+ }
214
+
215
+ .blog-rhythm-shell .presentation-item__video {
216
+ grid-column: 1 / -1;
217
+ margin: var(--blog-row-height) 0 0;
218
+ }
219
+
220
+ .blog-rhythm-shell .article-page__hero-video {
221
+ margin: 0 0 calc(var(--blog-rhythm) * 5);
222
+ }
223
+
224
+ .blog-rhythm-shell .article-page__hero-video .article-video,
225
+ .blog-rhythm-shell .presentation-item__video-figure {
226
+ container-type: inline-size;
227
+ }
228
+
229
+ .blog-rhythm-shell .presentation-item__video-figure .article-video__media,
230
+ .blog-rhythm-shell .article-page__hero-video .article-video__media {
231
+ height: round(up, calc(100cqw * 9 / 16), var(--blog-row-height));
232
+ }
233
+
234
+ .blog-rhythm-shell .article-float-grid[data-feature-layout][data-has-image="true"] {
235
+ display: grid;
236
+ gap: var(--feature-layout-gap);
237
+ grid-template-columns: minmax(0, 1fr) var(--feature-image-width);
238
+ }
239
+
240
+ .blog-rhythm-shell .article-float-grid[data-feature-layout] .article-page__hero-video,
241
+ .blog-rhythm-shell .article-float-grid[data-feature-layout] .article-float-grid__header {
242
+ grid-column: 1 / -1;
243
+ }
244
+
245
+ .blog-rhythm-shell .article-float-grid[data-feature-layout] .article-float-grid__body {
246
+ align-self: start;
247
+ grid-column: 1;
248
+ max-width: var(--feature-copy-width);
249
+ min-width: 0;
250
+ width: var(--feature-copy-width);
251
+ }
252
+
253
+ .blog-rhythm-shell .article-float-grid[data-feature-layout] .article-float-grid__media,
254
+ .blog-rhythm-shell .article-float-grid[data-feature-layout] .pictogram-figure__frame {
255
+ height: var(--feature-image-height);
256
+ width: var(--feature-image-width);
257
+ }
258
+
259
+ .blog-rhythm-shell .article-float-grid[data-feature-layout] .pictogram-figure__frame {
260
+ aspect-ratio: auto;
261
+ }
262
+
263
+ .blog-rhythm-shell .article-float-grid[data-feature-layout] .article-float-grid__media {
264
+ align-self: start;
265
+ grid-column: 2;
266
+ margin: 0;
267
+ position: relative;
268
+ }
269
+
270
+ .blog-rhythm-shell .article-float-grid .presentation-item__image {
271
+ margin: 0;
272
+ }
273
+
274
+ @media (max-width: 900px) {
275
+ .blog-rhythm-shell .presentation-item--blog {
276
+ grid-template-columns: 1fr;
277
+ }
278
+ }
@@ -0,0 +1,13 @@
1
+ import type { Metadata } from "next";
2
+ import { PrimingProofPage } from "../../components/priming-proof-page";
3
+ import { approvedPrimingProofReport } from "../../lib/priming-proof-report";
4
+ import { SITE_BRAND } from "../../lib/site-brand";
5
+
6
+ export const metadata: Metadata = {
7
+ title: `Cyclotron proof | ${SITE_BRAND.articleTitleSuffix}`,
8
+ description: "How often the relevance classifier agrees with editors on a fixed held-out set, before and after editor labels.",
9
+ };
10
+
11
+ export default function CyclotronProof() {
12
+ return <PrimingProofPage report={approvedPrimingProofReport(SITE_BRAND.id)} />;
13
+ }
package/app/globals.css CHANGED
@@ -45,6 +45,7 @@
45
45
  --color-surface-foreground: var(--surface-foreground);
46
46
  --color-foreground-strong: var(--foreground-strong);
47
47
  --color-foreground-muted: var(--foreground-muted);
48
+ --color-foreground-subtle: var(--foreground-subtle);
48
49
  }
49
50
 
50
51
  :root {
@@ -71,6 +72,7 @@
71
72
  --surface-foreground: var(--card-foreground);
72
73
  --foreground-strong: var(--slate-12);
73
74
  --foreground-muted: var(--slate-10);
75
+ --foreground-subtle: var(--slate-9);
74
76
  --border: var(--slate-6);
75
77
  --input: var(--slate-6);
76
78
  --ring: var(--slate-8);
package/app/layout.tsx CHANGED
@@ -4,12 +4,14 @@ import Script from "next/script";
4
4
  import { AmplifyClientProvider } from "../components/amplify-client-provider";
5
5
  import { PapyrusConsoleShell } from "../components/papyrus-console-shell";
6
6
  import { TooltipProvider } from "../components/ui/tooltip";
7
+ import { resolveBrandFontAssets } from "../lib/brand-fonts";
7
8
  import { getPresentationChoices, getSiteBrand } from "../lib/site-brand";
8
9
  import { getSiteStack } from "../lib/site-stack";
9
10
  import { isIndexable, showStagingBanner } from "../lib/site-env";
10
11
  import { StagingBanner } from "../components/staging-banner";
11
12
  import "./tailwind.css";
12
13
  import "./globals.css";
14
+ import "./blog-rhythm.css";
13
15
  import "papyrus-site-theme";
14
16
 
15
17
  const playfairDisplay = Playfair_Display({
@@ -43,6 +45,7 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
43
45
  const siteBrand = getSiteBrand();
44
46
  const presentationChoices = getPresentationChoices(siteBrand);
45
47
  const siteStack = getSiteStack(siteBrand);
48
+ const brandFonts = resolveBrandFontAssets(siteBrand.fonts);
46
49
 
47
50
  return (
48
51
  <html
@@ -56,8 +59,21 @@ export default async function RootLayout({ children }: Readonly<{ children: Reac
56
59
  data-default-presentation={siteBrand.defaultPresentation}
57
60
  data-presentation-choices={presentationChoices.join(",")}
58
61
  {...(siteBrand.forcedPresentation ? { "data-forced-presentation": siteBrand.forcedPresentation } : {})}
62
+ {...(siteBrand.blogLayout === "rhythm" ? { "data-blog-layout": "rhythm" } : {})}
63
+ {...(brandFonts.isEmpty ? {} : { style: brandFonts.rootStyle as React.CSSProperties })}
59
64
  suppressHydrationWarning
60
65
  >
66
+ {brandFonts.isEmpty ? null : (
67
+ <head>
68
+ {brandFonts.preconnectOrigins.map((origin) => (
69
+ <link crossOrigin="anonymous" href={origin} key={origin} rel="preconnect" />
70
+ ))}
71
+ {brandFonts.stylesheetHrefs.map((href) => (
72
+ <link href={href} key={href} rel="stylesheet" />
73
+ ))}
74
+ {brandFonts.fontFaceCss ? <style data-brand-fonts="true">{brandFonts.fontFaceCss}</style> : null}
75
+ </head>
76
+ )}
61
77
  <body className={`${playfairDisplay.variable} ${plusJakartaSans.variable}`}>
62
78
  <Script id="papyrus-favicon-color-scheme" strategy="beforeInteractive">
63
79
  {`(() => {
@@ -1,4 +1,6 @@
1
+ import { resolveActiveSiteBrand } from "../../../../lib/site-brand-tenant";
1
2
  import { notFound, redirect } from "next/navigation";
3
+ import { toPublicNewsroomPath } from "../../../../lib/newsroom-base-path";
2
4
  import { NewsDeskPage } from "../../../../components/news-desk-page";
3
5
  import type { NewsDeskTab } from "../../../../components/topic-steering-workspace";
4
6
 
@@ -26,6 +28,7 @@ type NewsDeskNestedSectionPageProps = {
26
28
  };
27
29
 
28
30
  const NEWS_DESK_ROUTE_SECTIONS = new Set<NewsDeskTab>([
31
+ "desks",
29
32
  "administration",
30
33
  "topics",
31
34
  "concepts",
@@ -37,13 +40,13 @@ const NEWS_DESK_ROUTE_SECTIONS = new Set<NewsDeskTab>([
37
40
 
38
41
  export default async function NewsDeskNestedSectionPage({ params, searchParams }: NewsDeskNestedSectionPageProps) {
39
42
  const { section, selection } = await params;
40
- if (section === "users") redirect("/newsroom/administration/users");
41
- if (section === "doctrine") redirect("/newsroom/administration/policies");
42
- if (section === "administration" && selection[0] === "analysis") redirect("/newsroom/assignments");
43
- if (section === "desks") {
43
+ if (section === "users") redirect(toPublicNewsroomPath("/newsroom/administration/users"));
44
+ if (section === "doctrine") redirect(toPublicNewsroomPath("/newsroom/administration/policies"));
45
+ if (section === "administration" && selection[0] === "analysis") redirect(toPublicNewsroomPath("/newsroom/assignments"));
46
+ if (section === "desks" && resolveActiveSiteBrand().newsroomLayout !== "organizer") {
44
47
  const category = selection[0] ? decodeURIComponent(selection[0]) : null;
45
- if (category) redirect(`/newsroom/topics?category=${encodeURIComponent(category)}`);
46
- redirect("/newsroom/topics");
48
+ if (category) redirect(toPublicNewsroomPath(`/newsroom/topics?category=${encodeURIComponent(category)}`));
49
+ redirect(toPublicNewsroomPath("/newsroom/topics"));
47
50
  }
48
51
  if (!NEWS_DESK_ROUTE_SECTIONS.has(section as NewsDeskTab)) notFound();
49
52
  return <NewsDeskPage section={section} selectionPath={selection} searchParams={searchParams} />;
@@ -1,4 +1,6 @@
1
+ import { resolveActiveSiteBrand } from "../../../lib/site-brand-tenant";
1
2
  import { notFound, redirect } from "next/navigation";
3
+ import { toPublicNewsroomPath } from "../../../lib/newsroom-base-path";
2
4
  import { NewsDeskPage } from "../../../components/news-desk-page";
3
5
  import type { NewsDeskTab } from "../../../components/topic-steering-workspace";
4
6
 
@@ -27,6 +29,7 @@ type NewsDeskSectionPageProps = {
27
29
  };
28
30
 
29
31
  const NEWS_DESK_ROUTE_SECTIONS = new Set<NewsDeskTab>([
32
+ "desks",
30
33
  "administration",
31
34
  "topics",
32
35
  "concepts",
@@ -39,9 +42,9 @@ const NEWS_DESK_ROUTE_SECTIONS = new Set<NewsDeskTab>([
39
42
 
40
43
  export default async function NewsDeskSectionPage({ params, searchParams }: NewsDeskSectionPageProps) {
41
44
  const { section } = await params;
42
- if (section === "users") redirect("/newsroom/administration/users");
43
- if (section === "doctrine") redirect("/newsroom/administration/policies");
44
- if (section === "desks") redirect("/newsroom/topics");
45
+ if (section === "users") redirect(toPublicNewsroomPath("/newsroom/administration/users"));
46
+ if (section === "doctrine") redirect(toPublicNewsroomPath("/newsroom/administration/policies"));
47
+ if (section === "desks" && resolveActiveSiteBrand().newsroomLayout !== "organizer") redirect(toPublicNewsroomPath("/newsroom/topics"));
45
48
  if (!NEWS_DESK_ROUTE_SECTIONS.has(section as NewsDeskTab)) notFound();
46
49
  return <NewsDeskPage section={section} searchParams={searchParams} />;
47
50
  }
@@ -1,12 +1,7 @@
1
- import { NewsDeskClientProvider } from "../../components/news-desk-client-provider";
2
- import { resolveDevSandboxEditorAuth } from "../../lib/dev-sandbox-editor-auth";
1
+ // Component rules only (no reset, no theme): the Cyclotron review control and status widget.
2
+ import "cyclotron/styles/components.css";
3
+ import { NewsroomClientShell } from "../../components/newsroom-client-shell";
3
4
 
4
5
  export default function NewsroomLayout({ children }: Readonly<{ children: React.ReactNode }>) {
5
- const devSandboxEditorAuth = resolveDevSandboxEditorAuth();
6
-
7
- return (
8
- <NewsDeskClientProvider devSandboxEditorAuth={devSandboxEditorAuth}>
9
- {children}
10
- </NewsDeskClientProvider>
11
- );
6
+ return <NewsroomClientShell>{children}</NewsroomClientShell>;
12
7
  }
@@ -0,0 +1,6 @@
1
+ import Settings from "../../settings/page";
2
+
3
+ export { metadata } from "../../settings/page";
4
+ export const dynamic = "force-dynamic";
5
+
6
+ export default Settings;
@@ -0,0 +1,10 @@
1
+ import { notFound } from "next/navigation";
2
+ import { resolveActiveSiteBrand } from "../../../lib/site-brand-tenant";
3
+ import { NewsroomBoardView } from "../../../components/newsroom-board-view";
4
+
5
+ export const dynamic = "force-dynamic";
6
+
7
+ export default function NewsroomWorkPage() {
8
+ if (resolveActiveSiteBrand().newsroomLayout !== "organizer") notFound();
9
+ return <NewsroomBoardView />;
10
+ }