basin / commit
Move controls to a sidebar and refine the config UI
index.html | 101 +++++++++++++++++++++++++++---------------------- src/game.ts | 5 ++- src/main.ts | 9 +++-- src/render/renderer.ts | 7 +++- src/style.css | 68 +++++++++++++++++++++++++++++---- 5 files changed, 131 insertions(+), 59 deletions(-) diff --git a/index.html b/index.html index 99a30f5..63d3ba2 100644 --- a/index.html +++ b/index.html @@ -7,51 +7,62 @@ </head> <body> <div id="app"> - <header> - <img id="logo" class="logo" alt="basin logo" /> - <h1>basin</h1> - <button id="randomize">๐ฒ Random</button> - </header> - <section id="config" class="config"> - <label class="ctl"> - Size <span id="cfg-size-val" class="val"></span> - <input type="range" id="cfg-size" min="5" max="20" step="1" /> - </label> - <label class="ctl"> - Springs - <input type="number" id="cfg-springs" min="1" max="6" step="1" /> - </label> - <fieldset class="ctl edges"> - <legend>Walls</legend> - <label><input type="checkbox" id="cfg-wall-n" /> N</label> - <label><input type="checkbox" id="cfg-wall-e" /> E</label> - <label><input type="checkbox" id="cfg-wall-s" /> S</label> - <label><input type="checkbox" id="cfg-wall-w" /> W</label> - </fieldset> - <label class="ctl"> - Terrain - <span class="seedrow"> - <input type="number" id="cfg-seed" min="0" step="1" /> - <button id="cfg-seed-roll" title="Randomize terrain">๐ฒ</button> - </span> - </label> - <label class="ctl code"> - Code - <input type="text" id="cfg-code" spellcheck="false" autocomplete="off" /> - </label> - </section> - <canvas id="board"></canvas> - <p id="status" class="status"></p> - <p class="hint"> - Find the hidden springs that make water exit exactly at the gold edges. - Water leaves only at ocean (blue) sides โ mountain walls block it. Tap to - explore the terrain; land on a spring and it locks in. - </p> - <footer class="credit"> - Logo by - <a href="https://www.flaticon.com/authors/icongeek26" target="_blank" rel="noopener">icongeek26</a> - on Flaticon - </footer> + <aside class="panel"> + <header> + <img id="logo" class="logo" alt="basin logo" /> + <h1>basin</h1> + <button id="randomize">๐ฒ Random</button> + </header> + <section id="config" class="config"> + <label class="ctl"> + Size <span id="cfg-size-val" class="val"></span> + <input type="range" id="cfg-size" min="5" max="20" step="1" /> + </label> + <label class="ctl"> + Springs + <input type="number" id="cfg-springs" min="1" max="6" step="1" /> + </label> + <fieldset class="ctl edges"> + <legend>Walls</legend> + <label><input type="checkbox" id="cfg-wall-n" /> N</label> + <label><input type="checkbox" id="cfg-wall-e" /> E</label> + <label><input type="checkbox" id="cfg-wall-s" /> S</label> + <label><input type="checkbox" id="cfg-wall-w" /> W</label> + </fieldset> + <label class="ctl"> + Terrain + <span class="seedrow"> + <input + type="text" + id="cfg-seed" + inputmode="numeric" + spellcheck="false" + autocomplete="off" + /> + <button id="cfg-seed-roll" title="Randomize terrain">๐ฒ</button> + </span> + </label> + <label class="ctl code"> + Code + <input type="text" id="cfg-code" spellcheck="false" autocomplete="off" /> + </label> + </section> + <p id="status" class="status"></p> + <p class="hint"> + Find the hidden springs that make water exit exactly at the gold edges. + Water leaves only at ocean (blue) sides โ mountain walls block it. Tap to + explore the terrain; land on a spring and it locks in. + </p> + <footer class="credit"> + <a href="https://gitlab.com/PavelSafronov/basin" target="_blank" rel="noopener">Source on GitLab</a> + ยท Logo by + <a href="https://www.flaticon.com/authors/icongeek26" target="_blank" rel="noopener">icongeek26</a> + on Flaticon + </footer> + </aside> + <main class="board-area"> + <canvas id="board"></canvas> + </main> </div> <script type="module" src="/src/main.ts"></script> </body> diff --git a/src/game.ts b/src/game.ts index 492c15d..5e15310 100644 --- a/src/game.ts +++ b/src/game.ts @@ -62,9 +62,10 @@ export class Game { private updateStatus(): void { const n = this.puzzle.solution.length; + const springs = n === 1 ? 'spring' : 'springs'; this.statusEl.textContent = this.solved - ? `Solved! All ${n} springs found ๐` - : `${this.found.length} / ${n} springs found`; + ? `Solved! ${n === 1 ? 'The spring' : `All ${n} springs`} found ๐` + : `${this.found.length} / ${n} ${springs} found`; } private draw(): void { diff --git a/src/main.ts b/src/main.ts index 6af3ea9..5d8d330 100644 --- a/src/main.ts +++ b/src/main.ts @@ -76,9 +76,12 @@ for (const side of ['n', 'e', 's', 'w'] as const) { apply({ ...config, edges: { ...config.edges, [side]: edgeOf(wall[side].checked) } }), ); } -seedInput.addEventListener('change', () => - apply({ ...config, terrainSeed: Number(seedInput.value) >>> 0 }), -); +seedInput.addEventListener('change', () => { + // Free-text field: accept a non-negative integer, otherwise snap back. + const raw = seedInput.value.trim(); + if (/^\d+$/.test(raw)) apply({ ...config, terrainSeed: Number(raw) >>> 0 }); + else syncControls(); +}); $<HTMLButtonElement>('#cfg-seed-roll').addEventListener('click', () => apply({ ...config, terrainSeed: (Math.random() * 2 ** 32) >>> 0 }), ); diff --git a/src/render/renderer.ts b/src/render/renderer.ts index 10736ac..63271fd 100644 --- a/src/render/renderer.ts +++ b/src/render/renderer.ts @@ -38,7 +38,12 @@ export class Renderer { render(hm: Heightmap, flow: FlowResult | null, view?: PuzzleView | null): void { const dpr = window.devicePixelRatio || 1; - const maxPx = Math.min(window.innerWidth - 32, 560); + // Fit the board to its container (the board area beside the sidebar), not the + // whole window, so it never underlaps the controls. Fall back to the window + // width if the canvas isn't in the layout yet. + const parent = this.canvas.parentElement; + const avail = parent && parent.clientWidth > 0 ? parent.clientWidth : window.innerWidth - 32; + const maxPx = Math.min(Math.max(avail, 120), 640); this.cell = Math.max(1, Math.floor(maxPx / hm.width)); const w = this.cell * hm.width; const h = this.cell * hm.height; diff --git a/src/style.css b/src/style.css index 126914c..3735dd7 100644 --- a/src/style.css +++ b/src/style.css @@ -5,8 +5,9 @@ body { margin: 0; - display: grid; - place-items: start center; + display: flex; + justify-content: center; + align-items: flex-start; min-height: 100vh; background: #11151c; color: #e7ecf3; @@ -15,9 +16,32 @@ body { #app { padding: 16px; display: flex; + flex-direction: row; + align-items: flex-start; + gap: 24px; + width: 100%; + min-width: 0; + max-width: 1100px; + box-sizing: border-box; +} + +/* Controls live in a sidebar beside the board rather than stacked on top of it. */ +.panel { + flex: 0 0 300px; + width: 300px; + display: flex; flex-direction: column; - align-items: center; gap: 12px; + position: sticky; + top: 16px; +} + +.board-area { + flex: 1 1 auto; + min-width: 0; + display: flex; + justify-content: center; + align-items: flex-start; } header { @@ -59,8 +83,8 @@ header button { .config { display: flex; - flex-wrap: wrap; - align-items: flex-end; + flex-direction: column; + align-items: stretch; gap: 10px 16px; width: 100%; padding: 10px 12px; @@ -112,11 +136,20 @@ header button { gap: 4px; } +/* The seed is a free-text field (paste a number or reroll) rather than a spinner, + so let it grow to fit a full 32-bit value beside the reroll button. */ +.seedrow input { + flex: 1; + min-width: 0; +} + .seedrow button { padding: 4px 8px; } .edges { + /* Override the column layout `.ctl` applies, so the four sides sit on one row. */ + display: block; border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 6px; padding: 2px 8px 6px; @@ -132,7 +165,7 @@ header button { display: inline-flex; align-items: center; gap: 2px; - margin-right: 6px; + margin-right: 10px; } canvas { @@ -152,8 +185,7 @@ canvas { margin: 0; font-size: 0.85rem; opacity: 0.65; - max-width: 36ch; - text-align: center; + line-height: 1.4; } .credit { @@ -165,3 +197,23 @@ canvas { .credit a { color: inherit; } + +/* Narrow screens: fall back to the stacked layout, controls above the board. */ +@media (max-width: 720px) { + #app { + flex-direction: column; + align-items: stretch; + } + + .panel { + flex-basis: auto; + width: 100%; + position: static; + } + + .config { + flex-direction: row; + flex-wrap: wrap; + align-items: flex-end; + } +}