@anthusai/papyrus 0.0.0 → 1.0.0-next.2

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 (254) hide show
  1. package/LICENSE +21 -0
  2. package/amplify/auth/resource.js +61 -0
  3. package/amplify/auth/resource.ts +65 -0
  4. package/amplify/data/resource.js +2037 -0
  5. package/amplify/data/resource.ts +2148 -0
  6. package/amplify/data/schema.js +1850 -0
  7. package/amplify/data/schema.ts +1953 -0
  8. package/amplify/functions/assignment-action/handler.ts +1416 -0
  9. package/amplify/functions/assignment-action/resource.js +8 -0
  10. package/amplify/functions/assignment-action/resource.ts +9 -0
  11. package/amplify/functions/category-action/handler.ts +2259 -0
  12. package/amplify/functions/category-action/resource.js +8 -0
  13. package/amplify/functions/category-action/resource.ts +9 -0
  14. package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
  15. package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
  16. package/amplify/functions/console-chat-responder/Dockerfile +19 -0
  17. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
  18. package/amplify/functions/console-chat-responder/resource.js +153 -0
  19. package/amplify/functions/console-chat-responder/resource.ts +179 -0
  20. package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
  21. package/amplify/functions/email-submission-processor/handler.py +46 -0
  22. package/amplify/functions/email-submission-processor/requirements.txt +2 -0
  23. package/amplify/functions/email-submission-processor/resource.js +56 -0
  24. package/amplify/functions/email-submission-processor/resource.ts +69 -0
  25. package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
  26. package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
  27. package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
  28. package/amplify/functions/knowledge-query/handler.py +1 -0
  29. package/amplify/functions/knowledge-query/requirements.txt +3 -0
  30. package/amplify/functions/knowledge-query/resource.js +50 -0
  31. package/amplify/functions/knowledge-query/resource.ts +61 -0
  32. package/amplify/functions/manage-user-role/handler.ts +799 -0
  33. package/amplify/functions/manage-user-role/resource.js +8 -0
  34. package/amplify/functions/manage-user-role/resource.ts +9 -0
  35. package/amplify/functions/model-attachment-upload/handler.ts +387 -0
  36. package/amplify/functions/model-attachment-upload/resource.js +8 -0
  37. package/amplify/functions/model-attachment-upload/resource.ts +9 -0
  38. package/amplify/functions/newsroom-summary/handler.ts +321 -0
  39. package/amplify/functions/newsroom-summary/resource.js +8 -0
  40. package/amplify/functions/newsroom-summary/resource.ts +9 -0
  41. package/amplify/functions/procedure-action/handler.ts +621 -0
  42. package/amplify/functions/procedure-action/resource.js +8 -0
  43. package/amplify/functions/procedure-action/resource.ts +9 -0
  44. package/amplify/functions/reader-settings/handler.ts +322 -0
  45. package/amplify/functions/reader-settings/resource.js +8 -0
  46. package/amplify/functions/reader-settings/resource.ts +9 -0
  47. package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
  48. package/amplify/functions/ses-inbound-receive/resource.js +8 -0
  49. package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
  50. package/amplify/functions/shared/amplify-secrets.ts +65 -0
  51. package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
  52. package/amplify/functions/shared/email-mime-intake.ts +196 -0
  53. package/amplify/functions/shared/email-submission.ts +415 -0
  54. package/amplify/functions/shared/inbound-email-intake.ts +37 -0
  55. package/amplify/functions/shared/lambda-data-client.ts +21 -0
  56. package/amplify/functions/shared/model-payloads.ts +186 -0
  57. package/amplify/functions/shared/python-bundle.js +115 -0
  58. package/amplify/functions/shared/python-bundle.ts +141 -0
  59. package/amplify/functions/shared/slack-events.ts +111 -0
  60. package/amplify/functions/slack-delivery/handler.py +112 -0
  61. package/amplify/functions/slack-delivery/requirements.txt +2 -0
  62. package/amplify/functions/slack-delivery/resource.js +38 -0
  63. package/amplify/functions/slack-delivery/resource.ts +51 -0
  64. package/amplify/functions/slack-events/handler.ts +155 -0
  65. package/amplify/functions/slack-events/resource.js +11 -0
  66. package/amplify/functions/slack-events/resource.ts +12 -0
  67. package/amplify/inbound-email/stack.js +46 -0
  68. package/amplify/inbound-email/stack.ts +61 -0
  69. package/amplify/package.json +3 -0
  70. package/amplify/site-backend-config.js +1 -0
  71. package/amplify/site-backend-config.ts +30 -0
  72. package/amplify/site-backend.js +587 -0
  73. package/amplify/site-backend.ts +704 -0
  74. package/amplify/storage/resource.js +18 -0
  75. package/amplify/storage/resource.ts +19 -0
  76. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
  77. package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
  78. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
  79. package/app/[year]/[month]/[day]/page.tsx +22 -0
  80. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
  81. package/app/api/archive/editions/route.ts +18 -0
  82. package/app/api/media/[...path]/route.ts +35 -0
  83. package/app/api/revalidate/route.ts +94 -0
  84. package/app/archive/page.tsx +24 -0
  85. package/app/articles/[slug]/page.tsx +35 -0
  86. package/app/globals.css +10329 -0
  87. package/app/layout.tsx +146 -0
  88. package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
  89. package/app/newsroom/[section]/page.tsx +47 -0
  90. package/app/newsroom/forum/[threadId]/page.tsx +35 -0
  91. package/app/newsroom/insights/[threadId]/page.tsx +22 -0
  92. package/app/newsroom/insights/page.tsx +14 -0
  93. package/app/newsroom/layout.tsx +12 -0
  94. package/app/newsroom/page.tsx +31 -0
  95. package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
  96. package/app/page.tsx +272 -0
  97. package/app/settings/page.tsx +14 -0
  98. package/app/tailwind.css +65 -0
  99. package/bin/papyrus-app.mjs +10 -0
  100. package/bin/papyrus-infra.mjs +39 -0
  101. package/components/ai-elements/conversation.tsx +168 -0
  102. package/components/ai-elements/message.tsx +360 -0
  103. package/components/ai-elements/model-selector.tsx +30 -0
  104. package/components/ai-elements/prompt-input.tsx +1461 -0
  105. package/components/ai-elements/shimmer.tsx +76 -0
  106. package/components/ai-elements/suggestion.tsx +54 -0
  107. package/components/ai-elements/tool.tsx +173 -0
  108. package/components/amplify-client-provider.tsx +140 -0
  109. package/components/archive-grid.tsx +89 -0
  110. package/components/archive-shell.tsx +41 -0
  111. package/components/article-video.tsx +47 -0
  112. package/components/blog-page-background.tsx +20 -0
  113. package/components/dev-sandbox-editor-sign-in.tsx +60 -0
  114. package/components/news-desk-client-provider.tsx +211 -0
  115. package/components/news-desk-page.tsx +119 -0
  116. package/components/news-desk-taxonomy-client.ts +3684 -0
  117. package/components/newspaper.tsx +1929 -0
  118. package/components/newsroom-assignments-view.tsx +664 -0
  119. package/components/newsroom-ops-overview.tsx +226 -0
  120. package/components/newsroom-ops-shell.tsx +319 -0
  121. package/components/newsroom-references-view.tsx +598 -0
  122. package/components/newsroom-topics-view.tsx +425 -0
  123. package/components/papyrus-console-shell.tsx +1912 -0
  124. package/components/pictogram-figure.tsx +65 -0
  125. package/components/presentation-footer.tsx +146 -0
  126. package/components/reader-auth-control.tsx +162 -0
  127. package/components/reader-auth-state.ts +122 -0
  128. package/components/reader-settings.ts +332 -0
  129. package/components/reader-theme-control.tsx +85 -0
  130. package/components/reference-source-preview.tsx +163 -0
  131. package/components/settings-page.tsx +239 -0
  132. package/components/topic-steering-workspace.tsx +18063 -0
  133. package/components/ui/badge.tsx +30 -0
  134. package/components/ui/button-group.tsx +87 -0
  135. package/components/ui/button.tsx +58 -0
  136. package/components/ui/card.tsx +32 -0
  137. package/components/ui/command.tsx +194 -0
  138. package/components/ui/dialog.tsx +158 -0
  139. package/components/ui/dropdown-menu.tsx +266 -0
  140. package/components/ui/hover-card.tsx +51 -0
  141. package/components/ui/input-group.tsx +158 -0
  142. package/components/ui/input.tsx +20 -0
  143. package/components/ui/scroll-area.tsx +16 -0
  144. package/components/ui/select.tsx +199 -0
  145. package/components/ui/separator.tsx +23 -0
  146. package/components/ui/sheet.tsx +92 -0
  147. package/components/ui/spinner.tsx +10 -0
  148. package/components/ui/tabs.tsx +113 -0
  149. package/components/ui/textarea.tsx +18 -0
  150. package/components/ui/tooltip.tsx +66 -0
  151. package/components/use-resolved-papyrus-theme.ts +47 -0
  152. package/infra/amplify-app-shell.js +109 -0
  153. package/infra/amplify-app-shell.ts +155 -0
  154. package/infra/index.js +12 -0
  155. package/lib/amplify-outputs-guard.ts +24 -0
  156. package/lib/amplify-outputs-path.ts +16 -0
  157. package/lib/amplify-server-runtime.ts +63 -0
  158. package/lib/archive-data.ts +30 -0
  159. package/lib/archive-types.ts +15 -0
  160. package/lib/articles.ts +320 -0
  161. package/lib/cached-content-repository.ts +89 -0
  162. package/lib/category-dashboard.ts +37 -0
  163. package/lib/category-repository.ts +1907 -0
  164. package/lib/console-chat-client.ts +1316 -0
  165. package/lib/console-web-context.ts +118 -0
  166. package/lib/content-load-timing.ts +23 -0
  167. package/lib/content-repository.ts +40 -0
  168. package/lib/content-types.ts +104 -0
  169. package/lib/define-site.js +3 -0
  170. package/lib/define-site.ts +21 -0
  171. package/lib/demo-amplify-outputs.ts +15 -0
  172. package/lib/dev-sandbox-editor-auth.ts +18 -0
  173. package/lib/doctrine.ts +128 -0
  174. package/lib/edition-routes.ts +155 -0
  175. package/lib/edition-sections.ts +111 -0
  176. package/lib/empty-edition-layout-plan.ts +109 -0
  177. package/lib/excerpts.ts +8 -0
  178. package/lib/graphql-content-repository.ts +986 -0
  179. package/lib/image-url.ts +10 -0
  180. package/lib/layout-plan.ts +981 -0
  181. package/lib/layout-scenarios.ts +564 -0
  182. package/lib/markus-body.ts +168 -0
  183. package/lib/markus-ir.ts +235 -0
  184. package/lib/markus-projection.ts +243 -0
  185. package/lib/markus-to-article.ts +93 -0
  186. package/lib/news-desk-session.ts +160 -0
  187. package/lib/newspaper-layout.ts +3459 -0
  188. package/lib/newsroom-assignments.ts +173 -0
  189. package/lib/newsroom-base-path.ts +48 -0
  190. package/lib/newsroom-card-layout.ts +102 -0
  191. package/lib/newsroom-category-drilldown.ts +133 -0
  192. package/lib/newsroom-demo-dashboard.ts +267 -0
  193. package/lib/newsroom-demo-profile.ts +46 -0
  194. package/lib/newsroom-forum-routes.ts +153 -0
  195. package/lib/newsroom-index-filters.ts +202 -0
  196. package/lib/newsroom-knowledge-query-request.ts +51 -0
  197. package/lib/newsroom-list-selection.ts +42 -0
  198. package/lib/newsroom-nav.ts +42 -0
  199. package/lib/newsroom-references.ts +116 -0
  200. package/lib/newsroom-sections.ts +52 -0
  201. package/lib/newsroom-topics.ts +118 -0
  202. package/lib/papyrus-object-kinds.ts +24 -0
  203. package/lib/papyrus-web-locations.ts +480 -0
  204. package/lib/pilobol-sample.ts +167 -0
  205. package/lib/presentation-footer.ts +92 -0
  206. package/lib/pretext-layout.ts +179 -0
  207. package/lib/public-placeholder-config.ts +102 -0
  208. package/lib/publication-items.ts +181 -0
  209. package/lib/reader-route-config.ts +1 -0
  210. package/lib/reader-static-params.ts +56 -0
  211. package/lib/reader-storage-url.ts +63 -0
  212. package/lib/reference-display.ts +74 -0
  213. package/lib/reference-policy.ts +97 -0
  214. package/lib/reference-source-preview.ts +175 -0
  215. package/lib/renderer-config.ts +50 -0
  216. package/lib/renderer.ts +87 -0
  217. package/lib/reporting-story-budget.ts +638 -0
  218. package/lib/semantic-graph.ts +554 -0
  219. package/lib/site-brand-tenant.ts +41 -0
  220. package/lib/site-brand.ts +179 -0
  221. package/lib/site-renderer.ts +17 -0
  222. package/lib/site-stack.ts +195 -0
  223. package/lib/themed-image.ts +30 -0
  224. package/lib/utils.ts +6 -0
  225. package/middleware.ts +78 -0
  226. package/package.json +296 -1
  227. package/publications/anth_us/brand.ts +68 -0
  228. package/publications/anth_us/theme.css +66 -0
  229. package/publications/papyrus/theme.css +11 -0
  230. package/publications/pilobol_us/brand.ts +37 -0
  231. package/publications/pilobol_us/theme.css +140 -0
  232. package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
  233. package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
  234. package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
  235. package/publications/threat_intelligence/brand.ts +40 -0
  236. package/publications/threat_intelligence/pictograms/art.tsx +608 -0
  237. package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
  238. package/publications/threat_intelligence/pictograms/registry.ts +30 -0
  239. package/publications/threat_intelligence/pictograms/system.tsx +305 -0
  240. package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
  241. package/publications/threat_intelligence/theme.css +1440 -0
  242. package/renderers/markus/index.ts +25 -0
  243. package/renderers/markus/stubs.tsx +33 -0
  244. package/renderers/pretext/article-page.tsx +174 -0
  245. package/renderers/pretext/index.ts +30 -0
  246. package/renderers/pretext/layouts.ts +16 -0
  247. package/renderers/pretext/presentation-shell.tsx +477 -0
  248. package/routes.manifest.json +182 -0
  249. package/scripts/fixtures/pilobol-sample.ast.json +234 -0
  250. package/src/backend.js +1 -0
  251. package/src/backend.ts +3 -0
  252. package/src/sync.mjs +87 -0
  253. package/src/with-papyrus.d.mts +2 -0
  254. package/src/with-papyrus.mjs +47 -0
@@ -0,0 +1,477 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode, RefObject } from "react";
5
+ import { useEffect, useMemo, useRef, useState } from "react";
6
+ import { findEditionSection, getEditionSectionItems } from "../../lib/edition-sections";
7
+ import { getEditionSectionPath } from "../../lib/edition-routes";
8
+ import { buildPresentationFooterEntries, type PresentationFooterEntry } from "../../lib/presentation-footer";
9
+ import { layoutAllTextLines, prepareWithSegments, type TextLine } from "../../lib/pretext-layout";
10
+ import { truncateWords } from "../../lib/excerpts";
11
+ import {
12
+ getPublicationItemImageAssets,
13
+ getPublicationItemVideoAsset,
14
+ type PublicationItem,
15
+ } from "../../lib/publication-items";
16
+ import type { EditionContent, EditionPresentationFormat, EditionSection } from "../../lib/content-types";
17
+ import type { ArticleVideoAsset } from "../../lib/articles";
18
+ import type { PresentationTarget, RenderEditionProps } from "../../lib/renderer";
19
+ import { SITE_BRAND, enforcePresentation, getDefaultPretextLayout } from "../../lib/site-brand";
20
+ import { ArticleVideoFigure } from "../../components/article-video";
21
+ import { BlogPageBackground as ThreatIntelligenceBlogPageBackground } from "../../publications/threat_intelligence/blog-defense/page-background";
22
+ import { BlogPageBackground as GenericBlogPageBackground } from "../../components/blog-page-background";
23
+ import { Newspaper } from "../../components/newspaper";
24
+ import { PictogramFigure as ThreatIntelligencePictogramFigure } from "../../publications/threat_intelligence/pictograms/figure";
25
+ import { PictogramFigure as GenericPictogramFigure } from "../../components/pictogram-figure";
26
+ import { PresentationFooter } from "../../components/presentation-footer";
27
+ import { readLocalReaderSettings, resolveReaderSettings, subscribeReaderSettingsChanges } from "../../components/reader-settings";
28
+
29
+ // Threat Intelligence supplies its own blog background and figure media; any
30
+ // other publication gets the generic fallback (components/). This is the
31
+ // coupling docs/pluggable-publishers.md section 2.6 flags, decoupled here
32
+ // per PPY-d79ff2 by making the choice publication-conditional instead of
33
+ // hardcoded.
34
+ const BlogPageBackground = SITE_BRAND.id === "threat-intelligence" ? ThreatIntelligenceBlogPageBackground : GenericBlogPageBackground;
35
+ const PictogramFigure = SITE_BRAND.id === "threat-intelligence" ? ThreatIntelligencePictogramFigure : GenericPictogramFigure;
36
+
37
+ export type { PresentationTarget };
38
+
39
+ type PresentationShellProps = RenderEditionProps;
40
+
41
+ const PRESENTATION_TEXT_FONT = SITE_BRAND.textFont;
42
+ const BLOG_TEXT_STYLE = {
43
+ fontSize: 18,
44
+ lineHeight: 28,
45
+ linePaintHeight: 24,
46
+ fontFamily: PRESENTATION_TEXT_FONT,
47
+ };
48
+ const MAGAZINE_TEXT_STYLE = {
49
+ fontSize: 17,
50
+ lineHeight: 25,
51
+ linePaintHeight: 22,
52
+ fontFamily: PRESENTATION_TEXT_FONT,
53
+ };
54
+
55
+ export function PresentationShell({
56
+ content,
57
+ editionBasePath,
58
+ mastheadHomeHref,
59
+ initialPageNumber = 1,
60
+ lockedPresentation,
61
+ target = { kind: "edition" },
62
+ }: PresentationShellProps) {
63
+ const defaultPresentation = enforcePresentation(getDefaultPretextLayout());
64
+ const [preferredPresentation, setPreferredPresentation] = useState<EditionPresentationFormat>(defaultPresentation);
65
+ const activePresentation = enforcePresentation(lockedPresentation ?? preferredPresentation);
66
+ const targetSection = target.kind === "section" ? findEditionSection(content.sections, target.sectionKey) : undefined;
67
+
68
+ useEffect(() => {
69
+ if (lockedPresentation) return;
70
+ const localSettings = readLocalReaderSettings();
71
+ setPreferredPresentation(enforcePresentation(localSettings.presentation));
72
+ const unsubscribe = subscribeReaderSettingsChanges((settings) => {
73
+ setPreferredPresentation(enforcePresentation(settings.presentation));
74
+ });
75
+ void resolveReaderSettings()
76
+ .then((resolution) => setPreferredPresentation(enforcePresentation(resolution.settings.presentation)))
77
+ .catch(() => undefined);
78
+ return unsubscribe;
79
+ }, [lockedPresentation]);
80
+
81
+ if (activePresentation === "newsprint") {
82
+ return (
83
+ <PresentationFrame>
84
+ <Newspaper
85
+ content={content}
86
+ editionBasePath={editionBasePath}
87
+ mastheadHomeHref={mastheadHomeHref}
88
+ initialPageNumber={initialPageNumber}
89
+ initialSectionKey={targetSection?.key}
90
+ preserveContentLocation={Boolean(targetSection)}
91
+ />
92
+ </PresentationFrame>
93
+ );
94
+ }
95
+
96
+ const presentationProps = {
97
+ content,
98
+ editionBasePath,
99
+ targetSection,
100
+ };
101
+
102
+ return (
103
+ <PresentationFrame>
104
+ {activePresentation === "blog" ? (
105
+ <BlogPresentation {...presentationProps} />
106
+ ) : (
107
+ <MagazinePresentation {...presentationProps} />
108
+ )}
109
+ </PresentationFrame>
110
+ );
111
+ }
112
+
113
+ function PresentationFrame({ children }: { children: ReactNode }) {
114
+ return <>{children}</>;
115
+ }
116
+
117
+ function BlogPresentation({
118
+ content,
119
+ editionBasePath,
120
+ targetSection,
121
+ }: {
122
+ content: EditionContent;
123
+ editionBasePath?: string;
124
+ targetSection?: EditionSection;
125
+ }) {
126
+ const sections = content.sections;
127
+ const footerEntries = useMemo(() => buildPresentationFooterEntries(content), [content]);
128
+ const footerSubtitle = SITE_BRAND.id === "papyrus" ? (content.description?.trim() || "Inside Papyrus") : SITE_BRAND.mastheadSubtitle;
129
+ const contentRef = useRef<HTMLDivElement | null>(null);
130
+ const pageRef = useRef<HTMLElement | null>(null);
131
+ usePresentationTargetScroll(targetSection);
132
+ return (
133
+ <main className="presentation-page presentation-page--blog" data-presentation-engine="blog" ref={pageRef}>
134
+ <BlogPageBackground pageRef={pageRef} />
135
+ <PresentationHeader content={content} />
136
+ <SectionNavigation content={content} editionBasePath={editionBasePath} />
137
+ <div className="blog-sections" ref={contentRef}>
138
+ {sections.map((section, sectionIndex) => (
139
+ <section className="blog-section" data-edition-section={section.key} id={getSectionAnchorId(section.key)} key={section.key}>
140
+ {sectionIndex === 0 && content.editionVideo ? (
141
+ <EditionOverviewVideo editionVideo={content.editionVideo} />
142
+ ) : null}
143
+ <header className="presentation-section-header">
144
+ <p>{section.label}</p>
145
+ {section.description ? <span>{section.description}</span> : null}
146
+ </header>
147
+ {getEditionSectionItems(section, content.items).map((item, index) => (
148
+ <PresentationItem
149
+ editionBasePath={editionBasePath}
150
+ index={index}
151
+ item={item}
152
+ key={item.slug}
153
+ mode="blog"
154
+ />
155
+ ))}
156
+ </section>
157
+ ))}
158
+ </div>
159
+ <PresentationFooter
160
+ editionBasePath={editionBasePath}
161
+ entries={footerEntries}
162
+ onSectionClick={handleBlogFooterSectionClick}
163
+ resolveSectionHref={(entry) => getBlogFooterSectionHref(entry, editionBasePath)}
164
+ subtitle={SITE_BRAND.footerSubtitleOverride ?? footerSubtitle}
165
+ title={SITE_BRAND.footerTitle}
166
+ />
167
+ </main>
168
+ );
169
+ }
170
+
171
+ function MagazinePresentation({
172
+ content,
173
+ editionBasePath,
174
+ targetSection,
175
+ }: {
176
+ content: EditionContent;
177
+ editionBasePath?: string;
178
+ targetSection?: EditionSection;
179
+ }) {
180
+ const sections = targetSection ? [targetSection] : content.sections;
181
+ usePresentationTargetScroll(targetSection);
182
+ return (
183
+ <main className="presentation-page presentation-page--magazine" data-presentation-engine="magazine">
184
+ <PresentationHeader content={content} />
185
+ <SectionNavigation content={content} editionBasePath={editionBasePath} />
186
+ <div className="magazine-sections">
187
+ {sections.map((section) => {
188
+ const items = getEditionSectionItems(section, content.items);
189
+ return (
190
+ <section className="magazine-section" data-edition-section={section.key} id={getSectionAnchorId(section.key)} key={section.key}>
191
+ <header className="presentation-section-header presentation-section-header--magazine">
192
+ <p>{section.label}</p>
193
+ {section.description ? <span>{section.description}</span> : null}
194
+ </header>
195
+ <div className="magazine-spread">
196
+ {items.map((item, index) => (
197
+ <PresentationItem
198
+ editionBasePath={editionBasePath}
199
+ item={item}
200
+ key={item.slug}
201
+ mode={index === 0 ? "magazine-feature" : "magazine"}
202
+ />
203
+ ))}
204
+ </div>
205
+ </section>
206
+ );
207
+ })}
208
+ </div>
209
+ </main>
210
+ );
211
+ }
212
+
213
+ function EditionOverviewVideo({ editionVideo }: { editionVideo: ArticleVideoAsset }) {
214
+ return (
215
+ <section className="presentation-edition-video" aria-label={editionVideo.alt}>
216
+ <ArticleVideoFigure
217
+ figureClassName="presentation-edition-video__figure article-video"
218
+ slug="edition-overview"
219
+ video={editionVideo}
220
+ />
221
+ </section>
222
+ );
223
+ }
224
+
225
+ function PresentationHeader({ content }: { content: EditionContent }) {
226
+ const title = SITE_BRAND.mastheadSource === "brand" ? SITE_BRAND.mastheadTitle : content.title;
227
+ const subtitle = SITE_BRAND.mastheadSource === "brand" ? SITE_BRAND.mastheadSubtitle : content.description;
228
+ const tagline = SITE_BRAND.mastheadTagline ?? null;
229
+ const displayDate = SITE_BRAND.mastheadDateFormat === "formatted"
230
+ ? formatMastheadDate(content.editionDate)
231
+ : content.editionDate;
232
+
233
+ return (
234
+ <header className="presentation-header">
235
+ <div className="presentation-header__copy-stack">
236
+ <h1>
237
+ {SITE_BRAND.mastheadWordSplit
238
+ ? title.split(/\s+/).map((word) => <span key={word}>{word}</span>)
239
+ : title}
240
+ </h1>
241
+ <div className="presentation-header__meta">
242
+ {subtitle ? <span className="presentation-header__subtitle">{subtitle}</span> : null}
243
+ <p className="presentation-header__date">{displayDate}</p>
244
+ </div>
245
+ {tagline ? <span className="presentation-header__tagline">{tagline}</span> : null}
246
+ </div>
247
+ </header>
248
+ );
249
+ }
250
+
251
+ function SectionNavigation({ content, editionBasePath }: { content: EditionContent; editionBasePath?: string }) {
252
+ return (
253
+ <nav className="presentation-section-nav" aria-label="Edition sections">
254
+ {content.sections.map((section) => (
255
+ <Link href={getSectionHref(content, section, editionBasePath)} key={section.key}>
256
+ {section.label}
257
+ </Link>
258
+ ))}
259
+ </nav>
260
+ );
261
+ }
262
+
263
+ function PresentationItem({
264
+ editionBasePath,
265
+ index,
266
+ item,
267
+ mode,
268
+ }: {
269
+ editionBasePath?: string;
270
+ index?: number;
271
+ item: PublicationItem;
272
+ mode: "blog" | "magazine" | "magazine-feature";
273
+ }) {
274
+ const frameRef = useRef<HTMLDivElement | null>(null);
275
+ const maxWidth = useMeasuredWidth(frameRef);
276
+ const image = getPublicationItemImageAssets(item)[0];
277
+ const video = getPublicationItemVideoAsset(item);
278
+ const textStyle = mode === "blog" ? BLOG_TEXT_STYLE : MAGAZINE_TEXT_STYLE;
279
+ const text = getPresentationBodyText(item, mode);
280
+ const itemRole = getPresentationItemRole(mode, index);
281
+ const lines = useMemo(() => {
282
+ if (!maxWidth || !text.trim()) return [];
283
+ return layoutAllTextLines({
284
+ prepared: prepareWithSegments(text, `${textStyle.fontSize}px ${textStyle.fontFamily}`),
285
+ maxWidth,
286
+ ...textStyle,
287
+ });
288
+ }, [maxWidth, text, textStyle]);
289
+ const textHeight = getMeasuredTextHeight(lines);
290
+ const directHref = editionBasePath ? `${editionBasePath}/${encodeURIComponent(item.slug)}` : `/articles/${encodeURIComponent(item.slug)}`;
291
+
292
+ return (
293
+ <article
294
+ className={`presentation-item presentation-item--${mode}`}
295
+ data-item-id={item.slug}
296
+ data-item-index={index}
297
+ data-item-role={itemRole}
298
+ data-item-type={item.type}
299
+ data-has-image={image ? "true" : "false"}
300
+ data-has-video={video ? "true" : "false"}
301
+ id={item.slug}
302
+ >
303
+ <div className="presentation-item__copy">
304
+ <header className="presentation-item__header">
305
+ <p>{item.section ?? "General"}</p>
306
+ <h2>
307
+ <Link href={directHref}>{getPresentationTitle(item)}</Link>
308
+ </h2>
309
+ {item.deck ? <span>{item.deck}</span> : null}
310
+ </header>
311
+ <div className="presentation-item__text-frame" ref={frameRef} style={{ height: textHeight }}>
312
+ <MeasuredPresentationLines lines={lines} />
313
+ </div>
314
+ {mode === "blog" ? (
315
+ <Link className="presentation-item__cta" href={directHref}>
316
+ Read Article
317
+ </Link>
318
+ ) : null}
319
+ </div>
320
+ {image ? (
321
+ <div className="presentation-item__media">
322
+ <PictogramFigure
323
+ alt={image.alt}
324
+ caption={image.caption}
325
+ credit={image.credit}
326
+ figureClassName="presentation-item__image"
327
+ height={760}
328
+ layout={image.layout}
329
+ sizes={mode === "blog" ? "(max-width: 900px) 100vw, 760px" : "(max-width: 900px) 100vw, 50vw"}
330
+ slug={item.slug}
331
+ src={image.src}
332
+ themeVariants={image.themeVariants}
333
+ width={1200}
334
+ />
335
+ </div>
336
+ ) : null}
337
+ {video && mode === "blog" ? (
338
+ <div className="presentation-item__video">
339
+ <ArticleVideoFigure
340
+ figureClassName="presentation-item__video-figure article-video"
341
+ slug={item.slug}
342
+ video={video}
343
+ />
344
+ </div>
345
+ ) : null}
346
+ </article>
347
+ );
348
+ }
349
+
350
+ function getPresentationItemRole(mode: "blog" | "magazine" | "magazine-feature", index?: number): "lead" | "secondary" {
351
+ if (mode === "magazine-feature") return "lead";
352
+ if (mode === "blog" && index === 0) return "lead";
353
+ return "secondary";
354
+ }
355
+
356
+ function MeasuredPresentationLines({ lines }: { lines: TextLine[] }) {
357
+ return (
358
+ <div className="presentation-measured-lines">
359
+ {lines.map((line, index) => (
360
+ <span
361
+ className="presentation-measured-line"
362
+ key={`${index}-${line.text}`}
363
+ style={{
364
+ "--line-font-family": line.fontFamily,
365
+ "--line-font-size": `${line.fontSize}px`,
366
+ "--line-height": `${line.lineHeight}px`,
367
+ "--line-paint-height": `${line.paintHeight}px`,
368
+ left: line.x,
369
+ top: line.y,
370
+ } as CSSProperties}
371
+ >
372
+ {line.text}
373
+ </span>
374
+ ))}
375
+ </div>
376
+ );
377
+ }
378
+
379
+ function useMeasuredWidth(ref: RefObject<HTMLElement | null>): number {
380
+ const [width, setWidth] = useState(0);
381
+ useEffect(() => {
382
+ const node = ref.current;
383
+ if (!node) return;
384
+ const update = () => setWidth(Math.max(1, Math.floor(node.getBoundingClientRect().width)));
385
+ update();
386
+ const observer = new ResizeObserver(update);
387
+ observer.observe(node);
388
+ window.addEventListener("resize", update);
389
+ return () => {
390
+ observer.disconnect();
391
+ window.removeEventListener("resize", update);
392
+ };
393
+ }, [ref]);
394
+ return width;
395
+ }
396
+
397
+ function usePresentationTargetScroll(targetSection: EditionSection | undefined) {
398
+ useEffect(() => {
399
+ const scrollToCurrentTarget = () => {
400
+ const hashTarget = parseItemAnchorHash(window.location.hash);
401
+ const targetId = hashTarget ?? (targetSection ? getSectionAnchorId(targetSection.key) : null);
402
+ if (!targetId) return;
403
+ document.getElementById(targetId)?.scrollIntoView({ block: "start" });
404
+ };
405
+ requestAnimationFrame(scrollToCurrentTarget);
406
+ window.addEventListener("hashchange", scrollToCurrentTarget);
407
+ return () => window.removeEventListener("hashchange", scrollToCurrentTarget);
408
+ }, [targetSection]);
409
+ }
410
+
411
+ function getSectionHref(content: EditionContent, section: EditionSection, editionBasePath?: string): string {
412
+ if (SITE_BRAND.sectionLinkStrategy === "anchor") {
413
+ const anchor = `#${getSectionAnchorId(section.key)}`;
414
+ return editionBasePath ? `${editionBasePath}${anchor}` : anchor;
415
+ }
416
+ if (editionBasePath) return `${editionBasePath}/section/${encodeURIComponent(section.key)}`;
417
+ const datedPath = getEditionSectionPath(content.editionDate, section.key);
418
+ return datedPath.startsWith("//") ? `#${getSectionAnchorId(section.key)}` : datedPath;
419
+ }
420
+
421
+ function getSectionAnchorId(sectionKey: string): string {
422
+ return `section-${sectionKey}`;
423
+ }
424
+
425
+ function getMeasuredTextHeight(lines: TextLine[]): number {
426
+ const last = lines[lines.length - 1];
427
+ return last ? last.y + last.paintHeight : 0;
428
+ }
429
+
430
+ function getPresentationTitle(item: PublicationItem): string {
431
+ return item.type === "article" ? item.headline : item.title;
432
+ }
433
+
434
+ function getPresentationBodyText(item: PublicationItem, mode: "blog" | "magazine" | "magazine-feature"): string {
435
+ const body = item.type === "article" ? item.body.join("\n\n") : (item.body ?? []).join("\n\n");
436
+ if (mode !== "blog") return body;
437
+ const excerpt = String(item.excerpt ?? "").trim();
438
+ if (excerpt) return excerpt;
439
+ return truncateWords(body, 80);
440
+ }
441
+
442
+ function parseItemAnchorHash(hash: string): string | null {
443
+ if (!hash) return null;
444
+ try {
445
+ const value = decodeURIComponent(hash.slice(1)).trim();
446
+ return /^[a-z0-9][a-z0-9-]*$/i.test(value) ? value : null;
447
+ } catch {
448
+ return null;
449
+ }
450
+ }
451
+
452
+ function formatMastheadDate(value: string): string {
453
+ const normalized = value?.trim();
454
+ if (!normalized) return value;
455
+ const date = new Date(`${normalized}T00:00:00`);
456
+ if (Number.isNaN(date.getTime())) return value;
457
+ return new Intl.DateTimeFormat("en-US", {
458
+ month: "long",
459
+ day: "numeric",
460
+ year: "numeric",
461
+ }).format(date);
462
+ }
463
+
464
+ function getBlogFooterSectionHref(entry: PresentationFooterEntry, editionBasePath?: string): string {
465
+ const anchor = `#${getSectionAnchorId(entry.sectionKey)}`;
466
+ return editionBasePath ? `${editionBasePath}${anchor}` : anchor;
467
+ }
468
+
469
+ function handleBlogFooterSectionClick(
470
+ event: ReactMouseEvent<HTMLAnchorElement>,
471
+ entry: PresentationFooterEntry,
472
+ href: string,
473
+ ) {
474
+ event.preventDefault();
475
+ window.history.pushState(null, "", href);
476
+ document.getElementById(getSectionAnchorId(entry.sectionKey))?.scrollIntoView({ block: "start" });
477
+ }
@@ -0,0 +1,182 @@
1
+ {
2
+ "version": "1.0.0-next.2",
3
+ "routes": [
4
+ {
5
+ "file": "app/[year]/[month]/[day]/[articleSlug]/page.tsx",
6
+ "hasDefault": true,
7
+ "exports": [
8
+ "generateMetadata"
9
+ ],
10
+ "config": {
11
+ "revalidate": "3600"
12
+ }
13
+ },
14
+ {
15
+ "file": "app/[year]/[month]/[day]/page.tsx",
16
+ "hasDefault": true,
17
+ "exports": [
18
+ "generateStaticParams"
19
+ ],
20
+ "config": {
21
+ "revalidate": "3600"
22
+ }
23
+ },
24
+ {
25
+ "file": "app/[year]/[month]/[day]/page/[pageNumber]/page.tsx",
26
+ "hasDefault": true,
27
+ "exports": [],
28
+ "config": {
29
+ "revalidate": "3600"
30
+ }
31
+ },
32
+ {
33
+ "file": "app/[year]/[month]/[day]/section/[sectionKey]/page.tsx",
34
+ "hasDefault": true,
35
+ "exports": [],
36
+ "config": {
37
+ "revalidate": "3600"
38
+ }
39
+ },
40
+ {
41
+ "file": "app/api/archive/editions/route.ts",
42
+ "hasDefault": false,
43
+ "exports": [
44
+ "GET"
45
+ ],
46
+ "config": {
47
+ "revalidate": "3600"
48
+ }
49
+ },
50
+ {
51
+ "file": "app/api/media/[...path]/route.ts",
52
+ "hasDefault": false,
53
+ "exports": [
54
+ "GET"
55
+ ],
56
+ "config": {
57
+ "dynamic": "\"force-dynamic\""
58
+ }
59
+ },
60
+ {
61
+ "file": "app/api/revalidate/route.ts",
62
+ "hasDefault": false,
63
+ "exports": [
64
+ "POST"
65
+ ],
66
+ "config": {
67
+ "dynamic": "\"force-dynamic\""
68
+ }
69
+ },
70
+ {
71
+ "file": "app/archive/page.tsx",
72
+ "hasDefault": true,
73
+ "exports": [],
74
+ "config": {
75
+ "revalidate": "3600"
76
+ }
77
+ },
78
+ {
79
+ "file": "app/articles/[slug]/page.tsx",
80
+ "hasDefault": true,
81
+ "exports": [
82
+ "generateMetadata",
83
+ "generateStaticParams"
84
+ ],
85
+ "config": {
86
+ "revalidate": "3600"
87
+ }
88
+ },
89
+ {
90
+ "file": "app/layout.tsx",
91
+ "hasDefault": true,
92
+ "exports": [
93
+ "generateMetadata"
94
+ ],
95
+ "config": {}
96
+ },
97
+ {
98
+ "file": "app/newsroom/[section]/[...selection]/page.tsx",
99
+ "hasDefault": true,
100
+ "exports": [],
101
+ "config": {
102
+ "dynamic": "\"force-dynamic\""
103
+ }
104
+ },
105
+ {
106
+ "file": "app/newsroom/[section]/page.tsx",
107
+ "hasDefault": true,
108
+ "exports": [],
109
+ "config": {
110
+ "dynamic": "\"force-dynamic\""
111
+ }
112
+ },
113
+ {
114
+ "file": "app/newsroom/forum/[threadId]/page.tsx",
115
+ "hasDefault": true,
116
+ "exports": [],
117
+ "config": {
118
+ "dynamic": "\"force-dynamic\""
119
+ }
120
+ },
121
+ {
122
+ "file": "app/newsroom/insights/[threadId]/page.tsx",
123
+ "hasDefault": true,
124
+ "exports": [],
125
+ "config": {
126
+ "dynamic": "\"force-dynamic\""
127
+ }
128
+ },
129
+ {
130
+ "file": "app/newsroom/insights/page.tsx",
131
+ "hasDefault": true,
132
+ "exports": [],
133
+ "config": {
134
+ "dynamic": "\"force-dynamic\""
135
+ }
136
+ },
137
+ {
138
+ "file": "app/newsroom/layout.tsx",
139
+ "hasDefault": true,
140
+ "exports": [],
141
+ "config": {}
142
+ },
143
+ {
144
+ "file": "app/newsroom/page.tsx",
145
+ "hasDefault": true,
146
+ "exports": [],
147
+ "config": {
148
+ "dynamic": "\"force-dynamic\""
149
+ }
150
+ },
151
+ {
152
+ "file": "app/newsroom/sections/[sectionId]/page.tsx",
153
+ "hasDefault": true,
154
+ "exports": [],
155
+ "config": {
156
+ "dynamic": "\"force-dynamic\""
157
+ }
158
+ },
159
+ {
160
+ "file": "app/page.tsx",
161
+ "hasDefault": true,
162
+ "exports": [],
163
+ "config": {
164
+ "dynamic": "\"force-dynamic\""
165
+ }
166
+ },
167
+ {
168
+ "file": "app/settings/page.tsx",
169
+ "hasDefault": true,
170
+ "exports": [
171
+ "metadata"
172
+ ],
173
+ "config": {
174
+ "dynamic": "\"force-dynamic\""
175
+ }
176
+ }
177
+ ],
178
+ "middleware": {
179
+ "file": "middleware.ts",
180
+ "matcher": "[\"/((?!_next/static|_next/image|favicon.ico|icon).*)\"]"
181
+ }
182
+ }