The table on a phone: the masthead keeps the title, who is playing and the room code, and folds the rest behind a Table button; the piece panel opens as a summary and unfolds the lore on request; the told game brings the move's board into view and keeps Back and Next at the foot of the screen with the move's name; a game against a drake can be started over at any time and played again when it ends
The masthead on a 390-pixel screen was 300 pixels tall, with seven buttons before the status line; it is 132 now. The piece panel was 30 percent of the screen with the lore scrolling inside; the summary is a third less, and the lore is a tap away. The told game's words were sticky on a phone as on a wide screen, and pinned themselves over the boards; they scroll with the page now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CCLa2PaEeCj8njQZSWGqHx
This commit is contained in:
1 parent
7dd113b1ff
commit
36508f29ca
4 files changed
+152
-6
No files matched your search
@@ -12,7 +12,8 @@
|
||||
legalMovesFrom, movesFrom, squareName, sideOf, kindOf, kingSquare, inCheck, levelOf, frozen, NAMES, SIDE_NAMES,
|
||||
type Move, type Side
|
||||
} from '$lib/game';
|
||||
import type { Room } from '$lib/net/room.svelte';
|
||||
import { Room } from '$lib/net/room.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { prefs, reducedMotion } from '$lib/prefs.svelte';
|
||||
|
||||
let { room }: { room: Room } = $props();
|
||||
@@ -199,6 +200,23 @@
|
||||
return g.over.winner === room.me ? 'You win.' : `${g.players[g.over.winner].name} wins.`;
|
||||
});
|
||||
|
||||
/** A game against a drake can be started over at any time: a fresh table, the same side and setup. */
|
||||
const drake = $derived(!room.spectating && room.view.seats.some((s) => s.bot && !s.standIn));
|
||||
let starting = $state(false);
|
||||
let fault = $state('');
|
||||
async function again() {
|
||||
starting = true;
|
||||
fault = '';
|
||||
try {
|
||||
const next = await Room.createWithBot(room.nameOf(room.me!), room.view.options);
|
||||
await goto(`/join/${next.roomId}`);
|
||||
} catch (e) {
|
||||
fault = e instanceof Error ? e.message : 'A new table could not be opened.';
|
||||
} finally {
|
||||
starting = false;
|
||||
}
|
||||
}
|
||||
|
||||
const pieceCount = (side: Side) => g.cells.filter((p) => p !== '' && sideOf(p) === side).length;
|
||||
</script>
|
||||
|
||||
@@ -227,7 +245,8 @@
|
||||
{/if}
|
||||
{#if room.error}<p class="warning">{room.error}</p>{/if}
|
||||
|
||||
<p class="muted small glass-way"><button type="button" class="link plain" disabled={fetching} aria-busy={fetching} onclick={() => seeGlass(!onGlass)}>{fetching ? 'Fetching the glass board…' : onGlass ? 'Back to the flat boards' : 'See the glass board'}</button></p>
|
||||
<p class="muted small glass-way"><button type="button" class="link plain" disabled={fetching} aria-busy={fetching} onclick={() => seeGlass(!onGlass)}>{fetching ? 'Fetching the glass board…' : onGlass ? 'Back to the flat boards' : 'See the glass board'}</button>{#if drake && !g.over} · <button type="button" class="link plain" disabled={starting} onclick={again}>Start over against a fresh drake</button>{/if}</p>
|
||||
{#if fault}<p class="warning">{fault}</p>{/if}
|
||||
{#if onGlass && Glass}
|
||||
<Glass {g} {mySide} {selected} {targets} {reach} {shownSquares} {shownCaptured} {checkSquare} {calm} {tap} />
|
||||
{:else}
|
||||
@@ -239,7 +258,7 @@
|
||||
|
||||
{#if g.over}
|
||||
<div class="verdict">
|
||||
<a class="commit" href="/">Back to the hall</a>
|
||||
{#if drake}<button type="button" class="commit" disabled={starting} onclick={again}>Play the drake again</button> <a class="quiet" href="/">Back to the hall</a>{:else}<a class="commit" href="/">Back to the hall</a>{/if}
|
||||
</div>
|
||||
{:else if yourMove}
|
||||
<p class="muted small how">Tap a piece, then a square on any board. Tap any piece, yours or not, to read what it is and what it can do. With a keyboard, the arrows move about a board, Page Up and Page Down change boards, Enter picks up and sets down, and Escape cancels.</p>
|
||||
@@ -366,7 +385,8 @@
|
||||
margin: 0.8rem auto 0;
|
||||
}
|
||||
|
||||
.verdict .commit {
|
||||
.verdict .commit,
|
||||
.verdict .quiet {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,13 @@
|
||||
return lines;
|
||||
});
|
||||
const letter = $derived(LETTERS[kind]);
|
||||
|
||||
/** On a phone the panel opens as a summary, and the lore unfolds on request. */
|
||||
let more = $state(false);
|
||||
$effect(() => {
|
||||
void at;
|
||||
more = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<aside class="lore" aria-label="About this piece">
|
||||
@@ -62,6 +69,8 @@
|
||||
<button type="button" class="dismiss" title="Close" aria-label="Close" onclick={close}>×</button>
|
||||
</header>
|
||||
<p class="now">{situation.join(' ')}</p>
|
||||
<button type="button" class="link plain unfold" aria-expanded={more} onclick={() => (more = !more)}>{more ? 'Less' : 'How it moves, why it is here, how to play it'}</button>
|
||||
<div class="rest" class:open={more}>
|
||||
<p>{lore.is}</p>
|
||||
<h3>How it moves</h3>
|
||||
<p>{lore.moves}</p>
|
||||
@@ -69,6 +78,7 @@
|
||||
<p>{lore.why}</p>
|
||||
<h3>How to play it</h3>
|
||||
<p>{lore.play}</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
@@ -107,6 +117,38 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.unfold {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.lore {
|
||||
padding-top: 0.6rem;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.now {
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.unfold {
|
||||
display: inline;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.rest {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rest.open {
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.now {
|
||||
color: var(--bone);
|
||||
border-left: 2px solid var(--ember);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
let error = $state('');
|
||||
let copied = $state(false);
|
||||
let reporting = $state(false);
|
||||
let tableOpen = $state(false);
|
||||
let prefsOpen = $state(false);
|
||||
/** A phrase minted to carry this seat to another device, while it lasts. */
|
||||
let phrase = $state<{ phrase: string; expiresAt: number } | null>(null);
|
||||
@@ -190,6 +191,10 @@
|
||||
{#if room.view.audience > 0}
|
||||
<span class="audience" title="the Peanut Gallery">{room.view.audience} in the gallery{room.view.galleryTalk ? ', talking' : ''}</span>
|
||||
{/if}
|
||||
{/if}
|
||||
<button type="button" class="quiet more" aria-expanded={tableOpen} onclick={() => (tableOpen = !tableOpen)}>Table {tableOpen ? '▴' : '▾'}</button>
|
||||
<div class="rest">
|
||||
{#if room}
|
||||
{#if !watching}
|
||||
<button type="button" class="quiet" title="A phrase that brings this seat to another device" onclick={transfer}>Transfer seat</button>
|
||||
{/if}
|
||||
@@ -204,6 +209,7 @@
|
||||
<button type="button" class="quiet" title="Something behaved unexpectedly? Tell the keeper." onclick={() => (reporting = true)}>Report</button>
|
||||
{/if}
|
||||
<a class="quiet" href="/">The hall</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@@ -311,6 +317,32 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* On a phone the masthead keeps the title, who is playing and the room code; the rest waits behind one button. */
|
||||
.more {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.rest {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.more {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.rest {
|
||||
display: none;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.more[aria-expanded='true'] + .rest {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.quiet.room b {
|
||||
letter-spacing: 0.12em;
|
||||
font-weight: 600;
|
||||
@@ -389,6 +421,12 @@
|
||||
@media (max-width: 860px) {
|
||||
.masthead {
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
padding: 0.6rem var(--gutter) 0.4rem;
|
||||
}
|
||||
|
||||
.standfirst {
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,7 +5,7 @@
|
||||
import PiecePanel from '$lib/components/PiecePanel.svelte';
|
||||
import { CALLOUTS, TOLD_MOVES, toldStates } from '$lib/game/told';
|
||||
import { narrate } from '$lib/game/chronicle';
|
||||
import { notation, kingSquare, inCheck, movesFrom, legalMovesFrom, sideOf } from '$lib/game';
|
||||
import { notation, kingSquare, inCheck, movesFrom, legalMovesFrom, sideOf, levelOf } from '$lib/game';
|
||||
import { prefs, reducedMotion } from '$lib/prefs.svelte';
|
||||
|
||||
/** The glass board, loaded the first time it is asked for. */
|
||||
@@ -49,11 +49,22 @@
|
||||
function go(n: number) {
|
||||
ply = Math.max(0, Math.min(TOLD_MOVES.length, n));
|
||||
inspected = null;
|
||||
showMove();
|
||||
}
|
||||
function tap(i: number) {
|
||||
inspected = g.cells[i] !== '' && inspected !== i ? i : null;
|
||||
}
|
||||
/** Left and right step the game, unless the keyboard is on a square, where the arrows walk the board. */
|
||||
/** On a phone the boards stack below the words, so the board the move was played on is brought into view. */
|
||||
function showMove() {
|
||||
const m = ply > 0 ? last.moves[ply - 1] : null;
|
||||
if (!m || typeof matchMedia === 'undefined' || !matchMedia('(max-width: 900px)').matches) return;
|
||||
requestAnimationFrame(() => {
|
||||
const target = onGlass ? document.querySelector('.boards canvas') : document.querySelector(`.boards g.world[data-level="${levelOf(m.to)}"]`);
|
||||
target?.scrollIntoView({ block: 'center', behavior: calm ? 'auto' : 'smooth' });
|
||||
});
|
||||
}
|
||||
|
||||
function key(e: KeyboardEvent) {
|
||||
if ((e.target as HTMLElement | null)?.tagName === 'rect') return;
|
||||
if (e.key === 'ArrowRight') go(ply + 1);
|
||||
@@ -97,6 +108,7 @@
|
||||
<button type="button" class="quiet" disabled={ply === 0} onclick={() => go(ply - 1)}>Back</button>
|
||||
<button type="button" class="commit small" disabled={ply === TOLD_MOVES.length} onclick={() => go(ply + 1)}>Next move</button>
|
||||
{#if ply === TOLD_MOVES.length}<a class="quiet" href="/">Play a drake</a>{/if}
|
||||
<span class="where">{ply === 0 ? 'Before the first move' : `Move ${ply} of ${TOLD_MOVES.length}`}{#if move} · {notation(move)}{/if}</span>
|
||||
</div>
|
||||
<p class="muted small marks">
|
||||
Jump to a callout:
|
||||
@@ -123,13 +135,47 @@
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.where {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.stage {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.words {
|
||||
/* The words come first and scroll with the page; the stickiness that keeps them beside the boards on a wide screen would pin them over the boards here. */
|
||||
.stage .words {
|
||||
order: -1;
|
||||
position: static;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Back and Next stay at the foot of the screen with the move's name, so the boards can be looked at without losing the thread. */
|
||||
.ways {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 4;
|
||||
margin: 0;
|
||||
padding: 0.55rem var(--gutter);
|
||||
background: var(--slate-deep);
|
||||
border-top: 1px solid var(--rule);
|
||||
}
|
||||
|
||||
.where {
|
||||
display: block;
|
||||
flex-basis: 100%;
|
||||
order: -1;
|
||||
font-size: 0.85rem;
|
||||
color: var(--bone-dim);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
main {
|
||||
padding-bottom: 4.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user