@anthusai/papyrus 1.0.0-next.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/amplify/auth/redirect-urls.js +54 -0
  2. package/amplify/auth/redirect-urls.ts +66 -0
  3. package/amplify/auth/resource.js +20 -5
  4. package/amplify/auth/resource.ts +27 -5
  5. package/amplify/data/resource.js +42 -80
  6. package/amplify/data/resource.ts +42 -80
  7. package/amplify/data/schema.js +45 -60
  8. package/amplify/data/schema.ts +47 -60
  9. package/amplify/functions/category-action/handler.ts +5 -3
  10. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
  11. package/amplify/functions/console-chat-responder/resource.js +0 -7
  12. package/amplify/functions/console-chat-responder/resource.ts +0 -8
  13. package/amplify/functions/console-chat-responder/src/main.rs +0 -9
  14. package/amplify/functions/content-actions/handler.py +143 -1
  15. package/amplify/functions/content-actions/requirements.txt +2 -1
  16. package/amplify/functions/content-actions/resource.js +6 -1
  17. package/amplify/functions/content-actions/resource.ts +9 -2
  18. package/amplify/functions/renditions/handler.py +120 -0
  19. package/amplify/functions/renditions/requirements.txt +4 -0
  20. package/amplify/functions/shared/python-bundle.js +42 -2
  21. package/amplify/functions/shared/python-bundle.ts +47 -2
  22. package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
  23. package/amplify/inbound-email/ses-intake.js +86 -0
  24. package/amplify/inbound-email/ses-intake.ts +110 -0
  25. package/amplify/plugins/registry.js +19 -0
  26. package/amplify/plugins/registry.ts +23 -0
  27. package/amplify/plugins/signup/function/handler.py +200 -0
  28. package/amplify/plugins/signup/function/resource.js +27 -0
  29. package/amplify/plugins/signup/function/resource.ts +35 -0
  30. package/amplify/plugins/signup/papyrus.plugin.js +59 -0
  31. package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
  32. package/amplify/plugins/signup/schema.js +40 -0
  33. package/amplify/plugins/signup/schema.ts +41 -0
  34. package/amplify/plugins/signup/waf.js +48 -0
  35. package/amplify/plugins/signup/waf.ts +51 -0
  36. package/amplify/plugins/types.js +1 -0
  37. package/amplify/plugins/types.ts +39 -0
  38. package/amplify/plugins/wire.js +24 -0
  39. package/amplify/plugins/wire.ts +26 -0
  40. package/amplify/publication/default-publication.js +18 -0
  41. package/amplify/publication/default-publication.ts +20 -0
  42. package/amplify/publication/invalidation-alarm.js +26 -0
  43. package/amplify/publication/invalidation-alarm.ts +30 -0
  44. package/amplify/publication/redirect-and-index.js +37 -0
  45. package/amplify/publication/renditions-function.js +74 -0
  46. package/amplify/publication/renditions-function.ts +112 -0
  47. package/amplify/publication/renditions-settings.js +51 -0
  48. package/amplify/publication/renditions-settings.ts +58 -0
  49. package/amplify/publication/reproject-on-deploy.js +73 -0
  50. package/amplify/publication/reproject-on-deploy.ts +78 -0
  51. package/amplify/publication/s3-projection-settings.js +67 -0
  52. package/amplify/publication/s3-projection-settings.ts +101 -0
  53. package/amplify/publication/s3-projection.js +151 -0
  54. package/amplify/publication/s3-projection.ts +201 -0
  55. package/amplify/resolver-waves.js +26 -0
  56. package/amplify/resolver-waves.ts +34 -0
  57. package/amplify/revalidate-invalidation-settings.js +30 -0
  58. package/amplify/revalidate-invalidation-settings.ts +35 -0
  59. package/amplify/revalidate-secret-policy.js +13 -0
  60. package/amplify/revalidate-secret-policy.ts +19 -0
  61. package/amplify/site-backend-config.js +65 -1
  62. package/amplify/site-backend-config.ts +153 -2
  63. package/amplify/site-backend-names.js +103 -0
  64. package/amplify/site-backend-names.ts +166 -0
  65. package/amplify/site-backend.js +143 -176
  66. package/amplify/site-backend.ts +181 -201
  67. package/amplify/storage-backups/construct.js +31 -0
  68. package/amplify/storage-backups/construct.ts +47 -0
  69. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
  70. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
  71. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
  72. package/app/[year]/[month]/[day]/page.tsx +1 -1
  73. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
  74. package/app/api/archive/editions/route.ts +1 -1
  75. package/app/api/cyclotron/priming-proof/route.ts +11 -0
  76. package/app/api/media/[...path]/route.ts +5 -16
  77. package/app/api/revalidate/route.ts +15 -7
  78. package/app/archive/page.tsx +1 -1
  79. package/app/articles/[slug]/page.tsx +7 -5
  80. package/app/blog-rhythm.css +278 -0
  81. package/app/cyclotron/page.tsx +13 -0
  82. package/app/globals.css +2 -0
  83. package/app/layout.tsx +16 -0
  84. package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
  85. package/app/newsroom/[section]/page.tsx +6 -3
  86. package/app/newsroom/layout.tsx +4 -9
  87. package/app/newsroom/settings/page.tsx +6 -0
  88. package/app/newsroom/work/page.tsx +10 -0
  89. package/app/page.tsx +22 -8
  90. package/app/robots.ts +2 -5
  91. package/app/tailwind.css +1 -0
  92. package/bin/papyrus-infra.mjs +3 -3
  93. package/components/amplify-client-provider.tsx +5 -22
  94. package/components/archive-shell.tsx +2 -22
  95. package/components/article-video.tsx +152 -5
  96. package/components/blog-page-background.tsx +23 -6
  97. package/components/news-desk-page.tsx +4 -15
  98. package/components/news-desk-taxonomy-client.ts +90 -3
  99. package/components/newspaper.tsx +6 -2
  100. package/components/newsroom-access-gate.tsx +2 -1
  101. package/components/newsroom-board-view.tsx +99 -0
  102. package/components/newsroom-client-shell.tsx +12 -0
  103. package/components/newsroom-curation-provider.tsx +17 -0
  104. package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
  105. package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
  106. package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
  107. package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
  108. package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
  109. package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
  110. package/components/newsroom-newsprint-board.css +48 -0
  111. package/components/newsroom-ops-overview.tsx +28 -11
  112. package/components/newsroom-ops-shell.tsx +7 -1
  113. package/components/newsroom-organizer-shell.tsx +130 -0
  114. package/components/newsroom-organizer.css +63 -0
  115. package/components/newsroom-references-view.tsx +92 -8
  116. package/components/papyrus-console-shell.tsx +4 -2
  117. package/components/pictogram-figure.tsx +9 -2
  118. package/components/presentation-header.tsx +135 -0
  119. package/components/presentation-rhythm-hrule.tsx +17 -0
  120. package/components/priming-proof-page.tsx +92 -0
  121. package/components/publication-cyclotron-panel.tsx +57 -0
  122. package/components/reader-auth-control.tsx +8 -5
  123. package/components/reference-curation-controls.tsx +77 -0
  124. package/components/reference-cyclotron-status.tsx +109 -0
  125. package/components/reference-priming.tsx +18 -0
  126. package/components/reference-relevance-review.tsx +75 -0
  127. package/components/settings-page.tsx +2 -1
  128. package/components/signup-entries-panel.tsx +94 -0
  129. package/components/topic-steering-workspace.tsx +78 -6
  130. package/components/use-blog-header-obstacles.ts +81 -0
  131. package/components/use-brand-fonts-ready.ts +28 -0
  132. package/components/use-near-viewport.ts +30 -0
  133. package/components/use-rhythm-overlay.ts +29 -0
  134. package/infra/amplify-app-shell.js +219 -71
  135. package/infra/amplify-app-shell.ts +231 -77
  136. package/infra/build-specs.js +23 -8
  137. package/infra/build-specs.ts +27 -11
  138. package/infra/site-config.js +277 -34
  139. package/infra/site-config.ts +340 -43
  140. package/lib/amplify-outputs-path.ts +5 -1
  141. package/lib/amplify-server-runtime.ts +36 -1
  142. package/lib/approved-priming-proof-reports.ts +6 -0
  143. package/lib/blog-feature-solver.ts +459 -0
  144. package/lib/blog-rhythm.ts +134 -0
  145. package/lib/brand-fonts-watch.ts +43 -0
  146. package/lib/brand-fonts.ts +112 -0
  147. package/lib/cached-content-repository.ts +12 -0
  148. package/lib/category-repository.ts +43 -93
  149. package/lib/content-item-lifecycle.ts +55 -0
  150. package/lib/content-repository.ts +3 -0
  151. package/lib/content-types.ts +4 -0
  152. package/lib/curation-client.ts +61 -0
  153. package/lib/edition-routes.ts +31 -29
  154. package/lib/graphql-content-repository.ts +56 -22
  155. package/lib/media-route-response.ts +45 -0
  156. package/lib/newspaper-layout.ts +3 -2
  157. package/lib/newsroom-base-path.ts +12 -3
  158. package/lib/newsroom-board.ts +99 -0
  159. package/lib/newsroom-category-drilldown.ts +3 -2
  160. package/lib/newsroom-forum-routes.ts +6 -4
  161. package/lib/newsroom-index-filters.ts +14 -10
  162. package/lib/newsroom-nav.ts +3 -1
  163. package/lib/newsroom-organizer-nav.ts +37 -0
  164. package/lib/newsroom-section-routing.ts +20 -0
  165. package/lib/oauth-redirect-priority.ts +40 -0
  166. package/lib/papyrus-web-locations.ts +7 -6
  167. package/lib/plugin-settings.ts +11 -0
  168. package/lib/presentation-footer.ts +3 -2
  169. package/lib/pretext-layout.ts +12 -3
  170. package/lib/priming-proof-report.ts +89 -0
  171. package/lib/prospectus-site-identity.ts +303 -0
  172. package/lib/publication-cyclotron.ts +74 -0
  173. package/lib/rate-limit-retry.ts +55 -0
  174. package/lib/reader-base-path.ts +24 -0
  175. package/lib/reader-cache-lifetime.ts +5 -0
  176. package/lib/reader-static-params.ts +85 -4
  177. package/lib/reader-storage-url.ts +9 -12
  178. package/lib/reference-policy.ts +5 -0
  179. package/lib/relevance-decisions.ts +103 -0
  180. package/lib/relevance-status.ts +63 -0
  181. package/lib/renderer.ts +7 -0
  182. package/lib/revalidate-prewarm.ts +53 -0
  183. package/lib/revalidate-secret.ts +59 -0
  184. package/lib/robots-policy.ts +14 -0
  185. package/lib/root-route-redirect.ts +16 -0
  186. package/lib/semantic-graph.ts +7 -0
  187. package/lib/shared-credentials-provider.ts +61 -0
  188. package/lib/signup-csv.ts +24 -0
  189. package/lib/site-brand.ts +59 -0
  190. package/lib/staging-gated-path.ts +23 -0
  191. package/lib/staging-preview-object.ts +21 -5
  192. package/lib/test-editor-override.ts +4 -0
  193. package/lib/video-loading.ts +22 -0
  194. package/lib/video-script.ts +14 -0
  195. package/middleware.ts +11 -19
  196. package/package.json +64 -1
  197. package/renderers/pretext/article-page.tsx +39 -3
  198. package/renderers/pretext/presentation-shared.tsx +113 -0
  199. package/renderers/pretext/presentation-shell.tsx +15 -102
  200. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  201. package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
  202. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  203. package/routes.manifest.json +68 -12
  204. package/src/sync.mjs +30 -6
  205. package/src/with-papyrus.d.mts +9 -0
  206. package/src/with-papyrus.mjs +48 -3
  207. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  208. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  209. package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
@@ -10,6 +10,9 @@ import Link from "next/link";
10
10
  import { usePathname, useRouter } from "next/navigation";
11
11
  import type { ReactNode, RefObject } from "react";
12
12
  import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useTransition } from "react";
13
+ import amplifyOutputs from "papyrus-amplify-outputs";
14
+ import { SignupEntriesPanel } from "./signup-entries-panel";
15
+ import { pluginSettings, type PluginOutputs } from "../lib/plugin-settings";
13
16
  import { generateClient } from "aws-amplify/data";
14
17
  import type { Schema } from "../amplify/data/resource";
15
18
  import {
@@ -53,6 +56,8 @@ import {
53
56
  type ForumThreadWithMessages,
54
57
  type KnowledgeQueryResponse,
55
58
  type NewsroomRecordPage,
59
+ loadCyclotronStatus,
60
+ requestCyclotronReviewRate,
56
61
  } from "./news-desk-taxonomy-client";
57
62
  import { listConsoleThreads } from "../lib/console-chat-client";
58
63
  import {
@@ -76,6 +81,7 @@ import { buildNewsroomKnowledgeQueryInput, type NewsroomKnowledgeQueryAnchor as
76
81
  import { NewsroomConsoleProgressToggle, PapyrusConsoleChatIcon, usePapyrusConsole } from "./papyrus-console-shell";
77
82
  import { useResolvedPapyrusTheme } from "./use-resolved-papyrus-theme";
78
83
  import { SITE_BRAND } from "../lib/site-brand";
84
+ import { browserPathToInternalNewsroomPath, toPublicNewsroomPath } from "../lib/newsroom-base-path";
79
85
  import { getNewsroomNavHref } from "../lib/newsroom-nav";
80
86
  import { cn } from "../lib/utils";
81
87
  import { useOptionalNewsDeskClient } from "./news-desk-client-provider";
@@ -150,9 +156,13 @@ import {
150
156
  type SemanticObjectSummary,
151
157
  } from "../lib/semantic-graph";
152
158
  import {
159
+ normalizeReferenceRejectionReasonCode,
153
160
  REFERENCE_REJECTION_REASON_CODES,
154
161
  referenceCurationStatusForAction,
155
162
  } from "../lib/reference-policy";
163
+ import { currentRelevanceDecisions } from "../lib/relevance-decisions";
164
+ import { buildReviewRateRequest, DEMO_CYCLOTRON_STATUS } from "../lib/relevance-status";
165
+ import type { CyclotronStatus } from "cyclotron/cyclotron-status";
156
166
  import {
157
167
  referenceDisplaySummary,
158
168
  referenceMetadataField,
@@ -228,7 +238,7 @@ type AssignmentAction = "claim" | "release" | "complete" | "cancel" | "reopen" |
228
238
  type ReportingPacketReviewDecision = "select" | "merge" | "brief" | "hold" | "kill";
229
239
  type AssignmentDeskViewMode = "queue" | "budget";
230
240
  type UserRoleAction = "grant" | "revoke";
231
- type AdministrationPanel = "users" | "policies" | "sections" | "procedures";
241
+ type AdministrationPanel = "users" | "policies" | "sections" | "procedures" | "signups";
232
242
  export type NewsDeskTab = "overview" | "desks" | "topics" | "concepts" | "references" | "insights" | "messages" | "assignments" | "administration" | "search";
233
243
  type LexicalRuleScope = "publication" | "corpus" | "classifier" | "category";
234
244
  type AnalysisReindexMode = "online-update" | "classifier-retrain" | "scoped-topic-rebuild" | "entity-graph-rebuild" | "generated-analysis-rebuild";
@@ -775,6 +785,24 @@ function NewsDeskDashboard({
775
785
  const [referenceAttachments, setReferenceAttachments] = useState(dashboard.referenceAttachments);
776
786
  const [messages, setMessages] = useState(dashboard.messages);
777
787
  const [semanticRelations, setSemanticRelations] = useState(dashboard.semanticRelations);
788
+ const relevanceDecisions = useMemo(() => currentRelevanceDecisions(semanticRelations), [semanticRelations]);
789
+ const relevanceCyclotronId = useMemo(
790
+ () => [...relevanceDecisions.values()].find((decision) => decision.cyclotronId)?.cyclotronId ?? null,
791
+ [relevanceDecisions],
792
+ );
793
+ const [cyclotronStatus, setCyclotronStatus] = useState<CyclotronStatus | null>(null);
794
+ useEffect(() => {
795
+ if (!relevanceCyclotronId) return;
796
+ if (dashboard.isDemo) {
797
+ setCyclotronStatus(DEMO_CYCLOTRON_STATUS);
798
+ return;
799
+ }
800
+ let cancelled = false;
801
+ loadCyclotronStatus(relevanceCyclotronId)
802
+ .then((status) => { if (!cancelled) setCyclotronStatus(status); })
803
+ .catch(() => { if (!cancelled) setCyclotronStatus(null); });
804
+ return () => { cancelled = true; };
805
+ }, [dashboard.isDemo, relevanceCyclotronId]);
778
806
  const [semanticNodes, setSemanticNodes] = useState(dashboard.semanticNodes);
779
807
  const [assignments, setAssignments] = useState(dashboard.assignments);
780
808
  const [assignmentEvents, setAssignmentEvents] = useState(dashboard.assignmentEvents);
@@ -1506,7 +1534,13 @@ function NewsDeskDashboard({
1506
1534
  });
1507
1535
  }
1508
1536
 
1509
- function runReferenceCurationAction(reference: ReferenceRecord, action: ReferenceCurationAction, note?: string, reasonCode?: ReferenceRejectionReasonCode | null) {
1537
+ function runReferenceCurationAction(
1538
+ reference: ReferenceRecord,
1539
+ action: ReferenceCurationAction,
1540
+ note?: string,
1541
+ reasonCode?: ReferenceRejectionReasonCode | null,
1542
+ relevance?: { decisionRelationId: string; shareable: boolean },
1543
+ ) {
1510
1544
  const nextStatus = referenceCurationStatusForAction(action);
1511
1545
  setActionState({ id: reference.id, message: `${action} pending`, tone: "pending" });
1512
1546
  if (dashboard.isDemo) {
@@ -1534,6 +1568,7 @@ function NewsDeskDashboard({
1534
1568
  action,
1535
1569
  curationStatus: nextStatus,
1536
1570
  reasonCode: action === "reject" ? reasonCode ?? null : null,
1571
+ ...(relevance ? { decisionRelationId: relevance.decisionRelationId, shareable: relevance.shareable } : {}),
1537
1572
  },
1538
1573
  }, ...current]);
1539
1574
  setSemanticRelations((current) => [{
@@ -1574,6 +1609,8 @@ function NewsDeskDashboard({
1574
1609
  actorLabel: authState.label,
1575
1610
  note: note?.trim() || undefined,
1576
1611
  reasonCode: action === "reject" ? reasonCode ?? undefined : undefined,
1612
+ decisionRelationId: relevance?.decisionRelationId,
1613
+ shareable: relevance ? relevance.shareable : undefined,
1577
1614
  },
1578
1615
  { authMode: USER_POOL_AUTH_MODE },
1579
1616
  );
@@ -3152,6 +3189,20 @@ function NewsDeskDashboard({
3152
3189
  demo={Boolean(dashboard.isDemo)}
3153
3190
  disabled={controlsDisabled}
3154
3191
  initialReferenceLineageId={initialSelection.reference ?? pathnameReferenceLineageId}
3192
+ cyclotronStatus={cyclotronStatus}
3193
+ decisions={relevanceDecisions}
3194
+ onRequestReviewRate={relevanceCyclotronId ? async (choice) => {
3195
+ const request = buildReviewRateRequest(choice, authState.label);
3196
+ if (dashboard.isDemo) return;
3197
+ await requestCyclotronReviewRate(relevanceCyclotronId, request);
3198
+ } : undefined}
3199
+ onRelevanceReview={(reference, curation) => runReferenceCurationAction(
3200
+ reference,
3201
+ curation.action,
3202
+ curation.note ?? undefined,
3203
+ normalizeReferenceRejectionReasonCode(curation.reasonCode),
3204
+ { decisionRelationId: curation.decisionRelationId, shareable: curation.shareable },
3205
+ )}
3155
3206
  onReview={(reference, action) => runReferenceCurationAction(reference, action)}
3156
3207
  referenceAttachments={referenceAttachments}
3157
3208
  references={references}
@@ -4404,8 +4455,20 @@ function AdministrationDeskView({
4404
4455
  <strong>Procedures</strong>
4405
4456
  <span>Procedure registry and versioned Tactus code</span>
4406
4457
  </Link>
4458
+ {signupPluginEnabled() ? (
4459
+ <Link
4460
+ href={administrationPanelHref("signups", isDemo)}
4461
+ data-news-desk-admin-nav="signups"
4462
+ data-active={administrationPanel === "signups" || undefined}
4463
+ onClick={() => onPanelChange("signups")}
4464
+ >
4465
+ <strong>Signups</strong>
4466
+ <span>Addresses from the site&apos;s signup forms</span>
4467
+ </Link>
4468
+ ) : null}
4407
4469
  </nav>
4408
4470
  <div className="news-desk-settings-panel">
4471
+ {administrationPanel === "signups" ? <SignupEntriesPanel /> : null}
4409
4472
  {administrationPanel === "users" ? (
4410
4473
  <AdministrationUsersPanel
4411
4474
  canManageUsers={canManageUsers}
@@ -13598,8 +13661,13 @@ function semanticVersionKey(kind: string, id: string): string {
13598
13661
  return `${kind}#${id}`;
13599
13662
  }
13600
13663
 
13664
+ function signupPluginEnabled(): boolean {
13665
+ return pluginSettings(amplifyOutputs as PluginOutputs, "signup") !== null;
13666
+ }
13667
+
13601
13668
  function normalizeAdministrationPanel(value: string | null | undefined): AdministrationPanel {
13602
13669
  if (value === "policies" || value === "sections" || value === "procedures") return value;
13670
+ if (value === "signups" && signupPluginEnabled()) return value;
13603
13671
  return "users";
13604
13672
  }
13605
13673
 
@@ -14974,6 +15042,9 @@ function buildUiCopywritingAssignment({ actorLabel, assignment, decision, messag
14974
15042
  message: MessageRecord;
14975
15043
  now: string;
14976
15044
  }): AssignmentRecord {
15045
+ if (!assignment.itemLineageId) {
15046
+ throw new Error(`Assignment ${assignment.id} has no content item lineage; copywriting must continue the same content item.`);
15047
+ }
14977
15048
  const type = decision === "brief" ? "brief" : "article";
14978
15049
  const assignmentTypeKey = type === "brief" ? "copywriting.brief-draft" : "copywriting.article-draft";
14979
15050
  const section = assignment.sectionKey ?? assignment.sectionId ?? "unsectioned";
@@ -15026,6 +15097,7 @@ function buildUiCopywritingAssignment({ actorLabel, assignment, decision, messag
15026
15097
  createdAt: now,
15027
15098
  updatedAt: now,
15028
15099
  newsroomFeedKey: "assignment#open",
15100
+ itemLineageId: assignment.itemLineageId,
15029
15101
  metadata,
15030
15102
  };
15031
15103
  }
@@ -15187,14 +15259,14 @@ function normalizeKnowledgeQueryAnchorKind(value: string | null | undefined): Kn
15187
15259
 
15188
15260
  function normalizeNewsroomFromHref(value: string | null | undefined): string | null {
15189
15261
  if (!value) return null;
15190
- const trimmed = value.trim();
15262
+ const trimmed = browserPathToInternalNewsroomPath(value.trim());
15191
15263
  if (!trimmed.startsWith("/newsroom")) return null;
15192
- return trimmed;
15264
+ return toPublicNewsroomPath(trimmed);
15193
15265
  }
15194
15266
 
15195
15267
  function currentNewsroomOriginHref(fallback: string | null): string | null {
15196
15268
  if (typeof window === "undefined") return fallback;
15197
- const current = `${window.location.pathname}${window.location.search}`;
15269
+ const current = browserPathToInternalNewsroomPath(`${window.location.pathname}${window.location.search}`);
15198
15270
  return current.startsWith("/newsroom/search") ? fallback : normalizeNewsroomFromHref(current);
15199
15271
  }
15200
15272
 
@@ -15228,7 +15300,7 @@ function buildNewsroomSearchHref(request: NewsroomSearchRequest, demo?: boolean)
15228
15300
  if (request.from) params.set("from", request.from);
15229
15301
  if (demo) params.set("demo", "1");
15230
15302
  const queryString = params.toString();
15231
- return queryString ? `/newsroom/search?${queryString}` : "/newsroom/search";
15303
+ return toPublicNewsroomPath(queryString ? `/newsroom/search?${queryString}` : "/newsroom/search");
15232
15304
  }
15233
15305
 
15234
15306
  function focusNewsroomSearchForm() {
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, type RefObject } from "react";
4
+ import type { BlogHeaderObstacle } from "./blog-page-background";
5
+
6
+ const HEADER_OBSTACLE_SELECTORS = [
7
+ ".presentation-header__eyebrow",
8
+ ".presentation-header__word",
9
+ ".presentation-header__meta",
10
+ ".presentation-header__subtitle",
11
+ ".presentation-header__date",
12
+ ".presentation-header__tagline",
13
+ ".presentation-section-nav a",
14
+ ];
15
+
16
+ function measureObstacleRect(element: HTMLElement): DOMRect {
17
+ const range = document.createRange();
18
+ range.selectNodeContents(element);
19
+ const contentRect = range.getBoundingClientRect();
20
+ range.detach();
21
+ if (contentRect.width > 0 && contentRect.height > 0) return contentRect;
22
+ return element.getBoundingClientRect();
23
+ }
24
+
25
+ /**
26
+ * Measures the rendered header text (eyebrow, masthead words, meta, tagline,
27
+ * section links) as rectangles in the coordinates of `.blog-page-background`,
28
+ * so a brand's background artwork can keep clear of them. Block-level header
29
+ * elements span the full page width even when their text ends mid-line, so
30
+ * the text range is measured instead of the element box.
31
+ */
32
+ export function useBlogHeaderObstacles(
33
+ pageRef: RefObject<HTMLElement | null>,
34
+ paintBuffer: number,
35
+ ): BlogHeaderObstacle[] {
36
+ const [obstacles, setObstacles] = useState<BlogHeaderObstacle[]>([]);
37
+
38
+ useEffect(() => {
39
+ const page = pageRef.current;
40
+ if (!page) return;
41
+
42
+ const update = () => {
43
+ const background = page.querySelector<HTMLElement>(".blog-page-background");
44
+ const backgroundRect = background?.getBoundingClientRect();
45
+ if (!backgroundRect) return;
46
+
47
+ const next: BlogHeaderObstacle[] = [];
48
+ for (const selector of HEADER_OBSTACLE_SELECTORS) {
49
+ for (const element of Array.from(page.querySelectorAll<HTMLElement>(selector))) {
50
+ const rect = measureObstacleRect(element);
51
+ const intersectionTop = Math.max(rect.top, backgroundRect.top);
52
+ const intersectionBottom = Math.min(rect.bottom, backgroundRect.bottom);
53
+ const intersectionLeft = Math.max(rect.left, backgroundRect.left);
54
+ const intersectionRight = Math.min(rect.right, backgroundRect.right);
55
+ const width = intersectionRight - intersectionLeft;
56
+ const height = intersectionBottom - intersectionTop;
57
+ if (width <= 0 || height <= 0) continue;
58
+ next.push({
59
+ x: intersectionLeft - backgroundRect.left,
60
+ y: intersectionTop - backgroundRect.top,
61
+ width,
62
+ height: height + paintBuffer * 2,
63
+ });
64
+ }
65
+ }
66
+ setObstacles(next);
67
+ };
68
+
69
+ update();
70
+ const observer = new ResizeObserver(update);
71
+ observer.observe(page);
72
+ window.addEventListener("resize", update);
73
+ void document.fonts?.ready.then(update);
74
+ return () => {
75
+ observer.disconnect();
76
+ window.removeEventListener("resize", update);
77
+ };
78
+ }, [pageRef, paintBuffer]);
79
+
80
+ return obstacles;
81
+ }
@@ -0,0 +1,28 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { listBrandFontFamilies } from "../lib/brand-fonts";
5
+ import { watchBrandFontsLoaded } from "../lib/brand-fonts-watch";
6
+ import { clearPretextMeasurementCache } from "../lib/pretext-layout";
7
+ import { SITE_BRAND } from "../lib/site-brand";
8
+
9
+ /**
10
+ * Returns a number that increments each time the brand's declared font
11
+ * families finish loading. Pretext's width cache is cleared first, so
12
+ * measurements taken with fallback metrics are re-run with the real glyph
13
+ * widths. Stays 0 for brands without a font slot, and on the server.
14
+ */
15
+ export function useBrandFontsReadyVersion(): number {
16
+ const [version, setVersion] = useState(0);
17
+
18
+ useEffect(() => {
19
+ return watchBrandFontsLoaded({
20
+ fontFaceSet: typeof document === "undefined" ? undefined : document.fonts,
21
+ families: listBrandFontFamilies(SITE_BRAND.fonts),
22
+ clearMeasurementCaches: clearPretextMeasurementCache,
23
+ onFontsSettled: () => setVersion((current) => current + 1),
24
+ });
25
+ }, []);
26
+
27
+ return version;
28
+ }
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, type RefObject } from "react";
4
+
5
+ export function useNearViewport(targetRef: RefObject<Element | null>, rootMargin: string): boolean {
6
+ const [isNearViewport, setIsNearViewport] = useState(false);
7
+
8
+ useEffect(() => {
9
+ if (isNearViewport) return;
10
+ const target = targetRef.current;
11
+ if (!target) return;
12
+ if (typeof IntersectionObserver === "undefined") {
13
+ setIsNearViewport(true);
14
+ return;
15
+ }
16
+ const observer = new IntersectionObserver(
17
+ (entries) => {
18
+ if (entries.some((entry) => entry.isIntersecting)) {
19
+ setIsNearViewport(true);
20
+ observer.disconnect();
21
+ }
22
+ },
23
+ { rootMargin },
24
+ );
25
+ observer.observe(target);
26
+ return () => observer.disconnect();
27
+ }, [isNearViewport, rootMargin, targetRef]);
28
+
29
+ return isNearViewport;
30
+ }
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+
5
+ export function useRhythmOverlay(): boolean {
6
+ const [showRhythmOverlay, setShowRhythmOverlay] = useState(false);
7
+
8
+ useEffect(() => {
9
+ const handleKeyDown = (event: KeyboardEvent) => {
10
+ if (isEditableEventTarget(event.target)) return;
11
+ if ((event.key === "=" || event.code === "Equal") && event.ctrlKey && !event.metaKey && !event.altKey) {
12
+ event.preventDefault();
13
+ setShowRhythmOverlay((current) => !current);
14
+ }
15
+ };
16
+
17
+ window.addEventListener("keydown", handleKeyDown);
18
+ return () => window.removeEventListener("keydown", handleKeyDown);
19
+ }, []);
20
+
21
+ return showRhythmOverlay;
22
+ }
23
+
24
+ function isEditableEventTarget(target: EventTarget | null): boolean {
25
+ if (!(target instanceof HTMLElement)) return false;
26
+ const tagName = target.tagName.toLowerCase();
27
+ if (tagName === "input" || tagName === "textarea" || tagName === "select") return true;
28
+ return target.isContentEditable || target.closest("[contenteditable='true']") !== null;
29
+ }