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:
+15
-38
@@ -4,6 +4,7 @@ import { useActivity } from "@/hooks/useActivity";
|
||||
import { useTailStream } from "@/hooks/useTailStream";
|
||||
import { useMergedTail } from "@/hooks/useMergedTail";
|
||||
import { TailStatusPill } from "@/components/TailStatusPill";
|
||||
import { PageHeader } from "@/components/PageHeader";
|
||||
import { ActivityFeed } from "@/components/ActivityFeed";
|
||||
import { ActivityFilters, type SinceValue } from "@/components/ActivityFilters";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -22,12 +23,6 @@ const VALID_KINDS: readonly ActivityKind[] = [
|
||||
|
||||
const VALID_SINCE: readonly SinceValue[] = ["1h", "24h", "7d", "all"];
|
||||
|
||||
/**
|
||||
* Convert a relative "since" window into an ISO timestamp string for the
|
||||
* backend's lexicographic `timestamp >= since` comparison. Memoized on
|
||||
* `since` so the queryKey stays stable across re-renders (otherwise the
|
||||
* refetch would fire on every Date.now() tick).
|
||||
*/
|
||||
function useSinceIso(since: SinceValue): string | undefined {
|
||||
return useMemo(() => {
|
||||
const now = Date.now();
|
||||
@@ -41,8 +36,6 @@ function useSinceIso(since: SinceValue): string | undefined {
|
||||
export function ActivityLog() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
// URL → filter state. `searchParams` is stable as long as the URL is,
|
||||
// so these are computed cheaply with useMemo.
|
||||
const selectedKinds = useMemo<ActivityKind[]>(
|
||||
() =>
|
||||
searchParams
|
||||
@@ -58,10 +51,6 @@ export function ActivityLog() {
|
||||
? (sinceRaw as SinceValue)
|
||||
: "all";
|
||||
|
||||
// Backend's `/api/activity` only accepts a single `kind` query param, so
|
||||
// for multi-select we fetch the unfiltered set and filter client-side.
|
||||
// Single-kind selections pass through to the backend (cheaper + benefits
|
||||
// from any future server-side filtering).
|
||||
const apiKind = selectedKinds.length === 1 ? selectedKinds[0] : undefined;
|
||||
const sinceIso = useSinceIso(since);
|
||||
|
||||
@@ -73,9 +62,6 @@ export function ActivityLog() {
|
||||
|
||||
const allItems = data?.items ?? [];
|
||||
|
||||
// SP5: live-tail wiring. `useMergedTail` appends tail-arriving events
|
||||
// to the base query result, then we apply the same client-side
|
||||
// multi-kind filter so live events respect the active filter set.
|
||||
const { status: tailStatus, lastEventAt: tailLastEventAt, forceReconnect } =
|
||||
useTailStream("activity");
|
||||
const merged = useMergedTail("activity", allItems);
|
||||
@@ -128,18 +114,19 @@ export function ActivityLog() {
|
||||
const hasFilters = selectedKinds.length > 0 || since !== "all";
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-fade-in">
|
||||
<header>
|
||||
<div className="text-[10.5px] font-semibold uppercase tracking-[0.18em] text-muted-foreground mb-2 flex items-center gap-2">
|
||||
<span className="inline-block h-px w-6 bg-border" />
|
||||
Activity
|
||||
</div>
|
||||
<h1 className="text-[22px] font-semibold tracking-tight">Activity log</h1>
|
||||
<p className="text-muted-foreground mt-1.5 text-[14px]">
|
||||
Every claim submission, denial, payment, and provider event, in
|
||||
reverse chronological order. Auto-refreshes every 30s.
|
||||
</p>
|
||||
</header>
|
||||
<div className="space-y-6 lg:space-y-8 animate-fade-in">
|
||||
<PageHeader
|
||||
eyebrow="Activity"
|
||||
title={<>Activity <span className="display italic text-muted-foreground">log</span></>}
|
||||
subtitle="Every claim submission, denial, payment, and provider event, in reverse chronological order. Auto-refreshes every 30s."
|
||||
actions={
|
||||
<TailStatusPill
|
||||
status={tailStatus}
|
||||
lastEventAt={tailLastEventAt}
|
||||
onReconnect={forceReconnect}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="surface rounded-xl p-4">
|
||||
<ActivityFilters
|
||||
@@ -160,16 +147,6 @@ export function ActivityLog() {
|
||||
) : null}
|
||||
|
||||
<div className="surface rounded-xl p-6">
|
||||
<div className="flex flex-wrap items-center gap-3 mb-4">
|
||||
{/* SP5: live-tail status pill, right-aligned in the toolbar. */}
|
||||
<div className="ml-auto">
|
||||
<TailStatusPill
|
||||
status={tailStatus}
|
||||
lastEventAt={tailLastEventAt}
|
||||
onReconnect={forceReconnect}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
@@ -191,4 +168,4 @@ export function ActivityLog() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user