@anthusai/papyrus 0.0.0 → 1.0.0-next.1

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 (254) hide show
  1. package/LICENSE +21 -0
  2. package/amplify/auth/resource.js +61 -0
  3. package/amplify/auth/resource.ts +65 -0
  4. package/amplify/data/resource.js +2037 -0
  5. package/amplify/data/resource.ts +2148 -0
  6. package/amplify/data/schema.js +1850 -0
  7. package/amplify/data/schema.ts +1953 -0
  8. package/amplify/functions/assignment-action/handler.ts +1416 -0
  9. package/amplify/functions/assignment-action/resource.js +8 -0
  10. package/amplify/functions/assignment-action/resource.ts +9 -0
  11. package/amplify/functions/category-action/handler.ts +2259 -0
  12. package/amplify/functions/category-action/resource.js +8 -0
  13. package/amplify/functions/category-action/resource.ts +9 -0
  14. package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
  15. package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
  16. package/amplify/functions/console-chat-responder/Dockerfile +19 -0
  17. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
  18. package/amplify/functions/console-chat-responder/resource.js +153 -0
  19. package/amplify/functions/console-chat-responder/resource.ts +179 -0
  20. package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
  21. package/amplify/functions/email-submission-processor/handler.py +46 -0
  22. package/amplify/functions/email-submission-processor/requirements.txt +2 -0
  23. package/amplify/functions/email-submission-processor/resource.js +56 -0
  24. package/amplify/functions/email-submission-processor/resource.ts +69 -0
  25. package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
  26. package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
  27. package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
  28. package/amplify/functions/knowledge-query/handler.py +1 -0
  29. package/amplify/functions/knowledge-query/requirements.txt +3 -0
  30. package/amplify/functions/knowledge-query/resource.js +50 -0
  31. package/amplify/functions/knowledge-query/resource.ts +61 -0
  32. package/amplify/functions/manage-user-role/handler.ts +799 -0
  33. package/amplify/functions/manage-user-role/resource.js +8 -0
  34. package/amplify/functions/manage-user-role/resource.ts +9 -0
  35. package/amplify/functions/model-attachment-upload/handler.ts +387 -0
  36. package/amplify/functions/model-attachment-upload/resource.js +8 -0
  37. package/amplify/functions/model-attachment-upload/resource.ts +9 -0
  38. package/amplify/functions/newsroom-summary/handler.ts +321 -0
  39. package/amplify/functions/newsroom-summary/resource.js +8 -0
  40. package/amplify/functions/newsroom-summary/resource.ts +9 -0
  41. package/amplify/functions/procedure-action/handler.ts +621 -0
  42. package/amplify/functions/procedure-action/resource.js +8 -0
  43. package/amplify/functions/procedure-action/resource.ts +9 -0
  44. package/amplify/functions/reader-settings/handler.ts +322 -0
  45. package/amplify/functions/reader-settings/resource.js +8 -0
  46. package/amplify/functions/reader-settings/resource.ts +9 -0
  47. package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
  48. package/amplify/functions/ses-inbound-receive/resource.js +8 -0
  49. package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
  50. package/amplify/functions/shared/amplify-secrets.ts +65 -0
  51. package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
  52. package/amplify/functions/shared/email-mime-intake.ts +196 -0
  53. package/amplify/functions/shared/email-submission.ts +415 -0
  54. package/amplify/functions/shared/inbound-email-intake.ts +37 -0
  55. package/amplify/functions/shared/lambda-data-client.ts +21 -0
  56. package/amplify/functions/shared/model-payloads.ts +186 -0
  57. package/amplify/functions/shared/python-bundle.js +115 -0
  58. package/amplify/functions/shared/python-bundle.ts +141 -0
  59. package/amplify/functions/shared/slack-events.ts +111 -0
  60. package/amplify/functions/slack-delivery/handler.py +112 -0
  61. package/amplify/functions/slack-delivery/requirements.txt +2 -0
  62. package/amplify/functions/slack-delivery/resource.js +38 -0
  63. package/amplify/functions/slack-delivery/resource.ts +51 -0
  64. package/amplify/functions/slack-events/handler.ts +155 -0
  65. package/amplify/functions/slack-events/resource.js +11 -0
  66. package/amplify/functions/slack-events/resource.ts +12 -0
  67. package/amplify/inbound-email/stack.js +46 -0
  68. package/amplify/inbound-email/stack.ts +61 -0
  69. package/amplify/package.json +3 -0
  70. package/amplify/site-backend-config.js +1 -0
  71. package/amplify/site-backend-config.ts +30 -0
  72. package/amplify/site-backend.js +587 -0
  73. package/amplify/site-backend.ts +704 -0
  74. package/amplify/storage/resource.js +18 -0
  75. package/amplify/storage/resource.ts +19 -0
  76. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
  77. package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
  78. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
  79. package/app/[year]/[month]/[day]/page.tsx +22 -0
  80. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
  81. package/app/api/archive/editions/route.ts +18 -0
  82. package/app/api/media/[...path]/route.ts +35 -0
  83. package/app/api/revalidate/route.ts +94 -0
  84. package/app/archive/page.tsx +24 -0
  85. package/app/articles/[slug]/page.tsx +35 -0
  86. package/app/globals.css +10329 -0
  87. package/app/layout.tsx +146 -0
  88. package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
  89. package/app/newsroom/[section]/page.tsx +47 -0
  90. package/app/newsroom/forum/[threadId]/page.tsx +35 -0
  91. package/app/newsroom/insights/[threadId]/page.tsx +22 -0
  92. package/app/newsroom/insights/page.tsx +14 -0
  93. package/app/newsroom/layout.tsx +12 -0
  94. package/app/newsroom/page.tsx +31 -0
  95. package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
  96. package/app/page.tsx +272 -0
  97. package/app/settings/page.tsx +14 -0
  98. package/app/tailwind.css +65 -0
  99. package/bin/papyrus-app.mjs +10 -0
  100. package/bin/papyrus-infra.mjs +39 -0
  101. package/components/ai-elements/conversation.tsx +168 -0
  102. package/components/ai-elements/message.tsx +360 -0
  103. package/components/ai-elements/model-selector.tsx +30 -0
  104. package/components/ai-elements/prompt-input.tsx +1461 -0
  105. package/components/ai-elements/shimmer.tsx +76 -0
  106. package/components/ai-elements/suggestion.tsx +54 -0
  107. package/components/ai-elements/tool.tsx +173 -0
  108. package/components/amplify-client-provider.tsx +140 -0
  109. package/components/archive-grid.tsx +89 -0
  110. package/components/archive-shell.tsx +41 -0
  111. package/components/article-video.tsx +47 -0
  112. package/components/blog-page-background.tsx +20 -0
  113. package/components/dev-sandbox-editor-sign-in.tsx +60 -0
  114. package/components/news-desk-client-provider.tsx +211 -0
  115. package/components/news-desk-page.tsx +119 -0
  116. package/components/news-desk-taxonomy-client.ts +3684 -0
  117. package/components/newspaper.tsx +1929 -0
  118. package/components/newsroom-assignments-view.tsx +664 -0
  119. package/components/newsroom-ops-overview.tsx +226 -0
  120. package/components/newsroom-ops-shell.tsx +319 -0
  121. package/components/newsroom-references-view.tsx +598 -0
  122. package/components/newsroom-topics-view.tsx +425 -0
  123. package/components/papyrus-console-shell.tsx +1912 -0
  124. package/components/pictogram-figure.tsx +65 -0
  125. package/components/presentation-footer.tsx +146 -0
  126. package/components/reader-auth-control.tsx +162 -0
  127. package/components/reader-auth-state.ts +122 -0
  128. package/components/reader-settings.ts +332 -0
  129. package/components/reader-theme-control.tsx +85 -0
  130. package/components/reference-source-preview.tsx +163 -0
  131. package/components/settings-page.tsx +239 -0
  132. package/components/topic-steering-workspace.tsx +18063 -0
  133. package/components/ui/badge.tsx +30 -0
  134. package/components/ui/button-group.tsx +87 -0
  135. package/components/ui/button.tsx +58 -0
  136. package/components/ui/card.tsx +32 -0
  137. package/components/ui/command.tsx +194 -0
  138. package/components/ui/dialog.tsx +158 -0
  139. package/components/ui/dropdown-menu.tsx +266 -0
  140. package/components/ui/hover-card.tsx +51 -0
  141. package/components/ui/input-group.tsx +158 -0
  142. package/components/ui/input.tsx +20 -0
  143. package/components/ui/scroll-area.tsx +16 -0
  144. package/components/ui/select.tsx +199 -0
  145. package/components/ui/separator.tsx +23 -0
  146. package/components/ui/sheet.tsx +92 -0
  147. package/components/ui/spinner.tsx +10 -0
  148. package/components/ui/tabs.tsx +113 -0
  149. package/components/ui/textarea.tsx +18 -0
  150. package/components/ui/tooltip.tsx +66 -0
  151. package/components/use-resolved-papyrus-theme.ts +47 -0
  152. package/infra/amplify-app-shell.js +109 -0
  153. package/infra/amplify-app-shell.ts +155 -0
  154. package/infra/index.js +12 -0
  155. package/lib/amplify-outputs-guard.ts +24 -0
  156. package/lib/amplify-outputs-path.ts +16 -0
  157. package/lib/amplify-server-runtime.ts +63 -0
  158. package/lib/archive-data.ts +30 -0
  159. package/lib/archive-types.ts +15 -0
  160. package/lib/articles.ts +320 -0
  161. package/lib/cached-content-repository.ts +89 -0
  162. package/lib/category-dashboard.ts +37 -0
  163. package/lib/category-repository.ts +1907 -0
  164. package/lib/console-chat-client.ts +1316 -0
  165. package/lib/console-web-context.ts +118 -0
  166. package/lib/content-load-timing.ts +23 -0
  167. package/lib/content-repository.ts +40 -0
  168. package/lib/content-types.ts +104 -0
  169. package/lib/define-site.js +3 -0
  170. package/lib/define-site.ts +21 -0
  171. package/lib/demo-amplify-outputs.ts +15 -0
  172. package/lib/dev-sandbox-editor-auth.ts +18 -0
  173. package/lib/doctrine.ts +128 -0
  174. package/lib/edition-routes.ts +155 -0
  175. package/lib/edition-sections.ts +111 -0
  176. package/lib/empty-edition-layout-plan.ts +109 -0
  177. package/lib/excerpts.ts +8 -0
  178. package/lib/graphql-content-repository.ts +986 -0
  179. package/lib/image-url.ts +10 -0
  180. package/lib/layout-plan.ts +981 -0
  181. package/lib/layout-scenarios.ts +564 -0
  182. package/lib/markus-body.ts +168 -0
  183. package/lib/markus-ir.ts +235 -0
  184. package/lib/markus-projection.ts +243 -0
  185. package/lib/markus-to-article.ts +93 -0
  186. package/lib/news-desk-session.ts +160 -0
  187. package/lib/newspaper-layout.ts +3459 -0
  188. package/lib/newsroom-assignments.ts +173 -0
  189. package/lib/newsroom-base-path.ts +48 -0
  190. package/lib/newsroom-card-layout.ts +102 -0
  191. package/lib/newsroom-category-drilldown.ts +133 -0
  192. package/lib/newsroom-demo-dashboard.ts +267 -0
  193. package/lib/newsroom-demo-profile.ts +46 -0
  194. package/lib/newsroom-forum-routes.ts +153 -0
  195. package/lib/newsroom-index-filters.ts +202 -0
  196. package/lib/newsroom-knowledge-query-request.ts +51 -0
  197. package/lib/newsroom-list-selection.ts +42 -0
  198. package/lib/newsroom-nav.ts +42 -0
  199. package/lib/newsroom-references.ts +116 -0
  200. package/lib/newsroom-sections.ts +52 -0
  201. package/lib/newsroom-topics.ts +118 -0
  202. package/lib/papyrus-object-kinds.ts +24 -0
  203. package/lib/papyrus-web-locations.ts +480 -0
  204. package/lib/pilobol-sample.ts +167 -0
  205. package/lib/presentation-footer.ts +92 -0
  206. package/lib/pretext-layout.ts +179 -0
  207. package/lib/public-placeholder-config.ts +102 -0
  208. package/lib/publication-items.ts +181 -0
  209. package/lib/reader-route-config.ts +1 -0
  210. package/lib/reader-static-params.ts +56 -0
  211. package/lib/reader-storage-url.ts +63 -0
  212. package/lib/reference-display.ts +74 -0
  213. package/lib/reference-policy.ts +97 -0
  214. package/lib/reference-source-preview.ts +175 -0
  215. package/lib/renderer-config.ts +50 -0
  216. package/lib/renderer.ts +87 -0
  217. package/lib/reporting-story-budget.ts +638 -0
  218. package/lib/semantic-graph.ts +554 -0
  219. package/lib/site-brand-tenant.ts +41 -0
  220. package/lib/site-brand.ts +179 -0
  221. package/lib/site-renderer.ts +17 -0
  222. package/lib/site-stack.ts +195 -0
  223. package/lib/themed-image.ts +30 -0
  224. package/lib/utils.ts +6 -0
  225. package/middleware.ts +78 -0
  226. package/package.json +296 -1
  227. package/publications/anth_us/brand.ts +68 -0
  228. package/publications/anth_us/theme.css +66 -0
  229. package/publications/papyrus/theme.css +11 -0
  230. package/publications/pilobol_us/brand.ts +37 -0
  231. package/publications/pilobol_us/theme.css +140 -0
  232. package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
  233. package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
  234. package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
  235. package/publications/threat_intelligence/brand.ts +40 -0
  236. package/publications/threat_intelligence/pictograms/art.tsx +608 -0
  237. package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
  238. package/publications/threat_intelligence/pictograms/registry.ts +30 -0
  239. package/publications/threat_intelligence/pictograms/system.tsx +305 -0
  240. package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
  241. package/publications/threat_intelligence/theme.css +1440 -0
  242. package/renderers/markus/index.ts +25 -0
  243. package/renderers/markus/stubs.tsx +33 -0
  244. package/renderers/pretext/article-page.tsx +174 -0
  245. package/renderers/pretext/index.ts +30 -0
  246. package/renderers/pretext/layouts.ts +16 -0
  247. package/renderers/pretext/presentation-shell.tsx +477 -0
  248. package/routes.manifest.json +182 -0
  249. package/scripts/fixtures/pilobol-sample.ast.json +234 -0
  250. package/src/backend.js +1 -0
  251. package/src/backend.ts +3 -0
  252. package/src/sync.mjs +87 -0
  253. package/src/with-papyrus.d.mts +2 -0
  254. package/src/with-papyrus.mjs +47 -0
@@ -0,0 +1,226 @@
1
+ "use client";
2
+
3
+ import {
4
+ BookOpenIcon,
5
+ ClipboardListIcon,
6
+ LayersIcon,
7
+ MessageSquareIcon,
8
+ } from "lucide-react";
9
+ import Link from "next/link";
10
+ import { usePathname } from "next/navigation";
11
+ import { useMemo } from "react";
12
+ import { Badge } from "./ui/badge";
13
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
14
+ import type { AssignmentRecord, MessageRecord, NewsroomSectionRecord, ReferenceRecord } from "../lib/category-repository";
15
+ import { getNewsroomNavHref } from "../lib/newsroom-nav";
16
+ import {
17
+ countReferencesByStatus,
18
+ filterReferencesByStatus,
19
+ formatReferenceListDate,
20
+ referenceDisplayTitle,
21
+ referenceLineageId,
22
+ selectCanonicalReferenceRecords,
23
+ } from "../lib/newsroom-references";
24
+ import {
25
+ buildNewsroomSectionHref,
26
+ displayNewsroomSectionShortTitle,
27
+ isEnabledNewsroomSection,
28
+ sortNewsroomSections,
29
+ } from "../lib/newsroom-sections";
30
+ import { newsroomInboxRowClassName } from "../lib/newsroom-list-selection";
31
+
32
+ type NewsroomOpsOverviewProps = {
33
+ assignments: AssignmentRecord[];
34
+ demo?: boolean;
35
+ messages: MessageRecord[];
36
+ newsroomSections: NewsroomSectionRecord[];
37
+ references: ReferenceRecord[];
38
+ };
39
+
40
+ type DestinationKey = "references" | "assignments" | "messages" | "topics";
41
+
42
+ const DESTINATION_META: Record<DestinationKey, { label: string; detail: string; href: string; icon: typeof BookOpenIcon }> = {
43
+ references: { label: "References", detail: "Source intake", href: "/newsroom/references", icon: BookOpenIcon },
44
+ assignments: { label: "Assignments", detail: "Work queue", href: "/newsroom/assignments", icon: ClipboardListIcon },
45
+ messages: { label: "Messages", detail: "Team threads", href: "/newsroom/messages", icon: MessageSquareIcon },
46
+ topics: { label: "Topics", detail: "Taxonomy", href: "/newsroom/topics", icon: LayersIcon },
47
+ };
48
+
49
+ function openAssignmentCount(assignments: AssignmentRecord[]): number {
50
+ return assignments.filter((assignment) => {
51
+ const status = assignment.status?.trim().toLowerCase() ?? "";
52
+ return status === "open" || status === "claimed" || status === "pending";
53
+ }).length;
54
+ }
55
+
56
+ function recentMessageCount(messages: MessageRecord[]): number {
57
+ return messages.filter((message) => message.status !== "archived").length;
58
+ }
59
+
60
+ export function NewsroomOpsOverview({
61
+ assignments,
62
+ demo = false,
63
+ messages,
64
+ newsroomSections,
65
+ references,
66
+ }: NewsroomOpsOverviewProps) {
67
+ const pathname = usePathname();
68
+ const canonicalReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
69
+ const referenceCounts = useMemo(() => countReferencesByStatus(canonicalReferences), [canonicalReferences]);
70
+ const pendingReferences = useMemo(
71
+ () => filterReferencesByStatus(canonicalReferences, "pending").slice(0, 5),
72
+ [canonicalReferences],
73
+ );
74
+ const attentionAssignments = useMemo(
75
+ () => assignments
76
+ .filter((assignment) => {
77
+ const status = assignment.status?.trim().toLowerCase() ?? "";
78
+ return status === "open" || status === "claimed" || status === "pending";
79
+ })
80
+ .slice(0, 4),
81
+ [assignments],
82
+ );
83
+ const desks = useMemo(
84
+ () => sortNewsroomSections(newsroomSections).filter(isEnabledNewsroomSection),
85
+ [newsroomSections],
86
+ );
87
+
88
+ const destinationCounts: Record<DestinationKey, number | null> = {
89
+ references: referenceCounts.pending ?? 0,
90
+ assignments: openAssignmentCount(assignments),
91
+ messages: recentMessageCount(messages),
92
+ topics: null,
93
+ };
94
+
95
+ return (
96
+ <div
97
+ className="space-y-8 font-sans"
98
+ data-news-desk-section="overview"
99
+ data-newsroom-ops-home="true"
100
+ >
101
+ <div className="space-y-8" data-newsroom-overview-feeds>
102
+ <section aria-label="Destinations" className="space-y-3">
103
+ <h2 className="text-sm font-medium text-muted-foreground">Go to</h2>
104
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
105
+ {(Object.keys(DESTINATION_META) as DestinationKey[]).map((key) => {
106
+ const meta = DESTINATION_META[key];
107
+ const Icon = meta.icon;
108
+ const count = destinationCounts[key];
109
+ const href = getNewsroomNavHref(meta.href, demo);
110
+ return (
111
+ <Link
112
+ className="group block rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
113
+ data-newsroom-ops-destination={key}
114
+ data-newsroom-overview-section={key}
115
+ href={href}
116
+ key={key}
117
+ >
118
+ <Card className="h-full transition-colors group-hover:border-primary/40">
119
+ <CardHeader className="space-y-3 p-4">
120
+ <div className="flex items-center justify-between gap-2">
121
+ <span className="flex size-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
122
+ <Icon className="size-4" aria-hidden="true" />
123
+ </span>
124
+ {count != null && count > 0 ? (
125
+ <Badge variant="default">{count}</Badge>
126
+ ) : null}
127
+ </div>
128
+ <div className="space-y-0.5">
129
+ <CardTitle className="text-base font-semibold">{meta.label}</CardTitle>
130
+ <CardDescription>{meta.detail}</CardDescription>
131
+ </div>
132
+ </CardHeader>
133
+ </Card>
134
+ </Link>
135
+ );
136
+ })}
137
+ </div>
138
+ </section>
139
+
140
+ <section aria-label="Needs attention" className="space-y-3">
141
+ <h2 className="text-sm font-medium text-muted-foreground">Needs attention</h2>
142
+ {pendingReferences.length || attentionAssignments.length ? (
143
+ <div className="space-y-2">
144
+ {pendingReferences.map((reference) => {
145
+ const lineageId = referenceLineageId(reference);
146
+ const href = getNewsroomNavHref(`/newsroom/references/${encodeURIComponent(lineageId)}`, demo);
147
+ const active = pathname === href || pathname.startsWith(`${href}/`);
148
+ return (
149
+ <Link
150
+ aria-current={active ? "true" : undefined}
151
+ className={newsroomInboxRowClassName(active)}
152
+ data-newsroom-inbox-item="reference"
153
+ data-newsroom-list-row
154
+ data-selected={active || undefined}
155
+ data-newsroom-card
156
+ data-newsroom-card-id={lineageId}
157
+ href={href}
158
+ key={lineageId}
159
+ >
160
+ <Badge className="mt-0.5 shrink-0" variant="outline">Pending</Badge>
161
+ <div className="min-w-0 flex-1">
162
+ <p className="truncate text-sm font-medium text-foreground">{referenceDisplayTitle(reference)}</p>
163
+ <p className="text-xs text-muted-foreground">Review source intake · {formatReferenceListDate(reference)}</p>
164
+ </div>
165
+ </Link>
166
+ );
167
+ })}
168
+ {attentionAssignments.map((assignment) => {
169
+ const href = getNewsroomNavHref(`/newsroom/assignments/${encodeURIComponent(assignment.id)}`, demo);
170
+ const active = pathname === href || pathname.startsWith(`${href}/`);
171
+ return (
172
+ <Link
173
+ aria-current={active ? "true" : undefined}
174
+ className={newsroomInboxRowClassName(active)}
175
+ data-newsroom-inbox-item="assignment"
176
+ data-newsroom-list-row
177
+ data-selected={active || undefined}
178
+ href={href}
179
+ key={assignment.id}
180
+ >
181
+ <Badge className="mt-0.5 shrink-0" variant="secondary">{assignment.status}</Badge>
182
+ <div className="min-w-0 flex-1">
183
+ <p className="truncate text-sm font-medium text-foreground">{assignment.title}</p>
184
+ <p className="text-xs text-muted-foreground">Assignment · {assignment.assignmentTypeKey}</p>
185
+ </div>
186
+ </Link>
187
+ );
188
+ })}
189
+ </div>
190
+ ) : (
191
+ <Card>
192
+ <CardContent className="p-4 text-sm text-muted-foreground">
193
+ Nothing needs attention right now. Check References for new intake or open Assignments from the destinations above.
194
+ </CardContent>
195
+ </Card>
196
+ )}
197
+ </section>
198
+
199
+ {desks.length ? (
200
+ <section aria-label="Desks" className="space-y-3" data-newsroom-ops-desks>
201
+ <h2 className="text-sm font-medium text-muted-foreground">Desks</h2>
202
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
203
+ {desks.map((section) => (
204
+ <Link
205
+ className="block rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
206
+ data-newsroom-ops-desk={section.id}
207
+ href={buildNewsroomSectionHref(section.id, demo)}
208
+ key={section.id}
209
+ >
210
+ <Card className="h-full transition-colors hover:border-primary/30">
211
+ <CardHeader className="p-4">
212
+ <CardTitle className="text-base font-semibold">{section.title}</CardTitle>
213
+ <CardDescription className="uppercase tracking-wide text-[0.65rem]">
214
+ {displayNewsroomSectionShortTitle(section)}
215
+ </CardDescription>
216
+ </CardHeader>
217
+ </Card>
218
+ </Link>
219
+ ))}
220
+ </div>
221
+ </section>
222
+ ) : null}
223
+ </div>
224
+ </div>
225
+ );
226
+ }
@@ -0,0 +1,319 @@
1
+ "use client";
2
+
3
+ import {
4
+ BookOpenIcon,
5
+ ClipboardListIcon,
6
+ LayoutGridIcon,
7
+ MenuIcon,
8
+ SearchIcon,
9
+ TagsIcon,
10
+ } from "lucide-react";
11
+ import Link from "next/link";
12
+ import type { ReactNode } from "react";
13
+ import { useMemo, useState } from "react";
14
+ import { Badge } from "./ui/badge";
15
+ import { Button, buttonVariants } from "./ui/button";
16
+ import { ScrollArea } from "./ui/scroll-area";
17
+ import { Separator } from "./ui/separator";
18
+ import {
19
+ Sheet,
20
+ SheetContent,
21
+ SheetHeader,
22
+ SheetTitle,
23
+ SheetTrigger,
24
+ } from "./ui/sheet";
25
+ import {
26
+ getNewsroomNavHref,
27
+ NEWSROOM_MOBILE_PRIMARY_NAV,
28
+ NEWSROOM_OPS_NAV,
29
+ type NewsroomNavTabId,
30
+ } from "../lib/newsroom-nav";
31
+ import {
32
+ NEWSROOM_SIDEBAR_NAV_ACTIVE,
33
+ NEWSROOM_SIDEBAR_NAV_INACTIVE,
34
+ } from "../lib/newsroom-list-selection";
35
+ import { cn } from "../lib/utils";
36
+
37
+ export type NewsroomNavCount = {
38
+ count: number | null;
39
+ missing?: boolean;
40
+ visible?: boolean;
41
+ };
42
+
43
+ type NewsroomOpsShellProps = {
44
+ activeTab: NewsroomNavTabId | string;
45
+ appTitle: string;
46
+ backHref?: string;
47
+ backLabel: string;
48
+ children: ReactNode;
49
+ demo?: boolean;
50
+ headerActions?: ReactNode;
51
+ pageTitle: ReactNode;
52
+ showNavigation?: boolean;
53
+ tabCounts?: Partial<Record<string, NewsroomNavCount>>;
54
+ };
55
+
56
+ const MOBILE_TAB_ICONS: Record<string, typeof LayoutGridIcon> = {
57
+ overview: LayoutGridIcon,
58
+ assignments: ClipboardListIcon,
59
+ references: BookOpenIcon,
60
+ topics: TagsIcon,
61
+ };
62
+
63
+ function formatCountLabel(count: number | null, missing?: boolean): string {
64
+ if (missing) return "?";
65
+ if (count == null) return "…";
66
+ if (count >= 1000) return `${Math.round(count / 100) / 10}k`;
67
+ return String(count);
68
+ }
69
+
70
+ function NavCountBadge({
71
+ countState,
72
+ }: {
73
+ countState?: NewsroomNavCount;
74
+ }) {
75
+ if (!countState || countState.visible === false) return null;
76
+ return (
77
+ <Badge
78
+ className="newsroom-nav-count min-w-7 px-1.5"
79
+ data-newsroom-nav-count
80
+ variant={countState.missing ? "outline" : "secondary"}
81
+ >
82
+ {formatCountLabel(countState.count, countState.missing)}
83
+ </Badge>
84
+ );
85
+ }
86
+
87
+ function SidebarNavLink({
88
+ active,
89
+ countState,
90
+ demo,
91
+ item,
92
+ onNavigate,
93
+ }: {
94
+ active: boolean;
95
+ countState?: NewsroomNavCount;
96
+ demo?: boolean;
97
+ item: (typeof NEWSROOM_OPS_NAV)[number];
98
+ onNavigate?: () => void;
99
+ }) {
100
+ return (
101
+ <Link
102
+ aria-current={active ? "page" : undefined}
103
+ className={cn(
104
+ buttonVariants({ variant: "ghost", size: "default" }),
105
+ "h-auto min-h-11 w-full justify-start gap-3 px-3 py-2.5 text-left font-normal",
106
+ active ? NEWSROOM_SIDEBAR_NAV_ACTIVE : NEWSROOM_SIDEBAR_NAV_INACTIVE,
107
+ )}
108
+ data-news-desk-tab={item.id}
109
+ href={getNewsroomNavHref(item.href, demo)}
110
+ onClick={onNavigate}
111
+ >
112
+ <NavCountBadge countState={countState} />
113
+ <span className="min-w-0 flex-1">
114
+ <span className="block text-sm font-medium">{item.label}</span>
115
+ <span className={cn("block text-xs", active ? "text-foreground/70" : "text-muted-foreground")}>{item.detail}</span>
116
+ </span>
117
+ </Link>
118
+ );
119
+ }
120
+
121
+ export function NewsroomOpsShell({
122
+ activeTab,
123
+ appTitle,
124
+ backHref = "/",
125
+ backLabel,
126
+ children,
127
+ demo = false,
128
+ headerActions,
129
+ pageTitle,
130
+ showNavigation = true,
131
+ tabCounts = {},
132
+ }: NewsroomOpsShellProps) {
133
+ const [menuOpen, setMenuOpen] = useState(false);
134
+ const resolvedActiveTab = activeTab === "desks" ? "topics" : activeTab;
135
+ const mobilePrimary = useMemo(() => NEWSROOM_MOBILE_PRIMARY_NAV, []);
136
+
137
+ return (
138
+ <div
139
+ className="flex h-dvh max-h-dvh flex-col overflow-hidden bg-background text-foreground"
140
+ data-news-desk
141
+ data-newsroom-chrome="app"
142
+ data-newsroom-ops-shell="true"
143
+ >
144
+ <header
145
+ className="flex h-14 shrink-0 items-center gap-3 border-b border-border bg-card/50 px-3 sm:px-4"
146
+ data-newsroom-ops-topbar
147
+ >
148
+ <Link
149
+ className={cn(buttonVariants({ variant: "ghost", size: "sm" }), "shrink-0 px-2")}
150
+ href={backHref}
151
+ >
152
+ {backLabel}
153
+ </Link>
154
+ <div className="min-w-0 flex-1">
155
+ <p className="truncate text-[0.65rem] font-semibold uppercase tracking-[0.12em] text-muted-foreground">
156
+ {appTitle}
157
+ </p>
158
+ <h1 className="truncate text-base font-semibold tracking-tight text-foreground" data-newsroom-ops-title>
159
+ {pageTitle}
160
+ </h1>
161
+ </div>
162
+ <div className="flex shrink-0 items-center gap-1.5">{headerActions}</div>
163
+ </header>
164
+
165
+ <div className="flex min-h-0 flex-1">
166
+ {showNavigation ? (
167
+ <aside
168
+ aria-label="Newsroom sections"
169
+ className="hidden w-64 shrink-0 flex-col border-r border-border bg-muted/20 md:flex"
170
+ data-newsroom-ops-sidebar
171
+ >
172
+ <ScrollArea className="min-h-0 flex-1 p-3">
173
+ <nav className="flex flex-col gap-1">
174
+ {NEWSROOM_OPS_NAV.map((item) => (
175
+ <SidebarNavLink
176
+ active={resolvedActiveTab === item.id}
177
+ countState={tabCounts[item.id]}
178
+ demo={demo}
179
+ item={item}
180
+ key={item.id}
181
+ />
182
+ ))}
183
+ </nav>
184
+ </ScrollArea>
185
+ </aside>
186
+ ) : null}
187
+
188
+ <main className="flex min-h-0 min-w-0 flex-1 flex-col">
189
+ <ScrollArea className="min-h-0 flex-1">
190
+ <div className="mx-auto w-full max-w-6xl p-4 sm:p-6">{children}</div>
191
+ </ScrollArea>
192
+ </main>
193
+ </div>
194
+
195
+ {showNavigation ? (
196
+ <nav
197
+ aria-label="Primary newsroom navigation"
198
+ className="shrink-0 border-t border-border bg-card/95 pb-[max(env(safe-area-inset-bottom),0.35rem)] pt-1 md:hidden"
199
+ data-newsroom-ops-bottom-nav
200
+ >
201
+ <ul className="grid grid-cols-5 gap-1 px-2">
202
+ {mobilePrimary.map((item) => {
203
+ const Icon = MOBILE_TAB_ICONS[item.id] ?? LayoutGridIcon;
204
+ const active = resolvedActiveTab === item.id;
205
+ return (
206
+ <li key={item.id}>
207
+ <Link
208
+ aria-current={active ? "page" : undefined}
209
+ className={cn(
210
+ "flex min-h-14 flex-col items-center justify-center gap-1 rounded-xl px-1 py-2 text-[0.68rem] font-medium",
211
+ active
212
+ ? "bg-primary text-primary-foreground shadow-sm"
213
+ : "text-muted-foreground",
214
+ )}
215
+ data-news-desk-tab={item.id}
216
+ href={getNewsroomNavHref(item.href, demo)}
217
+ >
218
+ <Icon className="size-4" aria-hidden="true" />
219
+ <span>{item.label}</span>
220
+ </Link>
221
+ </li>
222
+ );
223
+ })}
224
+ <li>
225
+ <Sheet onOpenChange={setMenuOpen} open={menuOpen}>
226
+ <SheetTrigger
227
+ className={cn(
228
+ "flex min-h-14 w-full flex-col items-center justify-center gap-1 rounded-xl px-1 py-2 text-[0.68rem] font-medium text-muted-foreground",
229
+ )}
230
+ >
231
+ <MenuIcon className="size-4" aria-hidden="true" />
232
+ <span>More</span>
233
+ </SheetTrigger>
234
+ <SheetContent className="gap-0 p-0" side="bottom">
235
+ <SheetHeader className="border-b border-border px-4 py-4">
236
+ <SheetTitle>All sections</SheetTitle>
237
+ </SheetHeader>
238
+ <ScrollArea className="max-h-[60dvh] p-3">
239
+ <div className="flex flex-col gap-1">
240
+ {NEWSROOM_OPS_NAV.map((item) => (
241
+ <SidebarNavLink
242
+ active={resolvedActiveTab === item.id}
243
+ countState={tabCounts[item.id]}
244
+ demo={demo}
245
+ item={item}
246
+ key={item.id}
247
+ onNavigate={() => setMenuOpen(false)}
248
+ />
249
+ ))}
250
+ </div>
251
+ </ScrollArea>
252
+ </SheetContent>
253
+ </Sheet>
254
+ </li>
255
+ </ul>
256
+ </nav>
257
+ ) : null}
258
+ </div>
259
+ );
260
+ }
261
+
262
+ export function NewsroomOpsSearchButton({
263
+ disabled,
264
+ onPress,
265
+ }: {
266
+ disabled?: boolean;
267
+ onPress?: () => void;
268
+ }) {
269
+ return (
270
+ <Button
271
+ aria-label="Search knowledge base"
272
+ disabled={disabled}
273
+ onClick={onPress}
274
+ size="icon-sm"
275
+ type="button"
276
+ variant="outline"
277
+ >
278
+ <SearchIcon className="size-4" />
279
+ </Button>
280
+ );
281
+ }
282
+
283
+ export function NewsroomOpsStatusBanner({
284
+ children,
285
+ tone = "info",
286
+ }: {
287
+ children: ReactNode;
288
+ tone?: "error" | "info" | "ok";
289
+ }) {
290
+ return (
291
+ <div
292
+ className={cn(
293
+ "mb-4 rounded-xl border px-3 py-2 text-sm",
294
+ tone === "error" && "border-destructive/30 bg-destructive/10 text-destructive",
295
+ tone === "ok" && "border-primary/20 bg-primary/10 text-foreground",
296
+ tone === "info" && "border-border bg-muted/40 text-foreground",
297
+ )}
298
+ role="status"
299
+ >
300
+ {children}
301
+ </div>
302
+ );
303
+ }
304
+
305
+ export function NewsroomOpsSectionIntro({
306
+ description,
307
+ title,
308
+ }: {
309
+ description: string;
310
+ title: string;
311
+ }) {
312
+ return (
313
+ <div className="mb-5 space-y-1">
314
+ <h2 className="text-lg font-semibold tracking-tight">{title}</h2>
315
+ <p className="max-w-2xl text-sm text-muted-foreground">{description}</p>
316
+ <Separator className="mt-4" />
317
+ </div>
318
+ );
319
+ }