@anthusai/papyrus 1.4.0-next.1 → 1.4.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/amplify/data/resource.js +134 -3
- package/amplify/data/resource.ts +145 -3
- package/amplify/data/schema.js +131 -0
- package/amplify/data/schema.ts +142 -0
- package/amplify/functions/category-action/handler.ts +272 -20
- package/amplify/functions/console-chat-responder/src/main.rs +1 -1
- package/amplify/functions/content-actions/handler.py +23 -0
- package/amplify/functions/content-actions/requirements.txt +1 -1
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/amplify/functions/renditions/requirements.txt +1 -1
- package/amplify/functions/shared/curation-delegation-flow.ts +147 -0
- package/amplify/functions/shared/curation-delegation.ts +115 -0
- package/amplify/functions/shared/python-bundle.js +3 -3
- package/amplify/functions/shared/python-bundle.ts +3 -3
- package/amplify/functions/shared/reference-curation-metadata.ts +51 -0
- package/amplify/functions/shared/slack-events.ts +2 -2
- package/amplify/publication/renditions-function.js +1 -1
- package/amplify/publication/renditions-function.ts +1 -1
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -1
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -1
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +2 -1
- package/app/[year]/[month]/[day]/page.tsx +3 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +2 -0
- package/app/api/archive/editions/route.ts +2 -0
- package/app/api/auth/prospectus/accounts/route.ts +9 -0
- package/app/api/local-preview/derive/route.ts +28 -0
- package/app/api/local-preview/media/route.ts +25 -0
- package/app/archive/page.tsx +3 -0
- package/app/globals.css +28 -28
- package/app/layout.tsx +8 -5
- package/app/newsroom/[section]/[...selection]/page.tsx +0 -1
- package/app/newsroom/[section]/page.tsx +3 -2
- package/app/newsroom/articles/page.tsx +8 -1
- package/app/newsroom/insights/[threadId]/page.tsx +5 -16
- package/app/newsroom/insights/page.tsx +4 -12
- package/app/newsroom/sections/[sectionId]/page.tsx +3 -0
- package/app/page.tsx +30 -1
- package/components/administration-delegations-panel.tsx +156 -0
- package/components/curation-delegation-client.ts +104 -0
- package/components/item-stream-view.tsx +63 -0
- package/components/markus-ir-preview.tsx +47 -2
- package/components/news-desk-page.tsx +0 -1
- package/components/news-desk-taxonomy-client.ts +82 -242
- package/components/newsroom-article-editor.tsx +138 -40
- package/components/newsroom-assignment-detail.tsx +79 -0
- package/components/newsroom-assignments-view.tsx +1 -1
- package/components/newsroom-board-view.tsx +128 -51
- package/components/newsroom-newsprint-board.css +5 -46
- package/components/newsroom-ops-overview.tsx +27 -5
- package/components/newsroom-organizer-shell.tsx +26 -8
- package/components/newsroom-organizer.css +267 -12
- package/components/newsroom-references-view.tsx +100 -23
- package/components/newsroom-topics-view.tsx +1 -1
- package/components/priming-proof-page.tsx +17 -8
- package/components/prospectus-account-chooser.tsx +131 -0
- package/components/reference-cyclotron-status.tsx +5 -5
- package/components/reference-cyclotron-suggestion.tsx +143 -0
- package/components/reference-live-agreement.tsx +62 -0
- package/components/reference-section-suggestion.tsx +104 -0
- package/components/story-reporting-trail-client.ts +91 -0
- package/components/story-reporting-trail.tsx +115 -0
- package/components/topic-steering-workspace.tsx +245 -678
- package/components/ui/sheet.tsx +10 -4
- package/components/use-paged-content-item-versions.ts +151 -0
- package/components/wiki-concepts-view.tsx +153 -0
- package/components/wiki-index-client.ts +51 -0
- package/infra/amplify-app-shell.js +17 -3
- package/infra/amplify-app-shell.ts +18 -3
- package/infra/site-config.js +6 -1
- package/infra/site-config.ts +7 -1
- package/lib/approved-priming-proof-reports.ts +6 -3
- package/lib/approved-priming-proofs/papyrus.json +90 -0
- package/lib/blog-feature-solver.ts +1 -109
- package/lib/category-repository.ts +1 -1
- package/lib/content-actions-client.ts +3 -0
- package/lib/content-actions-demo.ts +55 -0
- package/lib/content-item-lifecycle.ts +34 -5
- package/lib/content-source-context.ts +9 -1
- package/lib/curation-delegation-helpers.ts +69 -0
- package/lib/cyclotron-suggestions.ts +263 -0
- package/lib/front-matter-fields.ts +42 -0
- package/lib/front-mosaic.ts +2 -1
- package/lib/graphql-content-repository.ts +42 -7
- package/lib/item-stream.ts +47 -0
- package/lib/layout-plan.ts +3 -2
- package/lib/layout-warnings.ts +8 -0
- package/lib/local-preview-section-fixtures.ts +72 -0
- package/lib/local-preview-seed.ts +196 -7
- package/lib/local-preview-tenant.ts +27 -0
- package/lib/newsroom-articles.ts +2 -0
- package/lib/newsroom-assignments.ts +14 -1
- package/lib/newsroom-board.ts +52 -7
- package/lib/newsroom-demo-dashboard.ts +1 -1
- package/lib/newsroom-forum-routes.ts +2 -43
- package/lib/newsroom-index-filters.ts +3 -40
- package/lib/newsroom-media-backend.ts +5 -0
- package/lib/newsroom-nav.ts +1 -3
- package/lib/newsroom-organizer-nav.ts +8 -14
- package/lib/newsroom-section-routing.ts +1 -1
- package/lib/newsroom-sections.ts +12 -0
- package/lib/papyrus-web-locations.ts +3 -24
- package/lib/priming-proof-report.ts +1 -0
- package/lib/prospectus-account-client.ts +90 -0
- package/lib/prospectus-sign-in-routes.ts +45 -0
- package/lib/prospectus-site-identity.ts +13 -1
- package/lib/prospectus-tenant.ts +22 -0
- package/lib/reader-static-params.ts +2 -1
- package/lib/relevance-decisions.ts +29 -0
- package/lib/reporting-story-budget.ts +2 -2
- package/lib/semantic-graph.ts +5 -5
- package/lib/site-brand.ts +20 -0
- package/lib/site-env.ts +5 -0
- package/lib/site-section-suggestions.ts +120 -0
- package/lib/site-stack.ts +1 -1
- package/lib/story-reporting-trail.ts +106 -0
- package/lib/tenant-keys.ts +32 -0
- package/lib/wiki.ts +56 -0
- package/package.json +34 -2
- package/renderers/pretext/rhythm-article-page.tsx +1 -18
- package/routes.manifest.json +34 -7
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import {
|
|
5
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
6
|
+
import { IssueCollection, IssueDetailPanel, IssueTypeFilter, issueTypeFromFilter, useBoardColumnState } from "@kanbus/ui";
|
|
6
7
|
import "@kanbus/ui/styles/kanban.css";
|
|
8
|
+
import "@kanbus/ui/styles/newsprint.css";
|
|
7
9
|
import "./newsroom-newsprint-board.css";
|
|
8
10
|
import { PlusIcon } from "lucide-react";
|
|
9
|
-
import { buttonVariants } from "./ui/button";
|
|
10
|
-
import { cn } from "../lib/utils";
|
|
11
11
|
import { newsroomHref } from "../lib/newsroom-base-path";
|
|
12
12
|
import {
|
|
13
13
|
ASSIGNMENT_BOARD_TYPE,
|
|
@@ -15,22 +15,34 @@ import {
|
|
|
15
15
|
canReadNewsroomBoard,
|
|
16
16
|
contentItemBoardConfig,
|
|
17
17
|
contentItemBoardType,
|
|
18
|
+
deskBoardConfig,
|
|
19
|
+
deskContainsAssignment,
|
|
20
|
+
deskContainsContentItem,
|
|
18
21
|
projectAssignmentBoard,
|
|
19
22
|
projectContentItemBoard,
|
|
20
23
|
type NativeBoardIssue,
|
|
21
24
|
} from "../lib/newsroom-board";
|
|
22
|
-
import type { AssignmentRecord } from "../lib/category-repository";
|
|
23
|
-
import {
|
|
25
|
+
import type { AssignmentRecord, NewsroomSectionRecord } from "../lib/category-repository";
|
|
26
|
+
import { displayNewsroomSectionShortTitle, newsroomDeskOptions } from "../lib/newsroom-sections";
|
|
27
|
+
import { getContentItemName, getContentItemStageDefinitions, SITE_BRAND } from "../lib/site-brand";
|
|
24
28
|
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
25
|
-
import { loadNewsroomAssignmentPage,
|
|
29
|
+
import { loadNewsroomAssignmentPage, loadNewsroomPublishedContentItemVersions, saveNewsroomAssignmentDesk } from "./news-desk-taxonomy-client";
|
|
30
|
+
import { usePagedContentItemVersions } from "./use-paged-content-item-versions";
|
|
31
|
+
import type { ContentItemVersionRecord } from "../lib/content-item-lifecycle";
|
|
26
32
|
import { useOptionalNewsDeskClient } from "./news-desk-client-provider";
|
|
27
33
|
import { NewsDeskAccessGate } from "./newsroom-access-gate";
|
|
34
|
+
import { isLocalPreviewEnabled } from "../lib/local-preview";
|
|
35
|
+
import { NewsroomArticleEditor } from "./newsroom-article-editor";
|
|
36
|
+
import { NewsroomAssignmentDetail } from "./newsroom-assignment-detail";
|
|
28
37
|
import { NewsroomOpsShell, NewsroomOpsStatusBanner } from "./newsroom-ops-shell";
|
|
29
38
|
import { useResolvedPapyrusTheme } from "./use-resolved-papyrus-theme";
|
|
30
39
|
import { readLocalReaderSettings, subscribeReaderSettingsChanges } from "./reader-settings";
|
|
31
40
|
|
|
41
|
+
/** A board with a story or assignment open lives at `?open=<id>`, distinct from the standalone page for that record. */
|
|
42
|
+
const SELECTED_ISSUE_PARAMETER = "open";
|
|
32
43
|
const STATIC_MOTION = { mode: "static" } as const;
|
|
33
44
|
const BOARD_REFRESH_INTERVAL_MILLISECONDS = 30_000;
|
|
45
|
+
const COLUMN_LOAD_MORE_DISTANCE_PIXELS = 240;
|
|
34
46
|
|
|
35
47
|
async function loadEveryNewsroomAssignment() {
|
|
36
48
|
const assignments: AssignmentRecord[] = [];
|
|
@@ -43,43 +55,68 @@ async function loadEveryNewsroomAssignment() {
|
|
|
43
55
|
return assignments;
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
export type NewsroomBoardSubject = "contentItem" | "assignment";
|
|
58
|
+
export type NewsroomBoardSubject = "contentItem" | "assignment" | "desk";
|
|
47
59
|
|
|
48
60
|
/** Read-only board of one record kind over authenticated CMS records; existing workspaces own every mutation. */
|
|
49
|
-
export function NewsroomBoardView({ subject }: { subject: NewsroomBoardSubject }) {
|
|
61
|
+
export function NewsroomBoardView({ subject, deskId }: { subject: NewsroomBoardSubject; deskId?: string }) {
|
|
50
62
|
const session = useOptionalNewsDeskClient();
|
|
51
63
|
const allowed = canReadNewsroomBoard(session?.shell.phase);
|
|
52
64
|
const theme = useResolvedPapyrusTheme();
|
|
53
65
|
const [lowMotion, setLowMotion] = useState(false);
|
|
54
|
-
const [
|
|
66
|
+
const [assignmentBoardIssues, setAssignmentBoardIssues] = useState<NativeBoardIssue[]>([]);
|
|
67
|
+
const [publishedVersions, setPublishedVersions] = useState<ContentItemVersionRecord[]>([]);
|
|
68
|
+
const [assignments, setAssignments] = useState<AssignmentRecord[]>([]);
|
|
55
69
|
const [view, setView] = useState<"board" | "list">("board");
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
70
|
+
const router = useRouter();
|
|
71
|
+
const pathname = usePathname();
|
|
72
|
+
const searchParams = useSearchParams();
|
|
73
|
+
const selected = searchParams.get(SELECTED_ISSUE_PARAMETER);
|
|
74
|
+
const setSelected = useCallback((issueId: string | null) => {
|
|
75
|
+
const next = new URLSearchParams(searchParams.toString());
|
|
76
|
+
if (issueId) next.set(SELECTED_ISSUE_PARAMETER, issueId); else next.delete(SELECTED_ISSUE_PARAMETER);
|
|
77
|
+
const query = next.toString();
|
|
78
|
+
router.push(query ? `${pathname}?${query}` : pathname, { scroll: false });
|
|
79
|
+
}, [pathname, router, searchParams]);
|
|
80
|
+
const [supportingRecordsLoading, setSupportingRecordsLoading] = useState(false);
|
|
81
|
+
const [supportingRecordsError, setSupportingRecordsError] = useState<string | null>(null);
|
|
59
82
|
const generation = useRef(0);
|
|
83
|
+
const boardBodyRef = useRef<HTMLDivElement | null>(null);
|
|
84
|
+
const [typeFilter, setTypeFilter] = useState<string | null>("all");
|
|
85
|
+
const showsContentItems = subject !== "assignment";
|
|
86
|
+
const contentItemColumnKeyPrefix = `${contentItemBoardType()}/`;
|
|
87
|
+
const boardType = subject === "assignment" ? ASSIGNMENT_BOARD_TYPE : contentItemBoardType();
|
|
88
|
+
const deskAssignmentIssues = useMemo(() => subject === "desk" && deskId
|
|
89
|
+
? projectAssignmentBoard(assignments.filter(assignment => deskContainsAssignment(assignment, deskId))) : [], [assignments, deskId, subject]);
|
|
90
|
+
const columnStateConfig = useMemo(() => subject === "contentItem" ? contentItemBoardConfig([])
|
|
91
|
+
: subject === "desk" ? deskBoardConfig(deskAssignmentIssues)
|
|
92
|
+
: assignmentBoardConfig(assignmentBoardIssues), [assignmentBoardIssues, deskAssignmentIssues, subject]);
|
|
93
|
+
const columnState = useBoardColumnState({ config: columnStateConfig, storageKey: `papyrus.cms.columns.v1.${SITE_BRAND.id}.${subject}` });
|
|
94
|
+
const openedCollapsedUntilOpenedStages = useMemo(() => !showsContentItems ? [] : getContentItemStageDefinitions()
|
|
95
|
+
.filter(definition => definition.collapsedUntilOpened && !columnState.collapsedColumns.has(`${contentItemColumnKeyPrefix}${definition.key}`))
|
|
96
|
+
.map(definition => definition.key), [contentItemColumnKeyPrefix, columnState.collapsedColumns, showsContentItems]);
|
|
97
|
+
const pagedContentItems = usePagedContentItemVersions({ enabled: allowed && showsContentItems, openedCollapsedUntilOpenedStages });
|
|
60
98
|
useEffect(() => {
|
|
61
99
|
setLowMotion(readLocalReaderSettings().motion === "low");
|
|
62
100
|
return subscribeReaderSettingsChanges(settings => setLowMotion(settings.motion === "low"));
|
|
63
101
|
}, []);
|
|
64
102
|
useEffect(() => {
|
|
65
103
|
const request = ++generation.current;
|
|
66
|
-
|
|
67
|
-
if (!allowed) {
|
|
68
|
-
const
|
|
69
|
-
? Promise.all([
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
104
|
+
setAssignmentBoardIssues([]); setPublishedVersions([]); setAssignments([]); setSupportingRecordsError(null);
|
|
105
|
+
if (!allowed) { setSupportingRecordsLoading(false); return; }
|
|
106
|
+
const loadSupportingRecords = () => subject !== "assignment"
|
|
107
|
+
? Promise.all([loadNewsroomPublishedContentItemVersions(), loadEveryNewsroomAssignment()]).then(([published, everyAssignment]) => {
|
|
108
|
+
if (generation.current === request) { setPublishedVersions(published); setAssignments(everyAssignment); }
|
|
109
|
+
})
|
|
110
|
+
: loadEveryNewsroomAssignment().then(everyAssignment => {
|
|
111
|
+
if (generation.current === request) { setAssignments(everyAssignment); setAssignmentBoardIssues(projectAssignmentBoard(everyAssignment)); }
|
|
112
|
+
});
|
|
74
113
|
const refresh = (initial: boolean) => {
|
|
75
|
-
if (initial)
|
|
76
|
-
|
|
77
|
-
if (generation.current
|
|
78
|
-
setIssues(loadedIssues);
|
|
79
|
-
setError(null);
|
|
114
|
+
if (initial) setSupportingRecordsLoading(true);
|
|
115
|
+
loadSupportingRecords().then(() => {
|
|
116
|
+
if (generation.current === request) setSupportingRecordsError(null);
|
|
80
117
|
}).catch((reason: unknown) => {
|
|
81
|
-
if (generation.current === request && initial)
|
|
82
|
-
}).finally(() => { if (generation.current === request && initial)
|
|
118
|
+
if (generation.current === request && initial) setSupportingRecordsError(reason instanceof Error ? reason.message : "Could not load newsroom records.");
|
|
119
|
+
}).finally(() => { if (generation.current === request && initial) setSupportingRecordsLoading(false); });
|
|
83
120
|
};
|
|
84
121
|
refresh(true);
|
|
85
122
|
const refreshWhenVisible = () => { if (document.visibilityState === "visible") refresh(false); };
|
|
@@ -87,35 +124,75 @@ export function NewsroomBoardView({ subject }: { subject: NewsroomBoardSubject }
|
|
|
87
124
|
document.addEventListener("visibilitychange", refreshWhenVisible);
|
|
88
125
|
return () => { generation.current = request + 1; window.clearInterval(interval); document.removeEventListener("visibilitychange", refreshWhenVisible); };
|
|
89
126
|
}, [allowed, subject]);
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
127
|
+
const issues = useMemo(() => {
|
|
128
|
+
if (subject === "assignment") return assignmentBoardIssues;
|
|
129
|
+
if (subject === "contentItem") return projectContentItemBoard({ contentItemVersions: [...pagedContentItems.versions, ...publishedVersions], assignments });
|
|
130
|
+
const versionsOnDesk = [...pagedContentItems.versions, ...publishedVersions].filter(version => deskId && deskContainsContentItem(version, deskId));
|
|
131
|
+
return [...projectContentItemBoard({ contentItemVersions: versionsOnDesk, assignments: assignments.filter(assignment => deskId && deskContainsAssignment(assignment, deskId)) }), ...deskAssignmentIssues];
|
|
132
|
+
}, [assignmentBoardIssues, assignments, deskAssignmentIssues, deskId, pagedContentItems.versions, publishedVersions, subject]);
|
|
133
|
+
const loading = supportingRecordsLoading || (showsContentItems && pagedContentItems.loading);
|
|
134
|
+
const error = supportingRecordsError ?? (showsContentItems ? pagedContentItems.error : null);
|
|
135
|
+
const config = useMemo(() => subject === "contentItem" ? contentItemBoardConfig(issues) : subject === "desk" ? deskBoardConfig(issues) : assignmentBoardConfig(issues), [issues, subject]);
|
|
136
|
+
const columnStageFromElement = (element: Element | null) => {
|
|
137
|
+
const columnKey = element?.closest("[data-column-key]")?.getAttribute("data-column-key");
|
|
138
|
+
return columnKey?.startsWith(contentItemColumnKeyPrefix) ? columnKey.slice(contentItemColumnKeyPrefix.length) : null;
|
|
139
|
+
};
|
|
140
|
+
const loadMoreWhenColumnIsNearItsEnd = (columnScroll: HTMLElement) => {
|
|
141
|
+
const distanceToEnd = columnScroll.scrollHeight - columnScroll.scrollTop - columnScroll.clientHeight;
|
|
142
|
+
const stage = columnStageFromElement(columnScroll);
|
|
143
|
+
if (stage && distanceToEnd < COLUMN_LOAD_MORE_DISTANCE_PIXELS) pagedContentItems.loadMoreForStage(stage);
|
|
144
|
+
};
|
|
145
|
+
const handleBoardScroll = (event: React.UIEvent<HTMLDivElement>) => {
|
|
146
|
+
const target = event.target as HTMLElement;
|
|
147
|
+
if (showsContentItems && target.classList?.contains("kb-column-scroll")) loadMoreWhenColumnIsNearItsEnd(target);
|
|
148
|
+
};
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!showsContentItems) return;
|
|
151
|
+
boardBodyRef.current?.querySelectorAll<HTMLElement>(".kb-column-scroll").forEach(loadMoreWhenColumnIsNearItsEnd);
|
|
152
|
+
});
|
|
153
|
+
const deskTitle = session?.shell.dashboard?.newsroomSections?.find(section => section.id === deskId)?.title ?? (deskId ? displayNewsroomSectionShortTitle({ id: deskId } as NewsroomSectionRecord) : "Desk");
|
|
154
|
+
const pageTitle = subject === "contentItem" ? getContentItemName().plural : subject === "desk" ? deskTitle : "Assignments";
|
|
155
|
+
const deskOptions = useMemo(() => newsroomDeskOptions(session?.shell.dashboard?.newsroomSections ?? []), [session?.shell.dashboard?.newsroomSections]);
|
|
156
|
+
const changeAssignmentDesk = async (record: AssignmentRecord, deskId: string | null) => {
|
|
157
|
+
await saveNewsroomAssignmentDesk(record.id, deskId);
|
|
158
|
+
const moved = { ...record, sectionId: deskId, sectionKey: deskId };
|
|
159
|
+
setAssignments(current => current.map(candidate => candidate.id === record.id ? moved : candidate));
|
|
160
|
+
setAssignmentBoardIssues(current => current.map(candidate => candidate.nativeId === record.id ? { ...candidate } : candidate));
|
|
161
|
+
};
|
|
93
162
|
const issue = issues.find(item => item.id === selected);
|
|
94
163
|
if (!allowed) return <NewsDeskAccessGate shell={session?.shell ?? null} />;
|
|
95
|
-
return <NewsroomOpsShell activeTab={subject === "contentItem" ? "articles" : "assignments"} appTitle={SITE_BRAND.appTitle} backLabel={SITE_BRAND.backToHomeLabel} pageTitle={pageTitle}
|
|
96
|
-
headerActions={
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
? `Each ${getContentItemName().singular.toLowerCase()} appears once, in the column for its stage. Open one to use its existing editor.`
|
|
108
|
-
: "Each assignment appears once, in the column for its status. Open one to use its existing workflow controls."}</p>
|
|
164
|
+
return <NewsroomOpsShell activeTab={subject === "contentItem" ? "articles" : subject === "desk" ? "desks" : "assignments"} appTitle={SITE_BRAND.appTitle} backLabel={SITE_BRAND.backToHomeLabel} pageTitle={pageTitle}
|
|
165
|
+
headerActions={<>
|
|
166
|
+
<span role="status" className="organizer-board-note">{loading ? "Loading records…" : `${issues.length} records loaded.`}</span>
|
|
167
|
+
<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>
|
|
168
|
+
{showsContentItems && (
|
|
169
|
+
<Link className="organizer-header-button" data-newsroom-article-new href={getNewsroomNavHref(newsroomHref("articles", "new"))}>
|
|
170
|
+
<PlusIcon size={14} aria-hidden="true" />
|
|
171
|
+
{`New ${getContentItemName().singular.toLowerCase()}`}
|
|
172
|
+
</Link>
|
|
173
|
+
)}
|
|
174
|
+
</>}>
|
|
175
|
+
<div className="organizer-native-board papyrus-newsprint-board" data-kanbus-theme="newsprint" data-color-mode={theme}>
|
|
109
176
|
{error && <NewsroomOpsStatusBanner tone="error">{error}</NewsroomOpsStatusBanner>}
|
|
110
|
-
<
|
|
111
|
-
<div className="organizer-board-body" data-detail-open={Boolean(issue)}>
|
|
112
|
-
<IssueCollection issues={issues} config={config} query={{ type: boardType }} view={view}
|
|
177
|
+
{subject === "desk" && <IssueTypeFilter config={config} value={typeFilter} onChange={setTypeFilter} name="Record type" className="organizer-board-type-filter" />}
|
|
178
|
+
<div className="organizer-board-body" data-detail-open={Boolean(issue)} ref={boardBodyRef} onScrollCapture={handleBoardScroll}>
|
|
179
|
+
<IssueCollection issues={issues} config={config} query={{ type: subject === "desk" ? issueTypeFromFilter(typeFilter) : boardType }} view={view}
|
|
113
180
|
motion={lowMotion ? STATIC_MOTION : undefined}
|
|
114
|
-
|
|
181
|
+
collapsedColumns={columnState.collapsedColumns} onToggleCollapse={columnState.toggleColumn} initialScroll="start"
|
|
115
182
|
selectedIssueId={selected} detailOpen={Boolean(issue)} onSelectIssue={item => setSelected(item.id)} ariaLabel={pageTitle}
|
|
116
183
|
emptyMessage={loading ? "Loading records…" : error ? "Records could not be loaded." : "No records in this selection."} />
|
|
117
|
-
{issue && <IssueDetailPanel issue={issue} onClose={() => setSelected(null)} fields={[
|
|
118
|
-
|
|
184
|
+
{issue && <IssueDetailPanel issue={issue} onClose={() => setSelected(null)} fields={[]}>
|
|
185
|
+
{issue.kind === "contentItem"
|
|
186
|
+
? <>
|
|
187
|
+
<NewsroomArticleEditor key={issue.id} articleId={decodeURIComponent(issue.href.split("/").pop() ?? "")} demo={isLocalPreviewEnabled()} embedded />
|
|
188
|
+
<Link className="organizer-header-button" href={getNewsroomNavHref(issue.href)}>Open full page</Link>
|
|
189
|
+
</>
|
|
190
|
+
: (() => {
|
|
191
|
+
const record = assignments.find(candidate => candidate.id === issue.nativeId);
|
|
192
|
+
return record
|
|
193
|
+
? <NewsroomAssignmentDetail assignment={record} deskOptions={deskOptions} onDeskChange={deskId => changeAssignmentDesk(record, deskId)} pageHref={getNewsroomNavHref(issue.href)} showTitle={false} />
|
|
194
|
+
: <Link href={getNewsroomNavHref(issue.href)}>Open {issue.type}</Link>;
|
|
195
|
+
})()}
|
|
119
196
|
</IssueDetailPanel>}
|
|
120
197
|
</div>
|
|
121
198
|
</div>
|
|
@@ -1,48 +1,7 @@
|
|
|
1
1
|
.papyrus-newsprint-board {
|
|
2
|
-
--
|
|
3
|
-
--
|
|
4
|
-
--
|
|
5
|
-
--
|
|
6
|
-
--
|
|
7
|
-
--text-foreground: var(--org-ink);
|
|
8
|
-
--text-muted: var(--org-muted);
|
|
9
|
-
--text-selected: var(--org-ink);
|
|
10
|
-
--font-body: Newsroom UI, system-ui, sans-serif;
|
|
11
|
-
--kb-column-width: 245px;
|
|
12
|
-
--kb-column-radius: 0;
|
|
13
|
-
--kb-card-radius: 0;
|
|
14
|
-
--kb-card-border-width: 0;
|
|
15
|
-
--kb-card-shadow: none;
|
|
16
|
-
--kb-card-selected-shadow: none;
|
|
17
|
-
--kb-board-gap: 18px;
|
|
18
|
-
color: var(--org-ink);
|
|
19
|
-
background: transparent;
|
|
2
|
+
--kb-newsprint-paper: var(--org-paper);
|
|
3
|
+
--kb-newsprint-ink: var(--org-ink);
|
|
4
|
+
--kb-newsprint-line: var(--org-line);
|
|
5
|
+
--kb-newsprint-display-font: Newsroom Display, Georgia, serif;
|
|
6
|
+
--kb-newsprint-body-font: Newsroom UI, system-ui, sans-serif;
|
|
20
7
|
}
|
|
21
|
-
.papyrus-newsprint-board .kb-grid { height: 100%; min-height: 0; scrollbar-width: thin; }
|
|
22
|
-
.papyrus-newsprint-board .kb-column { border-left: 1px solid var(--org-line); padding-left: 10px; }
|
|
23
|
-
.papyrus-newsprint-board .kb-column-header {
|
|
24
|
-
border-bottom: 2px solid var(--org-ink);
|
|
25
|
-
padding: 5px 0;
|
|
26
|
-
font: 700 12px Newsroom Display, Georgia, serif;
|
|
27
|
-
text-transform: uppercase;
|
|
28
|
-
letter-spacing: .6px;
|
|
29
|
-
min-height: 32px;
|
|
30
|
-
}
|
|
31
|
-
.papyrus-newsprint-board .issue-card {
|
|
32
|
-
--issue-accent: var(--org-paper) !important;
|
|
33
|
-
--issue-accent-muted: var(--muted) !important;
|
|
34
|
-
--issue-accent-foreground: var(--org-ink) !important;
|
|
35
|
-
--issue-priority-bg: var(--org-paper) !important;
|
|
36
|
-
background: transparent !important;
|
|
37
|
-
border-radius: 0;
|
|
38
|
-
border-bottom: 1px solid var(--org-line) !important;
|
|
39
|
-
box-shadow: none !important;
|
|
40
|
-
}
|
|
41
|
-
.papyrus-newsprint-board .issue-accent-bar { border-bottom: 1px solid var(--org-line); background: var(--org-paper) !important; color: var(--org-ink) !important; }
|
|
42
|
-
.papyrus-newsprint-board .issue-accent-id { font: 400 9px Newsroom UI, system-ui, sans-serif; letter-spacing: .8px; white-space: nowrap; color: var(--org-ink) !important; }
|
|
43
|
-
.papyrus-newsprint-board .issue-accent-priority { border-radius: 0; color: var(--org-muted) !important; background: transparent !important; font-size: 8px; }
|
|
44
|
-
.papyrus-newsprint-board .issue-card h3 { font: 700 19px/1.2 Newsroom Display, Georgia, serif !important; letter-spacing: -.2px; }
|
|
45
|
-
.papyrus-newsprint-board .issue-card[data-selected=true] { outline: 2px solid var(--org-ink); outline-offset: -2px; }
|
|
46
|
-
.papyrus-newsprint-board .issue-card[data-selected=true] h3 { color: var(--org-ink) !important; }
|
|
47
|
-
.papyrus-newsprint-board .kb-issue-detail { background: var(--org-paper); border: 1px solid var(--org-line); border-radius: 0; box-shadow: none; }
|
|
48
|
-
.papyrus-newsprint-board .kb-issue-detail h2 { font: 700 28px/1.2 Newsroom Display, Georgia, serif; border-bottom: 3px double var(--org-ink); padding-bottom: 12px; }
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
ClipboardListIcon,
|
|
6
6
|
FileTextIcon,
|
|
7
7
|
LayersIcon,
|
|
8
|
-
LightbulbIcon,
|
|
9
8
|
MessageSquareIcon,
|
|
10
9
|
NetworkIcon,
|
|
11
10
|
SearchIcon,
|
|
@@ -30,6 +29,7 @@ import {
|
|
|
30
29
|
buildNewsroomSectionHref,
|
|
31
30
|
displayNewsroomSectionShortTitle,
|
|
32
31
|
isEnabledNewsroomSection,
|
|
32
|
+
newsroomDeskKind,
|
|
33
33
|
sortNewsroomSections,
|
|
34
34
|
} from "../lib/newsroom-sections";
|
|
35
35
|
import { newsroomInboxRowClassName } from "../lib/newsroom-list-selection";
|
|
@@ -49,7 +49,6 @@ const DESTINATION_ICONS: Record<DestinationKey, typeof BookOpenIcon> = {
|
|
|
49
49
|
assignments: ClipboardListIcon,
|
|
50
50
|
references: BookOpenIcon,
|
|
51
51
|
messages: MessageSquareIcon,
|
|
52
|
-
insights: LightbulbIcon,
|
|
53
52
|
topics: LayersIcon,
|
|
54
53
|
concepts: NetworkIcon,
|
|
55
54
|
administration: SettingsIcon,
|
|
@@ -102,6 +101,8 @@ export function NewsroomOpsOverview({
|
|
|
102
101
|
() => sortNewsroomSections(newsroomSections).filter(isEnabledNewsroomSection),
|
|
103
102
|
[newsroomSections],
|
|
104
103
|
);
|
|
104
|
+
const coreDesks = useMemo(() => desks.filter(desk => newsroomDeskKind(desk) === "core"), [desks]);
|
|
105
|
+
const rotatingDesks = useMemo(() => desks.filter(desk => newsroomDeskKind(desk) === "rotating"), [desks]);
|
|
105
106
|
|
|
106
107
|
const destinationCounts: Partial<Record<DestinationKey, number | null>> = {
|
|
107
108
|
references: referenceCounts.pending ?? 0,
|
|
@@ -174,11 +175,11 @@ export function NewsroomOpsOverview({
|
|
|
174
175
|
href={href}
|
|
175
176
|
key={lineageId}
|
|
176
177
|
>
|
|
177
|
-
<Badge className="mt-0.5 shrink-0" variant="outline">Pending</Badge>
|
|
178
178
|
<div className="min-w-0 flex-1">
|
|
179
179
|
<p className="truncate text-sm font-medium text-foreground">{referenceDisplayTitle(reference)}</p>
|
|
180
180
|
<p className="text-xs text-muted-foreground">Review source intake · {formatReferenceListDate(reference)}</p>
|
|
181
181
|
</div>
|
|
182
|
+
<Badge className="mt-0.5 shrink-0" variant="outline">Pending</Badge>
|
|
182
183
|
</Link>
|
|
183
184
|
);
|
|
184
185
|
})}
|
|
@@ -195,11 +196,11 @@ export function NewsroomOpsOverview({
|
|
|
195
196
|
href={href}
|
|
196
197
|
key={assignment.id}
|
|
197
198
|
>
|
|
198
|
-
<Badge className="mt-0.5 shrink-0" variant="secondary">{assignment.status}</Badge>
|
|
199
199
|
<div className="min-w-0 flex-1">
|
|
200
200
|
<p className="truncate text-sm font-medium text-foreground">{assignment.title}</p>
|
|
201
201
|
<p className="text-xs text-muted-foreground">Assignment · {assignment.assignmentTypeKey}</p>
|
|
202
202
|
</div>
|
|
203
|
+
<Badge className="mt-0.5 shrink-0" variant="secondary">{assignment.status}</Badge>
|
|
203
204
|
</Link>
|
|
204
205
|
);
|
|
205
206
|
})}
|
|
@@ -216,8 +217,9 @@ export function NewsroomOpsOverview({
|
|
|
216
217
|
{desks.length ? (
|
|
217
218
|
<section aria-label="Desks" className="space-y-3" data-newsroom-ops-desks>
|
|
218
219
|
<h2 className="text-sm font-medium text-muted-foreground">Desks</h2>
|
|
220
|
+
{coreDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="core">Core desks</h3> : null}
|
|
219
221
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
220
|
-
{
|
|
222
|
+
{coreDesks.map((section) => (
|
|
221
223
|
<Link
|
|
222
224
|
className="block rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
223
225
|
data-newsroom-ops-desk={section.id}
|
|
@@ -235,6 +237,26 @@ export function NewsroomOpsOverview({
|
|
|
235
237
|
</Link>
|
|
236
238
|
))}
|
|
237
239
|
</div>
|
|
240
|
+
{rotatingDesks.length ? <h3 className="newsroom-desk-group-heading" data-newsroom-ops-desk-group="rotating">Rotating desks <small>appear for a while, then rotate out</small></h3> : null}
|
|
241
|
+
{rotatingDesks.length ? <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
242
|
+
{rotatingDesks.map((section) => (
|
|
243
|
+
<Link
|
|
244
|
+
className="block rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
245
|
+
data-newsroom-ops-desk={section.id}
|
|
246
|
+
href={buildNewsroomSectionHref(section.id, demo)}
|
|
247
|
+
key={section.id}
|
|
248
|
+
>
|
|
249
|
+
<Card className="h-full transition-colors hover:border-primary/30">
|
|
250
|
+
<CardHeader className="p-4">
|
|
251
|
+
<CardTitle className="text-base font-semibold">{section.title}</CardTitle>
|
|
252
|
+
<CardDescription className="uppercase tracking-wide text-[0.65rem]">
|
|
253
|
+
{displayNewsroomSectionShortTitle(section)}
|
|
254
|
+
</CardDescription>
|
|
255
|
+
</CardHeader>
|
|
256
|
+
</Card>
|
|
257
|
+
</Link>
|
|
258
|
+
))}
|
|
259
|
+
</div> : null}
|
|
238
260
|
</section>
|
|
239
261
|
) : null}
|
|
240
262
|
</div>
|
|
@@ -2,26 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useId, useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
|
-
import { usePathname } from "next/navigation";
|
|
6
|
-
import { BookOpen, ClipboardList,
|
|
5
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
6
|
+
import { BookOpen, ClipboardList, Menu, Search, MessagesSquare, PanelLeftClose, PanelLeftOpen, Settings, UserRound, LogIn, LogOut } from "lucide-react";
|
|
7
7
|
import { ORGANIZER_NAV_GROUPS, ORGANIZER_PRIMARY_NAV, ORGANIZER_SETTINGS_DESTINATION } from "../lib/newsroom-organizer-nav";
|
|
8
8
|
import { browserPathToInternalNewsroomPath } from "../lib/newsroom-base-path";
|
|
9
9
|
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
10
10
|
import { SITE_BRAND } from "../lib/site-brand";
|
|
11
11
|
import { useOptionalNewsDeskClient } from "./news-desk-client-provider";
|
|
12
12
|
import { ReaderAuthControl } from "./reader-auth-control";
|
|
13
|
+
import { ProspectusAccountChooser, useProspectusAccountSummary } from "./prospectus-account-chooser";
|
|
13
14
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetTrigger } from "./ui/sheet";
|
|
14
15
|
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuSeparator } from "./ui/dropdown-menu";
|
|
15
16
|
import type { NewsroomOpsShellProps } from "./newsroom-ops-shell";
|
|
16
17
|
import "./newsroom-organizer.css";
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
/** The standard place to search the knowledge base; the Search page is the results page. */
|
|
20
|
+
function OrganizerSearch({ demo }: { demo: boolean }) {
|
|
21
|
+
const router = useRouter();
|
|
22
|
+
const searchParams = useSearchParams();
|
|
23
|
+
const [text, setText] = useState(searchParams.get("q") ?? "");
|
|
24
|
+
return <form className="organizer-search" role="search" onSubmit={event => {
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
const query = text.trim();
|
|
27
|
+
if (query) router.push(getNewsroomNavHref(`/newsroom/search?q=${encodeURIComponent(query)}`, demo));
|
|
28
|
+
}}>
|
|
29
|
+
<Search size={14} aria-hidden="true" />
|
|
30
|
+
<input aria-label="Search the knowledge base" data-newsroom-search type="search" onChange={event => setText(event.target.value)} placeholder="Search" value={text} />
|
|
31
|
+
</form>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ICONS = { articles: ClipboardList, references: BookOpen, messages: MessagesSquare };
|
|
19
35
|
|
|
20
36
|
/** Navigation is a presentation layer; existing route and server authorization still own access. */
|
|
21
37
|
function Navigation({ activePath, demo, admin, surface, onNavigate }: { activePath: string; demo: boolean; admin: boolean; surface: string; onNavigate?: () => void }) {
|
|
22
38
|
const id = useId();
|
|
23
39
|
const all = ORGANIZER_NAV_GROUPS.flatMap(group => group.items);
|
|
24
|
-
const active = all.filter(item => activePath ===
|
|
40
|
+
const active = all.filter(item => [item.path, ...(item.alsoActiveOnPaths ?? [])].some(candidate => activePath === candidate || (candidate !== "/newsroom" && activePath.startsWith(candidate + "/")))).sort((a, b) => b.path.length - a.path.length)[0]?.key;
|
|
25
41
|
return <nav className="organizer-destinations" aria-label={surface === "sidebar" ? "Newsroom" : "All workspaces"}>
|
|
26
42
|
{ORGANIZER_NAV_GROUPS.map((group, index) => <section key={group.title} aria-labelledby={`${id}-${index}`}>
|
|
27
43
|
<h3 id={`${id}-${index}`}>{group.title}</h3>
|
|
@@ -39,6 +55,7 @@ function Account({ placement }: { placement: "sidebar" | "header" }) {
|
|
|
39
55
|
const session = useOptionalNewsDeskClient();
|
|
40
56
|
const auth = session?.shell.auth ?? { status: "loading" as const, label: "Checking sign-in" };
|
|
41
57
|
const [open, setOpen] = useState(false);
|
|
58
|
+
const accountSummary = useProspectusAccountSummary();
|
|
42
59
|
useEffect(() => {
|
|
43
60
|
const media = matchMedia("(max-width: 900px)");
|
|
44
61
|
const close = () => setOpen(false);
|
|
@@ -46,12 +63,13 @@ function Account({ placement }: { placement: "sidebar" | "header" }) {
|
|
|
46
63
|
return () => media.removeEventListener("change", close);
|
|
47
64
|
}, []);
|
|
48
65
|
return <DropdownMenu open={open} onOpenChange={setOpen}>
|
|
49
|
-
<DropdownMenuTrigger className={`organizer-account organizer-account-${placement}`} aria-label="Account menu" title="Account menu">
|
|
50
|
-
<UserRound size={18} aria-hidden="true" /><span><strong>{auth.label}</strong><small>{auth.status === "signedIn" ? "Account & preferences" : auth.status === "signedOut" ? "Not signed in" : "Checking session"}</small></span>
|
|
66
|
+
<DropdownMenuTrigger id={`organizer-account-trigger-${placement}`} className={`organizer-account organizer-account-${placement}`} aria-label="Account menu" title="Account menu">
|
|
67
|
+
<UserRound size={18} aria-hidden="true" /><span><strong>{auth.label}</strong><small>{accountSummary ?? (auth.status === "signedIn" ? "Account & preferences" : auth.status === "signedOut" ? "Not signed in" : "Checking session")}</small></span>
|
|
51
68
|
</DropdownMenuTrigger>
|
|
52
69
|
<DropdownMenuContent className="organizer-account-popup" side={placement === "sidebar" ? "top" : "bottom"} align="end">
|
|
53
70
|
<DropdownMenuGroup>
|
|
54
71
|
<DropdownMenuLabel>{auth.label}</DropdownMenuLabel><DropdownMenuSeparator />
|
|
72
|
+
<ProspectusAccountChooser />
|
|
55
73
|
<DropdownMenuItem render={<Link href={getNewsroomNavHref(ORGANIZER_SETTINGS_DESTINATION.path)} />}><Settings aria-hidden="true" />{ORGANIZER_SETTINGS_DESTINATION.label}</DropdownMenuItem>
|
|
56
74
|
<ReaderAuthControl authState={auth} renderAction={({ disabled, onClick, signedIn }) => <DropdownMenuItem disabled={disabled} closeOnClick={false} onClick={onClick}>
|
|
57
75
|
{signedIn ? <LogOut aria-hidden="true" /> : <LogIn aria-hidden="true" />}{signedIn ? "Sign out" : "Sign in"}
|
|
@@ -107,7 +125,7 @@ export function NewsroomOrganizerShell({ activeTab, appTitle, backHref = "/", ba
|
|
|
107
125
|
return <div className="newsroom-organizer" data-news-desk data-newsroom-ops-shell="true" data-newsroom-chrome="organizer" data-sidebar-collapsed={collapsed || !showNavigation ? "true" : "false"}>
|
|
108
126
|
{showNavigation && <aside className="organizer-sidebar" hidden={collapsed} id="organizer-sidebar">
|
|
109
127
|
<div className="organizer-sidebar-head">
|
|
110
|
-
<Link className="organizer-brand" href={getNewsroomNavHref("/newsroom", demo)}>{appTitle}<small>The newsroom</small></Link>
|
|
128
|
+
<Link className="organizer-brand" href={getNewsroomNavHref("/newsroom", demo)}>{SITE_BRAND.components?.NewsroomLogo ? <SITE_BRAND.components.NewsroomLogo className="organizer-brand-logo" /> : appTitle}<small>The newsroom</small></Link>
|
|
111
129
|
{sidebarToggle}
|
|
112
130
|
</div>
|
|
113
131
|
<Navigation activePath={activePath} demo={demo} admin={admin} surface="sidebar" />
|
|
@@ -117,7 +135,7 @@ export function NewsroomOrganizerShell({ activeTab, appTitle, backHref = "/", ba
|
|
|
117
135
|
<header className="organizer-topbar" data-newsroom-ops-topbar>
|
|
118
136
|
{showNavigation && collapsed && sidebarToggle}
|
|
119
137
|
<Link className="organizer-masthead" href={backHref} title={backLabel}>{appTitle}</Link>
|
|
120
|
-
<h1 data-newsroom-ops-title>{pageTitle}</h1><div className="organizer-header-actions">{headerActions}</div><Account placement="header" />
|
|
138
|
+
<h1 data-newsroom-ops-title>{pageTitle}</h1><OrganizerSearch demo={demo} /><div className="organizer-header-actions">{headerActions}</div><Account placement="header" />
|
|
121
139
|
</header>
|
|
122
140
|
<main className="organizer-main">{demo && <p className="organizer-demo-notice">Demo workspace · synthetic examples</p>}{children}</main>
|
|
123
141
|
</div>
|