Deck analysis shows total cost; tidier analysis layout

A "cost" tile totals the USD price of every copy in the deck (commander
included, maybeboard not). The stat tiles now span the panel and size to
their values, with the color bars and mana curve sharing the row below,
and the left-hand panels no longer spread out to match the analysis
panel's height.

Closes #20
This commit is contained in:
2026-10-23 23:18:27 -05:00
parent b96931c7b8
commit 52873a2180
2 changed files with 46 additions and 11 deletions

View File

@@ -2620,7 +2620,11 @@ tbody tr:hover {
grid-template-areas:
"commander stats"
"meta stats"
"legality stats";
"legality stats"
". stats";
/* The last row takes whatever height the analysis panel adds, so the
left-hand panels stack together instead of spreading out to match. */
grid-template-rows: auto auto auto 1fr;
align-items: start;
}
}
@@ -2701,12 +2705,25 @@ tbody tr:hover {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 220px;
flex: 1 1 240px;
min-width: 0;
}
/* The analysis tiles span the panel above the bars and curve. They size to
their value (a "$1,234" cost is wider than a card count) and share out any
spare width, wrapping on narrow screens rather than squeezing a value past
its tile's edges. */
.deck-stats .stat-tiles-row {
flex-wrap: wrap;
}
.deck-stats .stat-tile {
flex: 1 0 auto;
padding: 8px 10px;
}
.deck-stats-curve {
flex: 1;
min-width: 220px;
max-width: 420px;
/* Shares the row with the color bars, or takes the full width once they
wrap onto separate lines. */
flex: 1 1 240px;
min-width: 0;
display: flex;
flex-direction: column;
}

View File

@@ -88,6 +88,9 @@ interface DeckStats {
uniqueCards: number;
landCount: number;
avgManaValue: number;
/** USD for every copy in the deck, commander included; cards without a
* known price count as $0. */
totalCost: number;
curve: {
label: string;
count: number;
@@ -175,11 +178,17 @@ function computeDeckStats(contents: DeckEntry[]): DeckStats {
}
}
// Unlike the rest, cost counts the commander: it's a card you have to buy.
const totalCost = contents
.filter((e) => !e.is_maybeboard)
.reduce((sum, e) => sum + e.price_usd * e.quantity, 0);
return {
totalCards,
uniqueCards: cards.length,
landCount,
avgManaValue,
totalCost,
curve,
colorCounts,
colorlessCount,
@@ -190,6 +199,14 @@ function computeDeckStats(contents: DeckEntry[]): DeckStats {
};
}
/** "$84.50", or whole dollars once it's big enough that cents are noise. */
function formatCost(usd: number): string {
return `$${usd.toLocaleString(undefined, {
minimumFractionDigits: usd >= 100 ? 0 : 2,
maximumFractionDigits: usd >= 100 ? 0 : 2,
})}`;
}
/** A prominent, art-forward display for a deck's commander (Commander/Brawl). */
function CommanderHero({
card,
@@ -1207,14 +1224,15 @@ export function DeckView({
<div className="deck-top-stats">
<div className="panel deck-stats">
<div className="deck-stats-head section-label">Deck analysis</div>
<div className="stat-tiles-row">
<StatTile value={deckStats.totalCards} label="cards" />
<StatTile value={deckStats.uniqueCards} label="unique" />
<StatTile value={deckStats.landCount} label="lands" />
<StatTile value={deckStats.avgManaValue.toFixed(2)} label="avg MV" />
<StatTile value={formatCost(deckStats.totalCost)} label="cost" />
</div>
<div className="deck-stats-body">
<div className="deck-stats-numbers">
<div className="stat-tiles-row">
<StatTile value={deckStats.totalCards} label="cards" />
<StatTile value={deckStats.uniqueCards} label="unique" />
<StatTile value={deckStats.landCount} label="lands" />
<StatTile value={deckStats.avgManaValue.toFixed(2)} label="avg MV" />
</div>
<div className="deck-stats-curve-label">Color identity</div>
<ColorDistributionBar
colorCounts={deckStats.colorCounts}