diff --git a/css/style.css b/css/style.css index 14e0f10..807f114 100644 --- a/css/style.css +++ b/css/style.css @@ -10,6 +10,9 @@ --bg-elevated: #21262d; --bg-border: #30363d; + /* Board size: at least 50 % of viewport width, capped at 760 px */ + --board-sz: clamp(320px, 50vw, 760px); + --text-primary: #e6edf3; --text-secondary: #8b949e; --text-muted: #484f58; @@ -169,7 +172,7 @@ a { color: var(--accent-blue); } align-items: center; gap: .6rem; width: 100%; - max-width: 480px; + max-width: var(--board-sz); padding: .4rem .6rem; background: var(--bg-surface); border: 1px solid var(--bg-border); @@ -261,9 +264,8 @@ a { color: var(--accent-blue); } border-radius: 3px; overflow: hidden; box-shadow: var(--shadow-lg); - /* Size is set dynamically in JS but default here */ - width: min(57vw, 460px); - height: min(57vw, 460px); + width: var(--board-sz); + height: var(--board-sz); } /* Individual squares */ @@ -308,7 +310,7 @@ a { color: var(--accent-blue); } /* Pieces */ .piece { - font-size: calc(min(57vw, 460px) / 10); + font-size: calc(var(--board-sz) / 10); line-height: 1; pointer-events: none; display: flex; @@ -327,7 +329,7 @@ a { color: var(--accent-blue); } align-items: center; gap: .5rem; width: 100%; - max-width: 480px; + max-width: var(--board-sz); } .game-status-text { @@ -340,7 +342,7 @@ a { color: var(--accent-blue); } /* ── Move History ───────────────────────────────────────────── */ .move-history-box { width: 100%; - max-width: 480px; + max-width: var(--board-sz); background: var(--bg-surface); border: 1px solid var(--bg-border); border-radius: var(--radius); @@ -472,7 +474,7 @@ a { color: var(--accent-blue); } animation: fadeSlideIn .2s ease; } @keyframes fadeSlideIn { - from { opacity: 0; transform: translateY(6px); } + from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } } @@ -837,14 +839,7 @@ input:checked + .slider::before { transform: translateX(18px); } border-top: none; } - .board-grid { - width: min(90vw, 420px); - height: min(90vw, 420px); - } - - .piece { - font-size: calc(min(90vw, 420px) / 10); - } + :root { --board-sz: min(90vw, 420px); } } @media (max-width: 480px) { @@ -853,11 +848,7 @@ input:checked + .slider::before { transform: translateX(18px); } .chess-panel { padding: .5rem; } .header-controls { gap: .3rem; } - .board-grid { - width: min(92vw, 360px); - height: min(92vw, 360px); - } - .piece { font-size: calc(min(92vw, 360px) / 10); } + :root { --board-sz: min(92vw, 360px); } #btnToggleSQL #sqlToggleLabel { display: none; } } diff --git a/index.html b/index.html index 2f32340..9e44779 100644 --- a/index.html +++ b/index.html @@ -112,6 +112,7 @@

placeholder="UPDATE chess_piece SET position = 'e4' WHERE position = 'e2'; -- or shorthand: e2 e4">
+
diff --git a/js/app.js b/js/app.js index 53b4efb..4ede065 100644 --- a/js/app.js +++ b/js/app.js @@ -16,6 +16,9 @@ 'use strict'; /* ─── Configuration ──────────────────────────────────────────── */ +const SAMPLE_SQL_QUERY = + `-- Sample: move the e-pawn two squares forward\nUPDATE chess_piece\nSET position = 'e4'\nWHERE position = 'e2';`; + const PIECE_UNICODE = { wK: '♔', wQ: '♕', wR: '♖', wB: '♗', wN: '♘', wP: '♙', bK: '♚', bQ: '♛', bR: '♜', bB: '♝', bN: '♞', bP: '♟', @@ -68,6 +71,53 @@ function showToast(msg, duration = 2500) { el._timer = setTimeout(() => el.classList.add('hidden'), duration); } +/* ─── Audio ───────────────────────────────────────────────────── */ +let _audioCtx = null; + +function getAudioCtx() { + if (!_audioCtx) { + _audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } + if (_audioCtx.state === 'suspended') { _audioCtx.resume(); } + return _audioCtx; +} + +function playSound(type) { + try { + const ctx = getAudioCtx(); + const o = ctx.createOscillator(); + const g = ctx.createGain(); + o.connect(g); + g.connect(ctx.destination); + if (type === 'capture') { + o.type = 'sawtooth'; + o.frequency.setValueAtTime(520, ctx.currentTime); + o.frequency.exponentialRampToValueAtTime(180, ctx.currentTime + 0.18); + g.gain.setValueAtTime(0.35, ctx.currentTime); + g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.18); + o.start(ctx.currentTime); + o.stop(ctx.currentTime + 0.18); + } else if (type === 'check') { + o.type = 'square'; + o.frequency.setValueAtTime(880, ctx.currentTime); + o.frequency.setValueAtTime(660, ctx.currentTime + 0.08); + g.gain.setValueAtTime(0.2, ctx.currentTime); + g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.25); + o.start(ctx.currentTime); + o.stop(ctx.currentTime + 0.25); + } else { + // normal move + o.type = 'sine'; + o.frequency.setValueAtTime(900, ctx.currentTime); + o.frequency.exponentialRampToValueAtTime(650, ctx.currentTime + 0.09); + g.gain.setValueAtTime(0.22, ctx.currentTime); + g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.1); + o.start(ctx.currentTime); + o.stop(ctx.currentTime + 0.1); + } + } catch (e) { /* ignore — AudioContext may be unavailable */ } +} + /* ─── Board Rendering ─────────────────────────────────────────── */ function buildBoard() { const board = document.getElementById('board'); @@ -420,6 +470,13 @@ function executeMove(from, to, promotion) { state.validMoves = []; state.moveCount++; + // Play move sound + if (moveResult.captured) { + playSound('capture'); + } else { + playSound('move'); + } + // Clear SQL input template after a successful board-click move const sqlMoveInput = document.getElementById('sqlMoveInput'); if (sqlMoveInput && state.sqlInputHasTemplate) { @@ -438,6 +495,7 @@ function executeMove(from, to, promotion) { const endSQL = SQLGen.gameEnd(state.chess, state.gameId); appendSQL(endSQL, 'Game Over', null); } else if (state.chess.in_check()) { + playSound('check'); const checkSQL = SQLGen.check(state.chess.turn(), state.gameId); appendSQL(checkSQL, '⚠ Check', null); } @@ -732,7 +790,7 @@ function highlightSQL(code) { .replace(kwRegex, '$1'); } -function appendSQL(code, label, moveNum) { +function appendSQL(code, label, moveNum, atEnd) { const placeholder = document.getElementById('sqlPlaceholder'); if (placeholder) placeholder.remove(); @@ -766,12 +824,25 @@ function appendSQL(code, label, moveNum) { block.appendChild(labelEl); block.appendChild(codeEl); - content.appendChild(block); + + // New blocks slide in at the TOP so each submitted query moves content DOWN; + // only the initial game-setup block is appended at the end. + if (atEnd) { + content.appendChild(block); + } else { + content.prepend(block); + } state.sqlBlocks.push({ code, label, moveNum }); if (document.getElementById('chkAutoScroll').checked) { - content.scrollTop = content.scrollHeight; + if (atEnd) { + // Game-init block appended at bottom — scroll down to show it + content.scrollTop = content.scrollHeight; + } else { + // Move blocks prepended at top — scroll up to reveal the new block + content.scrollTop = 0; + } } } @@ -796,9 +867,12 @@ function startGame(whiteName, blackName, showSQL, existingPGN) { document.getElementById('whitePlayerName').textContent = state.whitePlayer; document.getElementById('blackPlayerName').textContent = state.blackPlayer; - // Clear SQL input + // Clear SQL input and pre-fill with sample query const sqlMoveInput = document.getElementById('sqlMoveInput'); - if (sqlMoveInput) sqlMoveInput.value = ''; + if (sqlMoveInput) { + sqlMoveInput.value = SAMPLE_SQL_QUERY; + } + state.sqlInputHasTemplate = true; clearSQLRunError(); // SQL panel visibility @@ -828,9 +902,9 @@ function startGame(whiteName, blackName, showSQL, existingPGN) { `

Each chess move is translated into
real SQL statements in real time.

`; sqlContent.appendChild(placeholder); - // Emit game-start SQL + // Emit game-start SQL (placed at the end so move SQL slides in above it) const initSQL = SQLGen.gameStart(state.gameId, state.whitePlayer, state.blackPlayer); - appendSQL(initSQL, 'Game Initialized', null); + appendSQL(initSQL, 'Game Initialized', null, true); } // Load from PGN if provided (invite link) @@ -955,10 +1029,10 @@ function init() { if (undone.color === 'w') state.capturedByWhite.pop(); else state.capturedByBlack.pop(); } - // Remove last SQL block from UI + // Remove most-recent SQL block from UI (prepended = first child, no id) const content = document.getElementById('sqlContent'); - if (content.lastChild && !content.lastChild.id) { - content.removeChild(content.lastChild); + if (content.firstChild && !content.firstChild.id) { + content.removeChild(content.firstChild); state.sqlBlocks.pop(); } state.selectedSquare = null; @@ -1021,6 +1095,14 @@ function init() { // SQL Move Input document.getElementById('btnRunSQL').addEventListener('click', runSQLMove); + document.getElementById('btnSampleSQL').addEventListener('click', () => { + const input = document.getElementById('sqlMoveInput'); + if (input) { + input.value = SAMPLE_SQL_QUERY; + } + state.sqlInputHasTemplate = true; + clearSQLRunError(); + }); document.getElementById('btnClearInput').addEventListener('click', () => { const input = document.getElementById('sqlMoveInput'); if (input) input.value = '';