Skip to content

Commit 273e8b6

Browse files
committed
feat(accounts): add table/card view toggle on desktop
Card mode renders the existing mobile card grid up to 5 per row on xl screens; preference is persisted in localStorage so the choice survives reloads.
1 parent 9dac9c0 commit 273e8b6

3 files changed

Lines changed: 77 additions & 3 deletions

File tree

frontend/src/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,8 @@
622622
"groupDeleteForce": "Force delete",
623623
"columnSettings": "Columns",
624624
"columnReset": "Reset columns",
625+
"viewModeTable": "Table",
626+
"viewModeGrid": "Cards",
625627
"schedulerPreviewTitle": "Current Scheduler Preview",
626628
"schedulerPreviewRawScore": "Raw Score",
627629
"schedulerPreviewDispatchScore": "Dispatch Score",

frontend/src/locales/zh.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,8 @@
622622
"groupDeleteForce": "强制删除",
623623
"columnSettings": "列设置",
624624
"columnReset": "重置列",
625+
"viewModeTable": "表格",
626+
"viewModeGrid": "卡片",
625627
"schedulerPreviewTitle": "当前调度预览",
626628
"schedulerPreviewRawScore": "原始分",
627629
"schedulerPreviewDispatchScore": "当前调度分",

frontend/src/pages/Accounts.tsx

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ import {
7474
Hourglass,
7575
X,
7676
SlidersHorizontal,
77+
LayoutGrid,
78+
Rows3,
7779
} from "lucide-react";
7880
import { useTranslation } from "react-i18next";
7981
import AccountUsageModal from "../components/AccountUsageModal";
@@ -165,6 +167,27 @@ function persistAccountVisibleColumns(
165167
}
166168
}
167169

170+
const ACCOUNT_VIEW_MODE_KEY = "codex2api:accounts:view-mode";
171+
type AccountViewMode = "table" | "grid";
172+
173+
function getInitialAccountViewMode(): AccountViewMode {
174+
try {
175+
const raw = window.localStorage.getItem(ACCOUNT_VIEW_MODE_KEY);
176+
if (raw === "grid" || raw === "table") return raw;
177+
} catch {
178+
// ignore
179+
}
180+
return "table";
181+
}
182+
183+
function persistAccountViewMode(mode: AccountViewMode) {
184+
try {
185+
window.localStorage.setItem(ACCOUNT_VIEW_MODE_KEY, mode);
186+
} catch {
187+
// ignore
188+
}
189+
}
190+
168191
function getInitialAnalysisVisibility(): boolean {
169192
try {
170193
return (
@@ -496,6 +519,9 @@ export default function Accounts() {
496519
const [visibleColumns, setVisibleColumns] = useState<
497520
Record<AccountTableColumn, boolean>
498521
>(getInitialAccountVisibleColumns);
522+
const [viewMode, setViewMode] = useState<AccountViewMode>(
523+
getInitialAccountViewMode,
524+
);
499525
const fileInputRef = useRef<HTMLInputElement>(null);
500526
const jsonInputRef = useRef<HTMLInputElement>(null);
501527
const jsonAtInputRef = useRef<HTMLInputElement>(null);
@@ -636,6 +662,10 @@ export default function Accounts() {
636662
persistAccountVisibleColumns(visibleColumns);
637663
}, [visibleColumns]);
638664

665+
useEffect(() => {
666+
persistAccountViewMode(viewMode);
667+
}, [viewMode]);
668+
639669
useEffect(() => {
640670
if (groupFilter === null) return;
641671
if (!allGroups.some((group) => group.id === groupFilter)) {
@@ -2674,7 +2704,39 @@ export default function Accounts() {
26742704
<FolderOpen className="size-3.5" />
26752705
{t("accounts.groupManage")}
26762706
</Button>
2677-
<div className="ml-auto shrink-0">
2707+
<div className="ml-auto flex shrink-0 items-center gap-1.5">
2708+
<div className="hidden lg:inline-flex items-center rounded-md border border-border bg-muted/50 p-0.5">
2709+
<button
2710+
type="button"
2711+
onClick={() => setViewMode("table")}
2712+
title={t("accounts.viewModeTable")}
2713+
aria-label={t("accounts.viewModeTable")}
2714+
aria-pressed={viewMode === "table"}
2715+
className={`inline-flex items-center gap-1 rounded-sm px-2 py-1 text-[12px] font-medium transition-colors ${
2716+
viewMode === "table"
2717+
? "bg-background text-foreground shadow-sm"
2718+
: "text-muted-foreground hover:text-foreground"
2719+
}`}
2720+
>
2721+
<Rows3 className="size-3.5" />
2722+
{t("accounts.viewModeTable")}
2723+
</button>
2724+
<button
2725+
type="button"
2726+
onClick={() => setViewMode("grid")}
2727+
title={t("accounts.viewModeGrid")}
2728+
aria-label={t("accounts.viewModeGrid")}
2729+
aria-pressed={viewMode === "grid"}
2730+
className={`inline-flex items-center gap-1 rounded-sm px-2 py-1 text-[12px] font-medium transition-colors ${
2731+
viewMode === "grid"
2732+
? "bg-background text-foreground shadow-sm"
2733+
: "text-muted-foreground hover:text-foreground"
2734+
}`}
2735+
>
2736+
<LayoutGrid className="size-3.5" />
2737+
{t("accounts.viewModeGrid")}
2738+
</button>
2739+
</div>
26782740
<ColumnSettingsMenu
26792741
columns={visibleColumns}
26802742
onToggle={(column) =>
@@ -2818,7 +2880,13 @@ export default function Accounts() {
28182880
</Button>
28192881
}
28202882
>
2821-
<div className="grid gap-3 lg:hidden">
2883+
<div
2884+
className={
2885+
viewMode === "grid"
2886+
? "grid gap-3 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5"
2887+
: "grid gap-3 lg:hidden"
2888+
}
2889+
>
28222890
{pagedAccounts.map((account, index) => {
28232891
const isSelected = selected.has(account.id);
28242892
return (
@@ -2851,7 +2919,9 @@ export default function Accounts() {
28512919
})}
28522920
</div>
28532921

2854-
<div className="data-table-shell hidden lg:block">
2922+
<div
2923+
className={`data-table-shell hidden lg:block ${viewMode === "grid" ? "lg:hidden" : ""}`}
2924+
>
28552925
<Table>
28562926
<TableHeader>
28572927
<TableRow>

0 commit comments

Comments
 (0)