@anthusai/papyrus 0.0.0 → 1.0.0-next.2

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,425 @@
1
+ "use client";
2
+
3
+ import {
4
+ ChevronLeftIcon,
5
+ ChevronRightIcon,
6
+ PencilIcon,
7
+ ThumbsDownIcon,
8
+ ThumbsUpIcon,
9
+ } from "lucide-react";
10
+ import { useCallback, useEffect, useMemo, useState } from "react";
11
+ import type { CategorySteeringProposal } from "../lib/category-repository";
12
+ import { getNewsroomNavHref } from "../lib/newsroom-nav";
13
+ import { newsroomListRowClassName } from "../lib/newsroom-list-selection";
14
+ import {
15
+ countTopicProposalsByStatus,
16
+ filterTopicProposals,
17
+ formatProposalListDate,
18
+ proposalDisplayTitle,
19
+ proposalStatusBadgeVariant,
20
+ proposalStatusLabel,
21
+ selectedTopicProposalById,
22
+ sortTopicProposals,
23
+ topicProposalReviewBlockedReason,
24
+ type TopicProposalReviewAction,
25
+ type TopicProposalStatusFilter,
26
+ } from "../lib/newsroom-topics";
27
+ import { Badge } from "./ui/badge";
28
+ import { Button } from "./ui/button";
29
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
30
+ import { ScrollArea } from "./ui/scroll-area";
31
+ import { Separator } from "./ui/separator";
32
+ import {
33
+ Sheet,
34
+ SheetContent,
35
+ SheetHeader,
36
+ SheetTitle,
37
+ } from "./ui/sheet";
38
+ import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
39
+
40
+ type NewsroomTopicsViewProps = {
41
+ demo?: boolean;
42
+ disabled?: boolean;
43
+ initialProposalId?: string | null;
44
+ onEdit: (proposal: CategorySteeringProposal) => void;
45
+ onReview: (proposal: CategorySteeringProposal, action: TopicProposalReviewAction) => void;
46
+ proposals: CategorySteeringProposal[];
47
+ };
48
+
49
+ const STATUS_FILTERS: Array<{ key: TopicProposalStatusFilter; label: string }> = [
50
+ { key: "all", label: "All" },
51
+ { key: "proposed", label: "Proposed" },
52
+ { key: "deferred", label: "Deferred" },
53
+ { key: "reviewed", label: "Reviewed" },
54
+ ];
55
+
56
+ function buildTopicsDetailPath(proposalId: string, demo?: boolean): string {
57
+ const base = `/newsroom/topics?proposal=${encodeURIComponent(proposalId)}`;
58
+ return getNewsroomNavHref(base, demo);
59
+ }
60
+
61
+ function TopicProposalDetailPanel({
62
+ disabled,
63
+ onClose,
64
+ onEdit,
65
+ onReview,
66
+ proposal,
67
+ }: {
68
+ disabled?: boolean;
69
+ onClose?: () => void;
70
+ onEdit: () => void;
71
+ onReview: (action: TopicProposalReviewAction) => void;
72
+ proposal: CategorySteeringProposal;
73
+ }) {
74
+ const acceptBlockedReason = topicProposalReviewBlockedReason(proposal, "accept");
75
+ const editBlockedReason = topicProposalReviewBlockedReason(proposal, "edit");
76
+ const acceptDisabled = disabled || proposal.status === "accepted" || Boolean(acceptBlockedReason);
77
+ const rejectDisabled = disabled || proposal.status === "rejected";
78
+ const deferDisabled = disabled || proposal.status === "deferred";
79
+ const editDisabled = disabled || proposal.status === "accepted" || Boolean(editBlockedReason);
80
+
81
+ return (
82
+ <div
83
+ className="flex h-full flex-col font-sans"
84
+ data-news-desk-topic-proposal-detail={proposal.id}
85
+ data-news-desk-proposed-subcategory={proposal.categoryKey ?? undefined}
86
+ >
87
+ <div className="space-y-4 p-4">
88
+ <div className="space-y-2">
89
+ <div className="flex flex-wrap items-center gap-2">
90
+ <Badge variant={proposalStatusBadgeVariant(proposal.status)}>
91
+ {proposalStatusLabel(proposal.status)}
92
+ </Badge>
93
+ <Badge variant="outline">{proposal.proposalKind}</Badge>
94
+ {proposal.targetCategoryKey ? (
95
+ <Badge variant="outline">{proposal.targetCategoryKey}</Badge>
96
+ ) : null}
97
+ </div>
98
+ <h2 className="text-lg font-semibold leading-snug text-foreground">
99
+ {proposalDisplayTitle(proposal)}
100
+ </h2>
101
+ {proposal.summary ? (
102
+ <p className="text-sm text-muted-foreground">{proposal.summary}</p>
103
+ ) : null}
104
+ </div>
105
+
106
+ <div
107
+ aria-label={`${proposal.title} review actions`}
108
+ className="category-steering-proposal__actions flex flex-wrap items-center gap-2"
109
+ data-news-desk-topic-review-cluster
110
+ >
111
+ <Button
112
+ aria-label="Accept proposal"
113
+ aria-pressed={proposal.status === "accepted"}
114
+ data-review-action="accept"
115
+ disabled={acceptDisabled}
116
+ onClick={() => onReview("accept")}
117
+ size="sm"
118
+ title={acceptBlockedReason ?? undefined}
119
+ type="button"
120
+ variant={proposal.status === "accepted" ? "default" : "outline"}
121
+ >
122
+ <ThumbsUpIcon className="size-4" />
123
+ Accept
124
+ </Button>
125
+ <Button
126
+ aria-label="Reject proposal"
127
+ aria-pressed={proposal.status === "rejected"}
128
+ data-review-action="reject"
129
+ disabled={rejectDisabled}
130
+ onClick={() => onReview("reject")}
131
+ size="sm"
132
+ type="button"
133
+ variant={proposal.status === "rejected" ? "destructive" : "outline"}
134
+ >
135
+ <ThumbsDownIcon className="size-4" />
136
+ Reject
137
+ </Button>
138
+ <Button
139
+ aria-label="Defer proposal"
140
+ aria-pressed={proposal.status === "deferred"}
141
+ data-review-action="defer"
142
+ disabled={deferDisabled}
143
+ onClick={() => onReview("defer")}
144
+ size="sm"
145
+ type="button"
146
+ variant="outline"
147
+ >
148
+ Defer
149
+ </Button>
150
+ <Button
151
+ aria-label="Edit proposal"
152
+ data-review-action="edit"
153
+ disabled={editDisabled}
154
+ onClick={onEdit}
155
+ size="sm"
156
+ title={editBlockedReason ?? undefined}
157
+ type="button"
158
+ variant="outline"
159
+ >
160
+ <PencilIcon className="size-4" />
161
+ Edit
162
+ </Button>
163
+ </div>
164
+
165
+ <Separator />
166
+
167
+ <dl className="grid gap-3 text-sm">
168
+ {proposal.description ? (
169
+ <div>
170
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Description</dt>
171
+ <dd className="mt-1 text-foreground">{proposal.description}</dd>
172
+ </div>
173
+ ) : null}
174
+ {proposal.categoryKey ? (
175
+ <div>
176
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Category key</dt>
177
+ <dd className="mt-1 font-mono text-xs text-foreground">{proposal.categoryKey}</dd>
178
+ </div>
179
+ ) : null}
180
+ <div className="grid grid-cols-2 gap-3">
181
+ <div>
182
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Proposed</dt>
183
+ <dd className="mt-1 text-foreground">{formatProposalListDate(proposal)}</dd>
184
+ </div>
185
+ <div>
186
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Evidence</dt>
187
+ <dd className="mt-1 text-foreground">
188
+ {proposal.evidenceItemIds?.filter(Boolean).length ?? 0} refs
189
+ </dd>
190
+ </div>
191
+ </div>
192
+ </dl>
193
+
194
+ {onClose ? (
195
+ <Button className="w-full" onClick={onClose} type="button" variant="secondary">
196
+ Close
197
+ </Button>
198
+ ) : null}
199
+ </div>
200
+ </div>
201
+ );
202
+ }
203
+
204
+ export function NewsroomTopicsView({
205
+ demo = false,
206
+ disabled = false,
207
+ initialProposalId = null,
208
+ onEdit,
209
+ onReview,
210
+ proposals,
211
+ }: NewsroomTopicsViewProps) {
212
+ const sortedProposals = useMemo(() => sortTopicProposals(proposals), [proposals]);
213
+ const statusCounts = useMemo(() => countTopicProposalsByStatus(sortedProposals), [sortedProposals]);
214
+
215
+ const [statusFilter, setStatusFilter] = useState<TopicProposalStatusFilter>("proposed");
216
+ const [selectedProposalId, setSelectedProposalId] = useState(initialProposalId ?? "");
217
+ const [mobileDetailOpen, setMobileDetailOpen] = useState(Boolean(initialProposalId));
218
+
219
+ const filteredProposals = useMemo(
220
+ () => filterTopicProposals(sortedProposals, statusFilter),
221
+ [sortedProposals, statusFilter],
222
+ );
223
+
224
+ const selectedProposal = useMemo(
225
+ () => selectedTopicProposalById(filteredProposals, selectedProposalId)
226
+ ?? selectedTopicProposalById(sortedProposals, selectedProposalId),
227
+ [filteredProposals, selectedProposalId, sortedProposals],
228
+ );
229
+
230
+ const selectedIndex = selectedProposal
231
+ ? filteredProposals.findIndex((entry) => entry.id === selectedProposal.id)
232
+ : -1;
233
+ const previousProposal = selectedIndex > 0 ? filteredProposals[selectedIndex - 1] : null;
234
+ const nextProposal = selectedIndex >= 0 && selectedIndex < filteredProposals.length - 1
235
+ ? filteredProposals[selectedIndex + 1]
236
+ : null;
237
+
238
+ useEffect(() => {
239
+ if (!initialProposalId) return;
240
+ setSelectedProposalId(initialProposalId);
241
+ setMobileDetailOpen(true);
242
+ }, [initialProposalId]);
243
+
244
+ const selectProposal = useCallback((proposalId: string) => {
245
+ setSelectedProposalId(proposalId);
246
+ setMobileDetailOpen(true);
247
+ if (typeof window !== "undefined") {
248
+ const nextPath = buildTopicsDetailPath(proposalId, demo);
249
+ if (`${window.location.pathname}${window.location.search}` !== nextPath) {
250
+ window.history.pushState(null, "", nextPath);
251
+ }
252
+ }
253
+ }, [demo]);
254
+
255
+ const closeDetail = useCallback(() => {
256
+ setMobileDetailOpen(false);
257
+ setSelectedProposalId("");
258
+ if (typeof window !== "undefined") {
259
+ const indexPath = getNewsroomNavHref("/newsroom/topics", demo);
260
+ window.history.replaceState(null, "", indexPath);
261
+ }
262
+ }, [demo]);
263
+
264
+ const runReview = (action: TopicProposalReviewAction) => {
265
+ if (!selectedProposal) return;
266
+ if (action === "edit") {
267
+ onEdit(selectedProposal);
268
+ return;
269
+ }
270
+ onReview(selectedProposal, action);
271
+ };
272
+
273
+ const emptyLabel = sortedProposals.length
274
+ ? "No topic proposals match this filter."
275
+ : "No category steering proposals in the queue yet.";
276
+
277
+ return (
278
+ <div
279
+ className="flex min-h-0 flex-1 flex-col gap-4 font-sans md:flex-row"
280
+ data-detail-open={selectedProposal || initialProposalId ? "true" : "false"}
281
+ data-news-desk-section="topics"
282
+ >
283
+ <div className="flex min-h-0 min-w-0 flex-1 flex-col gap-4">
284
+ <div className="space-y-1">
285
+ <h2 className="text-lg font-semibold tracking-tight">Topics</h2>
286
+ <p className="text-sm text-muted-foreground">
287
+ Review taxonomy proposals before they shape accepted topic coverage.
288
+ </p>
289
+ </div>
290
+
291
+ <Tabs
292
+ defaultValue="proposed"
293
+ onValueChange={(value) => setStatusFilter(value as TopicProposalStatusFilter)}
294
+ value={statusFilter}
295
+ >
296
+ <TabsList className="h-auto w-full flex-wrap justify-start gap-1 bg-muted/40 p-1">
297
+ {STATUS_FILTERS.map((filter) => (
298
+ <TabsTrigger className="text-xs sm:text-sm" key={filter.key} value={filter.key}>
299
+ {filter.label}
300
+ <Badge className="ml-1.5 min-w-5 px-1.5" variant="secondary">
301
+ {statusCounts[filter.key]}
302
+ </Badge>
303
+ </TabsTrigger>
304
+ ))}
305
+ </TabsList>
306
+ </Tabs>
307
+
308
+ <ScrollArea className="min-h-0 flex-1 md:max-h-[calc(100dvh-16rem)]">
309
+ <div className="flex flex-col gap-2 pb-4" data-newsroom-card-grid>
310
+ {filteredProposals.length ? filteredProposals.map((proposal) => {
311
+ const active = selectedProposalId === proposal.id;
312
+ return (
313
+ <button
314
+ aria-current={active ? "true" : undefined}
315
+ className={newsroomListRowClassName(active)}
316
+ data-newsroom-list-row
317
+ data-selected={active || undefined}
318
+ data-newsroom-card
319
+ data-newsroom-card-id={proposal.id}
320
+ data-topic-queue-proposal={proposal.id}
321
+ key={proposal.id}
322
+ onClick={() => selectProposal(proposal.id)}
323
+ type="button"
324
+ >
325
+ <div className="flex items-start justify-between gap-3">
326
+ <div className="min-w-0 flex-1 space-y-1">
327
+ <p className="truncate text-sm font-medium">{proposalDisplayTitle(proposal)}</p>
328
+ <p className="truncate text-xs text-muted-foreground">{proposal.proposalKind}</p>
329
+ </div>
330
+ <Badge variant={proposalStatusBadgeVariant(proposal.status)}>
331
+ {proposalStatusLabel(proposal.status)}
332
+ </Badge>
333
+ </div>
334
+ <p className="mt-2 line-clamp-2 text-xs text-muted-foreground">
335
+ {proposal.summary ?? "No summary provided."}
336
+ </p>
337
+ <p className="mt-2 text-xs text-muted-foreground">{formatProposalListDate(proposal)}</p>
338
+ </button>
339
+ );
340
+ }) : (
341
+ <Card>
342
+ <CardHeader>
343
+ <CardTitle className="text-base">No proposals</CardTitle>
344
+ <CardDescription>{emptyLabel}</CardDescription>
345
+ </CardHeader>
346
+ </Card>
347
+ )}
348
+ </div>
349
+ </ScrollArea>
350
+ </div>
351
+
352
+ <div
353
+ className="hidden min-h-0 w-full shrink-0 flex-col rounded-xl border border-border bg-card md:flex md:w-[min(100%,24rem)] lg:w-[min(100%,28rem)]"
354
+ data-detail-open={selectedProposal ? "true" : "false"}
355
+ data-news-desk-section="topics"
356
+ data-newsroom-list-detail-shell
357
+ >
358
+ <div className="flex items-center justify-between gap-2 border-b border-border px-3 py-2">
359
+ <span className="text-sm font-medium text-foreground">Detail</span>
360
+ <div className="newsroom-list-detail-shell__detail-toolbar-trailing flex items-center gap-1">
361
+ <Button
362
+ aria-label="Previous"
363
+ disabled={!previousProposal}
364
+ onClick={() => previousProposal && selectProposal(previousProposal.id)}
365
+ size="icon-sm"
366
+ type="button"
367
+ variant="ghost"
368
+ >
369
+ <ChevronLeftIcon className="size-4" />
370
+ </Button>
371
+ <Button
372
+ aria-label="Next"
373
+ disabled={!nextProposal}
374
+ onClick={() => nextProposal && selectProposal(nextProposal.id)}
375
+ size="icon-sm"
376
+ type="button"
377
+ variant="ghost"
378
+ >
379
+ <ChevronRightIcon className="size-4" />
380
+ </Button>
381
+ </div>
382
+ </div>
383
+ <ScrollArea className="min-h-0 flex-1">
384
+ {selectedProposal ? (
385
+ <TopicProposalDetailPanel
386
+ disabled={disabled}
387
+ onEdit={() => onEdit(selectedProposal)}
388
+ onReview={(action) => onReview(selectedProposal, action)}
389
+ proposal={selectedProposal}
390
+ />
391
+ ) : (
392
+ <CardContent className="p-4 text-sm text-muted-foreground">
393
+ Select a topic proposal to review steering changes.
394
+ </CardContent>
395
+ )}
396
+ </ScrollArea>
397
+ </div>
398
+
399
+ <Sheet
400
+ onOpenChange={(open) => {
401
+ if (!open) closeDetail();
402
+ else setMobileDetailOpen(true);
403
+ }}
404
+ open={mobileDetailOpen && Boolean(selectedProposal)}
405
+ >
406
+ <SheetContent className="gap-0 p-0 md:hidden" side="bottom">
407
+ <SheetHeader className="border-b border-border px-4 py-3">
408
+ <SheetTitle className="text-left text-base">Topic proposal</SheetTitle>
409
+ </SheetHeader>
410
+ {selectedProposal ? (
411
+ <ScrollArea className="max-h-[75dvh]">
412
+ <TopicProposalDetailPanel
413
+ disabled={disabled}
414
+ onClose={closeDetail}
415
+ onEdit={() => onEdit(selectedProposal)}
416
+ onReview={(action) => onReview(selectedProposal, action)}
417
+ proposal={selectedProposal}
418
+ />
419
+ </ScrollArea>
420
+ ) : null}
421
+ </SheetContent>
422
+ </Sheet>
423
+ </div>
424
+ );
425
+ }