@anthusai/papyrus 1.0.0-next.2 → 1.0.0-next.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/amplify/auth/resource.js +17 -4
  2. package/amplify/auth/resource.ts +21 -4
  3. package/amplify/data/resource.js +52 -70
  4. package/amplify/data/resource.ts +57 -70
  5. package/amplify/data/schema.js +52 -58
  6. package/amplify/data/schema.ts +57 -58
  7. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
  8. package/amplify/functions/console-chat-responder/resource.js +0 -7
  9. package/amplify/functions/console-chat-responder/resource.ts +0 -8
  10. package/amplify/functions/console-chat-responder/src/main.rs +0 -9
  11. package/amplify/functions/content-actions/handler.py +149 -0
  12. package/amplify/functions/content-actions/requirements.txt +4 -0
  13. package/amplify/functions/content-actions/resource.js +40 -0
  14. package/amplify/functions/content-actions/resource.ts +54 -0
  15. package/amplify/functions/shared/python-bundle.js +17 -2
  16. package/amplify/functions/shared/python-bundle.ts +23 -2
  17. package/amplify/rebuild-trigger-settings.js +22 -0
  18. package/amplify/rebuild-trigger-settings.ts +42 -0
  19. package/amplify/site-backend-config.ts +12 -1
  20. package/amplify/site-backend.js +51 -27
  21. package/amplify/site-backend.ts +66 -34
  22. package/amplify/storage/resource.js +3 -0
  23. package/amplify/storage/resource.ts +3 -0
  24. package/app/%5F_preview/[[...path]]/route.ts +8 -0
  25. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +4 -2
  26. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
  27. package/app/api/revalidate/route.ts +8 -7
  28. package/app/articles/[slug]/page.tsx +3 -1
  29. package/app/blog-rhythm.css +278 -0
  30. package/app/globals.css +22 -4
  31. package/app/layout.tsx +24 -12
  32. package/app/newsroom/articles/[id]/page.tsx +15 -0
  33. package/app/newsroom/articles/page.tsx +13 -0
  34. package/app/page.tsx +12 -4
  35. package/app/robots.ts +11 -0
  36. package/bin/papyrus-app.mjs +12 -2
  37. package/bin/papyrus-infra.mjs +74 -18
  38. package/components/archive-shell.tsx +2 -22
  39. package/components/article-video.tsx +247 -3
  40. package/components/blog-page-background.tsx +23 -6
  41. package/components/markus-ir-preview.tsx +300 -0
  42. package/components/news-desk-page.tsx +1 -4
  43. package/components/newspaper.tsx +2 -1
  44. package/components/newsroom-access-gate.tsx +66 -0
  45. package/components/newsroom-article-editor.tsx +741 -0
  46. package/components/newsroom-articles-view.tsx +129 -0
  47. package/components/newsroom-ops-overview.tsx +28 -11
  48. package/components/newsroom-ops-shell.tsx +3 -1
  49. package/components/pictogram-figure.tsx +9 -2
  50. package/components/presentation-header.tsx +135 -0
  51. package/components/presentation-rhythm-hrule.tsx +17 -0
  52. package/components/settings-page.tsx +2 -1
  53. package/components/staging-banner.tsx +11 -0
  54. package/components/topic-steering-workspace.tsx +1 -59
  55. package/components/use-blog-header-obstacles.ts +81 -0
  56. package/components/use-brand-fonts-ready.ts +31 -0
  57. package/components/use-rhythm-overlay.ts +29 -0
  58. package/infra/amplify-app-shell.js +285 -80
  59. package/infra/amplify-app-shell.ts +326 -113
  60. package/infra/build-specs.js +94 -0
  61. package/infra/build-specs.ts +141 -0
  62. package/infra/github-oidc-provider.js +24 -0
  63. package/infra/github-oidc-provider.ts +30 -0
  64. package/infra/index.js +3 -0
  65. package/infra/site-config.js +273 -0
  66. package/infra/site-config.ts +336 -0
  67. package/lib/blog-feature-solver.ts +459 -0
  68. package/lib/blog-rhythm.ts +134 -0
  69. package/lib/brand-fonts.ts +112 -0
  70. package/lib/cached-content-repository.ts +2 -1
  71. package/lib/category-repository.ts +1 -93
  72. package/lib/content-actions-client.ts +98 -0
  73. package/lib/content-actions-demo.ts +190 -0
  74. package/lib/content-repository.ts +3 -0
  75. package/lib/content-source-context.ts +26 -0
  76. package/lib/content-types.ts +3 -0
  77. package/lib/edition-routes.ts +31 -29
  78. package/lib/empty-theme.css +1 -0
  79. package/lib/graphql-content-repository.ts +201 -42
  80. package/lib/layout-scenarios.ts +8 -8
  81. package/lib/newspaper-layout.ts +3 -2
  82. package/lib/newsroom-articles.ts +325 -0
  83. package/lib/newsroom-demo-articles.ts +90 -0
  84. package/lib/newsroom-media-backend.ts +142 -0
  85. package/lib/newsroom-media.ts +182 -0
  86. package/lib/newsroom-nav.ts +3 -0
  87. package/lib/papyrus-web-locations.ts +7 -6
  88. package/lib/presentation-footer.ts +3 -2
  89. package/lib/pretext-layout.ts +7 -3
  90. package/lib/reader-base-path.ts +24 -0
  91. package/lib/reader-static-params.ts +8 -0
  92. package/lib/renderer.ts +7 -0
  93. package/lib/revalidate-secret.ts +59 -0
  94. package/lib/root-route-redirect.ts +16 -0
  95. package/lib/site-brand-tenant.ts +3 -22
  96. package/lib/site-brand.ts +95 -42
  97. package/lib/site-env.ts +52 -0
  98. package/lib/staging-gate.ts +37 -0
  99. package/lib/staging-gated-path.ts +22 -0
  100. package/lib/staging-preview-object.ts +98 -0
  101. package/lib/video-mode.ts +42 -0
  102. package/lib/video-script.ts +63 -0
  103. package/middleware.ts +56 -42
  104. package/package.json +39 -1
  105. package/renderers/pretext/article-page.tsx +40 -9
  106. package/renderers/pretext/presentation-shared.tsx +113 -0
  107. package/renderers/pretext/presentation-shell.tsx +18 -112
  108. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  109. package/renderers/pretext/rhythm-blog-presentation.tsx +280 -0
  110. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  111. package/routes.manifest.json +37 -2
  112. package/scripts/videoml/build-browser-bundle.mjs +22 -0
  113. package/scripts/videoml/build-preview-bundle.mjs +50 -0
  114. package/scripts/videoml/bundle-common.mjs +129 -0
  115. package/scripts/videoml/preview/preview-entry.tsx +381 -0
  116. package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
  117. package/scripts/videoml/preview/preview-timing.ts +42 -0
  118. package/scripts/videoml/preview.html +77 -0
  119. package/scripts/videoml/standard-entry.tsx +46 -0
  120. package/src/sync.mjs +30 -6
  121. package/src/with-papyrus.mjs +15 -2
  122. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  123. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  124. package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
  125. package/publications/anth_us/brand.ts +0 -68
  126. package/publications/anth_us/theme.css +0 -66
  127. package/publications/pilobol_us/brand.ts +0 -37
  128. package/publications/pilobol_us/theme.css +0 -140
  129. package/publications/threat_intelligence/blog-defense/graph.ts +0 -636
  130. package/publications/threat_intelligence/blog-defense/layout.ts +0 -934
  131. package/publications/threat_intelligence/blog-defense/page-background.tsx +0 -590
  132. package/publications/threat_intelligence/brand.ts +0 -40
  133. package/publications/threat_intelligence/pictograms/art.tsx +0 -608
  134. package/publications/threat_intelligence/pictograms/figure.tsx +0 -82
  135. package/publications/threat_intelligence/pictograms/registry.ts +0 -30
  136. package/publications/threat_intelligence/pictograms/system.tsx +0 -305
  137. package/publications/threat_intelligence/seed/seed-edition-content.json +0 -629
  138. package/publications/threat_intelligence/theme.css +0 -1440
  139. /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,280 @@
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 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 EDITION_OVERVIEW_VIDEO_KEY = "edition-overview";
44
+ const SECONDARY_PAIR_ITEM_INDEX = 2;
45
+
46
+ type PapyrusTestWindow = Window & typeof globalThis & {
47
+ __PAPYRUS_SCENARIO__?: string;
48
+ };
49
+
50
+ export function RhythmBlogPresentation({
51
+ content,
52
+ editionBasePath,
53
+ targetSection,
54
+ }: {
55
+ content: EditionContent;
56
+ editionBasePath?: string;
57
+ targetSection?: EditionSection;
58
+ }) {
59
+ const footerEntries = useMemo(() => buildPresentationFooterEntries(content), [content]);
60
+ const footerSubtitle = SITE_BRAND.id === "papyrus" ? (content.description?.trim() || "Inside Papyrus") : SITE_BRAND.mastheadSubtitle;
61
+ const pageRef = useRef<HTMLElement | null>(null);
62
+ const showRhythmOverlay = useRhythmOverlay();
63
+ const headerObstacles = useBlogHeaderObstacles(pageRef, BLOG_RHYTHM.paintBuffer);
64
+ usePresentationTargetScroll(targetSection);
65
+ useEffect(() => {
66
+ (window as PapyrusTestWindow).__PAPYRUS_SCENARIO__ = content.scenarioId;
67
+ return () => {
68
+ delete (window as PapyrusTestWindow).__PAPYRUS_SCENARIO__;
69
+ };
70
+ }, [content.scenarioId]);
71
+
72
+ return (
73
+ <main
74
+ className="presentation-page presentation-page--blog blog-rhythm-shell"
75
+ data-presentation-engine="blog"
76
+ data-rhythm-overlay={showRhythmOverlay ? "true" : "false"}
77
+ ref={pageRef}
78
+ style={getRhythmShellStyle(BLOG_RHYTHM)}
79
+ >
80
+ <BlogPageBackground headerObstacles={headerObstacles} pageRef={pageRef} rhythm={BLOG_RHYTHM} />
81
+ <PresentationHeader
82
+ description={content.description}
83
+ editionBasePath={editionBasePath}
84
+ editionDate={content.editionDate}
85
+ sections={content.sections}
86
+ title={content.title}
87
+ />
88
+ <div className="blog-sections">
89
+ {content.sections.map((section, sectionIndex) => (
90
+ <section className="blog-section" data-edition-section={section.key} id={getSectionAnchorId(section.key)} key={section.key}>
91
+ {sectionIndex === 0 && content.editionVideo ? (
92
+ <EditionOverviewVideo
93
+ editionVideo={content.editionVideo}
94
+ videoScript={content.videoScripts?.[EDITION_OVERVIEW_VIDEO_KEY] ?? null}
95
+ />
96
+ ) : null}
97
+ <header className="presentation-section-header">
98
+ <div className="presentation-section-header__band">
99
+ <p>{section.label}</p>
100
+ </div>
101
+ {section.description ? <span>{section.description}</span> : null}
102
+ </header>
103
+ {getEditionSectionItems(section, content.items).flatMap((item, index) => [
104
+ ...(index > 0
105
+ ? [
106
+ <PresentationRhythmHRule
107
+ hideInSecondaryPair={index === SECONDARY_PAIR_ITEM_INDEX}
108
+ key={`hrule-before-${item.slug}`}
109
+ />,
110
+ ]
111
+ : []),
112
+ <RhythmPresentationItem
113
+ editionBasePath={editionBasePath}
114
+ index={index}
115
+ item={item}
116
+ key={item.slug}
117
+ videoScript={content.videoScripts?.[item.slug] ?? null}
118
+ />,
119
+ ])}
120
+ </section>
121
+ ))}
122
+ </div>
123
+ <PresentationFooter
124
+ editionBasePath={editionBasePath}
125
+ entries={footerEntries}
126
+ onSectionClick={handleBlogFooterSectionClick}
127
+ resolveSectionHref={(entry) => getBlogFooterSectionHref(entry, editionBasePath)}
128
+ subtitle={SITE_BRAND.footerSubtitleOverride ?? footerSubtitle}
129
+ title={SITE_BRAND.footerTitle}
130
+ />
131
+ </main>
132
+ );
133
+ }
134
+
135
+ function EditionOverviewVideo({
136
+ editionVideo,
137
+ videoScript,
138
+ }: {
139
+ editionVideo: ArticleVideoAsset;
140
+ videoScript: VideoScriptRef | null;
141
+ }) {
142
+ return (
143
+ <section className="presentation-edition-video" aria-label={editionVideo.alt}>
144
+ <ArticleVideoFigure
145
+ figureClassName="presentation-edition-video__figure article-video"
146
+ slug={EDITION_OVERVIEW_VIDEO_KEY}
147
+ video={editionVideo}
148
+ videoScript={videoScript}
149
+ />
150
+ </section>
151
+ );
152
+ }
153
+
154
+ function RhythmPresentationItem({
155
+ editionBasePath,
156
+ index,
157
+ item,
158
+ videoScript,
159
+ }: {
160
+ editionBasePath?: string;
161
+ index: number;
162
+ item: PublicationItem;
163
+ videoScript: VideoScriptRef | null;
164
+ }) {
165
+ const articleRef = useRef<HTMLElement | null>(null);
166
+ const frameRef = useRef<HTMLDivElement | null>(null);
167
+ const containerWidth = useMeasuredWidth(articleRef);
168
+ const frameWidth = useMeasuredWidth(frameRef);
169
+ const viewportWidth = useViewportWidth();
170
+ const fontsReadyVersion = useBrandFontsReadyVersion();
171
+ const textStyle = useMemo(() => ({ ...BLOG_TEXT_STYLE, fontsReadyVersion }), [fontsReadyVersion]);
172
+ const image = getPublicationItemImageAssets(item)[0];
173
+ const video = getPublicationItemVideoAsset(item);
174
+ const text = getPresentationBodyText(item, "blog");
175
+ const hasImage = Boolean(image);
176
+ const featuredLayout = useMemo(() => {
177
+ if (!hasImage || !containerWidth || !image) return null;
178
+ return solveFeaturedItem({
179
+ text,
180
+ containerWidth,
181
+ viewportWidth,
182
+ rhythm: BLOG_RHYTHM,
183
+ textStyle,
184
+ imageAsset: image,
185
+ itemIndex: index,
186
+ });
187
+ }, [containerWidth, hasImage, image, index, text, textStyle, viewportWidth]);
188
+ const lines = useMemo(() => {
189
+ if (featuredLayout) return featuredLayout.textLines;
190
+ const maxWidth = frameWidth || containerWidth;
191
+ if (!maxWidth || !text.trim()) return [];
192
+ return layoutAllTextLines({
193
+ prepared: prepareWithSegments(text, `${textStyle.fontSize}px ${textStyle.fontFamily}`, { whiteSpace: "pre-wrap" }),
194
+ maxWidth,
195
+ ...textStyle,
196
+ });
197
+ }, [containerWidth, featuredLayout, frameWidth, text, textStyle]);
198
+ const textHeight = featuredLayout?.textFrameHeight ?? reserveRhythmRows(getMeasuredTextHeight(lines), BLOG_RHYTHM);
199
+ const layoutMode = featuredLayout?.mode ?? "stacked";
200
+ const directHref = editionBasePath ? `${editionBasePath}/${encodeURIComponent(item.slug)}` : `/articles/${encodeURIComponent(item.slug)}`;
201
+
202
+ return (
203
+ <article
204
+ className="presentation-item presentation-item--blog"
205
+ data-feature-layout={hasImage ? layoutMode : undefined}
206
+ data-has-image={hasImage ? "true" : "false"}
207
+ data-has-video={video ? "true" : "false"}
208
+ data-item-id={item.slug}
209
+ data-item-index={index}
210
+ data-item-role={getPresentationItemRole("blog", index)}
211
+ data-item-type={item.type}
212
+ id={item.slug}
213
+ ref={articleRef}
214
+ style={featuredLayout ? getFeaturedLayoutStyle(featuredLayout) : undefined}
215
+ >
216
+ <div className="presentation-item__copy">
217
+ <header className="presentation-item__header">
218
+ <p>{item.section ?? "General"}</p>
219
+ <h2>
220
+ <Link href={directHref}>{getPresentationTitle(item)}</Link>
221
+ </h2>
222
+ {item.deck ? <span>{item.deck}</span> : null}
223
+ </header>
224
+ <div className="presentation-item__body">
225
+ {image ? (
226
+ <div className="presentation-item__media">
227
+ <PictogramFigure
228
+ alt={image.alt}
229
+ caption={image.caption}
230
+ credit={image.credit}
231
+ figureClassName="presentation-item__image"
232
+ frameHeight={featuredLayout?.imageHeight}
233
+ frameWidth={featuredLayout?.imageWidth}
234
+ height={760}
235
+ layout={image.layout}
236
+ sizes="(max-width: 900px) 100vw, 760px"
237
+ slug={item.slug}
238
+ src={image.src}
239
+ themeVariants={image.themeVariants}
240
+ width={1200}
241
+ />
242
+ </div>
243
+ ) : null}
244
+ <div
245
+ className="presentation-item__text-frame"
246
+ data-layout-mode={layoutMode}
247
+ ref={frameRef}
248
+ style={{ height: textHeight }}
249
+ >
250
+ <MeasuredPresentationLines lines={lines} />
251
+ </div>
252
+ <Link className="presentation-item__cta" href={directHref}>
253
+ Read Article
254
+ </Link>
255
+ </div>
256
+ </div>
257
+ {video ? (
258
+ <div className="presentation-item__video">
259
+ <ArticleVideoFigure
260
+ figureClassName="presentation-item__video-figure article-video"
261
+ slug={item.slug}
262
+ video={video}
263
+ videoScript={videoScript}
264
+ />
265
+ </div>
266
+ ) : null}
267
+ </article>
268
+ );
269
+ }
270
+
271
+ function useViewportWidth(): number {
272
+ const [width, setWidth] = useState(0);
273
+ useEffect(() => {
274
+ const update = () => setWidth(Math.max(1, Math.floor(window.innerWidth)));
275
+ update();
276
+ window.addEventListener("resize", update);
277
+ return () => window.removeEventListener("resize", update);
278
+ }, []);
279
+ return width;
280
+ }
@@ -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
+ }
@@ -1,8 +1,19 @@
1
1
  {
2
- "version": "1.0.0-next.2",
2
+ "version": "1.0.0-next.20",
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
+ }