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:
Tyler
2026-06-20 22:27:01 -06:00
parent 02841d7e6e
commit fbe9940a3f
70 changed files with 2582 additions and 1663 deletions
+110 -48
View File
@@ -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>
);
}