@anthusai/papyrus 5.48.0 → 5.50.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/app/api/newsroom/source-frameable/route.ts +12 -0
- package/components/needs-me-bodies.tsx +36 -25
- package/components/needs-me-detail.tsx +2 -1
- package/components/needs-me-feed.tsx +30 -13
- package/components/needs-me-reference-detail.tsx +13 -15
- package/components/needs-me-shared.tsx +9 -0
- package/components/newsroom-access-gate.tsx +4 -13
- package/components/newsroom-ops-overview.tsx +36 -30
- package/components/newsroom-ops-shell.tsx +4 -4
- package/components/newsroom-organizer.css +23 -11
- package/components/newsroom-references-view.tsx +15 -17
- package/components/notification-center.tsx +63 -11
- package/components/reference-capture-panel.tsx +14 -6
- package/components/reference-review-card.tsx +32 -10
- package/components/reference-source-preview.tsx +27 -5
- package/components/review-rating-grid.tsx +20 -10
- package/components/ui/auto-grow-textarea.tsx +18 -0
- package/components/use-reference-ratings.ts +63 -9
- package/lib/review-client.ts +16 -0
- package/lib/review-drafts.ts +33 -8
- package/lib/review-requests.ts +25 -0
- package/lib/source-frameable.ts +69 -0
- package/package.json +4 -2
- package/routes.manifest.json +12 -1
- package/components/newsroom-overview-skeleton.tsx +0 -25
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { checkSourceFrameable } from "../../../../lib/source-frameable";
|
|
3
|
+
|
|
4
|
+
export const runtime = "nodejs";
|
|
5
|
+
export const dynamic = "force-dynamic";
|
|
6
|
+
|
|
7
|
+
/** GET ?url=<source page> -> { frameable }: whether the newsroom can show that page's live preview. Cached for a day. */
|
|
8
|
+
export async function GET(request: Request) {
|
|
9
|
+
const url = new URL(request.url).searchParams.get("url") ?? "";
|
|
10
|
+
const frameable = url ? await checkSourceFrameable(url) : false;
|
|
11
|
+
return NextResponse.json({ frameable }, { headers: { "cache-control": "public, max-age=86400" } });
|
|
12
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { AutoGrowTextarea } from "./ui/auto-grow-textarea";
|
|
3
4
|
import { CheckIcon, CircleCheckIcon, CircleXIcon, ExternalLinkIcon, FileSearchIcon, Loader2Icon, LockIcon, MailIcon } from "lucide-react";
|
|
4
5
|
import Link from "next/link";
|
|
5
6
|
import { useEffect, useState, type ReactNode } from "react";
|
|
@@ -14,11 +15,11 @@ import { DecidedLine } from "./notification-cards";
|
|
|
14
15
|
import { SourceMark } from "./reference-feed";
|
|
15
16
|
import { RatingGrid } from "./review-rating-grid";
|
|
16
17
|
import type { RatingController } from "./use-reference-ratings";
|
|
17
|
-
import { FlagFooter, decidedChoiceOf } from "./needs-me-shared";
|
|
18
|
+
import { FlagFooter, decidedChoiceOf, recordedAnswer } from "./needs-me-shared";
|
|
18
19
|
|
|
19
20
|
import type { CorrectCaptureAction } from "./reference-capture-client";
|
|
20
|
-
/** What the editor
|
|
21
|
-
export type DecisionExtras = {
|
|
21
|
+
/** What the editor chose before deciding an item that is not a Reference: the one steering chip. (The Why lives in the rating store, so it can be typed before or after.) */
|
|
22
|
+
export type DecisionExtras = { notThisKind: boolean };
|
|
22
23
|
|
|
23
24
|
const CHIP = "inline-flex min-h-8 max-w-full items-center gap-1.5 rounded-full bg-[var(--feed-control)] px-2.5 text-xs font-medium text-foreground outline-none transition-colors hover:bg-[var(--feed-control)]/70 focus-visible:ring-2 focus-visible:ring-ring";
|
|
24
25
|
const PILL_TOGGLE = "inline-flex min-h-8 items-center justify-center gap-1.5 rounded-full px-3 text-xs font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring";
|
|
@@ -47,20 +48,27 @@ export function Heading({ item }: { item: NeedsMeEntry }) {
|
|
|
47
48
|
);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
/** What was recorded when the item was decided, for the line under the buttons. */
|
|
51
|
-
function recordedAnswer(item: NeedsMeEntry): { why: string | null; notThisKind: boolean } {
|
|
52
|
-
const { payload } = item;
|
|
53
|
-
if (payload.kind === "draft") return { why: payload.item.decision?.why ?? null, notThisKind: Boolean(payload.item.decision?.notThisKind) };
|
|
54
|
-
if (payload.kind === "request") return { why: payload.request.answer?.feedback ?? null, notThisKind: payload.request.answer?.steer === "not_this_kind" };
|
|
55
|
-
if (payload.kind === "email") return { why: payload.summary.quarantine?.decision?.reason || null, notThisKind: false };
|
|
56
|
-
return { why: null, notThisKind: false };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
51
|
function chipLabel(item: NeedsMeEntry): string | null {
|
|
60
52
|
return item.type === "draft" ? "Not this kind of story" : item.type === "stage_review" ? "Not this kind of thing" : null;
|
|
61
53
|
}
|
|
62
54
|
|
|
63
|
-
/**
|
|
55
|
+
/** The save state of a Why edited after the decision, in words and a tick: never colour alone. */
|
|
56
|
+
function ReasonStatus({ feedback, unsaved, hasReason }: { feedback: { phase: string; message?: string }; unsaved: boolean; hasReason: boolean }) {
|
|
57
|
+
return (
|
|
58
|
+
<div aria-live="polite" className="min-h-5 text-xs text-muted-foreground" data-reason-status={feedback.phase}>
|
|
59
|
+
{feedback.phase === "saving" ? <span className="inline-flex items-center gap-1"><Loader2Icon aria-hidden="true" className="size-3.5 animate-spin" />Saving…</span> : null}
|
|
60
|
+
{(feedback.phase === "saved" || (feedback.phase === "idle" && hasReason)) && !unsaved ? <span className="inline-flex items-center gap-1" data-reason-saved><CheckIcon aria-hidden="true" className="size-3.5" />Saved</span> : null}
|
|
61
|
+
{feedback.phase === "idle" && unsaved ? <span data-reason-unsaved>Not saved yet. It saves when you pause or tap away.</span> : null}
|
|
62
|
+
{feedback.phase === "error" ? <span className="text-destructive" role="alert">{feedback.message}</span> : null}
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Accept / Reject for everything that is not a Reference. The Why box is always there, and the label and the explanation can come in either
|
|
69
|
+
* order: a Why typed first goes with the vote; a Why typed after the vote is saved onto that decision (when you pause or tap away), and
|
|
70
|
+
* it can be changed again whenever the item is reopened. A decided item keeps its Accept or Reject; only its Why stays editable.
|
|
71
|
+
*/
|
|
64
72
|
export function DecisionPanel({ item, ratings, extras, onExtras, disabledReason, lockedNote }: Pick<DetailProps, "item" | "ratings" | "extras" | "onExtras" | "disabledReason"> & { lockedNote?: ReactNode }) {
|
|
65
73
|
const rating = ratings.get(item.id);
|
|
66
74
|
const decided = decidedChoiceOf(item);
|
|
@@ -69,6 +77,8 @@ export function DecisionPanel({ item, ratings, extras, onExtras, disabledReason,
|
|
|
69
77
|
const flag = item.type === "flag";
|
|
70
78
|
const chip = chipLabel(item);
|
|
71
79
|
const recorded = recordedAnswer(item);
|
|
80
|
+
const note = rating.note ?? "";
|
|
81
|
+
const unsaved = note.trim() !== (recorded.why ?? "").trim();
|
|
72
82
|
|
|
73
83
|
// Desktop shortcuts: A accepts, R rejects. Never while typing, and never once decided.
|
|
74
84
|
useEffect(() => {
|
|
@@ -86,25 +96,26 @@ export function DecisionPanel({ item, ratings, extras, onExtras, disabledReason,
|
|
|
86
96
|
|
|
87
97
|
return (
|
|
88
98
|
<section aria-label="Your decision" className="space-y-3 rounded-[20px] bg-[var(--feed-footer)] p-3" data-needs-me-decision {...(locked ? {} : { "data-review-controls": "" })}>
|
|
99
|
+
<AutoGrowTextarea
|
|
100
|
+
aria-label="Why? (optional)"
|
|
101
|
+
className="block min-h-20 w-full rounded-[16px] bg-[var(--feed-control)] p-3 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
102
|
+
data-needs-me-why
|
|
103
|
+
onBlur={() => { if (!disabledReason) ratings.commitNote(item.id); }}
|
|
104
|
+
onChange={(event) => ratings.setNote(item.id, event.target.value)}
|
|
105
|
+
placeholder={flag ? "Why? Optional. Tell the step what to do differently." : "Why? Optional. Your note teaches the AI."}
|
|
106
|
+
value={note}
|
|
107
|
+
/>
|
|
108
|
+
{locked ? <ReasonStatus feedback={rating.feedback} hasReason={Boolean(recorded.why)} unsaved={unsaved} /> : null}
|
|
89
109
|
{locked ? (
|
|
90
110
|
<>
|
|
91
111
|
{item.decisionState === "acknowledged"
|
|
92
|
-
? <p className="flex items-center gap-1.5 text-sm font-medium" data-flag-acknowledged><CircleCheckIcon aria-hidden="true" className="size-4" />Acknowledged
|
|
93
|
-
: <DecidedLine choice={decided ?? "accept"} notThisKind={recorded.notThisKind}
|
|
94
|
-
{flag ? null : <RatingGrid answer={decided} disabled feedback={{ phase: "
|
|
112
|
+
? <p className="flex items-center gap-1.5 text-sm font-medium" data-flag-acknowledged><CircleCheckIcon aria-hidden="true" className="size-4" />Acknowledged</p>
|
|
113
|
+
: <DecidedLine choice={decided ?? "accept"} notThisKind={recorded.notThisKind} />}
|
|
114
|
+
{flag ? null : <RatingGrid answer={decided} disabled feedback={{ phase: "idle" }} onChoose={() => undefined} prediction={{ state: "none" }} showPrediction={false} size="large" surface="footer" />}
|
|
95
115
|
{lockedNote ? <p className="text-xs text-muted-foreground" data-review-disabled-reason>{lockedNote}</p> : null}
|
|
96
116
|
</>
|
|
97
117
|
) : (
|
|
98
118
|
<>
|
|
99
|
-
<textarea
|
|
100
|
-
aria-label="Why? (optional)"
|
|
101
|
-
className="block min-h-20 w-full resize-y rounded-[16px] bg-[var(--feed-control)] p-3 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
102
|
-
data-needs-me-why
|
|
103
|
-
onChange={(event) => onExtras({ ...extras, why: event.target.value })}
|
|
104
|
-
placeholder={flag ? "Why? Optional. Tell the step what to do differently." : "Why? Optional. Your note teaches the AI."}
|
|
105
|
-
readOnly={busy}
|
|
106
|
-
value={extras.why}
|
|
107
|
-
/>
|
|
108
119
|
{chip && !flag ? (
|
|
109
120
|
<div className="flex flex-wrap items-center gap-2" data-review-reasons>
|
|
110
121
|
<button
|
|
@@ -12,7 +12,8 @@ export function NeedsMeDetail(props: DetailProps) {
|
|
|
12
12
|
const { payload } = item;
|
|
13
13
|
return (
|
|
14
14
|
<div className="flex flex-col gap-4 p-4 font-sans" data-needs-me-detail={item.id} data-needs-me-detail-type={item.type}>
|
|
15
|
-
|
|
15
|
+
{/* A Reference puts its title first and carries these badges under it. */}
|
|
16
|
+
{payload.kind === "reference" ? null : <Heading item={item} />}
|
|
16
17
|
{payload.kind === "reference" && isReferenceLike(item) ? <ReferenceBody {...props} payload={payload} /> : null}
|
|
17
18
|
{payload.kind === "draft" ? <DraftBody {...props} payload={payload} /> : null}
|
|
18
19
|
{payload.kind === "request" ? <RequestBody {...props} payload={payload} /> : null}
|
|
@@ -24,7 +24,7 @@ import type { FeedFreshness } from "../lib/reference-feed-ranking";
|
|
|
24
24
|
import type { RecentDecision } from "../lib/notification-feed";
|
|
25
25
|
import { getKanbusTheme, SITE_BRAND } from "../lib/site-brand";
|
|
26
26
|
import { NeedsMeDetail, type DecisionExtras } from "./needs-me-detail";
|
|
27
|
-
import { FlagFooter, decidedChoiceOf, isReferenceLike } from "./needs-me-shared";
|
|
27
|
+
import { FlagFooter, decidedChoiceOf, isReferenceLike, recordedAnswer } from "./needs-me-shared";
|
|
28
28
|
import { NotificationSkeleton, RecentDecisions } from "./notification-cards";
|
|
29
29
|
import { FEED_GRID, FEED_SURFACE_PROPS, FeedImage, PILL, REASON_LABELS, SourceMark } from "./reference-feed";
|
|
30
30
|
import { predictionFromDecision, ReviewQueueBar, useQueueGestures, type CardPrediction, type ReviewChoice } from "./reference-review-card";
|
|
@@ -102,6 +102,9 @@ export function NeedsMeCard({ item, now, onOpen, predictionLoading, ratings, sel
|
|
|
102
102
|
const feedback = rating.feedback.phase === "idle" && decided ? { phase: "saved" as const } : rating.feedback;
|
|
103
103
|
const reason = item.payload.kind === "reference" ? rating.reasonCode : null;
|
|
104
104
|
const reasonLabel = decided === "reject" && reason ? REASON_LABELS[reason] : null;
|
|
105
|
+
// A decided item shows the Why it was given (clipped; the whole text is edited in the detail pane).
|
|
106
|
+
const why = !refLike && locked ? rating.note?.trim() ?? "" : "";
|
|
107
|
+
const whyLine = why ? `Why: ${why.length > 90 ? `${why.slice(0, 89).trimEnd()}\u2026` : why}` : null;
|
|
105
108
|
const time = item.payload.kind === "reference" ? item.payload.displayTime ?? item.createdAt : item.createdAt;
|
|
106
109
|
const age = formatFeedAge(time, now);
|
|
107
110
|
const picture = item.image || item.host ? <FeedImage host={item.host} lead={false} name={item.source} src={item.image} /> : <TypeTile type={item.type} />;
|
|
@@ -169,7 +172,7 @@ export function NeedsMeCard({ item, now, onOpen, predictionLoading, ratings, sel
|
|
|
169
172
|
disabled={Boolean(disabledReason) || locked}
|
|
170
173
|
disabledReason={disabledReason}
|
|
171
174
|
feedback={feedback}
|
|
172
|
-
note={reasonLabel ? `Reason: ${reasonLabel}` : locked ? "Decided here." : null}
|
|
175
|
+
note={reasonLabel ? `Reason: ${reasonLabel}` : locked ? whyLine ?? "Decided here." : null}
|
|
173
176
|
onChoose={(choice) => { if (!disabledReason && !locked) ratings.choose(item.id, choice); }}
|
|
174
177
|
prediction={cardPrediction(item, predictionLoading)}
|
|
175
178
|
showPrediction={refLike}
|
|
@@ -247,7 +250,7 @@ export function NeedsMeRecipe({ freshness, threshold }: { freshness: FeedFreshne
|
|
|
247
250
|
// --- the view -----------------------------------------------------------------------------------------------------------
|
|
248
251
|
|
|
249
252
|
export type NeedsMeLoadState = "loading" | "ready" | "forbidden" | "unavailable";
|
|
250
|
-
const NO_EXTRAS: DecisionExtras = {
|
|
253
|
+
const NO_EXTRAS: DecisionExtras = { notThisKind: false };
|
|
251
254
|
const NO_ATTACHMENTS: ReferenceAttachmentRecord[] = [];
|
|
252
255
|
|
|
253
256
|
export type NeedsMeViewProps = {
|
|
@@ -276,9 +279,13 @@ export type NeedsMeViewProps = {
|
|
|
276
279
|
captureTeaserLine?: number;
|
|
277
280
|
onCorrectCapture?: CorrectCaptureAction;
|
|
278
281
|
onDecide: (item: NeedsMeEntry, decision: NeedsMeDecision) => Promise<void>;
|
|
282
|
+
/** Changes only the Why of an item that is already decided (an AI draft, a review request, a held email). Throws a sentence when it cannot. */
|
|
283
|
+
onEditReason?: (item: NeedsMeEntry, why: string) => Promise<void>;
|
|
279
284
|
onRetryDrafts?: () => void;
|
|
280
285
|
onOpened?: (item: NeedsMeEntry) => void;
|
|
281
286
|
loadEmail?: (receiptId: string) => Promise<MonitoringEmailDetails>;
|
|
287
|
+
/** Other sections of the page (open assignments, sections). They scroll inside the feed, below the cards, so the feed and the detail keep the viewport's full height. */
|
|
288
|
+
below?: ReactNode;
|
|
282
289
|
};
|
|
283
290
|
|
|
284
291
|
function Note({ children, ...rest }: { children: ReactNode } & Record<string, unknown>) {
|
|
@@ -288,7 +295,7 @@ function Note({ children, ...rest }: { children: ReactNode } & Record<string, un
|
|
|
288
295
|
/** The CMS console home: one feed that mixes everything that may need a person, with the References feed's cards and detail pane. */
|
|
289
296
|
export function NeedsMeView({
|
|
290
297
|
loadState, entries, freshness, threshold, now, shaping, recent, legacyAuto = [], settled, demo = false, stale = false, error = null, referencesLoading = false, predictionLoading = false,
|
|
291
|
-
referenceDisabledReason = null, draftsFailed = false, missingDraftCount = 0, initialFilter = "all", referenceAttachments = NO_ATTACHMENTS, captureTeaserLine, onCorrectCapture, onDecide, onRetryDrafts, onOpened, loadEmail,
|
|
298
|
+
referenceDisabledReason = null, draftsFailed = false, missingDraftCount = 0, initialFilter = "all", referenceAttachments = NO_ATTACHMENTS, captureTeaserLine, onCorrectCapture, onDecide, onEditReason, onRetryDrafts, onOpened, loadEmail, below,
|
|
292
299
|
}: NeedsMeViewProps) {
|
|
293
300
|
const themeMode = useResolvedPapyrusTheme();
|
|
294
301
|
const themeAttributes = { "data-kanbus-theme": getKanbusTheme(SITE_BRAND), "data-color-mode": themeMode };
|
|
@@ -319,13 +326,22 @@ export function NeedsMeView({
|
|
|
319
326
|
const item = latest.current.byId.get(id);
|
|
320
327
|
if (!item) throw new Error("This item is no longer in the list.");
|
|
321
328
|
const typed = latest.current.extras[id] ?? NO_EXTRAS;
|
|
322
|
-
|
|
323
|
-
await onDecide(item, { choice, reasonCode: detail.reasonCode, why: referenceLike ? detail.note : typed.why.trim(), notThisKind: choice === "reject" && typed.notThisKind });
|
|
329
|
+
await onDecide(item, { choice, reasonCode: detail.reasonCode, why: detail.note, notThisKind: choice === "reject" && typed.notThisKind });
|
|
324
330
|
},
|
|
325
|
-
|
|
331
|
+
// The Why of an AI draft, a review request or a held email is typed in either order and edited after the decision, onto that decision.
|
|
332
|
+
sticky: (id) => { const item = latest.current.byId.get(id); return item ? !isReferenceLike(item) : false; },
|
|
333
|
+
saveNote: async (id, note) => {
|
|
334
|
+
const item = latest.current.byId.get(id);
|
|
335
|
+
if (!item) throw new Error("This item is no longer in the list.");
|
|
336
|
+
if (!onEditReason) throw new Error("A reason cannot be changed here.");
|
|
337
|
+
await onEditReason(item, note);
|
|
338
|
+
},
|
|
339
|
+
// An acknowledged flag counts as an Accept: it is decided, and only its note can change.
|
|
340
|
+
saved: (id) => { const item = latest.current.byId.get(id); return item ? decidedChoiceOf(item) ?? (item.decisionState === "acknowledged" ? "accept" : null) : null; },
|
|
326
341
|
seed: (id) => {
|
|
327
342
|
const item = latest.current.byId.get(id);
|
|
328
|
-
if (!item
|
|
343
|
+
if (!item) return { note: "", reasonCode: null };
|
|
344
|
+
if (item.payload.kind !== "reference") return { note: recordedAnswer(item).why ?? "", reasonCode: null };
|
|
329
345
|
const { reference } = item.payload;
|
|
330
346
|
const pending = normalizeReferenceCurationStatus(reference.curationStatus) === "pending";
|
|
331
347
|
return { note: pending ? "" : reference.curationStatusReason ?? "", reasonCode: referenceReasonCode(reference) ?? null };
|
|
@@ -384,12 +400,12 @@ export function NeedsMeView({
|
|
|
384
400
|
</div>
|
|
385
401
|
);
|
|
386
402
|
const queueBar = queue.length || caughtUp ? (
|
|
387
|
-
<ReviewQueueBar caughtUp={caughtUp} onNext={goNext} onPrevious={goPrevious} position={selectedIndex >= 0 ? selectedIndex + 1 : null} total={queue.length} />
|
|
403
|
+
<ReviewQueueBar anchored caughtUp={caughtUp} onNext={goNext} onPrevious={goPrevious} position={selectedIndex >= 0 ? selectedIndex + 1 : null} total={queue.length} />
|
|
388
404
|
) : null;
|
|
389
405
|
|
|
390
406
|
return (
|
|
391
|
-
<div className="
|
|
392
|
-
<div className="min-w-0 flex-1 space-y-6 pb-6 md:min-h-0 md:overflow-y-auto md:pr-1" data-needs-me-main data-needs-me-scroll>
|
|
407
|
+
<div className="flex flex-col gap-5 md:h-full md:flex-row md:items-stretch" data-detail-reserved="true" data-notification-center {...FEED_SURFACE_PROPS}>
|
|
408
|
+
<div className="@container min-w-0 flex-1 space-y-6 pb-6 md:min-h-0 md:overflow-y-auto md:pr-1" data-needs-me-main data-needs-me-scroll>
|
|
393
409
|
<header className="space-y-4">
|
|
394
410
|
<div className="space-y-1.5">
|
|
395
411
|
<h1 className="text-4xl font-bold tracking-tight text-foreground sm:text-5xl" data-notification-title>What needs me?</h1>
|
|
@@ -523,11 +539,12 @@ export function NeedsMeView({
|
|
|
523
539
|
<RecentDecisions entries={recent} />
|
|
524
540
|
</section>
|
|
525
541
|
) : null}
|
|
542
|
+
{below}
|
|
526
543
|
</div>
|
|
527
544
|
|
|
528
545
|
<aside
|
|
529
546
|
aria-label="Detail"
|
|
530
|
-
className="hidden min-h-0 w-full shrink-0 flex-col overflow-hidden rounded-[20px] bg-[var(--feed-card)] md:flex md:h-full md:w-[min(100%,24rem)] lg:w-[min(100%,28rem)]"
|
|
547
|
+
className="hidden min-h-0 w-full shrink-0 flex-col overflow-hidden rounded-[20px] bg-[var(--feed-card)] md:flex md:h-full md:w-[min(100%,24rem)] md:pb-16 lg:w-[min(100%,28rem)]"
|
|
531
548
|
data-feed-flat=""
|
|
532
549
|
data-needs-me-detail-shell
|
|
533
550
|
data-detail-open={selected ? "true" : "false"}
|
|
@@ -545,7 +562,7 @@ export function NeedsMeView({
|
|
|
545
562
|
|
|
546
563
|
{isMobile ? (
|
|
547
564
|
<Sheet onOpenChange={(open) => { if (!open) closeDetail(); else setSheetOpen(true); }} open={(sheetOpen && Boolean(selected)) || caughtUp}>
|
|
548
|
-
<SheetContent className="gap-0 rounded-t-[
|
|
565
|
+
<SheetContent className="gap-0 rounded-t-[length:var(--feed-radius-xl)] bg-[var(--feed-card)] p-0" data-feed-flat="" data-feed-surface="" initialFocus={false} side="bottom" {...themeAttributes}>
|
|
549
566
|
<SheetHeader className="px-4 py-3"><SheetTitle className="text-left text-base">{selected ? NEEDS_ME_TYPE_LABELS[selected.type] : "Detail"}</SheetTitle></SheetHeader>
|
|
550
567
|
{selected ? (
|
|
551
568
|
<div className="min-w-0" data-review-gestures onPointerDown={gestures.onPointerDown} onPointerUp={gestures.onPointerUp} style={gestures.style}>
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { referenceDisplayTitle, referenceLineageId, formatReferenceListDate } from "../lib/newsroom-references";
|
|
4
4
|
import { referenceMetadataField } from "../lib/reference-display";
|
|
5
|
+
import { referenceDek, referencePreviewsByLineage } from "../lib/reference-preview";
|
|
5
6
|
import { DEFAULT_TEASER_WORDS } from "../lib/reference-capture";
|
|
6
7
|
import { referenceReasonCode, normalizeReferenceCurationStatus } from "../lib/reference-policy";
|
|
7
8
|
import type { ReferenceRecord } from "../lib/category-repository";
|
|
8
9
|
import type { NeedsMeEntry } from "../lib/needs-me-items";
|
|
9
|
-
import { DefaultOutcome, type DetailProps } from "./needs-me-bodies";
|
|
10
|
+
import { DefaultOutcome, Heading, type DetailProps } from "./needs-me-bodies";
|
|
10
11
|
import { decidedChoiceOf } from "./needs-me-shared";
|
|
11
12
|
import { predictionFromDecision, ReviewCard, type CardPrediction } from "./reference-review-card";
|
|
12
13
|
import { ReferenceCapturePanel } from "./reference-capture-panel";
|
|
@@ -20,7 +21,10 @@ function cardPrediction(item: NeedsMeEntry, predictionLoading: boolean): CardPre
|
|
|
20
21
|
return predictionLoading ? { state: "loading" } : { state: "none" };
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* A Reference, or a Reference whose capture may be cut off: the References detail's own pieces, in the same order. What the item is
|
|
26
|
+
* comes first (title, blurb, link, badges, summary), then the rating, then what was captured and how well.
|
|
27
|
+
*/
|
|
24
28
|
export function ReferenceBody(props: DetailProps & { payload: Extract<NeedsMeEntry["payload"], { kind: "reference" }> }) {
|
|
25
29
|
const { item, payload, ratings, disabledReason, attachments, captureTeaserLine = DEFAULT_TEASER_WORDS, onCorrectCapture, predictionLoading } = props;
|
|
26
30
|
const reference: ReferenceRecord = payload.reference;
|
|
@@ -29,26 +33,23 @@ export function ReferenceBody(props: DetailProps & { payload: Extract<NeedsMeEnt
|
|
|
29
33
|
const previewAttachments = useReferencePreviewAttachments(reference, attachments);
|
|
30
34
|
const capture = useReferenceCapture(reference, previewAttachments, captureTeaserLine, onCorrectCapture);
|
|
31
35
|
const url = reference.sourceUri?.trim() && /^https?:\/\//i.test(reference.sourceUri.trim()) ? reference.sourceUri.trim() : null;
|
|
32
|
-
const summary = referenceMetadataField(null, reference.metadata, "summary");
|
|
36
|
+
const summary = capture.summary ?? referenceMetadataField(null, reference.metadata, "summary");
|
|
37
|
+
const dek = referenceDek(reference, referencePreviewsByLineage(previewAttachments).get(lineageId));
|
|
33
38
|
const incomplete = item.type === "incomplete_capture";
|
|
34
|
-
const capturePanel = <ReferenceCapturePanel capture={capture} startEditing={incomplete} />;
|
|
35
39
|
return (
|
|
36
40
|
<div className="space-y-4" data-reference-curation-status={status} data-news-desk-reference-detail={lineageId}>
|
|
37
|
-
{incomplete ? (
|
|
38
|
-
<div className="space-y-2" data-needs-me-capture-first>
|
|
39
|
-
<p className="text-sm text-muted-foreground">The captured text looks cut off or missing. Paste the full text below and say whether the capture was complete, or rate the Reference as it is.</p>
|
|
40
|
-
{capturePanel}
|
|
41
|
-
</div>
|
|
42
|
-
) : null}
|
|
43
41
|
<ReviewCard
|
|
44
42
|
decided={decidedChoiceOf(item)}
|
|
45
43
|
disabledReason={disabledReason}
|
|
46
44
|
initialNote={status === "pending" ? "" : reference.curationStatusReason ?? ""}
|
|
47
45
|
initialReason={referenceReasonCode(reference) ?? null}
|
|
46
|
+
dek={dek}
|
|
48
47
|
itemId={item.id}
|
|
48
|
+
meta={<Heading item={item} />}
|
|
49
49
|
onDecide={async () => undefined}
|
|
50
50
|
prediction={cardPrediction(item, predictionLoading)}
|
|
51
51
|
ratings={ratings}
|
|
52
|
+
sourcePreview={<ReferenceSourcePreview attachments={previewAttachments} sourceUri={reference.sourceUri} />}
|
|
52
53
|
summary={summary}
|
|
53
54
|
title={referenceDisplayTitle(reference)}
|
|
54
55
|
titleHref={url}
|
|
@@ -56,11 +57,8 @@ export function ReferenceBody(props: DetailProps & { payload: Extract<NeedsMeEnt
|
|
|
56
57
|
{payload.decision ? <div data-reference-relevance-decision={lineageId}><ReferenceSuggestionProvenance decision={payload.decision} /></div> : null}
|
|
57
58
|
</ReviewCard>
|
|
58
59
|
<DefaultOutcome item={item} />
|
|
59
|
-
{incomplete ?
|
|
60
|
-
<
|
|
61
|
-
<h3 className="text-sm font-medium text-foreground">Source preview</h3>
|
|
62
|
-
<ReferenceSourcePreview attachments={previewAttachments} sourceUri={reference.sourceUri} />
|
|
63
|
-
</section>
|
|
60
|
+
{incomplete ? <p className="text-sm text-muted-foreground" data-needs-me-capture-note>The captured text looks cut off or missing. Read it below. If it is incomplete, choose Paste or correct the text and say whether the capture was complete, or rate the Reference as it is.</p> : null}
|
|
61
|
+
<ReferenceCapturePanel capture={capture} showSummary={false} />
|
|
64
62
|
<dl className="grid grid-cols-2 gap-3 text-sm">
|
|
65
63
|
<div>
|
|
66
64
|
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Imported</dt>
|
|
@@ -14,6 +14,15 @@ export function decidedChoiceOf(item: Pick<NeedsMeEntry, "decisionState">): Revi
|
|
|
14
14
|
return item.decisionState === "accepted" ? "accept" : item.decisionState === "rejected" ? "reject" : null;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** What was recorded when a non-Reference item was decided: its Why, and whether it was rejected as "not this kind". Shown again whenever the item is reopened. */
|
|
18
|
+
export function recordedAnswer(item: NeedsMeEntry): { why: string | null; notThisKind: boolean } {
|
|
19
|
+
const { payload } = item;
|
|
20
|
+
if (payload.kind === "draft") return { why: payload.item.decision?.why ?? null, notThisKind: Boolean(payload.item.decision?.notThisKind) };
|
|
21
|
+
if (payload.kind === "request") return { why: payload.request.answer?.feedback ?? null, notThisKind: payload.request.answer?.steer === "not_this_kind" };
|
|
22
|
+
if (payload.kind === "email") return { why: payload.summary.quarantine?.decision?.reason || null, notThisKind: false };
|
|
23
|
+
return { why: null, notThisKind: false };
|
|
24
|
+
}
|
|
25
|
+
|
|
17
26
|
/** A flag is information, not a question: one Acknowledge button in the footer's own tone. */
|
|
18
27
|
export function FlagFooter({ busy, disabledReason, done, error, onAcknowledge }: { busy: boolean; disabledReason: string | null; done: boolean; error: string | null; onAcknowledge: () => void }) {
|
|
19
28
|
return (
|
|
@@ -6,7 +6,7 @@ import { SITE_BRAND } from "../lib/site-brand";
|
|
|
6
6
|
import type { NewsDeskShellState } from "../lib/news-desk-session";
|
|
7
7
|
import { cn } from "../lib/utils";
|
|
8
8
|
import { NewsroomOpsShell } from "./newsroom-ops-shell";
|
|
9
|
-
import {
|
|
9
|
+
import { NotificationSkeleton } from "./notification-cards";
|
|
10
10
|
import { ReaderAuthControl } from "./reader-auth-control";
|
|
11
11
|
import { buttonVariants } from "./ui/button";
|
|
12
12
|
import { ReferencesPageLoading } from "./references-page-loading";
|
|
@@ -28,19 +28,10 @@ export function NewsDeskAccessGate({ initialTab, shell, showSectionTabs = false
|
|
|
28
28
|
pageTitle={referencesPage ? "References" : showSectionTabs ? "Notification Center" : "Newsroom"}
|
|
29
29
|
showNavigation={showSectionTabs || referencesPage}
|
|
30
30
|
>
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
<div className="h-6 w-48 rounded bg-muted" />
|
|
34
|
-
<div className="h-4 w-3/4 rounded bg-muted" />
|
|
35
|
-
<div className="grid gap-3 sm:grid-cols-2">
|
|
36
|
-
{Array.from({ length: 4 }, (_, index) => <div className="h-32 rounded-xl border bg-card p-4" key={index}>
|
|
37
|
-
<div className="h-4 w-2/3 rounded bg-muted" /><div className="mt-4 h-3 w-1/2 rounded bg-muted" />
|
|
38
|
-
</div>)}
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
)) : (
|
|
31
|
+
{/* The loading state is the feed's own skeleton, so what loads replaces it in place. */}
|
|
32
|
+
{pending && referencesPage ? <ReferencesPageLoading /> : pending ? <NotificationSkeleton bare={!showSectionTabs} count={4} /> : (
|
|
42
33
|
<section aria-live="polite" data-news-desk-access={accessPhase} data-news-desk-access-phase={accessPhase}>
|
|
43
|
-
<div className="space-y-3 rounded-xl
|
|
34
|
+
<div className="space-y-3 rounded-xl bg-card p-6">
|
|
44
35
|
<p className="m-0 text-[0.7rem] font-semibold uppercase tracking-[0.08em] text-muted-foreground">Access</p>
|
|
45
36
|
<h2 className="m-0 font-sans text-xl font-semibold tracking-tight">{formatAccessTitle(shell)}</h2>
|
|
46
37
|
<p className="m-0 text-sm text-muted-foreground">{formatAccessDetail(shell)}</p>
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import Link from "next/link";
|
|
4
4
|
import { usePathname } from "next/navigation";
|
|
5
5
|
import { useCallback, useMemo } from "react";
|
|
6
|
-
import { Badge } from "./ui/badge";
|
|
7
6
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card";
|
|
8
7
|
import type { AssignmentRecord, NewsroomSectionRecord, ReferenceAttachmentRecord, ReferenceRecord } from "../lib/category-repository";
|
|
9
8
|
import { getNewsroomNavHref } from "../lib/newsroom-nav";
|
|
@@ -15,7 +14,8 @@ import {
|
|
|
15
14
|
newsroomDeskKind,
|
|
16
15
|
sortNewsroomSections,
|
|
17
16
|
} from "../lib/newsroom-sections";
|
|
18
|
-
import {
|
|
17
|
+
import { cn } from "../lib/utils";
|
|
18
|
+
import { FEED_SURFACE_PROPS } from "./reference-feed";
|
|
19
19
|
import type { RelevanceDecisionView } from "../lib/relevance-decisions";
|
|
20
20
|
import type { ReferenceRejectionReasonCode } from "../lib/reference-policy";
|
|
21
21
|
import { referencePreviewsByLineage, type ReferencePreview } from "../lib/reference-preview";
|
|
@@ -100,30 +100,8 @@ export function NewsroomOpsOverview({
|
|
|
100
100
|
const rotatingDesks = useMemo(() => desks.filter(desk => newsroomDeskKind(desk) === "rotating"), [desks]);
|
|
101
101
|
const rateDisabled = disabledReason ?? (showingSavedCopy ? "Confirming your sign-in. You can answer in a moment." : null);
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
<div
|
|
105
|
-
className="space-y-12 font-sans"
|
|
106
|
-
data-news-desk-section="overview"
|
|
107
|
-
data-newsroom-ops-home="true"
|
|
108
|
-
>
|
|
109
|
-
<div className="space-y-12" data-newsroom-overview-feeds>
|
|
110
|
-
<div data-newsroom-overview-notifications>
|
|
111
|
-
<NotificationCenter
|
|
112
|
-
actorLabel={actorLabel}
|
|
113
|
-
decisions={shownDecisions}
|
|
114
|
-
demo={demo}
|
|
115
|
-
fixture={notificationFixture}
|
|
116
|
-
humanCuratedReferenceLineageIds={humanCuratedReferenceLineageIds}
|
|
117
|
-
onCorrectCapture={onCorrectCapture}
|
|
118
|
-
onRateReference={rateReference}
|
|
119
|
-
referenceAttachments={referenceAttachments}
|
|
120
|
-
predictionLoading={predictionState === "loading"}
|
|
121
|
-
previews={previews}
|
|
122
|
-
referenceDisabledReason={rateDisabled}
|
|
123
|
-
references={canonicalReferences}
|
|
124
|
-
referencesLoading={loading && !canonicalReferences.length}
|
|
125
|
-
/>
|
|
126
|
-
</div>
|
|
103
|
+
const below = (
|
|
104
|
+
<div className="space-y-12 pt-6" data-newsroom-overview-below {...FEED_SURFACE_PROPS}>
|
|
127
105
|
{attentionAssignments.length ? (
|
|
128
106
|
<section aria-label="Open assignments" className="space-y-3">
|
|
129
107
|
<h2 className="text-sm font-medium text-muted-foreground">Open assignments</h2>
|
|
@@ -134,7 +112,7 @@ export function NewsroomOpsOverview({
|
|
|
134
112
|
return (
|
|
135
113
|
<Link
|
|
136
114
|
aria-current={active ? "true" : undefined}
|
|
137
|
-
className={
|
|
115
|
+
className={cn("flex items-start gap-3 rounded-[16px] bg-[var(--feed-card)] p-3 text-foreground outline-none transition-colors hover:bg-[var(--feed-control)] focus-visible:ring-2 focus-visible:ring-ring", active && "bg-[var(--feed-control)]")}
|
|
138
116
|
data-newsroom-inbox-item="assignment"
|
|
139
117
|
data-newsroom-list-row
|
|
140
118
|
data-selected={active || undefined}
|
|
@@ -145,7 +123,7 @@ export function NewsroomOpsOverview({
|
|
|
145
123
|
<p className="truncate text-sm font-medium text-foreground">{assignment.title}</p>
|
|
146
124
|
<p className="text-xs text-muted-foreground">Assignment · {assignment.assignmentTypeKey}</p>
|
|
147
125
|
</div>
|
|
148
|
-
<
|
|
126
|
+
<span className="mt-0.5 shrink-0 rounded-full bg-[var(--feed-control)] px-2 py-0.5 text-xs font-medium">{assignment.status}</span>
|
|
149
127
|
</Link>
|
|
150
128
|
);
|
|
151
129
|
})}
|
|
@@ -165,7 +143,7 @@ export function NewsroomOpsOverview({
|
|
|
165
143
|
href={buildNewsroomSectionHref(section.id, demo)}
|
|
166
144
|
key={section.id}
|
|
167
145
|
>
|
|
168
|
-
<Card className="h-full transition-colors hover:
|
|
146
|
+
<Card className="h-full rounded-[20px] border-0 bg-[var(--feed-card)] shadow-none ring-0 transition-colors hover:bg-[var(--feed-control)]">
|
|
169
147
|
<CardHeader className="p-4">
|
|
170
148
|
<CardTitle className="text-base font-semibold">{section.title}</CardTitle>
|
|
171
149
|
<CardDescription className="uppercase tracking-wide text-[0.65rem]">
|
|
@@ -185,7 +163,7 @@ export function NewsroomOpsOverview({
|
|
|
185
163
|
href={buildNewsroomSectionHref(section.id, demo)}
|
|
186
164
|
key={section.id}
|
|
187
165
|
>
|
|
188
|
-
<Card className="h-full transition-colors hover:
|
|
166
|
+
<Card className="h-full rounded-[20px] border-0 bg-[var(--feed-card)] shadow-none ring-0 transition-colors hover:bg-[var(--feed-control)]">
|
|
189
167
|
<CardHeader className="p-4">
|
|
190
168
|
<CardTitle className="text-base font-semibold">{section.title}</CardTitle>
|
|
191
169
|
<CardDescription className="uppercase tracking-wide text-[0.65rem]">
|
|
@@ -198,6 +176,34 @@ export function NewsroomOpsOverview({
|
|
|
198
176
|
</div> : null}
|
|
199
177
|
</section>
|
|
200
178
|
) : null}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div
|
|
184
|
+
className="h-full font-sans"
|
|
185
|
+
data-news-desk-section="overview"
|
|
186
|
+
data-newsroom-ops-home="true"
|
|
187
|
+
>
|
|
188
|
+
<div className="h-full" data-newsroom-overview-feeds>
|
|
189
|
+
<div className="h-full" data-newsroom-overview-notifications>
|
|
190
|
+
<NotificationCenter
|
|
191
|
+
below={below}
|
|
192
|
+
actorLabel={actorLabel}
|
|
193
|
+
decisions={shownDecisions}
|
|
194
|
+
demo={demo}
|
|
195
|
+
fixture={notificationFixture}
|
|
196
|
+
humanCuratedReferenceLineageIds={humanCuratedReferenceLineageIds}
|
|
197
|
+
onCorrectCapture={onCorrectCapture}
|
|
198
|
+
onRateReference={rateReference}
|
|
199
|
+
referenceAttachments={referenceAttachments}
|
|
200
|
+
predictionLoading={predictionState === "loading"}
|
|
201
|
+
previews={previews}
|
|
202
|
+
referenceDisabledReason={rateDisabled}
|
|
203
|
+
references={canonicalReferences}
|
|
204
|
+
referencesLoading={loading && !canonicalReferences.length}
|
|
205
|
+
/>
|
|
206
|
+
</div>
|
|
201
207
|
</div>
|
|
202
208
|
</div>
|
|
203
209
|
);
|
|
@@ -210,10 +210,10 @@ export function NewsroomOpsStatusBanner({
|
|
|
210
210
|
return (
|
|
211
211
|
<div
|
|
212
212
|
className={cn(
|
|
213
|
-
|
|
214
|
-
tone === "
|
|
215
|
-
tone === "
|
|
216
|
-
tone === "info" && "
|
|
213
|
+
// A success is announced to screen readers and never drawn: it would push the page down. Errors and notes are flat fills, no outlines.
|
|
214
|
+
tone === "ok" ? "sr-only" : "mb-4 rounded-lg px-3 py-2 text-sm",
|
|
215
|
+
tone === "error" && "bg-destructive/10 text-destructive",
|
|
216
|
+
tone === "info" && "bg-muted/60 text-foreground",
|
|
217
217
|
)}
|
|
218
218
|
role="status"
|
|
219
219
|
>
|
|
@@ -16,7 +16,7 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
16
16
|
.organizer-destinations a[aria-current=page]{color:var(--org-ink);box-shadow:inset 3px 0 0 var(--org-accent)}
|
|
17
17
|
.newsroom-organizer :is(a,button):focus-visible,.organizer-menu :is(a,button):focus-visible{outline:2px solid currentColor;outline-offset:-3px}
|
|
18
18
|
.organizer-workspace{display:flex;flex-direction:column;min-height:0;min-width:0;overflow:hidden;padding:0 16px}
|
|
19
|
-
.organizer-topbar{height:48px;min-height:48px;display:flex;align-items:center;gap:16px
|
|
19
|
+
.organizer-topbar{height:48px;min-height:48px;display:flex;align-items:center;gap:16px}
|
|
20
20
|
.organizer-masthead{font:900 28px var(--font-wordmark),Georgia,serif;white-space:nowrap;max-width:30vw;overflow:hidden;text-overflow:ellipsis}
|
|
21
21
|
.organizer-masthead-logo{display:block;height:24px;width:auto;max-width:36vw;overflow:visible}
|
|
22
22
|
.organizer-topbar h1{font:400 12px var(--kb-theme-ui-font);flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:0}
|
|
@@ -362,15 +362,21 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
362
362
|
The newsroom chrome squares every "rounded" class above; inside a [data-feed-surface] the rounded classes mean what they say.
|
|
363
363
|
The repeated attribute raises specificity over the squaring rules without !important escalation. */
|
|
364
364
|
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface] [class*="rounded-full"],
|
|
365
|
-
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-full"] { border-radius:
|
|
365
|
+
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-full"] { border-radius: var(--feed-radius-pill) !important; }
|
|
366
366
|
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface] [class*="rounded-[20px]"],
|
|
367
|
-
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-[20px]"] { border-radius:
|
|
367
|
+
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-[20px]"] { border-radius: var(--feed-radius-lg) !important; }
|
|
368
368
|
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface] [class*="rounded-2xl"],
|
|
369
|
-
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-2xl"] { border-radius:
|
|
369
|
+
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-2xl"] { border-radius: var(--feed-radius-md) !important; }
|
|
370
370
|
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface] [class*="rounded-[16px]"],
|
|
371
|
-
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-[16px]"] { border-radius:
|
|
371
|
+
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-[16px]"] { border-radius: var(--feed-radius-md) !important; }
|
|
372
372
|
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface] [class*="rounded-3xl"],
|
|
373
|
-
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-3xl"] { border-radius:
|
|
373
|
+
.newsroom-organizer[data-kanbus-theme] [data-feed-surface][data-feed-surface][data-feed-surface][class*="rounded-3xl"] { border-radius: var(--feed-radius-xl) !important; }
|
|
374
|
+
|
|
375
|
+
/* The mobile sheet is portaled outside .newsroom-organizer; it carries the theme attributes itself, so the same radii apply inside it. */
|
|
376
|
+
[data-kanbus-theme][data-feed-surface] [class*="rounded-full"] { border-radius: var(--feed-radius-pill) !important; }
|
|
377
|
+
[data-kanbus-theme][data-feed-surface] [class*="rounded-[20px]"] { border-radius: var(--feed-radius-lg) !important; }
|
|
378
|
+
[data-kanbus-theme][data-feed-surface] [class*="rounded-2xl"], [data-kanbus-theme][data-feed-surface] [class*="rounded-[16px]"] { border-radius: var(--feed-radius-md) !important; }
|
|
379
|
+
[data-kanbus-theme][data-feed-surface] [class*="rounded-3xl"] { border-radius: var(--feed-radius-xl) !important; }
|
|
374
380
|
|
|
375
381
|
/* Feed tones. Light: cards a touch lighter than the page, footers between, controls a touch darker. Dark: each step lifts toward the ink.
|
|
376
382
|
Also set on the portaled mobile sheet, which carries the theme attributes itself. */
|
|
@@ -378,6 +384,12 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
378
384
|
--feed-card: color-mix(in oklab, var(--kb-theme-paper) 35%, white);
|
|
379
385
|
--feed-footer: color-mix(in oklab, var(--kb-theme-paper) 70%, white);
|
|
380
386
|
--feed-control: color-mix(in oklab, var(--kb-theme-paper) 90%, var(--kb-theme-ink));
|
|
387
|
+
/* Corner radii of the whole feed layer, in one place (Ryan, 2026-10-27: half of the first design). lg: cards and the detail pane;
|
|
388
|
+
md: rating cells, buttons, textareas; xl: sheets; pill: chips, filters and toggles (a rounded rectangle, not a capsule). */
|
|
389
|
+
--feed-radius-xl: 12px;
|
|
390
|
+
--feed-radius-lg: 10px;
|
|
391
|
+
--feed-radius-md: 8px;
|
|
392
|
+
--feed-radius-pill: 8px;
|
|
381
393
|
}
|
|
382
394
|
[data-kanbus-theme][data-color-mode="dark"] [data-feed-surface], [data-kanbus-theme][data-color-mode="dark"][data-feed-surface] {
|
|
383
395
|
--feed-card: color-mix(in oklab, var(--kb-theme-paper) 94%, var(--kb-theme-ink));
|
|
@@ -392,16 +404,16 @@ body:has(.newsroom-organizer)>[data-staging-banner]{flex-shrink:0}
|
|
|
392
404
|
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] *:not(:focus-visible) { box-shadow: none; }
|
|
393
405
|
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] :is([data-slot=separator], hr) { display: none !important; }
|
|
394
406
|
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] :is(button, [role=button], [data-slot=badge]):not([aria-pressed=true]):not([data-slot=sheet-close]):not([class*="hover:bg-muted"]) {
|
|
395
|
-
background: var(--feed-control) !important; border-radius:
|
|
407
|
+
background: var(--feed-control) !important; border-radius: var(--feed-radius-pill) !important; color: var(--foreground, inherit);
|
|
396
408
|
}
|
|
397
409
|
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] :is(button, [role=button])[aria-pressed=true]:not([class*="destructive"]) {
|
|
398
|
-
background: var(--primary) !important; color: var(--primary-foreground) !important; border-radius:
|
|
410
|
+
background: var(--primary) !important; color: var(--primary-foreground) !important; border-radius: var(--feed-radius-pill) !important;
|
|
399
411
|
}
|
|
400
|
-
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] :is(button, [role=button])[aria-pressed=true][class*="destructive"] { border-radius:
|
|
412
|
+
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] :is(button, [role=button])[aria-pressed=true][class*="destructive"] { border-radius: var(--feed-radius-pill) !important; }
|
|
401
413
|
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] :is(textarea, input:not([type=radio]):not([type=checkbox]), select, details, [data-cyclotron-choice]:not([data-predicted])) {
|
|
402
414
|
background: var(--feed-control) !important; border-radius: 16px !important;
|
|
403
415
|
}
|
|
404
|
-
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] [data-cyclotron-choice] { border-radius:
|
|
416
|
+
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] [data-cyclotron-choice] { border-radius: var(--feed-radius-pill) !important; }
|
|
405
417
|
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] [data-review-queue-bar] { background: var(--feed-footer) !important; }
|
|
406
418
|
/* The rating grid keeps its own rounded rectangles in the borderless panes (not the pill shape the flat rules give other buttons). */
|
|
407
|
-
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] [data-rating-grid] :is(button, [data-cyclotron-choice]) { border-radius:
|
|
419
|
+
[data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat][data-feed-flat] [data-rating-grid] :is(button, [data-cyclotron-choice]) { border-radius: var(--feed-radius-md) !important; }
|