@anthusai/papyrus 1.0.0-next.4 → 1.0.0-next.41
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/redirect-urls.js +54 -0
- package/amplify/auth/redirect-urls.ts +66 -0
- package/amplify/auth/resource.js +20 -5
- package/amplify/auth/resource.ts +27 -5
- package/amplify/data/resource.js +28 -70
- package/amplify/data/resource.ts +29 -70
- package/amplify/data/schema.js +33 -58
- package/amplify/data/schema.ts +34 -58
- 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 +45 -3
- 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/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/resolver-waves.js +26 -0
- package/amplify/resolver-waves.ts +34 -0
- package/amplify/site-backend-config.ts +36 -1
- package/amplify/site-backend-names.js +103 -0
- package/amplify/site-backend-names.ts +166 -0
- package/amplify/site-backend.js +93 -170
- package/amplify/site-backend.ts +126 -196
- package/amplify/storage/resource.js +3 -0
- package/amplify/storage/resource.ts +3 -0
- package/amplify/storage-backups/construct.js +31 -0
- package/amplify/storage-backups/construct.ts +47 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/api/media/[...path]/route.ts +5 -16
- package/app/api/revalidate/route.ts +8 -7
- package/app/articles/[slug]/page.tsx +6 -4
- package/app/blog-rhythm.css +278 -0
- package/app/globals.css +21 -0
- package/app/layout.tsx +16 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +6 -5
- package/app/newsroom/[section]/page.tsx +4 -3
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/newsroom/layout.tsx +4 -9
- package/app/page.tsx +18 -5
- package/app/robots.ts +2 -5
- package/bin/papyrus-app.mjs +12 -2
- package/bin/papyrus-infra.mjs +29 -3
- package/components/amplify-client-provider.tsx +4 -22
- package/components/archive-shell.tsx +2 -22
- package/components/article-video.tsx +247 -3
- package/components/blog-page-background.tsx +23 -6
- package/components/markus-ir-preview.tsx +300 -0
- package/components/news-desk-taxonomy-client.ts +51 -0
- package/components/newspaper.tsx +6 -2
- package/components/newsroom-access-gate.tsx +67 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-client-shell.tsx +12 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +3 -1
- package/components/newsroom-references-view.tsx +83 -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/reference-cyclotron-status.tsx +109 -0
- package/components/reference-relevance-review.tsx +75 -0
- package/components/settings-page.tsx +2 -1
- package/components/topic-steering-workspace.tsx +54 -64
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +28 -0
- package/components/use-rhythm-overlay.ts +29 -0
- package/infra/amplify-app-shell.js +228 -50
- package/infra/amplify-app-shell.ts +256 -53
- package/infra/build-specs.js +15 -5
- package/infra/build-specs.ts +18 -7
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +1 -0
- package/infra/site-config.js +236 -11
- package/infra/site-config.ts +276 -15
- package/lib/amplify-outputs-path.ts +5 -1
- 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 +42 -93
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-source-context.ts +12 -2
- package/lib/content-types.ts +4 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/graphql-content-repository.ts +61 -9
- package/lib/media-route-response.ts +45 -0
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-base-path.ts +12 -3
- package/lib/newsroom-category-drilldown.ts +3 -2
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-forum-routes.ts +6 -4
- package/lib/newsroom-index-filters.ts +14 -10
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +3 -0
- package/lib/oauth-redirect-priority.ts +40 -0
- package/lib/papyrus-web-locations.ts +7 -6
- package/lib/presentation-footer.ts +3 -2
- package/lib/pretext-layout.ts +12 -3
- package/lib/reader-base-path.ts +24 -0
- package/lib/reader-static-params.ts +13 -0
- 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-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/site-brand.ts +51 -0
- package/lib/staging-gated-path.ts +23 -0
- package/lib/staging-preview-object.ts +114 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +77 -0
- package/middleware.ts +23 -18
- package/package.json +45 -1
- 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 +45 -2
- package/scripts/videoml/build-browser-bundle.mjs +22 -0
- package/scripts/videoml/build-preview-bundle.mjs +50 -0
- package/scripts/videoml/bundle-common.mjs +129 -0
- package/scripts/videoml/preview/preview-entry.tsx +381 -0
- package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
- package/scripts/videoml/preview/preview-timing.ts +42 -0
- package/scripts/videoml/preview.html +77 -0
- package/scripts/videoml/standard-entry.tsx +46 -0
- package/src/sync.mjs +30 -6
- package/src/with-papyrus.mjs +19 -2
- 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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export type VideoElementAliases = Record<string, string>;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rewrites generic VideoML element names in a stored script to the publication's
|
|
5
|
+
* own scene components, e.g. `{ "quote-card": "acme-quote-card" }`.
|
|
6
|
+
*/
|
|
7
|
+
export function normalizeDevPreviewDsl(dsl: string, elementAliases: VideoElementAliases = {}): string {
|
|
8
|
+
let normalized = dsl;
|
|
9
|
+
for (const [generic, branded] of Object.entries(elementAliases)) {
|
|
10
|
+
const escaped = generic.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11
|
+
normalized = normalized.replace(new RegExp(`(</?)${escaped}(?=[\\s/>])`, "g"), `$1${branded}`);
|
|
12
|
+
}
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type VideoScriptTargetKind = "article" | "edition";
|
|
17
|
+
|
|
18
|
+
export type VideoScriptRef = {
|
|
19
|
+
slug: string;
|
|
20
|
+
dsl: string;
|
|
21
|
+
targetKind: VideoScriptTargetKind;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const EDITION_OVERVIEW_VIDEO_KEY = "edition-overview";
|
|
25
|
+
|
|
26
|
+
export function collectVideoScriptTargets(
|
|
27
|
+
items: ReadonlyArray<{ slug: string; video?: unknown }>,
|
|
28
|
+
hasEditionVideo: boolean,
|
|
29
|
+
): string[] {
|
|
30
|
+
const targets = new Set<string>();
|
|
31
|
+
if (hasEditionVideo) targets.add(EDITION_OVERVIEW_VIDEO_KEY);
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
if (item.video) targets.add(item.slug);
|
|
34
|
+
}
|
|
35
|
+
return [...targets];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function videomlItemSlug(targetSlug: string): string {
|
|
39
|
+
return `${targetSlug}--videoml`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parseVideoScriptRef(item: {
|
|
43
|
+
slug: string;
|
|
44
|
+
editorial?: unknown;
|
|
45
|
+
}): VideoScriptRef | null {
|
|
46
|
+
const editorial = parseEditorial(item.editorial);
|
|
47
|
+
const videoScript = editorial?.videoScript;
|
|
48
|
+
if (!videoScript || typeof videoScript !== "object" || Array.isArray(videoScript)) return null;
|
|
49
|
+
|
|
50
|
+
const scriptRecord = videoScript as Record<string, unknown>;
|
|
51
|
+
const dsl = typeof scriptRecord.dsl === "string" ? scriptRecord.dsl.trim() : "";
|
|
52
|
+
if (!dsl) return null;
|
|
53
|
+
|
|
54
|
+
const target = scriptRecord.target;
|
|
55
|
+
const targetKind: VideoScriptTargetKind =
|
|
56
|
+
target && typeof target === "object" && !Array.isArray(target) && (target as Record<string, unknown>).kind === "edition"
|
|
57
|
+
? "edition"
|
|
58
|
+
: "article";
|
|
59
|
+
|
|
60
|
+
return { slug: item.slug, dsl, targetKind };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseEditorial(editorial: unknown): Record<string, unknown> | null {
|
|
64
|
+
if (!editorial) return null;
|
|
65
|
+
if (typeof editorial === "string") {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(editorial) as unknown;
|
|
68
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record<string, unknown>) : null;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (typeof editorial === "object" && !Array.isArray(editorial)) {
|
|
74
|
+
return editorial as Record<string, unknown>;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
package/middleware.ts
CHANGED
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
import type { NextRequest } from "next/server";
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
3
|
import { getSiteBrand } from "./lib/site-brand";
|
|
4
|
+
import { newsroomHref } from "./lib/newsroom-base-path";
|
|
5
|
+
import { getReaderBasePath } from "./lib/reader-base-path";
|
|
6
|
+
import { isStagingGatedPath, isStaticOrApiPath } from "./lib/staging-gated-path";
|
|
4
7
|
import { getSiteEnv, isIndexable } from "./lib/site-env";
|
|
5
8
|
import { getStagingAccess } from "./lib/staging-gate";
|
|
9
|
+
import {
|
|
10
|
+
isPreviewGatedPath,
|
|
11
|
+
isStaticPreviewEnabled,
|
|
12
|
+
previewRewritePathname,
|
|
13
|
+
shouldRewriteToPreview,
|
|
14
|
+
} from "./lib/staging-preview-object";
|
|
6
15
|
|
|
7
16
|
function usesNewsroomRootPaths(): boolean {
|
|
8
17
|
const brand = getSiteBrand();
|
|
9
18
|
return brand.rootRoute?.kind === "newsroom" && brand.newsroomBasePath === "";
|
|
10
19
|
}
|
|
11
20
|
|
|
12
|
-
function
|
|
13
|
-
return (
|
|
14
|
-
pathname.startsWith("/_next")
|
|
15
|
-
|| pathname.startsWith("/api")
|
|
16
|
-
|| pathname === "/favicon.ico"
|
|
17
|
-
|| pathname.startsWith("/icon")
|
|
18
|
-
|| /\.[a-zA-Z0-9]+$/.test(pathname)
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function isStagingGatedPath(pathname: string): boolean {
|
|
23
|
-
if (isStaticOrApiPath(pathname)) return false;
|
|
24
|
-
if (pathname === "/robots.txt") return false;
|
|
25
|
-
if (pathname === "/newsroom" || pathname.startsWith("/newsroom/")) return false;
|
|
26
|
-
return true;
|
|
21
|
+
function isStagingGatedPathForBrand(pathname: string): boolean {
|
|
22
|
+
return isStagingGatedPath(pathname, getReaderBasePath(), usesNewsroomRootPaths());
|
|
27
23
|
}
|
|
28
24
|
|
|
29
25
|
function routeRequest(request: NextRequest): NextResponse {
|
|
26
|
+
if (isStaticPreviewEnabled() && shouldRewriteToPreview(request.nextUrl.pathname, usesNewsroomRootPaths())) {
|
|
27
|
+
const rewriteUrl = request.nextUrl.clone();
|
|
28
|
+
rewriteUrl.pathname = previewRewritePathname(request.nextUrl.pathname);
|
|
29
|
+
return NextResponse.rewrite(rewriteUrl);
|
|
30
|
+
}
|
|
30
31
|
if (!usesNewsroomRootPaths()) {
|
|
31
32
|
return NextResponse.next();
|
|
32
33
|
}
|
|
@@ -37,7 +38,7 @@ function routeRequest(request: NextRequest): NextResponse {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
if (pathname === "/newsroom" || pathname === "/newsroom/") {
|
|
40
|
-
return NextResponse.redirect(new URL(
|
|
41
|
+
return NextResponse.redirect(new URL(`/${request.nextUrl.search}`, request.url));
|
|
41
42
|
}
|
|
42
43
|
if (pathname.startsWith("/newsroom/")) {
|
|
43
44
|
const target = pathname.slice("/newsroom".length) || "/";
|
|
@@ -59,11 +60,15 @@ function routeRequest(request: NextRequest): NextResponse {
|
|
|
59
60
|
|
|
60
61
|
export async function middleware(request: NextRequest): Promise<NextResponse> {
|
|
61
62
|
let sessionResponse: NextResponse | null = null;
|
|
62
|
-
|
|
63
|
+
const { pathname } = request.nextUrl;
|
|
64
|
+
const gated = isStaticPreviewEnabled()
|
|
65
|
+
? isPreviewGatedPath(pathname, usesNewsroomRootPaths())
|
|
66
|
+
: isStagingGatedPathForBrand(pathname);
|
|
67
|
+
if (getSiteEnv() === "staging" && gated) {
|
|
63
68
|
sessionResponse = NextResponse.next();
|
|
64
69
|
const access = await getStagingAccess(request, sessionResponse);
|
|
65
70
|
if (access === "anonymous") {
|
|
66
|
-
return withRobotsHeader(NextResponse.redirect(new URL(
|
|
71
|
+
return withRobotsHeader(NextResponse.redirect(new URL(newsroomHref(), request.url)));
|
|
67
72
|
}
|
|
68
73
|
if (access === "forbidden") {
|
|
69
74
|
return withRobotsHeader(new NextResponse("Staging is limited to editors and admins.", { status: 403 }));
|
package/package.json
CHANGED
|
@@ -77,11 +77,16 @@
|
|
|
77
77
|
"./components/article-video": "./components/article-video.tsx",
|
|
78
78
|
"./components/blog-page-background": "./components/blog-page-background.tsx",
|
|
79
79
|
"./components/dev-sandbox-editor-sign-in": "./components/dev-sandbox-editor-sign-in.tsx",
|
|
80
|
+
"./components/markus-ir-preview": "./components/markus-ir-preview.tsx",
|
|
80
81
|
"./components/news-desk-client-provider": "./components/news-desk-client-provider.tsx",
|
|
81
82
|
"./components/news-desk-page": "./components/news-desk-page.tsx",
|
|
82
83
|
"./components/news-desk-taxonomy-client": "./components/news-desk-taxonomy-client.ts",
|
|
83
84
|
"./components/newspaper": "./components/newspaper.tsx",
|
|
85
|
+
"./components/newsroom-access-gate": "./components/newsroom-access-gate.tsx",
|
|
86
|
+
"./components/newsroom-article-editor": "./components/newsroom-article-editor.tsx",
|
|
87
|
+
"./components/newsroom-articles-view": "./components/newsroom-articles-view.tsx",
|
|
84
88
|
"./components/newsroom-assignments-view": "./components/newsroom-assignments-view.tsx",
|
|
89
|
+
"./components/newsroom-client-shell": "./components/newsroom-client-shell.tsx",
|
|
85
90
|
"./components/newsroom-ops-overview": "./components/newsroom-ops-overview.tsx",
|
|
86
91
|
"./components/newsroom-ops-shell": "./components/newsroom-ops-shell.tsx",
|
|
87
92
|
"./components/newsroom-references-view": "./components/newsroom-references-view.tsx",
|
|
@@ -89,10 +94,14 @@
|
|
|
89
94
|
"./components/papyrus-console-shell": "./components/papyrus-console-shell.tsx",
|
|
90
95
|
"./components/pictogram-figure": "./components/pictogram-figure.tsx",
|
|
91
96
|
"./components/presentation-footer": "./components/presentation-footer.tsx",
|
|
97
|
+
"./components/presentation-header": "./components/presentation-header.tsx",
|
|
98
|
+
"./components/presentation-rhythm-hrule": "./components/presentation-rhythm-hrule.tsx",
|
|
92
99
|
"./components/reader-auth-control": "./components/reader-auth-control.tsx",
|
|
93
100
|
"./components/reader-auth-state": "./components/reader-auth-state.ts",
|
|
94
101
|
"./components/reader-settings": "./components/reader-settings.ts",
|
|
95
102
|
"./components/reader-theme-control": "./components/reader-theme-control.tsx",
|
|
103
|
+
"./components/reference-cyclotron-status": "./components/reference-cyclotron-status.tsx",
|
|
104
|
+
"./components/reference-relevance-review": "./components/reference-relevance-review.tsx",
|
|
96
105
|
"./components/reference-source-preview": "./components/reference-source-preview.tsx",
|
|
97
106
|
"./components/settings-page": "./components/settings-page.tsx",
|
|
98
107
|
"./components/staging-banner": "./components/staging-banner.tsx",
|
|
@@ -115,19 +124,27 @@
|
|
|
115
124
|
"./components/ui/tabs": "./components/ui/tabs.tsx",
|
|
116
125
|
"./components/ui/textarea": "./components/ui/textarea.tsx",
|
|
117
126
|
"./components/ui/tooltip": "./components/ui/tooltip.tsx",
|
|
127
|
+
"./components/use-blog-header-obstacles": "./components/use-blog-header-obstacles.ts",
|
|
128
|
+
"./components/use-brand-fonts-ready": "./components/use-brand-fonts-ready.ts",
|
|
118
129
|
"./components/use-resolved-papyrus-theme": "./components/use-resolved-papyrus-theme.ts",
|
|
130
|
+
"./components/use-rhythm-overlay": "./components/use-rhythm-overlay.ts",
|
|
119
131
|
"./lib/amplify-outputs-guard": "./lib/amplify-outputs-guard.ts",
|
|
120
132
|
"./lib/amplify-outputs-path": "./lib/amplify-outputs-path.ts",
|
|
121
133
|
"./lib/amplify-server-runtime": "./lib/amplify-server-runtime.ts",
|
|
122
134
|
"./lib/archive-data": "./lib/archive-data.ts",
|
|
123
135
|
"./lib/archive-types": "./lib/archive-types.ts",
|
|
124
136
|
"./lib/articles": "./lib/articles.ts",
|
|
137
|
+
"./lib/blog-feature-solver": "./lib/blog-feature-solver.ts",
|
|
138
|
+
"./lib/blog-rhythm": "./lib/blog-rhythm.ts",
|
|
139
|
+
"./lib/brand-fonts-watch": "./lib/brand-fonts-watch.ts",
|
|
140
|
+
"./lib/brand-fonts": "./lib/brand-fonts.ts",
|
|
125
141
|
"./lib/cached-content-repository": "./lib/cached-content-repository.ts",
|
|
126
142
|
"./lib/category-dashboard": "./lib/category-dashboard.ts",
|
|
127
143
|
"./lib/category-repository": "./lib/category-repository.ts",
|
|
128
144
|
"./lib/console-chat-client": "./lib/console-chat-client.ts",
|
|
129
145
|
"./lib/console-web-context": "./lib/console-web-context.ts",
|
|
130
146
|
"./lib/content-actions-client": "./lib/content-actions-client.ts",
|
|
147
|
+
"./lib/content-actions-demo": "./lib/content-actions-demo.ts",
|
|
131
148
|
"./lib/content-load-timing": "./lib/content-load-timing.ts",
|
|
132
149
|
"./lib/content-repository": "./lib/content-repository.ts",
|
|
133
150
|
"./lib/content-source-context": "./lib/content-source-context.ts",
|
|
@@ -148,22 +165,28 @@
|
|
|
148
165
|
"./lib/markus-ir": "./lib/markus-ir.ts",
|
|
149
166
|
"./lib/markus-projection": "./lib/markus-projection.ts",
|
|
150
167
|
"./lib/markus-to-article": "./lib/markus-to-article.ts",
|
|
168
|
+
"./lib/media-route-response": "./lib/media-route-response.ts",
|
|
151
169
|
"./lib/news-desk-session": "./lib/news-desk-session.ts",
|
|
152
170
|
"./lib/newspaper-layout": "./lib/newspaper-layout.ts",
|
|
171
|
+
"./lib/newsroom-articles": "./lib/newsroom-articles.ts",
|
|
153
172
|
"./lib/newsroom-assignments": "./lib/newsroom-assignments.ts",
|
|
154
173
|
"./lib/newsroom-base-path": "./lib/newsroom-base-path.ts",
|
|
155
174
|
"./lib/newsroom-card-layout": "./lib/newsroom-card-layout.ts",
|
|
156
175
|
"./lib/newsroom-category-drilldown": "./lib/newsroom-category-drilldown.ts",
|
|
176
|
+
"./lib/newsroom-demo-articles": "./lib/newsroom-demo-articles.ts",
|
|
157
177
|
"./lib/newsroom-demo-dashboard": "./lib/newsroom-demo-dashboard.ts",
|
|
158
178
|
"./lib/newsroom-demo-profile": "./lib/newsroom-demo-profile.ts",
|
|
159
179
|
"./lib/newsroom-forum-routes": "./lib/newsroom-forum-routes.ts",
|
|
160
180
|
"./lib/newsroom-index-filters": "./lib/newsroom-index-filters.ts",
|
|
161
181
|
"./lib/newsroom-knowledge-query-request": "./lib/newsroom-knowledge-query-request.ts",
|
|
162
182
|
"./lib/newsroom-list-selection": "./lib/newsroom-list-selection.ts",
|
|
183
|
+
"./lib/newsroom-media-backend": "./lib/newsroom-media-backend.ts",
|
|
184
|
+
"./lib/newsroom-media": "./lib/newsroom-media.ts",
|
|
163
185
|
"./lib/newsroom-nav": "./lib/newsroom-nav.ts",
|
|
164
186
|
"./lib/newsroom-references": "./lib/newsroom-references.ts",
|
|
165
187
|
"./lib/newsroom-sections": "./lib/newsroom-sections.ts",
|
|
166
188
|
"./lib/newsroom-topics": "./lib/newsroom-topics.ts",
|
|
189
|
+
"./lib/oauth-redirect-priority": "./lib/oauth-redirect-priority.ts",
|
|
167
190
|
"./lib/papyrus-object-kinds": "./lib/papyrus-object-kinds.ts",
|
|
168
191
|
"./lib/papyrus-web-locations": "./lib/papyrus-web-locations.ts",
|
|
169
192
|
"./lib/pilobol-sample": "./lib/pilobol-sample.ts",
|
|
@@ -171,15 +194,21 @@
|
|
|
171
194
|
"./lib/pretext-layout": "./lib/pretext-layout.ts",
|
|
172
195
|
"./lib/public-placeholder-config": "./lib/public-placeholder-config.ts",
|
|
173
196
|
"./lib/publication-items": "./lib/publication-items.ts",
|
|
197
|
+
"./lib/reader-base-path": "./lib/reader-base-path.ts",
|
|
174
198
|
"./lib/reader-route-config": "./lib/reader-route-config.ts",
|
|
175
199
|
"./lib/reader-static-params": "./lib/reader-static-params.ts",
|
|
176
200
|
"./lib/reader-storage-url": "./lib/reader-storage-url.ts",
|
|
177
201
|
"./lib/reference-display": "./lib/reference-display.ts",
|
|
178
202
|
"./lib/reference-policy": "./lib/reference-policy.ts",
|
|
179
203
|
"./lib/reference-source-preview": "./lib/reference-source-preview.ts",
|
|
204
|
+
"./lib/relevance-decisions": "./lib/relevance-decisions.ts",
|
|
205
|
+
"./lib/relevance-status": "./lib/relevance-status.ts",
|
|
180
206
|
"./lib/renderer-config": "./lib/renderer-config.ts",
|
|
181
207
|
"./lib/renderer": "./lib/renderer.ts",
|
|
182
208
|
"./lib/reporting-story-budget": "./lib/reporting-story-budget.ts",
|
|
209
|
+
"./lib/revalidate-secret": "./lib/revalidate-secret.ts",
|
|
210
|
+
"./lib/robots-policy": "./lib/robots-policy.ts",
|
|
211
|
+
"./lib/root-route-redirect": "./lib/root-route-redirect.ts",
|
|
183
212
|
"./lib/semantic-graph": "./lib/semantic-graph.ts",
|
|
184
213
|
"./lib/site-brand-tenant": "./lib/site-brand-tenant.ts",
|
|
185
214
|
"./lib/site-brand": "./lib/site-brand.ts",
|
|
@@ -187,14 +216,23 @@
|
|
|
187
216
|
"./lib/site-renderer": "./lib/site-renderer.ts",
|
|
188
217
|
"./lib/site-stack": "./lib/site-stack.ts",
|
|
189
218
|
"./lib/staging-gate": "./lib/staging-gate.ts",
|
|
219
|
+
"./lib/staging-gated-path": "./lib/staging-gated-path.ts",
|
|
220
|
+
"./lib/staging-preview-object": "./lib/staging-preview-object.ts",
|
|
190
221
|
"./lib/themed-image": "./lib/themed-image.ts",
|
|
191
222
|
"./lib/utils": "./lib/utils.ts",
|
|
223
|
+
"./lib/video-mode": "./lib/video-mode.ts",
|
|
224
|
+
"./lib/video-script": "./lib/video-script.ts",
|
|
192
225
|
"./renderers/markus/index": "./renderers/markus/index.ts",
|
|
193
226
|
"./renderers/markus/stubs": "./renderers/markus/stubs.tsx",
|
|
194
227
|
"./renderers/pretext/article-page": "./renderers/pretext/article-page.tsx",
|
|
195
228
|
"./renderers/pretext/index": "./renderers/pretext/index.ts",
|
|
196
229
|
"./renderers/pretext/layouts": "./renderers/pretext/layouts.ts",
|
|
230
|
+
"./renderers/pretext/presentation-shared": "./renderers/pretext/presentation-shared.tsx",
|
|
197
231
|
"./renderers/pretext/presentation-shell": "./renderers/pretext/presentation-shell.tsx",
|
|
232
|
+
"./renderers/pretext/rhythm-article-page": "./renderers/pretext/rhythm-article-page.tsx",
|
|
233
|
+
"./renderers/pretext/rhythm-blog-presentation": "./renderers/pretext/rhythm-blog-presentation.tsx",
|
|
234
|
+
"./renderers/pretext/rhythm-shell-style": "./renderers/pretext/rhythm-shell-style.ts",
|
|
235
|
+
"./app/%5F_preview/[[...path]]/route": "./app/%5F_preview/[[...path]]/route.ts",
|
|
198
236
|
"./app/[year]/[month]/[day]/[articleSlug]/page": "./app/[year]/[month]/[day]/[articleSlug]/page.tsx",
|
|
199
237
|
"./app/[year]/[month]/[day]/page": "./app/[year]/[month]/[day]/page.tsx",
|
|
200
238
|
"./app/[year]/[month]/[day]/page/[pageNumber]/page": "./app/[year]/[month]/[day]/page/[pageNumber]/page.tsx",
|
|
@@ -207,6 +245,8 @@
|
|
|
207
245
|
"./app/layout": "./app/layout.tsx",
|
|
208
246
|
"./app/newsroom/[section]/[...selection]/page": "./app/newsroom/[section]/[...selection]/page.tsx",
|
|
209
247
|
"./app/newsroom/[section]/page": "./app/newsroom/[section]/page.tsx",
|
|
248
|
+
"./app/newsroom/articles/[id]/page": "./app/newsroom/articles/[id]/page.tsx",
|
|
249
|
+
"./app/newsroom/articles/page": "./app/newsroom/articles/page.tsx",
|
|
210
250
|
"./app/newsroom/forum/[threadId]/page": "./app/newsroom/forum/[threadId]/page.tsx",
|
|
211
251
|
"./app/newsroom/insights/[threadId]/page": "./app/newsroom/insights/[threadId]/page.tsx",
|
|
212
252
|
"./app/newsroom/insights/page": "./app/newsroom/insights/page.tsx",
|
|
@@ -214,6 +254,7 @@
|
|
|
214
254
|
"./app/newsroom/page": "./app/newsroom/page.tsx",
|
|
215
255
|
"./app/newsroom/sections/[sectionId]/page": "./app/newsroom/sections/[sectionId]/page.tsx",
|
|
216
256
|
"./app/page": "./app/page.tsx",
|
|
257
|
+
"./app/robots": "./app/robots.ts",
|
|
217
258
|
"./app/settings/page": "./app/settings/page.tsx"
|
|
218
259
|
},
|
|
219
260
|
"files": [
|
|
@@ -261,7 +302,7 @@
|
|
|
261
302
|
"optional": true
|
|
262
303
|
}
|
|
263
304
|
},
|
|
264
|
-
"version": "1.0.0-next.
|
|
305
|
+
"version": "1.0.0-next.41",
|
|
265
306
|
"dependencies": {
|
|
266
307
|
"@aws-amplify/adapter-nextjs": "^1.7.3",
|
|
267
308
|
"@aws-sdk/client-cognito-identity-provider": "3.1045.0",
|
|
@@ -283,6 +324,8 @@
|
|
|
283
324
|
"class-variance-authority": "^0.7.1",
|
|
284
325
|
"clsx": "^2.1.1",
|
|
285
326
|
"cmdk": "^1.1.1",
|
|
327
|
+
"cyclotron": "https://github.com/AnthusAI/Cyclotron/releases/download/ui-v0.2.1/cyclotron-0.2.1.tgz",
|
|
328
|
+
"esbuild": "^0.28.2",
|
|
286
329
|
"gray-matter": "^4.0.3",
|
|
287
330
|
"gsap": "^3.15.0",
|
|
288
331
|
"katex": "^0.17.0",
|
|
@@ -293,6 +336,7 @@
|
|
|
293
336
|
"streamdown": "^2.5.0",
|
|
294
337
|
"tailwind-merge": "^3.6.0",
|
|
295
338
|
"tailwindcss": "^4.3.0",
|
|
339
|
+
"tsx": "^4.23.15",
|
|
296
340
|
"tw-animate-css": "^1.4.0",
|
|
297
341
|
"use-stick-to-bottom": "^1.1.4",
|
|
298
342
|
"yaml": "^2.9.0",
|
|
@@ -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
|
-
}
|