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:
parent
5f039ea8cd
commit
0007408f14
2 changed files with 29 additions and 25 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Reference in a new issue