feat(ui): view-by-view UX redesign and accessibility pass

- Overview: limits hero with progress states, spend pill, primary quick-add, mobile FAB
- Logbook: day-grouped entries with subtotals, unified filter bar, Log again,
  human source labels
- Trends: unified chart cards, tick thinning, chart aria summaries, radiogroup
  forecast control
- Settings: sectioned layout with separated danger zone, ThemePicker aria fix
- EntryModal: fieldset grouping, Field primitive, safe date validation
- AuthView: autoComplete, password visibility toggle
- StatHint popover replacing title-only tooltips; mobile stacked import preview
- Remaining inline hex styles converted to theme tokens
This commit is contained in:
nh9961
2026-07-19 20:34:10 +00:00
parent 733c6cbae8
commit 3706021326
19 changed files with 945 additions and 299 deletions
+34 -6
View File
@@ -3,7 +3,7 @@ import { FileSpreadsheet, Loader2, X } from "lucide-react";
import { humanDateTime } from "../lib/metrics";
import { MiniMetric } from "./MetricTiles";
import { Modal } from "./Modal";
import type { ImportPreview } from "../types";
import type { ImportPreview, ImportPreviewRow } from "../types";
type ImportPreviewModalProps = {
busy: boolean;
@@ -50,7 +50,7 @@ export function ImportPreviewModal({
<MiniMetric label="Invalid" value={`${invalidRows.length}`} accent="#ff3448" />
</div>
<div className="max-h-[48vh] overflow-auto rounded-lg border border-white/10">
<div className="hidden max-h-[48vh] overflow-auto rounded-lg border border-white/10 sm:block">
<table className="w-full min-w-[760px] border-collapse text-left text-sm">
<thead className="sticky top-0 bg-[#0d142c] text-xs uppercase tracking-[0.14em] text-slate-400">
<tr>
@@ -68,21 +68,37 @@ export function ImportPreviewModal({
<tr key={row.rowNumber} className="border-t border-white/10">
<td className="px-3 py-3 text-slate-400">{row.rowNumber}</td>
<td className="px-3 py-3">
<span className={`rounded px-2 py-1 text-xs font-semibold ${row.errors.length ? "bg-red-500/15 text-red-100" : row.duplicate ? "bg-amber-300/15 text-amber-100" : "bg-cyan-300/15 text-cyan-100"}`}>
{row.errors.length ? "Invalid" : row.duplicate ? "Duplicate" : "Ready"}
</span>
<ImportStatusBadge invalid={row.errors.length > 0} duplicate={row.duplicate} />
</td>
<td className="px-3 py-3 text-slate-300">{row.entry ? humanDateTime(row.entry.dateTime) : "-"}</td>
<td className="px-3 py-3 text-white">{row.entry?.flavour ?? "-"}</td>
<td className="px-3 py-3 text-slate-300">{row.entry ? `${row.entry.sizeMl}ml` : "-"}</td>
<td className="px-3 py-3 text-slate-300">{row.entry?.cans ?? "-"}</td>
<td className="px-3 py-3 text-slate-400">{row.errors.join(" ") || row.duplicateReason || "Looks good."}</td>
<td className="px-3 py-3 text-slate-400">{rowMessage(row)}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="grid max-h-[48vh] gap-2 overflow-auto sm:hidden">
{preview.rows.map((row) => (
<article key={row.rowNumber} className="rounded-lg border border-white/10 p-3">
<div className="flex items-center justify-between gap-2">
<span className="text-xs text-slate-400">Row {row.rowNumber}</span>
<ImportStatusBadge invalid={row.errors.length > 0} duplicate={row.duplicate} />
</div>
<p className="mt-2 text-sm text-white">{row.entry?.flavour ?? "No entry parsed"}</p>
{row.entry ? (
<p className="mt-1 text-xs text-slate-400">
{humanDateTime(row.entry.dateTime)} · {row.entry.sizeMl}ml · {row.entry.cans} can{row.entry.cans === 1 ? "" : "s"}
</p>
) : null}
<p className="mt-1 text-xs text-slate-400">{rowMessage(row)}</p>
</article>
))}
</div>
<div className="mt-6 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<button className="secondary-button justify-center" type="button" onClick={onClose}>
Cancel
@@ -97,3 +113,15 @@ export function ImportPreviewModal({
</Modal>
);
}
function ImportStatusBadge({ invalid, duplicate }: { invalid: boolean; duplicate: boolean }) {
return (
<span className={`rounded px-2 py-1 text-xs font-semibold ${invalid ? "bg-red-500/15 text-red-100" : duplicate ? "bg-amber-300/15 text-amber-100" : "bg-cyan-300/15 text-cyan-100"}`}>
{invalid ? "Invalid" : duplicate ? "Duplicate" : "Ready"}
</span>
);
}
function rowMessage(row: ImportPreviewRow) {
return row.errors.join(" ") || row.duplicateReason || "Looks good.";
}