The masthead, the scrim, the quiet button and the colophon are styled once in app.css instead of pasted into each page and slip with drifting paddings; the style blocks of the play page and the board are indented like the rest of the files; and comments in script and CSS say what the code does or guards, in plain words, where they had been written in the game's own voice
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
1 parent
22979afd35
commit
42144c1cc9
19 files changed
+364
-481
No files matched your search
@@ -13,22 +13,22 @@
|
||||
let room = $state<Duel | null>(null);
|
||||
let name = $state('');
|
||||
let joining = $state(false);
|
||||
/** The host asked to begin while a seat is still held for someone on their way. */
|
||||
/** The host pressed Begin while a seat is still held for someone expected; confirm before going on. */
|
||||
let beginAsked = $state(false);
|
||||
let busy = $state(false);
|
||||
let error = $state('');
|
||||
let copied = $state(false);
|
||||
let reporting = $state(false);
|
||||
/** A phrase minted to carry this seat to another device, while it lasts. */
|
||||
/** This seat's transfer phrase, until it expires. */
|
||||
let phrase = $state<{ phrase: string; expiresAt: number } | null>(null);
|
||||
let rematching = $state(false);
|
||||
let seatsOpen = $state(false);
|
||||
/** Why this browser's seat no longer opens, when the server said so; shown while watching. */
|
||||
/** The server's reason for refusing this browser's seat; shown while spectating. */
|
||||
let seatLost = $state('');
|
||||
let claimPhrase = $state('');
|
||||
let claiming = $state(false);
|
||||
|
||||
/** The hour where the keeper lives, in words, so a caller knows whether to wait. */
|
||||
/** The current hour in the site maintainer's time zone, in words, shown beside the challenge button. */
|
||||
let minute = $state(0);
|
||||
$effect(() => {
|
||||
const id = setInterval(() => (minute += 1), 60_000);
|
||||
@@ -76,18 +76,18 @@
|
||||
const link = $derived(typeof location === 'undefined' ? '' : `${location.origin}/join/${roomId}`);
|
||||
const lobby = $derived(room !== null && !room.remote?.started);
|
||||
const watching = $derived(room?.spectating ?? false);
|
||||
/** A watcher may still sit while the table is laid and a seat is free. */
|
||||
/** A spectator may still take a seat before the game starts, while one is free. */
|
||||
const seatFree = $derived(!!room?.remote && !room.remote.started && room.remote.seats.length < room.remote.size);
|
||||
|
||||
// Return to a held seat, or take a place in the gallery and offer one. A link from the keeper carries a seat:
|
||||
// it leaves the address bar at once, is kept only once the server takes it, and never replaces a seat that
|
||||
// still opens, so a link someone else sends cannot take a working seat away.
|
||||
// Resume a held seat, else spectate. A link may carry a seat (#seat=...): it is stripped from the address
|
||||
// bar at once, saved only once the server accepts it, and never replaces a held seat the server still
|
||||
// accepts, so a forwarded link cannot take a working seat.
|
||||
$effect(() => {
|
||||
const id = roomId;
|
||||
room = null;
|
||||
error = '';
|
||||
const sent = seatInLink(location.hash);
|
||||
// The browser's own call: this runs as the page first loads, before the router could take one.
|
||||
// The History API directly, not SvelteKit's replaceState: this runs on first load, before the router is ready.
|
||||
if (sent) history.replaceState(history.state, '', location.pathname + location.search);
|
||||
const held = seatFor(id);
|
||||
const take = (s: HeldSeat) =>
|
||||
@@ -96,9 +96,9 @@
|
||||
trustSeat(id);
|
||||
return remote;
|
||||
});
|
||||
// Only the server's refusal of the held token lets a link in: a dropped connection is not a lost seat.
|
||||
// Only a 403 on the held token lets the link's seat in; a dropped connection is not a lost seat.
|
||||
const refused = (e: unknown) => e instanceof ServerError && e.status === 403;
|
||||
// A held seat the server refuses, with no link to replace it, is watched from the gallery, with the reason.
|
||||
// A refused seat with no link to replace it falls back to spectating, keeping the server's reason.
|
||||
const watchRefused = (e: Error) => {
|
||||
seatLost = e.message;
|
||||
return Duel.watchRoom(id);
|
||||
@@ -127,7 +127,7 @@
|
||||
return stop;
|
||||
});
|
||||
|
||||
// The seat stopped opening while the page was open (the host gave it to a bot): watch on from the gallery.
|
||||
// The seat was revoked while the page was open (the host gave it to a bot): switch to spectating.
|
||||
$effect(() => {
|
||||
const r = room;
|
||||
const lost = r?.remote?.lost;
|
||||
@@ -138,7 +138,7 @@
|
||||
.catch((e: Error) => (error = e.message));
|
||||
});
|
||||
|
||||
/** Sit back down by a phrase: the host's, or one this seat minted on another device. */
|
||||
/** Claim a seat by transfer phrase: the host's, or one this seat made on another device. */
|
||||
async function claim(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
claiming = true;
|
||||
@@ -380,57 +380,6 @@
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.masthead {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem var(--gutter) 0.6rem;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2rem, 4vw, 2.7rem);
|
||||
font-weight: 300;
|
||||
font-variation-settings: 'opsz' 144;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.standfirst {
|
||||
max-width: 38em;
|
||||
color: var(--bone-dim);
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.standfirst em {
|
||||
color: var(--bone);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.quiet {
|
||||
background: none;
|
||||
border: 1px solid var(--rule-strong);
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem 0.9rem;
|
||||
white-space: nowrap;
|
||||
color: var(--bone);
|
||||
text-decoration: none;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.quiet:hover {
|
||||
border-color: var(--bone);
|
||||
}
|
||||
|
||||
.quiet.room b {
|
||||
letter-spacing: 0.12em;
|
||||
font-weight: 600;
|
||||
@@ -517,11 +466,6 @@
|
||||
width: 14em;
|
||||
}
|
||||
|
||||
.reveal.small {
|
||||
padding: 0.4rem 0.9rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.roster {
|
||||
list-style: none;
|
||||
margin: 0.9rem 0 0.6rem;
|
||||
@@ -583,27 +527,4 @@
|
||||
border-radius: 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.colophon {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem var(--gutter) 3rem;
|
||||
color: var(--bone-faint);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.colophon a {
|
||||
color: var(--frost);
|
||||
}
|
||||
|
||||
/* Must match COMPACT_QUERY in duel.svelte.ts. */
|
||||
@media (max-width: 860px) {
|
||||
.masthead {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.colophon {
|
||||
padding-bottom: 6rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user