@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.
- package/amplify/auth/prospectus-google-client.js +66 -0
- package/amplify/auth/prospectus-google-client.ts +71 -0
- package/amplify/auth/redirect-urls.js +54 -0
- package/amplify/auth/redirect-urls.ts +66 -0
- package/amplify/auth/resource.js +23 -5
- package/amplify/auth/resource.ts +31 -5
- package/amplify/data/resource.js +42 -80
- package/amplify/data/resource.ts +42 -80
- package/amplify/data/schema.js +45 -62
- package/amplify/data/schema.ts +47 -62
- package/amplify/functions/category-action/handler.ts +5 -3
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
- package/amplify/functions/console-chat-responder/resource.js +0 -7
- package/amplify/functions/console-chat-responder/resource.ts +0 -8
- package/amplify/functions/console-chat-responder/src/main.rs +0 -9
- package/amplify/functions/content-actions/handler.py +143 -1
- package/amplify/functions/content-actions/requirements.txt +2 -1
- package/amplify/functions/content-actions/resource.js +6 -1
- package/amplify/functions/content-actions/resource.ts +9 -2
- package/amplify/functions/email-submission-processor/handler.py +3 -1
- package/amplify/functions/renditions/handler.py +120 -0
- package/amplify/functions/renditions/requirements.txt +4 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +32 -4
- package/amplify/functions/shared/email-submission.ts +109 -1
- package/amplify/functions/shared/python-bundle.js +42 -2
- package/amplify/functions/shared/python-bundle.ts +47 -2
- package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
- package/amplify/inbound-email/ses-intake.js +86 -0
- package/amplify/inbound-email/ses-intake.ts +110 -0
- package/amplify/plugins/registry.js +19 -0
- package/amplify/plugins/registry.ts +23 -0
- package/amplify/plugins/signup/function/handler.py +200 -0
- package/amplify/plugins/signup/function/resource.js +27 -0
- package/amplify/plugins/signup/function/resource.ts +35 -0
- package/amplify/plugins/signup/papyrus.plugin.js +59 -0
- package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
- package/amplify/plugins/signup/schema.js +40 -0
- package/amplify/plugins/signup/schema.ts +41 -0
- package/amplify/plugins/signup/waf.js +48 -0
- package/amplify/plugins/signup/waf.ts +51 -0
- package/amplify/plugins/types.js +1 -0
- package/amplify/plugins/types.ts +39 -0
- package/amplify/plugins/wire.js +24 -0
- package/amplify/plugins/wire.ts +26 -0
- package/amplify/publication/default-publication.js +18 -0
- package/amplify/publication/default-publication.ts +20 -0
- package/amplify/publication/invalidation-alarm.js +26 -0
- package/amplify/publication/invalidation-alarm.ts +30 -0
- package/amplify/publication/redirect-and-index.js +37 -0
- package/amplify/publication/renditions-function.js +74 -0
- package/amplify/publication/renditions-function.ts +112 -0
- package/amplify/publication/renditions-settings.js +51 -0
- package/amplify/publication/renditions-settings.ts +58 -0
- package/amplify/publication/reproject-on-deploy.js +73 -0
- package/amplify/publication/reproject-on-deploy.ts +78 -0
- package/amplify/publication/s3-projection-settings.js +67 -0
- package/amplify/publication/s3-projection-settings.ts +101 -0
- package/amplify/publication/s3-projection.js +151 -0
- package/amplify/publication/s3-projection.ts +201 -0
- package/amplify/resolver-waves.js +26 -0
- package/amplify/resolver-waves.ts +34 -0
- package/amplify/revalidate-invalidation-settings.js +30 -0
- package/amplify/revalidate-invalidation-settings.ts +35 -0
- package/amplify/revalidate-secret-policy.js +13 -0
- package/amplify/revalidate-secret-policy.ts +19 -0
- package/amplify/site-backend-config.js +65 -1
- package/amplify/site-backend-config.ts +153 -2
- package/amplify/site-backend-names.js +103 -0
- package/amplify/site-backend-names.ts +166 -0
- package/amplify/site-backend.js +150 -176
- package/amplify/site-backend.ts +189 -201
- package/amplify/storage-backups/construct.js +31 -0
- package/amplify/storage-backups/construct.ts +47 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
- package/app/[year]/[month]/[day]/page.tsx +1 -1
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
- package/app/api/archive/editions/route.ts +1 -1
- package/app/api/auth/prospectus/callback/route.ts +9 -0
- package/app/api/auth/prospectus/selection/route.ts +9 -0
- package/app/api/auth/prospectus/session/route.ts +9 -0
- package/app/api/auth/prospectus/sign-in/route.ts +9 -0
- package/app/api/auth/prospectus/sign-out/route.ts +9 -0
- package/app/api/cyclotron/priming-proof/route.ts +11 -0
- package/app/api/media/[...path]/route.ts +5 -16
- package/app/api/revalidate/route.ts +15 -7
- package/app/archive/page.tsx +1 -1
- package/app/articles/[slug]/page.tsx +7 -5
- package/app/blog-rhythm.css +278 -0
- package/app/cyclotron/page.tsx +13 -0
- package/app/globals.css +2 -0
- package/app/layout.tsx +16 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
- package/app/newsroom/[section]/page.tsx +6 -3
- package/app/newsroom/layout.tsx +4 -9
- package/app/newsroom/settings/page.tsx +6 -0
- package/app/newsroom/work/page.tsx +10 -0
- package/app/page.tsx +22 -8
- package/app/robots.ts +2 -5
- package/app/tailwind.css +1 -6
- package/components/amplify-client-provider.tsx +5 -22
- package/components/archive-shell.tsx +2 -22
- package/components/article-video.tsx +152 -5
- package/components/blog-page-background.tsx +23 -6
- package/components/news-desk-page.tsx +4 -15
- package/components/news-desk-taxonomy-client.ts +90 -3
- package/components/newspaper.tsx +6 -2
- package/components/newsroom-access-gate.tsx +2 -1
- package/components/newsroom-board-view.tsx +99 -0
- package/components/newsroom-client-shell.tsx +12 -0
- package/components/newsroom-curation-provider.tsx +17 -0
- package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
- package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
- package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
- package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
- package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
- package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
- package/components/newsroom-newsprint-board.css +48 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +7 -1
- package/components/newsroom-organizer-shell.tsx +130 -0
- package/components/newsroom-organizer.css +63 -0
- package/components/newsroom-references-view.tsx +92 -8
- package/components/papyrus-console-shell.tsx +4 -2
- package/components/pictogram-figure.tsx +9 -2
- package/components/presentation-header.tsx +135 -0
- package/components/presentation-rhythm-hrule.tsx +17 -0
- package/components/priming-proof-page.tsx +94 -0
- package/components/publication-cyclotron-panel.tsx +57 -0
- package/components/reader-auth-control.tsx +8 -5
- package/components/reference-curation-controls.tsx +77 -0
- package/components/reference-cyclotron-status.tsx +109 -0
- package/components/reference-priming.tsx +18 -0
- package/components/reference-relevance-review.tsx +75 -0
- package/components/settings-page.tsx +2 -1
- package/components/signup-entries-panel.tsx +94 -0
- package/components/topic-steering-workspace.tsx +78 -8
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +28 -0
- package/components/use-near-viewport.ts +30 -0
- package/components/use-rhythm-overlay.ts +29 -0
- package/infra/amplify-app-shell.js +166 -27
- package/infra/amplify-app-shell.ts +180 -31
- package/infra/build-specs.js +23 -7
- package/infra/build-specs.ts +27 -10
- package/infra/site-config.js +248 -36
- package/infra/site-config.ts +313 -51
- package/lib/amplify-outputs-path.ts +5 -1
- package/lib/amplify-server-runtime.ts +36 -1
- package/lib/approved-priming-proof-reports.ts +6 -0
- package/lib/blog-feature-solver.ts +459 -0
- package/lib/blog-rhythm.ts +134 -0
- package/lib/brand-fonts-watch.ts +43 -0
- package/lib/brand-fonts.ts +112 -0
- package/lib/cached-content-repository.ts +12 -0
- package/lib/category-repository.ts +43 -93
- package/lib/content-item-lifecycle.ts +55 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-types.ts +4 -0
- package/lib/curation-client.ts +61 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/graphql-content-repository.ts +56 -22
- package/lib/media-route-response.ts +45 -0
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-base-path.ts +12 -3
- package/lib/newsroom-board.ts +99 -0
- package/lib/newsroom-category-drilldown.ts +3 -2
- package/lib/newsroom-forum-routes.ts +6 -4
- package/lib/newsroom-index-filters.ts +14 -10
- package/lib/newsroom-nav.ts +3 -1
- package/lib/newsroom-organizer-nav.ts +37 -0
- package/lib/newsroom-section-routing.ts +20 -0
- package/lib/oauth-redirect-priority.ts +40 -0
- package/lib/papyrus-web-locations.ts +7 -6
- package/lib/plugin-settings.ts +11 -0
- package/lib/presentation-footer.ts +3 -2
- package/lib/pretext-layout.ts +12 -3
- package/lib/priming-proof-report.ts +90 -0
- package/lib/prospectus-publication-access.ts +53 -0
- package/lib/prospectus-session.ts +196 -0
- package/lib/prospectus-sign-in-routes.ts +284 -0
- package/lib/prospectus-site-identity.ts +303 -0
- package/lib/publication-cyclotron.ts +74 -0
- package/lib/rate-limit-retry.ts +55 -0
- package/lib/reader-base-path.ts +24 -0
- package/lib/reader-cache-lifetime.ts +5 -0
- package/lib/reader-static-params.ts +85 -4
- package/lib/reader-storage-url.ts +9 -12
- package/lib/reference-policy.ts +5 -0
- package/lib/relevance-decisions.ts +103 -0
- package/lib/relevance-status.ts +63 -0
- package/lib/renderer.ts +7 -0
- package/lib/revalidate-prewarm.ts +53 -0
- package/lib/revalidate-secret.ts +59 -0
- package/lib/robots-policy.ts +14 -0
- package/lib/root-route-redirect.ts +16 -0
- package/lib/semantic-graph.ts +7 -0
- package/lib/shared-credentials-provider.ts +61 -0
- package/lib/signup-csv.ts +24 -0
- package/lib/site-brand.ts +59 -0
- package/lib/staging-gated-path.ts +23 -0
- package/lib/staging-preview-object.ts +21 -5
- package/lib/test-editor-override.ts +4 -0
- package/lib/video-loading.ts +22 -0
- package/lib/video-script.ts +14 -0
- package/middleware.ts +11 -19
- package/package.json +73 -8
- package/renderers/pretext/article-page.tsx +39 -3
- package/renderers/pretext/presentation-shared.tsx +113 -0
- package/renderers/pretext/presentation-shell.tsx +15 -102
- package/renderers/pretext/rhythm-article-page.tsx +165 -0
- package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
- package/renderers/pretext/rhythm-shell-style.ts +35 -0
- package/routes.manifest.json +123 -12
- package/src/sync.mjs +30 -6
- package/src/with-papyrus.d.mts +9 -0
- package/src/with-papyrus.mjs +48 -3
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
- package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
- package/components/ai-elements/message.tsx +0 -360
|
@@ -9,6 +9,7 @@ import { SITE_BRAND } from "../../lib/site-brand";
|
|
|
9
9
|
import { ArticleVideoFigure } from "../../components/article-video";
|
|
10
10
|
import { PictogramFigure as GenericPictogramFigure } from "../../components/pictogram-figure";
|
|
11
11
|
import { PresentationFooter } from "../../components/presentation-footer";
|
|
12
|
+
import { RhythmArticlePage } from "./rhythm-article-page";
|
|
12
13
|
|
|
13
14
|
const PictogramFigure = SITE_BRAND.components?.PictogramFigure ?? GenericPictogramFigure;
|
|
14
15
|
|
|
@@ -16,7 +17,24 @@ export type { ArticlePageEditionFooter };
|
|
|
16
17
|
|
|
17
18
|
type ArticlePageViewProps = RenderArticleProps;
|
|
18
19
|
|
|
19
|
-
export function ArticlePageView({ article, backHref, backLabel = SITE_BRAND.backToHomeLabel, editionFooter, editionDate }: ArticlePageViewProps) {
|
|
20
|
+
export function ArticlePageView({ article, backHref, backLabel = SITE_BRAND.backToHomeLabel, editionFooter, editionDate, videoScript = null }: ArticlePageViewProps) {
|
|
21
|
+
if (SITE_BRAND.blogLayout === "rhythm") {
|
|
22
|
+
return (
|
|
23
|
+
<RhythmArticlePage
|
|
24
|
+
body={article.body}
|
|
25
|
+
deck={article.deck}
|
|
26
|
+
editionDate={editionDate}
|
|
27
|
+
editionFooter={editionFooter}
|
|
28
|
+
headline={article.headline}
|
|
29
|
+
image={article.image ?? null}
|
|
30
|
+
section={article.section}
|
|
31
|
+
slug={article.slug}
|
|
32
|
+
video={article.video ?? null}
|
|
33
|
+
videoScript={videoScript}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
20
38
|
const articleDate = editionDate ? formatArticleDate(editionDate) : null;
|
|
21
39
|
|
|
22
40
|
return (
|
|
@@ -55,9 +73,27 @@ export function ArticlePageView({ article, backHref, backLabel = SITE_BRAND.back
|
|
|
55
73
|
|
|
56
74
|
type ItemPageViewProps = RenderItemProps;
|
|
57
75
|
|
|
58
|
-
export function ItemPageView({ item, backHref, backLabel = "Back to edition", editionFooter, editionDate }: ItemPageViewProps) {
|
|
76
|
+
export function ItemPageView({ item, backHref, backLabel = "Back to edition", editionFooter, editionDate, videoScript = null }: ItemPageViewProps) {
|
|
59
77
|
if (item.type === "article") {
|
|
60
|
-
return <ArticlePageView article={item} backHref={backHref} backLabel={backLabel} editionFooter={editionFooter} editionDate={editionDate} />;
|
|
78
|
+
return <ArticlePageView article={item} backHref={backHref} backLabel={backLabel} editionFooter={editionFooter} editionDate={editionDate} videoScript={videoScript} />;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (SITE_BRAND.blogLayout === "rhythm") {
|
|
82
|
+
return (
|
|
83
|
+
<RhythmArticlePage
|
|
84
|
+
body={item.body ?? []}
|
|
85
|
+
deck={item.deck}
|
|
86
|
+
editionDate={editionDate}
|
|
87
|
+
editionFooter={editionFooter}
|
|
88
|
+
headline={item.title}
|
|
89
|
+
image={item.image ?? null}
|
|
90
|
+
itemType={item.type}
|
|
91
|
+
section={item.section ?? item.type}
|
|
92
|
+
slug={item.slug}
|
|
93
|
+
video={getPublicationItemVideoAsset(item) ?? null}
|
|
94
|
+
videoScript={videoScript}
|
|
95
|
+
/>
|
|
96
|
+
);
|
|
61
97
|
}
|
|
62
98
|
|
|
63
99
|
const itemDate = editionDate ? formatArticleDate(editionDate) : null;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties, MouseEvent as ReactMouseEvent, RefObject } from "react";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import type { EditionSection } from "../../lib/content-types";
|
|
6
|
+
import { truncateWords } from "../../lib/excerpts";
|
|
7
|
+
import type { PresentationFooterEntry } from "../../lib/presentation-footer";
|
|
8
|
+
import type { PublicationItem } from "../../lib/publication-items";
|
|
9
|
+
import type { TextLine } from "../../lib/pretext-layout";
|
|
10
|
+
|
|
11
|
+
export type PresentationItemMode = "blog" | "magazine" | "magazine-feature";
|
|
12
|
+
|
|
13
|
+
export function MeasuredPresentationLines({ lines }: { lines: TextLine[] }) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="presentation-measured-lines">
|
|
16
|
+
{lines.map((line, index) => (
|
|
17
|
+
<span
|
|
18
|
+
className="presentation-measured-line"
|
|
19
|
+
key={`${index}-${line.text}`}
|
|
20
|
+
style={{
|
|
21
|
+
"--line-font-family": line.fontFamily,
|
|
22
|
+
"--line-font-size": `${line.fontSize}px`,
|
|
23
|
+
"--line-height": `${line.lineHeight}px`,
|
|
24
|
+
"--line-paint-height": `${line.paintHeight}px`,
|
|
25
|
+
left: line.x,
|
|
26
|
+
top: line.y,
|
|
27
|
+
} as CSSProperties}
|
|
28
|
+
>
|
|
29
|
+
{line.text}
|
|
30
|
+
</span>
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useMeasuredWidth(ref: RefObject<HTMLElement | null>): number {
|
|
37
|
+
const [width, setWidth] = useState(0);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const node = ref.current;
|
|
40
|
+
if (!node) return;
|
|
41
|
+
const update = () => setWidth(Math.max(1, Math.floor(node.getBoundingClientRect().width)));
|
|
42
|
+
update();
|
|
43
|
+
const observer = new ResizeObserver(update);
|
|
44
|
+
observer.observe(node);
|
|
45
|
+
window.addEventListener("resize", update);
|
|
46
|
+
return () => {
|
|
47
|
+
observer.disconnect();
|
|
48
|
+
window.removeEventListener("resize", update);
|
|
49
|
+
};
|
|
50
|
+
}, [ref]);
|
|
51
|
+
return width;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function usePresentationTargetScroll(targetSection: EditionSection | undefined) {
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const scrollToCurrentTarget = () => {
|
|
57
|
+
const hashTarget = parseItemAnchorHash(window.location.hash);
|
|
58
|
+
const targetId = hashTarget ?? (targetSection ? getSectionAnchorId(targetSection.key) : null);
|
|
59
|
+
if (!targetId) return;
|
|
60
|
+
document.getElementById(targetId)?.scrollIntoView({ block: "start" });
|
|
61
|
+
};
|
|
62
|
+
requestAnimationFrame(scrollToCurrentTarget);
|
|
63
|
+
window.addEventListener("hashchange", scrollToCurrentTarget);
|
|
64
|
+
return () => window.removeEventListener("hashchange", scrollToCurrentTarget);
|
|
65
|
+
}, [targetSection]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getSectionAnchorId(sectionKey: string): string {
|
|
69
|
+
return `section-${sectionKey}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getPresentationTitle(item: PublicationItem): string {
|
|
73
|
+
return item.type === "article" ? item.headline : item.title;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getPresentationBodyText(item: PublicationItem, mode: PresentationItemMode): string {
|
|
77
|
+
const body = item.type === "article" ? item.body.join("\n\n") : (item.body ?? []).join("\n\n");
|
|
78
|
+
if (mode !== "blog") return body;
|
|
79
|
+
const excerpt = String(item.excerpt ?? "").trim();
|
|
80
|
+
if (excerpt) return excerpt;
|
|
81
|
+
return truncateWords(body, 80);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function parseItemAnchorHash(hash: string): string | null {
|
|
85
|
+
if (!hash) return null;
|
|
86
|
+
try {
|
|
87
|
+
const value = decodeURIComponent(hash.slice(1)).trim();
|
|
88
|
+
return /^[a-z0-9][a-z0-9-]*$/i.test(value) ? value : null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getBlogFooterSectionHref(entry: PresentationFooterEntry, editionBasePath?: string): string {
|
|
95
|
+
const anchor = `#${getSectionAnchorId(entry.sectionKey)}`;
|
|
96
|
+
return editionBasePath ? `${editionBasePath}${anchor}` : anchor;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function handleBlogFooterSectionClick(
|
|
100
|
+
event: ReactMouseEvent<HTMLAnchorElement>,
|
|
101
|
+
entry: PresentationFooterEntry,
|
|
102
|
+
href: string,
|
|
103
|
+
) {
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
window.history.pushState(null, "", href);
|
|
106
|
+
document.getElementById(getSectionAnchorId(entry.sectionKey))?.scrollIntoView({ block: "start" });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getPresentationItemRole(mode: PresentationItemMode, index?: number): "lead" | "secondary" {
|
|
110
|
+
if (mode === "magazine-feature") return "lead";
|
|
111
|
+
if (mode === "blog" && index === 0) return "lead";
|
|
112
|
+
return "secondary";
|
|
113
|
+
}
|
|
@@ -23,6 +23,18 @@ import { Newspaper } from "../../components/newspaper";
|
|
|
23
23
|
import { PictogramFigure as GenericPictogramFigure } from "../../components/pictogram-figure";
|
|
24
24
|
import { PresentationFooter } from "../../components/presentation-footer";
|
|
25
25
|
import { readLocalReaderSettings, resolveReaderSettings, subscribeReaderSettingsChanges } from "../../components/reader-settings";
|
|
26
|
+
import { RhythmBlogPresentation } from "./rhythm-blog-presentation";
|
|
27
|
+
import {
|
|
28
|
+
getBlogFooterSectionHref,
|
|
29
|
+
getPresentationBodyText,
|
|
30
|
+
getPresentationItemRole,
|
|
31
|
+
getPresentationTitle,
|
|
32
|
+
getSectionAnchorId,
|
|
33
|
+
handleBlogFooterSectionClick,
|
|
34
|
+
MeasuredPresentationLines,
|
|
35
|
+
useMeasuredWidth,
|
|
36
|
+
usePresentationTargetScroll,
|
|
37
|
+
} from "./presentation-shared";
|
|
26
38
|
|
|
27
39
|
const BlogPageBackground = SITE_BRAND.components?.BlogPageBackground ?? GenericBlogPageBackground;
|
|
28
40
|
const PictogramFigure = SITE_BRAND.components?.PictogramFigure ?? GenericPictogramFigure;
|
|
@@ -94,7 +106,9 @@ export function PresentationShell({
|
|
|
94
106
|
|
|
95
107
|
return (
|
|
96
108
|
<PresentationFrame>
|
|
97
|
-
{activePresentation === "blog" ? (
|
|
109
|
+
{activePresentation === "blog" && SITE_BRAND.blogLayout === "rhythm" ? (
|
|
110
|
+
<RhythmBlogPresentation {...presentationProps} />
|
|
111
|
+
) : activePresentation === "blog" ? (
|
|
98
112
|
<BlogPresentation {...presentationProps} />
|
|
99
113
|
) : (
|
|
100
114
|
<MagazinePresentation {...presentationProps} />
|
|
@@ -340,67 +354,6 @@ function PresentationItem({
|
|
|
340
354
|
);
|
|
341
355
|
}
|
|
342
356
|
|
|
343
|
-
function getPresentationItemRole(mode: "blog" | "magazine" | "magazine-feature", index?: number): "lead" | "secondary" {
|
|
344
|
-
if (mode === "magazine-feature") return "lead";
|
|
345
|
-
if (mode === "blog" && index === 0) return "lead";
|
|
346
|
-
return "secondary";
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function MeasuredPresentationLines({ lines }: { lines: TextLine[] }) {
|
|
350
|
-
return (
|
|
351
|
-
<div className="presentation-measured-lines">
|
|
352
|
-
{lines.map((line, index) => (
|
|
353
|
-
<span
|
|
354
|
-
className="presentation-measured-line"
|
|
355
|
-
key={`${index}-${line.text}`}
|
|
356
|
-
style={{
|
|
357
|
-
"--line-font-family": line.fontFamily,
|
|
358
|
-
"--line-font-size": `${line.fontSize}px`,
|
|
359
|
-
"--line-height": `${line.lineHeight}px`,
|
|
360
|
-
"--line-paint-height": `${line.paintHeight}px`,
|
|
361
|
-
left: line.x,
|
|
362
|
-
top: line.y,
|
|
363
|
-
} as CSSProperties}
|
|
364
|
-
>
|
|
365
|
-
{line.text}
|
|
366
|
-
</span>
|
|
367
|
-
))}
|
|
368
|
-
</div>
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function useMeasuredWidth(ref: RefObject<HTMLElement | null>): number {
|
|
373
|
-
const [width, setWidth] = useState(0);
|
|
374
|
-
useEffect(() => {
|
|
375
|
-
const node = ref.current;
|
|
376
|
-
if (!node) return;
|
|
377
|
-
const update = () => setWidth(Math.max(1, Math.floor(node.getBoundingClientRect().width)));
|
|
378
|
-
update();
|
|
379
|
-
const observer = new ResizeObserver(update);
|
|
380
|
-
observer.observe(node);
|
|
381
|
-
window.addEventListener("resize", update);
|
|
382
|
-
return () => {
|
|
383
|
-
observer.disconnect();
|
|
384
|
-
window.removeEventListener("resize", update);
|
|
385
|
-
};
|
|
386
|
-
}, [ref]);
|
|
387
|
-
return width;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function usePresentationTargetScroll(targetSection: EditionSection | undefined) {
|
|
391
|
-
useEffect(() => {
|
|
392
|
-
const scrollToCurrentTarget = () => {
|
|
393
|
-
const hashTarget = parseItemAnchorHash(window.location.hash);
|
|
394
|
-
const targetId = hashTarget ?? (targetSection ? getSectionAnchorId(targetSection.key) : null);
|
|
395
|
-
if (!targetId) return;
|
|
396
|
-
document.getElementById(targetId)?.scrollIntoView({ block: "start" });
|
|
397
|
-
};
|
|
398
|
-
requestAnimationFrame(scrollToCurrentTarget);
|
|
399
|
-
window.addEventListener("hashchange", scrollToCurrentTarget);
|
|
400
|
-
return () => window.removeEventListener("hashchange", scrollToCurrentTarget);
|
|
401
|
-
}, [targetSection]);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
357
|
function getSectionHref(content: EditionContent, section: EditionSection, editionBasePath?: string): string {
|
|
405
358
|
if (SITE_BRAND.sectionLinkStrategy === "anchor") {
|
|
406
359
|
const anchor = `#${getSectionAnchorId(section.key)}`;
|
|
@@ -411,37 +364,11 @@ function getSectionHref(content: EditionContent, section: EditionSection, editio
|
|
|
411
364
|
return datedPath.startsWith("//") ? `#${getSectionAnchorId(section.key)}` : datedPath;
|
|
412
365
|
}
|
|
413
366
|
|
|
414
|
-
function getSectionAnchorId(sectionKey: string): string {
|
|
415
|
-
return `section-${sectionKey}`;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
367
|
function getMeasuredTextHeight(lines: TextLine[]): number {
|
|
419
368
|
const last = lines[lines.length - 1];
|
|
420
369
|
return last ? last.y + last.paintHeight : 0;
|
|
421
370
|
}
|
|
422
371
|
|
|
423
|
-
function getPresentationTitle(item: PublicationItem): string {
|
|
424
|
-
return item.type === "article" ? item.headline : item.title;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function getPresentationBodyText(item: PublicationItem, mode: "blog" | "magazine" | "magazine-feature"): string {
|
|
428
|
-
const body = item.type === "article" ? item.body.join("\n\n") : (item.body ?? []).join("\n\n");
|
|
429
|
-
if (mode !== "blog") return body;
|
|
430
|
-
const excerpt = String(item.excerpt ?? "").trim();
|
|
431
|
-
if (excerpt) return excerpt;
|
|
432
|
-
return truncateWords(body, 80);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function parseItemAnchorHash(hash: string): string | null {
|
|
436
|
-
if (!hash) return null;
|
|
437
|
-
try {
|
|
438
|
-
const value = decodeURIComponent(hash.slice(1)).trim();
|
|
439
|
-
return /^[a-z0-9][a-z0-9-]*$/i.test(value) ? value : null;
|
|
440
|
-
} catch {
|
|
441
|
-
return null;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
372
|
function formatMastheadDate(value: string): string {
|
|
446
373
|
const normalized = value?.trim();
|
|
447
374
|
if (!normalized) return value;
|
|
@@ -454,17 +381,3 @@ function formatMastheadDate(value: string): string {
|
|
|
454
381
|
}).format(date);
|
|
455
382
|
}
|
|
456
383
|
|
|
457
|
-
function getBlogFooterSectionHref(entry: PresentationFooterEntry, editionBasePath?: string): string {
|
|
458
|
-
const anchor = `#${getSectionAnchorId(entry.sectionKey)}`;
|
|
459
|
-
return editionBasePath ? `${editionBasePath}${anchor}` : anchor;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function handleBlogFooterSectionClick(
|
|
463
|
-
event: ReactMouseEvent<HTMLAnchorElement>,
|
|
464
|
-
entry: PresentationFooterEntry,
|
|
465
|
-
href: string,
|
|
466
|
-
) {
|
|
467
|
-
event.preventDefault();
|
|
468
|
-
window.history.pushState(null, "", href);
|
|
469
|
-
document.getElementById(getSectionAnchorId(entry.sectionKey))?.scrollIntoView({ block: "start" });
|
|
470
|
-
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
|
4
|
+
import type { ArticleImage, ArticleVideoAsset } from "../../lib/articles";
|
|
5
|
+
import { solveFeaturedFloatGeometry } from "../../lib/blog-feature-solver";
|
|
6
|
+
import type { ArticlePageEditionFooter } from "../../lib/renderer";
|
|
7
|
+
import { SITE_BRAND } from "../../lib/site-brand";
|
|
8
|
+
import type { VideoScriptRef } from "../../lib/video-script";
|
|
9
|
+
import { ArticleVideoFigure } from "../../components/article-video";
|
|
10
|
+
import { BlogPageBackground as GenericBlogPageBackground } from "../../components/blog-page-background";
|
|
11
|
+
import { PictogramFigure as GenericPictogramFigure } from "../../components/pictogram-figure";
|
|
12
|
+
import { PresentationFooter } from "../../components/presentation-footer";
|
|
13
|
+
import { PresentationHeader } from "../../components/presentation-header";
|
|
14
|
+
import { useBlogHeaderObstacles } from "../../components/use-blog-header-obstacles";
|
|
15
|
+
import { useRhythmOverlay } from "../../components/use-rhythm-overlay";
|
|
16
|
+
import { BLOG_RHYTHM, getFeaturedLayoutStyle, getRhythmShellStyle } from "./rhythm-shell-style";
|
|
17
|
+
|
|
18
|
+
const BlogPageBackground = SITE_BRAND.components?.BlogPageBackground ?? GenericBlogPageBackground;
|
|
19
|
+
const PictogramFigure = SITE_BRAND.components?.PictogramFigure ?? GenericPictogramFigure;
|
|
20
|
+
|
|
21
|
+
export type RhythmArticlePageProps = {
|
|
22
|
+
body: string[];
|
|
23
|
+
deck?: string;
|
|
24
|
+
editionDate?: string;
|
|
25
|
+
editionFooter?: ArticlePageEditionFooter;
|
|
26
|
+
headline: string;
|
|
27
|
+
image: ArticleImage | null;
|
|
28
|
+
itemType?: string;
|
|
29
|
+
section: string;
|
|
30
|
+
slug: string;
|
|
31
|
+
video: ArticleVideoAsset | null;
|
|
32
|
+
videoScript: VideoScriptRef | null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function RhythmArticlePage({
|
|
36
|
+
body,
|
|
37
|
+
deck,
|
|
38
|
+
editionDate,
|
|
39
|
+
editionFooter,
|
|
40
|
+
headline,
|
|
41
|
+
image,
|
|
42
|
+
itemType,
|
|
43
|
+
section,
|
|
44
|
+
slug,
|
|
45
|
+
video,
|
|
46
|
+
videoScript,
|
|
47
|
+
}: RhythmArticlePageProps) {
|
|
48
|
+
const articleRef = useRef<HTMLElement | null>(null);
|
|
49
|
+
const pageRef = useRef<HTMLElement | null>(null);
|
|
50
|
+
const containerWidth = useMeasuredWidth(articleRef);
|
|
51
|
+
const viewportWidth = useViewportWidth();
|
|
52
|
+
const showRhythmOverlay = useRhythmOverlay();
|
|
53
|
+
const headerObstacles = useBlogHeaderObstacles(pageRef, BLOG_RHYTHM.paintBuffer);
|
|
54
|
+
const floatGeometry = useMemo(() => {
|
|
55
|
+
if (!image || !containerWidth) return null;
|
|
56
|
+
return solveFeaturedFloatGeometry({
|
|
57
|
+
containerWidth,
|
|
58
|
+
viewportWidth,
|
|
59
|
+
rhythm: BLOG_RHYTHM,
|
|
60
|
+
imageAsset: image,
|
|
61
|
+
itemIndex: 0,
|
|
62
|
+
});
|
|
63
|
+
}, [containerWidth, image, viewportWidth]);
|
|
64
|
+
|
|
65
|
+
const hasImage = Boolean(image);
|
|
66
|
+
const articleClassName = hasImage ? "article-page article-float-grid" : "article-page";
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<main
|
|
70
|
+
className={`${editionFooter ? "article-shell article-shell--edition" : "article-shell"} blog-rhythm-shell`}
|
|
71
|
+
data-rhythm-overlay={showRhythmOverlay ? "true" : "false"}
|
|
72
|
+
ref={pageRef}
|
|
73
|
+
style={getRhythmShellStyle(BLOG_RHYTHM)}
|
|
74
|
+
>
|
|
75
|
+
<BlogPageBackground headerObstacles={headerObstacles} pageRef={pageRef} rhythm={BLOG_RHYTHM} />
|
|
76
|
+
<PresentationHeader
|
|
77
|
+
editionBasePath={editionFooter?.editionBasePath}
|
|
78
|
+
editionDate={editionDate}
|
|
79
|
+
sections={editionFooter?.sections}
|
|
80
|
+
/>
|
|
81
|
+
<article
|
|
82
|
+
className={articleClassName}
|
|
83
|
+
data-feature-layout={hasImage ? (floatGeometry?.mode ?? "float") : undefined}
|
|
84
|
+
data-has-image={hasImage ? "true" : "false"}
|
|
85
|
+
data-item-type={itemType}
|
|
86
|
+
ref={articleRef}
|
|
87
|
+
style={floatGeometry ? getFeaturedLayoutStyle(floatGeometry) : undefined}
|
|
88
|
+
>
|
|
89
|
+
{video ? (
|
|
90
|
+
<div className="article-page__hero-video">
|
|
91
|
+
<ArticleVideoFigure slug={slug} video={video} videoScript={videoScript} />
|
|
92
|
+
</div>
|
|
93
|
+
) : null}
|
|
94
|
+
<header className={hasImage ? "article-float-grid__header" : undefined}>
|
|
95
|
+
<p className="story-label">{section}</p>
|
|
96
|
+
<h1>{headline}</h1>
|
|
97
|
+
{deck ? <p className="article-deck">{deck}</p> : null}
|
|
98
|
+
</header>
|
|
99
|
+
{image ? (
|
|
100
|
+
<div className="presentation-item__media article-float-grid__media">
|
|
101
|
+
<PictogramFigure
|
|
102
|
+
alt={image.alt}
|
|
103
|
+
caption={image.caption}
|
|
104
|
+
credit={image.credit}
|
|
105
|
+
figureClassName="presentation-item__image"
|
|
106
|
+
frameHeight={floatGeometry?.imageHeight}
|
|
107
|
+
frameWidth={floatGeometry?.imageWidth}
|
|
108
|
+
height={760}
|
|
109
|
+
layout={image.layout}
|
|
110
|
+
priority
|
|
111
|
+
sizes="(max-width: 900px) 100vw, 760px"
|
|
112
|
+
slug={slug}
|
|
113
|
+
src={image.src}
|
|
114
|
+
themeVariants={image.themeVariants}
|
|
115
|
+
width={1200}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
) : null}
|
|
119
|
+
<div className={hasImage ? "article-body article-float-grid__body" : "article-body"}>
|
|
120
|
+
{body.map((paragraph) => (
|
|
121
|
+
<p key={paragraph}>{paragraph}</p>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
</article>
|
|
125
|
+
{editionFooter ? (
|
|
126
|
+
<PresentationFooter
|
|
127
|
+
editionBasePath={editionFooter.editionBasePath}
|
|
128
|
+
entries={editionFooter.entries}
|
|
129
|
+
resolveSectionHref={(entry) => `${editionFooter.editionBasePath}#section-${entry.sectionKey}`}
|
|
130
|
+
subtitle={editionFooter.subtitle}
|
|
131
|
+
title={editionFooter.title}
|
|
132
|
+
/>
|
|
133
|
+
) : null}
|
|
134
|
+
</main>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function useMeasuredWidth(ref: RefObject<HTMLElement | null>): number {
|
|
139
|
+
const [width, setWidth] = useState(0);
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const node = ref.current;
|
|
142
|
+
if (!node) return;
|
|
143
|
+
const update = () => setWidth(Math.max(1, Math.floor(node.getBoundingClientRect().width)));
|
|
144
|
+
update();
|
|
145
|
+
const observer = new ResizeObserver(update);
|
|
146
|
+
observer.observe(node);
|
|
147
|
+
window.addEventListener("resize", update);
|
|
148
|
+
return () => {
|
|
149
|
+
observer.disconnect();
|
|
150
|
+
window.removeEventListener("resize", update);
|
|
151
|
+
};
|
|
152
|
+
}, [ref]);
|
|
153
|
+
return width;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function useViewportWidth(): number {
|
|
157
|
+
const [width, setWidth] = useState(0);
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const update = () => setWidth(Math.max(1, Math.floor(window.innerWidth)));
|
|
160
|
+
update();
|
|
161
|
+
window.addEventListener("resize", update);
|
|
162
|
+
return () => window.removeEventListener("resize", update);
|
|
163
|
+
}, []);
|
|
164
|
+
return width;
|
|
165
|
+
}
|