@anthusai/papyrus 0.0.0 → 1.0.0-next.10

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