Fix: the mobile table was mislabelled and clipping its own totals

A screenshot at phone width showed an "INVOICE | LINES | TOTAL | STATUS"
header row while the data under LINES was money. The header and its data cell
had drifted apart: the earlier pass hid the Lines <td> but not the matching
<th>, because the loop that added the hide classes only matched text-left
headers and Lines is text-right. So the Total values rendered under a heading
that named a column which no longer existed.

The same screenshot showed the totals clipped — "41,710.50" reading as
"710,50". With only three columns left, the 520px min-width was still forcing
a sideways scroll, and the opaque sticky first column then sat on top of the
left of the money values. The mobile floor is gone, so the three remaining
columns size to their content, and the sticky and status gutters are trimmed
at mobile widths to let them fit 375px.

Below sm the table is now exactly Invoice, Total, Status — and every header
matches a visible cell.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
David 2026-10-02 15:34:19 +02:00
parent 999feb37fa
commit dc7024d98a
16 changed files with 152 additions and 136 deletions

View file

@ -117,7 +117,7 @@ export default async function ExpensesHub() {
<Link
key={s.key}
href={s.href}
className="group relative bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 p-5 shadow-sm hover:shadow-md transition overflow-hidden"
className="group relative bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border p-5 shadow-sm hover:shadow-md transition overflow-hidden"
>
<div className="relative">
<div className="flex items-start justify-between mb-3">
@ -133,12 +133,12 @@ export default async function ExpensesHub() {
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">{s.description}</div>
<div className="mt-4 flex items-end justify-between">
<div>
<div className="text-[10px] text-gray-400 dark:text-gray-500 uppercase tracking-wider">Monthly</div>
<div className="text-[10px] text-gray-400 dark:text-text-soft uppercase tracking-wider">Monthly</div>
<div className="text-xl font-semibold tabular-nums" style={{ color: s.iconColor }}>
{ZAR(s.total)}
</div>
</div>
<div className="text-[10px] text-gray-400 dark:text-gray-500">{s.count} items</div>
<div className="text-[10px] text-gray-400 dark:text-text-soft">{s.count} items</div>
</div>
</div>
</Link>

View file

@ -36,19 +36,35 @@
--color-text-muted: #6b7280;
--color-text-soft: #9ca3af;
--color-row-hover: rgba(0, 0, 0, 0.02);
/* "Ink" steps — the light-mode colour for text that borrows a brand hue.
The base brand palette above is never used as text on a light surface unless
it clears 4.5:1; these names exist so `text-accent-ink` works in BOTH modes
and only its value changes. See the .dark block for the dark counterparts. */
--color-accent-ink: #30638E;
--color-brick-ink: #D1495B;
--color-indigo-ink: #003D5B;
}
/* Dark theme overrides */
.dark {
--color-bg: #0b1014;
--color-surface: #131a21;
--color-surface-muted: #0f1419;
--color-border: #1f2937;
--color-border-soft: #1a222b;
--color-text: #e6edf3;
--color-text-muted: #9ca3af;
--color-text-soft: #6b7280;
--color-row-hover: rgba(255, 255, 255, 0.03);
--color-bg: #0f151b;
--color-surface: #1e2732;
--color-surface-muted: #182029;
--color-border: #303c48;
--color-border-soft: #26313c;
--color-text: #e8eef4;
--color-text-muted: #adb8c2;
--color-text-soft: #87919a;
--color-row-hover: rgba(255, 255, 255, 0.04);
/* Dark "ink" steps. The base brand hues are correct as *fills* but fail WCAG AA
as text on a dark surface (seaweed 3.6:1, casual 2.9:1, brick 3.7:1, indigo 1.6:1).
These are lighter steps of the same hues, reaching 4.8–7.3:1. Text-only —
never a fill — so the @theme brand palette stays untouched. */
--color-accent-ink: #2BB0C4;
--color-brick-ink: #E86E7E;
--color-indigo-ink: #7FA8C9;
/* Part A.4: dark-mode chart hues */
--color-chart-income: #00A6BE;
@ -81,9 +97,9 @@ body, .dark body { transition: background-color 0.2s ease, color 0.2s ease; }
.recharts-legend-item-text { color: #6b7280 !important; font-size: 12px !important; }
.dark .recharts-default-tooltip {
background: rgba(15, 20, 25, 0.98) !important;
border: 1px solid #1f2937 !important;
color: #e6edf3 !important;
background: rgba(30, 39, 50, 0.98) !important;
border: 1px solid #303c48 !important;
color: #e8eef4 !important;
}
.dark .recharts-tooltip-label { color: #e6edf3 !important; }
.dark .recharts-legend-item-text { color: #9ca3af !important; }
.dark .recharts-tooltip-label { color: #e8eef4 !important; }
.dark .recharts-legend-item-text { color: #adb8c2 !important; }

View file

@ -99,13 +99,13 @@ export default async function RootLayout({ children }: { children: React.ReactNo
</head>
<body className="min-h-screen">
<ThemeProvider>
<header className="sticky top-0 z-30 h-16 bg-white/80 dark:bg-[#0b1014]/80 backdrop-blur border-b border-gray-200 dark:border-white/5">
<header className="sticky top-0 z-30 h-16 bg-white/80 dark:bg-bg/80 backdrop-blur border-b border-gray-200 dark:border-border">
<div className="flex items-center gap-3 sm:gap-6 px-4 sm:px-6 h-full">
<Link href="/" className="flex items-center gap-2 shrink-0">
<img
src="/brand/logo.png"
alt="Mega Plant"
className="w-9 h-9 rounded-xl object-contain bg-white dark:bg-[#0b1014] shadow-sm"
className="w-9 h-9 rounded-xl object-contain bg-white dark:bg-bg shadow-sm"
/>
<div className="leading-tight hidden sm:block">
<div className="text-sm font-semibold text-[#003D5B] dark:text-white">Mega Plant</div>

View file

@ -42,7 +42,7 @@ export default async function OverheadsPage() {
const items = overheads.filter((o) => o.section === sec);
const secTotal = items.reduce((s, o) => s + (o.amount_incl || 0), 0);
return (
<section key={sec} className="bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 p-6 mb-5 shadow-sm">
<section key={sec} className="bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border p-6 mb-5 shadow-sm">
<div className="flex items-center justify-between mb-4">
<h2 className="text-base font-semibold text-[#003D5B] dark:text-white">{sec}</h2>
<div className="text-right">
@ -53,7 +53,7 @@ export default async function OverheadsPage() {
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-100 dark:border-white/5">
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-100 dark:border-border">
<th className="text-left py-2.5 px-3 font-medium">Description</th>
<th className="text-left py-2.5 px-3 font-medium">GL Account</th>
<th className="text-left py-2.5 px-3 font-medium">Supplier</th>
@ -64,7 +64,7 @@ export default async function OverheadsPage() {
</thead>
<tbody>
{items.map((o) => (
<tr key={o.id} className="border-b border-gray-50 dark:border-white/5 hover:bg-gray-50/40 dark:hover:bg-white/[0.02]">
<tr key={o.id} className="border-b border-gray-50 dark:border-border hover:bg-gray-50/40 dark:hover:bg-white/[0.02]">
<td className="py-2.5 px-3 font-medium text-[#003D5B] dark:text-white">{o.description}</td>
<td className="py-2.5 px-3 text-gray-500 dark:text-gray-400">{o.gl_account || "—"}</td>
<td className="py-2.5 px-3 text-gray-500 dark:text-gray-400">{o.supplier || "—"}</td>

View file

@ -100,7 +100,7 @@ export default async function DashboardPage() {
{/* Main charts */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 mb-6">
<section className="lg:col-span-2 bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 p-6 shadow-sm">
<section className="lg:col-span-2 bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border p-6 shadow-sm">
<div className="flex items-end justify-between mb-5">
<div>
<h2 className="text-base font-semibold text-[#003D5B] dark:text-white">Income vs Fixed vs Running</h2>
@ -115,7 +115,7 @@ export default async function DashboardPage() {
<IncomeAreaChart data={monthsIncome} />
</section>
<section className="bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 p-6 shadow-sm">
<section className="bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border p-6 shadow-sm">
<div className="mb-4">
<h2 className="text-base font-semibold text-[#003D5B] dark:text-white">Overhead mix</h2>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">By section · {ZAR(totalMonthlyIncl)} total</p>
@ -137,7 +137,7 @@ export default async function DashboardPage() {
{/* Bottom row */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<section className="lg:col-span-2 bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 p-6 shadow-sm">
<section className="lg:col-span-2 bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border p-6 shadow-sm">
<div className="mb-5">
<h2 className="text-base font-semibold text-[#003D5B] dark:text-white">Monthly Income trend</h2>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">Project income per month</p>

View file

@ -89,14 +89,14 @@ export function ChartsGrid({ items }: { items: Item[] }) {
</div>
{/* Toolbar — wraps to multiple rows on mobile. Round 4 Part B.1/B.3/B.5. */}
<div className="bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 p-4 shadow-sm mb-5 flex flex-wrap items-center gap-3">
<div className="bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border p-4 shadow-sm mb-5 flex flex-wrap items-center gap-3">
<div className="relative flex-1 min-w-[160px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search by code or name…"
className="w-full min-h-[44px] pl-9 pr-3 bg-gray-50 dark:bg-white/5 border border-transparent rounded-lg text-sm placeholder:text-gray-400 focus:bg-white dark:focus:bg-[#0f1419] focus:border-[#30638E] focus:outline-none"
className="w-full min-h-[44px] pl-9 pr-3 bg-gray-50 dark:bg-white/5 border border-transparent rounded-lg text-sm placeholder:text-gray-400 focus:bg-white dark:focus:bg-white/10 focus:border-[#30638E] focus:outline-none"
/>
</div>
<div className="flex items-center gap-1">
@ -119,7 +119,7 @@ export function ChartsGrid({ items }: { items: Item[] }) {
))}
</div>
<div className="flex items-center gap-1 sm:pl-3 sm:border-l sm:border-gray-200 sm:dark:border-white/10">
<span className="text-[10px] uppercase tracking-wider text-gray-400 dark:text-gray-500 mr-1 hidden sm:inline">View</span>
<span className="text-[10px] uppercase tracking-wider text-gray-400 dark:text-text-soft mr-1 hidden sm:inline">View</span>
{[
{ id: "lines", label: "Lines", icon: LineChartIcon },
{ id: "stack", label: "Cost stack", icon: BarChart3 },
@ -142,10 +142,10 @@ export function ChartsGrid({ items }: { items: Item[] }) {
</div>
{filtered.length === 0 ? (
<div className="bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 p-12 text-center">
<Search className="w-8 h-8 text-gray-300 dark:text-gray-600 mx-auto mb-3" />
<div className="bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border p-12 text-center">
<Search className="w-8 h-8 text-gray-300 dark:text-text-soft mx-auto mb-3" />
<div className="text-sm text-gray-500 dark:text-gray-400">No machines match "{query}"</div>
<button onClick={() => setQuery("")} className="mt-3 text-xs text-[#30638E] dark:text-[#00798C] hover:underline">
<button onClick={() => setQuery("")} className="mt-3 text-xs text-accent-ink hover:underline">
Clear search
</button>
</div>
@ -157,7 +157,7 @@ export function ChartsGrid({ items }: { items: Item[] }) {
const profit = computeProfit({ income: item.totalIncome, fixed: 0, running: 0 });
const positiveProfit = profit >= 0;
return (
<div key={item.id} className="bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 p-5 shadow-sm hover:shadow-md transition">
<div key={item.id} className="bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border p-5 shadow-sm hover:shadow-md transition">
<div className="flex items-start justify-between mb-3">
<div>
<span className="font-mono text-[10px] px-1.5 py-0.5 rounded bg-[#00798C]/10 text-[#00798C] font-semibold">

View file

@ -65,7 +65,7 @@ export async function ExpenseSectionPage({
</div>
{glBreakdown.length > 1 && (
<section className="bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 p-6 shadow-sm mb-6">
<section className="bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border p-6 shadow-sm mb-6">
<h2 className="text-base font-semibold text-[#003D5B] dark:text-white mb-4">By GL account</h2>
<div className="space-y-3">
{glBreakdown.map((g) => {
@ -75,7 +75,7 @@ export async function ExpenseSectionPage({
<div className="flex items-center justify-between text-sm mb-1">
<span className="text-gray-700 dark:text-gray-300 font-medium">{g.gl}</span>
<span className="text-gray-500 dark:text-gray-400">
{ZAR(g.total)} · <span className="text-gray-400 dark:text-gray-500">{g.count} item{g.count !== 1 && "s"}</span> · {pct.toFixed(0)}%
{ZAR(g.total)} · <span className="text-gray-400 dark:text-text-soft">{g.count} item{g.count !== 1 && "s"}</span> · {pct.toFixed(0)}%
</span>
</div>
<div className="h-2 rounded-full bg-gray-100 dark:bg-white/5 overflow-hidden">
@ -93,16 +93,16 @@ export async function ExpenseSectionPage({
{/* Part B.1: empty state must say so plainly, not render a table of zeros */}
{items.length === 0 ? (
<section className="bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 p-12 shadow-sm text-center">
<section className="bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border p-12 shadow-sm text-center">
<div className="text-sm text-gray-500 dark:text-gray-400">
No <strong className="text-gray-700 dark:text-gray-200">{section}</strong> entries in PocketBase.
</div>
<div className="text-xs text-gray-400 dark:text-gray-500 mt-1">
<div className="text-xs text-gray-400 dark:text-text-soft mt-1">
This category either doesn&apos;t apply yet or lives elsewhere — see round-3 brief.
</div>
</section>
) : (
<section className="bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 shadow-sm overflow-hidden">
<section className="bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border shadow-sm overflow-hidden">
<div className="p-6 pb-3 flex items-end justify-between">
<h2 className="text-base font-semibold text-[#003D5B] dark:text-white">Line items</h2>
<span className="text-xs text-gray-500 dark:text-gray-400">{items.length} entries</span>
@ -110,7 +110,7 @@ export async function ExpenseSectionPage({
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-y border-gray-100 dark:border-white/5 bg-gray-50/50 dark:bg-white/[0.02]">
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-y border-gray-100 dark:border-border bg-gray-50/50 dark:bg-white/[0.04]">
<th className="text-left py-3 px-6 font-medium">Description</th>
<th className="text-left py-3 px-3 font-medium">GL Account</th>
<th className="text-left py-3 px-3 font-medium">Supplier</th>
@ -121,11 +121,11 @@ export async function ExpenseSectionPage({
</thead>
<tbody>
{items.map((o) => (
<tr key={o.id} className="border-b border-gray-50 dark:border-white/5 hover:bg-gray-50/40 dark:hover:bg-white/[0.02]">
<tr key={o.id} className="border-b border-gray-50 dark:border-border hover:bg-gray-50/40 dark:hover:bg-white/[0.02]">
<td className="py-3 px-6 font-medium text-[#003D5B] dark:text-white">{o.description}</td>
<td className="py-3 px-3">
{o.gl_account ? (
<span className="text-xs px-2 py-0.5 rounded-md bg-[#30638E]/10 text-[#30638E] dark:text-[#00798C] font-medium">
<span className="text-xs px-2 py-0.5 rounded-md bg-[#30638E]/10 text-accent-ink font-medium">
{o.gl_account}
</span>
) : (
@ -140,7 +140,7 @@ export async function ExpenseSectionPage({
</td>
</tr>
))}
<tr className="bg-gradient-to-r from-gray-50 dark:from-white/[0.02] to-white dark:to-transparent border-t-2 border-gray-200 dark:border-white/5">
<tr className="bg-gradient-to-r from-gray-50 dark:from-white/[0.02] to-white dark:to-transparent border-t-2 border-gray-200 dark:border-border">
<td colSpan={3} className="py-3 px-6 font-semibold text-right text-gray-700 dark:text-gray-300">Total</td>
<td className="py-3 px-3 text-right tabular-nums font-semibold text-gray-900 dark:text-white">{ZAR(totalExcl)}</td>
<td className="py-3 px-3 text-right tabular-nums font-semibold text-gray-900 dark:text-white">{ZAR(totalVat)}</td>
@ -159,7 +159,7 @@ export async function ExpenseSectionPage({
function StatCard({ label, value, color }: { label: string; value: string; color: string }) {
return (
<div className="bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 p-5 shadow-sm">
<div className="bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border p-5 shadow-sm">
<div className="text-xs uppercase tracking-wider text-gray-500 dark:text-gray-400">{label}</div>
<div className="text-2xl font-semibold mt-2 tabular-nums" style={{ color }}>{value}</div>
</div>

View file

@ -35,7 +35,7 @@ export function GlobalSearch({ assets }: { assets: Asset[] }) {
return (
<div className="relative flex-1 max-w-md">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 dark:text-gray-500 pointer-events-none" />
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 dark:text-text-soft pointer-events-none" />
<input
value={query}
onChange={(e) => {
@ -52,12 +52,12 @@ export function GlobalSearch({ assets }: { assets: Asset[] }) {
}
}}
placeholder="Search plant, supplier, code…"
className="w-full h-9 pl-9 pr-3 bg-gray-100 dark:bg-white/5 border border-transparent rounded-lg text-sm placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:bg-white dark:focus:bg-[#0f1419] focus:border-[#30638E] focus:outline-none transition"
className="w-full h-9 pl-9 pr-3 bg-gray-100 dark:bg-white/5 border border-transparent rounded-lg text-sm placeholder:text-gray-400 dark:placeholder:text-gray-500 focus:bg-white dark:focus:bg-white/10 focus:border-[#30638E] focus:outline-none transition"
/>
</div>
{open && matches.length > 0 && (
<div className="absolute top-full left-0 right-0 mt-2 bg-white dark:bg-[#131a21] border border-gray-100 dark:border-white/10 rounded-xl shadow-lg overflow-hidden z-50">
<div className="absolute top-full left-0 right-0 mt-2 bg-white dark:bg-surface border border-gray-100 dark:border-white/10 rounded-xl shadow-lg overflow-hidden z-50">
{matches.map((a) => (
<Link
key={a.id}
@ -67,7 +67,7 @@ export function GlobalSearch({ assets }: { assets: Asset[] }) {
setQuery("");
}}
className={cn(
"flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 dark:hover:bg-white/5 text-sm border-b border-gray-50 dark:border-white/5 last:border-0"
"flex items-center gap-3 px-4 py-2.5 hover:bg-gray-50 dark:hover:bg-white/5 text-sm border-b border-gray-50 dark:border-border last:border-0"
)}
>
{a.code ? (
@ -86,7 +86,7 @@ export function GlobalSearch({ assets }: { assets: Asset[] }) {
)}
</div>
{a.category && (
<span className="text-[10px] uppercase tracking-wider text-gray-400 dark:text-gray-500 shrink-0">
<span className="text-[10px] uppercase tracking-wider text-gray-400 dark:text-text-soft shrink-0">
{a.category}
</span>
)}

View file

@ -50,7 +50,7 @@ export function IncomeView({ rows, palette }: { rows: IncomeRow[]; palette: { in
</div>
{/* Round 4 Part B.3: Sort collapses to a select on mobile. View toggle stays as buttons. */}
<div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-1 p-0.5 rounded-lg bg-white dark:bg-white/5 border border-gray-200 dark:border-white/5">
<div className="flex items-center gap-1 p-0.5 rounded-lg bg-white dark:bg-white/5 border border-gray-200 dark:border-border">
<button
onClick={() => setView("cards")}
aria-label="Cards view"
@ -75,7 +75,7 @@ export function IncomeView({ rows, palette }: { rows: IncomeRow[]; palette: { in
</button>
</div>
<div className="hidden sm:flex items-center gap-1">
<span className="text-xs text-gray-400 dark:text-gray-500 mr-1">Sort:</span>
<span className="text-xs text-gray-400 dark:text-text-soft mr-1">Sort:</span>
{[
{ id: "profit", label: "Profit" },
{ id: "income", label: "Income" },
@ -137,7 +137,7 @@ function CardsView({
return (
<div
key={r.id}
className="bg-white dark:bg-[#131a21] border border-gray-100 dark:border-white/5 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition"
className="bg-white dark:bg-surface border border-gray-100 dark:border-border rounded-xl overflow-hidden shadow-sm hover:shadow-md transition"
>
<button
onClick={() => setExpanded(isOpen ? null : r.id)}
@ -163,7 +163,7 @@ function CardsView({
<Sparkline data={r.months} color={palette.income} />
</div>
<div className="text-right shrink-0 w-32 hidden md:block">
<div className="text-xs text-gray-400 dark:text-gray-500 uppercase tracking-wider">YTD income</div>
<div className="text-xs text-gray-400 dark:text-text-soft uppercase tracking-wider">YTD income</div>
<div className="text-sm font-semibold tabular-nums text-[#003D5B] dark:text-white">
{ZAR2(r.income)}
</div>
@ -185,7 +185,7 @@ function CardsView({
<div className="flex sm:hidden items-center justify-between gap-3 pt-2 text-xs">
<Sparkline data={r.months} color={palette.income} />
<div className="text-right">
<div className="text-xs text-gray-400 dark:text-gray-500 uppercase tracking-wider">YTD income</div>
<div className="text-xs text-gray-400 dark:text-text-soft uppercase tracking-wider">YTD income</div>
<div className="text-sm font-semibold tabular-nums text-[#003D5B] dark:text-white">{ZAR2(r.income)}</div>
</div>
<div
@ -201,17 +201,17 @@ function CardsView({
</button>
{isOpen && (
<div className="border-t border-gray-100 dark:border-white/5 px-4 py-4 bg-gray-50/50 dark:bg-white/[0.02]">
<div className="border-t border-gray-100 dark:border-border px-4 py-4 bg-gray-50/50 dark:bg-white/[0.04]">
{/* 12-col on md+, 3-col on mobile — Part 3: unreadable at 375px otherwise */}
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-12 gap-2 text-xs">
{r.months.map((v, i) => (
<div key={i} className="text-center">
<div className="text-gray-400 dark:text-gray-500 text-xs mb-1">{MONTH_NAMES[i]}</div>
<div className="text-gray-400 dark:text-text-soft text-xs mb-1">{MONTH_NAMES[i]}</div>
<div
className={`py-1.5 rounded tabular-nums ${
(v ?? 0) > 0
? "bg-[#00798C]/5 text-[#00798C] dark:bg-[#00798C]/15"
: "text-gray-300 dark:text-gray-600"
: "text-gray-300 dark:text-text-soft"
}`}
>
{v == null ? "—" : (v > 0 ? ZAR2(v) : "R 0")}
@ -219,7 +219,7 @@ function CardsView({
</div>
))}
</div>
<div className="grid grid-cols-3 gap-4 mt-4 pt-3 border-t border-gray-100 dark:border-white/5 text-xs">
<div className="grid grid-cols-3 gap-4 mt-4 pt-3 border-t border-gray-100 dark:border-border text-xs">
<div>
<div className="text-gray-400 uppercase tracking-wider text-xs">YTD income</div>
<div className="font-semibold tabular-nums text-[#003D5B] dark:text-white">{ZAR(r.income)}</div>
@ -248,12 +248,12 @@ function CardsView({
function GridView({ rows }: { rows: IncomeRow[] }) {
return (
<div className="relative bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 shadow-sm overflow-hidden">
<div className="relative bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border shadow-sm overflow-hidden">
<div className="overflow-x-auto" style={{ WebkitOverflowScrolling: "touch" }}>
<table className="w-full text-sm min-w-[720px]">
<thead>
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-100 dark:border-white/5 bg-gray-50/50 dark:bg-white/[0.02]">
<th className="text-left py-3 px-6 font-medium sticky left-0 bg-gray-50/95 dark:bg-[#1a222b]/95 backdrop-blur z-10">Code</th>
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-100 dark:border-border bg-gray-50/50 dark:bg-white/[0.04]">
<th className="text-left py-3 px-6 font-medium sticky left-0 bg-gray-50/95 dark:bg-surface-muted/95 backdrop-blur z-10">Code</th>
<th className="text-left py-3 px-3 font-medium">Machine</th>
{MONTH_NAMES.map((m) => (
<th key={m} className="text-right py-3 px-2 font-medium min-w-[78px]">{m}</th>
@ -265,8 +265,8 @@ function GridView({ rows }: { rows: IncomeRow[] }) {
{rows.map((r) => {
const positive = r.ytdProfit >= 0;
return (
<tr key={r.id} className="border-b border-gray-50 dark:border-white/5 hover:bg-gray-50/40 dark:hover:bg-white/[0.02]">
<td className="py-2.5 px-6 sticky left-0 bg-white dark:bg-[#131a21]">
<tr key={r.id} className="border-b border-gray-50 dark:border-border hover:bg-gray-50/40 dark:hover:bg-white/[0.02]">
<td className="py-2.5 px-6 sticky left-0 bg-white dark:bg-surface">
<span className="font-mono text-xs px-2 py-1 rounded-md bg-[#00798C]/10 text-[#00798C] font-semibold">
{r.code}
</span>
@ -278,7 +278,7 @@ function GridView({ rows }: { rows: IncomeRow[] }) {
</div>
</td>
{r.months.map((v, i) => (
<td key={i} className={`py-2.5 px-2 text-right tabular-nums ${(v ?? 0) > 0 ? "text-gray-700 dark:text-gray-300" : "text-gray-300 dark:text-gray-600"}`}>
<td key={i} className={`py-2.5 px-2 text-right tabular-nums ${(v ?? 0) > 0 ? "text-gray-700 dark:text-gray-300" : "text-gray-300 dark:text-text-soft"}`}>
{v == null ? "—" : (v > 0 ? ZAR2(v) : "R 0")}
</td>
))}
@ -293,8 +293,8 @@ function GridView({ rows }: { rows: IncomeRow[] }) {
</tbody>
{/* Totals row: the sum of every machine for each month. */}
<tfoot>
<tr className="border-t-2 border-gray-200 dark:border-white/15 bg-gray-50/80 dark:bg-white/[0.03]">
<td className="py-3 px-6 sticky left-0 z-10 bg-gray-50/95 dark:bg-[#1a222b]/95 backdrop-blur text-sm font-semibold text-[#003D5B] dark:text-white">
<tr className="border-t-2 border-gray-200 dark:border-white/15 bg-gray-50/80 dark:bg-white/[0.04]">
<td className="py-3 px-6 sticky left-0 z-10 bg-gray-50/95 dark:bg-surface-muted/95 backdrop-blur text-sm font-semibold text-[#003D5B] dark:text-white">
Total
</td>
<td className="py-3 px-3 text-sm font-semibold text-[#003D5B] dark:text-white whitespace-nowrap">

View file

@ -166,9 +166,9 @@ export function AddInvoiceButton() {
role="dialog"
aria-modal="true"
aria-label="Add invoice"
className="relative w-full sm:max-w-lg bg-white dark:bg-[#131a21] rounded-t-2xl sm:rounded-2xl shadow-2xl border border-gray-100 dark:border-white/10 max-h-[90vh] overflow-y-auto"
className="relative w-full sm:max-w-lg bg-white dark:bg-surface rounded-t-2xl sm:rounded-2xl shadow-2xl border border-gray-100 dark:border-white/10 max-h-[90vh] overflow-y-auto"
>
<div className="flex items-center justify-between px-5 py-4 border-b border-gray-100 dark:border-white/5">
<div className="flex items-center justify-between px-5 py-4 border-b border-gray-100 dark:border-border">
<h2 className="text-base font-semibold text-[#003D5B] dark:text-white">Add invoice</h2>
<button
type="button"
@ -269,7 +269,7 @@ export function AddInvoiceButton() {
</p>
</div>
<div className="flex items-center justify-end gap-2 px-5 py-4 border-t border-gray-100 dark:border-white/5 bg-gray-50/60 dark:bg-white/[0.02] rounded-b-2xl">
<div className="flex items-center justify-end gap-2 px-5 py-4 border-t border-gray-100 dark:border-border bg-gray-50/60 dark:bg-white/[0.04] rounded-b-2xl">
<button
type="button"
onClick={close}
@ -360,8 +360,8 @@ export function ProblemInbox() {
};
return (
<div className="bg-white dark:bg-[#131a21] border border-gray-100 dark:border-white/5 rounded-2xl shadow-sm overflow-hidden mb-6">
<div className="flex items-center gap-2.5 px-5 py-4 border-b border-gray-100 dark:border-white/5">
<div className="bg-white dark:bg-surface border border-gray-100 dark:border-border rounded-2xl shadow-sm overflow-hidden mb-6">
<div className="flex items-center gap-2.5 px-5 py-4 border-b border-gray-100 dark:border-border">
<Inbox className="w-4 h-4 text-[#D1495B] shrink-0" />
<h2 className="text-sm font-semibold text-[#003D5B] dark:text-white">Problem invoices</h2>
<span className="text-xs px-2 py-0.5 rounded-md bg-[#D1495B]/10 text-[#D1495B] font-semibold">
@ -380,7 +380,7 @@ export function ProblemInbox() {
Nothing in the inbox — every PDF offered so far imported cleanly.
</p>
) : (
<ul className="divide-y divide-gray-100 dark:border-white/5">
<ul className="divide-y divide-gray-100 dark:border-border">
{items.map((it) => (
<li key={it.id} className="px-5 py-4">
<div className="flex flex-wrap items-start justify-between gap-3">
@ -396,7 +396,7 @@ export function ProblemInbox() {
)}
</div>
<p className="text-sm text-[#D1495B] mt-1.5 break-words">{it.error}</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
<p className="text-xs text-gray-400 dark:text-text-soft mt-1">
{(it.file_size / 1024).toFixed(0)} KB
{it.created ? ` · ${new Date(it.created).toLocaleString("en-ZA")}` : ""}
</p>

View file

@ -60,22 +60,22 @@ export function InvoiceList({
<Stat label="Billed (incl VAT)" value={ZAR2(totals.incl)} tone="brand" />
</div>
<div className="bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 shadow-sm overflow-hidden">
<div className="bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border shadow-sm overflow-hidden">
{/* Rule 1 + 4: scrolls inside its own container; page never scrolls sideways. */}
<div className="relative overflow-x-auto" style={{ WebkitOverflowScrolling: "touch" }}>
<table className="w-full text-sm min-w-[520px] sm:min-w-[820px]">
<table className="w-full text-sm sm:min-w-[820px]">
<thead>
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-100 dark:border-white/5 bg-gray-50/50 dark:bg-white/[0.02]">
<th className="text-left py-3 px-4 font-medium sticky left-0 z-10 bg-gray-50/95 dark:bg-[#1a222b]/95 backdrop-blur">
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-100 dark:border-border bg-gray-50/50 dark:bg-white/[0.04]">
<th className="text-left py-3 px-3 sm:px-4 font-medium sticky left-0 z-10 bg-gray-50/95 dark:bg-surface-muted/95 backdrop-blur">
Invoice
</th>
<th className="hidden sm:table-cell text-left py-3 px-3 font-medium whitespace-nowrap">Site</th>
<th className="hidden sm:table-cell text-left py-3 px-3 font-medium whitespace-nowrap">Period</th>
<th className="text-right py-3 px-3 font-medium whitespace-nowrap">Lines</th>
<th className="hidden sm:table-cell text-right py-3 px-3 font-medium whitespace-nowrap">Lines</th>
<th className="hidden sm:table-cell text-right py-3 px-3 font-medium whitespace-nowrap">Revenue ex VAT</th>
<th className="hidden sm:table-cell text-right py-3 px-3 font-medium whitespace-nowrap">VAT</th>
<th className="text-right py-3 px-3 font-medium whitespace-nowrap">Total</th>
<th className="text-left py-3 px-4 font-medium whitespace-nowrap">Status</th>
<th className="text-left py-3 px-3 sm:px-4 font-medium whitespace-nowrap">Status</th>
</tr>
</thead>
<tbody>
@ -103,7 +103,7 @@ export function InvoiceList({
onToggle={() => setOpen(isOpen ? null : inv.id)}
expanded={
isOpen ? (
<div className="p-3 sm:p-4 bg-gray-50/60 dark:bg-white/[0.02]">
<div className="p-3 sm:p-4 bg-gray-50/60 dark:bg-white/[0.04]">
<InvoiceSummary invoice={inv} lines={lines} assets={assets} embedded />
</div>
) : null
@ -135,14 +135,14 @@ function InvoiceRow({
return (
<>
<tr
className={`border-b border-gray-50 dark:border-white/5 transition ${
className={`border-b border-gray-50 dark:border-border transition ${
hasProblem ? "bg-[#D1495B]/[0.04]" : "hover:bg-gray-50/60 dark:hover:bg-white/[0.02]"
}`}
>
{/* Sticky first column so the invoice identity survives horizontal scroll. */}
<td
className={`py-2 px-4 sticky left-0 z-10 ${
hasProblem ? "bg-[#D1495B]/[0.04] dark:bg-[#1a1319]" : "bg-white dark:bg-[#131a21]"
hasProblem ? "bg-[#D1495B]/[0.04] dark:bg-[#1a1319]" : "bg-white dark:bg-surface"
}`}
>
<button
@ -166,7 +166,7 @@ function InvoiceRow({
{invoice.site}
</span>
) : (
<span className="text-sm text-gray-400 dark:text-gray-500">—</span>
<span className="text-sm text-gray-400 dark:text-text-soft">—</span>
)}
</td>
<td className="hidden sm:table-cell py-2 px-3 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap tabular-nums">
@ -184,7 +184,7 @@ function InvoiceRow({
<td className="py-2 px-3 text-right tabular-nums font-semibold text-[#003D5B] dark:text-white whitespace-nowrap">
{ZAR2(invoice.total_incl)}
</td>
<td className="py-2 px-4 whitespace-nowrap">
<td className="py-2 px-3 sm:px-4 whitespace-nowrap">
{hasProblem ? (
<span className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-[#D1495B]/10 text-[#D1495B] text-xs font-semibold">
<AlertTriangle className="w-3.5 h-3.5 shrink-0" />
@ -202,7 +202,7 @@ function InvoiceRow({
{isOpen && (
<tr>
{/* Span the full table so the detail panel is not clipped by the sticky column. */}
<td colSpan={8} className="p-0 border-b border-gray-100 dark:border-white/5">
<td colSpan={8} className="p-0 border-b border-gray-100 dark:border-border">
{expanded}
</td>
</tr>
@ -231,10 +231,10 @@ function Stat({ label, value, tone }: { label: string; value: string; tone: "tea
// Kept so an empty folder still renders a usable page.
export function EmptyInvoices() {
return (
<div className="bg-white dark:bg-[#131a21] border border-gray-100 dark:border-white/5 rounded-2xl shadow-sm p-10 text-center">
<FileText className="w-8 h-8 mx-auto text-gray-300 dark:text-gray-600 mb-3" />
<div className="bg-white dark:bg-surface border border-gray-100 dark:border-border rounded-2xl shadow-sm p-10 text-center">
<FileText className="w-8 h-8 mx-auto text-gray-300 dark:text-text-soft mb-3" />
<p className="text-gray-500 dark:text-gray-400">No invoices yet.</p>
<p className="text-sm text-gray-400 dark:text-gray-500 mt-1">
<p className="text-sm text-gray-400 dark:text-text-soft mt-1">
Add a <code className="font-mono text-[#00798C]">data/source/invoice-*.json</code> then run{" "}
<code className="font-mono text-[#00798C]">npm run seed</code>.
</p>

View file

@ -39,12 +39,12 @@ export function InvoiceSummary({
<div
className={
embedded
? "bg-white dark:bg-[#131a21] border border-gray-100 dark:border-white/5 rounded-xl shadow-sm overflow-hidden"
: "bg-white dark:bg-[#131a21] border border-gray-100 dark:border-white/5 rounded-2xl shadow-sm overflow-hidden mb-6"
? "bg-white dark:bg-surface border border-gray-100 dark:border-border rounded-xl shadow-sm overflow-hidden"
: "bg-white dark:bg-surface border border-gray-100 dark:border-border rounded-2xl shadow-sm overflow-hidden mb-6"
}
>
{!embedded && (
<div className="p-5 border-b border-gray-100 dark:border-white/5">
<div className="p-5 border-b border-gray-100 dark:border-border">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
@ -84,7 +84,7 @@ export function InvoiceSummary({
)}
{embedded && (
<div className="px-4 py-3 border-b border-gray-100 dark:border-white/5 flex flex-wrap items-center justify-between gap-3">
<div className="px-4 py-3 border-b border-gray-100 dark:border-border flex flex-wrap items-center justify-between gap-3">
<div className="min-w-0">
<div className="text-sm font-medium text-[#003D5B] dark:text-white">
{invoice.debtor} · {invoice.description}
@ -109,7 +109,7 @@ export function InvoiceSummary({
{unbooked.length > 0 ? (
<div className="flex items-start gap-3 px-5 py-3 bg-[#D1495B]/5 border-b border-[#D1495B]/15">
<AlertTriangle className="w-4 h-4 text-[#D1495B] shrink-0 mt-0.5" />
<div className="text-sm text-[#D1495B] dark:text-[#D1495B]">
<div className="text-sm text-[#D1495B] dark:text-brick-ink">
<p>
{unbooked.length} line{unbooked.length > 1 ? "s" : ""} not yet booked to a machine — choose a
plant item below.
@ -129,7 +129,7 @@ export function InvoiceSummary({
<InvoiceTable lines={lines} assets={assets} />
<div className="p-5 border-t border-gray-100 dark:border-white/5 bg-gray-50/50 dark:bg-white/[0.02]">
<div className="p-5 border-t border-gray-100 dark:border-border bg-gray-50/50 dark:bg-white/[0.04]">
<div className="flex flex-col lg:flex-row lg:items-start justify-between gap-6">
<div className="lg:max-w-sm w-full">
<div className="text-sm font-medium text-[#003D5B] dark:text-white mb-2">
@ -230,8 +230,8 @@ export function InvoiceTable({ lines, assets }: { lines: BookedLine[]; assets: A
<div className="overflow-x-auto" style={{ WebkitOverflowScrolling: "touch" }}>
<table className="w-full text-sm min-w-[900px]">
<thead>
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-100 dark:border-white/5 bg-gray-50/50 dark:bg-white/[0.02]">
<th className="text-left py-3 px-5 font-medium sticky left-0 z-10 bg-gray-50/95 dark:bg-[#1a222b]/95 backdrop-blur">
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-100 dark:border-border bg-gray-50/50 dark:bg-white/[0.04]">
<th className="text-left py-3 px-5 font-medium sticky left-0 z-10 bg-gray-50/95 dark:bg-surface-muted/95 backdrop-blur">
Machine
</th>
<th className="text-left py-3 px-3 font-medium">Description</th>
@ -248,7 +248,7 @@ export function InvoiceTable({ lines, assets }: { lines: BookedLine[]; assets: A
{lines.map((l) => (
<tr
key={l.id}
className={`border-b border-gray-50 dark:border-white/5 ${
className={`border-b border-gray-50 dark:border-border ${
l.isBooked ? "hover:bg-gray-50/40 dark:hover:bg-white/[0.02]" : "bg-[#D1495B]/[0.04]"
}`}
>
@ -258,12 +258,12 @@ export function InvoiceTable({ lines, assets }: { lines: BookedLine[]; assets: A
<td
className={`py-3 px-5 sticky left-0 z-10 ${
l.isBooked
? "bg-white dark:bg-[#131a21]"
? "bg-white dark:bg-surface"
: "bg-[#D1495B]/[0.04] dark:bg-[#1a1319]"
}`}
>
<div className="flex items-center gap-2 whitespace-nowrap">
<span className="text-xs text-gray-400 dark:text-gray-500 tabular-nums">{l.line_no}</span>
<span className="text-xs text-gray-400 dark:text-text-soft tabular-nums">{l.line_no}</span>
<span className="font-mono text-xs font-semibold text-[#00798C]">{l.machine_code}</span>
</div>
</td>

View file

@ -336,7 +336,7 @@ export function PlantRegisterTable({ assets: initialAssets }: { assets: Asset[]
);
return (
<section className="bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 p-6 shadow-sm">
<section className="bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border p-6 shadow-sm">
{/* Header / search / edit button */}
<div className="flex flex-wrap items-center justify-between gap-3 mb-4">
<div>
@ -352,7 +352,7 @@ export function PlantRegisterTable({ assets: initialAssets }: { assets: Asset[]
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Filter code, name, type…"
className="h-11 min-h-[44px] pl-9 pr-3 w-full sm:w-56 bg-gray-50 dark:bg-white/5 border border-transparent rounded-lg text-sm placeholder:text-gray-400 focus:bg-white dark:focus:bg-[#0f1419] focus:border-[#30638E] focus:outline-none"
className="h-11 min-h-[44px] pl-9 pr-3 w-full sm:w-56 bg-gray-50 dark:bg-white/5 border border-transparent rounded-lg text-sm placeholder:text-gray-400 focus:bg-white dark:focus:bg-white/10 focus:border-[#30638E] focus:outline-none"
/>
</div>
{!editing ? (
@ -454,7 +454,7 @@ export function PlantRegisterTable({ assets: initialAssets }: { assets: Asset[]
<span
className={cn(
"tabular-nums text-xs",
on ? "text-white/70" : "text-gray-400 dark:text-gray-500"
on ? "text-white/70" : "text-gray-400 dark:text-text-soft"
)}
>
{opt.count}
@ -504,12 +504,12 @@ export function PlantRegisterTable({ assets: initialAssets }: { assets: Asset[]
{/* Table */}
<div
className="overflow-x-auto rounded-lg border border-gray-100 dark:border-white/5 relative"
className="overflow-x-auto rounded-lg border border-gray-100 dark:border-border relative"
style={{ WebkitOverflowScrolling: "touch" }}
>
<table className="w-full text-sm">
<thead className="sticky top-0 z-10">
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-200 dark:border-white/10 bg-gray-50/95 dark:bg-[#1a222b]/95 backdrop-blur">
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-200 dark:border-white/10 bg-gray-50/95 dark:bg-surface-muted/95 backdrop-blur">
{headerCell("code", "Code")}
{headerCell("name", "Name")}
{headerCell("type", "Type")}
@ -529,14 +529,14 @@ export function PlantRegisterTable({ assets: initialAssets }: { assets: Asset[]
<tr
key={a.id}
className={cn(
"border-b border-gray-50 dark:border-white/5 hover:bg-gray-50/40 dark:hover:bg-white/[0.02] transition",
"border-b border-gray-50 dark:border-border hover:bg-gray-50/40 dark:hover:bg-white/[0.02] transition",
!shownActive && !rowDirty && "opacity-60",
errored && "bg-[#D1495B]/10",
rowDirty && "bg-[#EDAE49]/[0.06]"
)}
>
{/* Code — inline editable */}
<td className="py-2.5 px-3 sticky left-0 z-10 bg-white dark:bg-[#131a21]">
<td className="py-2.5 px-3 sticky left-0 z-10 bg-white dark:bg-surface">
{editing ? (
<div className="flex items-center gap-1.5">
{a.is_provisional && (
@ -550,7 +550,7 @@ export function PlantRegisterTable({ assets: initialAssets }: { assets: Asset[]
placeholder="—"
maxLength={15}
className={cn(
"w-24 min-h-[44px] px-2 rounded-md text-xs font-mono font-semibold border bg-gray-50 dark:bg-white/5 focus:bg-white dark:focus:bg-[#0f1419] focus:outline-none",
"w-24 min-h-[44px] px-2 rounded-md text-xs font-mono font-semibold border bg-gray-50 dark:bg-white/5 focus:bg-white dark:focus:bg-white/10 focus:outline-none",
dirty.has(draftKey(a.id, "code"))
? "border-[#EDAE49] text-[#EDAE49]"
: "border-transparent focus:border-[#30638E]"
@ -585,14 +585,14 @@ export function PlantRegisterTable({ assets: initialAssets }: { assets: Asset[]
placeholder="Machine name"
maxLength={120}
className={cn(
"w-full sm:w-44 min-h-[44px] px-2 rounded-md text-sm font-medium border bg-gray-50 dark:bg-white/5 focus:bg-white dark:focus:bg-[#0f1419] focus:outline-none",
"w-full sm:w-44 min-h-[44px] px-2 rounded-md text-sm font-medium border bg-gray-50 dark:bg-white/5 focus:bg-white dark:focus:bg-white/10 focus:outline-none",
dirty.has(draftKey(a.id, "name"))
? "border-[#EDAE49] text-[#EDAE49]"
: "border-transparent text-[#003D5B] dark:text-white focus:border-[#30638E]"
)}
/>
{!shownActive && (
<span className="text-xs uppercase tracking-wider text-gray-400 dark:text-gray-500 font-semibold shrink-0">inactive</span>
<span className="text-xs uppercase tracking-wider text-gray-400 dark:text-text-soft font-semibold shrink-0">inactive</span>
)}
</div>
) : (
@ -602,7 +602,7 @@ export function PlantRegisterTable({ assets: initialAssets }: { assets: Asset[]
<span className="ml-2 text-xs uppercase tracking-wider text-[#EDAE49] font-semibold">provisional</span>
)}
{!shownActive && (
<span className="ml-2 text-xs uppercase tracking-wider text-gray-400 dark:text-gray-500 font-semibold">inactive</span>
<span className="ml-2 text-xs uppercase tracking-wider text-gray-400 dark:text-text-soft font-semibold">inactive</span>
)}
</span>
)}
@ -619,7 +619,7 @@ export function PlantRegisterTable({ assets: initialAssets }: { assets: Asset[]
maxLength={80}
list="plant-type-options"
className={cn(
"w-full sm:w-40 min-h-[44px] px-2 rounded-md text-sm border bg-gray-50 dark:bg-white/5 focus:bg-white dark:focus:bg-[#0f1419] focus:outline-none",
"w-full sm:w-40 min-h-[44px] px-2 rounded-md text-sm border bg-gray-50 dark:bg-white/5 focus:bg-white dark:focus:bg-white/10 focus:outline-none",
dirty.has(draftKey(a.id, "type"))
? "border-[#EDAE49] text-[#EDAE49]"
: "border-transparent text-gray-600 dark:text-gray-400 focus:border-[#30638E]"
@ -641,7 +641,7 @@ export function PlantRegisterTable({ assets: initialAssets }: { assets: Asset[]
className={cn(
"relative inline-flex flex-col items-center justify-center min-h-[44px] min-w-[44px] rounded-full transition",
"active:scale-95 disabled:opacity-40",
activeIsDirty(a) && "ring-2 ring-[#EDAE49] ring-offset-1 dark:ring-offset-[#131a21]",
activeIsDirty(a) && "ring-2 ring-[#EDAE49] ring-offset-1 dark:ring-offset-surface",
shownActive
? "bg-[#00798C]/15 hover:bg-[#00798C]/25 text-[#00798C]"
: "bg-gray-100 dark:bg-white/5 hover:bg-gray-200 dark:hover:bg-white/10 text-gray-400"
@ -649,7 +649,7 @@ export function PlantRegisterTable({ assets: initialAssets }: { assets: Asset[]
>
<Power className={cn("w-4 h-4", saving && "animate-pulse")} />
{activeIsDirty(a) && (
<span className="absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-[#EDAE49] border border-white dark:border-[#131a21]" />
<span className="absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-[#EDAE49] border border-white dark:border-surface" />
)}
</button>
) : (
@ -721,7 +721,7 @@ export function PlantRegisterTable({ assets: initialAssets }: { assets: Asset[]
>
<div
onClick={(e) => e.stopPropagation()}
className="w-full sm:max-w-md bg-white dark:bg-[#131a21] rounded-2xl border border-gray-200 dark:border-white/10 shadow-2xl p-5 max-h-[90vh] overflow-y-auto"
className="w-full sm:max-w-md bg-white dark:bg-surface rounded-2xl border border-gray-200 dark:border-white/10 shadow-2xl p-5 max-h-[90vh] overflow-y-auto"
>
<div className="flex items-start gap-3 mb-4">
<div className="w-10 h-10 rounded-xl bg-[#D1495B]/10 grid place-items-center shrink-0">
@ -758,7 +758,7 @@ export function PlantRegisterTable({ assets: initialAssets }: { assets: Asset[]
<span className="text-gray-600 dark:text-gray-400 capitalize">{label}</span>
<span className={cn(
"font-semibold tabular-nums",
n > 0 ? "text-[#D1495B]" : "text-gray-400 dark:text-gray-500"
n > 0 ? "text-[#D1495B]" : "text-gray-400 dark:text-text-soft"
)}>
{n}
</span>
@ -792,7 +792,7 @@ export function PlantRegisterTable({ assets: initialAssets }: { assets: Asset[]
onChange={(e) => setConfirmText(e.target.value)}
placeholder="Type DELETE to confirm"
autoComplete="off"
className="w-full min-h-[44px] px-3 mb-3 text-sm rounded-lg border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5 focus:bg-white dark:focus:bg-[#0f1419] focus:border-[#30638E] focus:outline-none"
className="w-full min-h-[44px] px-3 mb-3 text-sm rounded-lg border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5 focus:bg-white dark:focus:bg-white/10 focus:border-[#30638E] focus:outline-none"
/>
{deleteError && impact && (

View file

@ -38,7 +38,7 @@ export function RunningView({
<span className="hidden md:inline">· sorted by {sort === "ytd" ? "total spend" : "code"}</span>
</div>
<div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-1 p-0.5 rounded-lg bg-white dark:bg-white/5 border border-gray-200 dark:border-white/5">
<div className="flex items-center gap-1 p-0.5 rounded-lg bg-white dark:bg-white/5 border border-gray-200 dark:border-border">
<button
onClick={() => setView("cards")}
aria-label="Cards view"
@ -63,7 +63,7 @@ export function RunningView({
</button>
</div>
<div className="hidden sm:flex items-center gap-1">
<span className="text-xs text-gray-400 dark:text-gray-500 mr-1">Sort:</span>
<span className="text-xs text-gray-400 dark:text-text-soft mr-1">Sort:</span>
{[
{ id: "ytd", label: "Total" },
{ id: "code", label: "Code" },
@ -119,7 +119,7 @@ function CardsView({
return (
<div
key={r.id}
className="bg-white dark:bg-[#131a21] border border-gray-100 dark:border-white/5 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition"
className="bg-white dark:bg-surface border border-gray-100 dark:border-border rounded-xl overflow-hidden shadow-sm hover:shadow-md transition"
>
<button
onClick={() => setExpanded(isOpen ? null : r.id)}
@ -156,19 +156,19 @@ function CardsView({
</button>
{isOpen && (
<div className="border-t border-gray-100 dark:border-white/5 px-4 py-4 bg-gray-50/50 dark:bg-white/[0.02]">
<div className="border-t border-gray-100 dark:border-border px-4 py-4 bg-gray-50/50 dark:bg-white/[0.04]">
{/* Mobile: 3 cols (4 rows × 3 cols = 12 months). md+: 6 cols. lg+: 12 cols. */}
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-12 gap-2 text-[11px]">
{r.months.map((v, i) => (
<div key={i} className="text-center">
<div className="text-gray-400 dark:text-gray-500 text-[10px] mb-1">{MONTH_NAMES[i]}</div>
<div className={`py-1.5 rounded tabular-nums ${v > 0 ? "bg-[#EDAE49]/5 text-[#EDAE49] dark:bg-[#EDAE49]/15" : "text-gray-300 dark:text-gray-600"}`}>
<div className="text-gray-400 dark:text-text-soft text-[10px] mb-1">{MONTH_NAMES[i]}</div>
<div className={`py-1.5 rounded tabular-nums ${v > 0 ? "bg-[#EDAE49]/5 text-[#EDAE49] dark:bg-[#EDAE49]/15" : "text-gray-300 dark:text-text-soft"}`}>
{v > 0 ? ZAR2(v) : "—"}
</div>
</div>
))}
</div>
<div className="mt-4 pt-3 border-t border-gray-100 dark:border-white/5 text-xs">
<div className="mt-4 pt-3 border-t border-gray-100 dark:border-border text-xs">
<span className="text-gray-400 uppercase tracking-wider text-[10px]">YTD total</span>
<span className="ml-2 font-semibold tabular-nums text-[#EDAE49]">{ZAR(r.ytd)}</span>
</div>
@ -189,12 +189,12 @@ function GridView({
grandTotal: number;
}) {
return (
<div className="bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 shadow-sm overflow-hidden">
<div className="bg-white dark:bg-surface rounded-2xl border border-gray-100 dark:border-border shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-y border-gray-100 dark:border-white/5 bg-gray-50/50 dark:bg-white/[0.02]">
<th className="text-left py-3 px-6 font-medium sticky left-0 bg-gray-50/95 dark:bg-[#1a222b]/95 backdrop-blur z-10">Code</th>
<tr className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider border-y border-gray-100 dark:border-border bg-gray-50/50 dark:bg-white/[0.04]">
<th className="text-left py-3 px-6 font-medium sticky left-0 bg-gray-50/95 dark:bg-surface-muted/95 backdrop-blur z-10">Code</th>
<th className="text-left py-3 px-3 font-medium">Machine</th>
{MONTH_NAMES.map((m) => (
<th key={m} className="text-right py-3 px-2 font-medium min-w-[78px]">{m}</th>
@ -204,8 +204,8 @@ function GridView({
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.id} className="border-b border-gray-50 dark:border-white/5 hover:bg-gray-50/40 dark:hover:bg-white/[0.02]">
<td className="py-2.5 px-6 sticky left-0 bg-white dark:bg-[#131a21]">
<tr key={r.id} className="border-b border-gray-50 dark:border-border hover:bg-gray-50/40 dark:hover:bg-white/[0.02]">
<td className="py-2.5 px-6 sticky left-0 bg-white dark:bg-surface">
<span className="font-mono text-xs px-2 py-1 rounded-md bg-[#00798C]/10 text-[#00798C] font-semibold">
{r.code}
</span>
@ -217,11 +217,11 @@ function GridView({
</div>
</td>
{r.months.map((v, i) => (
<td key={i} className={`py-2.5 px-2 text-right tabular-nums ${v > 0 ? "text-gray-700 dark:text-gray-300" : "text-gray-300 dark:text-gray-600"}`}>
<td key={i} className={`py-2.5 px-2 text-right tabular-nums ${v > 0 ? "text-gray-700 dark:text-gray-300" : "text-gray-300 dark:text-text-soft"}`}>
{v > 0 ? ZAR2(v) : "—"}
</td>
))}
<td className={`py-2.5 px-6 text-right tabular-nums font-semibold whitespace-nowrap ${r.ytd > 0 ? "text-[#EDAE49]" : "text-gray-300 dark:text-gray-600"}`}>
<td className={`py-2.5 px-6 text-right tabular-nums font-semibold whitespace-nowrap ${r.ytd > 0 ? "text-[#EDAE49]" : "text-gray-300 dark:text-text-soft"}`}>
{r.ytd > 0 ? ZAR(r.ytd) : "—"}
</td>
</tr>

View file

@ -4,7 +4,7 @@ export function ScrollFade({ className = "" }: { className?: string }) {
return (
<div
aria-hidden="true"
className={`pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-white to-transparent dark:from-[#131a21] ${className}`}
className={`pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l from-white to-transparent dark:from-surface ${className}`}
/>
);
}

View file

@ -54,7 +54,7 @@ export function Sidebar({ items }: { items: NavItem[] }) {
return (
<>
{/* Desktop: persistent sidebar (hidden on mobile) */}
<aside className="hidden lg:block w-64 shrink-0 bg-white dark:bg-[#0f1419] border-r border-gray-200 dark:border-white/5 min-h-[calc(100vh-4rem)]">
<aside className="hidden lg:block w-64 shrink-0 bg-white dark:bg-surface-muted border-r border-gray-200 dark:border-border min-h-[calc(100vh-4rem)]">
{renderNav()}
</aside>
@ -76,13 +76,13 @@ export function Sidebar({ items }: { items: NavItem[] }) {
)}
<aside
className={cn(
"lg:hidden fixed top-0 left-0 bottom-0 z-50 w-72 bg-white dark:bg-[#0f1419] shadow-2xl transition-transform",
"lg:hidden fixed top-0 left-0 bottom-0 z-50 w-72 bg-white dark:bg-surface-muted shadow-2xl transition-transform",
mobileOpen ? "translate-x-0" : "-translate-x-full"
)}
>
<div className="flex items-center justify-between px-4 h-16 border-b border-gray-200 dark:border-white/5">
<div className="flex items-center justify-between px-4 h-16 border-b border-gray-200 dark:border-border">
<div className="flex items-center gap-2">
<img src="/brand/logo.png" alt="Mega Plant" className="w-8 h-8 rounded-xl object-contain bg-white dark:bg-[#0b1014] shadow-sm" />
<img src="/brand/logo.png" alt="Mega Plant" className="w-8 h-8 rounded-xl object-contain bg-white dark:bg-bg shadow-sm" />
<span className="text-sm font-semibold text-[#003D5B] dark:text-white">Mega Plant</span>
</div>
<button
@ -116,7 +116,7 @@ function LeafLink({
: "text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-white/5 hover:text-gray-900 dark:hover:text-white"
)}
>
<span className={cn(active ? "text-[#30638E] dark:text-[#00798C]" : "text-gray-400")}>{icon}</span>
<span className={cn(active ? "text-accent-ink" : "text-gray-400")}>{icon}</span>
<span>{label}</span>
</Link>
);
@ -136,12 +136,12 @@ function Group({ item, active }: { item: Extract<NavItem, { kind: "group" }>; ac
: "text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-white/5 hover:text-gray-900 dark:hover:text-white"
)}
>
<span className={cn(active ? "text-[#30638E] dark:text-[#00798C]" : "text-gray-400")}>{item.icon}</span>
<span className={cn(active ? "text-accent-ink" : "text-gray-400")}>{item.icon}</span>
<span className="flex-1 text-left">{item.label}</span>
<ChevronDown className={cn("w-4 h-4 text-gray-400 transition-transform", open && "rotate-180")} />
</button>
{open && (
<div className="ml-7 mt-0.5 space-y-0.5 border-l-2 border-gray-100 dark:border-white/5 pl-2">
<div className="ml-7 mt-0.5 space-y-0.5 border-l-2 border-gray-100 dark:border-border pl-2">
{item.children.map((child) => {
const childActive = pathname === child.href;
return (