Info page: show the app version, two-column layout on wide screens

An About panel shows the installed version (from Tauri at runtime, since
release builds stamp it at build time; dev builds are labelled as such).
On wide screens the page splits into panels in two columns — card
database, library stats and libraries on the left; sync and about on the
right — while narrow screens keep the single-column order. The sync panel
is now headed "Sync" and the Info page's summary "Library stats".

Closes #22
This commit is contained in:
2026-10-23 23:27:58 -05:00
parent 52873a2180
commit 7415289257
4 changed files with 111 additions and 38 deletions

View File

@@ -1,4 +1,5 @@
import { useEffect, useRef, useState, useCallback, ReactNode } from "react";
import { getVersion } from "@tauri-apps/api/app";
import { listen } from "@tauri-apps/api/event";
import { open } from "@tauri-apps/plugin-dialog";
import {
@@ -168,10 +169,12 @@ function LibrarySummary({
ready,
libStats,
status,
title = "Your library",
}: {
ready: boolean;
libStats: LibraryStats | null;
status: Status | null;
title?: string;
}) {
if (!ready) {
return (
@@ -185,7 +188,7 @@ function LibrarySummary({
<div className="lib-stats">
<div className="lib-stats-head">
<span className="status-dot on" />
<span className="lib-stats-title">Your library</span>
<span className="lib-stats-title">{title}</span>
</div>
{!libStats ? (
<div className="lib-stats-loading">Loading…</div>
@@ -242,6 +245,12 @@ export default function App() {
const [status, setStatus] = useState<Status | null>(null);
const [libStats, setLibStats] = useState<LibraryStats | null>(null);
const [tab, setTab] = usePersistedState<Tab>("active-tab", "add");
// The installed build's version, for the Info page. Release builds stamp
// it at build time, so it comes from Tauri rather than package.json.
const [appVersion, setAppVersion] = useState<string | null>(null);
useEffect(() => {
getVersion().then(setAppVersion).catch(() => setAppVersion(null));
}, []);
// A deck a chip elsewhere asked to open; DeckView picks it up and clears it.
const [openDeckId, setOpenDeckId] = useState<number | null>(null);
// Deck chips only carry the deck's name, which is unique per library.
@@ -798,41 +807,64 @@ export default function App() {
// rail it normally sits in is replaced by the bottom nav);
// on desktop those also appear in the topbar and rail, but sync
// lives only here.
<div className="panel info-panel">
<h2 style={{ margin: 0 }}>Card database</h2>
<div className="row" style={{ gap: "var(--space-3)", flexWrap: "wrap" }}>
<button className="primary" onClick={updateCardData} disabled={busy}>
{ICON_DOWNLOAD}
{busy ? "Downloading…" : "Update card data"}
</button>
<button onClick={importReferenceDb} disabled={busy}>
{ICON_OPEN_LIBRARY}
Import card data…
</button>
// Two columns on wide screens — card data, library stats and the
// libraries on the left; sync and the version on the right. On
// narrow ones the columns dissolve into one list in the order
// set by `.info-*` (see styles.css).
<div className="info-grid">
<div className="info-col">
<section className="panel info-section info-db">
<h2 style={{ margin: 0 }}>Card database</h2>
<div className="row" style={{ gap: "var(--space-3)", flexWrap: "wrap" }}>
<button className="primary" onClick={updateCardData} disabled={busy}>
{ICON_DOWNLOAD}
{busy ? "Downloading…" : "Update card data"}
</button>
<button onClick={importReferenceDb} disabled={busy}>
{ICON_OPEN_LIBRARY}
Import card data…
</button>
</div>
</section>
<section className="panel info-section info-summary">
<LibrarySummary ready={ready} libStats={libStats} status={status} title="Library stats" />
</section>
<section className="panel info-section info-libraries">
<LibrariesPanel
libraries={libraries}
signedIn={sync?.signed_in ?? false}
onChanged={librariesChanged}
/>
</section>
</div>
<div className="info-col">
<section className="panel info-section info-sync">
{sync === null ? (
<>
<h2 style={{ margin: 0 }}>Sync</h2>
<div className="hint">Checking…</div>
</>
) : (
<SyncPanel
sync={sync}
setSync={setSync}
syncing={syncing}
runSync={runSync}
onLibraryReplaced={refreshEverything}
/>
)}
</section>
<section className="panel info-section info-about">
<h2 style={{ margin: 0 }}>About</h2>
<div className="info-about-row">
<span className="muted">mtgc version</span>
<span className="info-version">
{appVersion ?? "unknown"}
{import.meta.env.DEV && <span className="muted"> · development build</span>}
</span>
</div>
</section>
</div>
<hr className="deck-section-divider" style={{ margin: 0 }} />
<LibrariesPanel
libraries={libraries}
signedIn={sync?.signed_in ?? false}
onChanged={librariesChanged}
/>
<hr className="deck-section-divider" style={{ margin: 0 }} />
{sync === null ? (
<>
<h2 style={{ margin: 0 }}>Your library</h2>
<div className="hint">Checking…</div>
</>
) : (
<SyncPanel
sync={sync}
setSync={setSync}
syncing={syncing}
runSync={runSync}
onLibraryReplaced={refreshEverything}
/>
)}
<hr className="deck-section-divider" style={{ margin: 0 }} />
<LibrarySummary ready={ready} libStats={libStats} status={status} />
</div>
)}
</div>

View File

@@ -208,7 +208,7 @@ export function SyncPanel({
return (
<>
<h2 style={{ margin: 0 }}>Your library</h2>
<h2 style={{ margin: 0 }}>Sync</h2>
{!sync.signed_in ? (
<>

View File

@@ -805,11 +805,51 @@ input[type="checkbox"] {
/* Mobile's Info tab — card-database management, which lives in the topbar
instead on desktop (see INFO_TAB in App.tsx). */
.info-panel {
/* Info page: a column of panels on narrow screens, two columns on wide ones.
Below the breakpoint the two `.info-col` wrappers dissolve (display:
contents) so `order` can interleave their panels into the reading order
the single column always had: card data, libraries, sync, summary, about. */
.info-grid {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.info-col {
display: contents;
}
.info-section {
display: flex;
flex-direction: column;
gap: var(--space-3);
max-width: 480px;
min-width: 0;
}
.info-db { order: 1; }
.info-libraries { order: 2; }
.info-sync { order: 3; }
.info-summary { order: 4; }
.info-about { order: 5; }
@media (min-width: 900px) {
.info-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
align-items: start;
max-width: 1200px;
}
.info-col {
display: flex;
flex-direction: column;
gap: var(--space-4);
min-width: 0;
}
}
.info-about-row {
display: flex;
justify-content: space-between;
gap: var(--space-3);
}
.info-version {
font-variant-numeric: tabular-nums;
font-weight: 600;
}
/* Top-bar sync state; opens the Info tab. Quiet when all is well, so it
only draws the eye when something needs attention. */

1
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />