feat(admin): apply new design system to Products pages
- List page: new PageHeader with 'OPERATIONS' eyebrow + 'Add product' CTA - New/Edit pages: PageHeader with eyebrow, icon, status badge (edit) - ProductsClient: EmptyState for zero products, AdminBadge for status, Fragment Mono on stats, token-based colors throughout - ProductTableBody: AdminBadge tones for status/taxable, token-based hover/selected states, Fragment Mono on price column - NewProductForm / ProductEditForm / ProductFormModal: surrounding chrome on tokens; save bars use .ha-btn-primary / .ha-btn-ghost - .atelier-* body preserved as editorial treatment
This commit is contained in:
@@ -68,35 +68,35 @@ export default function ProductTableBody({
|
||||
return (
|
||||
<>
|
||||
{/* Filter bar — sits outside <table> in the parent */}
|
||||
<div className="border-b border-slate-100 px-5 py-3 flex gap-3 flex-wrap items-center">
|
||||
<div className="border-b border-[var(--admin-border-light)] px-5 py-3 flex gap-3 flex-wrap items-center">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search products..."
|
||||
value={search}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="flex-1 min-w-40 rounded-lg border border-zinc-600 bg-zinc-900 px-3 py-2 text-sm text-zinc-100 outline-none focus:border-slate-900"
|
||||
className="flex-1 min-w-40 rounded-lg border border-[var(--admin-border)] bg-[var(--admin-card-bg)] px-3 py-2 text-sm text-[var(--admin-text-primary)] outline-none focus:border-[var(--admin-primary)]"
|
||||
/>
|
||||
<div className="flex gap-1 rounded-lg border border-zinc-600 bg-zinc-900 p-1">
|
||||
<div className="flex gap-1 rounded-lg border border-[var(--admin-border)] bg-[var(--admin-card-bg)] p-1">
|
||||
{(["all", "active", "inactive"] as const).map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => onStatusChange(f)}
|
||||
className={`rounded-md px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
statusFilter === f
|
||||
? "bg-slate-900 text-white"
|
||||
: "text-zinc-400 hover:bg-zinc-950"
|
||||
? "bg-[var(--admin-primary)] text-white"
|
||||
: "text-[var(--admin-text-muted)] hover:bg-[var(--admin-bg)]"
|
||||
}`}
|
||||
>
|
||||
{f === "all" ? "All" : f === "active" ? "Active" : "Inactive"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-xs text-slate-400">{filtered.length}</span>
|
||||
<span className="text-xs text-[var(--admin-text-muted)]">{filtered.length}</span>
|
||||
</div>
|
||||
|
||||
{/* Delete error banner */}
|
||||
{deleteError && (
|
||||
<div className="mx-5 mt-3 rounded-lg bg-red-900/30 border border-red-200 px-4 py-3 text-sm text-red-400">
|
||||
<div className="mx-5 mt-3 rounded-lg bg-[var(--admin-danger-soft)] border border-[var(--admin-danger)] px-4 py-3 text-sm text-[var(--admin-danger)]">
|
||||
{deleteError}
|
||||
<button
|
||||
onClick={() => setDeleteError(null)}
|
||||
@@ -107,10 +107,10 @@ export default function ProductTableBody({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
<tbody className="divide-y divide-[var(--admin-border-light)]">
|
||||
{filtered.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-5 py-10 text-center text-sm text-zinc-500">
|
||||
<td colSpan={7} className="px-5 py-10 text-center text-sm text-[var(--admin-text-muted)]">
|
||||
{search || statusFilter !== "all"
|
||||
? "No products match your search."
|
||||
: "No products found."}
|
||||
@@ -120,43 +120,46 @@ export default function ProductTableBody({
|
||||
filtered.map((product) => (
|
||||
<tr
|
||||
key={product.id}
|
||||
className="hover:bg-zinc-800 transition-colors relative"
|
||||
className="hover:bg-[var(--admin-primary-soft)] transition-colors relative"
|
||||
>
|
||||
<td className="px-3 py-2">
|
||||
<Link
|
||||
href={`/admin/products/${product.id}`}
|
||||
className="block font-medium text-zinc-100 hover:text-zinc-400"
|
||||
className="block font-medium text-[var(--admin-text-primary)] hover:text-[var(--admin-primary)]"
|
||||
>
|
||||
{product.name}
|
||||
</Link>
|
||||
<div className="text-zinc-500 line-clamp-1 text-sm">
|
||||
{product.description || <span className="italic text-slate-400">No description</span>}
|
||||
<div className="text-[var(--admin-text-muted)] line-clamp-1 text-sm">
|
||||
{product.description || <span className="italic text-[var(--admin-text-muted)]">No description</span>}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2 text-zinc-300">
|
||||
<td className="px-3 py-2 text-[var(--admin-text-secondary)]">
|
||||
{Array.isArray(product.brands)
|
||||
? product.brands[0]?.name
|
||||
: product.brands?.name}
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2 text-zinc-300">{product.type}</td>
|
||||
<td className="px-3 py-2 text-[var(--admin-text-secondary)]">{product.type}</td>
|
||||
|
||||
<td className="px-3 py-2 font-semibold text-zinc-100">
|
||||
<td
|
||||
className="px-3 py-2 font-semibold text-[var(--admin-text-primary)]"
|
||||
style={{ fontFamily: "var(--font-fragment-mono)", fontVariantNumeric: "tabular-nums" }}
|
||||
>
|
||||
${Number(product.price).toFixed(2)}
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2">
|
||||
<AdminBadge variant={product.active ? "success" : "default"} dot>
|
||||
<AdminBadge tone={product.active ? "success" : "neutral"} dot>
|
||||
{product.active ? "Active" : "Inactive"}
|
||||
</AdminBadge>
|
||||
</td>
|
||||
|
||||
<td className="px-3 py-2">
|
||||
{product.is_taxable === false ? (
|
||||
<AdminBadge variant="warning">Non-taxable</AdminBadge>
|
||||
<AdminBadge tone="warning">Non-taxable</AdminBadge>
|
||||
) : (
|
||||
<AdminBadge variant="success">Taxable</AdminBadge>
|
||||
<AdminBadge tone="success">Taxable</AdminBadge>
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -165,7 +168,7 @@ export default function ProductTableBody({
|
||||
<div className="relative inline-flex items-center justify-end gap-2">
|
||||
<Link
|
||||
href={`/admin/products/${product.id}`}
|
||||
className="rounded-lg px-3 py-1.5 text-xs font-medium text-zinc-300 hover:bg-zinc-950"
|
||||
className="rounded-lg px-3 py-1.5 text-xs font-medium text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg)]"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
@@ -174,7 +177,7 @@ export default function ProductTableBody({
|
||||
e.preventDefault();
|
||||
setOpenMenu(openMenu === product.id ? null : product.id);
|
||||
}}
|
||||
className="rounded-lg px-2 py-1.5 text-xs text-zinc-500 hover:bg-zinc-950"
|
||||
className="rounded-lg px-2 py-1.5 text-xs text-[var(--admin-text-muted)] hover:bg-[var(--admin-bg)]"
|
||||
>
|
||||
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<circle cx="10" cy="4" r="1.5"/><circle cx="10" cy="10" r="1.5"/><circle cx="10" cy="16" r="1.5"/>
|
||||
@@ -187,10 +190,10 @@ export default function ProductTableBody({
|
||||
className="fixed inset-0 z-10"
|
||||
onClick={() => { setOpenMenu(null); setConfirmDelete(null); }}
|
||||
/>
|
||||
<div className="absolute right-0 top-full mt-1 z-20 w-44 rounded-xl bg-zinc-900 shadow-lg ring-1 ring-zinc-700 overflow-hidden">
|
||||
<div className="absolute right-0 top-full mt-1 z-20 w-44 rounded-xl bg-[var(--admin-card-bg)] shadow-lg ring-1 ring-[var(--admin-border)] overflow-hidden">
|
||||
<button
|
||||
onClick={() => { setOpenMenu(null); setConfirmDelete(product.id); }}
|
||||
className="w-full text-left px-4 py-2.5 text-sm text-red-400 hover:bg-red-900/30"
|
||||
className="w-full text-left px-4 py-2.5 text-sm text-[var(--admin-danger)] hover:bg-[var(--admin-danger-soft)]"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
@@ -201,21 +204,22 @@ export default function ProductTableBody({
|
||||
{confirmDelete === product.id && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-30 bg-black/60 backdrop-blur-sm"
|
||||
className="fixed inset-0 z-30 backdrop-blur-sm"
|
||||
style={{ backgroundColor: "color-mix(in srgb, var(--admin-text-primary) 60%, transparent)" }}
|
||||
onClick={() => { setConfirmDelete(null); setOpenMenu(null); }}
|
||||
/>
|
||||
<div className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-40 w-80 rounded-xl bg-zinc-900 shadow-xl ring-1 ring-zinc-700 p-6">
|
||||
<p className="text-sm font-semibold text-zinc-100">
|
||||
<div className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-40 w-80 rounded-xl bg-[var(--admin-card-bg)] shadow-xl ring-1 ring-[var(--admin-border)] p-6">
|
||||
<p className="text-sm font-semibold text-[var(--admin-text-primary)]">
|
||||
Delete "{product.name}"?
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-zinc-500">
|
||||
<p className="mt-2 text-xs text-[var(--admin-text-muted)]">
|
||||
This will remove the product. If it is attached to any
|
||||
orders, it will be hidden instead of deleted.
|
||||
</p>
|
||||
<div className="mt-5 flex gap-3">
|
||||
<button
|
||||
onClick={() => { setConfirmDelete(null); setOpenMenu(null); }}
|
||||
className="flex-1 rounded-lg border border-zinc-600 px-3 py-2 text-sm font-medium text-zinc-300 hover:bg-zinc-800"
|
||||
className="flex-1 rounded-lg border border-[var(--admin-border)] px-3 py-2 text-sm font-medium text-[var(--admin-text-secondary)] hover:bg-[var(--admin-bg)]"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user