style(popover,modal): warn-toned AI card + warning-bordered modal

Popover (the ⓘ AI snapshot card):
- Switch from raw amber-200/amber-50 to the project's warn theme
  tokens so it matches the rest of the design system and respects
  dark mode.
- Subtle gradient backgrounds (from-warn/10 to-warn/5) so each card
  reads as a distinct section without screaming for attention.
- Icons now sit in coloured chips (rounded-md bg-warn/20 etc.) for
  better visual hierarchy.
- Freshness badges are pill-shaped with an inset ring instead of a
  flat square — softer.
- Fuzzy-match warning box uses warn tokens too so it doesn't clash
  with the parent card.
- Supplier / quote rows get a faint hover background and tighter
  spacing for easier scanning.

Process modal:
- Outer Card border switched from border-border/60 to
  border-2 border-warn/40 with a warm shadow-warn/15. The whole
  modal now pops on the dimmed background — fits the "stand out
  orange/yellow" ask.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
SJCoetz 2026-07-30 14:12:59 +02:00
parent 5f039ea8cd
commit 0007408f14
2 changed files with 29 additions and 25 deletions

View file

@ -57,13 +57,13 @@ export function ItemHistoryPopover({ data, loading, error }: Props) {
);
}
function FreshnessBadge({ label, ts, variant }: { label: string; ts: string | null; variant: "indigo" | "amber" }) {
function FreshnessBadge({ label, ts, variant }: { label: string; ts: string | null; variant: "indigo" | "warn" }) {
const cls =
variant === "indigo"
? "bg-indigo-500/10 text-indigo-700 dark:text-indigo-300"
: "bg-amber-500/10 text-amber-700 dark:text-amber-300";
? "bg-indigo-500/15 text-indigo-700 dark:text-indigo-300 ring-1 ring-inset ring-indigo-500/20"
: "bg-warn/15 text-warn-foreground ring-1 ring-inset ring-warn/25";
return (
<span className={cn("inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium", cls)}>
<span className={cn("inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium", cls)}>
{label} {ts ? relTime(ts) : ""}
</span>
);
@ -72,7 +72,7 @@ function FreshnessBadge({ label, ts, variant }: { label: string; ts: string | nu
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="space-y-1">
<div className="text-[10px] font-bold uppercase tracking-wide text-muted-foreground">{title}</div>
<div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground/80">{title}</div>
<div className="text-xs">{children}</div>
</div>
);
@ -87,34 +87,36 @@ function DeterministicCard({
}) {
const fuzzy = data.match.match_kind === "description_fuzzy";
return (
<div className="rounded-lg border border-indigo-200 bg-indigo-50/40 p-2.5 space-y-2">
<div className="rounded-lg border border-indigo-500/20 bg-gradient-to-b from-indigo-50/60 to-indigo-50/20 p-3 space-y-2.5 shadow-sm shadow-indigo-500/5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<TrendingUp className="h-3.5 w-3.5 text-indigo-700" />
<span className="text-xs font-semibold text-indigo-700">PB-derived</span>
<span className="inline-flex h-5 w-5 items-center justify-center rounded-md bg-indigo-500/15">
<TrendingUp className="h-3 w-3 text-indigo-700 dark:text-indigo-300" />
</span>
<span className="text-xs font-semibold text-indigo-700 dark:text-indigo-300">PB-derived</span>
</div>
<FreshnessBadge label="fresh" ts={data.generated_at} variant="indigo" />
</div>
{fuzzy && (
<div className="rounded border border-amber-300 bg-amber-50 px-2 py-1 text-[10px] text-amber-900">
<div className="rounded-md border border-warn/30 bg-warn/10 px-2 py-1 text-[10px] text-warn-foreground ring-1 ring-inset ring-warn/15">
⚠ best-guess match · {Math.round(data.match.match_confidence * 100)}% confidence
</div>
)}
{data.cheapest && (
<div className="text-xs font-bold text-emerald-700">
<div className="text-xs font-semibold text-emerald-700 dark:text-emerald-400">
↓ Cheapest: {data.cheapest.supplier_name} R{data.cheapest.last_cost}
</div>
)}
{data.suppliers.length > 0 && (
<Section title={`We buy this from (${data.suppliers.length})`}>
<ul className="space-y-0.5">
<ul className="space-y-1">
{data.suppliers.map((s) => (
<li key={s.supplier_id} className="flex items-center justify-between gap-2">
<li key={s.supplier_id} className="flex items-center justify-between gap-2 rounded px-1.5 py-1 hover:bg-white/40">
<span className="font-medium">{s.supplier_name}</span>
<span className="tabular-nums text-emerald-700">R{s.last_cost}</span>
<span className="tabular-nums font-medium text-emerald-700 dark:text-emerald-400">R{s.last_cost}</span>
<span className="text-muted-foreground text-[10px]">{relTime(s.last_bought_at)} (×{s.times_bought})</span>
</li>
))}
@ -124,9 +126,9 @@ function DeterministicCard({
{data.other_quotes.length > 0 && (
<Section title={`Other quotes (${data.other_quotes.length})`}>
<ul className="space-y-0.5">
<ul className="space-y-1">
{data.other_quotes.map((q) => (
<li key={q.quote_id} className="flex items-center justify-between gap-2">
<li key={q.quote_id} className="flex items-center justify-between gap-2 rounded px-1.5 py-1 hover:bg-white/40">
<span className="font-mono text-[11px]">{q.quote_number}</span>
<span className="tabular-nums">R{q.selling_price}</span>
<span className="text-muted-foreground text-[10px]">{q.status} · {q.days_ago}d</span>
@ -159,21 +161,23 @@ function AiCard({ data, updatedAt }: { data: Record<string, unknown>; updatedAt:
// a `summary` (or any *_summary / first long string) as the headline
// and turns named arrays (top_historical_matches with
// matched_description, etc.) into structured rows. We only wrap it
// here to keep the amber framing + freshness badge + Raw data
// fallback that operators expect.
// here to keep the warm warn-toned framing + freshness badge +
// Raw data fallback that operators expect.
return (
<div className="rounded-lg border border-amber-200 bg-amber-50/40 p-2.5 space-y-2">
<div className="rounded-lg border border-warn/25 bg-gradient-to-b from-warn/10 to-warn/5 p-3 space-y-2.5 shadow-sm shadow-warn/5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1.5">
<Info className="h-3.5 w-3.5 text-amber-700" />
<span className="text-xs font-semibold text-amber-700">AI snapshot</span>
<span className="inline-flex h-5 w-5 items-center justify-center rounded-md bg-warn/20">
<Info className="h-3 w-3 text-warn-foreground" />
</span>
<span className="text-xs font-semibold text-warn-foreground">AI snapshot</span>
</div>
<FreshnessBadge label="snapshot" ts={updatedAt} variant="amber" />
<FreshnessBadge label="snapshot" ts={updatedAt} variant="warn" />
</div>
<AiInsightsCard insights={data} variant="popover" title="" className="border-0 bg-transparent p-0" />
<details className="text-[10px] text-muted-foreground">
<summary className="cursor-pointer">Raw AI data</summary>
<pre className="mt-1 max-h-32 overflow-auto rounded bg-muted/40 p-1 text-[10px]">
<details className="text-[10px] text-muted-foreground group">
<summary className="cursor-pointer hover:text-foreground transition-colors">Raw AI data</summary>
<pre className="mt-1.5 max-h-32 overflow-auto rounded-md bg-muted/60 p-2 text-[10px] ring-1 ring-inset ring-border/40">
{JSON.stringify(data, null, 2)}
</pre>
</details>

View file

@ -303,7 +303,7 @@ export function OrderSprintProcessModal({ item, suppliers, open, onClose, onProc
aria-modal="true"
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
>
<Card className="w-full max-w-4xl max-h-[90vh] flex flex-col shadow-2xl border-border/60">
<Card className="w-full max-w-4xl max-h-[90vh] flex flex-col shadow-2xl border-2 border-warn/40 shadow-warn/15">
<CardHeader className="flex flex-row items-start justify-between gap-4 space-y-0 border-b bg-gradient-to-b from-muted/40 to-transparent">
<div className="min-w-0 flex-1">
<CardTitle className="text-sm flex items-center gap-2">