Card preview modal, library quantity steppers, and UI polish

- Fix keyboard-nav selection highlight (search results, deck list): the
  Discord-styling rewrite dropped the --panel-2 CSS variable it relied
  on, so ↑/↓ moved selection but rendered no visible highlight.
- Add click-to-preview: clicking a card thumbnail/tile now opens a
  larger centered modal (list and tile views), with a flip button for
  double-faced cards' back face. Hover preview is unchanged and clears
  when a click-preview opens.
- Add a QtyStepper component (rounded [− N +] pill, with an `overlay`
  variant for tile corners) and use it consistently for owned-copy
  counts on Add Cards, and the Library list and tile views.
- Add matching +/- steppers to the Library views; decrementing to 0
  dims the row for a 5s grace period (still fully interactive) before
  it drops out, so an accidental removal can be undone with +.
- Topbar: fill remaining width with the library path (no more premature
  truncation), add icons to Open/New Library and Download/Update card
  data, and drop the trailing "…" from the library buttons.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 20:55:18 -05:00
parent e18664ea8c
commit eefe1dc954
7 changed files with 448 additions and 60 deletions

View File

@@ -9,11 +9,11 @@ import { DeckView } from "./views/DeckView";
type Tab = "add" | "library" | "decks";
const svg = (children: ReactNode) => (
const svg = (children: ReactNode, size = 20) => (
<svg
viewBox="0 0 24 24"
width="20"
height="20"
width={size}
height={size}
fill="none"
stroke="currentColor"
strokeWidth="2"
@@ -24,6 +24,27 @@ const svg = (children: ReactNode) => (
</svg>
);
const ICON_OPEN_LIBRARY = svg(
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />,
16,
);
const ICON_NEW_LIBRARY = svg(
<>
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
<line x1="12" y1="11" x2="12" y2="17" />
<line x1="9" y1="14" x2="15" y2="14" />
</>,
16,
);
const ICON_DOWNLOAD = svg(
<>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</>,
16,
);
const NAV: { id: Tab; label: string; icon: ReactNode }[] = [
{ id: "add", label: "Add Cards", icon: svg(<path d="M12 5v14M5 12h14" />) },
{
@@ -124,14 +145,25 @@ export default function App() {
await api.cancelUpdateCardData();
}
async function switchLibrary(path: string) {
try {
setStatus(await api.setLibraryPath(path));
} catch (err) {
setProgress({
stage: "error",
message: `Couldn't open that library: ${String(err)}`,
overall_percent: 0,
stages: lastProgress.current?.stages ?? [],
});
}
}
async function openLibrary() {
const path = await open({
title: "Open an existing library.db",
filters: [{ name: "SQLite", extensions: ["db", "sqlite"] }],
});
if (typeof path === "string") {
setStatus(await api.setLibraryPath(path));
}
if (typeof path === "string") await switchLibrary(path);
}
async function newLibrary() {
@@ -140,9 +172,7 @@ export default function App() {
defaultPath: "library.db",
filters: [{ name: "SQLite", extensions: ["db", "sqlite"] }],
});
if (typeof path === "string") {
setStatus(await api.setLibraryPath(path));
}
if (typeof path === "string") await switchLibrary(path);
}
const ready = status?.reference_ready ?? false;
@@ -177,19 +207,19 @@ export default function App() {
<div className="main">
<header className="topbar">
{status && (
<span className="path" title={status.library_path}>
{status.library_path}
</span>
)}
<span className="spacer" />
<span className="path" title={status?.library_path ?? ""}>
{status?.library_path ?? ""}
</span>
<button onClick={openLibrary} disabled={busy}>
Open Library…
{ICON_OPEN_LIBRARY}
Open Library
</button>
<button onClick={newLibrary} disabled={busy}>
New Library…
{ICON_NEW_LIBRARY}
New Library
</button>
<button onClick={updateCardData} disabled={busy} className="primary">
{ICON_DOWNLOAD}
{ready ? "Update card data" : "Download card data"}
</button>
</header>
@@ -249,6 +279,7 @@ export default function App() {
be synced.
</p>
<button className="primary big" onClick={updateCardData} disabled={busy}>
{ICON_DOWNLOAD}
{busy ? "Downloading…" : "Download card data"}
</button>
</div>