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:
@@ -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.";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user