@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
package/package.json CHANGED
@@ -77,10 +77,14 @@
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",
85
89
  "./components/newsroom-ops-overview": "./components/newsroom-ops-overview.tsx",
86
90
  "./components/newsroom-ops-shell": "./components/newsroom-ops-shell.tsx",
@@ -89,12 +93,15 @@
89
93
  "./components/papyrus-console-shell": "./components/papyrus-console-shell.tsx",
90
94
  "./components/pictogram-figure": "./components/pictogram-figure.tsx",
91
95
  "./components/presentation-footer": "./components/presentation-footer.tsx",
96
+ "./components/presentation-header": "./components/presentation-header.tsx",
97
+ "./components/presentation-rhythm-hrule": "./components/presentation-rhythm-hrule.tsx",
92
98
  "./components/reader-auth-control": "./components/reader-auth-control.tsx",
93
99
  "./components/reader-auth-state": "./components/reader-auth-state.ts",
94
100
  "./components/reader-settings": "./components/reader-settings.ts",
95
101
  "./components/reader-theme-control": "./components/reader-theme-control.tsx",
96
102
  "./components/reference-source-preview": "./components/reference-source-preview.tsx",
97
103
  "./components/settings-page": "./components/settings-page.tsx",
104
+ "./components/staging-banner": "./components/staging-banner.tsx",
98
105
  "./components/topic-steering-workspace": "./components/topic-steering-workspace.tsx",
99
106
  "./components/ui/badge": "./components/ui/badge.tsx",
100
107
  "./components/ui/button-group": "./components/ui/button-group.tsx",
@@ -114,20 +121,29 @@
114
121
  "./components/ui/tabs": "./components/ui/tabs.tsx",
115
122
  "./components/ui/textarea": "./components/ui/textarea.tsx",
116
123
  "./components/ui/tooltip": "./components/ui/tooltip.tsx",
124
+ "./components/use-blog-header-obstacles": "./components/use-blog-header-obstacles.ts",
125
+ "./components/use-brand-fonts-ready": "./components/use-brand-fonts-ready.ts",
117
126
  "./components/use-resolved-papyrus-theme": "./components/use-resolved-papyrus-theme.ts",
127
+ "./components/use-rhythm-overlay": "./components/use-rhythm-overlay.ts",
118
128
  "./lib/amplify-outputs-guard": "./lib/amplify-outputs-guard.ts",
119
129
  "./lib/amplify-outputs-path": "./lib/amplify-outputs-path.ts",
120
130
  "./lib/amplify-server-runtime": "./lib/amplify-server-runtime.ts",
121
131
  "./lib/archive-data": "./lib/archive-data.ts",
122
132
  "./lib/archive-types": "./lib/archive-types.ts",
123
133
  "./lib/articles": "./lib/articles.ts",
134
+ "./lib/blog-feature-solver": "./lib/blog-feature-solver.ts",
135
+ "./lib/blog-rhythm": "./lib/blog-rhythm.ts",
136
+ "./lib/brand-fonts": "./lib/brand-fonts.ts",
124
137
  "./lib/cached-content-repository": "./lib/cached-content-repository.ts",
125
138
  "./lib/category-dashboard": "./lib/category-dashboard.ts",
126
139
  "./lib/category-repository": "./lib/category-repository.ts",
127
140
  "./lib/console-chat-client": "./lib/console-chat-client.ts",
128
141
  "./lib/console-web-context": "./lib/console-web-context.ts",
142
+ "./lib/content-actions-client": "./lib/content-actions-client.ts",
143
+ "./lib/content-actions-demo": "./lib/content-actions-demo.ts",
129
144
  "./lib/content-load-timing": "./lib/content-load-timing.ts",
130
145
  "./lib/content-repository": "./lib/content-repository.ts",
146
+ "./lib/content-source-context": "./lib/content-source-context.ts",
131
147
  "./lib/content-types": "./lib/content-types.ts",
132
148
  "./lib/define-site": "./lib/define-site.ts",
133
149
  "./lib/demo-amplify-outputs": "./lib/demo-amplify-outputs.ts",
@@ -147,16 +163,20 @@
147
163
  "./lib/markus-to-article": "./lib/markus-to-article.ts",
148
164
  "./lib/news-desk-session": "./lib/news-desk-session.ts",
149
165
  "./lib/newspaper-layout": "./lib/newspaper-layout.ts",
166
+ "./lib/newsroom-articles": "./lib/newsroom-articles.ts",
150
167
  "./lib/newsroom-assignments": "./lib/newsroom-assignments.ts",
151
168
  "./lib/newsroom-base-path": "./lib/newsroom-base-path.ts",
152
169
  "./lib/newsroom-card-layout": "./lib/newsroom-card-layout.ts",
153
170
  "./lib/newsroom-category-drilldown": "./lib/newsroom-category-drilldown.ts",
171
+ "./lib/newsroom-demo-articles": "./lib/newsroom-demo-articles.ts",
154
172
  "./lib/newsroom-demo-dashboard": "./lib/newsroom-demo-dashboard.ts",
155
173
  "./lib/newsroom-demo-profile": "./lib/newsroom-demo-profile.ts",
156
174
  "./lib/newsroom-forum-routes": "./lib/newsroom-forum-routes.ts",
157
175
  "./lib/newsroom-index-filters": "./lib/newsroom-index-filters.ts",
158
176
  "./lib/newsroom-knowledge-query-request": "./lib/newsroom-knowledge-query-request.ts",
159
177
  "./lib/newsroom-list-selection": "./lib/newsroom-list-selection.ts",
178
+ "./lib/newsroom-media-backend": "./lib/newsroom-media-backend.ts",
179
+ "./lib/newsroom-media": "./lib/newsroom-media.ts",
160
180
  "./lib/newsroom-nav": "./lib/newsroom-nav.ts",
161
181
  "./lib/newsroom-references": "./lib/newsroom-references.ts",
162
182
  "./lib/newsroom-sections": "./lib/newsroom-sections.ts",
@@ -168,6 +188,7 @@
168
188
  "./lib/pretext-layout": "./lib/pretext-layout.ts",
169
189
  "./lib/public-placeholder-config": "./lib/public-placeholder-config.ts",
170
190
  "./lib/publication-items": "./lib/publication-items.ts",
191
+ "./lib/reader-base-path": "./lib/reader-base-path.ts",
171
192
  "./lib/reader-route-config": "./lib/reader-route-config.ts",
172
193
  "./lib/reader-static-params": "./lib/reader-static-params.ts",
173
194
  "./lib/reader-storage-url": "./lib/reader-storage-url.ts",
@@ -177,19 +198,32 @@
177
198
  "./lib/renderer-config": "./lib/renderer-config.ts",
178
199
  "./lib/renderer": "./lib/renderer.ts",
179
200
  "./lib/reporting-story-budget": "./lib/reporting-story-budget.ts",
201
+ "./lib/revalidate-secret": "./lib/revalidate-secret.ts",
202
+ "./lib/root-route-redirect": "./lib/root-route-redirect.ts",
180
203
  "./lib/semantic-graph": "./lib/semantic-graph.ts",
181
204
  "./lib/site-brand-tenant": "./lib/site-brand-tenant.ts",
182
205
  "./lib/site-brand": "./lib/site-brand.ts",
206
+ "./lib/site-env": "./lib/site-env.ts",
183
207
  "./lib/site-renderer": "./lib/site-renderer.ts",
184
208
  "./lib/site-stack": "./lib/site-stack.ts",
209
+ "./lib/staging-gate": "./lib/staging-gate.ts",
210
+ "./lib/staging-gated-path": "./lib/staging-gated-path.ts",
211
+ "./lib/staging-preview-object": "./lib/staging-preview-object.ts",
185
212
  "./lib/themed-image": "./lib/themed-image.ts",
186
213
  "./lib/utils": "./lib/utils.ts",
214
+ "./lib/video-mode": "./lib/video-mode.ts",
215
+ "./lib/video-script": "./lib/video-script.ts",
187
216
  "./renderers/markus/index": "./renderers/markus/index.ts",
188
217
  "./renderers/markus/stubs": "./renderers/markus/stubs.tsx",
189
218
  "./renderers/pretext/article-page": "./renderers/pretext/article-page.tsx",
190
219
  "./renderers/pretext/index": "./renderers/pretext/index.ts",
191
220
  "./renderers/pretext/layouts": "./renderers/pretext/layouts.ts",
221
+ "./renderers/pretext/presentation-shared": "./renderers/pretext/presentation-shared.tsx",
192
222
  "./renderers/pretext/presentation-shell": "./renderers/pretext/presentation-shell.tsx",
223
+ "./renderers/pretext/rhythm-article-page": "./renderers/pretext/rhythm-article-page.tsx",
224
+ "./renderers/pretext/rhythm-blog-presentation": "./renderers/pretext/rhythm-blog-presentation.tsx",
225
+ "./renderers/pretext/rhythm-shell-style": "./renderers/pretext/rhythm-shell-style.ts",
226
+ "./app/%5F_preview/[[...path]]/route": "./app/%5F_preview/[[...path]]/route.ts",
193
227
  "./app/[year]/[month]/[day]/[articleSlug]/page": "./app/[year]/[month]/[day]/[articleSlug]/page.tsx",
194
228
  "./app/[year]/[month]/[day]/page": "./app/[year]/[month]/[day]/page.tsx",
195
229
  "./app/[year]/[month]/[day]/page/[pageNumber]/page": "./app/[year]/[month]/[day]/page/[pageNumber]/page.tsx",
@@ -202,6 +236,8 @@
202
236
  "./app/layout": "./app/layout.tsx",
203
237
  "./app/newsroom/[section]/[...selection]/page": "./app/newsroom/[section]/[...selection]/page.tsx",
204
238
  "./app/newsroom/[section]/page": "./app/newsroom/[section]/page.tsx",
239
+ "./app/newsroom/articles/[id]/page": "./app/newsroom/articles/[id]/page.tsx",
240
+ "./app/newsroom/articles/page": "./app/newsroom/articles/page.tsx",
205
241
  "./app/newsroom/forum/[threadId]/page": "./app/newsroom/forum/[threadId]/page.tsx",
206
242
  "./app/newsroom/insights/[threadId]/page": "./app/newsroom/insights/[threadId]/page.tsx",
207
243
  "./app/newsroom/insights/page": "./app/newsroom/insights/page.tsx",
@@ -256,7 +292,7 @@
256
292
  "optional": true
257
293
  }
258
294
  },
259
- "version": "1.0.0-next.2",
295
+ "version": "1.0.0-next.20",
260
296
  "dependencies": {
261
297
  "@aws-amplify/adapter-nextjs": "^1.7.3",
262
298
  "@aws-sdk/client-cognito-identity-provider": "3.1045.0",
@@ -278,6 +314,7 @@
278
314
  "class-variance-authority": "^0.7.1",
279
315
  "clsx": "^2.1.1",
280
316
  "cmdk": "^1.1.1",
317
+ "esbuild": "^0.28.2",
281
318
  "gray-matter": "^4.0.3",
282
319
  "gsap": "^3.15.0",
283
320
  "katex": "^0.17.0",
@@ -288,6 +325,7 @@
288
325
  "streamdown": "^2.5.0",
289
326
  "tailwind-merge": "^3.6.0",
290
327
  "tailwindcss": "^4.3.0",
328
+ "tsx": "^4.23.15",
291
329
  "tw-animate-css": "^1.4.0",
292
330
  "use-stick-to-bottom": "^1.1.4",
293
331
  "yaml": "^2.9.0",
@@ -7,21 +7,34 @@ import { getPublicationItemVideoAsset } from "../../lib/publication-items";
7
7
  import type { ArticlePageEditionFooter, RenderArticleProps, RenderItemProps } from "../../lib/renderer";
8
8
  import { SITE_BRAND } from "../../lib/site-brand";
9
9
  import { ArticleVideoFigure } from "../../components/article-video";
10
- import { PictogramFigure as ThreatIntelligencePictogramFigure } from "../../publications/threat_intelligence/pictograms/figure";
11
10
  import { PictogramFigure as GenericPictogramFigure } from "../../components/pictogram-figure";
12
11
  import { PresentationFooter } from "../../components/presentation-footer";
12
+ import { RhythmArticlePage } from "./rhythm-article-page";
13
13
 
14
- // Threat Intelligence supplies its own pictogram figure media; any other
15
- // publication gets the generic fallback (components/pictogram-figure.tsx).
16
- // Same decoupling as renderers/pretext/presentation-shell.tsx -- see
17
- // docs/pluggable-publishers.md section 2.6 and PPY-d79ff2.
18
- const PictogramFigure = SITE_BRAND.id === "threat-intelligence" ? ThreatIntelligencePictogramFigure : GenericPictogramFigure;
14
+ const PictogramFigure = SITE_BRAND.components?.PictogramFigure ?? GenericPictogramFigure;
19
15
 
20
16
  export type { ArticlePageEditionFooter };
21
17
 
22
18
  type ArticlePageViewProps = RenderArticleProps;
23
19
 
24
- 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
+
25
38
  const articleDate = editionDate ? formatArticleDate(editionDate) : null;
26
39
 
27
40
  return (
@@ -60,9 +73,27 @@ export function ArticlePageView({ article, backHref, backLabel = SITE_BRAND.back
60
73
 
61
74
  type ItemPageViewProps = RenderItemProps;
62
75
 
63
- 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) {
64
77
  if (item.type === "article") {
65
- 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
+ );
66
97
  }
67
98
 
68
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
+ }
@@ -18,21 +18,26 @@ import type { ArticleVideoAsset } from "../../lib/articles";
18
18
  import type { PresentationTarget, RenderEditionProps } from "../../lib/renderer";
19
19
  import { SITE_BRAND, enforcePresentation, getDefaultPretextLayout } from "../../lib/site-brand";
20
20
  import { ArticleVideoFigure } from "../../components/article-video";
21
- import { BlogPageBackground as ThreatIntelligenceBlogPageBackground } from "../../publications/threat_intelligence/blog-defense/page-background";
22
21
  import { BlogPageBackground as GenericBlogPageBackground } from "../../components/blog-page-background";
23
22
  import { Newspaper } from "../../components/newspaper";
24
- import { PictogramFigure as ThreatIntelligencePictogramFigure } from "../../publications/threat_intelligence/pictograms/figure";
25
23
  import { PictogramFigure as GenericPictogramFigure } from "../../components/pictogram-figure";
26
24
  import { PresentationFooter } from "../../components/presentation-footer";
27
25
  import { readLocalReaderSettings, resolveReaderSettings, subscribeReaderSettingsChanges } from "../../components/reader-settings";
28
-
29
- // Threat Intelligence supplies its own blog background and figure media; any
30
- // other publication gets the generic fallback (components/). This is the
31
- // coupling docs/pluggable-publishers.md section 2.6 flags, decoupled here
32
- // per PPY-d79ff2 by making the choice publication-conditional instead of
33
- // hardcoded.
34
- const BlogPageBackground = SITE_BRAND.id === "threat-intelligence" ? ThreatIntelligenceBlogPageBackground : GenericBlogPageBackground;
35
- const PictogramFigure = SITE_BRAND.id === "threat-intelligence" ? ThreatIntelligencePictogramFigure : GenericPictogramFigure;
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";
38
+
39
+ const BlogPageBackground = SITE_BRAND.components?.BlogPageBackground ?? GenericBlogPageBackground;
40
+ const PictogramFigure = SITE_BRAND.components?.PictogramFigure ?? GenericPictogramFigure;
36
41
 
37
42
  export type { PresentationTarget };
38
43
 
@@ -101,7 +106,9 @@ export function PresentationShell({
101
106
 
102
107
  return (
103
108
  <PresentationFrame>
104
- {activePresentation === "blog" ? (
109
+ {activePresentation === "blog" && SITE_BRAND.blogLayout === "rhythm" ? (
110
+ <RhythmBlogPresentation {...presentationProps} />
111
+ ) : activePresentation === "blog" ? (
105
112
  <BlogPresentation {...presentationProps} />
106
113
  ) : (
107
114
  <MagazinePresentation {...presentationProps} />
@@ -347,67 +354,6 @@ function PresentationItem({
347
354
  );
348
355
  }
349
356
 
350
- function getPresentationItemRole(mode: "blog" | "magazine" | "magazine-feature", index?: number): "lead" | "secondary" {
351
- if (mode === "magazine-feature") return "lead";
352
- if (mode === "blog" && index === 0) return "lead";
353
- return "secondary";
354
- }
355
-
356
- function MeasuredPresentationLines({ lines }: { lines: TextLine[] }) {
357
- return (
358
- <div className="presentation-measured-lines">
359
- {lines.map((line, index) => (
360
- <span
361
- className="presentation-measured-line"
362
- key={`${index}-${line.text}`}
363
- style={{
364
- "--line-font-family": line.fontFamily,
365
- "--line-font-size": `${line.fontSize}px`,
366
- "--line-height": `${line.lineHeight}px`,
367
- "--line-paint-height": `${line.paintHeight}px`,
368
- left: line.x,
369
- top: line.y,
370
- } as CSSProperties}
371
- >
372
- {line.text}
373
- </span>
374
- ))}
375
- </div>
376
- );
377
- }
378
-
379
- function useMeasuredWidth(ref: RefObject<HTMLElement | null>): number {
380
- const [width, setWidth] = useState(0);
381
- useEffect(() => {
382
- const node = ref.current;
383
- if (!node) return;
384
- const update = () => setWidth(Math.max(1, Math.floor(node.getBoundingClientRect().width)));
385
- update();
386
- const observer = new ResizeObserver(update);
387
- observer.observe(node);
388
- window.addEventListener("resize", update);
389
- return () => {
390
- observer.disconnect();
391
- window.removeEventListener("resize", update);
392
- };
393
- }, [ref]);
394
- return width;
395
- }
396
-
397
- function usePresentationTargetScroll(targetSection: EditionSection | undefined) {
398
- useEffect(() => {
399
- const scrollToCurrentTarget = () => {
400
- const hashTarget = parseItemAnchorHash(window.location.hash);
401
- const targetId = hashTarget ?? (targetSection ? getSectionAnchorId(targetSection.key) : null);
402
- if (!targetId) return;
403
- document.getElementById(targetId)?.scrollIntoView({ block: "start" });
404
- };
405
- requestAnimationFrame(scrollToCurrentTarget);
406
- window.addEventListener("hashchange", scrollToCurrentTarget);
407
- return () => window.removeEventListener("hashchange", scrollToCurrentTarget);
408
- }, [targetSection]);
409
- }
410
-
411
357
  function getSectionHref(content: EditionContent, section: EditionSection, editionBasePath?: string): string {
412
358
  if (SITE_BRAND.sectionLinkStrategy === "anchor") {
413
359
  const anchor = `#${getSectionAnchorId(section.key)}`;
@@ -418,37 +364,11 @@ function getSectionHref(content: EditionContent, section: EditionSection, editio
418
364
  return datedPath.startsWith("//") ? `#${getSectionAnchorId(section.key)}` : datedPath;
419
365
  }
420
366
 
421
- function getSectionAnchorId(sectionKey: string): string {
422
- return `section-${sectionKey}`;
423
- }
424
-
425
367
  function getMeasuredTextHeight(lines: TextLine[]): number {
426
368
  const last = lines[lines.length - 1];
427
369
  return last ? last.y + last.paintHeight : 0;
428
370
  }
429
371
 
430
- function getPresentationTitle(item: PublicationItem): string {
431
- return item.type === "article" ? item.headline : item.title;
432
- }
433
-
434
- function getPresentationBodyText(item: PublicationItem, mode: "blog" | "magazine" | "magazine-feature"): string {
435
- const body = item.type === "article" ? item.body.join("\n\n") : (item.body ?? []).join("\n\n");
436
- if (mode !== "blog") return body;
437
- const excerpt = String(item.excerpt ?? "").trim();
438
- if (excerpt) return excerpt;
439
- return truncateWords(body, 80);
440
- }
441
-
442
- function parseItemAnchorHash(hash: string): string | null {
443
- if (!hash) return null;
444
- try {
445
- const value = decodeURIComponent(hash.slice(1)).trim();
446
- return /^[a-z0-9][a-z0-9-]*$/i.test(value) ? value : null;
447
- } catch {
448
- return null;
449
- }
450
- }
451
-
452
372
  function formatMastheadDate(value: string): string {
453
373
  const normalized = value?.trim();
454
374
  if (!normalized) return value;
@@ -461,17 +381,3 @@ function formatMastheadDate(value: string): string {
461
381
  }).format(date);
462
382
  }
463
383
 
464
- function getBlogFooterSectionHref(entry: PresentationFooterEntry, editionBasePath?: string): string {
465
- const anchor = `#${getSectionAnchorId(entry.sectionKey)}`;
466
- return editionBasePath ? `${editionBasePath}${anchor}` : anchor;
467
- }
468
-
469
- function handleBlogFooterSectionClick(
470
- event: ReactMouseEvent<HTMLAnchorElement>,
471
- entry: PresentationFooterEntry,
472
- href: string,
473
- ) {
474
- event.preventDefault();
475
- window.history.pushState(null, "", href);
476
- document.getElementById(getSectionAnchorId(entry.sectionKey))?.scrollIntoView({ block: "start" });
477
- }