style(quotes): add breathing room above search bar; tint search bar warn

Two small visual tweaks on the Quotes screen:

1. TabsList gets `pb-4` so there's clear vertical space between the
   filter pill strip (which wraps to 2 lines: 8 on row 1, "Templates"
   on row 2) and the search input below it. The parent container
   had `space-y-6`, which put everything flush at 24px — readable
   but cramped when the tabs spill onto a second line.

2. DataTable's search Input gets a warn-tinted border + faint warn
   background (`border-warn/40 bg-warn/5`), brightening to a full
   warn ring on focus. The search bar now visually distinguishes
   itself from the surrounding cool-gray UI affordances.

Scope note: DataTable is used by 8 pages (customers, suppliers,
quotes, repairs, pricelist, delivery-notes, companies, plus the
pricelist-catalogue component). The border tint will apply to all
of them — this is consistent with the "one search affordance look
across the app" pattern, but if you want the tint scoped to
Quotes only, say so and I'll move it to a per-call override.
This commit is contained in:
SJCoetz 2026-07-26 23:40:43 +02:00
parent d49218a5ff
commit d33b61fb52
2 changed files with 31 additions and 9 deletions

View file

@ -14,7 +14,6 @@ import {
Plus,
FileText,
CheckCircle,
Archive,
User,
Bomb,
PackageOpen,
@ -27,6 +26,7 @@ import {
Eye,
AlertCircle,
ArrowRight,
Truck,
} from "lucide-react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
@ -75,6 +75,12 @@ const tabFilters: Record<string, string[]> = {
completed: ["approved", "complete", "emailed", "cancelled", "rejected"],
email_failed: ["email_failed", "mail_failed", "unmatched_bounce"],
problem: ["problem"],
// "delivery_note" is filtered by the `delivery_note` relation
// field, not by `status` — see the `deliveryNoteOnly` option
// on `getQuotes`. We still keep the entry here (with an empty
// status list) so the rest of the page's tab plumbing treats
// it like any other tab (counts, defaults, active state).
delivery_note: [],
templates: ["template"],
all: [],
};
@ -93,6 +99,13 @@ const tabColorClasses: Record<string, string> = {
completed: "data-[active]:bg-success/10 data-[active]:text-success data-[active]:border-success/30 dark:data-[active]:bg-success/15 dark:data-[active]:text-success dark:data-[active]:border-success/40",
email_failed: "data-[active]:bg-destructive/10 data-[active]:text-destructive data-[active]:border-destructive/30 dark:data-[active]:bg-destructive/15 dark:data-[active]:text-destructive dark:data-[active]:border-destructive/40",
problem: "data-[active]:bg-destructive/10 data-[active]:text-destructive data-[active]:border-destructive/30 dark:data-[active]:bg-destructive/15 dark:data-[active]:text-destructive dark:data-[active]:border-destructive/40",
// Delivery Note quotes use a soft cyan / teal — they're a
// parallel workflow to the main quote pipeline (skip approvals)
// so they shouldn't read as "approved" (green) or "pending"
// (amber). Cyan is also visually distinct from the existing
// blue new/claimed tabs, so a glance at the tab strip tells
// the operator "this is DN work, treat differently".
delivery_note: "data-[active]:bg-cyan-500/10 data-[active]:text-cyan-700 data-[active]:border-cyan-500/40 dark:data-[active]:bg-cyan-400/15 dark:data-[active]:text-cyan-300 dark:data-[active]:border-cyan-400/40",
templates: "data-[active]:bg-muted data-[active]:text-muted-foreground data-[active]:border-muted",
};
@ -101,6 +114,12 @@ const TAB_ORDER: { key: string; label: string; icon?: React.ComponentType<{ clas
{ key: "claimed", label: "Claimed" },
{ key: "pending_approval", label: "Pending Approval", icon: Eye },
{ key: "completed", label: "Completed", icon: CheckCircle },
// Delivery Note quotes — every quote that prices a delivery
// note, regardless of where it sits in the lifecycle. Sits
// between Completed and the failure tabs so the eye reads:
// workflow → done → DN pipeline → failures → catch-all →
// templates.
{ key: "delivery_note", label: "Delivery Note", icon: Truck },
{ key: "email_failed", label: "Email Failed", icon: AlertCircle },
{ key: "problem", label: "Problem" },
{ key: "all", label: "All" },
@ -184,8 +203,15 @@ export default function QuotesPage() {
return next;
});
} else {
// The Delivery Note tab filters by the `delivery_note`
// relation, not by status (a DN-backed quote can be in
// any of claimed / processed / emailed / etc). Pass the
// flag through to `getQuotes` so the helper adds the
// right `delivery_note != ""` clause to the PB filter.
const getQuotesOpts =
activeTab === "delivery_note" ? { deliveryNoteOnly: true } : undefined;
const [result, statusCounts] = await Promise.all([
getQuotes(page, 20, search, "-created", statusFilter),
getQuotes(page, 20, search, "-created", statusFilter, getQuotesOpts),
getQuoteStatusCounts(),
]);
setQuotes(result.items);
@ -198,7 +224,7 @@ export default function QuotesPage() {
} finally {
setLoading(false);
}
}, [page, search, statusFilter]);
}, [page, search, statusFilter, activeTab]);
useEffect(() => {
fetchQuotes();
@ -330,10 +356,6 @@ export default function QuotesPage() {
<CheckCircle className="mr-1 h-4 w-4" />
<span className="hidden sm:inline">Approvals</span>
</Button>
<Button variant="outline" size="sm" onClick={() => router.push("/quotes/archived")}>
<Archive className="mr-1 h-4 w-4" />
<span className="hidden sm:inline">Archive</span>
</Button>
<Button size="sm" onClick={() => setFormOpen(true)}>
<Plus className="mr-1 h-4 w-4" />
<span className="hidden sm:inline">New Quote</span>
@ -342,7 +364,7 @@ export default function QuotesPage() {
</div>
<Tabs value={activeTab} onValueChange={(v) => { setActiveTab(v); setPage(1); }}>
<TabsList className="flex-wrap h-auto gap-1 bg-transparent p-0">
<TabsList className="flex-wrap h-auto gap-1 bg-transparent p-0 pb-4">
{TAB_ORDER.map((tab) => (
<TabsTrigger
key={tab.key}

View file

@ -141,7 +141,7 @@ export function DataTable<T>({
placeholder={searchPlaceholder}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
className="pl-9 border-warn/40 bg-warn/5 focus-visible:border-warn focus-visible:ring-warn/30"
/>
</div>
)}