Mobile: make the invoice tables readable at 375px

Audited the invoice UI against the Round 4 checklist and fixed four things.

The summary table carried eight columns at a fixed 820px min-width, which at
375px is a long sideways scroll through three figures before reaching the
status badge — the one thing that needs acting on. Below sm it now keeps only
Invoice, Site, Total and Status, and drops to a 520px min-width, so the useful
columns are readable without scrolling. Period, line count and the ex-VAT/VAT
split are one tap away in the expanded detail, and the full eight columns
return from sm up where there is room.

The summary table also had no right-edge fade. Rule 4 asks for a visible
affordance on any wide table and it was simply missing, so scrolling sideways
looked like the table ended. Same for the month grid on /income, which was also
missing -webkit-overflow-scrolling. Both now share components/ScrollFade.

Two 10px labels in the app header ("Overheads 2026", "FY") were invisible at
375px but visible from 640/768px — and 768x1024 is one of the two sizes the
checklist tests, so they were a rule-2 violation at tablet width. Bumped to
12px. PlantRegisterTable's 10px filter count was checked and left alone: it
sits inside hidden lg:flex, so it never renders on mobile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
David 2026-10-02 15:29:00 +02:00
parent 58c972c4c9
commit 999feb37fa
5 changed files with 38 additions and 32 deletions

View file

@ -109,7 +109,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
/>
<div className="leading-tight hidden sm:block">
<div className="text-sm font-semibold text-[#003D5B] dark:text-white">Mega Plant</div>
<div className="text-[10px] text-gray-500 dark:text-gray-400 uppercase tracking-wider">Overheads 2026</div>
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider">Overheads 2026</div>
</div>
</Link>
@ -119,7 +119,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<div className="flex items-center gap-3 shrink-0">
<div className="hidden md:flex items-center gap-2 px-3 py-1.5 rounded-xl bg-gradient-to-br from-[#30638E] to-[#003D5B] text-white shadow-lg shadow-blue-900/20">
<span className="text-[10px] uppercase tracking-wider text-white/70">FY</span>
<span className="text-xs uppercase tracking-wider text-white/70">FY</span>
<span className="text-sm font-semibold">2026</span>
</div>
<ThemeToggle />

View file

@ -6,6 +6,7 @@ import { Sparkline } from "@/components/Sparkline";
import { MONTH_NAMES } from "@/lib/data";
import type { IncomeRow } from "@/lib/rows";
import { ZAR, ZAR2 } from "@/lib/utils";
import { ScrollFade } from "@/components/ScrollFade";
export function IncomeView({ rows, palette }: { rows: IncomeRow[]; palette: { income: string; profit: string; loss: string } }) {
const [view, setView] = useState<"cards" | "grid">("cards");
@ -202,7 +203,7 @@ function CardsView({
{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]">
{/* 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-[11px]">
<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>
@ -220,17 +221,17 @@ function CardsView({
</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>
<div className="text-gray-400 uppercase tracking-wider text-[10px]">YTD income</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>
</div>
<div>
<div className="text-gray-400 uppercase tracking-wider text-[10px]">Fixed + running</div>
<div className="text-gray-400 uppercase tracking-wider text-xs">Fixed + running</div>
<div className="font-semibold tabular-nums text-[#003D5B] dark:text-white">
{ZAR(r.fixed + r.running)}
</div>
</div>
<div>
<div className="text-gray-400 uppercase tracking-wider text-[10px]">Profit</div>
<div className="text-gray-400 uppercase tracking-wider text-xs">Profit</div>
<div className={`font-semibold tabular-nums ${positive ? "text-[#00798C]" : "text-[#D1495B]"}`}>
{positive ? "+" : ""}{ZAR(r.ytdProfit)}
</div>
@ -247,9 +248,9 @@ function CardsView({
function GridView({ rows }: { rows: IncomeRow[] }) {
return (
<div className="bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 shadow-sm overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<div className="relative bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 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>
@ -317,6 +318,7 @@ function GridView({ rows }: { rows: IncomeRow[] }) {
</tfoot>
</table>
</div>
<ScrollFade />
</div>
);
}

View file

@ -7,6 +7,7 @@ import { BOOK_INCLUSIVE_VAT, BOOKED_LONG_LABEL } from "@/lib/invoice-booking";
import { ZAR, ZAR2 } from "@/lib/utils";
import { bookedAmount } from "@/lib/invoice-booking";
import { InvoiceSummary } from "@/components/InvoiceTable";
import { ScrollFade } from "@/components/ScrollFade";
// One row per invoice. Clicking a row expands the full line-item detail underneath it,
// so the common case (scan for problems) is a single table read and the detail is opt-in.
@ -61,19 +62,19 @@ export function InvoiceList({
<div className="bg-white dark:bg-[#131a21] rounded-2xl border border-gray-100 dark:border-white/5 shadow-sm overflow-hidden">
{/* Rule 1 + 4: scrolls inside its own container; page never scrolls sideways. */}
<div className="overflow-x-auto" style={{ WebkitOverflowScrolling: "touch" }}>
<table className="w-full text-sm min-w-[820px]">
<div className="relative overflow-x-auto" style={{ WebkitOverflowScrolling: "touch" }}>
<table className="w-full text-sm min-w-[520px] 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">
Invoice
</th>
<th className="text-left py-3 px-3 font-medium whitespace-nowrap">Site</th>
<th className="text-left py-3 px-3 font-medium whitespace-nowrap">Period</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="text-right py-3 px-3 font-medium whitespace-nowrap">Revenue ex VAT</th>
<th className="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 incl VAT</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>
</tr>
</thead>
@ -113,6 +114,7 @@ export function InvoiceList({
</tbody>
</table>
</div>
<ScrollFade />
</div>
</>
);
@ -157,7 +159,7 @@ function InvoiceRow({
</span>
</button>
</td>
<td className="py-2 px-3 whitespace-nowrap">
<td className="hidden sm:table-cell py-2 px-3 whitespace-nowrap">
{invoice.site ? (
<span className="inline-flex items-center gap-1.5 text-sm text-[#003D5B] dark:text-white">
<MapPin className="w-3.5 h-3.5 text-[#EDAE49] shrink-0" />
@ -167,14 +169,16 @@ function InvoiceRow({
<span className="text-sm text-gray-400 dark:text-gray-500">—</span>
)}
</td>
<td className="py-2 px-3 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap tabular-nums">
<td className="hidden sm:table-cell py-2 px-3 text-sm text-gray-500 dark:text-gray-400 whitespace-nowrap tabular-nums">
{invoice.period_from.slice(0, 10)} → {invoice.period_to.slice(0, 10)}
</td>
<td className="py-2 px-3 text-right tabular-nums text-gray-700 dark:text-gray-300">{lineCount}</td>
<td className="py-2 px-3 text-right tabular-nums font-medium text-[#003D5B] dark:text-white whitespace-nowrap">
<td className="hidden sm:table-cell py-2 px-3 text-right tabular-nums text-gray-700 dark:text-gray-300">
{lineCount}
</td>
<td className="hidden sm:table-cell py-2 px-3 text-right tabular-nums font-medium text-[#003D5B] dark:text-white whitespace-nowrap">
{ZAR2(invoice.total_excl)}
</td>
<td className="py-2 px-3 text-right tabular-nums text-gray-500 dark:text-gray-400 whitespace-nowrap">
<td className="hidden sm:table-cell py-2 px-3 text-right tabular-nums text-gray-500 dark:text-gray-400 whitespace-nnowrap">
{ZAR2(invoice.vat)}
</td>
<td className="py-2 px-3 text-right tabular-nums font-semibold text-[#003D5B] dark:text-white whitespace-nowrap">

View file

@ -2,19 +2,9 @@ import type { Asset, BookedLine, Invoice } from "@/lib/data";
import { ZAR2 } from "@/lib/utils";
import { AlertTriangle, CheckCircle2 } from "lucide-react";
import { BookingSelect } from "@/components/BookingSelect";
import { ScrollFade } from "@/components/ScrollFade";
import { InvoiceUploader, AttachedDocument } from "@/components/InvoiceUploader";
// Right-edge fade so a horizontally scrollable table reads as scrollable on touch.
// pointer-events-none so it never swallows a tap on the row underneath.
function ScrollFade() {
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]"
/>
);
}
export function InvoiceSummary({
invoice,
lines,

10
components/ScrollFade.tsx Normal file
View file

@ -0,0 +1,10 @@
// Right-edge fade telling you a wide table scrolls sideways.
// pointer-events-none so it never swallows a tap on the row underneath.
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}`}
/>
);
}