@anthusai/papyrus 1.4.0-next.2 → 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.
@@ -126,9 +126,9 @@ function bundleFromPip(outputDir, options) {
126
126
  }
127
127
  export function bundleFromCheckout(outputDir, options) {
128
128
  fs.copyFileSync(path.join(packageRoot, "amplify/functions", options.functionDir, "handler.py"), path.join(outputDir, "handler.py"));
129
- for (const module of options.modules) {
130
- fs.mkdirSync(path.join(outputDir, module), { recursive: true });
131
- fs.cpSync(path.join(packageRoot, "src", module), path.join(outputDir, module), { recursive: true });
129
+ for (const moduleName of options.modules) {
130
+ fs.mkdirSync(path.join(outputDir, moduleName), { recursive: true });
131
+ fs.cpSync(path.join(packageRoot, "src", moduleName), path.join(outputDir, moduleName), { recursive: true });
132
132
  }
133
133
  options.checkoutExtras?.(outputDir);
134
134
  copySiteFiles(outputDir, options.siteFiles);
@@ -164,9 +164,9 @@ export function bundleFromCheckout(outputDir: string, options: PythonBundleOptio
164
164
  path.join(packageRoot, "amplify/functions", options.functionDir, "handler.py"),
165
165
  path.join(outputDir, "handler.py"),
166
166
  );
167
- for (const module of options.modules) {
168
- fs.mkdirSync(path.join(outputDir, module), { recursive: true });
169
- fs.cpSync(path.join(packageRoot, "src", module), path.join(outputDir, module), { recursive: true });
167
+ for (const moduleName of options.modules) {
168
+ fs.mkdirSync(path.join(outputDir, moduleName), { recursive: true });
169
+ fs.cpSync(path.join(packageRoot, "src", moduleName), path.join(outputDir, moduleName), { recursive: true });
170
170
  }
171
171
  options.checkoutExtras?.(outputDir);
172
172
  copySiteFiles(outputDir, options.siteFiles);
@@ -1,4 +1,6 @@
1
1
  import { notFound } from "next/navigation";
2
+ import { resolveActiveSiteBrand } from "../../../../lib/site-brand-tenant";
3
+ import { NewsroomBoardView } from "../../../../components/newsroom-board-view";
2
4
  import { NewsDeskPage } from "../../../../components/news-desk-page";
3
5
 
4
6
  export const dynamic = "force-dynamic";
@@ -13,5 +15,6 @@ export default async function NewsroomDeepSectionPage({ params, searchParams }:
13
15
  const normalizedSectionId = decodeURIComponent(sectionId ?? "").trim();
14
16
  if (!normalizedSectionId) notFound();
15
17
 
18
+ if (resolveActiveSiteBrand().newsroomLayout === "organizer") return <NewsroomBoardView subject="desk" deskId={normalizedSectionId} />;
16
19
  return <NewsDeskPage sectionPageId={normalizedSectionId} searchParams={searchParams} />;
17
20
  }
@@ -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
 
@@ -2172,40 +2173,45 @@ const CONTENT_ITEMS_BY_STAGE_QUERY = `
2172
2173
  }
2173
2174
  `;
2174
2175
 
2175
- export async function loadNewsroomContentItemVersions(options: { includeCollapsedUntilOpenedStages?: readonly string[] } = {}): Promise<ContentItemVersionRecord[]> {
2176
- const stageDefinitions = getContentItemStageDefinitions();
2177
- const stagesToLoad = [
2178
- ...stageDefinitions
2179
- .filter(definition => !definition.collapsedUntilOpened || options.includeCollapsedUntilOpenedStages?.includes(definition.key))
2180
- .map(definition => definition.key),
2181
- ...legacyStageKeysToLoad(stageDefinitions),
2182
- ];
2176
+ export const CONTENT_ITEM_COLUMN_PAGE_LIMIT = 25;
2177
+
2178
+ export type ContentItemStagePage = {
2179
+ versions: ContentItemVersionRecord[];
2180
+ nextToken: string | null;
2181
+ };
2182
+
2183
+ /** One newest-updated-first page of the content items in one stage, so a board column can load as it is scrolled. */
2184
+ export async function loadNewsroomContentItemStagePage(
2185
+ stage: string,
2186
+ nextToken: string | null = null,
2187
+ limit: number = CONTENT_ITEM_COLUMN_PAGE_LIMIT,
2188
+ ): Promise<ContentItemStagePage> {
2183
2189
  if (isLocalPreviewEnabled()) {
2184
- return createLocalPreviewContentItemVersions().filter(version => stagesToLoad.includes(version.stage ?? ""));
2190
+ const versionsInStage = createLocalPreviewContentItemVersions().filter(version => version.stage === stage);
2191
+ const offset = nextToken ? Number(nextToken) : 0;
2192
+ const followingOffset = offset + limit;
2193
+ return {
2194
+ versions: versionsInStage.slice(offset, followingOffset),
2195
+ nextToken: followingOffset < versionsInStage.length ? String(followingOffset) : null,
2196
+ };
2185
2197
  }
2186
2198
  configureAmplifyClient();
2187
2199
  const client = generateClient<Schema>() as unknown as {
2188
2200
  graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<ContentItemVersionRecord>>;
2189
2201
  };
2190
2202
  const field = "listItemsByStageAndUpdatedAt";
2191
- const versionsPerStage = await Promise.all(stagesToLoad.map(async (stage) => {
2192
- const versions: ContentItemVersionRecord[] = [];
2193
- let nextToken: string | null = null;
2194
- do {
2195
- const response: GraphQLConnectionResponse<ContentItemVersionRecord> = await client.graphql({
2196
- query: CONTENT_ITEMS_BY_STAGE_QUERY,
2197
- variables: { stage, sortDirection: "DESC", limit: NEWSROOM_PAGE_LIMIT, nextToken },
2198
- authMode: USER_POOL_AUTH_MODE,
2199
- });
2200
- assertNoGraphQLErrors(response.errors);
2201
- const connection = response.data?.[field];
2202
- if (!connection) throw new Error(`Missing GraphQL connection payload for ${field}.`);
2203
- versions.push(...((connection.items ?? []).filter(Boolean) as ContentItemVersionRecord[]));
2204
- nextToken = connection.nextToken ?? null;
2205
- } while (nextToken);
2206
- return versions;
2207
- }));
2208
- return versionsPerStage.flat();
2203
+ const response: GraphQLConnectionResponse<ContentItemVersionRecord> = await client.graphql({
2204
+ query: CONTENT_ITEMS_BY_STAGE_QUERY,
2205
+ variables: { stage, sortDirection: "DESC", limit, nextToken },
2206
+ authMode: USER_POOL_AUTH_MODE,
2207
+ });
2208
+ assertNoGraphQLErrors(response.errors);
2209
+ const connection = response.data?.[field];
2210
+ if (!connection) throw new Error(`Missing GraphQL connection payload for ${field}.`);
2211
+ return {
2212
+ versions: (connection.items ?? []).filter(Boolean) as ContentItemVersionRecord[],
2213
+ nextToken: connection.nextToken ?? null,
2214
+ };
2209
2215
  }
2210
2216
 
2211
2217
  const PUBLISHED_ITEMS_QUERY = `
@@ -2231,6 +2237,7 @@ type PublishedItemBoardRow = {
2231
2237
  };
2232
2238
 
2233
2239
  export async function loadNewsroomPublishedContentItemVersions(): Promise<ContentItemVersionRecord[]> {
2240
+ if (isLocalPreviewEnabled()) return [];
2234
2241
  configureAmplifyClient();
2235
2242
  const client = generateClient<Schema>() as unknown as {
2236
2243
  graphql: (options: Record<string, unknown>) => Promise<GraphQLConnectionResponse<PublishedItemBoardRow>>;
@@ -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
+ }
@@ -2,12 +2,10 @@
2
2
 
3
3
  import { useEffect, useMemo, useRef, useState } from "react";
4
4
  import Link from "next/link";
5
- import { IssueCollection, IssueDetailPanel, useBoardColumnState } from "@kanbus/ui";
5
+ import { IssueCollection, IssueDetailPanel, IssueTypeFilter, issueTypeFromFilter, useBoardColumnState } from "@kanbus/ui";
6
6
  import "@kanbus/ui/styles/kanban.css";
7
7
  import "./newsroom-newsprint-board.css";
8
8
  import { PlusIcon } from "lucide-react";
9
- import { buttonVariants } from "./ui/button";
10
- import { cn } from "../lib/utils";
11
9
  import { newsroomHref } from "../lib/newsroom-base-path";
12
10
  import {
13
11
  ASSIGNMENT_BOARD_TYPE,
@@ -15,22 +13,32 @@ import {
15
13
  canReadNewsroomBoard,
16
14
  contentItemBoardConfig,
17
15
  contentItemBoardType,
16
+ deskBoardConfig,
17
+ deskContainsAssignment,
18
+ deskContainsContentItem,
18
19
  projectAssignmentBoard,
19
20
  projectContentItemBoard,
20
21
  type NativeBoardIssue,
21
22
  } from "../lib/newsroom-board";
22
- import type { AssignmentRecord } from "../lib/category-repository";
23
+ import type { AssignmentRecord, NewsroomSectionRecord } from "../lib/category-repository";
24
+ import { displayNewsroomSectionShortTitle } from "../lib/newsroom-sections";
23
25
  import { getContentItemName, getContentItemStageDefinitions, SITE_BRAND } from "../lib/site-brand";
24
26
  import { getNewsroomNavHref } from "../lib/newsroom-nav";
25
- import { loadNewsroomAssignmentPage, loadNewsroomContentItemVersions, loadNewsroomPublishedContentItemVersions } from "./news-desk-taxonomy-client";
27
+ import { loadNewsroomAssignmentPage, loadNewsroomPublishedContentItemVersions } from "./news-desk-taxonomy-client";
28
+ import { usePagedContentItemVersions } from "./use-paged-content-item-versions";
29
+ import type { ContentItemVersionRecord } from "../lib/content-item-lifecycle";
26
30
  import { useOptionalNewsDeskClient } from "./news-desk-client-provider";
27
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";
28
35
  import { NewsroomOpsShell, NewsroomOpsStatusBanner } from "./newsroom-ops-shell";
29
36
  import { useResolvedPapyrusTheme } from "./use-resolved-papyrus-theme";
30
37
  import { readLocalReaderSettings, subscribeReaderSettingsChanges } from "./reader-settings";
31
38
 
32
39
  const STATIC_MOTION = { mode: "static" } as const;
33
40
  const BOARD_REFRESH_INTERVAL_MILLISECONDS = 30_000;
41
+ const COLUMN_LOAD_MORE_DISTANCE_PIXELS = 240;
34
42
 
35
43
  async function loadEveryNewsroomAssignment() {
36
44
  const assignments: AssignmentRecord[] = [];
@@ -43,87 +51,128 @@ async function loadEveryNewsroomAssignment() {
43
51
  return assignments;
44
52
  }
45
53
 
46
- export type NewsroomBoardSubject = "contentItem" | "assignment";
54
+ export type NewsroomBoardSubject = "contentItem" | "assignment" | "desk";
47
55
 
48
56
  /** Read-only board of one record kind over authenticated CMS records; existing workspaces own every mutation. */
49
- export function NewsroomBoardView({ subject }: { subject: NewsroomBoardSubject }) {
57
+ export function NewsroomBoardView({ subject, deskId }: { subject: NewsroomBoardSubject; deskId?: string }) {
50
58
  const session = useOptionalNewsDeskClient();
51
59
  const allowed = canReadNewsroomBoard(session?.shell.phase);
52
60
  const theme = useResolvedPapyrusTheme();
53
61
  const [lowMotion, setLowMotion] = useState(false);
54
- const [issues, setIssues] = useState<NativeBoardIssue[]>([]);
62
+ const [assignmentBoardIssues, setAssignmentBoardIssues] = useState<NativeBoardIssue[]>([]);
63
+ const [publishedVersions, setPublishedVersions] = useState<ContentItemVersionRecord[]>([]);
64
+ const [assignments, setAssignments] = useState<AssignmentRecord[]>([]);
55
65
  const [view, setView] = useState<"board" | "list">("board");
56
66
  const [selected, setSelected] = useState<string | null>(null);
57
- const [loading, setLoading] = useState(false);
58
- const [error, setError] = useState<string | null>(null);
67
+ const [supportingRecordsLoading, setSupportingRecordsLoading] = useState(false);
68
+ const [supportingRecordsError, setSupportingRecordsError] = useState<string | null>(null);
59
69
  const generation = useRef(0);
60
- const openedCollapsedUntilOpenedStagesRef = useRef<string[]>([]);
61
- const refreshIssuesRef = useRef<((initial: boolean) => void) | null>(null);
70
+ const boardBodyRef = useRef<HTMLDivElement | null>(null);
71
+ const [typeFilter, setTypeFilter] = useState<string | null>("all");
72
+ const showsContentItems = subject !== "assignment";
73
+ const contentItemColumnKeyPrefix = `${contentItemBoardType()}/`;
74
+ const boardType = subject === "assignment" ? ASSIGNMENT_BOARD_TYPE : contentItemBoardType();
75
+ const deskAssignmentIssues = useMemo(() => subject === "desk" && deskId
76
+ ? projectAssignmentBoard(assignments.filter(assignment => deskContainsAssignment(assignment, deskId))) : [], [assignments, deskId, subject]);
77
+ const columnStateConfig = useMemo(() => subject === "contentItem" ? contentItemBoardConfig([])
78
+ : subject === "desk" ? deskBoardConfig(deskAssignmentIssues)
79
+ : assignmentBoardConfig(assignmentBoardIssues), [assignmentBoardIssues, deskAssignmentIssues, subject]);
80
+ const columnState = useBoardColumnState({ config: columnStateConfig, storageKey: `papyrus.cms.columns.v1.${SITE_BRAND.id}.${subject}` });
81
+ const openedCollapsedUntilOpenedStages = useMemo(() => !showsContentItems ? [] : getContentItemStageDefinitions()
82
+ .filter(definition => definition.collapsedUntilOpened && !columnState.collapsedColumns.has(`${contentItemColumnKeyPrefix}${definition.key}`))
83
+ .map(definition => definition.key), [contentItemColumnKeyPrefix, columnState.collapsedColumns, showsContentItems]);
84
+ const pagedContentItems = usePagedContentItemVersions({ enabled: allowed && showsContentItems, openedCollapsedUntilOpenedStages });
62
85
  useEffect(() => {
63
86
  setLowMotion(readLocalReaderSettings().motion === "low");
64
87
  return subscribeReaderSettingsChanges(settings => setLowMotion(settings.motion === "low"));
65
88
  }, []);
66
89
  useEffect(() => {
67
90
  const request = ++generation.current;
68
- setIssues([]); setSelected(null); setError(null);
69
- if (!allowed) { setLoading(false); return; }
70
- const loadIssues = () => subject === "contentItem"
71
- ? Promise.all([
72
- Promise.all([loadNewsroomContentItemVersions({ includeCollapsedUntilOpenedStages: openedCollapsedUntilOpenedStagesRef.current }), loadNewsroomPublishedContentItemVersions()]).then(([workingVersions, publishedVersions]) => [...workingVersions, ...publishedVersions]),
73
- loadEveryNewsroomAssignment(),
74
- ]).then(([contentItemVersions, assignments]) => projectContentItemBoard({ contentItemVersions, assignments }))
75
- : loadEveryNewsroomAssignment().then(projectAssignmentBoard);
91
+ setAssignmentBoardIssues([]); setPublishedVersions([]); setAssignments([]); setSelected(null); setSupportingRecordsError(null);
92
+ if (!allowed) { setSupportingRecordsLoading(false); return; }
93
+ const loadSupportingRecords = () => subject !== "assignment"
94
+ ? Promise.all([loadNewsroomPublishedContentItemVersions(), loadEveryNewsroomAssignment()]).then(([published, everyAssignment]) => {
95
+ if (generation.current === request) { setPublishedVersions(published); setAssignments(everyAssignment); }
96
+ })
97
+ : loadEveryNewsroomAssignment().then(everyAssignment => {
98
+ if (generation.current === request) { setAssignments(everyAssignment); setAssignmentBoardIssues(projectAssignmentBoard(everyAssignment)); }
99
+ });
76
100
  const refresh = (initial: boolean) => {
77
- if (initial) setLoading(true);
78
- loadIssues().then(loadedIssues => {
79
- if (generation.current !== request) return;
80
- setIssues(loadedIssues);
81
- setError(null);
101
+ if (initial) setSupportingRecordsLoading(true);
102
+ loadSupportingRecords().then(() => {
103
+ if (generation.current === request) setSupportingRecordsError(null);
82
104
  }).catch((reason: unknown) => {
83
- if (generation.current === request && initial) setError(reason instanceof Error ? reason.message : "Could not load newsroom records.");
84
- }).finally(() => { if (generation.current === request && initial) setLoading(false); });
105
+ if (generation.current === request && initial) setSupportingRecordsError(reason instanceof Error ? reason.message : "Could not load newsroom records.");
106
+ }).finally(() => { if (generation.current === request && initial) setSupportingRecordsLoading(false); });
85
107
  };
86
- refreshIssuesRef.current = refresh;
87
108
  refresh(true);
88
109
  const refreshWhenVisible = () => { if (document.visibilityState === "visible") refresh(false); };
89
110
  const interval = window.setInterval(refreshWhenVisible, BOARD_REFRESH_INTERVAL_MILLISECONDS);
90
111
  document.addEventListener("visibilitychange", refreshWhenVisible);
91
112
  return () => { generation.current = request + 1; window.clearInterval(interval); document.removeEventListener("visibilitychange", refreshWhenVisible); };
92
113
  }, [allowed, subject]);
93
- const config = useMemo(() => subject === "contentItem" ? contentItemBoardConfig(issues) : assignmentBoardConfig(issues), [issues, subject]);
94
- const boardType = subject === "contentItem" ? contentItemBoardType() : ASSIGNMENT_BOARD_TYPE;
95
- const columnState = useBoardColumnState({ config, storageKey: `papyrus.cms.columns.v1.${SITE_BRAND.id}.${subject}` });
96
- const openedCollapsedUntilOpenedStageKeys = useMemo(() => subject !== "contentItem" ? "" : getContentItemStageDefinitions()
97
- .filter(definition => definition.collapsedUntilOpened && !columnState.collapsedColumns.has(`${boardType}/${definition.key}`))
98
- .map(definition => definition.key).join(","), [boardType, columnState.collapsedColumns, subject]);
114
+ const issues = useMemo(() => {
115
+ if (subject === "assignment") return assignmentBoardIssues;
116
+ if (subject === "contentItem") return projectContentItemBoard({ contentItemVersions: [...pagedContentItems.versions, ...publishedVersions], assignments });
117
+ const versionsOnDesk = [...pagedContentItems.versions, ...publishedVersions].filter(version => deskId && deskContainsContentItem(version, deskId));
118
+ return [...projectContentItemBoard({ contentItemVersions: versionsOnDesk, assignments: assignments.filter(assignment => deskId && deskContainsAssignment(assignment, deskId)) }), ...deskAssignmentIssues];
119
+ }, [assignmentBoardIssues, assignments, deskAssignmentIssues, deskId, pagedContentItems.versions, publishedVersions, subject]);
120
+ const loading = supportingRecordsLoading || (showsContentItems && pagedContentItems.loading);
121
+ const error = supportingRecordsError ?? (showsContentItems ? pagedContentItems.error : null);
122
+ const config = useMemo(() => subject === "contentItem" ? contentItemBoardConfig(issues) : subject === "desk" ? deskBoardConfig(issues) : assignmentBoardConfig(issues), [issues, subject]);
123
+ const columnStageFromElement = (element: Element | null) => {
124
+ const columnKey = element?.closest("[data-column-key]")?.getAttribute("data-column-key");
125
+ return columnKey?.startsWith(contentItemColumnKeyPrefix) ? columnKey.slice(contentItemColumnKeyPrefix.length) : null;
126
+ };
127
+ const loadMoreWhenColumnIsNearItsEnd = (columnScroll: HTMLElement) => {
128
+ const distanceToEnd = columnScroll.scrollHeight - columnScroll.scrollTop - columnScroll.clientHeight;
129
+ const stage = columnStageFromElement(columnScroll);
130
+ if (stage && distanceToEnd < COLUMN_LOAD_MORE_DISTANCE_PIXELS) pagedContentItems.loadMoreForStage(stage);
131
+ };
132
+ const handleBoardScroll = (event: React.UIEvent<HTMLDivElement>) => {
133
+ const target = event.target as HTMLElement;
134
+ if (showsContentItems && target.classList?.contains("kb-column-scroll")) loadMoreWhenColumnIsNearItsEnd(target);
135
+ };
99
136
  useEffect(() => {
100
- openedCollapsedUntilOpenedStagesRef.current = openedCollapsedUntilOpenedStageKeys ? openedCollapsedUntilOpenedStageKeys.split(",") : [];
101
- if (openedCollapsedUntilOpenedStageKeys) refreshIssuesRef.current?.(false);
102
- }, [openedCollapsedUntilOpenedStageKeys]);
103
- const pageTitle = subject === "contentItem" ? getContentItemName().plural : "Assignments";
137
+ if (!showsContentItems) return;
138
+ boardBodyRef.current?.querySelectorAll<HTMLElement>(".kb-column-scroll").forEach(loadMoreWhenColumnIsNearItsEnd);
139
+ });
140
+ const deskTitle = session?.shell.dashboard?.newsroomSections?.find(section => section.id === deskId)?.title ?? (deskId ? displayNewsroomSectionShortTitle({ id: deskId } as NewsroomSectionRecord) : "Desk");
141
+ const pageTitle = subject === "contentItem" ? getContentItemName().plural : subject === "desk" ? deskTitle : "Assignments";
104
142
  const issue = issues.find(item => item.id === selected);
105
143
  if (!allowed) return <NewsDeskAccessGate shell={session?.shell ?? null} />;
106
- return <NewsroomOpsShell activeTab={subject === "contentItem" ? "articles" : "assignments"} appTitle={SITE_BRAND.appTitle} backLabel={SITE_BRAND.backToHomeLabel} pageTitle={pageTitle}
144
+ return <NewsroomOpsShell activeTab={subject === "contentItem" ? "articles" : subject === "desk" ? "desks" : "assignments"} appTitle={SITE_BRAND.appTitle} backLabel={SITE_BRAND.backToHomeLabel} pageTitle={pageTitle}
107
145
  headerActions={<>
108
- <div role="group" aria-label="Record presentation" className="organizer-board-view-toggle"><button type="button" aria-pressed={view === "board"} onClick={() => setView("board")}>Board</button><button type="button" aria-pressed={view === "list"} onClick={() => setView("list")}>List</button></div>
109
- {subject === "contentItem" && (
110
- <Link className={cn(buttonVariants({ size: "sm" }))} data-newsroom-article-new href={getNewsroomNavHref(newsroomHref("articles", "new"))}>
111
- <PlusIcon aria-hidden="true" />
146
+ <span role="status" className="organizer-board-note">{loading ? "Loading records…" : `${issues.length} records loaded.`}</span>
147
+ <div role="group" aria-label="Record presentation" className="organizer-board-view-toggle"><button type="button" className="organizer-header-button" aria-pressed={view === "board"} onClick={() => setView("board")}>Board</button><button type="button" className="organizer-header-button" aria-pressed={view === "list"} onClick={() => setView("list")}>List</button></div>
148
+ {showsContentItems && (
149
+ <Link className="organizer-header-button" data-newsroom-article-new href={getNewsroomNavHref(newsroomHref("articles", "new"))}>
150
+ <PlusIcon size={14} aria-hidden="true" />
112
151
  {`New ${getContentItemName().singular.toLowerCase()}`}
113
152
  </Link>
114
153
  )}
115
154
  </>}>
116
155
  <div className="organizer-native-board papyrus-newsprint-board" data-color-mode={theme}>
117
156
  {error && <NewsroomOpsStatusBanner tone="error">{error}</NewsroomOpsStatusBanner>}
118
- <p role="status" className="organizer-board-note">{loading ? "Loading records…" : `${issues.length} records loaded.`}</p>
119
- <div className="organizer-board-body" data-detail-open={Boolean(issue)}>
120
- <IssueCollection issues={issues} config={config} query={{ type: boardType }} view={view}
157
+ {subject === "desk" && <IssueTypeFilter config={config} value={typeFilter} onChange={setTypeFilter} name="Record type" className="organizer-board-type-filter" />}
158
+ <div className="organizer-board-body" data-detail-open={Boolean(issue)} ref={boardBodyRef} onScrollCapture={handleBoardScroll}>
159
+ <IssueCollection issues={issues} config={config} query={{ type: subject === "desk" ? issueTypeFromFilter(typeFilter) : boardType }} view={view}
121
160
  motion={lowMotion ? STATIC_MOTION : undefined}
122
161
  collapsedColumns={columnState.collapsedColumns} onToggleCollapse={columnState.toggleColumn} initialScroll="start"
123
162
  selectedIssueId={selected} detailOpen={Boolean(issue)} onSelectIssue={item => setSelected(item.id)} ariaLabel={pageTitle}
124
163
  emptyMessage={loading ? "Loading records…" : error ? "Records could not be loaded." : "No records in this selection."} />
125
- {issue && <IssueDetailPanel issue={issue} onClose={() => setSelected(null)} fields={[{ label: "CMS record ID", value: issue.nativeId }, { label: "Status", value: issue.nativeStatus }]}>
126
- <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
+ })()}
127
176
  </IssueDetailPanel>}
128
177
  </div>
129
178
  </div>
@@ -20,7 +20,7 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
20
20
  .organizer-destinations a[aria-current=page]{border-bottom:2px solid var(--org-ink);color:var(--org-ink)}
21
21
  .newsroom-organizer :is(a,button):focus-visible,.organizer-menu :is(a,button):focus-visible{outline:2px solid currentColor;outline-offset:-3px}
22
22
  .organizer-workspace{display:flex;flex-direction:column;min-height:0;min-width:0;overflow:hidden;padding:0 16px}
23
- .organizer-topbar{height:57px;min-height:57px;display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--org-ink)}
23
+ .organizer-topbar{height:48px;min-height:48px;display:flex;align-items:center;gap:16px;border-bottom:1px solid var(--org-ink)}
24
24
  .organizer-masthead{font:900 28px Newsroom Display,Georgia,serif;white-space:nowrap;max-width:30vw;overflow:hidden;text-overflow:ellipsis}
25
25
  .organizer-topbar h1{font:400 12px Newsroom UI,system-ui;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0}
26
26
  .organizer-toggle{display:flex;align-items:center;justify-content:center;width:36px;min-height:36px;padding:0;border:1px solid var(--org-line);flex-shrink:0}
@@ -36,20 +36,22 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
36
36
  .organizer-account-popup [role=menuitem]{min-height:40px;font-size:12px}.organizer-account-popup .reader-auth{display:contents}.organizer-account-popup .reader-auth__error{display:block;white-space:normal;font-size:11px;padding:8px}
37
37
  .organizer-bottom-nav{display:none}
38
38
  .organizer-main:has(.organizer-native-board){display:flex;overflow:hidden}
39
- .organizer-native-board{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;gap:12px}
39
+ .organizer-native-board{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;gap:8px}
40
40
  .organizer-board-view-toggle{display:flex;gap:4px;flex-shrink:0}
41
- .organizer-board-view-toggle button{min-height:36px;padding:4px 12px;border:1px solid var(--org-line)}
42
- .organizer-board-view-toggle button[aria-pressed=true]{background:var(--org-ink);color:var(--org-paper)}
43
- .organizer-board-note{font-size:11px;line-height:1.5;margin:0;color:var(--org-muted)}
41
+ .organizer-header-button{display:inline-flex;align-items:center;gap:6px;min-height:36px;padding:4px 12px;border:1px solid var(--org-line);font:inherit;color:inherit;background:transparent;text-decoration:none;white-space:nowrap;cursor:pointer}
42
+ .organizer-header-button[aria-pressed=true]{background:var(--org-ink);color:var(--org-paper)}
43
+ .organizer-board-note{font-size:11px;line-height:1.5;margin:0;color:var(--org-muted);white-space:nowrap}
44
+ @media(min-width:901px){.newsroom-organizer[data-sidebar-collapsed=false] .organizer-masthead{display:none}}
44
45
  .organizer-board-body{position:relative;display:grid;grid-template-columns:minmax(0,1fr);flex:1;min-width:0;min-height:0;gap:16px}
45
- .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)}
46
47
  .organizer-board-body>.kb-issue-collection{height:100%;min-height:0;overflow:auto}
47
48
  .organizer-board-body>.kb-issue-detail{overflow:auto;min-height:0}
48
- .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}
49
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}
50
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}
51
52
  .organizer-menu .organizer-destinations a{border:1px solid var(--org-line);padding:12px;color:var(--org-ink)}
52
53
  @media(max-width:900px){
54
+ .organizer-board-note{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
53
55
  .newsroom-organizer,.newsroom-organizer[data-sidebar-collapsed=true]{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr) auto}
54
56
  .organizer-sidebar,.organizer-toggle{display:none}.organizer-workspace{padding:0 16px}.organizer-topbar{gap:8px;height:55px;min-height:55px}
55
57
  .organizer-masthead{font-size:24px;max-width:36vw}.organizer-topbar h1{font-size:10px}.organizer-account-header{display:flex}.organizer-header-actions{gap:4px;max-width:35%}
@@ -61,3 +63,22 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
61
63
  .organizer-board-body>.kb-issue-detail{position:absolute;inset:0;z-index:2;overflow:auto}
62
64
  }
63
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>
@@ -13297,7 +13297,7 @@ function buildUiAnalysisReindexAssignmentPlan({
13297
13297
  createdBy: actorLabel,
13298
13298
  createdAt: now,
13299
13299
  updatedAt: now,
13300
- newsroomFeedKey: "assignments",
13300
+ newsroomFeedKey: "operations",
13301
13301
  metadata,
13302
13302
  };
13303
13303
  const event: AssignmentEventRecord = {
@@ -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(