@anthusai/papyrus 2.0.0-next.11 → 2.0.0-next.110

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 (227) hide show
  1. package/amplify/auth/resource.js +16 -1
  2. package/amplify/auth/resource.ts +19 -1
  3. package/amplify/data/function-handler.js +20 -0
  4. package/amplify/data/function-handler.ts +40 -0
  5. package/amplify/data/resource.js +152 -62
  6. package/amplify/data/resource.ts +158 -62
  7. package/amplify/data/schema.js +2101 -1987
  8. package/amplify/data/schema.ts +197 -70
  9. package/amplify/functions/assignment-action/data-access.js +2 -0
  10. package/amplify/functions/assignment-action/data-access.ts +2 -0
  11. package/amplify/functions/assignment-action/handler.ts +1 -1
  12. package/amplify/functions/automation-view/handler.py +17 -0
  13. package/amplify/functions/automation-view/resource.js +24 -0
  14. package/amplify/functions/automation-view/resource.ts +34 -0
  15. package/amplify/functions/automation-view/schema.js +26 -0
  16. package/amplify/functions/automation-view/schema.ts +28 -0
  17. package/amplify/functions/automation-view/wire.js +56 -0
  18. package/amplify/functions/automation-view/wire.ts +76 -0
  19. package/amplify/functions/content-actions/handler.py +64 -3
  20. package/amplify/functions/content-actions/requirements.txt +1 -0
  21. package/amplify/functions/content-actions/resource.js +1 -1
  22. package/amplify/functions/content-actions/resource.ts +1 -1
  23. package/amplify/functions/editorial-checks/handler.py +2 -2
  24. package/amplify/functions/editorial-checks/requirements.txt +1 -1
  25. package/amplify/functions/editorial-checks/resource.js +3 -3
  26. package/amplify/functions/editorial-checks/resource.ts +3 -3
  27. package/amplify/functions/editorial-checks/schema.js +38 -8
  28. package/amplify/functions/editorial-checks/schema.ts +38 -8
  29. package/amplify/functions/editorial-checks/wire.js +4 -0
  30. package/amplify/functions/editorial-checks/wire.ts +4 -0
  31. package/amplify/functions/monitoring-view/handler.py +11 -0
  32. package/amplify/functions/monitoring-view/resource.js +24 -0
  33. package/amplify/functions/monitoring-view/resource.ts +34 -0
  34. package/amplify/functions/monitoring-view/schema.js +29 -0
  35. package/amplify/functions/monitoring-view/schema.ts +31 -0
  36. package/amplify/functions/monitoring-view/wire.js +64 -0
  37. package/amplify/functions/monitoring-view/wire.ts +81 -0
  38. package/amplify/functions/public-forms/core.js +247 -0
  39. package/amplify/functions/public-forms/core.ts +310 -0
  40. package/amplify/functions/public-forms/handler.ts +41 -0
  41. package/amplify/functions/public-forms/origins.js +35 -0
  42. package/amplify/functions/public-forms/origins.ts +43 -0
  43. package/amplify/functions/public-forms/resource.js +9 -0
  44. package/amplify/functions/public-forms/resource.ts +10 -0
  45. package/amplify/functions/public-forms/wire.js +25 -0
  46. package/amplify/functions/public-forms/wire.ts +32 -0
  47. package/amplify/functions/shared/editorial-context.ts +63 -4
  48. package/amplify/functions/shared/python-bundle.js +20 -14
  49. package/amplify/functions/shared/python-bundle.ts +25 -18
  50. package/amplify/plugins/newsletter/config.js +245 -0
  51. package/amplify/plugins/newsletter/config.ts +305 -0
  52. package/amplify/plugins/newsletter/function/handler.py +6 -0
  53. package/amplify/plugins/newsletter/function/requirements.txt +5 -0
  54. package/amplify/plugins/newsletter/ops/handler.py +12 -0
  55. package/amplify/plugins/newsletter/ops/resource.js +27 -0
  56. package/amplify/plugins/newsletter/ops/resource.ts +37 -0
  57. package/amplify/plugins/newsletter/papyrus.plugin.js +263 -0
  58. package/amplify/plugins/newsletter/papyrus.plugin.ts +311 -0
  59. package/amplify/plugins/newsletter/schema.js +57 -0
  60. package/amplify/plugins/newsletter/schema.ts +62 -0
  61. package/amplify/plugins/newsletter/secure-string-parameter.js +107 -0
  62. package/amplify/plugins/newsletter/secure-string-parameter.ts +120 -0
  63. package/amplify/plugins/newsletter/sending.js +140 -0
  64. package/amplify/plugins/newsletter/sending.ts +186 -0
  65. package/amplify/plugins/reader-steering/config.js +182 -0
  66. package/amplify/plugins/reader-steering/config.ts +290 -0
  67. package/amplify/plugins/reader-steering/function/ai_moderation.py +246 -0
  68. package/amplify/plugins/reader-steering/function/handler.py +1589 -0
  69. package/amplify/plugins/reader-steering/function/moderation-prompt.md +27 -0
  70. package/amplify/plugins/reader-steering/function/moderation.py +316 -0
  71. package/amplify/plugins/reader-steering/function/proposals.py +487 -0
  72. package/amplify/plugins/reader-steering/function/resource.js +27 -0
  73. package/amplify/plugins/reader-steering/function/resource.ts +35 -0
  74. package/amplify/plugins/reader-steering/papyrus.plugin.js +187 -0
  75. package/amplify/plugins/reader-steering/papyrus.plugin.ts +213 -0
  76. package/amplify/plugins/reader-steering/schema.js +252 -0
  77. package/amplify/plugins/reader-steering/schema.ts +254 -0
  78. package/amplify/plugins/registry.js +4 -0
  79. package/amplify/plugins/registry.ts +4 -0
  80. package/amplify/plugins/signup/schema.js +2 -1
  81. package/amplify/plugins/signup/schema.ts +2 -1
  82. package/amplify/plugins/types.ts +5 -0
  83. package/amplify/plugins/wire.js +1 -0
  84. package/amplify/plugins/wire.ts +1 -0
  85. package/amplify/publication/projection-lease.js +30 -0
  86. package/amplify/publication/projection-lease.ts +39 -0
  87. package/amplify/publication/redirect-and-index.js +105 -0
  88. package/amplify/publication/redirect-rules.js +73 -0
  89. package/amplify/publication/redirect-rules.ts +93 -0
  90. package/amplify/publication/s3-projection.js +7 -1
  91. package/amplify/publication/s3-projection.ts +9 -1
  92. package/amplify/site-backend-config.js +19 -0
  93. package/amplify/site-backend-config.ts +45 -1
  94. package/amplify/site-backend-names.js +4 -2
  95. package/amplify/site-backend-names.ts +4 -2
  96. package/amplify/site-backend.js +42 -4
  97. package/amplify/site-backend.ts +45 -4
  98. package/app/.well-known/papyrus-cli.json/route.ts +9 -0
  99. package/app/api/auth/prospectus/sign-in/route.ts +4 -0
  100. package/app/api/newsletter/confirm/route.ts +13 -0
  101. package/app/api/newsletter/preferences/route.ts +13 -0
  102. package/app/api/newsletter/state/route.ts +9 -0
  103. package/app/api/newsletter/subscribe/route.ts +9 -0
  104. package/app/api/newsletter/unsubscribe/route.ts +13 -0
  105. package/app/api/newsroom/chat/[...path]/route.ts +17 -0
  106. package/app/api/newsroom/curation/context/route.ts +9 -0
  107. package/app/api/newsroom/curation/priming/route.ts +9 -0
  108. package/app/api/newsroom/curation/publication/route.ts +9 -0
  109. package/app/api/reader/comment/delete/route.ts +9 -0
  110. package/app/api/reader/comment/edit/route.ts +9 -0
  111. package/app/api/reader/comment/route.ts +9 -0
  112. package/app/api/reader/flag/route.ts +9 -0
  113. package/app/api/reader/handle/route.ts +9 -0
  114. package/app/api/reader/me/route.ts +9 -0
  115. package/app/api/reader/rating/route.ts +9 -0
  116. package/app/api/reader/report/route.ts +9 -0
  117. package/app/api/reader/retired/route.ts +9 -0
  118. package/app/api/reader/submit/route.ts +9 -0
  119. package/app/api/reader/vote/route.ts +9 -0
  120. package/app/layout.tsx +5 -1
  121. package/bin/papyrus-infra.mjs +20 -1
  122. package/components/automation-panel.tsx +49 -0
  123. package/components/automation-run-now.tsx +97 -0
  124. package/components/automation-view.tsx +195 -0
  125. package/components/editorial-checks-admin-panel.tsx +32 -17
  126. package/components/moderation-panel.tsx +248 -0
  127. package/components/monitoring-overview-view.tsx +422 -0
  128. package/components/monitoring-panel.tsx +60 -0
  129. package/components/news-desk-client-provider.tsx +18 -18
  130. package/components/news-desk-taxonomy-client.ts +28 -8
  131. package/components/newsletter-panel.tsx +474 -0
  132. package/components/newsletter-sections.tsx +163 -0
  133. package/components/newsroom-access-gate.tsx +16 -2
  134. package/components/newsroom-article-editor.tsx +9 -7
  135. package/components/newsroom-articles-view.tsx +6 -6
  136. package/components/newsroom-assignment-detail.tsx +6 -6
  137. package/components/newsroom-board-view.tsx +8 -3
  138. package/components/newsroom-curation-bootstrap.tsx +45 -0
  139. package/components/newsroom-ops-overview.tsx +26 -96
  140. package/components/newsroom-ops-shell.tsx +2 -2
  141. package/components/newsroom-organizer.css +33 -0
  142. package/components/newsroom-overview-skeleton.tsx +25 -0
  143. package/components/newsroom-references-view.tsx +192 -49
  144. package/components/notification-center.tsx +281 -0
  145. package/components/papyrus-console-conversation.css +14 -0
  146. package/components/papyrus-console-conversation.tsx +55 -0
  147. package/components/papyrus-console-shell.tsx +62 -420
  148. package/components/papyrus-private-chat-panel.tsx +134 -0
  149. package/components/publication-cyclotron-panel.tsx +15 -10
  150. package/components/reader-auth-state.ts +1 -1
  151. package/components/reference-add-entry.tsx +75 -0
  152. package/components/reference-curation-controls.tsx +31 -22
  153. package/components/reference-cyclotron-priming.tsx +58 -19
  154. package/components/reference-cyclotron-suggestion.tsx +10 -6
  155. package/components/reference-priming.tsx +14 -10
  156. package/components/reference-relevance-review.tsx +3 -3
  157. package/components/reference-section-suggestion.tsx +1 -1
  158. package/components/topic-steering-workspace.tsx +230 -144
  159. package/infra/amplify-app-shell.js +96 -16
  160. package/infra/amplify-app-shell.ts +108 -18
  161. package/infra/cloud-email-config.js +32 -0
  162. package/infra/cloud-email-config.ts +39 -0
  163. package/infra/cloud-email.js +57 -0
  164. package/infra/cloud-email.test.js +69 -0
  165. package/infra/cloud-email.test.ts +66 -0
  166. package/infra/cloud-email.ts +60 -0
  167. package/infra/index.js +1 -0
  168. package/infra/secure-string-parameter-handler.js +41 -0
  169. package/infra/secure-string-parameter-handler.ts +41 -0
  170. package/infra/secure-string-parameter.js +79 -0
  171. package/infra/secure-string-parameter.ts +91 -0
  172. package/infra/site-config.js +92 -6
  173. package/infra/site-config.ts +138 -6
  174. package/lib/automation-client.ts +54 -0
  175. package/lib/automation-fixture.ts +93 -0
  176. package/lib/automation-overview.ts +252 -0
  177. package/lib/category-repository.ts +14 -12
  178. package/lib/cli-login-discovery.ts +78 -0
  179. package/lib/console-web-context.ts +1 -1
  180. package/lib/curation-routes.ts +345 -0
  181. package/lib/cyclotron-suggestions.ts +4 -4
  182. package/lib/editorial-checks-client.ts +18 -0
  183. package/lib/editorial-checks-demo.ts +1 -1
  184. package/lib/editorial-checks.ts +8 -4
  185. package/lib/editorial-preview.ts +5 -5
  186. package/lib/monitoring-client.ts +83 -0
  187. package/lib/monitoring-overview-fixture.ts +51 -0
  188. package/lib/monitoring-overview.ts +191 -0
  189. package/lib/news-desk-session.ts +12 -21
  190. package/lib/newsletter-api.ts +418 -0
  191. package/lib/newsletter-client.ts +46 -0
  192. package/lib/newsletter-fixture.ts +184 -0
  193. package/lib/newsletter-model.ts +202 -0
  194. package/lib/newsroom-demo-dashboard.ts +1 -1
  195. package/lib/newsroom-nav.ts +1 -1
  196. package/lib/newsroom-organizer-nav.ts +2 -1
  197. package/lib/papyrus-chat-admission.ts +65 -0
  198. package/lib/papyrus-hosted-chat.ts +217 -0
  199. package/lib/papyrus-private-chat-client.ts +81 -0
  200. package/lib/prospectus-session.ts +30 -14
  201. package/lib/prospectus-sign-in-routes.ts +106 -31
  202. package/lib/prospectus-site-identity.ts +46 -6
  203. package/lib/reader-api.ts +145 -0
  204. package/lib/reader-moderation.ts +220 -0
  205. package/lib/reader-retirement-route.ts +89 -0
  206. package/lib/reader-subject.md +29 -0
  207. package/lib/reader-subject.ts +53 -0
  208. package/lib/reference-review-details.ts +71 -0
  209. package/lib/reference-review-request.ts +14 -0
  210. package/lib/relevance-decisions.ts +42 -10
  211. package/lib/relevance-status.ts +2 -2
  212. package/lib/review-client.ts +120 -0
  213. package/lib/review-fixture.ts +45 -0
  214. package/lib/review-requests.ts +246 -0
  215. package/lib/semantic-graph.ts +2 -0
  216. package/lib/shared-chat-admission.ts +182 -0
  217. package/lib/site-brand-tenant.ts +11 -6
  218. package/lib/site-brand.ts +6 -3
  219. package/lib/site-origin-value.ts +50 -0
  220. package/lib/site-origin.ts +8 -0
  221. package/lib/use-reference-review-data.ts +44 -0
  222. package/middleware.ts +14 -3
  223. package/package.json +67 -3
  224. package/routes.manifest.json +238 -2
  225. package/src/sync.mjs +11 -2
  226. package/src/with-papyrus.d.mts +1 -0
  227. package/src/with-papyrus.mjs +17 -1
@@ -6,11 +6,13 @@ import { SITE_BRAND } from "../lib/site-brand";
6
6
  import type { NewsDeskShellState } from "../lib/news-desk-session";
7
7
  import { cn } from "../lib/utils";
8
8
  import { NewsroomOpsShell } from "./newsroom-ops-shell";
9
+ import { NewsroomOverviewSkeleton } from "./newsroom-overview-skeleton";
9
10
  import { ReaderAuthControl } from "./reader-auth-control";
10
11
  import { buttonVariants } from "./ui/button";
11
12
 
12
13
  export function NewsDeskAccessGate({ shell, showSectionTabs = false }: { shell: NewsDeskShellState | null; showSectionTabs?: boolean }) {
13
14
  const accessPhase = shell?.phase ?? "checkingAccess";
15
+ const pending = accessPhase === "checkingAccess" || accessPhase === "loadingDesk";
14
16
 
15
17
  return (
16
18
  <NewsroomOpsShell
@@ -21,9 +23,20 @@ export function NewsDeskAccessGate({ shell, showSectionTabs = false }: { shell:
21
23
  headerActions={(
22
24
  <Link className={cn(buttonVariants({ variant: "ghost", size: "sm" }))} href="/settings">Settings</Link>
23
25
  )}
24
- pageTitle="Newsroom"
26
+ pageTitle={showSectionTabs ? "Notification Center" : "Newsroom"}
25
27
  showNavigation={showSectionTabs}
26
28
  >
29
+ {pending ? (showSectionTabs ? <NewsroomOverviewSkeleton /> : (
30
+ <div aria-label="Loading newsroom" className="space-y-4" data-newsroom-page-skeleton role="status">
31
+ <div className="h-6 w-48 rounded bg-muted" />
32
+ <div className="h-4 w-3/4 rounded bg-muted" />
33
+ <div className="grid gap-3 sm:grid-cols-2">
34
+ {Array.from({ length: 4 }, (_, index) => <div className="h-32 rounded-xl border bg-card p-4" key={index}>
35
+ <div className="h-4 w-2/3 rounded bg-muted" /><div className="mt-4 h-3 w-1/2 rounded bg-muted" />
36
+ </div>)}
37
+ </div>
38
+ </div>
39
+ )) : (
27
40
  <section aria-live="polite" data-news-desk-access={accessPhase} data-news-desk-access-phase={accessPhase}>
28
41
  <div className="space-y-3 rounded-xl border border-border bg-card p-6">
29
42
  <p className="m-0 text-[0.7rem] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Access</p>
@@ -38,12 +51,13 @@ export function NewsDeskAccessGate({ shell, showSectionTabs = false }: { shell:
38
51
  ) : null}
39
52
  </div>
40
53
  </section>
54
+ )}
41
55
  </NewsroomOpsShell>
42
56
  );
43
57
  }
44
58
 
45
59
  function formatAccessTitle(state: NewsDeskShellState | null): string {
46
- if (!state || state.phase === "checkingAccess") return "Checking Desk Credentials";
60
+ if (!state || state.phase === "checkingAccess") return "Checking Credentials";
47
61
  if (state.phase === "loadingDesk") return "Loading Newsroom Records";
48
62
  if (state.phase === "forbidden") return "Editor Role Required";
49
63
  if (state.phase === "error") return "Newsroom Unavailable";
@@ -513,8 +513,10 @@ export function NewsroomArticleEditor({ articleId, demo, embedded = false }: { a
513
513
  }
514
514
  if (!editor) {
515
515
  return (
516
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
517
- <Spinner /> Loading article…
516
+ <div aria-label="Loading article" className="space-y-5" role="status">
517
+ <div className="h-8 w-2/3 rounded bg-muted" />
518
+ <div className="grid gap-4 sm:grid-cols-2"><div className="h-12 rounded bg-muted" /><div className="h-12 rounded bg-muted" /></div>
519
+ <div className="h-64 rounded-xl border bg-card p-4"><div className="h-4 w-5/6 rounded bg-muted" /><div className="mt-3 h-4 w-3/4 rounded bg-muted" /></div>
518
520
  </div>
519
521
  );
520
522
  }
@@ -668,21 +670,21 @@ export function NewsroomArticleEditor({ articleId, demo, embedded = false }: { a
668
670
  </Select>
669
671
  </label>
670
672
  <label className="space-y-1 text-sm font-medium">
671
- <span>Desk</span>
673
+ <span>Section</span>
672
674
  <Select
673
675
  onValueChange={(value) => {
674
676
  if (typeof value === "string") patchEditor({ section: value === NO_DESK_VALUE ? "" : value });
675
677
  }}
676
678
  value={deskOptions.some(option => option.value === editor.section) ? editor.section : NO_DESK_VALUE}
677
679
  >
678
- <SelectTrigger aria-label="Desk" className="w-full" data-newsroom-article-section>
679
- <SelectValue>{deskOptions.find(option => option.value === editor.section)?.label ?? "No desk"}</SelectValue>
680
+ <SelectTrigger aria-label="Section" className="w-full" data-newsroom-article-section>
681
+ <SelectValue>{deskOptions.find(option => option.value === editor.section)?.label ?? "No section"}</SelectValue>
680
682
  </SelectTrigger>
681
683
  <SelectContent>
682
- <SelectItem value={NO_DESK_VALUE}>No desk</SelectItem>
684
+ <SelectItem value={NO_DESK_VALUE}>No section</SelectItem>
683
685
  {(["core", "rotating"] as const).map((kind) => deskOptions.some(option => option.kind === kind) ? (
684
686
  <SelectGroup key={kind}>
685
- <SelectLabel>{kind === "core" ? "Core desks" : "Rotating desks"}</SelectLabel>
687
+ <SelectLabel>{kind === "core" ? "Core sections" : "Rotating sections"}</SelectLabel>
686
688
  {deskOptions.filter(option => option.kind === kind).map((option) => (
687
689
  <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>
688
690
  ))}
@@ -5,7 +5,6 @@ import Link from "next/link";
5
5
  import { useEffect, useMemo, useState } from "react";
6
6
  import { Badge } from "./ui/badge";
7
7
  import { buttonVariants } from "./ui/button";
8
- import { Spinner } from "./ui/spinner";
9
8
  import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
10
9
  import { SITE_BRAND } from "../lib/site-brand";
11
10
  import { getNewsroomNavHref } from "../lib/newsroom-nav";
@@ -89,15 +88,16 @@ export function NewsroomArticlesView({ demo }: { demo: boolean }) {
89
88
  </TabsList>
90
89
  </Tabs>
91
90
  {loadError ? <NewsroomOpsStatusBanner tone="error">{loadError}</NewsroomOpsStatusBanner> : null}
92
- {rows === null && !loadError ? (
93
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
94
- <Spinner /> Loading articles…
95
- </div>
96
- ) : null}
97
91
  {rows !== null && visibleRows.length === 0 ? (
98
92
  <p className="text-sm text-muted-foreground">Nothing here yet.</p>
99
93
  ) : null}
100
94
  <ul className="divide-y divide-border rounded-xl border border-border bg-card" data-newsroom-articles-list>
95
+ {rows === null && !loadError ? Array.from({ length: 5 }, (_, index) => (
96
+ <li aria-hidden="true" className="flex items-center justify-between px-4 py-3" key={index}>
97
+ <span className="w-2/3"><span className="block h-4 w-3/4 rounded bg-muted" /><span className="mt-2 block h-3 w-1/2 rounded bg-muted" /></span>
98
+ <span className="h-5 w-16 rounded bg-muted" />
99
+ </li>
100
+ )) : null}
101
101
  {visibleRows.map((row) => (
102
102
  <li key={row.id}>
103
103
  <Link
@@ -36,7 +36,7 @@ export function NewsroomAssignmentDetail({ assignment, deskOptions = [], onDeskC
36
36
  const changeDesk = (value: string) => {
37
37
  if (!onDeskChange) return;
38
38
  setSavingDesk(true); setDeskError(null);
39
- onDeskChange(value === NO_DESK_VALUE ? null : value).catch((reason: unknown) => setDeskError(reason instanceof Error ? reason.message : "Could not save the desk.")).finally(() => setSavingDesk(false));
39
+ onDeskChange(value === NO_DESK_VALUE ? null : value).catch((reason: unknown) => setDeskError(reason instanceof Error ? reason.message : "Could not save the section.")).finally(() => setSavingDesk(false));
40
40
  };
41
41
  const facts: DetailField[] = [
42
42
  { label: "Status", value: assignment.status },
@@ -58,14 +58,14 @@ export function NewsroomAssignmentDetail({ assignment, deskOptions = [], onDeskC
58
58
  {facts.map(field => <div key={field.label}><dt>{field.label}</dt><dd>{field.value}</dd></div>)}
59
59
  </dl>
60
60
  {onDeskChange && <label className="newsroom-assignment-detail-desk">
61
- <span>Desk</span>
61
+ <span>Section</span>
62
62
  <Select onValueChange={(value) => { if (typeof value === "string") changeDesk(value); }} value={deskChoices.some(option => option.value === currentDesk) ? currentDesk : NO_DESK_VALUE} disabled={savingDesk}>
63
- <SelectTrigger aria-label="Desk" className="w-full" data-newsroom-assignment-desk>
64
- <SelectValue>{deskChoices.find(option => option.value === currentDesk)?.label ?? "No desk"}</SelectValue>
63
+ <SelectTrigger aria-label="Section" className="w-full" data-newsroom-assignment-desk>
64
+ <SelectValue>{deskChoices.find(option => option.value === currentDesk)?.label ?? "No section"}</SelectValue>
65
65
  </SelectTrigger>
66
66
  <SelectContent>
67
- <SelectItem value={NO_DESK_VALUE}>No desk</SelectItem>
68
- {(["core", "rotating"] as const).map(kind => deskChoices.some(option => option.kind === kind) ? <SelectGroup key={kind}><SelectLabel>{kind === "core" ? "Core desks" : "Rotating desks"}</SelectLabel>{deskChoices.filter(option => option.kind === kind).map(option => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}</SelectGroup> : null)}
67
+ <SelectItem value={NO_DESK_VALUE}>No section</SelectItem>
68
+ {(["core", "rotating"] as const).map(kind => deskChoices.some(option => option.kind === kind) ? <SelectGroup key={kind}><SelectLabel>{kind === "core" ? "Core sections" : "Rotating sections"}</SelectLabel>{deskChoices.filter(option => option.kind === kind).map(option => <SelectItem key={option.value} value={option.value}>{option.label}</SelectItem>)}</SelectGroup> : null)}
69
69
  </SelectContent>
70
70
  </Select>
71
71
  {currentDesk && !deskChoices.some(option => option.value === currentDesk) ? <small className="newsroom-desk-legacy-value">Old section value: {currentDesk}</small> : null}
@@ -160,7 +160,7 @@ export function NewsroomBoardView({ subject, deskId }: { subject: NewsroomBoardS
160
160
  if (!allowed) return <NewsDeskAccessGate shell={session?.shell ?? null} />;
161
161
  return <NewsroomOpsShell activeTab={subject === "contentItem" ? "articles" : subject === "desk" ? "desks" : "assignments"} appTitle={SITE_BRAND.appTitle} backLabel={SITE_BRAND.backToHomeLabel} pageTitle={pageTitle}
162
162
  headerActions={<>
163
- <span role="status" className="organizer-board-note">{loading ? "Loading records…" : `${issues.length} records loaded.`}</span>
163
+ {!loading ? <span role="status" className="organizer-board-note">{issues.length} records loaded.</span> : null}
164
164
  <div role="group" aria-label="Record presentation" className="organizer-board-view-toggle"><button type="button" className="organizer-header-button" aria-pressed={view === "board"} onClick={() => setView("board")}>Board</button><button type="button" className="organizer-header-button" aria-pressed={view === "list"} onClick={() => setView("list")}>List</button></div>
165
165
  {showsContentItems && (
166
166
  <Link className="organizer-header-button" data-newsroom-article-new href={getNewsroomNavHref(newsroomHref("articles", "new"))}>
@@ -174,11 +174,16 @@ export function NewsroomBoardView({ subject, deskId }: { subject: NewsroomBoardS
174
174
  {error && <NewsroomOpsStatusBanner tone="error">{error}</NewsroomOpsStatusBanner>}
175
175
  {subject === "desk" && <IssueTypeFilter config={config} value={typeFilter} onChange={setTypeFilter} name="Record type" className="organizer-board-type-filter" />}
176
176
  <div className="organizer-board-body" data-detail-open={Boolean(issue)} ref={boardBodyRef} onScrollCapture={handleBoardScroll}>
177
- <IssueCollection issues={issues} config={config} query={{ type: subject === "desk" ? issueTypeFromFilter(typeFilter) : boardType }} view={view}
177
+ {loading && !issues.length ? <div aria-label="Loading board records" className="grid w-full grid-cols-1 gap-3 sm:grid-cols-3" role="status">
178
+ {Array.from({ length: 3 }, (_, column) => <div className="min-h-64 space-y-3 rounded-xl border bg-card p-4" key={column}>
179
+ <div className="h-4 w-1/2 rounded bg-muted" />
180
+ {Array.from({ length: 2 }, (_, row) => <div className="h-24 rounded-lg border bg-background p-3" key={row}><div className="h-4 w-3/4 rounded bg-muted" /><div className="mt-3 h-3 w-1/2 rounded bg-muted" /></div>)}
181
+ </div>)}
182
+ </div> : <IssueCollection issues={issues} config={config} query={{ type: subject === "desk" ? issueTypeFromFilter(typeFilter) : boardType }} view={view}
178
183
  motion={lowMotion ? STATIC_MOTION : undefined}
179
184
  collapsedColumns={columnState.collapsedColumns} onToggleCollapse={columnState.toggleColumn} initialScroll="start"
180
185
  selectedIssueId={selected} detailOpen={Boolean(issue)} onSelectIssue={item => setSelected(item.id)} ariaLabel={pageTitle}
181
- emptyMessage={loading ? "Loading records…" : error ? "Records could not be loaded." : "No records in this selection."} />
186
+ emptyMessage={error ? "Records could not be loaded." : "No records in this selection."} />}
182
187
  {issue && <IssueDetailPanel issue={issue} onClose={() => setSelected(null)} fields={[]}>
183
188
  {issue.kind === "contentItem"
184
189
  ? <>
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ import {useEffect,useState,type ReactNode} from 'react';
4
+ import {loadProspectusAccountState} from '../lib/prospectus-account-client';
5
+ import type {CurationBootstrap} from '../lib/curation-client';
6
+ import {NewsroomCurationProvider} from './newsroom-curation-provider';
7
+
8
+ type Fetcher=(input:string,init?:RequestInit)=>Promise<Response>;
9
+ const CONTEXT_PATH='/api/newsroom/curation/context';
10
+
11
+ function validBootstrap(value:unknown):value is CurationBootstrap{
12
+ const body=value as Partial<CurationBootstrap>|null;
13
+ const scope=body?.scope;
14
+ return !!scope&&[scope.accountId,scope.publicationId,scope.bindingRevision].every(item=>typeof item==='string'&&item.trim()!=='')
15
+ &&(body!.mode==='embedded'||body!.mode==='remote')&&Array.isArray(body!.capabilities);
16
+ }
17
+
18
+ /**
19
+ * Ask the server for the curation bootstrap, or null. Only a signed-in person with a selected
20
+ * account causes a context read; every other outcome (no Prospectus, signed out, no selection,
21
+ * any refusal or failure) is null, so the released CMS is unchanged.
22
+ */
23
+ export async function loadCurationBootstrap(returnPath:string,fetcher:Fetcher=fetch):Promise<CurationBootstrap|null>{
24
+ try{
25
+ const account=await loadProspectusAccountState(returnPath,fetcher);
26
+ if(account.status!=='signedIn'||!account.selectedAccountId)return null;
27
+ const response=await fetcher(CONTEXT_PATH,{credentials:'same-origin',cache:'no-store'});
28
+ if(!response.ok)return null;
29
+ const body=await response.json().catch(()=>null);
30
+ if(!validBootstrap(body)||body.scope.accountId!==account.selectedAccountId)return null;
31
+ return body;
32
+ }catch{return null;}
33
+ }
34
+
35
+ /** Mounts the curation provider for the References tab. Fails closed to no provider content. */
36
+ export function NewsroomCurationBootstrap({enabled=true,children}:{enabled?:boolean;children:ReactNode}){
37
+ const [bootstrap,setBootstrap]=useState<CurationBootstrap|null>(null);
38
+ useEffect(()=>{
39
+ if(!enabled)return;
40
+ let current=true;
41
+ void loadCurationBootstrap(window.location.pathname).then(value=>{if(current)setBootstrap(value);});
42
+ return()=>{current=false;setBootstrap(null);};
43
+ },[enabled]);
44
+ return <NewsroomCurationProvider bootstrap={bootstrap}>{children}</NewsroomCurationProvider>;
45
+ }
@@ -1,24 +1,13 @@
1
1
  "use client";
2
2
 
3
- import {
4
- BookOpenIcon,
5
- ClipboardListIcon,
6
- FileTextIcon,
7
- LayersIcon,
8
- MessageSquareIcon,
9
- NetworkIcon,
10
- SearchIcon,
11
- SettingsIcon,
12
- } from "lucide-react";
13
3
  import Link from "next/link";
14
4
  import { usePathname } from "next/navigation";
15
5
  import { useMemo } from "react";
16
6
  import { Badge } from "./ui/badge";
17
7
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
18
- import type { AssignmentRecord, MessageRecord, NewsroomSectionRecord, ReferenceRecord } from "../lib/category-repository";
19
- import { getNewsroomNavHref, NEWSROOM_OPS_NAV, type NewsroomNavTabId } from "../lib/newsroom-nav";
8
+ import type { AssignmentRecord, NewsroomSectionRecord, ReferenceRecord } from "../lib/category-repository";
9
+ import { getNewsroomNavHref } from "../lib/newsroom-nav";
20
10
  import {
21
- countReferencesByStatus,
22
11
  filterReferencesByStatus,
23
12
  formatReferenceListDate,
24
13
  referenceDisplayTitle,
@@ -33,57 +22,33 @@ import {
33
22
  sortNewsroomSections,
34
23
  } from "../lib/newsroom-sections";
35
24
  import { newsroomInboxRowClassName } from "../lib/newsroom-list-selection";
25
+ import type { RelevanceDecisionView } from "../lib/relevance-decisions";
26
+ import { NotificationCenter } from "./notification-center";
36
27
 
37
28
  type NewsroomOpsOverviewProps = {
38
29
  assignments: AssignmentRecord[];
30
+ actorLabel?: string;
31
+ decisions?: ReadonlyMap<string, RelevanceDecisionView>;
39
32
  demo?: boolean;
40
- messages: MessageRecord[];
33
+ humanCuratedReferenceLineageIds?: ReadonlySet<string>;
41
34
  newsroomSections: NewsroomSectionRecord[];
42
35
  references: ReferenceRecord[];
43
36
  };
44
37
 
45
- type DestinationKey = Exclude<NewsroomNavTabId, "overview" | "desks">;
46
-
47
- const DESTINATION_ICONS: Record<DestinationKey, typeof BookOpenIcon> = {
48
- articles: FileTextIcon,
49
- assignments: ClipboardListIcon,
50
- references: BookOpenIcon,
51
- messages: MessageSquareIcon,
52
- topics: LayersIcon,
53
- concepts: NetworkIcon,
54
- administration: SettingsIcon,
55
- search: SearchIcon,
56
- };
57
-
58
- const DESTINATIONS = NEWSROOM_OPS_NAV.filter((item) => item.id !== "overview").map((item) => ({
59
- key: item.id as DestinationKey,
60
- label: item.label,
61
- detail: item.detail,
62
- href: item.href,
63
- icon: DESTINATION_ICONS[item.id as DestinationKey],
64
- }));
65
-
66
- function openAssignmentCount(assignments: AssignmentRecord[]): number {
67
- return assignments.filter((assignment) => {
68
- const status = assignment.status?.trim().toLowerCase() ?? "";
69
- return status === "open" || status === "claimed" || status === "pending";
70
- }).length;
71
- }
72
-
73
- function recentMessageCount(messages: MessageRecord[]): number {
74
- return messages.filter((message) => message.status !== "archived").length;
75
- }
38
+ const NO_DECISIONS: ReadonlyMap<string, RelevanceDecisionView> = new Map();
39
+ const NO_IDS: ReadonlySet<string> = new Set();
76
40
 
77
41
  export function NewsroomOpsOverview({
42
+ actorLabel = "",
78
43
  assignments,
44
+ decisions = NO_DECISIONS,
79
45
  demo = false,
80
- messages,
46
+ humanCuratedReferenceLineageIds = NO_IDS,
81
47
  newsroomSections,
82
48
  references,
83
49
  }: NewsroomOpsOverviewProps) {
84
50
  const pathname = usePathname();
85
51
  const canonicalReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
86
- const referenceCounts = useMemo(() => countReferencesByStatus(canonicalReferences), [canonicalReferences]);
87
52
  const pendingReferences = useMemo(
88
53
  () => filterReferencesByStatus(canonicalReferences, "pending").slice(0, 5),
89
54
  [canonicalReferences],
@@ -104,12 +69,6 @@ export function NewsroomOpsOverview({
104
69
  const coreDesks = useMemo(() => desks.filter(desk => newsroomDeskKind(desk) === "core"), [desks]);
105
70
  const rotatingDesks = useMemo(() => desks.filter(desk => newsroomDeskKind(desk) === "rotating"), [desks]);
106
71
 
107
- const destinationCounts: Partial<Record<DestinationKey, number | null>> = {
108
- references: referenceCounts.pending ?? 0,
109
- assignments: openAssignmentCount(assignments),
110
- messages: recentMessageCount(messages),
111
- };
112
-
113
72
  return (
114
73
  <div
115
74
  className="space-y-8 font-sans"
@@ -117,44 +76,15 @@ export function NewsroomOpsOverview({
117
76
  data-newsroom-ops-home="true"
118
77
  >
119
78
  <div className="space-y-8" data-newsroom-overview-feeds>
120
- <section aria-label="Destinations" className="space-y-3">
121
- <h2 className="text-sm font-medium text-muted-foreground">Go to</h2>
122
- <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
123
- {DESTINATIONS.map((meta) => {
124
- const key = meta.key;
125
- const Icon = meta.icon;
126
- const count = destinationCounts[key];
127
- const href = getNewsroomNavHref(meta.href, demo);
128
- return (
129
- <Link
130
- className="group block rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
131
- data-newsroom-ops-destination={key}
132
- data-newsroom-overview-section={key}
133
- href={href}
134
- key={key}
135
- >
136
- <Card className="h-full transition-colors group-hover:border-primary/40">
137
- <CardHeader className="space-y-3 p-4">
138
- <div className="flex items-center justify-between gap-2">
139
- <span className="flex size-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
140
- <Icon className="size-4" aria-hidden="true" />
141
- </span>
142
- {count != null && count > 0 ? (
143
- <Badge variant="default">{count}</Badge>
144
- ) : null}
145
- </div>
146
- <div className="space-y-0.5">
147
- <CardTitle className="text-base font-semibold">{meta.label}</CardTitle>
148
- <CardDescription>{meta.detail}</CardDescription>
149
- </div>
150
- </CardHeader>
151
- </Card>
152
- </Link>
153
- );
154
- })}
155
- </div>
156
- </section>
157
-
79
+ <div aria-label="Notification Center feed" className="newsroom-overview-feed-frame rounded-xl border bg-card p-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" data-newsroom-overview-notifications role="region" tabIndex={0}>
80
+ <NotificationCenter
81
+ actorLabel={actorLabel}
82
+ decisions={decisions}
83
+ demo={demo}
84
+ humanCuratedReferenceLineageIds={humanCuratedReferenceLineageIds}
85
+ references={canonicalReferences}
86
+ />
87
+ </div>
158
88
  <section aria-label="Needs attention" className="space-y-3">
159
89
  <h2 className="text-sm font-medium text-muted-foreground">Needs attention</h2>
160
90
  {pendingReferences.length || attentionAssignments.length ? (
@@ -208,16 +138,16 @@ export function NewsroomOpsOverview({
208
138
  ) : (
209
139
  <Card>
210
140
  <CardContent className="p-4 text-sm text-muted-foreground">
211
- Nothing needs attention right now. Check References for new intake or open Assignments from the destinations above.
141
+ No pending references or open assignments. Check those destinations for other work.
212
142
  </CardContent>
213
143
  </Card>
214
144
  )}
215
145
  </section>
216
146
 
217
147
  {desks.length ? (
218
- <section aria-label="Desks" className="space-y-3" data-newsroom-ops-desks>
219
- <h2 className="text-sm font-medium text-muted-foreground">Desks</h2>
220
- {coreDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="core">Core desks</h3> : null}
148
+ <section aria-label="Sections" className="space-y-3" data-newsroom-ops-desks>
149
+ <h2 className="text-sm font-medium text-muted-foreground">Sections</h2>
150
+ {coreDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="core">Core sections</h3> : null}
221
151
  <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
222
152
  {coreDesks.map((section) => (
223
153
  <Link
@@ -237,7 +167,7 @@ export function NewsroomOpsOverview({
237
167
  </Link>
238
168
  ))}
239
169
  </div>
240
- {rotatingDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="rotating">Rotating desks <small>appear for a while, then rotate out</small></h3> : null}
170
+ {rotatingDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="rotating">Rotating sections <small>appear for a while, then rotate out</small></h3> : null}
241
171
  {rotatingDesks.length ? <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
242
172
  {rotatingDesks.map((section) => (
243
173
  <Link
@@ -8,7 +8,7 @@ import { cn } from "../lib/utils";
8
8
  import { useEffect, useId, useState, type ReactNode } from "react";
9
9
  import Link from "next/link";
10
10
  import { usePathname, useRouter, useSearchParams } from "next/navigation";
11
- import { BookOpen, ClipboardList, Menu, Search, MessagesSquare, PanelLeftClose, PanelLeftOpen, Settings, UserRound, LogIn, LogOut } from "lucide-react";
11
+ import { BookOpen, ClipboardList, House, Menu, Search, MessagesSquare, PanelLeftClose, PanelLeftOpen, Settings, UserRound, LogIn, LogOut } from "lucide-react";
12
12
  import { ORGANIZER_NAV_GROUPS, ORGANIZER_PRIMARY_NAV, ORGANIZER_SETTINGS_DESTINATION } from "../lib/newsroom-organizer-nav";
13
13
  import { browserPathToInternalNewsroomPath } from "../lib/newsroom-base-path";
14
14
  import { getNewsroomNavHref } from "../lib/newsroom-nav";
@@ -55,7 +55,7 @@ function OrganizerSearch({ demo }: { demo: boolean }) {
55
55
  </form>;
56
56
  }
57
57
 
58
- const ICONS = { articles: ClipboardList, references: BookOpen, messages: MessagesSquare };
58
+ const ICONS = { overview: House, articles: ClipboardList, references: BookOpen, messages: MessagesSquare };
59
59
 
60
60
  /** Navigation is a presentation layer; existing route and server authorization still own access. */
61
61
  function Navigation({ activePath, demo, admin, surface, onNavigate }: { activePath: string; demo: boolean; admin: boolean; surface: string; onNavigate?: () => void }) {
@@ -23,6 +23,8 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
23
23
  .organizer-sidebar-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.organizer-sidebar-head .organizer-brand{flex:1;min-width:0}
24
24
  .organizer-header-actions{display:flex;align-items:center;gap:8px;min-width:0;max-width:45%;overflow-x:auto}
25
25
  .organizer-main{min-width:0;min-height:0;overflow:auto;overscroll-behavior:contain;flex:1;padding:16px 0}
26
+ .newsroom-overview-feed-frame{height:auto}
27
+ @media(min-width:640px){.newsroom-overview-feed-frame{height:38rem;overflow-y:auto}}
26
28
  .organizer-main>[data-category-steering]{min-height:0}.organizer-demo-notice{font-size:11px;color:var(--org-muted);margin-bottom:12px}
27
29
  .organizer-account{display:flex;align-items:center;gap:10px;min-height:44px;flex-shrink:0;border:1px solid var(--org-line);padding:8px;text-align:left;border-radius:3px}
28
30
  .organizer-account span{min-width:0}.organizer-account strong{display:block;font:600 11px var(--kb-theme-ui-font);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.organizer-account small{display:block;font-size:9px;color:var(--org-muted);margin-top:2px}
@@ -313,3 +315,34 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
313
315
  .news-desk-columns--administration .news-desk-delegation-decision button:focus-visible{outline:2px solid var(--org-ink);outline-offset:2px}
314
316
  @container (max-width:640px){.news-desk-columns--administration .news-desk-delegation-pending-list li{grid-template-columns:minmax(0,1fr)}.news-desk-columns--administration .news-desk-delegation-code{font-size:28px}}
315
317
  @container (max-width:640px){.news-desk-columns--administration .news-desk-delegation-form{grid-template-columns:minmax(0,1fr)}.news-desk-columns--administration .news-desk-delegation-list li{grid-template-columns:minmax(0,1fr)}}
318
+ .news-desk-columns--administration .news-desk-monitoring__scope{display:flex;flex-wrap:wrap;gap:8px}
319
+ .news-desk-columns--administration .news-desk-monitoring__scope button{min-height:36px;padding:4px 12px;border:1px solid var(--org-line);background:transparent;color:var(--administration-strong);font:600 12px/1.2 var(--kb-theme-ui-font);letter-spacing:.05em;text-transform:uppercase;cursor:pointer}
320
+ .news-desk-columns--administration .news-desk-monitoring__scope button[aria-pressed=true]{border-color:var(--org-ink);border-width:2px;text-decoration:underline;text-underline-offset:4px}
321
+ .news-desk-columns--administration .news-desk-monitoring__list,.news-desk-columns--administration .news-desk-monitoring__queue,.news-desk-columns--administration .news-desk-monitoring__relevance{list-style:none;margin:0;padding:0;display:grid}
322
+ .news-desk-columns--administration .news-desk-monitoring__list li{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr) minmax(0,2fr);gap:4px 16px;align-items:baseline;padding:10px 0;border-top:1px solid var(--org-line);min-width:0;overflow-wrap:anywhere}
323
+ .news-desk-columns--administration .news-desk-monitoring__list li>div{display:grid;gap:2px}
324
+ .news-desk-columns--administration .news-desk-monitoring__queue>li{display:grid;gap:6px;padding:12px 0;border-top:1px solid var(--org-line);min-width:0}
325
+ .news-desk-columns--administration .news-desk-monitoring__queue>li>div{display:grid;gap:2px}
326
+ .news-desk-columns--administration .news-desk-monitoring__queue>li>div>span,.news-desk-columns--administration .news-desk-monitoring__routing{margin:0;font:400 12px/1.4 var(--kb-theme-ui-font);color:var(--administration-quiet)}
327
+ .news-desk-columns--administration .news-desk-monitoring__queue li[data-routing=error]{padding-left:12px;border-left:3px solid var(--org-ink)}
328
+ .news-desk-columns--administration .news-desk-monitoring__relevance{gap:4px;padding-left:12px;border-left:1px solid var(--org-line)}
329
+ .news-desk-columns--administration .news-desk-monitoring__relevance li{display:flex;flex-wrap:wrap;gap:4px 12px;font:400 13px/1.4 var(--kb-theme-ui-font)}
330
+ .news-desk-columns--administration .news-desk-monitoring__relevance li[data-delivery=failed] span:last-child{font-weight:700}
331
+ .news-desk-columns--administration .news-desk-monitoring__usage{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px 24px;margin:0}
332
+ .news-desk-columns--administration .news-desk-monitoring__usage dt{font:600 11px/1.4 var(--kb-theme-ui-font);letter-spacing:.07em;text-transform:uppercase;color:var(--administration-quiet)}
333
+ .news-desk-columns--administration .news-desk-monitoring__usage dd{margin:0;font:700 26px/1.1 var(--kb-theme-title-font);font-variant-numeric:tabular-nums}
334
+ @container (max-width:640px){.news-desk-columns--administration .news-desk-monitoring__list li{grid-template-columns:minmax(0,1fr)}}
335
+
336
+ /* Newsletter panel (components/newsletter-panel.tsx): reuses the administration card, row and button styles above. */
337
+ .news-desk-columns--administration .news-desk-newsletter__quiet{font:400 12px/1.4 var(--kb-theme-ui-font);color:var(--administration-quiet)}
338
+ .news-desk-columns--administration .news-desk-monitoring__usage>div{display:grid;gap:4px;align-content:start}
339
+ .news-desk-columns--administration .news-desk-newsletter__links{display:flex;flex-wrap:wrap;gap:4px 8px}
340
+ .news-desk-columns--administration .news-desk-newsletter__table{width:100%;border-collapse:collapse;font:400 13px/1.4 var(--kb-theme-ui-font)}
341
+ .news-desk-columns--administration .news-desk-newsletter__table td:first-child{word-break:keep-all;overflow-wrap:break-word}
342
+ .news-desk-columns--administration .news-desk-newsletter__table th{text-align:left;padding:6px 8px;border-bottom:2px solid var(--administration-strong);font:600 11px/1.4 var(--kb-theme-ui-font);letter-spacing:.07em;text-transform:uppercase;color:var(--administration-quiet)}
343
+ .news-desk-columns--administration .news-desk-newsletter__table td{padding:8px;border-bottom:1px solid var(--org-line);vertical-align:top}
344
+ .news-desk-columns--administration .news-desk-newsletter__drawer{grid-column:1/-1;display:grid;gap:12px;padding:12px 0 0;border-top:1px dashed var(--org-line);min-width:0}
345
+ .news-desk-columns--administration .news-desk-newsletter__frame{width:100%;height:480px;border:1px solid var(--org-line);background:#fff}
346
+ .news-desk-columns--administration .news-desk-newsletter__text{margin:0;padding:12px;max-height:280px;overflow:auto;border:1px solid var(--org-line);font:400 12px/1.5 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere}
347
+ .news-desk-columns--administration .news-desk-newsletter__filters{display:flex;flex-wrap:wrap;gap:12px 24px}
348
+ .news-desk-columns--administration .news-desk-newsletter__filters .news-desk-doctrine-card__field{min-width:180px}
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ /** Placeholder geometry for the overview while private records or access are unresolved. */
6
+ export function NewsroomOverviewSkeleton() {
7
+ return (
8
+ <div aria-label="Loading newsroom overview" className="space-y-8 font-sans" data-newsroom-overview-skeleton role="status">
9
+ <section className="newsroom-overview-feed-frame space-y-4 overflow-hidden rounded-xl border bg-card p-4" aria-label="Notification Center">
10
+ <div className="h-6 w-48 rounded bg-muted" />
11
+ <div className="h-4 w-3/4 max-w-xl rounded bg-muted" />
12
+ <div className="h-4 w-2/3 max-w-lg rounded bg-muted" />
13
+ <div className="grid gap-3 sm:grid-cols-2">
14
+ {Array.from({ length: 4 }, (_, index) => <div className="h-28 rounded-xl border bg-card p-4" key={index}>
15
+ <div className="h-4 w-2/3 rounded bg-muted" /><div className="mt-4 h-3 w-1/2 rounded bg-muted" />
16
+ </div>)}
17
+ </div>
18
+ </section>
19
+ <section aria-label="Needs attention" className="space-y-3">
20
+ <div className="h-4 w-32 rounded bg-muted" />
21
+ <div className="h-16 rounded-xl border bg-card p-4"><div className="h-4 w-1/2 rounded bg-muted" /><div className="mt-2 h-3 w-1/3 rounded bg-muted" /></div>
22
+ </section>
23
+ </div>
24
+ );
25
+ }