Add deck synergy suggestions, a responsive deck layout, and undoable library bulk-add
Suggestion scoring is fully offline (tribal/mechanic/archetype/mana-base signals derived at ingest, no external synergy database), with an "unusual picks" mode that surfaces underappreciated cards instead of staples. The deck page's top panels now use a CSS grid so wide windows don't waste horizontal space. Library's bulk add-to-deck can clobber existing deck quantities, so it now offers an undo (snackbar + command palette, the latter with no timeout) that restores each card's prior state exactly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
54
src/App.tsx
54
src/App.tsx
@@ -8,6 +8,7 @@ import {
|
||||
IconButton,
|
||||
Icons,
|
||||
StatTile,
|
||||
UndoBar,
|
||||
usePersistedState,
|
||||
} from "./components";
|
||||
import { CommandPalette } from "./CommandPalette";
|
||||
@@ -133,6 +134,12 @@ function segmentFill(stages: IngestStageInfo[], index: number, overallPercent: n
|
||||
return ((overallPercent - startPct) / (endPct - startPct)) * 100;
|
||||
}
|
||||
|
||||
// How long the floating "Undo" snackbar stays up before auto-hiding itself.
|
||||
// This only controls the snackbar's visibility — the underlying undo action
|
||||
// stays available in the command palette well past this (see `undo` below),
|
||||
// so missing the snackbar isn't the last chance to undo.
|
||||
const UNDO_SNACKBAR_MS = 8000;
|
||||
|
||||
export default function App() {
|
||||
const [status, setStatus] = useState<Status | null>(null);
|
||||
const [libStats, setLibStats] = useState<LibraryStats | null>(null);
|
||||
@@ -140,6 +147,44 @@ export default function App() {
|
||||
const [progress, setProgress] = useState<IngestProgress | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const lastProgress = useRef<IngestProgress | null>(null);
|
||||
// Lives here (not in LibraryBrowser) so it's reachable from the command
|
||||
// palette, which is mounted at the app level and outlives any one tab's
|
||||
// view across tab switches. Unlike the snackbar, this has no timeout —
|
||||
// it's only replaced by a newer undo offer, run, or explicitly dismissed —
|
||||
// so an undo that's missed in the snackbar is still reachable via Alt+/.
|
||||
const [undo, setUndo] = useState<{ message: string; run: () => void | Promise<void> } | null>(
|
||||
null,
|
||||
);
|
||||
const [snackbarVisible, setSnackbarVisible] = useState(false);
|
||||
const snackbarTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
// Bumped once an undo actually runs, so views mounted elsewhere than where
|
||||
// the undo was offered (e.g. the Decks tab, when the undo came from a
|
||||
// Library bulk add-to-deck) know to refetch instead of showing stale data
|
||||
// until the user leaves and comes back.
|
||||
const [deckRefreshSignal, setDeckRefreshSignal] = useState(0);
|
||||
|
||||
function offerUndo(message: string, run: () => void | Promise<void>) {
|
||||
if (snackbarTimer.current) clearTimeout(snackbarTimer.current);
|
||||
setUndo({ message, run });
|
||||
setSnackbarVisible(true);
|
||||
snackbarTimer.current = setTimeout(() => setSnackbarVisible(false), UNDO_SNACKBAR_MS);
|
||||
}
|
||||
|
||||
// Explicit "×" on the snackbar: reject the whole offer, not just hide it.
|
||||
function dismissUndo() {
|
||||
if (snackbarTimer.current) clearTimeout(snackbarTimer.current);
|
||||
setSnackbarVisible(false);
|
||||
setUndo(null);
|
||||
}
|
||||
|
||||
function runUndo() {
|
||||
if (!undo) return;
|
||||
const { run } = undo;
|
||||
if (snackbarTimer.current) clearTimeout(snackbarTimer.current);
|
||||
setSnackbarVisible(false);
|
||||
setUndo(null);
|
||||
Promise.resolve(run()).then(() => setDeckRefreshSignal((v) => v + 1));
|
||||
}
|
||||
|
||||
const refreshStatus = useCallback(async () => {
|
||||
setStatus(await api.getStatus());
|
||||
@@ -393,18 +438,23 @@ export default function App() {
|
||||
) : tab === "add" ? (
|
||||
<CardEntry />
|
||||
) : tab === "library" ? (
|
||||
<LibraryBrowser />
|
||||
<LibraryBrowser onOfferUndo={offerUndo} />
|
||||
) : (
|
||||
<DeckView />
|
||||
<DeckView refreshSignal={deckRefreshSignal} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{undo && snackbarVisible && (
|
||||
<UndoBar message={undo.message} onUndo={runUndo} onDismiss={dismissUndo} />
|
||||
)}
|
||||
<CommandPalette
|
||||
onNavigate={setTab}
|
||||
onUpdateCardData={updateCardData}
|
||||
onOpenLibrary={openLibrary}
|
||||
onNewLibrary={newLibrary}
|
||||
undo={undo}
|
||||
onRunUndo={runUndo}
|
||||
/>
|
||||
</HoverPreviewProvider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user