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:
@@ -83,7 +83,7 @@ Each wizard page has a **Reset this page** button that clears only that page's d
|
|||||||
|
|
||||||
### Character sheet (A4)
|
### 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
|
### Lobbies
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
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 J = { 'Content-Type': 'application/json' }
|
||||||
const send = (url, method, body) => fetch(url, { method, headers: J, body: body === undefined ? undefined : JSON.stringify(body) })
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{err && <p className="warn">{err}</p>}
|
{err && <p className="warn">{err}</p>}
|
||||||
{data && (
|
{data && <PaperSheet data={data} baseRules={rules} bare />}
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
} />
|
|
||||||
)}
|
|
||||||
</div>
|
</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' : ''}
|
<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>
|
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>
|
</div>
|
||||||
|
|
||||||
{active.startsWith('sheet:') && (() => {
|
{active.startsWith('sheet:') && (() => {
|
||||||
|
|||||||
+31
-13
@@ -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 Tip from './Tip.jsx'
|
||||||
import { compute, effectiveRules, kitItems, normalize } from './calc.js'
|
import { compute, effectiveRules, kitItems, normalize } from './calc.js'
|
||||||
import { explainAttr, explainCapacity, explainCarry, explainDerived, explainSkill } from './explain.js'
|
import { explainAttr, explainCapacity, explainCarry, explainDerived, explainSkill } from './explain.js'
|
||||||
|
|
||||||
const PAPER_PX = 794 // 210 mm at 96 dpi
|
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:
|
// 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.
|
// "Save as PDF" there gives an A4 PDF that looks exactly like the screen.
|
||||||
export function printSheet(root, title) {
|
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).
|
// 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 ch = useMemo(() => normalize(data, baseRules), [data, baseRules])
|
||||||
const rules = useMemo(() => effectiveRules(baseRules, ch), [baseRules, ch])
|
const rules = useMemo(() => effectiveRules(baseRules, ch), [baseRules, ch])
|
||||||
const c = useMemo(() => compute(ch, rules), [ch, rules])
|
const c = useMemo(() => compute(ch, rules), [ch, rules])
|
||||||
const x = { ch, rules, c }
|
const x = { ch, rules, c }
|
||||||
const root = useRef(null)
|
const root = useRef(null)
|
||||||
const [dark, setDark] = useState(() => { try { return localStorage.getItem('aw_sheettheme') === 'dark' } catch { return false } })
|
const [dark] = useSheetTheme()
|
||||||
const pickTheme = (d) => { setDark(d); try { localStorage.setItem('aw_sheettheme', d ? 'dark' : 'light') } catch { /* storage may be blocked */ } }
|
|
||||||
|
|
||||||
const prof = rules.professions.find((p) => p.id === ch.profession)
|
const prof = rules.professions.find((p) => p.id === ch.profession)
|
||||||
const cls = rules.classes.find((k) => k.id === ch.class_id)
|
const cls = rules.classes.find((k) => k.id === ch.class_id)
|
||||||
@@ -73,16 +92,15 @@ export default function PaperSheet({ data, baseRules, tools }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="papersheet">
|
<div className="papersheet">
|
||||||
<div className="papertools">
|
{!bare && (
|
||||||
{tools}
|
<div className="papertools">
|
||||||
<span className="toolright">
|
{tools}
|
||||||
<span className="seg" role="group" aria-label="Sheet colours">
|
<span className="toolright">
|
||||||
<button className={dark ? 'ghost' : 'on'} onClick={() => pickTheme(false)} title="Light sheet">Light</button>
|
<ThemeSwitch />
|
||||||
<button className={dark ? 'on' : 'ghost'} onClick={() => pickTheme(true)} title="Dark sheet (prints light)">Dark</button>
|
<button className="ghost" onClick={() => printSheet(root.current, name)} title="Opens the pages for printing, choose Save as PDF there">Print / PDF (A4)</button>
|
||||||
</span>
|
</span>
|
||||||
<button className="ghost" onClick={() => printSheet(root.current, name)} title="Opens the pages for printing, choose Save as PDF there">Print / PDF (A4)</button>
|
</div>
|
||||||
</span>
|
)}
|
||||||
</div>
|
|
||||||
<Fit rootRef={root} dark={dark}>
|
<Fit rootRef={root} dark={dark}>
|
||||||
<section className="paper">
|
<section className="paper">
|
||||||
<header className="phead">
|
<header className="phead">
|
||||||
|
|||||||
@@ -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; }
|
.lobbyhead h1 { font-size: 1.3rem; margin: 0; }
|
||||||
.lobbypage .tabs { margin: .4rem 0 .7rem; }
|
.lobbypage .tabs { margin: .4rem 0 .7rem; }
|
||||||
.lobbypage .tabs button { padding: .3rem .8rem; }
|
.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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user