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:
parent
58c972c4c9
commit
999feb37fa
5 changed files with 38 additions and 32 deletions
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
10
components/ScrollFade.tsx
Normal 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}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue