@anthusai/papyrus 1.0.0-next.2 → 1.0.0-next.21
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/resource.js +17 -4
- package/amplify/auth/resource.ts +21 -4
- package/amplify/data/resource.js +52 -70
- package/amplify/data/resource.ts +57 -70
- package/amplify/data/schema.js +52 -58
- package/amplify/data/schema.ts +57 -58
- 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 +149 -0
- package/amplify/functions/content-actions/requirements.txt +4 -0
- package/amplify/functions/content-actions/resource.js +40 -0
- package/amplify/functions/content-actions/resource.ts +54 -0
- package/amplify/functions/shared/python-bundle.js +17 -2
- package/amplify/functions/shared/python-bundle.ts +23 -2
- package/amplify/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/site-backend-config.ts +12 -1
- package/amplify/site-backend.js +51 -27
- package/amplify/site-backend.ts +66 -34
- package/amplify/storage/resource.js +3 -0
- package/amplify/storage/resource.ts +3 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +4 -2
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- 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 +22 -4
- package/app/layout.tsx +24 -12
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/page.tsx +12 -4
- package/app/robots.ts +11 -0
- package/bin/papyrus-app.mjs +12 -2
- package/bin/papyrus-infra.mjs +74 -18
- 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-page.tsx +1 -4
- package/components/newspaper.tsx +6 -2
- package/components/newsroom-access-gate.tsx +66 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +3 -1
- package/components/pictogram-figure.tsx +9 -2
- package/components/presentation-header.tsx +135 -0
- package/components/presentation-rhythm-hrule.tsx +17 -0
- package/components/settings-page.tsx +2 -1
- package/components/staging-banner.tsx +11 -0
- package/components/topic-steering-workspace.tsx +1 -59
- 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 +285 -80
- package/infra/amplify-app-shell.ts +326 -113
- package/infra/build-specs.js +94 -0
- package/infra/build-specs.ts +141 -0
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +3 -0
- package/infra/site-config.js +273 -0
- package/infra/site-config.ts +336 -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 +14 -1
- package/lib/category-repository.ts +1 -93
- package/lib/content-actions-client.ts +98 -0
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-repository.ts +6 -0
- package/lib/content-source-context.ts +26 -0
- package/lib/content-types.ts +4 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/empty-theme.css +1 -0
- package/lib/graphql-content-repository.ts +239 -43
- package/lib/layout-scenarios.ts +8 -8
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +3 -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 +8 -0
- package/lib/renderer.ts +7 -0
- package/lib/revalidate-secret.ts +59 -0
- package/lib/root-route-redirect.ts +16 -0
- package/lib/site-brand-tenant.ts +3 -22
- package/lib/site-brand.ts +95 -42
- package/lib/site-env.ts +52 -0
- package/lib/staging-gate.ts +37 -0
- package/lib/staging-gated-path.ts +22 -0
- package/lib/staging-preview-object.ts +98 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +77 -0
- package/middleware.ts +56 -42
- package/package.json +40 -1
- package/renderers/pretext/article-page.tsx +40 -9
- package/renderers/pretext/presentation-shared.tsx +113 -0
- package/renderers/pretext/presentation-shell.tsx +18 -112
- 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 +37 -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 +15 -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
- package/publications/anth_us/brand.ts +0 -68
- package/publications/anth_us/theme.css +0 -66
- package/publications/pilobol_us/brand.ts +0 -37
- package/publications/pilobol_us/theme.css +0 -140
- package/publications/threat_intelligence/blog-defense/graph.ts +0 -636
- package/publications/threat_intelligence/blog-defense/layout.ts +0 -934
- package/publications/threat_intelligence/blog-defense/page-background.tsx +0 -590
- package/publications/threat_intelligence/brand.ts +0 -40
- package/publications/threat_intelligence/pictograms/art.tsx +0 -608
- package/publications/threat_intelligence/pictograms/figure.tsx +0 -82
- package/publications/threat_intelligence/pictograms/registry.ts +0 -30
- package/publications/threat_intelligence/pictograms/system.tsx +0 -305
- package/publications/threat_intelligence/seed/seed-edition-content.json +0 -629
- package/publications/threat_intelligence/theme.css +0 -1440
- /package/{publications/papyrus/theme.css → app/papyrus-theme.css} +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { solveFeaturedItem } from "../../lib/blog-feature-solver";
|
|
6
|
+
import { createDefaultBlogTextStyle, getMeasuredTextHeight, reserveRhythmRows } from "../../lib/blog-rhythm";
|
|
7
|
+
import type { ArticleVideoAsset } from "../../lib/articles";
|
|
8
|
+
import type { EditionContent, EditionSection } from "../../lib/content-types";
|
|
9
|
+
import { getEditionSectionItems } from "../../lib/edition-sections";
|
|
10
|
+
import { layoutAllTextLines, prepareWithSegments } from "../../lib/pretext-layout";
|
|
11
|
+
import { buildPresentationFooterEntries } from "../../lib/presentation-footer";
|
|
12
|
+
import {
|
|
13
|
+
getPublicationItemImageAssets,
|
|
14
|
+
getPublicationItemVideoAsset,
|
|
15
|
+
type PublicationItem,
|
|
16
|
+
} from "../../lib/publication-items";
|
|
17
|
+
import { SITE_BRAND } from "../../lib/site-brand";
|
|
18
|
+
import { EDITION_OVERVIEW_VIDEO_KEY, type VideoScriptRef } from "../../lib/video-script";
|
|
19
|
+
import { ArticleVideoFigure } from "../../components/article-video";
|
|
20
|
+
import { BlogPageBackground as GenericBlogPageBackground } from "../../components/blog-page-background";
|
|
21
|
+
import { PictogramFigure as GenericPictogramFigure } from "../../components/pictogram-figure";
|
|
22
|
+
import { PresentationFooter } from "../../components/presentation-footer";
|
|
23
|
+
import { getSectionAnchorId, PresentationHeader } from "../../components/presentation-header";
|
|
24
|
+
import { PresentationRhythmHRule } from "../../components/presentation-rhythm-hrule";
|
|
25
|
+
import { useBlogHeaderObstacles } from "../../components/use-blog-header-obstacles";
|
|
26
|
+
import { useBrandFontsReadyVersion } from "../../components/use-brand-fonts-ready";
|
|
27
|
+
import { useRhythmOverlay } from "../../components/use-rhythm-overlay";
|
|
28
|
+
import {
|
|
29
|
+
getBlogFooterSectionHref,
|
|
30
|
+
getPresentationBodyText,
|
|
31
|
+
getPresentationItemRole,
|
|
32
|
+
getPresentationTitle,
|
|
33
|
+
handleBlogFooterSectionClick,
|
|
34
|
+
MeasuredPresentationLines,
|
|
35
|
+
useMeasuredWidth,
|
|
36
|
+
usePresentationTargetScroll,
|
|
37
|
+
} from "./presentation-shared";
|
|
38
|
+
import { BLOG_RHYTHM, getFeaturedLayoutStyle, getRhythmShellStyle } from "./rhythm-shell-style";
|
|
39
|
+
|
|
40
|
+
const BlogPageBackground = SITE_BRAND.components?.BlogPageBackground ?? GenericBlogPageBackground;
|
|
41
|
+
const PictogramFigure = SITE_BRAND.components?.PictogramFigure ?? GenericPictogramFigure;
|
|
42
|
+
const BLOG_TEXT_STYLE = createDefaultBlogTextStyle(SITE_BRAND.textFont);
|
|
43
|
+
const SECONDARY_PAIR_ITEM_INDEX = 2;
|
|
44
|
+
|
|
45
|
+
type PapyrusTestWindow = Window & typeof globalThis & {
|
|
46
|
+
__PAPYRUS_SCENARIO__?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function RhythmBlogPresentation({
|
|
50
|
+
content,
|
|
51
|
+
editionBasePath,
|
|
52
|
+
targetSection,
|
|
53
|
+
}: {
|
|
54
|
+
content: EditionContent;
|
|
55
|
+
editionBasePath?: string;
|
|
56
|
+
targetSection?: EditionSection;
|
|
57
|
+
}) {
|
|
58
|
+
const footerEntries = useMemo(() => buildPresentationFooterEntries(content), [content]);
|
|
59
|
+
const footerSubtitle = SITE_BRAND.id === "papyrus" ? (content.description?.trim() || "Inside Papyrus") : SITE_BRAND.mastheadSubtitle;
|
|
60
|
+
const pageRef = useRef<HTMLElement | null>(null);
|
|
61
|
+
const showRhythmOverlay = useRhythmOverlay();
|
|
62
|
+
const headerObstacles = useBlogHeaderObstacles(pageRef, BLOG_RHYTHM.paintBuffer);
|
|
63
|
+
usePresentationTargetScroll(targetSection);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
(window as PapyrusTestWindow).__PAPYRUS_SCENARIO__ = content.scenarioId;
|
|
66
|
+
return () => {
|
|
67
|
+
delete (window as PapyrusTestWindow).__PAPYRUS_SCENARIO__;
|
|
68
|
+
};
|
|
69
|
+
}, [content.scenarioId]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<main
|
|
73
|
+
className="presentation-page presentation-page--blog blog-rhythm-shell"
|
|
74
|
+
data-presentation-engine="blog"
|
|
75
|
+
data-rhythm-overlay={showRhythmOverlay ? "true" : "false"}
|
|
76
|
+
ref={pageRef}
|
|
77
|
+
style={getRhythmShellStyle(BLOG_RHYTHM)}
|
|
78
|
+
>
|
|
79
|
+
<BlogPageBackground headerObstacles={headerObstacles} pageRef={pageRef} rhythm={BLOG_RHYTHM} />
|
|
80
|
+
<PresentationHeader
|
|
81
|
+
description={content.description}
|
|
82
|
+
editionBasePath={editionBasePath}
|
|
83
|
+
editionDate={content.editionDate}
|
|
84
|
+
sections={content.sections}
|
|
85
|
+
title={content.title}
|
|
86
|
+
/>
|
|
87
|
+
<div className="blog-sections">
|
|
88
|
+
{content.sections.map((section, sectionIndex) => (
|
|
89
|
+
<section className="blog-section" data-edition-section={section.key} id={getSectionAnchorId(section.key)} key={section.key}>
|
|
90
|
+
{sectionIndex === 0 && content.editionVideo ? (
|
|
91
|
+
<EditionOverviewVideo
|
|
92
|
+
editionVideo={content.editionVideo}
|
|
93
|
+
videoScript={content.videoScripts?.[EDITION_OVERVIEW_VIDEO_KEY] ?? null}
|
|
94
|
+
/>
|
|
95
|
+
) : null}
|
|
96
|
+
<header className="presentation-section-header">
|
|
97
|
+
<div className="presentation-section-header__band">
|
|
98
|
+
<p>{section.label}</p>
|
|
99
|
+
</div>
|
|
100
|
+
{section.description ? <span>{section.description}</span> : null}
|
|
101
|
+
</header>
|
|
102
|
+
{getEditionSectionItems(section, content.items).flatMap((item, index) => [
|
|
103
|
+
...(index > 0
|
|
104
|
+
? [
|
|
105
|
+
<PresentationRhythmHRule
|
|
106
|
+
hideInSecondaryPair={index === SECONDARY_PAIR_ITEM_INDEX}
|
|
107
|
+
key={`hrule-before-${item.slug}`}
|
|
108
|
+
/>,
|
|
109
|
+
]
|
|
110
|
+
: []),
|
|
111
|
+
<RhythmPresentationItem
|
|
112
|
+
editionBasePath={editionBasePath}
|
|
113
|
+
index={index}
|
|
114
|
+
item={item}
|
|
115
|
+
key={item.slug}
|
|
116
|
+
videoScript={content.videoScripts?.[item.slug] ?? null}
|
|
117
|
+
/>,
|
|
118
|
+
])}
|
|
119
|
+
</section>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
<PresentationFooter
|
|
123
|
+
editionBasePath={editionBasePath}
|
|
124
|
+
entries={footerEntries}
|
|
125
|
+
onSectionClick={handleBlogFooterSectionClick}
|
|
126
|
+
resolveSectionHref={(entry) => getBlogFooterSectionHref(entry, editionBasePath)}
|
|
127
|
+
subtitle={SITE_BRAND.footerSubtitleOverride ?? footerSubtitle}
|
|
128
|
+
title={SITE_BRAND.footerTitle}
|
|
129
|
+
/>
|
|
130
|
+
</main>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function EditionOverviewVideo({
|
|
135
|
+
editionVideo,
|
|
136
|
+
videoScript,
|
|
137
|
+
}: {
|
|
138
|
+
editionVideo: ArticleVideoAsset;
|
|
139
|
+
videoScript: VideoScriptRef | null;
|
|
140
|
+
}) {
|
|
141
|
+
return (
|
|
142
|
+
<section className="presentation-edition-video" aria-label={editionVideo.alt}>
|
|
143
|
+
<ArticleVideoFigure
|
|
144
|
+
figureClassName="presentation-edition-video__figure article-video"
|
|
145
|
+
slug={EDITION_OVERVIEW_VIDEO_KEY}
|
|
146
|
+
video={editionVideo}
|
|
147
|
+
videoScript={videoScript}
|
|
148
|
+
/>
|
|
149
|
+
</section>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function RhythmPresentationItem({
|
|
154
|
+
editionBasePath,
|
|
155
|
+
index,
|
|
156
|
+
item,
|
|
157
|
+
videoScript,
|
|
158
|
+
}: {
|
|
159
|
+
editionBasePath?: string;
|
|
160
|
+
index: number;
|
|
161
|
+
item: PublicationItem;
|
|
162
|
+
videoScript: VideoScriptRef | null;
|
|
163
|
+
}) {
|
|
164
|
+
const articleRef = useRef<HTMLElement | null>(null);
|
|
165
|
+
const frameRef = useRef<HTMLDivElement | null>(null);
|
|
166
|
+
const containerWidth = useMeasuredWidth(articleRef);
|
|
167
|
+
const frameWidth = useMeasuredWidth(frameRef);
|
|
168
|
+
const viewportWidth = useViewportWidth();
|
|
169
|
+
const fontsReadyVersion = useBrandFontsReadyVersion();
|
|
170
|
+
const textStyle = useMemo(() => ({ ...BLOG_TEXT_STYLE, fontsReadyVersion }), [fontsReadyVersion]);
|
|
171
|
+
const image = getPublicationItemImageAssets(item)[0];
|
|
172
|
+
const video = getPublicationItemVideoAsset(item);
|
|
173
|
+
const text = getPresentationBodyText(item, "blog");
|
|
174
|
+
const hasImage = Boolean(image);
|
|
175
|
+
const featuredLayout = useMemo(() => {
|
|
176
|
+
if (!hasImage || !containerWidth || !image) return null;
|
|
177
|
+
return solveFeaturedItem({
|
|
178
|
+
text,
|
|
179
|
+
containerWidth,
|
|
180
|
+
viewportWidth,
|
|
181
|
+
rhythm: BLOG_RHYTHM,
|
|
182
|
+
textStyle,
|
|
183
|
+
imageAsset: image,
|
|
184
|
+
itemIndex: index,
|
|
185
|
+
});
|
|
186
|
+
}, [containerWidth, hasImage, image, index, text, textStyle, viewportWidth]);
|
|
187
|
+
const lines = useMemo(() => {
|
|
188
|
+
if (featuredLayout) return featuredLayout.textLines;
|
|
189
|
+
const maxWidth = frameWidth || containerWidth;
|
|
190
|
+
if (!maxWidth || !text.trim()) return [];
|
|
191
|
+
return layoutAllTextLines({
|
|
192
|
+
prepared: prepareWithSegments(text, `${textStyle.fontSize}px ${textStyle.fontFamily}`, { whiteSpace: "pre-wrap" }),
|
|
193
|
+
maxWidth,
|
|
194
|
+
...textStyle,
|
|
195
|
+
});
|
|
196
|
+
}, [containerWidth, featuredLayout, frameWidth, text, textStyle]);
|
|
197
|
+
const textHeight = featuredLayout?.textFrameHeight ?? reserveRhythmRows(getMeasuredTextHeight(lines), BLOG_RHYTHM);
|
|
198
|
+
const layoutMode = featuredLayout?.mode ?? "stacked";
|
|
199
|
+
const directHref = editionBasePath ? `${editionBasePath}/${encodeURIComponent(item.slug)}` : `/articles/${encodeURIComponent(item.slug)}`;
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<article
|
|
203
|
+
className="presentation-item presentation-item--blog"
|
|
204
|
+
data-feature-layout={hasImage ? layoutMode : undefined}
|
|
205
|
+
data-has-image={hasImage ? "true" : "false"}
|
|
206
|
+
data-has-video={video ? "true" : "false"}
|
|
207
|
+
data-item-id={item.slug}
|
|
208
|
+
data-item-index={index}
|
|
209
|
+
data-item-role={getPresentationItemRole("blog", index)}
|
|
210
|
+
data-item-type={item.type}
|
|
211
|
+
id={item.slug}
|
|
212
|
+
ref={articleRef}
|
|
213
|
+
style={featuredLayout ? getFeaturedLayoutStyle(featuredLayout) : undefined}
|
|
214
|
+
>
|
|
215
|
+
<div className="presentation-item__copy">
|
|
216
|
+
<header className="presentation-item__header">
|
|
217
|
+
<p>{item.section ?? "General"}</p>
|
|
218
|
+
<h2>
|
|
219
|
+
<Link href={directHref}>{getPresentationTitle(item)}</Link>
|
|
220
|
+
</h2>
|
|
221
|
+
{item.deck ? <span>{item.deck}</span> : null}
|
|
222
|
+
</header>
|
|
223
|
+
<div className="presentation-item__body">
|
|
224
|
+
{image ? (
|
|
225
|
+
<div className="presentation-item__media">
|
|
226
|
+
<PictogramFigure
|
|
227
|
+
alt={image.alt}
|
|
228
|
+
caption={image.caption}
|
|
229
|
+
credit={image.credit}
|
|
230
|
+
figureClassName="presentation-item__image"
|
|
231
|
+
frameHeight={featuredLayout?.imageHeight}
|
|
232
|
+
frameWidth={featuredLayout?.imageWidth}
|
|
233
|
+
height={760}
|
|
234
|
+
layout={image.layout}
|
|
235
|
+
sizes="(max-width: 900px) 100vw, 760px"
|
|
236
|
+
slug={item.slug}
|
|
237
|
+
src={image.src}
|
|
238
|
+
themeVariants={image.themeVariants}
|
|
239
|
+
width={1200}
|
|
240
|
+
/>
|
|
241
|
+
</div>
|
|
242
|
+
) : null}
|
|
243
|
+
<div
|
|
244
|
+
className="presentation-item__text-frame"
|
|
245
|
+
data-layout-mode={layoutMode}
|
|
246
|
+
ref={frameRef}
|
|
247
|
+
style={{ height: textHeight }}
|
|
248
|
+
>
|
|
249
|
+
<MeasuredPresentationLines lines={lines} />
|
|
250
|
+
</div>
|
|
251
|
+
<Link className="presentation-item__cta" href={directHref}>
|
|
252
|
+
Read Article
|
|
253
|
+
</Link>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
{video ? (
|
|
257
|
+
<div className="presentation-item__video">
|
|
258
|
+
<ArticleVideoFigure
|
|
259
|
+
figureClassName="presentation-item__video-figure article-video"
|
|
260
|
+
slug={item.slug}
|
|
261
|
+
video={video}
|
|
262
|
+
videoScript={videoScript}
|
|
263
|
+
/>
|
|
264
|
+
</div>
|
|
265
|
+
) : null}
|
|
266
|
+
</article>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function useViewportWidth(): number {
|
|
271
|
+
const [width, setWidth] = useState(0);
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
const update = () => setWidth(Math.max(1, Math.floor(window.innerWidth)));
|
|
274
|
+
update();
|
|
275
|
+
window.addEventListener("resize", update);
|
|
276
|
+
return () => window.removeEventListener("resize", update);
|
|
277
|
+
}, []);
|
|
278
|
+
return width;
|
|
279
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import {
|
|
3
|
+
BLOG_COPY_ROW_MULTIPLE,
|
|
4
|
+
createDefaultBlogVerticalRhythm,
|
|
5
|
+
type VerticalRhythm,
|
|
6
|
+
} from "../../lib/blog-rhythm";
|
|
7
|
+
|
|
8
|
+
export const BLOG_RHYTHM: VerticalRhythm = createDefaultBlogVerticalRhythm();
|
|
9
|
+
|
|
10
|
+
export function getRhythmShellStyle(rhythm: VerticalRhythm): CSSProperties {
|
|
11
|
+
return {
|
|
12
|
+
"--blog-rhythm": `${rhythm.rowHeight / BLOG_COPY_ROW_MULTIPLE}px`,
|
|
13
|
+
"--blog-row-height": `${rhythm.rowHeight}px`,
|
|
14
|
+
"--blog-paint-buffer": `${rhythm.paintBuffer}px`,
|
|
15
|
+
"--blog-paint-height": `${rhythm.paintHeight}px`,
|
|
16
|
+
} as CSSProperties;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getFeaturedLayoutStyle(layout: {
|
|
20
|
+
copyWidth: number;
|
|
21
|
+
imageWidth: number;
|
|
22
|
+
imageHeight: number;
|
|
23
|
+
gap: number;
|
|
24
|
+
mediaHeight?: number;
|
|
25
|
+
textFrameHeight?: number;
|
|
26
|
+
}): CSSProperties {
|
|
27
|
+
return {
|
|
28
|
+
"--feature-copy-width": `${layout.copyWidth}px`,
|
|
29
|
+
"--feature-image-width": `${layout.imageWidth}px`,
|
|
30
|
+
"--feature-image-height": `${layout.imageHeight}px`,
|
|
31
|
+
"--feature-layout-gap": `${layout.gap}px`,
|
|
32
|
+
...(layout.mediaHeight === undefined ? {} : { "--feature-media-height": `${layout.mediaHeight}px` }),
|
|
33
|
+
...(layout.textFrameHeight === undefined ? {} : { "--feature-text-frame-height": `${layout.textFrameHeight}px` }),
|
|
34
|
+
} as CSSProperties;
|
|
35
|
+
}
|
package/routes.manifest.json
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.0.0-next.
|
|
2
|
+
"version": "1.0.0-next.21",
|
|
3
3
|
"routes": [
|
|
4
|
+
{
|
|
5
|
+
"file": "app/%5F_preview/[[...path]]/route.ts",
|
|
6
|
+
"hasDefault": false,
|
|
7
|
+
"exports": [
|
|
8
|
+
"GET"
|
|
9
|
+
],
|
|
10
|
+
"config": {
|
|
11
|
+
"dynamic": "\"force-dynamic\""
|
|
12
|
+
}
|
|
13
|
+
},
|
|
4
14
|
{
|
|
5
15
|
"file": "app/[year]/[month]/[day]/[articleSlug]/page.tsx",
|
|
16
|
+
"reader": true,
|
|
6
17
|
"hasDefault": true,
|
|
7
18
|
"exports": [
|
|
8
19
|
"generateMetadata"
|
|
@@ -13,6 +24,7 @@
|
|
|
13
24
|
},
|
|
14
25
|
{
|
|
15
26
|
"file": "app/[year]/[month]/[day]/page.tsx",
|
|
27
|
+
"reader": true,
|
|
16
28
|
"hasDefault": true,
|
|
17
29
|
"exports": [
|
|
18
30
|
"generateStaticParams"
|
|
@@ -23,6 +35,7 @@
|
|
|
23
35
|
},
|
|
24
36
|
{
|
|
25
37
|
"file": "app/[year]/[month]/[day]/page/[pageNumber]/page.tsx",
|
|
38
|
+
"reader": true,
|
|
26
39
|
"hasDefault": true,
|
|
27
40
|
"exports": [],
|
|
28
41
|
"config": {
|
|
@@ -31,6 +44,7 @@
|
|
|
31
44
|
},
|
|
32
45
|
{
|
|
33
46
|
"file": "app/[year]/[month]/[day]/section/[sectionKey]/page.tsx",
|
|
47
|
+
"reader": true,
|
|
34
48
|
"hasDefault": true,
|
|
35
49
|
"exports": [],
|
|
36
50
|
"config": {
|
|
@@ -69,6 +83,7 @@
|
|
|
69
83
|
},
|
|
70
84
|
{
|
|
71
85
|
"file": "app/archive/page.tsx",
|
|
86
|
+
"reader": true,
|
|
72
87
|
"hasDefault": true,
|
|
73
88
|
"exports": [],
|
|
74
89
|
"config": {
|
|
@@ -77,6 +92,7 @@
|
|
|
77
92
|
},
|
|
78
93
|
{
|
|
79
94
|
"file": "app/articles/[slug]/page.tsx",
|
|
95
|
+
"reader": true,
|
|
80
96
|
"hasDefault": true,
|
|
81
97
|
"exports": [
|
|
82
98
|
"generateMetadata",
|
|
@@ -110,6 +126,22 @@
|
|
|
110
126
|
"dynamic": "\"force-dynamic\""
|
|
111
127
|
}
|
|
112
128
|
},
|
|
129
|
+
{
|
|
130
|
+
"file": "app/newsroom/articles/[id]/page.tsx",
|
|
131
|
+
"hasDefault": true,
|
|
132
|
+
"exports": [],
|
|
133
|
+
"config": {
|
|
134
|
+
"dynamic": "\"force-dynamic\""
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"file": "app/newsroom/articles/page.tsx",
|
|
139
|
+
"hasDefault": true,
|
|
140
|
+
"exports": [],
|
|
141
|
+
"config": {
|
|
142
|
+
"dynamic": "\"force-dynamic\""
|
|
143
|
+
}
|
|
144
|
+
},
|
|
113
145
|
{
|
|
114
146
|
"file": "app/newsroom/forum/[threadId]/page.tsx",
|
|
115
147
|
"hasDefault": true,
|
|
@@ -158,6 +190,7 @@
|
|
|
158
190
|
},
|
|
159
191
|
{
|
|
160
192
|
"file": "app/page.tsx",
|
|
193
|
+
"reader": true,
|
|
161
194
|
"hasDefault": true,
|
|
162
195
|
"exports": [],
|
|
163
196
|
"config": {
|
|
@@ -166,6 +199,7 @@
|
|
|
166
199
|
},
|
|
167
200
|
{
|
|
168
201
|
"file": "app/settings/page.tsx",
|
|
202
|
+
"reader": true,
|
|
169
203
|
"hasDefault": true,
|
|
170
204
|
"exports": [
|
|
171
205
|
"metadata"
|
|
@@ -177,6 +211,7 @@
|
|
|
177
211
|
],
|
|
178
212
|
"middleware": {
|
|
179
213
|
"file": "middleware.ts",
|
|
180
|
-
"matcher": "[\"/((?!_next/static|_next/image|favicon.ico|icon).*)\"]"
|
|
214
|
+
"matcher": "[\"/((?!_next/static|_next/image|favicon.ico|icon).*)\"]",
|
|
215
|
+
"runtime": "nodejs"
|
|
181
216
|
}
|
|
182
217
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { bundleVideoEntries, loadBrandVideoSlot, standardEntryPath } from "./bundle-common.mjs";
|
|
5
|
+
|
|
6
|
+
export async function buildBrowserBundle(projectRoot = process.cwd(), env = process.env) {
|
|
7
|
+
const { brandId, bundleEntry } = await loadBrandVideoSlot(projectRoot, env);
|
|
8
|
+
const result = await bundleVideoEntries({
|
|
9
|
+
projectRoot,
|
|
10
|
+
entries: [standardEntryPath, bundleEntry],
|
|
11
|
+
outfile: path.join(projectRoot, "public/videoml/browser-bundle.js"),
|
|
12
|
+
});
|
|
13
|
+
console.error(`VideoML browser bundle for '${brandId}': ${result.outfile} (${result.size} bytes)`);
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
18
|
+
buildBrowserBundle().catch((error) => {
|
|
19
|
+
console.error(error.message);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
bundleVideoEntries,
|
|
7
|
+
loadBrandVideoSlot,
|
|
8
|
+
previewEntryPath,
|
|
9
|
+
previewHtmlTemplatePath,
|
|
10
|
+
standardEntryPath,
|
|
11
|
+
} from "./bundle-common.mjs";
|
|
12
|
+
|
|
13
|
+
const REACT_18_VERSION = "18.3.1";
|
|
14
|
+
const VENDOR_FILES = {
|
|
15
|
+
"react.production.min.js": `https://unpkg.com/react@${REACT_18_VERSION}/umd/react.production.min.js`,
|
|
16
|
+
"react-dom.production.min.js": `https://unpkg.com/react-dom@${REACT_18_VERSION}/umd/react-dom.production.min.js`,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
async function ensureReactVendor(outDirectory) {
|
|
20
|
+
const vendorDirectory = path.join(outDirectory, "vendor");
|
|
21
|
+
fs.mkdirSync(vendorDirectory, { recursive: true });
|
|
22
|
+
for (const [name, url] of Object.entries(VENDOR_FILES)) {
|
|
23
|
+
const target = path.join(vendorDirectory, name);
|
|
24
|
+
if (fs.existsSync(target)) continue;
|
|
25
|
+
const response = await fetch(url);
|
|
26
|
+
if (!response.ok) throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
|
|
27
|
+
fs.writeFileSync(target, Buffer.from(await response.arrayBuffer()));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function buildPreviewBundle(projectRoot = process.cwd(), env = process.env) {
|
|
32
|
+
const { brandId, bundleEntry } = await loadBrandVideoSlot(projectRoot, env);
|
|
33
|
+
const outDirectory = path.join(projectRoot, "public/videoml");
|
|
34
|
+
await ensureReactVendor(outDirectory);
|
|
35
|
+
fs.copyFileSync(previewHtmlTemplatePath, path.join(outDirectory, "preview.html"));
|
|
36
|
+
const result = await bundleVideoEntries({
|
|
37
|
+
projectRoot,
|
|
38
|
+
entries: [standardEntryPath, previewEntryPath, bundleEntry],
|
|
39
|
+
outfile: path.join(outDirectory, "preview-bundle.js"),
|
|
40
|
+
});
|
|
41
|
+
console.error(`VideoML preview bundle for '${brandId}': ${result.outfile} (${result.size} bytes)`);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
46
|
+
buildPreviewBundle().catch((error) => {
|
|
47
|
+
console.error(error.message);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
});
|
|
50
|
+
}
|