feat(ui): cohesive frontend polish — design system + per-screen refinement
Distill the UI into a single, recognizable voice: a precision instrument for one operator on one machine. Bloomberg-coded chrome, warm-paper detail surfaces, mono-heavy numerics, with one editorial serif accent for moments of weight. Design system - Three-font stack: Geist Sans (UI), Geist Mono (data), Instrument Serif (editorial display). No Inter, no system fonts. - Two surfaces: dark chrome (--background, --accent, --signal) and warm paper detail surfaces (--surface, --surface-ink*). - Inbox has its own Ticker Tape terminal palette (--tt-*). - Shared component classes: .eyebrow, .mono, .display, .surface, .surface-2, .row-hover, .nav-active, .kbd, .editorial, .hairline. - --m-* token aliases for the legacy drawer components so test- asserted class strings keep resolving to the same hue family. Drawers (Claim + Remit) - Editorial display face for totals (paid/adjustment amounts). - Color-coded money tiles: green-tinted paid card, amber-tinted adjustment card when non-zero, muted otherwise. - Tabs get accent-blue underline + CSS-driven active state. - Validation banners with proper background opacity + ring-around- dot success badge. - StateHistoryTimeline: dashed border, ring around dots, ↳ prefix for remit ids. - DiagnosesList, PartiesGrid, MatchedRemitCard, CasAdjustmentsPanel: refined typography, dashed dividers, italic descriptions, mono amounts, font-semibold totals, hover row tints. Inbox Ticker Tape - Custom RowCheckbox with sr-only input + amber accent. - Alternating row striping, hover tints, accent rail with inset shadow on selection. - Refined sparkline with glow at high values. - BulkBar: bottom-floating bar with amber count chip, larger shadow. - CandidateBreakdown: animated progress bars with amber gradient. - InboxHeader: Instrument Serif headline, mono day/date stamp, pulsing amber status dot. Dialogs & search - NewClaimDialog: editorial title, mono NPI/CPT/amount fields. - SearchBar: refined input row with mono, footer with pulsing loading indicator. - KeyboardCheatsheet: monogram icon chip, hover row states, refined eyebrow header. Primitives - StatusBadge / ClaimStateBadge: per-state dot indicators. - SelectItem: data-[highlighted]:outline tokens for keyboard a11y. - Table primitives: refined header treatment, hover/focus states. Tests + build - 354/354 tests passing across 59 files. - Vite build clean (53.84 kB CSS / 560.86 kB JS). - Eyebrow assertions updated to match the consolidated .eyebrow class (intact visual contract, abstracted class string). - Badge variant tokens updated to the polished bg-muted/80 / /0.14 opacity scale.
This commit is contained in:
+110
-48
@@ -24,20 +24,45 @@ const nav = [
|
||||
{ to: "/activity", label: "Activity Log", icon: Activity },
|
||||
];
|
||||
|
||||
/**
|
||||
* Sidebar. Three sections under the brand mark:
|
||||
* - WORKSPACE — the day-to-day surface
|
||||
* - WORKFLOWS — triage & reconciliation lanes
|
||||
* - REFERENCE — long-tail batches & acks
|
||||
*
|
||||
* The active state uses a 2px left accent + an inset tint + a
|
||||
* subtle inner ring so the selected item reads instantly. Counts
|
||||
* (e.g. unmatched on Reconciliation) live as mono digits aligned to
|
||||
* the right edge so the eye can scan state at a glance.
|
||||
*/
|
||||
export function Sidebar() {
|
||||
// Surface the unmatched-bucket size on the Reconciliation nav entry so the
|
||||
// operator notices when manual reconciliation is needed.
|
||||
const { unmatched } = useReconciliation();
|
||||
const unmatchedCount =
|
||||
(unmatched.data?.claims.length ?? 0) +
|
||||
(unmatched.data?.remittances.length ?? 0);
|
||||
|
||||
return (
|
||||
<aside className="hidden md:flex md:w-60 md:flex-col md:fixed md:inset-y-0 z-30 border-r border-border/60 bg-sidebar/60 backdrop-blur-xl">
|
||||
<div className="flex h-16 items-center px-6 border-b border-border/60">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="h-7 w-7 rounded-md bg-accent flex items-center justify-center shadow-sm">
|
||||
<svg viewBox="0 0 24 24" className="h-3.5 w-3.5 text-white" fill="none">
|
||||
<aside
|
||||
className="hidden md:flex md:w-60 md:flex-col md:fixed md:inset-y-0 z-30 border-r border-border/60 bg-sidebar/80 backdrop-blur-xl"
|
||||
data-print="hide"
|
||||
>
|
||||
{/* Brand mark — the editorial display face carries the wordmark
|
||||
while a small monospaced slug below keeps the brand
|
||||
unmistakably "machine". */}
|
||||
<div className="flex h-16 items-center px-5 border-b border-sidebar-border/80">
|
||||
<NavLink
|
||||
to="/"
|
||||
end
|
||||
className="flex items-center gap-2.5 group"
|
||||
aria-label="Cyclone — go to dashboard"
|
||||
>
|
||||
<div className="relative h-7 w-7 rounded-md bg-accent flex items-center justify-center shadow-[0_0_0_1px_hsl(var(--accent)/0.4),0_0_18px_-2px_hsl(var(--accent)/0.5)]">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className="h-3.5 w-3.5 text-white"
|
||||
fill="none"
|
||||
aria-hidden
|
||||
>
|
||||
<path
|
||||
d="M6 8h8a3 3 0 0 1 0 6h-5a3 3 0 0 0 0 6h9"
|
||||
stroke="currentColor"
|
||||
@@ -48,59 +73,47 @@ export function Sidebar() {
|
||||
</svg>
|
||||
</div>
|
||||
<div className="leading-none">
|
||||
<div className="text-[16px] font-semibold tracking-tight text-foreground">
|
||||
<div className="display text-[19px] tracking-tight text-foreground -mt-px">
|
||||
Cyclone
|
||||
</div>
|
||||
<div className="text-[10px] uppercase tracking-[0.14em] text-muted-foreground mt-0.5">
|
||||
<div className="mono text-[9.5px] uppercase tracking-[0.18em] text-muted-foreground mt-1">
|
||||
CuNtx · Claims
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 px-3 py-6">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground px-3 mb-2">
|
||||
Workspace
|
||||
</div>
|
||||
<ul className="space-y-0.5">
|
||||
<nav className="flex-1 overflow-y-auto px-3 py-5">
|
||||
<SectionLabel>Workspace</SectionLabel>
|
||||
<ul className="space-y-0.5 mb-5">
|
||||
{nav.map((item) => (
|
||||
<li key={item.to}>
|
||||
<NavLink
|
||||
to={item.to}
|
||||
end={item.end}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"group relative flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
||||
isActive
|
||||
? "nav-active"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
||||
)
|
||||
}
|
||||
>
|
||||
<item.icon className="h-4 w-4" strokeWidth={1.75} />
|
||||
<span>{item.label}</span>
|
||||
</NavLink>
|
||||
<NavItem item={item} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<SectionLabel>Workflows</SectionLabel>
|
||||
<ul className="space-y-0.5 mb-5">
|
||||
<li>
|
||||
<NavLink
|
||||
to="/reconciliation"
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"group relative flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
||||
"group relative flex items-center gap-3 rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors",
|
||||
isActive
|
||||
? "nav-active"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
||||
)
|
||||
}
|
||||
>
|
||||
<GitMerge className="h-4 w-4" strokeWidth={1.5} />
|
||||
<GitMerge className="h-3.5 w-3.5" strokeWidth={1.5} />
|
||||
<span>Reconciliation</span>
|
||||
{unmatchedCount > 0 && (
|
||||
<span className="ml-auto text-[10px] font-mono tabular-nums text-warning">
|
||||
{unmatchedCount > 0 ? (
|
||||
<span className="ml-auto mono text-[10px] text-signal">
|
||||
{unmatchedCount > 99 ? "99+" : unmatchedCount}
|
||||
</span>
|
||||
)}
|
||||
) : null}
|
||||
</NavLink>
|
||||
</li>
|
||||
<li>
|
||||
@@ -108,30 +121,34 @@ export function Sidebar() {
|
||||
to="/inbox"
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"group relative flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
||||
"group relative flex items-center gap-3 rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors",
|
||||
isActive
|
||||
? "nav-active"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
||||
)
|
||||
}
|
||||
>
|
||||
<InboxIcon className="h-4 w-4" strokeWidth={1.5} />
|
||||
<InboxIcon className="h-3.5 w-3.5" strokeWidth={1.5} />
|
||||
<span>Inbox</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<SectionLabel>Reference</SectionLabel>
|
||||
<ul className="space-y-0.5">
|
||||
<li>
|
||||
<NavLink
|
||||
to="/acks"
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"group relative flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
||||
"group relative flex items-center gap-3 rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors",
|
||||
isActive
|
||||
? "nav-active"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
||||
)
|
||||
}
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4" strokeWidth={1.5} />
|
||||
<CheckCircle2 className="h-3.5 w-3.5" strokeWidth={1.5} />
|
||||
<span>999 ACKs</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
@@ -140,14 +157,14 @@ export function Sidebar() {
|
||||
to="/batches"
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"group relative flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
||||
"group relative flex items-center gap-3 rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors",
|
||||
isActive
|
||||
? "nav-active"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
||||
)
|
||||
}
|
||||
>
|
||||
<Layers className="h-4 w-4" strokeWidth={1.5} />
|
||||
<Layers className="h-3.5 w-3.5" strokeWidth={1.5} />
|
||||
<span>Batches</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
@@ -156,31 +173,76 @@ export function Sidebar() {
|
||||
to="/batch-diff"
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"group relative flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
|
||||
"group relative flex items-center gap-3 rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors",
|
||||
isActive
|
||||
? "nav-active"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
||||
)
|
||||
}
|
||||
>
|
||||
<GitCompareArrows className="h-4 w-4" strokeWidth={1.5} />
|
||||
<GitCompareArrows className="h-3.5 w-3.5" strokeWidth={1.5} />
|
||||
<span>Batch diff</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div className="px-3 py-4 border-t border-border/60">
|
||||
{/* Operator card — always at the bottom. Ringed, mono id, the
|
||||
account role under the name. */}
|
||||
<div className="px-3 py-4 border-t border-sidebar-border/80">
|
||||
<div className="flex items-center gap-3 rounded-md p-2 hover:bg-muted/40 cursor-default">
|
||||
<div className="h-8 w-8 rounded-full bg-accent flex items-center justify-center text-[11px] font-semibold text-white ring-1 ring-inset ring-white/10">
|
||||
<div className="relative h-8 w-8 rounded-full bg-accent/20 ring-1 ring-inset ring-accent/40 flex items-center justify-center text-[10.5px] font-semibold text-accent mono">
|
||||
JK
|
||||
<span className="absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full bg-[hsl(var(--success))] ring-2 ring-sidebar" />
|
||||
</div>
|
||||
<div className="leading-tight">
|
||||
<div className="text-sm font-medium">Jordan K.</div>
|
||||
<div className="text-[11px] text-muted-foreground">Administrator</div>
|
||||
<div className="leading-tight min-w-0">
|
||||
<div className="text-[13px] font-medium truncate">Jordan K.</div>
|
||||
<div className="mono text-[10px] text-muted-foreground tracking-wider uppercase">
|
||||
Administrator
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionLabel({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="px-3 mb-1.5 eyebrow text-muted-foreground/60">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavItem({
|
||||
item,
|
||||
}: {
|
||||
item: {
|
||||
to: string;
|
||||
label: string;
|
||||
// Lucide icons accept `strokeWidth` as a wider union (string | number | null | undefined);
|
||||
// we declare it as `number | string` so any Lucide icon can be assigned
|
||||
// here without the prop-types assignability complaint from TS.
|
||||
icon: React.ComponentType<{ className?: string; strokeWidth?: number | string }>;
|
||||
end?: boolean;
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<NavLink
|
||||
to={item.to}
|
||||
end={item.end}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"group relative flex items-center gap-3 rounded-md px-3 py-1.5 text-[13px] font-medium transition-colors",
|
||||
isActive
|
||||
? "nav-active"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
||||
)
|
||||
}
|
||||
>
|
||||
<item.icon className="h-3.5 w-3.5" strokeWidth={1.5} />
|
||||
<span>{item.label}</span>
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user