Lobby: sheet theme switch in the tab bar, remove extra toolbar

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-22 11:12:32 +00:00
parent 7e69b68b37
commit 59df681cfa
4 changed files with 42 additions and 23 deletions
+1 -1
View File
@@ -83,7 +83,7 @@ Each wizard page has a **Reset this page** button that clears only that page's d
### Character sheet (A4)
The Summary page and the sheet tabs in the lobbies show the character as **A4 pages**: page 1 has name, attributes, derived stats and all skills (proficiency dots, attribute, points, total), page 2 has talents, mutations and implants, equipment, background and a ruled notes area. **Print / PDF (A4)** opens just the pages in a clean window; choose "Save as PDF" in the print dialog (margins none, background graphics on if your browser asks). Hover or tap a value to see how it is made up (not printed). **Light / Dark** switches the colours of the sheet on screen (remembered in the browser; printed pages are always light). The Word and PDF downloads on the Summary page are separate exports with their own layout.
The Summary page and the sheet tabs in the lobbies show the character as **A4 pages**: page 1 has name, attributes, derived stats and all skills (proficiency dots, attribute, points, total), page 2 has talents, mutations and implants, equipment, background and a ruled notes area. On the Summary page, **Print / PDF (A4)** opens just the pages in a clean window; choose "Save as PDF" in the print dialog (margins none, background graphics on if your browser asks). Hover or tap a value to see how it is made up (not printed). **Light / Dark** (in the lobby it sits in the tab bar) switches the colours of the sheet on screen (remembered in the browser; printed pages are always light). The Word and PDF downloads on the Summary page are separate exports with their own layout.
### Lobbies
+3 -9
View File
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import PaperSheet from './PaperSheet.jsx'
import PaperSheet, { ThemeSwitch } from './PaperSheet.jsx'
const J = { 'Content-Type': 'application/json' }
const send = (url, method, body) => fetch(url, { method, headers: J, body: body === undefined ? undefined : JSON.stringify(body) })
@@ -323,14 +323,7 @@ function CharTab({ cid, owner, rules, onOpen, name }) {
return (
<div>
{err && <p className="warn">{err}</p>}
{data && (
<PaperSheet data={data} baseRules={rules} tools={
<>
<span className="muted small">{owner ? `Played by ${owner}` : 'Your character'} · read-only, hover or tap a value for details</span>
<button className="ghost" onClick={onOpen}>{owner ? 'Open in Character tab' : 'Edit in Character tab'}</button>
</>
} />
)}
{data && <PaperSheet data={data} baseRules={rules} bare />}
</div>
)
}
@@ -400,6 +393,7 @@ function LobbyView({ id, rules, user, onOpen }) {
<button key={m.username} role="tab" aria-selected={active === `sheet:${m.username}`} className={active === `sheet:${m.username}` ? 'on' : ''}
onClick={() => setTab(`sheet:${m.username}`)} title={m.username}>{m.me && !lobby.is_dm ? 'My character' : m.character.name}</button>
))}
{active.startsWith('sheet:') && <span className="tabright"><ThemeSwitch /></span>}
</div>
{active.startsWith('sheet:') && (() => {
+26 -8
View File
@@ -1,10 +1,30 @@
import { useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
import Tip from './Tip.jsx'
import { compute, effectiveRules, kitItems, normalize } from './calc.js'
import { explainAttr, explainCapacity, explainCarry, explainDerived, explainSkill } from './explain.js'
const PAPER_PX = 794 // 210 mm at 96 dpi
// Light/dark choice for the sheet, shared by all sheets and switches on the page and remembered in the browser
const THEME_KEY = 'aw_sheettheme'
const listeners = new Set()
const readTheme = () => { try { return localStorage.getItem(THEME_KEY) === 'dark' } catch { return false } }
let darkNow = readTheme()
export function useSheetTheme() {
const dark = useSyncExternalStore((cb) => { listeners.add(cb); return () => listeners.delete(cb) }, () => darkNow)
const set = (d) => { darkNow = d; try { localStorage.setItem(THEME_KEY, d ? 'dark' : 'light') } catch { /* storage may be blocked */ } listeners.forEach((f) => f()) }
return [dark, set]
}
export function ThemeSwitch() {
const [dark, setDark] = useSheetTheme()
return (
<span className="seg" role="group" aria-label="Sheet colours">
<button className={dark ? 'ghost' : 'on'} onClick={() => setDark(false)} title="Light sheet">Light</button>
<button className={dark ? 'on' : 'ghost'} onClick={() => setDark(true)} title="Dark sheet (prints light)">Dark</button>
</span>
)
}
// Opens the sheet pages in a clean window (only the paper, A4, no margins) and starts the print dialog:
// "Save as PDF" there gives an A4 PDF that looks exactly like the screen.
export function printSheet(root, title) {
@@ -48,14 +68,13 @@ const Pips = ({ n, locked }) => (
)
// The character sheet as A4 pages (page 1: attributes and skills, page 2: talents, mutations, equipment, notes).
export default function PaperSheet({ data, baseRules, tools }) {
export default function PaperSheet({ data, baseRules, tools, bare }) {
const ch = useMemo(() => normalize(data, baseRules), [data, baseRules])
const rules = useMemo(() => effectiveRules(baseRules, ch), [baseRules, ch])
const c = useMemo(() => compute(ch, rules), [ch, rules])
const x = { ch, rules, c }
const root = useRef(null)
const [dark, setDark] = useState(() => { try { return localStorage.getItem('aw_sheettheme') === 'dark' } catch { return false } })
const pickTheme = (d) => { setDark(d); try { localStorage.setItem('aw_sheettheme', d ? 'dark' : 'light') } catch { /* storage may be blocked */ } }
const [dark] = useSheetTheme()
const prof = rules.professions.find((p) => p.id === ch.profession)
const cls = rules.classes.find((k) => k.id === ch.class_id)
@@ -73,16 +92,15 @@ export default function PaperSheet({ data, baseRules, tools }) {
return (
<div className="papersheet">
{!bare && (
<div className="papertools">
{tools}
<span className="toolright">
<span className="seg" role="group" aria-label="Sheet colours">
<button className={dark ? 'ghost' : 'on'} onClick={() => pickTheme(false)} title="Light sheet">Light</button>
<button className={dark ? 'on' : 'ghost'} onClick={() => pickTheme(true)} title="Dark sheet (prints light)">Dark</button>
</span>
<ThemeSwitch />
<button className="ghost" onClick={() => printSheet(root.current, name)} title="Opens the pages for printing, choose Save as PDF there">Print / PDF (A4)</button>
</span>
</div>
)}
<Fit rootRef={root} dark={dark}>
<section className="paper">
<header className="phead">
+7
View File
@@ -328,3 +328,10 @@ body:has(.lobbychat) main { max-width: none; margin: 0; padding-right: calc(var(
.lobbyhead h1 { font-size: 1.3rem; margin: 0; }
.lobbypage .tabs { margin: .4rem 0 .7rem; }
.lobbypage .tabs button { padding: .3rem .8rem; }
.tabs { align-items: center; }
.tabs .tabright { margin-left: auto; align-self: center; margin-bottom: .3rem; }
.tabs .tabright button { padding: .25rem .75rem; margin-bottom: 0; border-radius: 0; border-bottom: 1px solid var(--line); }
.tabs .tabright button.on { border-color: var(--noxium); }
.tabs .tabright button:first-child { border-radius: var(--radius) 0 0 var(--radius); }
.tabs .tabright button:last-child { border-radius: 0 var(--radius) var(--radius) 0; }