Phone fixes at a crowded table: focus scroll in viewport coordinates, a wrapping masthead, a slimmer bar
Turning the ledger toward a wizard summed a sticky cell's offset, which moves with the scroll; it now uses viewport rectangles and the width the pinned columns cover, so repeated and switched taps land the same. The masthead's actions wrap instead of running off a phone. The phone bar names you, the wizard in focus and the count, with the roster a tap away. The hands row's first cell is pinned with the rest of the turn column. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
5c79a65319
commit
effe17c729
@@ -75,17 +75,22 @@
|
|||||||
|
|
||||||
const crowded = $derived(duel.others.length >= 3);
|
const crowded = $derived(duel.others.length >= 3);
|
||||||
|
|
||||||
// The focused wizard's columns slide in just past the pinned ones.
|
// The focused wizard's columns slide in just past the pinned ones. Sticky cells
|
||||||
|
// report where they are stuck, not where they lie, so the sums are done in
|
||||||
|
// viewport coordinates: the header's distance from the scroller's edge plus
|
||||||
|
// the current scroll, less the width the pinned columns always cover.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const who = duel.focus;
|
const who = duel.focus;
|
||||||
void duel.focusTick;
|
void duel.focusTick;
|
||||||
if (!who || !scroller) return;
|
if (!who || !scroller) return;
|
||||||
const th = scroller.querySelector<HTMLElement>(`th[data-who="${who}"]`);
|
const th = scroller.querySelector<HTMLElement>(`th[data-who="${who}"]`);
|
||||||
const pinned = scroller.querySelector<HTMLElement>('thead th.pin-right');
|
|
||||||
if (!th) return;
|
if (!th) return;
|
||||||
const left = pinned ? pinned.offsetLeft + pinned.offsetWidth : 0;
|
const pinnedWidth = ['th.turn', 'thead tr.hands th.pin-left', 'thead tr.hands th.pin-right']
|
||||||
|
.map((sel) => scroller!.querySelector<HTMLElement>(sel)?.getBoundingClientRect().width ?? 0)
|
||||||
|
.reduce((a, b) => a + b, 0);
|
||||||
|
const left = th.getBoundingClientRect().left - scroller.getBoundingClientRect().left + scroller.scrollLeft - pinnedWidth;
|
||||||
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
scroller.scrollTo({ left: th.offsetLeft - left, behavior: reduce ? 'auto' : 'smooth' });
|
scroller.scrollTo({ left: Math.max(0, left), behavior: reduce ? 'auto' : 'smooth' });
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -110,7 +115,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="hands">
|
<tr class="hands">
|
||||||
<th></th>
|
<th class="turn"></th>
|
||||||
{#each columns as who (who)}
|
{#each columns as who (who)}
|
||||||
<th scope="col" class:pin-left={who === duel.me}>left</th>
|
<th scope="col" class:pin-left={who === duel.me}>left</th>
|
||||||
<th scope="col" class:pin-right={who === duel.me}>right</th>
|
<th scope="col" class:pin-right={who === duel.me}>right</th>
|
||||||
|
|||||||
@@ -4,16 +4,28 @@
|
|||||||
import { MAX_HP } from '$lib/game/state';
|
import { MAX_HP } from '$lib/game/state';
|
||||||
|
|
||||||
let { duel }: { duel: Duel } = $props();
|
let { duel }: { duel: Duel } = $props();
|
||||||
|
|
||||||
|
/** Past three wizards the bar names you, the wizard in focus, and the count; the roster is a tap away. */
|
||||||
|
const crowded = $derived(duel.others.length >= 3);
|
||||||
|
const shown = $derived(crowded ? [duel.focus ?? duel.foeId].filter((id) => id !== duel.me) : duel.others);
|
||||||
|
|
||||||
|
function toRoster() {
|
||||||
|
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
document.getElementById('roster')?.scrollIntoView({ behavior: reduce ? 'auto' : 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !duel.state.over}
|
{#if !duel.state.over}
|
||||||
<div class="bar" class:watching={duel.spectating}>
|
<div class="bar" class:watching={duel.spectating}>
|
||||||
<div class="health" class:many={duel.others.length > 1}>
|
<div class="health" class:many={duel.others.length > 1}>
|
||||||
{#if !duel.spectating}<span><em>{duel.you.name}</em> {duel.you.hp}/{MAX_HP}</span>{/if}
|
{#if !duel.spectating}<span><em>{duel.you.name}</em> {duel.you.hp}/{MAX_HP}</span>{/if}
|
||||||
{#each duel.others as id (id)}
|
{#each shown as id (id)}
|
||||||
{@const w = duel.state.wizards[id]}
|
{@const w = duel.state.wizards[id]}
|
||||||
<span class:gone={!w.alive || w.surrendered}><em>{w.name}</em> {w.hp}/{MAX_HP}</span>
|
<span class:gone={!w.alive || w.surrendered}><em>{w.name}</em> {w.hp}/{MAX_HP}</span>
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if crowded}
|
||||||
|
<button type="button" class="count" onclick={toRoster}>{duel.state.seats.length} wizards</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if duel.spectating}
|
{#if duel.spectating}
|
||||||
<span class="gallery">the gallery</span>
|
<span class="gallery">the gallery</span>
|
||||||
@@ -88,6 +100,15 @@
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.count {
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
color: var(--frost);
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
}
|
||||||
|
|
||||||
.gestures {
|
.gestures {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="roster" aria-label="The other wizards">
|
<section class="roster" id="roster" aria-label="The other wizards">
|
||||||
<h2>At the table</h2>
|
<h2>At the table</h2>
|
||||||
<ul>
|
<ul>
|
||||||
{#each duel.others as id (id)}
|
{#each duel.others as id (id)}
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.roster {
|
.roster {
|
||||||
|
scroll-margin-top: 0.5rem;
|
||||||
padding: 0.75rem 0 0.4rem;
|
padding: 0.75rem 0 0.4rem;
|
||||||
border-bottom: 1px solid var(--rule);
|
border-bottom: 1px solid var(--rule);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -234,8 +234,11 @@
|
|||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quiet {
|
.quiet {
|
||||||
|
|||||||
@@ -136,8 +136,11 @@
|
|||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quiet {
|
.quiet {
|
||||||
|
|||||||
Reference in New Issue
Block a user