More random UI fixes.

This commit is contained in:
Greyson Parrelli
2026-07-24 11:23:38 -04:00
parent 6558806ba0
commit add2218978
6 changed files with 92 additions and 38 deletions
+9 -3
View File
@@ -537,13 +537,19 @@ export function BattlePhase({ view, send, step, setStep }: Props) {
? { left: peek.rect.left }
: { right: window.innerWidth - peek.rect.right }),
}
// Your deck sits on the left of the board and fights from its right
// edge; the rival's sits on the right and fights from its left edge.
// Order each peek to match, so "first" always points toward the clash.
const mine = peek.seat === youSeat
const firstSide = mine ? 'right' : 'left'
return createPortal(
<div className={`deck-peek deck-peek-${peek.dir}`} style={style}>
<div className="deck-peek-label">
{peek.seat === youSeat ? 'Your' : 'Opponents'} deck · {lineup.length} card
{lineup.length !== 1 ? 's' : ''} (first on the right)
{mine ? 'Your' : 'Opponents'} deck · {lineup.length} card
{lineup.length !== 1 ? 's' : ''} (first on the {firstSide})
</div>
<div className="deck-peek-cards">
<div className={`deck-peek-cards first-${firstSide}`}>
<div className="deck-peek-first"> first</div>
{lineup.map((c, i) => (
<CardView key={c.id || i} card={c} size="sm" />
))}
+1 -1
View File
@@ -62,7 +62,7 @@ const TRIGGER_GLOSS: Record<string, string> = {
Hurt: 'when it survives taking damage',
Sell: 'when you sell it in the shop',
Buy: 'when you buy it',
Triple: 'when traded in as one of three',
Triple: 'when tripled as one of three same-suit pets',
'After Attacking': 'right after it attacks in a clash',
'Battle Prep': 'as the battle is arranged',
'Shop start': 'when the shop opens each round',
+6 -6
View File
@@ -227,7 +227,7 @@ export function ShopPhase({ view, you, send }: Props) {
<div className="shop-status">
{pending && !myPending ? (
<span className="muted">
{opponent?.name ?? 'Opponent'} is trading up a tier
{opponent?.name ?? 'Opponent'} is tripling up a tier
</span>
) : you.ready ? (
<span className="muted">
@@ -239,7 +239,7 @@ export function ShopPhase({ view, you, send }: Props) {
Too many pets! Sell down to {view.maxPets} before you can pass 🍎
</span>
) : myTurn ? (
<span className="status-hot">Your turn buy, sell, trade, or pass</span>
<span className="shop-status-pill">Your turn buy, sell, triple, or pass</span>
) : (
<span className="muted">
{view.players[view.turn]?.name ?? 'Opponent'}s turn
@@ -305,7 +305,7 @@ export function ShopPhase({ view, you, send }: Props) {
? 'Tap a card to buy it for 1 🪙 — selling and trading are free'
: avocados > 0
? 'No coins — tap a card to buy it by discarding an Avocado 🥑'
: 'No coins left — you can still sell, trade, or pass'}
: 'No coins left — you can still sell, triple, or pass'}
</div>
)}
</section>
@@ -398,9 +398,9 @@ export function ShopPhase({ view, you, send }: Props) {
className="btn btn-secondary"
disabled={!myTurn || !sameSuit || view.round >= view.maxRounds}
onClick={() => act({ type: 'trade', cards: selected })}
title="Trade 3 same-suit pets for a pick from the next tier — free"
title="Triple 3 same-suit pets for a pick from the next tier — free"
>
Trade 3{' '}
Triple 3{' '}
{sameSuit && selectedCards[0].suit ? (
<span className={`suit-dot suit-${selectedCards[0].suit}`} />
) : (
@@ -455,7 +455,7 @@ export function ShopPhase({ view, you, send }: Props) {
</div>
)}
{/* Trade picker */}
{/* Triple picker */}
{myPending && pending && (
<div className="modal-backdrop">
<div className="modal">
+15 -15
View File
@@ -36,8 +36,8 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
})
// A shop-phase peek at an opponent's deck from the previous round's battle
// (its arranged lineup is already public). Anchored under the clicked button.
const [deckPeek, setDeckPeek] = useState<{ seat: number; rect: DOMRect } | null>(null)
// (its arranged lineup is already public). Shown as a centered modal.
const [deckPeek, setDeckPeek] = useState<{ seat: number } | null>(null)
// The battle outcome is known before the replay plays out, so we hold its
// "result" log entry back: it's dropped from the persistent list during the
@@ -158,10 +158,9 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
<button
className={`chip chip-btn ${deckPeek?.seat === p.seat ? 'is-active' : ''}`}
title="See their deck from last round's battle"
onClick={(e) => {
const rect = e.currentTarget.getBoundingClientRect()
setDeckPeek((cur) => (cur?.seat === p.seat ? null : { seat: p.seat, rect }))
}}
onClick={() =>
setDeckPeek((cur) => (cur?.seat === p.seat ? null : { seat: p.seat }))
}
>
👁 deck
</button>
@@ -214,25 +213,26 @@ export function Table({ session, onLeave }: { session: Session; onLeave: () => v
const lineup = view.battle?.lineups?.[deckPeek.seat] ?? []
if (!lineup.length) return null
const oppName = view.players.find((p) => p.seat === deckPeek.seat)?.name ?? 'Opponent'
const left = Math.max(8, Math.min(deckPeek.rect.left, window.innerWidth - 380))
return createPortal(
<>
<div className="deck-peek-backdrop" onClick={() => setDeckPeek(null)} />
<div className="modal-backdrop" onClick={() => setDeckPeek(null)}>
<div
className="deck-peek deck-peek-toolbar"
style={{ top: deckPeek.rect.bottom + 8, left }}
className="modal deck-peek-modal"
onClick={(e) => e.stopPropagation()}
>
<div className="deck-peek-label">
{oppName}s deck last round · {lineup.length} card
{lineup.length !== 1 ? 's' : ''} (first on the right)
{oppName}s deck from last rounds battle · {lineup.length} card
{lineup.length !== 1 ? 's' : ''} (first on the left)
</div>
<div className="deck-peek-cards">
{/* The rival battles on your right, so their first pet faces you
on the left — order the peek to match, first card on the left. */}
<div className="deck-peek-cards first-left">
<div className="deck-peek-first"> first</div>
{lineup.map((c, i) => (
<CardView key={c.id || i} card={c} size="sm" />
))}
</div>
</div>
</>,
</div>,
document.body,
)
})()}
+58 -10
View File
@@ -1246,6 +1246,24 @@ h3 {
animation: pulse 1.6s ease-in-out infinite;
}
/* The "it's your turn" prompt as a solid golden badge with light text, so it
reads as a call to action rather than bare pulsing text. */
.shop-status-pill {
display: inline-block;
font-family: var(--font-display);
font-size: 0.92em;
color: #fff8e6;
background: linear-gradient(180deg, var(--gold), var(--gold-deep));
padding: 0.22em 0.85em;
border-radius: 999px;
border: 2px solid rgba(255, 255, 255, 0.5);
text-shadow: 0 1px 3px rgba(120, 78, 8, 0.5);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.5),
inset 0 -4px 10px rgba(150, 100, 20, 0.3),
0 4px 14px rgba(0, 0, 0, 0.35);
}
@keyframes pulse {
0%,
100% {
@@ -2601,22 +2619,52 @@ h3 {
.deck-peek-cards {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
max-width: min(60vw, 560px);
/* Read right-to-left like the arrangement screen: the first (top) card sits
on the right and later pets flow left. */
direction: rtl;
}
/* Keep each card's own contents left-to-right despite the rtl row. */
/* first-right: the first pet sits on the right (matches your own arrangement).
first-left: the first pet sits on the left (a rival, who faces you from the
right of the board). The "⚔️ first" marker is the first DOM child, so it
lands next to the first card at whichever end. */
.deck-peek-cards.first-right {
direction: rtl;
}
.deck-peek-cards.first-left {
direction: ltr;
}
/* Keep each card's own contents left-to-right regardless of row direction. */
.deck-peek-cards > * {
direction: ltr;
}
/* Backdrop behind the toolbar deck-peek: any click outside dismisses it. Sits
below the popover (z 60) but above the board. */
.deck-peek-backdrop {
position: fixed;
inset: 0;
z-index: 55;
/* Compact echo of the arrangement phase's "⚔️ first" indicator. */
.deck-peek-first {
display: flex;
align-items: center;
align-self: stretch;
font-family: var(--font-display);
font-size: 0.68rem;
color: var(--gold);
white-space: nowrap;
padding: 0 2px;
}
/* Deck peek shown as a centered modal (shop-phase rival deck): cap the width so
a wide lineup wraps instead of running off-screen. */
.deck-peek-modal {
padding: 20px;
gap: 12px;
max-width: min(92vw, 720px);
}
.deck-peek-modal .deck-peek-cards {
max-width: 100%;
justify-content: flex-start;
}
.deck-peek-modal .deck-peek-label {
font-size: 0.85rem;
}