@anthusai/papyrus 1.4.0-next.3 → 1.4.0-next.4

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.
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { isEditorialWorkAssignment } from "../lib/newsroom-assignments";
3
4
  import { isLocalPreviewEnabled } from "../lib/local-preview";
4
5
  import { createLocalPreviewAssignments, createLocalPreviewContentItemVersions, createLocalPreviewReferences } from "../lib/local-preview-seed";
5
6
  import { generateClient } from "aws-amplify/data";
@@ -2156,10 +2157,10 @@ export async function loadNewsroomAssignmentPage(options: NewsroomAssignmentPage
2156
2157
  newsroomFeedKey: "assignments",
2157
2158
  limit: options.limit,
2158
2159
  nextToken: options.nextToken,
2159
- matches: equalityMatcher({
2160
+ matches: (assignment) => isEditorialWorkAssignment(assignment) && (equalityMatcher<AssignmentRecord>({
2160
2161
  assignmentTypeKey: options.type,
2161
2162
  status: options.status,
2162
- }),
2163
+ })?.(assignment) ?? true),
2163
2164
  });
2164
2165
  }
2165
2166
 
@@ -91,7 +91,7 @@ function failureMessage(error: unknown): string {
91
91
  return error instanceof Error ? error.message : "The request failed.";
92
92
  }
93
93
 
94
- export function NewsroomArticleEditor({ articleId, demo }: { articleId: string; demo: boolean }) {
94
+ export function NewsroomArticleEditor({ articleId, demo, embedded = false }: { articleId: string; demo: boolean; embedded?: boolean }) {
95
95
  const session = useOptionalNewsDeskClient();
96
96
  const blocked = !demo && articlesAccessBlocked(session?.shell.phase);
97
97
  const backend: ArticlesBackend = useMemo(() => resolveArticlesBackend(demo), [demo]);
@@ -725,6 +725,8 @@ export function NewsroomArticleEditor({ articleId, demo }: { articleId: string;
725
725
  );
726
726
  })();
727
727
 
728
+ if (embedded) return <div className="newsroom-embedded-article">{body}</div>;
729
+
728
730
  return (
729
731
  <NewsroomOpsShell
730
732
  activeTab="articles"
@@ -0,0 +1,45 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import type { AssignmentRecord } from "../lib/category-repository";
5
+
6
+ const assignmentDateFormat = new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" });
7
+
8
+ function formattedTimestamp(value: string | null | undefined): string | null {
9
+ if (!value) return null;
10
+ const parsed = new Date(value);
11
+ return Number.isNaN(parsed.getTime()) ? value : assignmentDateFormat.format(parsed);
12
+ }
13
+
14
+ type DetailField = { label: string; value: string | null | undefined };
15
+
16
+ /**
17
+ * The whole assignment, laid out by container width: one column in a narrow Board panel, labelled
18
+ * columns when the container is wide, such as the dedicated page on a large monitor.
19
+ */
20
+ export function NewsroomAssignmentDetail({ assignment, pageHref, showTitle = true }: { assignment: AssignmentRecord; pageHref?: string; showTitle?: boolean }) {
21
+ const facts: DetailField[] = [
22
+ { label: "Status", value: assignment.status },
23
+ { label: "Type", value: assignment.assignmentTypeKey },
24
+ { label: "Assignee", value: assignment.assigneeId ?? assignment.assigneeKey },
25
+ { label: "Section", value: assignment.sectionKey },
26
+ { label: "Created", value: formattedTimestamp(assignment.createdAt) },
27
+ { label: "Updated", value: formattedTimestamp(assignment.updatedAt) },
28
+ { label: "Completed", value: formattedTimestamp(assignment.completedAt) },
29
+ ].filter(field => field.value);
30
+ const prose: DetailField[] = [
31
+ { label: "Summary", value: assignment.summary },
32
+ { label: "Brief", value: assignment.brief },
33
+ { label: "Instructions", value: assignment.instructions },
34
+ ].filter(field => field.value);
35
+ return <article className="newsroom-assignment-detail">
36
+ {showTitle && <h2 className="newsroom-assignment-detail-title">{assignment.title}</h2>}
37
+ <dl className="newsroom-assignment-detail-facts">
38
+ {facts.map(field => <div key={field.label}><dt>{field.label}</dt><dd>{field.value}</dd></div>)}
39
+ </dl>
40
+ <div className="newsroom-assignment-detail-prose">
41
+ {prose.map(field => <section key={field.label}><h3>{field.label}</h3><p>{field.value}</p></section>)}
42
+ </div>
43
+ {pageHref && <Link className="organizer-header-button" href={pageHref}>Open full page</Link>}
44
+ </article>;
45
+ }
@@ -29,6 +29,9 @@ import { usePagedContentItemVersions } from "./use-paged-content-item-versions";
29
29
  import type { ContentItemVersionRecord } from "../lib/content-item-lifecycle";
30
30
  import { useOptionalNewsDeskClient } from "./news-desk-client-provider";
31
31
  import { NewsDeskAccessGate } from "./newsroom-access-gate";
32
+ import { isLocalPreviewEnabled } from "../lib/local-preview";
33
+ import { NewsroomArticleEditor } from "./newsroom-article-editor";
34
+ import { NewsroomAssignmentDetail } from "./newsroom-assignment-detail";
32
35
  import { NewsroomOpsShell, NewsroomOpsStatusBanner } from "./newsroom-ops-shell";
33
36
  import { useResolvedPapyrusTheme } from "./use-resolved-papyrus-theme";
34
37
  import { readLocalReaderSettings, subscribeReaderSettingsChanges } from "./reader-settings";
@@ -92,7 +95,7 @@ export function NewsroomBoardView({ subject, deskId }: { subject: NewsroomBoardS
92
95
  if (generation.current === request) { setPublishedVersions(published); setAssignments(everyAssignment); }
93
96
  })
94
97
  : loadEveryNewsroomAssignment().then(everyAssignment => {
95
- if (generation.current === request) setAssignmentBoardIssues(projectAssignmentBoard(everyAssignment));
98
+ if (generation.current === request) { setAssignments(everyAssignment); setAssignmentBoardIssues(projectAssignmentBoard(everyAssignment)); }
96
99
  });
97
100
  const refresh = (initial: boolean) => {
98
101
  if (initial) setSupportingRecordsLoading(true);
@@ -158,8 +161,18 @@ export function NewsroomBoardView({ subject, deskId }: { subject: NewsroomBoardS
158
161
  collapsedColumns={columnState.collapsedColumns} onToggleCollapse={columnState.toggleColumn} initialScroll="start"
159
162
  selectedIssueId={selected} detailOpen={Boolean(issue)} onSelectIssue={item => setSelected(item.id)} ariaLabel={pageTitle}
160
163
  emptyMessage={loading ? "Loading records…" : error ? "Records could not be loaded." : "No records in this selection."} />
161
- {issue && <IssueDetailPanel issue={issue} onClose={() => setSelected(null)} fields={[{ label: "CMS record ID", value: issue.nativeId }, { label: "Status", value: issue.nativeStatus }]}>
162
- <Link href={getNewsroomNavHref(issue.href)}>Open {issue.type}</Link>
164
+ {issue && <IssueDetailPanel issue={issue} onClose={() => setSelected(null)} fields={[]}>
165
+ {issue.kind === "contentItem"
166
+ ? <>
167
+ <NewsroomArticleEditor key={issue.id} articleId={decodeURIComponent(issue.href.split("/").pop() ?? "")} demo={isLocalPreviewEnabled()} embedded />
168
+ <Link className="organizer-header-button" href={getNewsroomNavHref(issue.href)}>Open full page</Link>
169
+ </>
170
+ : (() => {
171
+ const record = assignments.find(candidate => candidate.id === issue.nativeId);
172
+ return record
173
+ ? <NewsroomAssignmentDetail assignment={record} pageHref={getNewsroomNavHref(issue.href)} showTitle={false} />
174
+ : <Link href={getNewsroomNavHref(issue.href)}>Open {issue.type}</Link>;
175
+ })()}
163
176
  </IssueDetailPanel>}
164
177
  </div>
165
178
  </div>
@@ -43,10 +43,10 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
43
43
  .organizer-board-note{font-size:11px;line-height:1.5;margin:0;color:var(--org-muted);white-space:nowrap}
44
44
  @media(min-width:901px){.newsroom-organizer[data-sidebar-collapsed=false] .organizer-masthead{display:none}}
45
45
  .organizer-board-body{position:relative;display:grid;grid-template-columns:minmax(0,1fr);flex:1;min-width:0;min-height:0;gap:16px}
46
- .organizer-board-body[data-detail-open=true]{grid-template-columns:minmax(0,1fr) minmax(280px,360px)}
46
+ .organizer-board-body[data-detail-open=true]{grid-template-columns:minmax(0,1fr) clamp(320px,34vw,1000px)}
47
47
  .organizer-board-body>.kb-issue-collection{height:100%;min-height:0;overflow:auto}
48
48
  .organizer-board-body>.kb-issue-detail{overflow:auto;min-height:0}
49
- .organizer-board-body .kb-issue-detail a{display:block;min-height:44px;text-decoration:underline;padding:12px 0}
49
+ .organizer-board-body .kb-issue-detail a:not(.organizer-header-button){display:block;min-height:44px;text-decoration:underline;padding:12px 0}
50
50
  .organizer-menu{height:min(85dvh,760px);max-height:85dvh;display:flex;flex-direction:column;padding:16px;border-radius:24px 24px 0 0;gap:20px}
51
51
  .organizer-menu .organizer-destinations{overflow-y:auto}.organizer-menu .organizer-destinations section{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.organizer-menu .organizer-destinations h3{grid-column:1/-1;font-size:12px;text-transform:none;margin:0 0 4px}
52
52
  .organizer-menu .organizer-destinations a{border:1px solid var(--org-line);padding:12px;color:var(--org-ink)}
@@ -63,3 +63,22 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
63
63
  .organizer-board-body>.kb-issue-detail{position:absolute;inset:0;z-index:2;overflow:auto}
64
64
  }
65
65
  @media(prefers-reduced-motion:reduce){.newsroom-organizer,.newsroom-organizer *,.organizer-menu,.organizer-menu *,.organizer-account-popup,.organizer-account-popup *{scroll-behavior:auto;animation:none!important;transition:none!important}}
66
+ .newsroom-organizer :is(button,input,select,textarea,[role=button],[role=tab],[role=combobox],[role=option],[role=menuitem],a[class]),.organizer-menu :is(button,a),.organizer-account-popup :is(button,a){border-radius:0 !important}
67
+ .newsroom-assignment-detail,.newsroom-embedded-article{container-type:inline-size;min-width:0}
68
+ .newsroom-assignment-detail{display:grid;gap:16px;padding:12px}
69
+ .newsroom-assignment-detail-title{margin:0;font-size:1.25rem;line-height:1.25;overflow-wrap:anywhere}
70
+ .newsroom-assignment-detail-facts{display:grid;gap:8px 16px;grid-template-columns:minmax(0,1fr);margin:0}
71
+ .newsroom-assignment-detail-facts>div{display:grid;grid-template-columns:minmax(80px,max-content) minmax(0,1fr);gap:8px;border-top:1px solid var(--org-line);padding-top:6px}
72
+ .newsroom-assignment-detail-facts dt{color:var(--org-muted);font-size:12px;text-transform:uppercase;letter-spacing:.04em}
73
+ .newsroom-assignment-detail-facts dd{margin:0;overflow-wrap:anywhere}
74
+ .newsroom-assignment-detail-prose{display:grid;gap:12px}
75
+ .newsroom-assignment-detail-prose h3{margin:0 0 4px;font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--org-muted)}
76
+ .newsroom-assignment-detail-prose p{margin:0;white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.5}
77
+ @container (min-width:560px){.newsroom-assignment-detail-facts{grid-template-columns:repeat(2,minmax(0,1fr))}}
78
+ @container (min-width:900px){.newsroom-assignment-detail{grid-template-columns:minmax(0,2fr) minmax(280px,1fr);align-items:start;column-gap:32px}.newsroom-assignment-detail-title{grid-column:1/-1}.newsroom-assignment-detail-facts{grid-template-columns:minmax(0,1fr)}}
79
+ @container (min-width:1500px){.newsroom-assignment-detail-prose{grid-template-columns:repeat(2,minmax(0,1fr))}}
80
+ .newsroom-embedded-article{padding:0 8px 12px}
81
+ .newsroom-embedded-article :is(textarea,input,select){max-width:100%}
82
+ .newsroom-embedded-article [class*="grid-cols-2"],.newsroom-embedded-article [class*="lg:grid"]{grid-template-columns:minmax(0,1fr) !important}
83
+ @container (min-width:1100px){.newsroom-embedded-article [class*="lg:grid"]{grid-template-columns:repeat(2,minmax(0,1fr)) !important}}
84
+ .newsroom-organizer [class*="rounded"]:not([data-slot=avatar]):not([class*="animate-spin"]):not([role=progressbar]),.organizer-menu,.organizer-account-popup,[data-slot=dialog-content],[data-slot=popover-content],[data-slot=dropdown-menu-content],[data-slot=select-content],[data-slot=tooltip-content]{border-radius:0 !important}
@@ -403,7 +403,7 @@ export function NewsroomTopicsView({
403
403
  }}
404
404
  open={mobileDetailOpen && Boolean(selectedProposal)}
405
405
  >
406
- <SheetContent className="gap-0 p-0 md:hidden" side="bottom">
406
+ <SheetContent backdropClassName="md:hidden" className="gap-0 p-0 md:hidden" side="bottom">
407
407
  <SheetHeader className="border-b border-border px-4 py-3">
408
408
  <SheetTitle className="text-left text-base">Topic proposal</SheetTitle>
409
409
  </SheetHeader>
@@ -11,9 +11,9 @@ type SheetSide = "bottom" | "left" | "right";
11
11
 
12
12
  const sideClasses: Record<SheetSide, string> = {
13
13
  bottom:
14
- "inset-x-0 bottom-0 top-auto max-h-[85dvh] w-full rounded-t-2xl border-t data-open:slide-in-from-bottom data-closed:slide-out-to-bottom",
15
- left: "inset-y-0 left-0 h-full w-[min(100vw,20rem)] rounded-r-2xl border-r data-open:slide-in-from-left data-closed:slide-out-to-left",
16
- right: "inset-y-0 right-0 h-full w-[min(100vw,20rem)] rounded-l-2xl border-l data-open:slide-in-from-right data-closed:slide-out-to-right",
14
+ "inset-x-0 bottom-0 top-auto max-h-[85dvh] w-full border-t data-open:slide-in-from-bottom data-closed:slide-out-to-bottom",
15
+ left: "inset-y-0 left-0 h-full w-[min(100vw,20rem)] border-r data-open:slide-in-from-left data-closed:slide-out-to-left",
16
+ right: "inset-y-0 right-0 h-full w-[min(100vw,20rem)] border-l data-open:slide-in-from-right data-closed:slide-out-to-right",
17
17
  };
18
18
 
19
19
  function Sheet({ ...props }: DialogPrimitive.Root.Props) {
@@ -31,17 +31,22 @@ function SheetClose({ ...props }: DialogPrimitive.Close.Props) {
31
31
  function SheetContent({
32
32
  children,
33
33
  className,
34
+ backdropClassName,
34
35
  showCloseButton = true,
35
36
  side = "bottom",
36
37
  ...props
37
38
  }: DialogPrimitive.Popup.Props & {
39
+ backdropClassName?: string;
38
40
  showCloseButton?: boolean;
39
41
  side?: SheetSide;
40
42
  }) {
41
43
  return (
42
44
  <DialogPrimitive.Portal>
43
45
  <DialogPrimitive.Backdrop
44
- className="fixed inset-0 z-50 bg-black/40 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0"
46
+ className={cn(
47
+ "fixed inset-0 z-50 bg-black/40 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
48
+ backdropClassName,
49
+ )}
45
50
  />
46
51
  <DialogPrimitive.Popup
47
52
  className={cn(
@@ -1,6 +1,9 @@
1
+ import approvedReport0 from "./approved-priming-proofs/papyrus.json";
2
+
1
3
  /**
2
4
  * Priming proof reports a publication owner approved for visitors, keyed by site brand id.
3
- * A report reaches this map only through a reviewed commit of its
4
- * `publications/priming-proofs/<brand id>.json` file; see docs/cyclotron-priming.md.
5
+ * A report reaches this map only through a reviewed commit made by scripts/approve-priming-proof-report.ts.
5
6
  */
6
- export const APPROVED_PRIMING_PROOF_REPORTS: Record<string, unknown> = {};
7
+ export const APPROVED_PRIMING_PROOF_REPORTS: Record<string, unknown> = {
8
+ "papyrus": approvedReport0,
9
+ };
@@ -0,0 +1,90 @@
1
+ {
2
+ "version": "cyclotron-priming-proof/v1",
3
+ "proofId": "ti-2026-10",
4
+ "publication": "Anthus Threat Intelligence",
5
+ "cyclotronId": "papyrus-relevance",
6
+ "question": "Should this candidate source become a reference for this publication?",
7
+ "labels": [
8
+ "include",
9
+ "exclude"
10
+ ],
11
+ "positiveLabel": "include",
12
+ "decisionModel": "jev:jev-1.13.0",
13
+ "ranAt": "2026-10-18T13:34:13.426310Z",
14
+ "heldOut": {
15
+ "size": 10,
16
+ "byLabel": {
17
+ "include": 5,
18
+ "exclude": 5
19
+ },
20
+ "fingerprint": "b12fe63012c925bd6def343a13b94ccedafd5184ddb93c8312998486045da779",
21
+ "frozenAt": "2026-10-18T13:02:20.090030Z",
22
+ "source": "References editors reviewed in the Newsroom, labeled by the scope-training rule"
23
+ },
24
+ "priming": {
25
+ "acceptedExamples": 13,
26
+ "excludedBecauseInHeldOut": 0,
27
+ "fingerprint": null,
28
+ "source": "editor reviews of References",
29
+ "order": "reviews in review order, then priming examples in acceptance order"
30
+ },
31
+ "conditions": {
32
+ "freshStorePerPoint": true,
33
+ "rubric": "publication doctrine seed",
34
+ "optimizer": false,
35
+ "reviewProgram": false
36
+ },
37
+ "points": [
38
+ {
39
+ "labels": 0,
40
+ "labelsByClass": {
41
+ "include": 0,
42
+ "exclude": 0
43
+ },
44
+ "decided": 10,
45
+ "agreement": 1,
46
+ "precision": 1,
47
+ "recall": 1,
48
+ "confusion": {
49
+ "truePositives": 5,
50
+ "falsePositives": 0,
51
+ "trueNegatives": 5,
52
+ "falseNegatives": 0
53
+ }
54
+ },
55
+ {
56
+ "labels": 5,
57
+ "labelsByClass": {
58
+ "include": 4,
59
+ "exclude": 1
60
+ },
61
+ "decided": 10,
62
+ "agreement": 1,
63
+ "precision": 1,
64
+ "recall": 1,
65
+ "confusion": {
66
+ "truePositives": 5,
67
+ "falsePositives": 0,
68
+ "trueNegatives": 5,
69
+ "falseNegatives": 0
70
+ }
71
+ },
72
+ {
73
+ "labels": 13,
74
+ "labelsByClass": {
75
+ "include": 4,
76
+ "exclude": 9
77
+ },
78
+ "decided": 10,
79
+ "agreement": 0.9,
80
+ "precision": 1,
81
+ "recall": 0.8,
82
+ "confusion": {
83
+ "truePositives": 4,
84
+ "falsePositives": 0,
85
+ "trueNegatives": 5,
86
+ "falseNegatives": 1
87
+ }
88
+ }
89
+ ]
90
+ }
@@ -12,6 +12,7 @@ import {
12
12
  demoContentHash,
13
13
  type DemoArticleRecord,
14
14
  } from "./newsroom-demo-articles";
15
+ import { createLocalPreviewEditableArticle } from "./local-preview-seed";
15
16
  import { sourceLineOffset, type ArticlesBackend, type EditableArticle, type NewsroomArticleRow } from "./newsroom-articles";
16
17
 
17
18
  function titleFromYaml(frontMatterYaml: string | null, fallback: string): string {
@@ -171,6 +172,8 @@ export function createDemoArticlesBackend(): ArticlesBackend {
171
172
  .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
172
173
  },
173
174
  async loadArticle(id: string): Promise<EditableArticle> {
175
+ const previewRecord = records.get(id) ? null : createLocalPreviewEditableArticle(id);
176
+ if (previewRecord) return previewRecord;
174
177
  const record = requireRecord(id);
175
178
  return {
176
179
  id: record.id,
@@ -1,3 +1,4 @@
1
+ import { warnAboutMissingMedia } from "./layout-warnings";
1
2
  import {
2
3
  type ArticleFrameBlockSpec,
3
4
  type ArticleFrameCompositionSlotSpec,
@@ -605,7 +606,7 @@ function solveComposedFrontArticleFrame(
605
606
  flow.usedImageAssetIds,
606
607
  );
607
608
  if (!image) {
608
- if (mediaSpec?.required) throw new Error(`${blockSpec.id} requires media slot ${slot.mediaIndex ?? 0} but no image could be solved`);
609
+ if (mediaSpec?.required) warnAboutMissingMedia(`${blockSpec.id} requires media slot ${slot.mediaIndex ?? 0} but no image could be solved and renders without it`);
609
610
  continue;
610
611
  }
611
612
  const stackTop = getOccupiedColumnBottom(columnBottoms, image.columnStart, image.columnSpan);
@@ -1,3 +1,4 @@
1
+ import { warnAboutMissingMedia } from "./layout-warnings";
1
2
  import { z } from "zod";
2
3
  import {
3
4
  type PublicationItem,
@@ -894,7 +895,7 @@ function validateBlock(
894
895
  if (block.type === "mediaCluster") {
895
896
  const assets = findAssetsByRole(item, block.assetRole);
896
897
  if (block.required && assets.length < block.count.min) {
897
- throw new Error(`${label}.${block.id} requires at least ${block.count.min} media assets`);
898
+ warnAboutMissingMedia(`${label}.${block.id} requires at least ${block.count.min} media assets`);
898
899
  }
899
900
  }
900
901
  }
@@ -902,7 +903,7 @@ function validateBlock(
902
903
  function validateMediaSpec(item: PublicationItem, media: LayoutMediaSpec, label: string): void {
903
904
  const matchingAssets = findAssetsByRole(item, media.assetRole);
904
905
  if (media.required && matchingAssets.length === 0) {
905
- throw new Error(`${label} requires an image asset${media.assetRole ? ` with role ${media.assetRole}` : ""}`);
906
+ warnAboutMissingMedia(`${label} requires an image asset${media.assetRole ? ` with role ${media.assetRole}` : ""} and renders without it`);
906
907
  }
907
908
  }
908
909
 
@@ -0,0 +1,8 @@
1
+ const missingMediaWarningsAlreadyLogged = new Set<string>();
2
+
3
+ /** A story without its lead image must still publish; the layout renders without the slot. */
4
+ export function warnAboutMissingMedia(message: string): void {
5
+ if (missingMediaWarningsAlreadyLogged.has(message)) return;
6
+ missingMediaWarningsAlreadyLogged.add(message);
7
+ console.warn(`[layout-plan] ${message}`);
8
+ }
@@ -1,4 +1,5 @@
1
1
  import type { AssignmentRecord, ReferenceRecord } from "./category-repository";
2
+ import type { EditableArticle } from "./newsroom-articles";
2
3
  import { contentItemStageKeys, type ContentItemVersionRecord } from "./content-item-lifecycle";
3
4
  import { getContentItemStageDefinitions } from "./site-brand";
4
5
 
@@ -93,3 +94,22 @@ export function createLocalPreviewAssignments(): AssignmentRecord[] {
93
94
  }));
94
95
  return [...assignmentsOnContentItems, ...assignmentsWithoutContentItem];
95
96
  }
97
+
98
+ export function createLocalPreviewEditableArticle(id: string): EditableArticle | null {
99
+ const version = createLocalPreviewContentItemVersions().find(candidate => candidate.id === id);
100
+ if (!version) return null;
101
+ const headline = version.headline || version.title || version.slug;
102
+ return {
103
+ id: version.id,
104
+ type: version.type,
105
+ section: version.section ?? null,
106
+ slug: version.slug,
107
+ frontMatterYaml: `title: ${headline}\nsection: ${version.section ?? ""}`,
108
+ bodyMarkus: `# ${headline}\n\nThe local preview shows this story's record. Its full body text is loaded from the live backend.`,
109
+ contentHash: null,
110
+ status: version.status === "published" ? "published" : "draft",
111
+ versionNumber: version.versionNumber,
112
+ aliases: [],
113
+ everPublished: version.status === "published",
114
+ };
115
+ }
@@ -18,6 +18,19 @@ export type AssignmentMetrics = {
18
18
  canceled: number;
19
19
  };
20
20
 
21
+ export const BOOKKEEPING_ASSIGNMENT_TYPE_KEYS: ReadonlySet<string> = new Set([
22
+ "analysis.reindex",
23
+ "curation.reference-intake",
24
+ "reference.corpus-accession",
25
+ "reference.identifier-backfill",
26
+ "reference.doi-backfill",
27
+ "reference.text-extraction",
28
+ ]);
29
+
30
+ export function isEditorialWorkAssignment(assignment: Pick<AssignmentRecord, "assignmentTypeKey">): boolean {
31
+ return !BOOKKEEPING_ASSIGNMENT_TYPE_KEYS.has(assignment.assignmentTypeKey?.trim() ?? "");
32
+ }
33
+
21
34
  export function assignmentTypeKeyForFilter(assignment: AssignmentRecord): string {
22
35
  return assignment.assignmentTypeKey?.trim() || "unknown";
23
36
  }
package/package.json CHANGED
@@ -84,6 +84,7 @@
84
84
  "./components/newsroom-access-gate": "./components/newsroom-access-gate.tsx",
85
85
  "./components/newsroom-article-editor": "./components/newsroom-article-editor.tsx",
86
86
  "./components/newsroom-articles-view": "./components/newsroom-articles-view.tsx",
87
+ "./components/newsroom-assignment-detail": "./components/newsroom-assignment-detail.tsx",
87
88
  "./components/newsroom-assignments-view": "./components/newsroom-assignments-view.tsx",
88
89
  "./components/newsroom-board-view": "./components/newsroom-board-view.tsx",
89
90
  "./components/newsroom-client-shell": "./components/newsroom-client-shell.tsx",
@@ -182,6 +183,7 @@
182
183
  "./lib/layout-plan": "./lib/layout-plan.ts",
183
184
  "./lib/layout-scenarios": "./lib/layout-scenarios.ts",
184
185
  "./lib/layout-types": "./lib/layout-types.ts",
186
+ "./lib/layout-warnings": "./lib/layout-warnings.ts",
185
187
  "./lib/local-preview-seed": "./lib/local-preview-seed.ts",
186
188
  "./lib/local-preview": "./lib/local-preview.ts",
187
189
  "./lib/markus-body": "./lib/markus-body.ts",
@@ -352,7 +354,7 @@
352
354
  "optional": true
353
355
  }
354
356
  },
355
- "version": "1.4.0-next.3",
357
+ "version": "1.4.0-next.4",
356
358
  "dependencies": {
357
359
  "@aws-amplify/adapter-nextjs": "^1.7.3",
358
360
  "@aws-crypto/sha256-js": "5.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.4.0-next.3",
2
+ "version": "1.4.0-next.4",
3
3
  "routes": [
4
4
  {
5
5
  "file": "app/%5F_preview/[[...path]]/route.ts",