Skip to content

Commit fd21de9

Browse files
committed
feat(ui): table views for users, the journal and both payment lists
Closes #64, and carries the same treatment to the three other lists where every row holds the same handful of facts. Each keeps its cards; the table is the default and the choice is remembered per list, in this browser, because it is a working preference rather than a per-visit one. Users: user, status, online, traffic, expiry, devices, groups, actions. Journal: what happened, to whom, the details, who did it, when. Pending orders keep both decisions in the row; history is read-only and dated by when an order settled, falling back to when it was raised for one that never did. Decisions worth recording: - Narrow screens drop columns rather than scroll. A horizontal scrollbar on a phone is worse than three fewer columns, and everything hidden is one tap away in the detail sheet — the user's name is the button that opens it. Which columns survive lives next to the cells they match. - Row actions are icons. Two labelled buttons per row spent most of the row's width on the two things every row repeats; the words survive as the accessible name and the hover title. IconButton gained a link form, because a control that navigates has to BE an anchor — middle-click, "open in new tab" and the status-bar preview all come from the element, not a click handler — and target=_blank now carries rel=noopener. - The groups column shows the first badge and a "+N" chip carrying the rest as hover text; one account in four groups used to stretch every row on the page. - Icon-only controls set aria-label from their title. Hover text never reaches a touch device and a screen reader announces nothing without it. - One definition, not four: TableShell/THead/TR/TD and ViewSwitch live in ui.tsx, and useViewMode owns the persistence, so the surface and the toggle cannot drift apart. Verified in the built bundle — the surface class list appears exactly once. - The per-user journal inside the detail modal keeps its cards: it is narrow, and there is nothing to align it with. Storage is guarded everywhere: Safari in private mode throws on access, and a display preference is never worth failing a render over.
1 parent ec41ee2 commit fd21de9

8 files changed

Lines changed: 786 additions & 96 deletions

File tree

web/src/EventsPanel.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import { useCallback, useEffect, useState } from "react";
22
import { useTranslation } from "react-i18next";
33
import { getEventCatalog, listEvents } from "./api";
44
import { actionMeta, actorOptions, EventList } from "./events";
5+
import { useViewMode } from "./hooks";
56
import { errMessage, notifyError } from "./notify";
6-
import { Select, SettingCard } from "./ui";
7+
import { Select, SettingCard, ViewSwitch } from "./ui";
78

89
// The global audit trail: every recorded action across all users, newest first,
910
// filterable by action and by who performed it.
1011
export function EventsPanel() {
1112
const { t } = useTranslation();
13+
const [view, setView] = useViewMode("events");
1214
const [keys, setKeys] = useState<string[]>([]);
1315
const [action, setAction] = useState("");
1416
const [actor, setActor] = useState("");
@@ -39,6 +41,15 @@ export function EventsPanel() {
3941
<SettingCard
4042
title={t("events.title")}
4143
description={t("events.description", { days: RETENTION_DAYS })}
44+
action={
45+
<ViewSwitch
46+
value={view}
47+
onChange={setView}
48+
tableLabel={t("usersPanel.viewTable")}
49+
cardsLabel={t("usersPanel.viewCards")}
50+
/>
51+
}
52+
stackAction
4253
>
4354
<div className="flex flex-col gap-4">
4455
<div className="grid gap-3 sm:grid-cols-2">
@@ -55,7 +66,7 @@ export function EventsPanel() {
5566
data={actorOptions()}
5667
/>
5768
</div>
58-
<EventList load={load} showUser />
69+
<EventList load={load} showUser table={view === "table"} />
5970
</div>
6071
</SettingCard>
6172
);

0 commit comments

Comments
 (0)