Refine Inbox with editorial hero and paper-toned queue ledger
InboxHeader: - Editorial dark hero replaces the 30px sticky header - Massive 'Inbox.' (clamp 48-80px) with ghost amber 'TRIAGE' watermark - Italic subtitle: 'Five lanes, one queue. N need eyes; N done today.' - Right column: SUN JUN 21 / 14:15 / local - Live status row: amber pulse + counts + date Inbox page: - Amber fold with '↘ FIVE LANES · ONE QUEUE ↙' between header and lanes - Lanes stay dark (working surface preserved) - New paper-toned Queue ledger panel below lanes: - Title block: 'The eye-flow, at a glance.' with need-eyes total - 5 paper tiles (Rejected / Payer rejected / Candidates / Unmatched / Done today) - Each tile shows lane accent dot + count + 'Clear'/'rows' chip - Loading and error states now also use the new InboxHeader for consistency - SummaryTile helper with paper-toned metric cards
This commit is contained in:
@@ -1,16 +1,15 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// InboxHeader
|
||||
//
|
||||
// Compact working-surface header: the day/date, a live clock, and the
|
||||
// two top-line counts ("N items need eyes" and "N done today") that
|
||||
// anchor the operator's day. "Need eyes" is computed by the Inbox
|
||||
// page (sum of actionable lanes) and passed in.
|
||||
//
|
||||
// SP14: payer_rejected (277CA) is now part of the actionable lanes —
|
||||
// it's a working-surface rejection that needs operator follow-up.
|
||||
// The Inbox page sums it into the needEyes count.
|
||||
// Editorial dark hero for the Inbox. Larger than the previous 30px title
|
||||
// so it carries weight against the bright lane surface below. "Inbox."
|
||||
// is the page's anchor, the day/date sits as a serif italic, and the
|
||||
// "N items need eyes / N done today" line replaces the small status
|
||||
// pulse with a more dramatic mono announcement.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { fmt } from "@/lib/format";
|
||||
|
||||
export function InboxHeader({
|
||||
needEyesCount,
|
||||
doneTodayCount,
|
||||
@@ -32,66 +31,152 @@ export function InboxHeader({
|
||||
|
||||
return (
|
||||
<header
|
||||
className="sticky top-0 z-10 px-6 pt-6 pb-4"
|
||||
className="sticky top-0 z-10 px-6 lg:px-10 pt-6 pb-5"
|
||||
style={{
|
||||
background: "var(--tt-bg)",
|
||||
borderBottom: "1px solid var(--tt-bg-elev)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-baseline justify-between gap-4">
|
||||
<div className="flex items-baseline gap-4">
|
||||
<h1
|
||||
className="display tracking-tight"
|
||||
style={{
|
||||
color: "var(--tt-ink)",
|
||||
fontSize: 30,
|
||||
lineHeight: 1,
|
||||
}}
|
||||
>
|
||||
Inbox
|
||||
</h1>
|
||||
<span
|
||||
className="mono uppercase"
|
||||
style={{
|
||||
color: "var(--tt-amber)",
|
||||
fontSize: 11,
|
||||
letterSpacing: "0.18em",
|
||||
}}
|
||||
>
|
||||
· {day} {date}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className="mono tabular-nums"
|
||||
<div className="relative">
|
||||
{/* Ghost "TRIAGE" watermark — a print-shop stamp behind the title. */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none select-none absolute inset-x-0 top-1/2 -translate-y-1/2 whitespace-nowrap display text-center"
|
||||
style={{
|
||||
color: "var(--tt-ink-dim)",
|
||||
fontSize: 12,
|
||||
letterSpacing: "0.05em",
|
||||
fontSize: "clamp(120px, 18vw, 260px)",
|
||||
letterSpacing: "-0.05em",
|
||||
opacity: 0.05,
|
||||
lineHeight: 1,
|
||||
color: "var(--tt-amber)",
|
||||
}}
|
||||
>
|
||||
{time}
|
||||
</span>
|
||||
TRIAGE
|
||||
</div>
|
||||
|
||||
<div className="relative flex items-end justify-between gap-6 flex-wrap">
|
||||
<div className="min-w-0 max-w-3xl">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
className="h-px w-14"
|
||||
style={{ backgroundColor: "var(--tt-amber)" }}
|
||||
/>
|
||||
<span
|
||||
className="mono uppercase"
|
||||
style={{
|
||||
color: "var(--tt-amber)",
|
||||
fontSize: 11,
|
||||
letterSpacing: "0.22em",
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Inbox · Working surface
|
||||
</span>
|
||||
</div>
|
||||
<h1
|
||||
className="display tracking-[-0.04em]"
|
||||
style={{
|
||||
color: "var(--tt-ink)",
|
||||
fontSize: "clamp(48px, 6vw, 80px)",
|
||||
lineHeight: 0.92,
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
Inbox.
|
||||
</h1>
|
||||
<p
|
||||
className="display italic mt-3"
|
||||
style={{
|
||||
color: "var(--tt-ink-dim)",
|
||||
fontSize: "clamp(15px, 1.4vw, 19px)",
|
||||
lineHeight: 1.4,
|
||||
maxWidth: "44ch",
|
||||
}}
|
||||
>
|
||||
Five lanes, one queue.{" "}
|
||||
<span style={{ color: "var(--tt-amber)" }}>
|
||||
{needEyesCount}
|
||||
</span>{" "}
|
||||
need eyes; {doneTodayCount} are done today.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex flex-col items-start lg:items-end gap-2"
|
||||
aria-label="Day and time"
|
||||
>
|
||||
<div
|
||||
className="mono uppercase"
|
||||
style={{
|
||||
color: "var(--tt-ink-dim)",
|
||||
fontSize: 11,
|
||||
letterSpacing: "0.22em",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{day} {date}
|
||||
</div>
|
||||
<div
|
||||
className="display tabular-nums"
|
||||
style={{
|
||||
color: "var(--tt-ink)",
|
||||
fontSize: 26,
|
||||
lineHeight: 1,
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
{time}
|
||||
</div>
|
||||
<div
|
||||
className="mono uppercase"
|
||||
style={{
|
||||
color: "var(--tt-ink-dim)",
|
||||
fontSize: 10,
|
||||
letterSpacing: "0.20em",
|
||||
}}
|
||||
>
|
||||
local
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live status row — a horizontal "ticker" of the lane counts so
|
||||
the operator can read the queue at a glance. */}
|
||||
<div
|
||||
className="relative mt-5 flex items-center gap-5 flex-wrap mono uppercase"
|
||||
style={{
|
||||
color: "var(--tt-ink-dim)",
|
||||
fontSize: 11,
|
||||
letterSpacing: "0.14em",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
aria-hidden
|
||||
className="inline-block w-1.5 h-1.5 rounded-full animate-pulse-dot"
|
||||
style={{ background: "var(--tt-amber)" }}
|
||||
/>
|
||||
<span>Live</span>
|
||||
</div>
|
||||
<span className="opacity-50">·</span>
|
||||
<span>
|
||||
<span style={{ color: "var(--tt-amber)", fontWeight: 600 }}>
|
||||
{needEyesCount}
|
||||
</span>{" "}
|
||||
need eyes
|
||||
</span>
|
||||
<span className="opacity-50">·</span>
|
||||
<span>
|
||||
<span style={{ color: "var(--tt-ink)", fontWeight: 600 }}>
|
||||
{doneTodayCount}
|
||||
</span>{" "}
|
||||
done today
|
||||
</span>
|
||||
<span className="opacity-50 hidden sm:inline">·</span>
|
||||
<span className="hidden sm:inline">
|
||||
{fmt.date(new Date().toISOString())}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
className="mono mt-2 flex items-center gap-2"
|
||||
style={{
|
||||
color: "var(--tt-ink-dim)",
|
||||
fontSize: 11,
|
||||
letterSpacing: "0.08em",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="inline-block w-1.5 h-1.5 rounded-full animate-pulse-dot"
|
||||
style={{ background: "var(--tt-amber)" }}
|
||||
aria-hidden
|
||||
/>
|
||||
<span style={{ color: "var(--tt-amber)", fontWeight: 600 }}>{needEyesCount}</span>
|
||||
items need eyes
|
||||
<span className="opacity-50">·</span>
|
||||
<span style={{ color: "var(--tt-ink)", fontWeight: 600 }}>{doneTodayCount}</span>
|
||||
done today
|
||||
</p>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
+259
-16
@@ -13,6 +13,7 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Lane, type LaneRow } from "@/components/inbox/Lane";
|
||||
import { InboxHeader } from "@/components/inbox/InboxHeader";
|
||||
import { BulkBar } from "@/components/inbox/BulkBar";
|
||||
@@ -160,15 +161,21 @@ export default function Inbox() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen p-8 mono flex items-center gap-2"
|
||||
style={{ background: "var(--tt-bg)", color: "var(--tt-ink-dim)", fontSize: 12 }}
|
||||
className="min-h-screen"
|
||||
style={{ background: "var(--tt-bg)", color: "var(--tt-ink)" }}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="inline-block h-1.5 w-1.5 rounded-full animate-pulse-dot"
|
||||
style={{ background: "var(--tt-amber)" }}
|
||||
/>
|
||||
loading inbox…
|
||||
<InboxHeader needEyesCount={0} doneTodayCount={0} />
|
||||
<div
|
||||
className="px-6 lg:px-10 py-10 mono flex items-center gap-2"
|
||||
style={{ color: "var(--tt-ink-dim)", fontSize: 12 }}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className="inline-block h-1.5 w-1.5 rounded-full animate-pulse-dot"
|
||||
style={{ background: "var(--tt-amber)" }}
|
||||
/>
|
||||
loading inbox…
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -176,13 +183,22 @@ export default function Inbox() {
|
||||
if (error) {
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen p-8 mono flex flex-col gap-2"
|
||||
style={{ background: "var(--tt-bg)", color: "var(--tt-oxblood)", fontSize: 12 }}
|
||||
className="min-h-screen"
|
||||
style={{ background: "var(--tt-bg)", color: "var(--tt-ink)" }}
|
||||
>
|
||||
<span className="uppercase tracking-[0.18em]" style={{ fontSize: 10, fontWeight: 700 }}>
|
||||
Connection error
|
||||
</span>
|
||||
<span>{error.message}</span>
|
||||
<InboxHeader needEyesCount={0} doneTodayCount={0} />
|
||||
<div
|
||||
className="px-6 lg:px-10 py-10 mono flex flex-col gap-2"
|
||||
style={{ color: "var(--tt-oxblood)", fontSize: 12 }}
|
||||
>
|
||||
<span
|
||||
className="uppercase tracking-[0.18em]"
|
||||
style={{ fontSize: 10, fontWeight: 700 }}
|
||||
>
|
||||
Connection error
|
||||
</span>
|
||||
<span>{error.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -203,7 +219,53 @@ export default function Inbox() {
|
||||
style={{ background: "var(--tt-bg)", color: "var(--tt-ink)" }}
|
||||
>
|
||||
<InboxHeader needEyesCount={needEyes} doneTodayCount={lanes.done_today.length} />
|
||||
<main className="px-6 pb-24 pt-4 flex gap-4 items-stretch flex-wrap">
|
||||
|
||||
{/* Fold — a thin amber rule + italic serif annotation that
|
||||
transitions the editorial header into the lane grid below. */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="relative h-10 flex items-center"
|
||||
style={{ background: "var(--tt-bg)" }}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-x-0 top-1/2 -translate-y-1/2 h-px"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(to right, transparent, var(--tt-amber) 12%, var(--tt-amber) 88%, transparent)",
|
||||
opacity: 0.35,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="relative z-10 mx-auto px-3 flex items-center gap-2"
|
||||
style={{ background: "var(--tt-bg)" }}
|
||||
>
|
||||
<span
|
||||
className="display italic"
|
||||
style={{ color: "var(--tt-amber)", fontSize: 14 }}
|
||||
>
|
||||
↘
|
||||
</span>
|
||||
<span
|
||||
className="mono uppercase"
|
||||
style={{
|
||||
color: "var(--tt-ink-dim)",
|
||||
fontSize: 10.5,
|
||||
letterSpacing: "0.24em",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Five lanes · one queue
|
||||
</span>
|
||||
<span
|
||||
className="display italic"
|
||||
style={{ color: "var(--tt-amber)", fontSize: 14 }}
|
||||
>
|
||||
↙
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main className="px-6 lg:px-10 pb-24 pt-4 flex gap-4 items-stretch flex-wrap">
|
||||
<Lane
|
||||
name="REJECTED"
|
||||
accent="oxblood"
|
||||
@@ -247,6 +309,103 @@ export default function Inbox() {
|
||||
/>
|
||||
</main>
|
||||
|
||||
{/* ----------------------------------------------------------------
|
||||
PAPER-TONED SUMMARY STRIP
|
||||
A cream "ledger" panel below the lanes that shows the aggregate
|
||||
eye-flow across the queue. Mirrors the "Magazine Spread" pattern
|
||||
used on the Dashboard/Claims/etc pages so the Inbox feels part
|
||||
of the same document set, not a stand-alone terminal.
|
||||
---------------------------------------------------------------- */}
|
||||
<section
|
||||
aria-label="Queue summary"
|
||||
className="relative mx-6 lg:mx-10 mt-6 mb-4"
|
||||
>
|
||||
<div
|
||||
className="relative rounded-xl overflow-hidden border"
|
||||
style={{
|
||||
backgroundColor: "hsl(var(--surface))",
|
||||
borderColor: "hsl(30 14% 14% / 0.10)",
|
||||
boxShadow:
|
||||
"inset 0 1px 0 0 hsl(0 0% 100% / 0.5), 0 1px 0 0 hsl(30 14% 22% / 0.06), 0 12px 40px -16px hsl(0 0% 0% / 0.45)",
|
||||
}}
|
||||
>
|
||||
{/* Title row */}
|
||||
<div
|
||||
className="px-5 lg:px-7 py-4 border-b flex items-center justify-between gap-4 flex-wrap"
|
||||
style={{ borderColor: "hsl(30 14% 14% / 0.10)" }}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="mono text-[10.5px] uppercase tracking-[0.20em] font-semibold mb-1.5"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
Queue ledger
|
||||
</div>
|
||||
<h2
|
||||
className="display leading-[0.95] tracking-[-0.03em]"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink))",
|
||||
fontSize: "clamp(22px, 2.2vw, 28px)",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
The eye-flow, <span className="italic">at a glance.</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="text-right"
|
||||
aria-label="Aggregate metrics"
|
||||
>
|
||||
<div
|
||||
className="mono text-[10px] uppercase tracking-[0.18em] font-semibold"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
Need eyes
|
||||
</div>
|
||||
<div
|
||||
className="display tabular-nums"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink))",
|
||||
fontSize: 24,
|
||||
lineHeight: 1.1,
|
||||
}}
|
||||
>
|
||||
{needEyes}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lane metrics row — paper tiles that mirror each lane's accent */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-5 divide-x divide-y lg:divide-y-0" style={{ borderColor: "hsl(30 14% 14% / 0.08)" }}>
|
||||
<SummaryTile
|
||||
label="Rejected"
|
||||
value={lanes.rejected.length}
|
||||
tone="oxblood"
|
||||
/>
|
||||
<SummaryTile
|
||||
label="Payer rejected"
|
||||
value={lanes.payer_rejected.length}
|
||||
tone="oxblood"
|
||||
/>
|
||||
<SummaryTile
|
||||
label="Candidates"
|
||||
value={lanes.candidates.length}
|
||||
tone="amber"
|
||||
/>
|
||||
<SummaryTile
|
||||
label="Unmatched"
|
||||
value={lanes.unmatched.length}
|
||||
tone="ink"
|
||||
/>
|
||||
<SummaryTile
|
||||
label="Done today"
|
||||
value={lanes.done_today.length}
|
||||
tone="muted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Per-lane bulk bars. Each shows when its lane has a selection. */}
|
||||
<BulkBar
|
||||
lane="rejected"
|
||||
@@ -307,7 +466,7 @@ export default function Inbox() {
|
||||
borderColor: "var(--tt-amber)",
|
||||
color: "var(--tt-ink)",
|
||||
boxShadow:
|
||||
"0 0 0 1px hsl(36 88% 56% / 0.18), 0 24px 60px -12px hsl(0 0% 0% / 0.7), inset 0 1px 0 0 hsl(0 0% 100% / 0.04)",
|
||||
"0 0 0 1px hsl(36 75% 58% / 0.18), 0 24px 60px -12px hsl(0 0% 0% / 0.5), inset 0 1px 0 0 hsl(0 0% 100% / 0.03)",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
@@ -404,3 +563,87 @@ export default function Inbox() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SummaryTile — paper-toned metric for the queue ledger strip. Mirrors
|
||||
// the lane accent (oxblood / amber / ink-blue / muted) so the eye-flow
|
||||
// reads at a glance.
|
||||
// ---------------------------------------------------------------------------
|
||||
const SUMMARY_ACCENTS: Record<
|
||||
"oxblood" | "amber" | "ink" | "muted",
|
||||
{ dot: string; tint: string; text: string }
|
||||
> = {
|
||||
oxblood: {
|
||||
dot: "hsl(358 70% 42%)",
|
||||
tint: "hsl(358 70% 92%)",
|
||||
text: "hsl(358 70% 30%)",
|
||||
},
|
||||
amber: {
|
||||
dot: "hsl(36 92% 50%)",
|
||||
tint: "hsl(36 82% 88%)",
|
||||
text: "hsl(36 92% 30%)",
|
||||
},
|
||||
ink: {
|
||||
dot: "hsl(212 80% 45%)",
|
||||
tint: "hsl(212 85% 92%)",
|
||||
text: "hsl(212 80% 30%)",
|
||||
},
|
||||
muted: {
|
||||
dot: "hsl(30 8% 50%)",
|
||||
tint: "hsl(36 22% 90%)",
|
||||
text: "hsl(30 8% 30%)",
|
||||
},
|
||||
};
|
||||
|
||||
function SummaryTile({
|
||||
label,
|
||||
value,
|
||||
tone,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
tone: "oxblood" | "amber" | "ink" | "muted";
|
||||
}) {
|
||||
const a = SUMMARY_ACCENTS[tone];
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative px-4 py-4 first:pl-5 lg:first:pl-7 last:pr-5 lg:last:pr-7"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span
|
||||
aria-hidden
|
||||
className="block h-1.5 w-1.5 rounded-full"
|
||||
style={{ backgroundColor: a.dot }}
|
||||
/>
|
||||
<span
|
||||
className="mono text-[10px] uppercase tracking-[0.20em] font-semibold"
|
||||
style={{ color: "hsl(var(--surface-ink-3))" }}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="display tabular-nums tracking-[-0.03em]"
|
||||
style={{
|
||||
color: "hsl(var(--surface-ink))",
|
||||
fontSize: "clamp(24px, 2vw, 30px)",
|
||||
lineHeight: 1,
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
<div
|
||||
className="mono text-[10px] uppercase tracking-[0.16em] mt-1.5 inline-flex items-center gap-1 px-1.5 py-0.5 rounded-sm"
|
||||
style={{
|
||||
color: a.text,
|
||||
backgroundColor: a.tint,
|
||||
}}
|
||||
>
|
||||
{value === 0 ? "Clear" : value === 1 ? "row" : "rows"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user