@anthusai/papyrus 1.0.0-next.9 → 1.1.0
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/auth/prospectus-google-client.js +66 -0
- package/amplify/auth/prospectus-google-client.ts +71 -0
- package/amplify/auth/redirect-urls.js +54 -0
- package/amplify/auth/redirect-urls.ts +66 -0
- package/amplify/auth/resource.js +23 -5
- package/amplify/auth/resource.ts +31 -5
- package/amplify/data/resource.js +42 -80
- package/amplify/data/resource.ts +42 -80
- package/amplify/data/schema.js +45 -62
- package/amplify/data/schema.ts +47 -62
- package/amplify/functions/category-action/handler.ts +5 -3
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
- package/amplify/functions/console-chat-responder/resource.js +0 -7
- package/amplify/functions/console-chat-responder/resource.ts +0 -8
- package/amplify/functions/console-chat-responder/src/main.rs +0 -9
- package/amplify/functions/content-actions/handler.py +143 -1
- package/amplify/functions/content-actions/requirements.txt +2 -1
- package/amplify/functions/content-actions/resource.js +6 -1
- package/amplify/functions/content-actions/resource.ts +9 -2
- package/amplify/functions/email-submission-processor/handler.py +3 -1
- package/amplify/functions/renditions/handler.py +120 -0
- package/amplify/functions/renditions/requirements.txt +4 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +32 -4
- package/amplify/functions/shared/email-submission.ts +109 -1
- package/amplify/functions/shared/python-bundle.js +42 -2
- package/amplify/functions/shared/python-bundle.ts +47 -2
- package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
- package/amplify/inbound-email/ses-intake.js +86 -0
- package/amplify/inbound-email/ses-intake.ts +110 -0
- package/amplify/plugins/registry.js +19 -0
- package/amplify/plugins/registry.ts +23 -0
- package/amplify/plugins/signup/function/handler.py +200 -0
- package/amplify/plugins/signup/function/resource.js +27 -0
- package/amplify/plugins/signup/function/resource.ts +35 -0
- package/amplify/plugins/signup/papyrus.plugin.js +59 -0
- package/amplify/plugins/signup/papyrus.plugin.ts +74 -0
- package/amplify/plugins/signup/schema.js +40 -0
- package/amplify/plugins/signup/schema.ts +41 -0
- package/amplify/plugins/signup/waf.js +48 -0
- package/amplify/plugins/signup/waf.ts +51 -0
- package/amplify/plugins/types.js +1 -0
- package/amplify/plugins/types.ts +39 -0
- package/amplify/plugins/wire.js +24 -0
- package/amplify/plugins/wire.ts +26 -0
- package/amplify/publication/default-publication.js +18 -0
- package/amplify/publication/default-publication.ts +20 -0
- package/amplify/publication/invalidation-alarm.js +26 -0
- package/amplify/publication/invalidation-alarm.ts +30 -0
- package/amplify/publication/redirect-and-index.js +37 -0
- package/amplify/publication/renditions-function.js +74 -0
- package/amplify/publication/renditions-function.ts +112 -0
- package/amplify/publication/renditions-settings.js +51 -0
- package/amplify/publication/renditions-settings.ts +58 -0
- package/amplify/publication/reproject-on-deploy.js +73 -0
- package/amplify/publication/reproject-on-deploy.ts +78 -0
- package/amplify/publication/s3-projection-settings.js +67 -0
- package/amplify/publication/s3-projection-settings.ts +101 -0
- package/amplify/publication/s3-projection.js +151 -0
- package/amplify/publication/s3-projection.ts +201 -0
- package/amplify/resolver-waves.js +26 -0
- package/amplify/resolver-waves.ts +34 -0
- package/amplify/revalidate-invalidation-settings.js +30 -0
- package/amplify/revalidate-invalidation-settings.ts +35 -0
- package/amplify/revalidate-secret-policy.js +13 -0
- package/amplify/revalidate-secret-policy.ts +19 -0
- package/amplify/site-backend-config.js +65 -1
- package/amplify/site-backend-config.ts +153 -2
- package/amplify/site-backend-names.js +103 -0
- package/amplify/site-backend-names.ts +166 -0
- package/amplify/site-backend.js +150 -176
- package/amplify/site-backend.ts +189 -201
- package/amplify/storage-backups/construct.js +31 -0
- package/amplify/storage-backups/construct.ts +47 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +8 -1
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +6 -1
- package/app/[year]/[month]/[day]/page.tsx +1 -1
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +6 -1
- package/app/api/archive/editions/route.ts +1 -1
- package/app/api/auth/prospectus/callback/route.ts +9 -0
- package/app/api/auth/prospectus/selection/route.ts +9 -0
- package/app/api/auth/prospectus/session/route.ts +9 -0
- package/app/api/auth/prospectus/sign-in/route.ts +9 -0
- package/app/api/auth/prospectus/sign-out/route.ts +9 -0
- package/app/api/cyclotron/priming-proof/route.ts +11 -0
- package/app/api/media/[...path]/route.ts +5 -16
- package/app/api/revalidate/route.ts +15 -7
- package/app/archive/page.tsx +1 -1
- package/app/articles/[slug]/page.tsx +7 -5
- package/app/blog-rhythm.css +278 -0
- package/app/cyclotron/page.tsx +13 -0
- package/app/globals.css +2 -0
- package/app/layout.tsx +16 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +9 -6
- package/app/newsroom/[section]/page.tsx +6 -3
- package/app/newsroom/layout.tsx +4 -9
- package/app/newsroom/settings/page.tsx +6 -0
- package/app/newsroom/work/page.tsx +10 -0
- package/app/page.tsx +22 -8
- package/app/robots.ts +2 -5
- package/app/tailwind.css +1 -6
- package/components/amplify-client-provider.tsx +5 -22
- package/components/archive-shell.tsx +2 -22
- package/components/article-video.tsx +152 -5
- package/components/blog-page-background.tsx +23 -6
- package/components/news-desk-page.tsx +4 -15
- package/components/news-desk-taxonomy-client.ts +90 -3
- package/components/newspaper.tsx +6 -2
- package/components/newsroom-access-gate.tsx +2 -1
- package/components/newsroom-board-view.tsx +99 -0
- package/components/newsroom-client-shell.tsx +12 -0
- package/components/newsroom-curation-provider.tsx +17 -0
- package/components/newsroom-fonts/playfair-display-700.ttf +0 -0
- package/components/newsroom-fonts/playfair-display-900.ttf +0 -0
- package/components/newsroom-fonts/playfair-display-OFL.txt +93 -0
- package/components/newsroom-fonts/plus-jakarta-sans-400.ttf +0 -0
- package/components/newsroom-fonts/plus-jakarta-sans-600.ttf +0 -0
- package/components/newsroom-fonts/plus-jakarta-sans-OFL.txt +93 -0
- package/components/newsroom-newsprint-board.css +48 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +7 -1
- package/components/newsroom-organizer-shell.tsx +130 -0
- package/components/newsroom-organizer.css +63 -0
- package/components/newsroom-references-view.tsx +92 -8
- package/components/papyrus-console-shell.tsx +4 -2
- package/components/pictogram-figure.tsx +9 -2
- package/components/presentation-header.tsx +135 -0
- package/components/presentation-rhythm-hrule.tsx +17 -0
- package/components/priming-proof-page.tsx +94 -0
- package/components/publication-cyclotron-panel.tsx +57 -0
- package/components/reader-auth-control.tsx +8 -5
- package/components/reference-curation-controls.tsx +77 -0
- package/components/reference-cyclotron-status.tsx +109 -0
- package/components/reference-priming.tsx +18 -0
- package/components/reference-relevance-review.tsx +75 -0
- package/components/settings-page.tsx +2 -1
- package/components/signup-entries-panel.tsx +94 -0
- package/components/topic-steering-workspace.tsx +78 -8
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +28 -0
- package/components/use-near-viewport.ts +30 -0
- package/components/use-rhythm-overlay.ts +29 -0
- package/infra/amplify-app-shell.js +166 -27
- package/infra/amplify-app-shell.ts +180 -31
- package/infra/build-specs.js +23 -7
- package/infra/build-specs.ts +27 -10
- package/infra/site-config.js +248 -36
- package/infra/site-config.ts +313 -51
- package/lib/amplify-outputs-path.ts +5 -1
- package/lib/amplify-server-runtime.ts +36 -1
- package/lib/approved-priming-proof-reports.ts +6 -0
- package/lib/blog-feature-solver.ts +459 -0
- package/lib/blog-rhythm.ts +134 -0
- package/lib/brand-fonts-watch.ts +43 -0
- package/lib/brand-fonts.ts +112 -0
- package/lib/cached-content-repository.ts +12 -0
- package/lib/category-repository.ts +43 -93
- package/lib/content-item-lifecycle.ts +55 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-types.ts +4 -0
- package/lib/curation-client.ts +61 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/graphql-content-repository.ts +56 -22
- package/lib/media-route-response.ts +45 -0
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-base-path.ts +12 -3
- package/lib/newsroom-board.ts +99 -0
- package/lib/newsroom-category-drilldown.ts +3 -2
- package/lib/newsroom-forum-routes.ts +6 -4
- package/lib/newsroom-index-filters.ts +14 -10
- package/lib/newsroom-nav.ts +3 -1
- package/lib/newsroom-organizer-nav.ts +37 -0
- package/lib/newsroom-section-routing.ts +20 -0
- package/lib/oauth-redirect-priority.ts +40 -0
- package/lib/papyrus-web-locations.ts +7 -6
- package/lib/plugin-settings.ts +11 -0
- package/lib/presentation-footer.ts +3 -2
- package/lib/pretext-layout.ts +12 -3
- package/lib/priming-proof-report.ts +90 -0
- package/lib/prospectus-publication-access.ts +53 -0
- package/lib/prospectus-session.ts +196 -0
- package/lib/prospectus-sign-in-routes.ts +284 -0
- package/lib/prospectus-site-identity.ts +303 -0
- package/lib/publication-cyclotron.ts +74 -0
- package/lib/rate-limit-retry.ts +55 -0
- package/lib/reader-base-path.ts +24 -0
- package/lib/reader-cache-lifetime.ts +5 -0
- package/lib/reader-static-params.ts +85 -4
- package/lib/reader-storage-url.ts +9 -12
- package/lib/reference-policy.ts +5 -0
- package/lib/relevance-decisions.ts +103 -0
- package/lib/relevance-status.ts +63 -0
- package/lib/renderer.ts +7 -0
- package/lib/revalidate-prewarm.ts +53 -0
- package/lib/revalidate-secret.ts +59 -0
- package/lib/robots-policy.ts +14 -0
- package/lib/root-route-redirect.ts +16 -0
- package/lib/semantic-graph.ts +7 -0
- package/lib/shared-credentials-provider.ts +61 -0
- package/lib/signup-csv.ts +24 -0
- package/lib/site-brand.ts +59 -0
- package/lib/staging-gated-path.ts +23 -0
- package/lib/staging-preview-object.ts +21 -5
- package/lib/test-editor-override.ts +4 -0
- package/lib/video-loading.ts +22 -0
- package/lib/video-script.ts +14 -0
- package/middleware.ts +11 -19
- package/package.json +73 -8
- package/renderers/pretext/article-page.tsx +39 -3
- package/renderers/pretext/presentation-shared.tsx +113 -0
- package/renderers/pretext/presentation-shell.tsx +15 -102
- package/renderers/pretext/rhythm-article-page.tsx +165 -0
- package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
- package/renderers/pretext/rhythm-shell-style.ts +35 -0
- package/routes.manifest.json +123 -12
- package/src/sync.mjs +30 -6
- package/src/with-papyrus.d.mts +9 -0
- package/src/with-papyrus.mjs +48 -3
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
- package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
- package/components/ai-elements/message.tsx +0 -360
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useId, useState } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { usePathname } from "next/navigation";
|
|
6
|
+
import { BookOpen, ClipboardList, LayoutDashboard, Menu, MessagesSquare, PanelLeftClose, PanelLeftOpen, Settings, UserRound, LogIn, LogOut } from "lucide-react";
|
|
7
|
+
import { ORGANIZER_NAV_GROUPS, ORGANIZER_PRIMARY_NAV, ORGANIZER_SETTINGS_DESTINATION } from "../lib/newsroom-organizer-nav";
|
|
8
|
+
import { browserPathToInternalNewsroomPath } from "../lib/newsroom-base-path";
|
|
9
|
+
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
10
|
+
import { SITE_BRAND } from "../lib/site-brand";
|
|
11
|
+
import { useOptionalNewsDeskClient } from "./news-desk-client-provider";
|
|
12
|
+
import { ReaderAuthControl } from "./reader-auth-control";
|
|
13
|
+
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetTrigger } from "./ui/sheet";
|
|
14
|
+
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuSeparator } from "./ui/dropdown-menu";
|
|
15
|
+
import type { NewsroomOpsShellProps } from "./newsroom-ops-shell";
|
|
16
|
+
import "./newsroom-organizer.css";
|
|
17
|
+
|
|
18
|
+
const ICONS = { overview: LayoutDashboard, board: ClipboardList, references: BookOpen, messages: MessagesSquare };
|
|
19
|
+
|
|
20
|
+
/** Navigation is a presentation layer; existing route and server authorization still own access. */
|
|
21
|
+
function Navigation({ activePath, demo, admin, surface, onNavigate }: { activePath: string; demo: boolean; admin: boolean; surface: string; onNavigate?: () => void }) {
|
|
22
|
+
const id = useId();
|
|
23
|
+
const all = ORGANIZER_NAV_GROUPS.flatMap(group => group.items);
|
|
24
|
+
const active = all.filter(item => activePath === item.path || (item.path !== "/newsroom" && activePath.startsWith(item.path + "/"))).sort((a, b) => b.path.length - a.path.length)[0]?.key;
|
|
25
|
+
return <nav className="organizer-destinations" aria-label={surface === "sidebar" ? "Newsroom" : "All workspaces"}>
|
|
26
|
+
{ORGANIZER_NAV_GROUPS.map((group, index) => <section key={group.title} aria-labelledby={`${id}-${index}`}>
|
|
27
|
+
<h3 id={`${id}-${index}`}>{group.title}</h3>
|
|
28
|
+
{group.items.filter(item => item.access !== "admin" || admin).map(item => <Link key={item.key}
|
|
29
|
+
href={getNewsroomNavHref(item.path, demo)} onClick={onNavigate} aria-label={item.label}
|
|
30
|
+
aria-current={active === item.key ? "page" : undefined} aria-describedby={`${id}-${item.key}`}
|
|
31
|
+
data-news-desk-tab={item.key}>
|
|
32
|
+
<strong>{item.label}</strong><small id={`${id}-${item.key}`}>{item.description}</small>
|
|
33
|
+
</Link>)}
|
|
34
|
+
</section>)}
|
|
35
|
+
</nav>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function Account({ placement }: { placement: "sidebar" | "header" }) {
|
|
39
|
+
const session = useOptionalNewsDeskClient();
|
|
40
|
+
const auth = session?.shell.auth ?? { status: "loading" as const, label: "Checking sign-in" };
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const media = matchMedia("(max-width: 900px)");
|
|
44
|
+
const close = () => setOpen(false);
|
|
45
|
+
media.addEventListener("change", close);
|
|
46
|
+
return () => media.removeEventListener("change", close);
|
|
47
|
+
}, []);
|
|
48
|
+
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>
|
|
51
|
+
</DropdownMenuTrigger>
|
|
52
|
+
<DropdownMenuContent className="organizer-account-popup" side={placement === "sidebar" ? "top" : "bottom"} align="end">
|
|
53
|
+
<DropdownMenuGroup>
|
|
54
|
+
<DropdownMenuLabel>{auth.label}</DropdownMenuLabel><DropdownMenuSeparator />
|
|
55
|
+
<DropdownMenuItem render={<Link href={getNewsroomNavHref(ORGANIZER_SETTINGS_DESTINATION.path)} />}><Settings aria-hidden="true" />{ORGANIZER_SETTINGS_DESTINATION.label}</DropdownMenuItem>
|
|
56
|
+
<ReaderAuthControl authState={auth} renderAction={({ disabled, onClick, signedIn }) => <DropdownMenuItem disabled={disabled} closeOnClick={false} onClick={onClick}>
|
|
57
|
+
{signedIn ? <LogOut aria-hidden="true" /> : <LogIn aria-hidden="true" />}{signedIn ? "Sign out" : "Sign in"}
|
|
58
|
+
</DropdownMenuItem>} />
|
|
59
|
+
</DropdownMenuGroup>
|
|
60
|
+
</DropdownMenuContent>
|
|
61
|
+
</DropdownMenu>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function NewsroomOrganizerShell({ activeTab, appTitle, backHref = "/", backLabel, children, demo = false, headerActions, pageTitle, showNavigation = true }: NewsroomOpsShellProps) {
|
|
65
|
+
const pathname = usePathname();
|
|
66
|
+
const session = useOptionalNewsDeskClient();
|
|
67
|
+
const admin = demo || session?.shell.dashboard?.canManageUsers === true;
|
|
68
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
69
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
70
|
+
const [loadedPreference, setLoadedPreference] = useState(false);
|
|
71
|
+
const preferenceKey = `papyrus.cms.sidebar.v1.${SITE_BRAND.id}`;
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
try { setCollapsed(localStorage.getItem(preferenceKey) === "true"); } catch { /* Optional browser preference. */ }
|
|
74
|
+
setLoadedPreference(true);
|
|
75
|
+
}, [preferenceKey]);
|
|
76
|
+
useEffect(() => { if (loadedPreference) try { localStorage.setItem(preferenceKey, String(collapsed)); } catch { /* Controls still work without storage. */ } }, [collapsed, loadedPreference, preferenceKey]);
|
|
77
|
+
useEffect(() => { setMenuOpen(false); }, [pathname]);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const media = matchMedia("(max-width: 900px)");
|
|
80
|
+
let previousFocus: Element | null = null;
|
|
81
|
+
let frame = 0;
|
|
82
|
+
const remember = () => { if (document.activeElement !== document.body) previousFocus = document.activeElement; };
|
|
83
|
+
const change = () => {
|
|
84
|
+
const active = document.activeElement === document.body ? previousFocus : document.activeElement;
|
|
85
|
+
if (!(active instanceof HTMLElement)) return;
|
|
86
|
+
if (!media.matches) setMenuOpen(false);
|
|
87
|
+
const selector = active.closest(".organizer-account, .organizer-account-popup") ? ".organizer-account"
|
|
88
|
+
: media.matches && active.closest(".organizer-sidebar, .organizer-toggle") ? ".organizer-more"
|
|
89
|
+
: !media.matches && active.closest(".organizer-bottom-nav, .organizer-menu") ? ".organizer-toggle" : null;
|
|
90
|
+
if (!selector) return;
|
|
91
|
+
cancelAnimationFrame(frame);
|
|
92
|
+
frame = requestAnimationFrame(() => {
|
|
93
|
+
if (document.activeElement === active || document.activeElement === document.body || !(document.activeElement as HTMLElement)?.getClientRects().length)
|
|
94
|
+
Array.from(document.querySelectorAll<HTMLElement>(selector)).find(element => element.getClientRects().length)?.focus();
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
document.addEventListener("focusin", remember);
|
|
98
|
+
media.addEventListener("change", change);
|
|
99
|
+
return () => { document.removeEventListener("focusin", remember); media.removeEventListener("change", change); cancelAnimationFrame(frame); };
|
|
100
|
+
}, []);
|
|
101
|
+
const activePath = browserPathToInternalNewsroomPath(pathname ?? "/newsroom");
|
|
102
|
+
return <div className="newsroom-organizer" data-news-desk data-newsroom-ops-shell="true" data-newsroom-chrome="organizer" data-sidebar-collapsed={collapsed || !showNavigation ? "true" : "false"}>
|
|
103
|
+
{showNavigation && <aside className="organizer-sidebar" hidden={collapsed} id="organizer-sidebar">
|
|
104
|
+
<Link className="organizer-brand" href={getNewsroomNavHref("/newsroom", demo)}>{appTitle}<small>The newsroom</small></Link>
|
|
105
|
+
<Navigation activePath={activePath} demo={demo} admin={admin} surface="sidebar" />
|
|
106
|
+
<Account placement="sidebar" />
|
|
107
|
+
</aside>}
|
|
108
|
+
<div className="organizer-workspace">
|
|
109
|
+
<header className="organizer-topbar" data-newsroom-ops-topbar>
|
|
110
|
+
{showNavigation && <button type="button" className="organizer-toggle" title={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
111
|
+
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"} aria-expanded={!collapsed} aria-controls="organizer-sidebar" onClick={() => setCollapsed(value => !value)}>
|
|
112
|
+
{collapsed ? <PanelLeftOpen size={16} aria-hidden="true" /> : <PanelLeftClose size={16} aria-hidden="true" />}<span>{collapsed ? "Expand sidebar" : "Collapse sidebar"}</span>
|
|
113
|
+
</button>}
|
|
114
|
+
<Link className="organizer-masthead" href={backHref} title={backLabel}>{appTitle}</Link>
|
|
115
|
+
<h1 data-newsroom-ops-title>{pageTitle}</h1><div className="organizer-header-actions">{headerActions}</div><Account placement="header" />
|
|
116
|
+
</header>
|
|
117
|
+
<main className="organizer-main">{demo && <p className="organizer-demo-notice">Demo workspace · synthetic examples</p>}{children}</main>
|
|
118
|
+
</div>
|
|
119
|
+
{showNavigation && <nav className="organizer-bottom-nav" aria-label="Primary newsroom navigation"><ul>
|
|
120
|
+
{ORGANIZER_PRIMARY_NAV.map(item => { const Icon = ICONS[item.key as keyof typeof ICONS]; return <li key={item.key}><Link
|
|
121
|
+
href={getNewsroomNavHref(item.path, demo)} title={item.description} aria-label={item.label} aria-current={activeTab === item.key || activeTab === "work" && item.key === "board" ? "page" : undefined}>
|
|
122
|
+
<Icon size={18} aria-hidden="true" /><span>{item.label}</span></Link></li>; })}
|
|
123
|
+
<li><Sheet open={menuOpen} onOpenChange={setMenuOpen}><SheetTrigger className="organizer-more" aria-label="More"><Menu size={18} aria-hidden="true" /><span>More</span></SheetTrigger>
|
|
124
|
+
<SheetContent side="bottom" className="organizer-menu"><SheetHeader><SheetTitle>Newsroom</SheetTitle><SheetDescription>Workspaces and administration</SheetDescription></SheetHeader>
|
|
125
|
+
<Navigation activePath={activePath} demo={demo} admin={admin} surface="menu" onNavigate={() => setMenuOpen(false)} />
|
|
126
|
+
</SheetContent>
|
|
127
|
+
</Sheet></li>
|
|
128
|
+
</ul></nav>}
|
|
129
|
+
</div>;
|
|
130
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
@font-face{font-family:Newsroom Display;src:url('./newsroom-fonts/playfair-display-700.ttf') format('truetype');font-weight:700;font-display:swap}
|
|
2
|
+
@font-face{font-family:Newsroom Display;src:url('./newsroom-fonts/playfair-display-900.ttf') format('truetype');font-weight:900;font-display:swap}
|
|
3
|
+
@font-face{font-family:Newsroom UI;src:url('./newsroom-fonts/plus-jakarta-sans-400.ttf') format('truetype');font-weight:400;font-display:swap}
|
|
4
|
+
@font-face{font-family:Newsroom UI;src:url('./newsroom-fonts/plus-jakarta-sans-600.ttf') format('truetype');font-weight:600;font-display:swap}
|
|
5
|
+
.newsroom-organizer,.organizer-menu,.organizer-account-popup{--org-paper:var(--background);--org-ink:var(--foreground);--org-line:var(--border);--org-muted:var(--muted-foreground);font-family:Newsroom UI,system-ui,sans-serif;color:var(--org-ink);background:var(--org-paper)}
|
|
6
|
+
.newsroom-organizer{display:grid;grid-template-columns:264px minmax(0,1fr);height:100dvh;max-height:100dvh;overflow:clip;font-size:14px}
|
|
7
|
+
.newsroom-organizer[data-sidebar-collapsed=true]{grid-template-columns:minmax(0,1fr)}
|
|
8
|
+
body:has(.newsroom-organizer){display:flex;flex-direction:column;height:100dvh;overflow:hidden}
|
|
9
|
+
body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
10
|
+
.papyrus-console-page:has(.newsroom-organizer){display:flex;flex:1;min-height:0;min-width:0;overflow:hidden}
|
|
11
|
+
.papyrus-console-page .newsroom-organizer{height:100%;max-height:100%;flex:1}
|
|
12
|
+
.newsroom-organizer h1,.newsroom-organizer h2,.newsroom-organizer h3{font-family:Newsroom Display,Georgia,serif}
|
|
13
|
+
.organizer-sidebar{min-height:0;display:flex;flex-direction:column;border-right:1px solid var(--org-line);padding:16px;gap:16px;overflow:hidden}
|
|
14
|
+
.organizer-sidebar[hidden]{display:none}.organizer-brand{font:900 40px/1.1 Newsroom Display,Georgia,serif;padding-bottom:20px;border-bottom:3px double var(--org-ink)}
|
|
15
|
+
.organizer-brand small{display:block;font:400 10px Newsroom UI,system-ui;letter-spacing:.3em;text-transform:uppercase;margin-top:12px}
|
|
16
|
+
.organizer-destinations{min-height:0;overflow:auto;overscroll-behavior:contain;flex:1;padding-right:1px}
|
|
17
|
+
.organizer-destinations section+section{margin-top:24px}.organizer-destinations h3{font:600 10px Newsroom UI,system-ui;letter-spacing:.07em;text-transform:uppercase;color:var(--org-muted);margin:16px 0 8px}
|
|
18
|
+
.organizer-destinations a{display:flex;flex-direction:column;align-items:start;gap:6px;padding:14px 8px;border-bottom:1px solid var(--org-line);color:var(--org-muted);text-decoration:none;min-height:44px}
|
|
19
|
+
.organizer-destinations a strong{font:700 16px Newsroom Display,Georgia,serif}.organizer-destinations a small{font:400 11px/1.5 Newsroom UI,system-ui;white-space:normal;overflow-wrap:anywhere}
|
|
20
|
+
.organizer-destinations a[aria-current=page]{border-bottom:2px solid var(--org-ink);color:var(--org-ink)}
|
|
21
|
+
.newsroom-organizer :is(a,button):focus-visible,.organizer-menu :is(a,button):focus-visible{outline:2px solid currentColor;outline-offset:-3px}
|
|
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)}
|
|
24
|
+
.organizer-masthead{font:900 28px Newsroom Display,Georgia,serif;white-space:nowrap;max-width:30vw;overflow:hidden;text-overflow:ellipsis}
|
|
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
|
+
.organizer-toggle{display:flex;align-items:center;gap:8px;min-height:36px;padding:8px 12px;border:1px solid var(--org-line);font:400 11px Newsroom UI,system-ui;flex-shrink:0}
|
|
27
|
+
.organizer-header-actions{display:flex;align-items:center;gap:8px;min-width:0;max-width:45%;overflow-x:auto}
|
|
28
|
+
.organizer-main{min-width:0;min-height:0;overflow:auto;overscroll-behavior:contain;flex:1;padding:16px 0}
|
|
29
|
+
.organizer-main>[data-category-steering]{min-height:0}.organizer-demo-notice{font-size:11px;color:var(--org-muted);margin-bottom:12px}
|
|
30
|
+
.organizer-account{display:flex;align-items:center;gap:10px;min-height:44px;flex-shrink:0;border:1px solid var(--org-line);padding:8px;text-align:left;border-radius:3px}
|
|
31
|
+
.organizer-account span{min-width:0}.organizer-account strong{display:block;font:600 11px Newsroom UI,system-ui;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.organizer-account small{display:block;font-size:9px;color:var(--org-muted);margin-top:2px}
|
|
32
|
+
.organizer-account-header{display:none;padding:8px;min-width:36px;min-height:36px}.organizer-account-header span{display:none}
|
|
33
|
+
.newsroom-organizer[data-sidebar-collapsed=true] .organizer-account-header{display:flex}
|
|
34
|
+
.organizer-account-popup{min-width:250px;max-width:calc(100vw - 32px);border:1px solid var(--org-line);padding:6px;border-radius:4px}
|
|
35
|
+
.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}
|
|
36
|
+
.organizer-bottom-nav{display:none}
|
|
37
|
+
.organizer-main:has(.organizer-native-board){display:flex;overflow:hidden}
|
|
38
|
+
.organizer-native-board{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;gap:12px}
|
|
39
|
+
.organizer-board-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;min-width:0;flex-wrap:wrap}
|
|
40
|
+
.organizer-board-toolbar [role=group],.organizer-board-pages{display:flex;gap:8px;flex-wrap:wrap}
|
|
41
|
+
.organizer-board-toolbar button,.organizer-board-pages button{min-height:44px;padding:8px 12px;border:1px solid var(--org-line)}
|
|
42
|
+
.organizer-board-toolbar 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)}
|
|
44
|
+
.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>.kb-issue-collection{height:100%;min-height:0;overflow:auto}
|
|
47
|
+
.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-menu{height:min(85dvh,760px);max-height:85dvh;display:flex;flex-direction:column;padding:16px;border-radius:24px 24px 0 0;gap:20px}
|
|
50
|
+
.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
|
+
.organizer-menu .organizer-destinations a{border:1px solid var(--org-line);padding:12px;color:var(--org-ink)}
|
|
52
|
+
@media(max-width:900px){
|
|
53
|
+
.newsroom-organizer,.newsroom-organizer[data-sidebar-collapsed=true]{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr) auto}
|
|
54
|
+
.organizer-sidebar,.organizer-toggle{display:none}.organizer-workspace{padding:0 16px}.organizer-topbar{gap:8px;height:55px;min-height:55px}
|
|
55
|
+
.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%}
|
|
56
|
+
.organizer-bottom-nav{display:block;border-top:1px solid var(--org-ink);background:var(--org-paper);padding:4px 8px max(4px,env(safe-area-inset-bottom))}
|
|
57
|
+
.organizer-bottom-nav ul{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:4px;list-style:none;margin:0;padding:0}
|
|
58
|
+
.organizer-bottom-nav :is(a,button){display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;min-height:54px;width:100%;font:400 10px Newsroom UI,system-ui;padding:6px 0;text-decoration:none;border-radius:8px}
|
|
59
|
+
.organizer-bottom-nav a[aria-current=page]{background:var(--muted);font-weight:600}
|
|
60
|
+
.organizer-board-body[data-detail-open=true]{grid-template-columns:minmax(0,1fr)}
|
|
61
|
+
.organizer-board-body>.kb-issue-detail{position:absolute;inset:0;z-index:2;overflow:auto}
|
|
62
|
+
}
|
|
63
|
+
@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}}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import type { PrimingPanelProps } from "cyclotron/components/priming-panel";
|
|
4
|
+
import { ReferencePriming } from "./reference-priming";
|
|
5
|
+
import { ReferenceCurationControls } from "./reference-curation-controls";
|
|
6
|
+
|
|
3
7
|
import {
|
|
4
8
|
ArchiveIcon,
|
|
5
9
|
ChevronLeftIcon,
|
|
@@ -44,21 +48,38 @@ import {
|
|
|
44
48
|
import { normalizeReferenceCurationStatus } from "../lib/reference-policy";
|
|
45
49
|
import { newsroomListRowClassName } from "../lib/newsroom-list-selection";
|
|
46
50
|
import { ReferenceSourcePreview } from "./reference-source-preview";
|
|
51
|
+
import { ReferenceRelevanceReview } from "./reference-relevance-review";
|
|
52
|
+
import { ReferenceCyclotronStatus, type ReviewRateChoice } from "./reference-cyclotron-status";
|
|
53
|
+
import type { CyclotronStatus } from "cyclotron/cyclotron-status";
|
|
54
|
+
import type { RelevanceCuration, RelevanceDecisionView } from "../lib/relevance-decisions";
|
|
47
55
|
import {
|
|
48
56
|
loadReferenceAttachmentsForLineageId,
|
|
49
57
|
loadStoragePathUrl,
|
|
50
58
|
} from "./news-desk-taxonomy-client";
|
|
51
59
|
|
|
52
60
|
type NewsroomReferencesViewProps = {
|
|
61
|
+
/** Present only when the host has configured optional example priming. */
|
|
62
|
+
priming?: PrimingPanelProps;
|
|
63
|
+
/** The relevance cyclotron's status snapshot, shown above the list. */
|
|
64
|
+
cyclotronStatus?: CyclotronStatus | null;
|
|
65
|
+
/** Present for editors who may ask for a manual review rate. */
|
|
66
|
+
onRequestReviewRate?: (choice: ReviewRateChoice) => Promise<void> | void;
|
|
53
67
|
demo?: boolean;
|
|
68
|
+
/** The relevance cyclotron's current decisions, keyed by reference lineage. */
|
|
69
|
+
decisions?: Map<string, RelevanceDecisionView>;
|
|
54
70
|
disabled?: boolean;
|
|
71
|
+
onRelevanceReview?: (reference: ReferenceRecord, curation: RelevanceCuration) => void;
|
|
55
72
|
initialReferenceLineageId?: string | null;
|
|
56
73
|
onReview: (reference: ReferenceRecord, action: ReferenceCurationAction) => void;
|
|
57
74
|
referenceAttachments?: ReferenceAttachmentRecord[];
|
|
58
75
|
references: ReferenceRecord[];
|
|
59
76
|
};
|
|
60
77
|
|
|
61
|
-
|
|
78
|
+
type ReferenceListFilter = ReferenceStatusFilter | "needs_review";
|
|
79
|
+
|
|
80
|
+
const NEEDS_REVIEW_FILTER: { key: ReferenceListFilter; label: string } = { key: "needs_review", label: "Needs review" };
|
|
81
|
+
|
|
82
|
+
const STATUS_FILTERS: Array<{ key: ReferenceListFilter; label: string }> = [
|
|
62
83
|
{ key: "all", label: "All" },
|
|
63
84
|
{ key: "pending", label: "Pending" },
|
|
64
85
|
{ key: "accepted", label: "Accepted" },
|
|
@@ -183,16 +204,30 @@ function useReferencePreviewAttachments(
|
|
|
183
204
|
return previewAttachments;
|
|
184
205
|
}
|
|
185
206
|
|
|
207
|
+
function decisionSummary(decision: RelevanceDecisionView): string {
|
|
208
|
+
const sure = decision.confidence == null ? "confidence unknown" : `${Math.round(decision.confidence * 100)}% sure`;
|
|
209
|
+
return `${decision.label} · ${sure}${decision.version != null ? ` · version ${decision.version}` : ""}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function needsReview(reference: ReferenceRecord, decision: RelevanceDecisionView | undefined): boolean {
|
|
213
|
+
return Boolean(decision?.reviewRecommended)
|
|
214
|
+
&& normalizeReferenceCurationStatus(reference.curationStatus) === "pending";
|
|
215
|
+
}
|
|
216
|
+
|
|
186
217
|
function ReferenceDetailPanel({
|
|
218
|
+
decision,
|
|
187
219
|
demo,
|
|
188
220
|
disabled,
|
|
221
|
+
onRelevanceReview,
|
|
189
222
|
onReview,
|
|
190
223
|
previewAttachments,
|
|
191
224
|
reference,
|
|
192
225
|
onClose,
|
|
193
226
|
}: {
|
|
227
|
+
decision?: RelevanceDecisionView;
|
|
194
228
|
demo?: boolean;
|
|
195
229
|
disabled?: boolean;
|
|
230
|
+
onRelevanceReview?: (curation: RelevanceCuration) => void;
|
|
196
231
|
onReview: (action: ReferenceCurationAction) => void;
|
|
197
232
|
previewAttachments: ReferenceAttachmentRecord[];
|
|
198
233
|
reference: ReferenceRecord;
|
|
@@ -224,11 +259,25 @@ function ReferenceDetailPanel({
|
|
|
224
259
|
) : null}
|
|
225
260
|
</div>
|
|
226
261
|
|
|
262
|
+
{decision && status === "pending" && onRelevanceReview ? (
|
|
263
|
+
<ReferenceRelevanceReview
|
|
264
|
+
decision={decision}
|
|
265
|
+
disabled={disabled}
|
|
266
|
+
onCurate={onRelevanceReview}
|
|
267
|
+
referenceLineageId={lineageId}
|
|
268
|
+
/>
|
|
269
|
+
) : decision ? (
|
|
270
|
+
<p className="text-sm text-muted-foreground" data-reference-relevance-decision={lineageId}>
|
|
271
|
+
Cyclotron decision: {decisionSummary(decision)}
|
|
272
|
+
</p>
|
|
273
|
+
) : null}
|
|
274
|
+
|
|
227
275
|
<div
|
|
228
276
|
className="flex flex-wrap items-center gap-2"
|
|
229
277
|
data-news-desk-reference-curation-cluster
|
|
230
278
|
data-reference-curation-status={status}
|
|
231
279
|
>
|
|
280
|
+
{decision && status === "pending" && onRelevanceReview ? null : <>
|
|
232
281
|
<Button
|
|
233
282
|
aria-label="Accept reference"
|
|
234
283
|
aria-pressed={status === "accepted"}
|
|
@@ -255,6 +304,7 @@ function ReferenceDetailPanel({
|
|
|
255
304
|
<ThumbsDownIcon className="size-4" />
|
|
256
305
|
Reject
|
|
257
306
|
</Button>
|
|
307
|
+
</>}
|
|
258
308
|
<Button
|
|
259
309
|
aria-label="Archive reference"
|
|
260
310
|
disabled={archiveDisabled}
|
|
@@ -347,8 +397,13 @@ function ReferenceDetailPanel({
|
|
|
347
397
|
}
|
|
348
398
|
|
|
349
399
|
export function NewsroomReferencesView({
|
|
400
|
+
priming,
|
|
401
|
+
cyclotronStatus,
|
|
402
|
+
decisions,
|
|
403
|
+
onRequestReviewRate,
|
|
350
404
|
demo = false,
|
|
351
405
|
disabled = false,
|
|
406
|
+
onRelevanceReview,
|
|
352
407
|
initialReferenceLineageId = null,
|
|
353
408
|
onReview,
|
|
354
409
|
referenceAttachments = [],
|
|
@@ -365,13 +420,20 @@ export function NewsroomReferencesView({
|
|
|
365
420
|
const canonicalReferences = useMemo(() => selectCanonicalReferenceRecords(references), [references]);
|
|
366
421
|
const statusCounts = useMemo(() => countReferencesByStatus(canonicalReferences), [canonicalReferences]);
|
|
367
422
|
|
|
368
|
-
const [statusFilter, setStatusFilter] = useState<
|
|
423
|
+
const [statusFilter, setStatusFilter] = useState<ReferenceListFilter>("all");
|
|
424
|
+
const needsReviewCount = useMemo(
|
|
425
|
+
() => canonicalReferences.filter((reference) => needsReview(reference, decisions?.get(referenceLineageId(reference)))).length,
|
|
426
|
+
[canonicalReferences, decisions],
|
|
427
|
+
);
|
|
428
|
+
const filters = decisions?.size ? [NEEDS_REVIEW_FILTER, ...STATUS_FILTERS] : STATUS_FILTERS;
|
|
369
429
|
const [selectedLineageId, setSelectedLineageId] = useState(routeLineageId);
|
|
370
430
|
const [mobileDetailOpen, setMobileDetailOpen] = useState(Boolean(routeLineageId) && isMobileDetail);
|
|
371
431
|
|
|
372
432
|
const filteredReferences = useMemo(
|
|
373
|
-
() =>
|
|
374
|
-
|
|
433
|
+
() => statusFilter === "needs_review"
|
|
434
|
+
? canonicalReferences.filter((reference) => needsReview(reference, decisions?.get(referenceLineageId(reference))))
|
|
435
|
+
: filterReferencesByStatus(canonicalReferences, statusFilter),
|
|
436
|
+
[canonicalReferences, decisions, statusFilter],
|
|
375
437
|
);
|
|
376
438
|
|
|
377
439
|
const selectedReference = useMemo(
|
|
@@ -390,7 +452,7 @@ export function NewsroomReferencesView({
|
|
|
390
452
|
? filteredReferences[selectedIndex + 1]
|
|
391
453
|
: null;
|
|
392
454
|
|
|
393
|
-
const syncStatusUrl = useCallback((nextStatus:
|
|
455
|
+
const syncStatusUrl = useCallback((nextStatus: ReferenceListFilter) => {
|
|
394
456
|
if (demo) return;
|
|
395
457
|
syncBrowserNewsroomIndexUrl("references", {
|
|
396
458
|
status: nextStatus === "all" ? "" : nextStatus,
|
|
@@ -452,22 +514,31 @@ export function NewsroomReferencesView({
|
|
|
452
514
|
Review source intake before it becomes accepted evidence.
|
|
453
515
|
</p>
|
|
454
516
|
</div>
|
|
517
|
+
<ReferenceCurationControls disabled={disabled} />
|
|
518
|
+
{priming && !disabled ? <ReferencePriming {...priming} /> : null}
|
|
519
|
+
{cyclotronStatus ? (
|
|
520
|
+
<div className="shrink-0">
|
|
521
|
+
<ReferenceCyclotronStatus onRequestRate={disabled ? undefined : onRequestReviewRate} status={cyclotronStatus} />
|
|
522
|
+
</div>
|
|
523
|
+
) : null}
|
|
455
524
|
|
|
456
525
|
<Tabs
|
|
457
526
|
className="shrink-0"
|
|
458
527
|
defaultValue="all"
|
|
459
528
|
onValueChange={(value) => {
|
|
460
|
-
const next = value as
|
|
529
|
+
const next = value as ReferenceListFilter;
|
|
461
530
|
setStatusFilter(next);
|
|
462
531
|
syncStatusUrl(next);
|
|
463
532
|
}}
|
|
464
533
|
value={statusFilter}
|
|
465
534
|
>
|
|
466
535
|
<TabsList className="h-auto w-full flex-wrap justify-start gap-1 bg-muted/40 p-1">
|
|
467
|
-
{
|
|
536
|
+
{filters.map((filter) => {
|
|
468
537
|
const count = filter.key === "all"
|
|
469
538
|
? canonicalReferences.length
|
|
470
|
-
:
|
|
539
|
+
: filter.key === "needs_review"
|
|
540
|
+
? needsReviewCount
|
|
541
|
+
: statusCounts[filter.key] ?? 0;
|
|
471
542
|
return (
|
|
472
543
|
<TabsTrigger className="text-xs sm:text-sm" key={filter.key} value={filter.key}>
|
|
473
544
|
{filter.label}
|
|
@@ -484,6 +555,7 @@ export function NewsroomReferencesView({
|
|
|
484
555
|
const lineageId = referenceLineageId(reference);
|
|
485
556
|
const status = normalizeReferenceCurationStatus(reference.curationStatus);
|
|
486
557
|
const active = selectedLineageId === lineageId;
|
|
558
|
+
const decision = decisions?.get(lineageId);
|
|
487
559
|
return (
|
|
488
560
|
<button
|
|
489
561
|
aria-current={active ? "true" : undefined}
|
|
@@ -504,6 +576,14 @@ export function NewsroomReferencesView({
|
|
|
504
576
|
<Badge variant={referenceStatusBadgeVariant(status)}>{referenceStatusLabel(status)}</Badge>
|
|
505
577
|
</div>
|
|
506
578
|
<p className="mt-2 text-xs text-muted-foreground">{formatReferenceListDate(reference)}</p>
|
|
579
|
+
{decision ? (
|
|
580
|
+
<p className="mt-1 flex flex-wrap items-center gap-2 text-xs text-muted-foreground" data-reference-relevance-row={lineageId}>
|
|
581
|
+
<span>Decision: {decisionSummary(decision)}</span>
|
|
582
|
+
{needsReview(reference, decision) ? (
|
|
583
|
+
<Badge variant="outline"><span aria-hidden="true">●</span> Needs review</Badge>
|
|
584
|
+
) : null}
|
|
585
|
+
</p>
|
|
586
|
+
) : null}
|
|
507
587
|
</button>
|
|
508
588
|
);
|
|
509
589
|
}) : (
|
|
@@ -552,8 +632,10 @@ export function NewsroomReferencesView({
|
|
|
552
632
|
<ScrollArea className="min-h-0 flex-1">
|
|
553
633
|
{selectedReference ? (
|
|
554
634
|
<ReferenceDetailPanel
|
|
635
|
+
decision={decisions?.get(referenceLineageId(selectedReference))}
|
|
555
636
|
demo={demo}
|
|
556
637
|
disabled={disabled}
|
|
638
|
+
onRelevanceReview={onRelevanceReview ? (curation) => onRelevanceReview(selectedReference, curation) : undefined}
|
|
557
639
|
onReview={runReview}
|
|
558
640
|
previewAttachments={previewAttachments}
|
|
559
641
|
reference={selectedReference}
|
|
@@ -581,9 +663,11 @@ export function NewsroomReferencesView({
|
|
|
581
663
|
{selectedReference ? (
|
|
582
664
|
<ScrollArea className="max-h-[75dvh]">
|
|
583
665
|
<ReferenceDetailPanel
|
|
666
|
+
decision={decisions?.get(referenceLineageId(selectedReference))}
|
|
584
667
|
demo={demo}
|
|
585
668
|
disabled={disabled}
|
|
586
669
|
onClose={closeDetail}
|
|
670
|
+
onRelevanceReview={onRelevanceReview ? (curation) => onRelevanceReview(selectedReference, curation) : undefined}
|
|
587
671
|
onReview={runReview}
|
|
588
672
|
previewAttachments={previewAttachments}
|
|
589
673
|
reference={selectedReference}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { browserPathToInternalNewsroomPath } from "../lib/newsroom-base-path";
|
|
3
4
|
import type { BotAvatarState } from "anthus-vultus";
|
|
4
5
|
import dynamic from "next/dynamic";
|
|
5
6
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
@@ -152,7 +153,8 @@ export function PapyrusConsoleShell({ children }: PapyrusConsoleShellProps) {
|
|
|
152
153
|
const [session, setSession] = useState<ReaderSessionSnapshot | null>(null);
|
|
153
154
|
const [open, setOpen] = useState(false);
|
|
154
155
|
const [openingReferenceChat, setOpeningReferenceChat] = useState(false);
|
|
155
|
-
const
|
|
156
|
+
const internalPathname = browserPathToInternalNewsroomPath(pathname);
|
|
157
|
+
const isNewsroomPath = internalPathname === "/newsroom" || internalPathname.startsWith("/newsroom/");
|
|
156
158
|
const canUseConsole = Boolean(session?.hasSession && session.groups.some((group) => group === "editor" || group === "admin"));
|
|
157
159
|
const shouldOfferConsole = isNewsroomPath && canUseConsole;
|
|
158
160
|
|
|
@@ -433,7 +435,7 @@ function ConsolePanel({ actorEmail, actorLabel, onClose }: { actorEmail: string
|
|
|
433
435
|
const handledNavigationMessageIdsRef = useRef<Set<string>>(new Set());
|
|
434
436
|
|
|
435
437
|
const currentWebPath = useMemo(() => {
|
|
436
|
-
const path = pathname ?? "/newsroom";
|
|
438
|
+
const path = browserPathToInternalNewsroomPath(pathname ?? "/newsroom");
|
|
437
439
|
const query = searchParams.toString();
|
|
438
440
|
return query ? `${path}?${query}` : path;
|
|
439
441
|
}, [pathname, searchParams]);
|
|
@@ -12,6 +12,9 @@ export type PictogramFigureProps = {
|
|
|
12
12
|
credit: string;
|
|
13
13
|
figureClassName: string;
|
|
14
14
|
height: number;
|
|
15
|
+
/** Rhythm blog layout: solved frame size in CSS pixels; wins over `height`/`width` when set. */
|
|
16
|
+
frameHeight?: number;
|
|
17
|
+
frameWidth?: number;
|
|
15
18
|
layout?: ArticleImageLayout;
|
|
16
19
|
priority?: boolean;
|
|
17
20
|
sizes: string;
|
|
@@ -34,6 +37,8 @@ export function PictogramFigure({
|
|
|
34
37
|
caption,
|
|
35
38
|
credit,
|
|
36
39
|
figureClassName,
|
|
40
|
+
frameHeight,
|
|
41
|
+
frameWidth,
|
|
37
42
|
height,
|
|
38
43
|
priority = false,
|
|
39
44
|
sizes,
|
|
@@ -41,6 +46,8 @@ export function PictogramFigure({
|
|
|
41
46
|
themeVariants,
|
|
42
47
|
width,
|
|
43
48
|
}: PictogramFigureProps) {
|
|
49
|
+
const imageHeight = frameHeight ?? height;
|
|
50
|
+
const imageWidth = frameWidth ?? width;
|
|
44
51
|
const resolvedTheme = useResolvedPapyrusTheme();
|
|
45
52
|
const themedImageSrc = resolveThemedImageSrc(src, themeVariants, resolvedTheme);
|
|
46
53
|
|
|
@@ -53,8 +60,8 @@ export function PictogramFigure({
|
|
|
53
60
|
<Image
|
|
54
61
|
src={themedImageSrc}
|
|
55
62
|
alt={alt}
|
|
56
|
-
width={
|
|
57
|
-
height={
|
|
63
|
+
width={imageWidth}
|
|
64
|
+
height={imageHeight}
|
|
58
65
|
sizes={sizes}
|
|
59
66
|
priority={priority}
|
|
60
67
|
unoptimized={shouldBypassImageOptimization(themedImageSrc)}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { getEditionSectionPath } from "../lib/edition-routes";
|
|
5
|
+
import { SITE_BRAND } from "../lib/site-brand";
|
|
6
|
+
|
|
7
|
+
export type PresentationHeaderSection = {
|
|
8
|
+
key: string;
|
|
9
|
+
label: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type PresentationHeaderProps = {
|
|
13
|
+
editionDate?: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
sections?: PresentationHeaderSection[];
|
|
17
|
+
editionBasePath?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function PresentationHeader({
|
|
21
|
+
editionDate,
|
|
22
|
+
title: contentTitle,
|
|
23
|
+
description,
|
|
24
|
+
sections,
|
|
25
|
+
editionBasePath,
|
|
26
|
+
}: PresentationHeaderProps) {
|
|
27
|
+
const title = SITE_BRAND.mastheadSource === "brand" ? SITE_BRAND.mastheadTitle : contentTitle;
|
|
28
|
+
const subtitle = SITE_BRAND.mastheadSource === "brand" ? SITE_BRAND.mastheadSubtitle : description;
|
|
29
|
+
const eyebrow = SITE_BRAND.mastheadEyebrow ?? null;
|
|
30
|
+
const tagline = SITE_BRAND.mastheadTagline ?? null;
|
|
31
|
+
const taglineLines = SITE_BRAND.mastheadTaglineLines ?? null;
|
|
32
|
+
const displayDate = editionDate
|
|
33
|
+
? SITE_BRAND.mastheadDateFormat === "formatted"
|
|
34
|
+
? formatMastheadDate(editionDate)
|
|
35
|
+
: editionDate
|
|
36
|
+
: null;
|
|
37
|
+
|
|
38
|
+
const eyebrowParts = eyebrow ? splitEyebrow(eyebrow) : null;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<header className="presentation-header">
|
|
43
|
+
<div className="presentation-header__copy-stack">
|
|
44
|
+
{eyebrowParts ? (
|
|
45
|
+
<span className="presentation-header__eyebrow">
|
|
46
|
+
<span className="presentation-header__eyebrow-strong">{eyebrowParts.lead}</span>
|
|
47
|
+
{eyebrowParts.rest ? (
|
|
48
|
+
<span className="presentation-header__eyebrow-muted">{eyebrowParts.rest}</span>
|
|
49
|
+
) : null}
|
|
50
|
+
</span>
|
|
51
|
+
) : null}
|
|
52
|
+
<h1>
|
|
53
|
+
{SITE_BRAND.mastheadWordSplit && title
|
|
54
|
+
? title.split(/\s+/).map((word) => (
|
|
55
|
+
<span className="presentation-header__word" key={word}>
|
|
56
|
+
<span className="presentation-header__word-text">{word}</span>
|
|
57
|
+
</span>
|
|
58
|
+
))
|
|
59
|
+
: title}
|
|
60
|
+
</h1>
|
|
61
|
+
<div className="presentation-header__meta">
|
|
62
|
+
{subtitle && !eyebrow ? <span className="presentation-header__subtitle">{subtitle}</span> : null}
|
|
63
|
+
{displayDate ? <p className="presentation-header__date">{displayDate}</p> : null}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
{tagline ? (
|
|
67
|
+
<span className="presentation-header__tagline">
|
|
68
|
+
{taglineLines && taglineLines.length > 0
|
|
69
|
+
? taglineLines.map((line, index) => (
|
|
70
|
+
<span className="presentation-header__tagline-line" key={index}>
|
|
71
|
+
<span className="presentation-header__tagline-emphasis">{line.emphasis}</span>
|
|
72
|
+
<span className="presentation-header__tagline-tail">{line.tail}</span>
|
|
73
|
+
{index < taglineLines.length - 1 ? " " : null}
|
|
74
|
+
</span>
|
|
75
|
+
))
|
|
76
|
+
: tagline}
|
|
77
|
+
</span>
|
|
78
|
+
) : null}
|
|
79
|
+
</header>
|
|
80
|
+
{sections && sections.length > 0 ? (
|
|
81
|
+
<nav className="presentation-section-nav" aria-label="Edition sections">
|
|
82
|
+
{sections.map((section) => (
|
|
83
|
+
<Link
|
|
84
|
+
href={getSectionHref(section, { editionBasePath, editionDate })}
|
|
85
|
+
key={section.key}
|
|
86
|
+
>
|
|
87
|
+
{section.label}
|
|
88
|
+
</Link>
|
|
89
|
+
))}
|
|
90
|
+
</nav>
|
|
91
|
+
) : null}
|
|
92
|
+
</>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getSectionHref(
|
|
97
|
+
section: PresentationHeaderSection,
|
|
98
|
+
options: { editionBasePath?: string; editionDate?: string },
|
|
99
|
+
): string {
|
|
100
|
+
if (SITE_BRAND.sectionLinkStrategy === "anchor") {
|
|
101
|
+
const anchor = `#${getSectionAnchorId(section.key)}`;
|
|
102
|
+
return options.editionBasePath ? `${options.editionBasePath}${anchor}` : anchor;
|
|
103
|
+
}
|
|
104
|
+
if (options.editionBasePath) {
|
|
105
|
+
return `${options.editionBasePath}/section/${encodeURIComponent(section.key)}`;
|
|
106
|
+
}
|
|
107
|
+
if (options.editionDate) {
|
|
108
|
+
const datedPath = getEditionSectionPath(options.editionDate, section.key);
|
|
109
|
+
return datedPath.startsWith("//") ? `#${getSectionAnchorId(section.key)}` : datedPath;
|
|
110
|
+
}
|
|
111
|
+
return `#${getSectionAnchorId(section.key)}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function getSectionAnchorId(sectionKey: string): string {
|
|
115
|
+
return `section-${sectionKey}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function splitEyebrow(text: string): { lead: string; rest: string } {
|
|
119
|
+
const trimmed = text.trim();
|
|
120
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
121
|
+
if (firstSpace === -1) return { lead: trimmed, rest: "" };
|
|
122
|
+
return { lead: trimmed.slice(0, firstSpace), rest: trimmed.slice(firstSpace + 1) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function formatMastheadDate(value: string): string {
|
|
126
|
+
const normalized = value?.trim();
|
|
127
|
+
if (!normalized) return value;
|
|
128
|
+
const date = new Date(`${normalized}T00:00:00`);
|
|
129
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
130
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
131
|
+
month: "long",
|
|
132
|
+
day: "numeric",
|
|
133
|
+
year: "numeric",
|
|
134
|
+
}).format(date);
|
|
135
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type PresentationRhythmHRuleProps = {
|
|
2
|
+
/** Hide in wide 2-column secondary pairs (items 1+2 side-by-side). */
|
|
3
|
+
hideInSecondaryPair?: boolean;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function PresentationRhythmHRule({ hideInSecondaryPair = false }: PresentationRhythmHRuleProps) {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
aria-hidden="true"
|
|
10
|
+
className="presentation-rhythm-hrule"
|
|
11
|
+
data-hide-in-secondary-pair={hideInSecondaryPair ? "true" : undefined}
|
|
12
|
+
role="presentation"
|
|
13
|
+
>
|
|
14
|
+
<div className="presentation-rhythm-hrule__line" />
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|