feat(data): modernize SectionCard chrome (Batch 0 — Data overhaul)
Replace the tiny grey uppercase section titles with a modern header: optional leading icon in a soft chip, sentence-case high-contrast title, calm subtitle, a right-aligned rotating chevron, and optional statusDot/badge slots. API is unchanged (title/subtitle/collapsible/summary/storageKey/actions preserved) so no section internals change — purely the shared card chrome for the Data page. Build clean; client suite 46 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5aa5c0cc0e
commit
212117a61a
|
|
@ -1,6 +1,14 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
// At-a-glance health tones for the optional SectionCard status dot.
|
||||
const DOT_TONES = {
|
||||
green: 'bg-emerald-500',
|
||||
amber: 'bg-amber-500',
|
||||
red: 'bg-rose-500',
|
||||
gray: 'bg-muted-foreground/40',
|
||||
};
|
||||
|
||||
export function fmt(isoStr) {
|
||||
if (!isoStr) return '—';
|
||||
|
|
@ -23,6 +31,9 @@ export function importErrorState(err, fallback) {
|
|||
export function SectionCard({
|
||||
title,
|
||||
subtitle,
|
||||
icon: Icon, // optional lucide icon component, shown in a soft chip
|
||||
statusDot, // optional 'green' | 'amber' | 'red' | 'gray' health dot
|
||||
badge, // optional node rendered next to the title (e.g. a count pill)
|
||||
children,
|
||||
className,
|
||||
collapsible = false,
|
||||
|
|
@ -44,35 +55,44 @@ export function SectionCard({
|
|||
|
||||
const headerContent = (
|
||||
<>
|
||||
{collapsible && (
|
||||
open
|
||||
? <ChevronDown className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
: <ChevronRight className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
{Icon && (
|
||||
<span className="grid h-9 w-9 shrink-0 place-items-center rounded-lg border border-border/60 bg-muted/40 text-muted-foreground">
|
||||
<Icon className="h-5 w-5" />
|
||||
</span>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-xs font-bold uppercase tracking-widest text-muted-foreground">{title}</h2>
|
||||
{subtitle && <p className="mt-1 text-sm text-muted-foreground">{subtitle}</p>}
|
||||
<div className="flex items-center gap-2">
|
||||
{statusDot && <span className={cn('h-2 w-2 shrink-0 rounded-full', DOT_TONES[statusDot] || DOT_TONES.gray)} />}
|
||||
<h2 className="truncate text-base font-semibold text-foreground">{title}</h2>
|
||||
{badge}
|
||||
</div>
|
||||
{subtitle && <p className="mt-0.5 text-sm text-muted-foreground">{subtitle}</p>}
|
||||
{collapsible && !open && summary && (
|
||||
<p className="mt-1 truncate text-xs font-medium text-muted-foreground/80">{summary}</p>
|
||||
)}
|
||||
</div>
|
||||
{actions && <div className="shrink-0">{actions}</div>}
|
||||
{collapsible && (
|
||||
<ChevronDown
|
||||
className={cn('mt-1 h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200', open && 'rotate-180')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn('table-surface mb-6', className)}>
|
||||
<div className={cn('table-surface mb-5', className)}>
|
||||
{collapsible ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(value => !value)}
|
||||
className="flex w-full items-start gap-2 border-b border-border/50 px-6 py-4 text-left transition-colors hover:bg-muted/20"
|
||||
className="flex w-full items-start gap-3 border-b border-border/50 px-5 py-4 text-left transition-colors hover:bg-muted/20"
|
||||
aria-expanded={open}
|
||||
>
|
||||
{headerContent}
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-start gap-2 border-b border-border/50 px-6 py-4">
|
||||
<div className="flex items-start gap-3 border-b border-border/50 px-5 py-4">
|
||||
{headerContent}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue