@anthusai/papyrus 1.0.0-next.9 → 1.1.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 (222) hide show
  1. package/amplify/auth/prospectus-google-client.js +66 -0
  2. package/amplify/auth/prospectus-google-client.ts +71 -0
  3. package/amplify/auth/redirect-urls.js +54 -0
  4. package/amplify/auth/redirect-urls.ts +66 -0
  5. package/amplify/auth/resource.js +23 -5
  6. package/amplify/auth/resource.ts +31 -5
  7. package/amplify/data/resource.js +42 -80
  8. package/amplify/data/resource.ts +42 -80
  9. package/amplify/data/schema.js +45 -62
  10. package/amplify/data/schema.ts +47 -62
  11. package/amplify/functions/category-action/handler.ts +5 -3
  12. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
  13. package/amplify/functions/console-chat-responder/resource.js +0 -7
  14. package/amplify/functions/console-chat-responder/resource.ts +0 -8
  15. package/amplify/functions/console-chat-responder/src/main.rs +0 -9
  16. package/amplify/functions/content-actions/handler.py +143 -1
  17. package/amplify/functions/content-actions/requirements.txt +2 -1
  18. package/amplify/functions/content-actions/resource.js +6 -1
  19. package/amplify/functions/content-actions/resource.ts +9 -2
  20. package/amplify/functions/email-submission-processor/handler.py +3 -1
  21. package/amplify/functions/renditions/handler.py +120 -0
  22. package/amplify/functions/renditions/requirements.txt +4 -0
  23. package/amplify/functions/ses-inbound-receive/handler.ts +32 -4
  24. package/amplify/functions/shared/email-submission.ts +109 -1
  25. package/amplify/functions/shared/python-bundle.js +42 -2
  26. package/amplify/functions/shared/python-bundle.ts +47 -2
  27. package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
  28. package/amplify/inbound-email/ses-intake.js +86 -0
  29. package/amplify/inbound-email/ses-intake.ts +110 -0
  30. package/amplify/plugins/registry.js +19 -0
  31. package/amplify/plugins/registry.ts +23 -0
  32. package/amplify/plugins/signup/function/handler.py +200 -0
  33. package/amplify/plugins/signup/function/resource.js +27 -0
  34. package/amplify/plugins/signup/function/resource.ts +35 -0
  35. package/amplify/plugins/signup/papyrus.plugin.js +59 -0
  36. package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
  37. package/amplify/plugins/signup/schema.js +40 -0
  38. package/amplify/plugins/signup/schema.ts +41 -0
  39. package/amplify/plugins/signup/waf.js +48 -0
  40. package/amplify/plugins/signup/waf.ts +51 -0
  41. package/amplify/plugins/types.js +1 -0
  42. package/amplify/plugins/types.ts +39 -0
  43. package/amplify/plugins/wire.js +24 -0
  44. package/amplify/plugins/wire.ts +26 -0
  45. package/amplify/publication/default-publication.js +18 -0
  46. package/amplify/publication/default-publication.ts +20 -0
  47. package/amplify/publication/invalidation-alarm.js +26 -0
  48. package/amplify/publication/invalidation-alarm.ts +30 -0
  49. package/amplify/publication/redirect-and-index.js +37 -0
  50. package/amplify/publication/renditions-function.js +74 -0
  51. package/amplify/publication/renditions-function.ts +112 -0
  52. package/amplify/publication/renditions-settings.js +51 -0
  53. package/amplify/publication/renditions-settings.ts +58 -0
  54. package/amplify/publication/reproject-on-deploy.js +73 -0
  55. package/amplify/publication/reproject-on-deploy.ts +78 -0
  56. package/amplify/publication/s3-projection-settings.js +67 -0
  57. package/amplify/publication/s3-projection-settings.ts +101 -0
  58. package/amplify/publication/s3-projection.js +151 -0
  59. package/amplify/publication/s3-projection.ts +201 -0
  60. package/amplify/resolver-waves.js +26 -0
  61. package/amplify/resolver-waves.ts +34 -0
  62. package/amplify/revalidate-invalidation-settings.js +30 -0
  63. package/amplify/revalidate-invalidation-settings.ts +35 -0
  64. package/amplify/revalidate-secret-policy.js +13 -0
  65. package/amplify/revalidate-secret-policy.ts +19 -0
  66. package/amplify/site-backend-config.js +65 -1
  67. package/amplify/site-backend-config.ts +153 -2
  68. package/amplify/site-backend-names.js +103 -0
  69. package/amplify/site-backend-names.ts +166 -0
  70. package/amplify/site-backend.js +150 -176
  71. package/amplify/site-backend.ts +189 -201
  72. package/amplify/storage-backups/construct.js +31 -0
  73. package/amplify/storage-backups/construct.ts +47 -0
  74. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
  75. package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
  76. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
  77. package/app/[year]/[month]/[day]/page.tsx +1 -1
  78. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
  79. package/app/api/archive/editions/route.ts +1 -1
  80. package/app/api/auth/prospectus/callback/route.ts +9 -0
  81. package/app/api/auth/prospectus/selection/route.ts +9 -0
  82. package/app/api/auth/prospectus/session/route.ts +9 -0
  83. package/app/api/auth/prospectus/sign-in/route.ts +9 -0
  84. package/app/api/auth/prospectus/sign-out/route.ts +9 -0
  85. package/app/api/cyclotron/priming-proof/route.ts +11 -0
  86. package/app/api/media/[...path]/route.ts +5 -16
  87. package/app/api/revalidate/route.ts +15 -7
  88. package/app/archive/page.tsx +1 -1
  89. package/app/articles/[slug]/page.tsx +7 -5
  90. package/app/blog-rhythm.css +278 -0
  91. package/app/cyclotron/page.tsx +13 -0
  92. package/app/globals.css +2 -0
  93. package/app/layout.tsx +16 -0
  94. package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
  95. package/app/newsroom/[section]/page.tsx +6 -3
  96. package/app/newsroom/layout.tsx +4 -9
  97. package/app/newsroom/settings/page.tsx +6 -0
  98. package/app/newsroom/work/page.tsx +10 -0
  99. package/app/page.tsx +22 -8
  100. package/app/robots.ts +2 -5
  101. package/app/tailwind.css +1 -6
  102. package/components/amplify-client-provider.tsx +5 -22
  103. package/components/archive-shell.tsx +2 -22
  104. package/components/article-video.tsx +152 -5
  105. package/components/blog-page-background.tsx +23 -6
  106. package/components/news-desk-page.tsx +4 -15
  107. package/components/news-desk-taxonomy-client.ts +90 -3
  108. package/components/newspaper.tsx +6 -2
  109. package/components/newsroom-access-gate.tsx +2 -1
  110. package/components/newsroom-board-view.tsx +99 -0
  111. package/components/newsroom-client-shell.tsx +12 -0
  112. package/components/newsroom-curation-provider.tsx +17 -0
  113. package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
  114. package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
  115. package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
  116. package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
  117. package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
  118. package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
  119. package/components/newsroom-newsprint-board.css +48 -0
  120. package/components/newsroom-ops-overview.tsx +28 -11
  121. package/components/newsroom-ops-shell.tsx +7 -1
  122. package/components/newsroom-organizer-shell.tsx +130 -0
  123. package/components/newsroom-organizer.css +63 -0
  124. package/components/newsroom-references-view.tsx +92 -8
  125. package/components/papyrus-console-shell.tsx +4 -2
  126. package/components/pictogram-figure.tsx +9 -2
  127. package/components/presentation-header.tsx +135 -0
  128. package/components/presentation-rhythm-hrule.tsx +17 -0
  129. package/components/priming-proof-page.tsx +94 -0
  130. package/components/publication-cyclotron-panel.tsx +57 -0
  131. package/components/reader-auth-control.tsx +8 -5
  132. package/components/reference-curation-controls.tsx +77 -0
  133. package/components/reference-cyclotron-status.tsx +109 -0
  134. package/components/reference-priming.tsx +18 -0
  135. package/components/reference-relevance-review.tsx +75 -0
  136. package/components/settings-page.tsx +2 -1
  137. package/components/signup-entries-panel.tsx +94 -0
  138. package/components/topic-steering-workspace.tsx +78 -8
  139. package/components/use-blog-header-obstacles.ts +81 -0
  140. package/components/use-brand-fonts-ready.ts +28 -0
  141. package/components/use-near-viewport.ts +30 -0
  142. package/components/use-rhythm-overlay.ts +29 -0
  143. package/infra/amplify-app-shell.js +166 -27
  144. package/infra/amplify-app-shell.ts +180 -31
  145. package/infra/build-specs.js +23 -7
  146. package/infra/build-specs.ts +27 -10
  147. package/infra/site-config.js +248 -36
  148. package/infra/site-config.ts +313 -51
  149. package/lib/amplify-outputs-path.ts +5 -1
  150. package/lib/amplify-server-runtime.ts +36 -1
  151. package/lib/approved-priming-proof-reports.ts +6 -0
  152. package/lib/blog-feature-solver.ts +459 -0
  153. package/lib/blog-rhythm.ts +134 -0
  154. package/lib/brand-fonts-watch.ts +43 -0
  155. package/lib/brand-fonts.ts +112 -0
  156. package/lib/cached-content-repository.ts +12 -0
  157. package/lib/category-repository.ts +43 -93
  158. package/lib/content-item-lifecycle.ts +55 -0
  159. package/lib/content-repository.ts +3 -0
  160. package/lib/content-types.ts +4 -0
  161. package/lib/curation-client.ts +61 -0
  162. package/lib/edition-routes.ts +31 -29
  163. package/lib/graphql-content-repository.ts +56 -22
  164. package/lib/media-route-response.ts +45 -0
  165. package/lib/newspaper-layout.ts +3 -2
  166. package/lib/newsroom-base-path.ts +12 -3
  167. package/lib/newsroom-board.ts +99 -0
  168. package/lib/newsroom-category-drilldown.ts +3 -2
  169. package/lib/newsroom-forum-routes.ts +6 -4
  170. package/lib/newsroom-index-filters.ts +14 -10
  171. package/lib/newsroom-nav.ts +3 -1
  172. package/lib/newsroom-organizer-nav.ts +37 -0
  173. package/lib/newsroom-section-routing.ts +20 -0
  174. package/lib/oauth-redirect-priority.ts +40 -0
  175. package/lib/papyrus-web-locations.ts +7 -6
  176. package/lib/plugin-settings.ts +11 -0
  177. package/lib/presentation-footer.ts +3 -2
  178. package/lib/pretext-layout.ts +12 -3
  179. package/lib/priming-proof-report.ts +90 -0
  180. package/lib/prospectus-publication-access.ts +53 -0
  181. package/lib/prospectus-session.ts +196 -0
  182. package/lib/prospectus-sign-in-routes.ts +284 -0
  183. package/lib/prospectus-site-identity.ts +303 -0
  184. package/lib/publication-cyclotron.ts +74 -0
  185. package/lib/rate-limit-retry.ts +55 -0
  186. package/lib/reader-base-path.ts +24 -0
  187. package/lib/reader-cache-lifetime.ts +5 -0
  188. package/lib/reader-static-params.ts +85 -4
  189. package/lib/reader-storage-url.ts +9 -12
  190. package/lib/reference-policy.ts +5 -0
  191. package/lib/relevance-decisions.ts +103 -0
  192. package/lib/relevance-status.ts +63 -0
  193. package/lib/renderer.ts +7 -0
  194. package/lib/revalidate-prewarm.ts +53 -0
  195. package/lib/revalidate-secret.ts +59 -0
  196. package/lib/robots-policy.ts +14 -0
  197. package/lib/root-route-redirect.ts +16 -0
  198. package/lib/semantic-graph.ts +7 -0
  199. package/lib/shared-credentials-provider.ts +61 -0
  200. package/lib/signup-csv.ts +24 -0
  201. package/lib/site-brand.ts +59 -0
  202. package/lib/staging-gated-path.ts +23 -0
  203. package/lib/staging-preview-object.ts +21 -5
  204. package/lib/test-editor-override.ts +4 -0
  205. package/lib/video-loading.ts +22 -0
  206. package/lib/video-script.ts +14 -0
  207. package/middleware.ts +11 -19
  208. package/package.json +73 -8
  209. package/renderers/pretext/article-page.tsx +39 -3
  210. package/renderers/pretext/presentation-shared.tsx +113 -0
  211. package/renderers/pretext/presentation-shell.tsx +15 -102
  212. package/renderers/pretext/rhythm-article-page.tsx +165 -0
  213. package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
  214. package/renderers/pretext/rhythm-shell-style.ts +35 -0
  215. package/routes.manifest.json +123 -12
  216. package/src/sync.mjs +30 -6
  217. package/src/with-papyrus.d.mts +9 -0
  218. package/src/with-papyrus.mjs +48 -3
  219. package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
  220. package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
  221. package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
  222. package/components/ai-elements/message.tsx +0 -360
@@ -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";
@@ -253,7 +263,6 @@ type TopicProposalEditState = {
253
263
  };
254
264
  type AnalysisCommandPlanEntry = {
255
265
  label: string;
256
- cwd: string;
257
266
  executable: string;
258
267
  args: string[];
259
268
  metadata?: Record<string, unknown>;
@@ -775,6 +784,24 @@ function NewsDeskDashboard({
775
784
  const [referenceAttachments, setReferenceAttachments] = useState(dashboard.referenceAttachments);
776
785
  const [messages, setMessages] = useState(dashboard.messages);
777
786
  const [semanticRelations, setSemanticRelations] = useState(dashboard.semanticRelations);
787
+ const relevanceDecisions = useMemo(() => currentRelevanceDecisions(semanticRelations), [semanticRelations]);
788
+ const relevanceCyclotronId = useMemo(
789
+ () => [...relevanceDecisions.values()].find((decision) => decision.cyclotronId)?.cyclotronId ?? null,
790
+ [relevanceDecisions],
791
+ );
792
+ const [cyclotronStatus, setCyclotronStatus] = useState<CyclotronStatus | null>(null);
793
+ useEffect(() => {
794
+ if (!relevanceCyclotronId) return;
795
+ if (dashboard.isDemo) {
796
+ setCyclotronStatus(DEMO_CYCLOTRON_STATUS);
797
+ return;
798
+ }
799
+ let cancelled = false;
800
+ loadCyclotronStatus(relevanceCyclotronId)
801
+ .then((status) => { if (!cancelled) setCyclotronStatus(status); })
802
+ .catch(() => { if (!cancelled) setCyclotronStatus(null); });
803
+ return () => { cancelled = true; };
804
+ }, [dashboard.isDemo, relevanceCyclotronId]);
778
805
  const [semanticNodes, setSemanticNodes] = useState(dashboard.semanticNodes);
779
806
  const [assignments, setAssignments] = useState(dashboard.assignments);
780
807
  const [assignmentEvents, setAssignmentEvents] = useState(dashboard.assignmentEvents);
@@ -1506,7 +1533,13 @@ function NewsDeskDashboard({
1506
1533
  });
1507
1534
  }
1508
1535
 
1509
- function runReferenceCurationAction(reference: ReferenceRecord, action: ReferenceCurationAction, note?: string, reasonCode?: ReferenceRejectionReasonCode | null) {
1536
+ function runReferenceCurationAction(
1537
+ reference: ReferenceRecord,
1538
+ action: ReferenceCurationAction,
1539
+ note?: string,
1540
+ reasonCode?: ReferenceRejectionReasonCode | null,
1541
+ relevance?: { decisionRelationId: string; shareable: boolean },
1542
+ ) {
1510
1543
  const nextStatus = referenceCurationStatusForAction(action);
1511
1544
  setActionState({ id: reference.id, message: `${action} pending`, tone: "pending" });
1512
1545
  if (dashboard.isDemo) {
@@ -1534,6 +1567,7 @@ function NewsDeskDashboard({
1534
1567
  action,
1535
1568
  curationStatus: nextStatus,
1536
1569
  reasonCode: action === "reject" ? reasonCode ?? null : null,
1570
+ ...(relevance ? { decisionRelationId: relevance.decisionRelationId, shareable: relevance.shareable } : {}),
1537
1571
  },
1538
1572
  }, ...current]);
1539
1573
  setSemanticRelations((current) => [{
@@ -1574,6 +1608,8 @@ function NewsDeskDashboard({
1574
1608
  actorLabel: authState.label,
1575
1609
  note: note?.trim() || undefined,
1576
1610
  reasonCode: action === "reject" ? reasonCode ?? undefined : undefined,
1611
+ decisionRelationId: relevance?.decisionRelationId,
1612
+ shareable: relevance ? relevance.shareable : undefined,
1577
1613
  },
1578
1614
  { authMode: USER_POOL_AUTH_MODE },
1579
1615
  );
@@ -3152,6 +3188,20 @@ function NewsDeskDashboard({
3152
3188
  demo={Boolean(dashboard.isDemo)}
3153
3189
  disabled={controlsDisabled}
3154
3190
  initialReferenceLineageId={initialSelection.reference ?? pathnameReferenceLineageId}
3191
+ cyclotronStatus={cyclotronStatus}
3192
+ decisions={relevanceDecisions}
3193
+ onRequestReviewRate={relevanceCyclotronId ? async (choice) => {
3194
+ const request = buildReviewRateRequest(choice, authState.label);
3195
+ if (dashboard.isDemo) return;
3196
+ await requestCyclotronReviewRate(relevanceCyclotronId, request);
3197
+ } : undefined}
3198
+ onRelevanceReview={(reference, curation) => runReferenceCurationAction(
3199
+ reference,
3200
+ curation.action,
3201
+ curation.note ?? undefined,
3202
+ normalizeReferenceRejectionReasonCode(curation.reasonCode),
3203
+ { decisionRelationId: curation.decisionRelationId, shareable: curation.shareable },
3204
+ )}
3155
3205
  onReview={(reference, action) => runReferenceCurationAction(reference, action)}
3156
3206
  referenceAttachments={referenceAttachments}
3157
3207
  references={references}
@@ -4404,8 +4454,20 @@ function AdministrationDeskView({
4404
4454
  <strong>Procedures</strong>
4405
4455
  <span>Procedure registry and versioned Tactus code</span>
4406
4456
  </Link>
4457
+ {signupPluginEnabled() ? (
4458
+ <Link
4459
+ href={administrationPanelHref("signups", isDemo)}
4460
+ data-news-desk-admin-nav="signups"
4461
+ data-active={administrationPanel === "signups" || undefined}
4462
+ onClick={() => onPanelChange("signups")}
4463
+ >
4464
+ <strong>Signups</strong>
4465
+ <span>Addresses from the site&apos;s signup forms</span>
4466
+ </Link>
4467
+ ) : null}
4407
4468
  </nav>
4408
4469
  <div className="news-desk-settings-panel">
4470
+ {administrationPanel === "signups" ? <SignupEntriesPanel /> : null}
4409
4471
  {administrationPanel === "users" ? (
4410
4472
  <AdministrationUsersPanel
4411
4473
  canManageUsers={canManageUsers}
@@ -13544,7 +13606,6 @@ function buildAnalysisDestructivePreview({
13544
13606
  function analysisCommand(label: string, args: string[], mode: AnalysisReindexMode): AnalysisCommandPlanEntry {
13545
13607
  return {
13546
13608
  label,
13547
- cwd: "/Users/ryan/Projects/Biblicus",
13548
13609
  executable: "uv",
13549
13610
  args: ["run", "--extra", "topic-modeling", "biblicus", ...args],
13550
13611
  metadata: { mode },
@@ -13598,8 +13659,13 @@ function semanticVersionKey(kind: string, id: string): string {
13598
13659
  return `${kind}#${id}`;
13599
13660
  }
13600
13661
 
13662
+ function signupPluginEnabled(): boolean {
13663
+ return pluginSettings(amplifyOutputs as PluginOutputs, "signup") !== null;
13664
+ }
13665
+
13601
13666
  function normalizeAdministrationPanel(value: string | null | undefined): AdministrationPanel {
13602
13667
  if (value === "policies" || value === "sections" || value === "procedures") return value;
13668
+ if (value === "signups" && signupPluginEnabled()) return value;
13603
13669
  return "users";
13604
13670
  }
13605
13671
 
@@ -14974,6 +15040,9 @@ function buildUiCopywritingAssignment({ actorLabel, assignment, decision, messag
14974
15040
  message: MessageRecord;
14975
15041
  now: string;
14976
15042
  }): AssignmentRecord {
15043
+ if (!assignment.itemLineageId) {
15044
+ throw new Error(`Assignment ${assignment.id} has no content item lineage; copywriting must continue the same content item.`);
15045
+ }
14977
15046
  const type = decision === "brief" ? "brief" : "article";
14978
15047
  const assignmentTypeKey = type === "brief" ? "copywriting.brief-draft" : "copywriting.article-draft";
14979
15048
  const section = assignment.sectionKey ?? assignment.sectionId ?? "unsectioned";
@@ -15026,6 +15095,7 @@ function buildUiCopywritingAssignment({ actorLabel, assignment, decision, messag
15026
15095
  createdAt: now,
15027
15096
  updatedAt: now,
15028
15097
  newsroomFeedKey: "assignment#open",
15098
+ itemLineageId: assignment.itemLineageId,
15029
15099
  metadata,
15030
15100
  };
15031
15101
  }
@@ -15187,14 +15257,14 @@ function normalizeKnowledgeQueryAnchorKind(value: string | null | undefined): Kn
15187
15257
 
15188
15258
  function normalizeNewsroomFromHref(value: string | null | undefined): string | null {
15189
15259
  if (!value) return null;
15190
- const trimmed = value.trim();
15260
+ const trimmed = browserPathToInternalNewsroomPath(value.trim());
15191
15261
  if (!trimmed.startsWith("/newsroom")) return null;
15192
- return trimmed;
15262
+ return toPublicNewsroomPath(trimmed);
15193
15263
  }
15194
15264
 
15195
15265
  function currentNewsroomOriginHref(fallback: string | null): string | null {
15196
15266
  if (typeof window === "undefined") return fallback;
15197
- const current = `${window.location.pathname}${window.location.search}`;
15267
+ const current = browserPathToInternalNewsroomPath(`${window.location.pathname}${window.location.search}`);
15198
15268
  return current.startsWith("/newsroom/search") ? fallback : normalizeNewsroomFromHref(current);
15199
15269
  }
15200
15270
 
@@ -15228,7 +15298,7 @@ function buildNewsroomSearchHref(request: NewsroomSearchRequest, demo?: boolean)
15228
15298
  if (request.from) params.set("from", request.from);
15229
15299
  if (demo) params.set("demo", "1");
15230
15300
  const queryString = params.toString();
15231
- return queryString ? `/newsroom/search?${queryString}` : "/newsroom/search";
15301
+ return toPublicNewsroomPath(queryString ? `/newsroom/search?${queryString}` : "/newsroom/search");
15232
15302
  }
15233
15303
 
15234
15304
  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
+ }