Lobby: CSS 3D dice tray at the top right next to the chat

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K
This commit is contained in:
Claude
2026-10-21 18:07:07 +00:00
parent 968d168ff1
commit d730022447
4 changed files with 147 additions and 62 deletions
+4 -42
View File
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import PaperSheet from './PaperSheet.jsx'
import DiceTray from './DiceTray.jsx'
const J = { 'Content-Type': 'application/json' }
const send = (url, method, body) => fetch(url, { method, headers: J, body: body === undefined ? undefined : JSON.stringify(body) })
@@ -128,7 +129,6 @@ function Card({ m, isDm, onOpen, onKick }) {
const clock = (t) => new Date(t * 1000).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })
const DICE = [4, 6, 8, 10, 12, 20, 100]
const reduced = () => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
// Chat, whispers, event log and dice rolls arrive through one feed that is polled while the page is open.
function useFeed(id, onEvent, onRoll) {
@@ -271,44 +271,6 @@ function DiceBar({ id, to, poll, isDm }) {
)
}
function Die({ sides, value, delay }) {
const [shown, setShown] = useState(reduced() ? value : null)
useEffect(() => {
if (reduced()) return
const t0 = Date.now(), run = 650 + delay
const iv = setInterval(() => {
if (Date.now() - t0 >= run) { setShown(value); clearInterval(iv) } else setShown(1 + Math.floor(Math.random() * sides))
}, 70)
return () => clearInterval(iv)
}, [sides, value, delay])
const rolling = shown !== value
return (
<span className={`die d${sides} ${rolling ? 'rolling' : 'landed'}`} style={{ animationDelay: `${delay % 200}ms` }}>
<span>{shown ?? '?'}</span>
</span>
)
}
function DiceTray({ rolls, me }) {
if (!rolls.length) return null
return (
<div className="dicetray" aria-hidden="true">
{rolls.map((m) => {
const t = rollText(m)
return (
<div key={m.key} className="trayroll">
<div className="small">{m.mine ? 'You' : m.from} rolled <strong>{t.dice}</strong>{m.to ? (m.to === m.from ? ' · only you' : ` · private${m.mine ? ` to ${m.to}` : ''}`) : ''}</div>
<div className="trayrow">
{m.roll.results.map((v, i) => <Die key={i} sides={m.roll.sides} value={v} delay={i * 120} />)}
{t.n > 1 && <span className="traytotal">= {t.sum}</span>}
</div>
</div>
)
})}
</div>
)
}
// ---- sheet of one character, refreshed regularly so the DM sees changes the player saves
function CharTab({ cid, owner, rules, onOpen, name }) {
const [data, setData] = useState(null)
@@ -359,8 +321,8 @@ function LobbyView({ id, rules, user, onOpen }) {
const onRoll = useCallback((m) => {
const key = `${m.id}`
setTray((t) => [...t.slice(-2), { ...m, key }])
setTimeout(() => setTray((t) => t.filter((x) => x.key !== key)), 9000)
setTray((t) => [...t.slice(-1), { ...m, key }])
setTimeout(() => setTray((t) => t.filter((x) => x.key !== key)), 10000)
}, [])
const feed = useFeed(id, load, onRoll)
@@ -462,7 +424,7 @@ function LobbyView({ id, rules, user, onOpen }) {
</aside>
</div>
<DiceTray rolls={tray} me={user} />
<DiceTray rolls={tray} />
<DiceBar id={id} to={dest} poll={feed.poll} isDm={lobby.is_dm} />
</section>
)