fix(web): apply Terminal palette and UX fixes to game page
Build and Deploy / build-and-push (push) Successful in 1m19s
Build and Deploy / build-and-push (push) Successful in 1m19s
Aligns /play with the landing page and app color scheme — same bg, panel, accent, and felt tokens from ui_theme.rs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,21 +1,28 @@
|
|||||||
/* Solitaire Quest — interactive game page.
|
/* Solitaire Quest — interactive game page.
|
||||||
Palette and card styles mirror replay.css; adds drag, selection,
|
Palette mirrors the Bevy app's Terminal (base16-eighties) design system.
|
||||||
HUD, and win-overlay layers. */
|
Card faces/backs are PNG images served from /assets/cards/. */
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "FiraMono";
|
||||||
|
src: url("/assets/fonts/main.ttf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg: #0f0a1f;
|
--bg: #151515;
|
||||||
--felt: #0f4c30;
|
--felt: #0f5232;
|
||||||
--panel: #1a0f2e;
|
--panel: #202020;
|
||||||
--panel-hi: #2d1b69;
|
--panel-hi: #2a2a2a;
|
||||||
--text: #f5f0ff;
|
--text: #d0d0d0;
|
||||||
--text-muted: #b5a8d5;
|
--text-muted: #a0a0a0;
|
||||||
--accent: #ffd23f;
|
--accent: #a54242;
|
||||||
--red: #cc3344;
|
--accent-hi: #c25e5e;
|
||||||
--black: #1a0f2e;
|
--red: #fb9fb1;
|
||||||
--card-bg: #ffffff;
|
--black: #151515;
|
||||||
--card-border: #ccc;
|
--drop-success: #acc267;
|
||||||
|
--card-bg: #f8f5f0;
|
||||||
|
--card-border: #c8b8a0;
|
||||||
--card-w: 80px;
|
--card-w: 80px;
|
||||||
--card-h: 112px;
|
--card-h: 120px;
|
||||||
--gap: 12px;
|
--gap: 12px;
|
||||||
--fan: 28px;
|
--fan: 28px;
|
||||||
}
|
}
|
||||||
@@ -23,7 +30,7 @@
|
|||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
font-family: "FiraMono", "Fira Mono", monospace;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
@@ -66,7 +73,7 @@ button {
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
transition: background 120ms;
|
transition: background 120ms;
|
||||||
}
|
}
|
||||||
button:hover { background: var(--accent); color: var(--black); }
|
button:hover { background: var(--accent); color: var(--text); }
|
||||||
button:disabled { opacity: 0.4; cursor: default; }
|
button:disabled { opacity: 0.4; cursor: default; }
|
||||||
|
|
||||||
.toggle-label {
|
.toggle-label {
|
||||||
@@ -84,25 +91,32 @@ button:disabled { opacity: 0.4; cursor: default; }
|
|||||||
main {
|
main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
overflow: hidden;
|
||||||
padding: 20px;
|
|
||||||
overflow-x: auto;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Full-bleed felt surface — flex:1 reliably fills main's remaining height. */
|
||||||
#board {
|
#board {
|
||||||
position: relative;
|
flex: 1;
|
||||||
background: var(--felt);
|
background: var(--felt);
|
||||||
border-radius: 12px;
|
display: flex;
|
||||||
padding: 20px;
|
align-items: center;
|
||||||
/* 7 columns wide */
|
justify-content: center;
|
||||||
width: calc(7 * var(--card-w) + 6 * var(--gap) + 40px);
|
overflow: hidden;
|
||||||
/* top row + generous fan budget for a 13-card column */
|
|
||||||
height: calc(var(--card-h) + 28px + var(--card-h) + 12 * var(--fan) + 40px);
|
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Natural-size coordinate space for cards and slots.
|
||||||
|
The CSS transform scale is applied here, not on #board. */
|
||||||
|
#card-area {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: calc(7 * var(--card-w) + 6 * var(--gap) + 40px);
|
||||||
|
height: calc(var(--card-h) + 28px + var(--card-h) + 12 * var(--fan) + 40px);
|
||||||
|
touch-action: none; /* prevent browser scroll/pan from stealing pointer events */
|
||||||
|
}
|
||||||
|
|
||||||
/* Empty-pile slot markers */
|
/* Empty-pile slot markers */
|
||||||
.slot {
|
.slot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -114,8 +128,8 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.slot.drop-active {
|
.slot.drop-active {
|
||||||
border-color: var(--accent);
|
border-color: var(--drop-success);
|
||||||
background: rgba(255, 210, 63, 0.08);
|
background: rgba(172, 194, 103, 0.10);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Cards ───────────────────────────────────────────────────────────── */
|
/* ── Cards ───────────────────────────────────────────────────────────── */
|
||||||
@@ -125,13 +139,15 @@ main {
|
|||||||
top: 0; left: 0;
|
top: 0; left: 0;
|
||||||
width: var(--card-w);
|
width: var(--card-w);
|
||||||
height: var(--card-h);
|
height: var(--card-h);
|
||||||
background: var(--card-bg);
|
background-color: var(--card-bg);
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
border: 1px solid var(--card-border);
|
border: 1px solid var(--card-border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.35);
|
box-shadow: 0 2px 5px rgba(0,0,0,0.35);
|
||||||
padding: 4px 6px;
|
padding: 0;
|
||||||
font-weight: 600;
|
overflow: hidden;
|
||||||
line-height: 1;
|
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1),
|
transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1),
|
||||||
opacity 200ms ease,
|
opacity 200ms ease,
|
||||||
@@ -142,40 +158,12 @@ main {
|
|||||||
.card:active { cursor: grabbing; }
|
.card:active { cursor: grabbing; }
|
||||||
|
|
||||||
.card.face-down {
|
.card.face-down {
|
||||||
background:
|
background-color: #2d1b69;
|
||||||
repeating-linear-gradient(
|
|
||||||
45deg,
|
|
||||||
#482f97 0, #482f97 6px,
|
|
||||||
#2d1b69 6px, #2d1b69 12px
|
|
||||||
);
|
|
||||||
color: transparent;
|
color: transparent;
|
||||||
border-color: #4a3a8a;
|
border-color: #4a3a8a;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card .corner {
|
|
||||||
position: absolute;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.1;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.card .corner.top { top: 4px; left: 6px; }
|
|
||||||
/* No rotation — ♠ rotated 180° is indistinguishable from ♥ */
|
|
||||||
.card .corner.bottom { bottom: 4px; right: 6px; text-align: right; }
|
|
||||||
|
|
||||||
.card.red { color: var(--red); }
|
|
||||||
.card.black { color: var(--black); }
|
|
||||||
|
|
||||||
.card .center {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%; left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
font-size: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stock pile: pointer cursor since it's a click target, not draggable */
|
|
||||||
.card.stock-card { cursor: pointer; }
|
|
||||||
|
|
||||||
/* Selected / being-dragged state */
|
/* Selected / being-dragged state */
|
||||||
.card.selected {
|
.card.selected {
|
||||||
box-shadow: 0 0 0 2px var(--accent), 0 4px 12px rgba(0,0,0,0.5);
|
box-shadow: 0 0 0 2px var(--accent), 0 4px 12px rgba(0,0,0,0.5);
|
||||||
@@ -185,14 +173,19 @@ main {
|
|||||||
|
|
||||||
/* Drop-target highlight on cards (top of a tableau column) */
|
/* Drop-target highlight on cards (top of a tableau column) */
|
||||||
.card.drop-target {
|
.card.drop-target {
|
||||||
box-shadow: 0 0 0 2px var(--accent), 0 4px 12px rgba(0,0,0,0.5);
|
box-shadow: 0 0 0 2px var(--drop-success), 0 4px 12px rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Recycle indicator on empty stock — JS sets transform to position it */
|
/* Recycle indicator on empty stock — sized to match the slot, symbol centred */
|
||||||
.recycle-label {
|
.recycle-label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0; left: 0;
|
top: 0; left: 0;
|
||||||
font-size: 26px;
|
width: var(--card-w);
|
||||||
|
height: var(--card-h);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 32px;
|
||||||
color: rgba(255,255,255,0.3);
|
color: rgba(255,255,255,0.3);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -202,7 +195,7 @@ main {
|
|||||||
#win-overlay {
|
#win-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(10, 5, 20, 0.75);
|
background: rgba(21, 21, 21, 0.92);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -28,7 +28,9 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<section id="board"></section>
|
<section id="board">
|
||||||
|
<div id="card-area"></div>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div id="win-overlay" class="hidden">
|
<div id="win-overlay" class="hidden">
|
||||||
|
|||||||
@@ -14,12 +14,16 @@ import init, { SolitaireGame } from "/web/pkg/solitaire_wasm.js";
|
|||||||
|
|
||||||
// ── Layout constants (must match game.css --card-w / --card-h / --gap / --pad)
|
// ── Layout constants (must match game.css --card-w / --card-h / --gap / --pad)
|
||||||
const CARD_W = 80;
|
const CARD_W = 80;
|
||||||
const CARD_H = 112;
|
const CARD_H = 120; // 2:3 ratio matching 256×384 card PNGs
|
||||||
const GAP = 12;
|
const GAP = 12;
|
||||||
const PAD = 20; // board inner padding — cards start at (PAD, PAD)
|
const PAD = 20; // board inner padding — cards start at (PAD, PAD)
|
||||||
const FAN = 28; // vertical offset per fanned tableau card
|
const FAN = 28; // vertical offset per fanned tableau card
|
||||||
const WASTE_FAN = 18; // horizontal offset for draw-3 waste fan
|
const WASTE_FAN = 18; // horizontal offset for draw-3 waste fan
|
||||||
|
|
||||||
|
// Natural board dimensions — used for scale-to-fit calculation.
|
||||||
|
const BOARD_W = PAD * 2 + 7 * CARD_W + 6 * GAP; // 672
|
||||||
|
const BOARD_H = PAD * 2 + CARD_H + 28 + CARD_H + 12 * FAN; // 644
|
||||||
|
|
||||||
// Pile origins in board-element coordinates (include PAD so (0,0) = board edge).
|
// Pile origins in board-element coordinates (include PAD so (0,0) = board edge).
|
||||||
const TOP_Y = PAD;
|
const TOP_Y = PAD;
|
||||||
const BOTTOM_Y = PAD + CARD_H + 28;
|
const BOTTOM_Y = PAD + CARD_H + 28;
|
||||||
@@ -45,7 +49,7 @@ const PILE_ORIGIN = {
|
|||||||
// Foundation suit hints shown when the slot is empty.
|
// Foundation suit hints shown when the slot is empty.
|
||||||
const FOUND_SUIT_HINT = ["♠", "♥", "♦", "♣"];
|
const FOUND_SUIT_HINT = ["♠", "♥", "♦", "♣"];
|
||||||
|
|
||||||
const SUIT_GLYPH = { clubs: "♣", diamonds: "♦", hearts: "♥", spades: "♠" };
|
const SUIT_CODE = { clubs: "C", diamonds: "D", hearts: "H", spades: "S" };
|
||||||
const RANK_LABELS = ["","A","2","3","4","5","6","7","8","9","10","J","Q","K"];
|
const RANK_LABELS = ["","A","2","3","4","5","6","7","8","9","10","J","Q","K"];
|
||||||
const RED_SUITS = new Set(["diamonds", "hearts"]);
|
const RED_SUITS = new Set(["diamonds", "hearts"]);
|
||||||
|
|
||||||
@@ -73,8 +77,11 @@ let elapsedSecs = 0;
|
|||||||
// Auto-complete
|
// Auto-complete
|
||||||
let acTimer = null;
|
let acTimer = null;
|
||||||
|
|
||||||
|
// Current scale factor applied to #board.
|
||||||
|
let boardScale = 1.0;
|
||||||
|
|
||||||
// ── DOM refs ─────────────────────────────────────────────────────────────────
|
// ── DOM refs ─────────────────────────────────────────────────────────────────
|
||||||
const board = document.getElementById("board");
|
const board = document.getElementById("card-area");
|
||||||
const hudScore = document.getElementById("hud-score");
|
const hudScore = document.getElementById("hud-score");
|
||||||
const hudMoves = document.getElementById("hud-moves");
|
const hudMoves = document.getElementById("hud-moves");
|
||||||
const hudTimer = document.getElementById("hud-timer");
|
const hudTimer = document.getElementById("hud-timer");
|
||||||
@@ -89,6 +96,21 @@ const winMoves = document.getElementById("win-moves");
|
|||||||
const winTime = document.getElementById("win-time");
|
const winTime = document.getElementById("win-time");
|
||||||
const btnWinNew = document.getElementById("btn-win-new");
|
const btnWinNew = document.getElementById("btn-win-new");
|
||||||
|
|
||||||
|
// ── Scale to fit ─────────────────────────────────────────────────────────────
|
||||||
|
// Scales #card-area to fill #board without overflowing either dimension.
|
||||||
|
// boardRelative() divides by boardScale to keep hit-testing correct.
|
||||||
|
function scaleBoard() {
|
||||||
|
// Measure the actual rendered #board element — more reliable than
|
||||||
|
// computing window.innerHeight minus estimated header height, which
|
||||||
|
// breaks under different browser chrome / OS scaling factors.
|
||||||
|
const outerBoard = document.getElementById("board");
|
||||||
|
const bw = outerBoard.clientWidth;
|
||||||
|
const bh = outerBoard.clientHeight;
|
||||||
|
boardScale = Math.min(bw / BOARD_W, bh / BOARD_H, 2.0);
|
||||||
|
board.style.transform = `scale(${boardScale})`;
|
||||||
|
board.style.transformOrigin = "center center";
|
||||||
|
}
|
||||||
|
|
||||||
// ── Bootstrap ────────────────────────────────────────────────────────────────
|
// ── Bootstrap ────────────────────────────────────────────────────────────────
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
await init();
|
await init();
|
||||||
@@ -99,6 +121,8 @@ async function bootstrap() {
|
|||||||
chkDraw3.checked = drawThree;
|
chkDraw3.checked = drawThree;
|
||||||
|
|
||||||
buildSlots();
|
buildSlots();
|
||||||
|
scaleBoard();
|
||||||
|
window.addEventListener("resize", scaleBoard);
|
||||||
startGame(urlSeed);
|
startGame(urlSeed);
|
||||||
attachHandlers();
|
attachHandlers();
|
||||||
}
|
}
|
||||||
@@ -242,7 +266,7 @@ function render(s) {
|
|||||||
board.appendChild(recycleEl);
|
board.appendChild(recycleEl);
|
||||||
}
|
}
|
||||||
const o = PILE_ORIGIN.stock;
|
const o = PILE_ORIGIN.stock;
|
||||||
recycleEl.style.transform = `translate(${o.x + CARD_W / 2}px, ${o.y + CARD_H / 2}px)`;
|
recycleEl.style.transform = `translate(${o.x}px, ${o.y}px)`;
|
||||||
} else if (recycleEl) {
|
} else if (recycleEl) {
|
||||||
recycleEl.remove();
|
recycleEl.remove();
|
||||||
}
|
}
|
||||||
@@ -268,15 +292,13 @@ function updateCardEl(el, card, pileName, idx, total) {
|
|||||||
|
|
||||||
if (!card.face_up) {
|
if (!card.face_up) {
|
||||||
el.className = "card face-down";
|
el.className = "card face-down";
|
||||||
el.innerHTML = "";
|
el.style.backgroundImage = "url('/assets/cards/backs/back_0.png')";
|
||||||
|
el.innerHTML = "";
|
||||||
} else {
|
} else {
|
||||||
const isRed = RED_SUITS.has(card.suit);
|
const isRed = RED_SUITS.has(card.suit);
|
||||||
el.className = `card ${isRed ? "red" : "black"}`;
|
el.className = `card ${isRed ? "red" : "black"}`;
|
||||||
const r = RANK_LABELS[card.rank];
|
el.style.backgroundImage = `url('/assets/cards/faces/${RANK_LABELS[card.rank]}${SUIT_CODE[card.suit]}.png')`;
|
||||||
const s = SUIT_GLYPH[card.suit];
|
el.innerHTML = "";
|
||||||
el.innerHTML = `<div class="corner top">${r}<br>${s}</div>
|
|
||||||
<div class="center">${s}</div>
|
|
||||||
<div class="corner bottom">${r}<br>${s}</div>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,9 +367,14 @@ function attachHandlers() {
|
|||||||
// ── Coordinate helpers ────────────────────────────────────────────────────────
|
// ── Coordinate helpers ────────────────────────────────────────────────────────
|
||||||
// Returns cursor position in board-element coordinates
|
// Returns cursor position in board-element coordinates
|
||||||
// (0,0 = board element top-left corner, which is the padding edge).
|
// (0,0 = board element top-left corner, which is the padding edge).
|
||||||
|
// Divides by boardScale because getBoundingClientRect() returns the SCALED
|
||||||
|
// visual rect; we need coordinates in the natural (pre-scale) system.
|
||||||
function boardRelative(clientX, clientY) {
|
function boardRelative(clientX, clientY) {
|
||||||
const rect = board.getBoundingClientRect();
|
const rect = board.getBoundingClientRect();
|
||||||
return { x: clientX - rect.left, y: clientY - rect.top };
|
return {
|
||||||
|
x: (clientX - rect.left) / boardScale,
|
||||||
|
y: (clientY - rect.top) / boardScale,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function hitTestCard(bx, by) {
|
function hitTestCard(bx, by) {
|
||||||
@@ -487,6 +514,8 @@ function onPointerUp(e) {
|
|||||||
const targetPile = findDropTarget(bx, by);
|
const targetPile = findDropTarget(bx, by);
|
||||||
|
|
||||||
let moved = false;
|
let moved = false;
|
||||||
|
let illegalAttempt = false;
|
||||||
|
|
||||||
if (targetPile && targetPile !== drag.fromPile) {
|
if (targetPile && targetPile !== drag.fromPile) {
|
||||||
const r = game.move_cards(drag.fromPile, targetPile, drag.cardIds.length);
|
const r = game.move_cards(drag.fromPile, targetPile, drag.cardIds.length);
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
@@ -494,13 +523,14 @@ function onPointerUp(e) {
|
|||||||
drag.cardIds.forEach(id => cardEls.get(id)?.classList.remove("selected"));
|
drag.cardIds.forEach(id => cardEls.get(id)?.classList.remove("selected"));
|
||||||
render(r.snapshot);
|
render(r.snapshot);
|
||||||
} else {
|
} else {
|
||||||
flashIllegal(drag.cardIds);
|
illegalAttempt = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!moved) {
|
if (!moved) {
|
||||||
drag.cardIds.forEach(id => cardEls.get(id)?.classList.remove("selected"));
|
drag.cardIds.forEach(id => cardEls.get(id)?.classList.remove("selected"));
|
||||||
render(snap); // snap cards back to their pre-drag positions
|
render(snap); // snap cards back first — then animate so shake plays on settled positions
|
||||||
|
if (illegalAttempt) flashIllegal(drag.cardIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
drag = null;
|
drag = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user