Sheet: light/dark switch, slimmer lobby header, remove wide view
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K
This commit is contained in:
+11
-19
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import SheetView from './SheetView.jsx'
|
||||
import PaperSheet from './PaperSheet.jsx'
|
||||
|
||||
const J = { 'Content-Type': 'application/json' }
|
||||
@@ -321,24 +320,17 @@ function CharTab({ cid, owner, rules, onOpen, name }) {
|
||||
const t = setInterval(() => !document.hidden && load(), 15000)
|
||||
return () => clearInterval(t)
|
||||
}, [cid])
|
||||
const [view, setView] = useState(() => { try { return localStorage.getItem('aw_sheetview') === 'wide' ? 'wide' : 'paper' } catch { return 'paper' } })
|
||||
const pick = (v) => { setView(v); try { localStorage.setItem('aw_sheetview', v) } catch { /* storage may be blocked */ } }
|
||||
const viewSwitch = (
|
||||
<span className="seg" role="group" aria-label="Sheet layout">
|
||||
<button className={view === 'paper' ? 'on' : 'ghost'} onClick={() => pick('paper')} title="A4 page layout, compact">A4 sheet</button>
|
||||
<button className={view === 'wide' ? 'on' : 'ghost'} onClick={() => pick('wide')} title="Expand to the full width">Wide</button>
|
||||
</span>
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<div className="row">
|
||||
<h2>{data?.name || name || 'Nameless'}</h2>
|
||||
<button className="ghost" onClick={onOpen}>{owner ? 'Open in the Character tab' : 'Edit in the Character tab'}</button>
|
||||
</div>
|
||||
<p className="muted small">{owner ? `Played by ${owner}. ` : ''}Read-only, hover or tap a value to see how it is made up.</p>
|
||||
{err && <p className="warn">{err}</p>}
|
||||
{data && view === 'paper' && <PaperSheet data={data} baseRules={rules} tools={viewSwitch} />}
|
||||
{data && view === 'wide' && <><div className="papertools">{viewSwitch}</div><SheetView data={data} baseRules={rules} /></>}
|
||||
{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>
|
||||
</>
|
||||
} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -392,10 +384,10 @@ function LobbyView({ id, rules, user, onOpen }) {
|
||||
|
||||
return (
|
||||
<section className="page lobbypage">
|
||||
<p className="small"><a href="#lobbies">← All lobbies</a></p>
|
||||
<div className="row">
|
||||
<div className="lobbyhead">
|
||||
<a className="small" href="#lobbies">← Lobbies</a>
|
||||
<h1>{lobby.name}</h1>
|
||||
<span className="muted">DM: {lobby.dm}{lobby.is_dm ? ' (you)' : ''}</span>
|
||||
<span className="muted small">DM: {lobby.dm}{lobby.is_dm ? ' (you)' : ''}</span>
|
||||
</div>
|
||||
{err && <p className="warn">{err}</p>}
|
||||
{info && <p className="small">{info}</p>}
|
||||
|
||||
@@ -12,6 +12,7 @@ export function printSheet(root, title) {
|
||||
if (!win) { window.alert('Please allow pop-ups for this site to print the sheet.'); return }
|
||||
const clone = root.cloneNode(true)
|
||||
clone.style.zoom = ''
|
||||
clone.classList.remove('dark') // printed pages are always light
|
||||
const head = [...document.querySelectorAll('link[rel=stylesheet], style')].map((n) => n.outerHTML).join('')
|
||||
win.document.open()
|
||||
win.document.write(`<!doctype html><html><head><meta charset="utf-8"><title>${(title || 'Character').replace(/[<>&]/g, '')}</title>${head}
|
||||
@@ -27,7 +28,7 @@ export function printSheet(root, title) {
|
||||
}
|
||||
|
||||
// Scales the A4 pages down when the space is narrower than the paper
|
||||
function Fit({ children, rootRef }) {
|
||||
function Fit({ children, rootRef, dark }) {
|
||||
const wrap = useRef(null)
|
||||
const [z, setZ] = useState(1)
|
||||
useLayoutEffect(() => {
|
||||
@@ -37,7 +38,7 @@ function Fit({ children, rootRef }) {
|
||||
ro.observe(wrap.current)
|
||||
return () => ro.disconnect()
|
||||
}, [])
|
||||
return <div ref={wrap} className="paperfit"><div ref={rootRef} className="papers" style={{ zoom: z }}>{children}</div></div>
|
||||
return <div ref={wrap} className="paperfit"><div ref={rootRef} className={`papers${dark ? ' dark' : ''}`} style={{ zoom: z }}>{children}</div></div>
|
||||
}
|
||||
|
||||
const Pips = ({ n, locked }) => (
|
||||
@@ -53,6 +54,8 @@ export default function PaperSheet({ data, baseRules, tools }) {
|
||||
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 prof = rules.professions.find((p) => p.id === ch.profession)
|
||||
const cls = rules.classes.find((k) => k.id === ch.class_id)
|
||||
@@ -72,9 +75,15 @@ export default function PaperSheet({ data, baseRules, tools }) {
|
||||
<div className="papersheet">
|
||||
<div className="papertools">
|
||||
{tools}
|
||||
<button className="ghost" onClick={() => printSheet(root.current, name)}>Print / save as PDF (A4)</button>
|
||||
<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>
|
||||
<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}>
|
||||
<Fit rootRef={root} dark={dark}>
|
||||
<section className="paper">
|
||||
<header className="phead">
|
||||
<div className="pname">{name}</div>
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { useMemo } from 'react'
|
||||
import Tip from './Tip.jsx'
|
||||
import { normalize } from './calc.js'
|
||||
import { sheetModel } from './sheetModel.js'
|
||||
|
||||
function Rows({ rows, valueLabel }) {
|
||||
if (!rows.length) return <p className="muted small">–</p>
|
||||
return (
|
||||
<table className="difftable"><tbody>
|
||||
{rows.map((r) => (
|
||||
<tr key={r.k} className={r.dim ? 'dim' : ''}>
|
||||
<td><Tip text={r.tip}>{r.label}</Tip>{r.desc && <div className="rowdesc">{r.desc}</div>}</td>
|
||||
<td>{valueLabel ? valueLabel(r) : r.val}</td>
|
||||
{r.extra !== undefined && <td className="muted">{r.extra}</td>}
|
||||
</tr>
|
||||
))}
|
||||
</tbody></table>
|
||||
)
|
||||
}
|
||||
|
||||
// Read-only character sheet (used in lobbies). Hover or tap a value to see how it is made up.
|
||||
export default function SheetView({ data, baseRules }) {
|
||||
const m = useMemo(() => sheetModel(normalize(data, baseRules), baseRules), [data, baseRules])
|
||||
const c = m.c
|
||||
const trained = m.skills.filter((s) => c.skills.find((x) => x.id === s.k && (x.points > 0 || x.pips > 0 || (x.type === 'advanced' && x.usable))))
|
||||
const rest = m.skills.filter((s) => !trained.includes(s))
|
||||
|
||||
return (
|
||||
<div className="sheetview">
|
||||
<Rows rows={m.head.filter((h) => h.k !== 'name')} />
|
||||
<div className="sheetcols">
|
||||
<div>
|
||||
<h4>Attributes</h4>
|
||||
<Rows rows={m.attrs} />
|
||||
<h4>Derived</h4>
|
||||
<Rows rows={m.derived} />
|
||||
</div>
|
||||
<div>
|
||||
<h4>Skills</h4>
|
||||
<Rows rows={trained} />
|
||||
{rest.length > 0 && (
|
||||
<details>
|
||||
<summary className="small muted">All other skills ({rest.length})</summary>
|
||||
<Rows rows={rest} />
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<h4>Talents</h4>
|
||||
<Rows rows={m.talents} />
|
||||
<h4><Tip text={m.tips.capacity}>Mutations / implants <span className="muted small">{c.capacityUsed}/{c.capacityMax}</span></Tip></h4>
|
||||
<Rows rows={m.augments} />
|
||||
<h4><Tip text={m.tips.carry}>Equipment <span className="muted small">{c.weight}/{c.carryMax} kg</span></Tip></h4>
|
||||
<Rows rows={m.gear} valueLabel={(r) => (r.val > 1 ? `x${r.val}` : '')} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+39
-28
@@ -253,67 +253,78 @@ body:has(.lobbychat) main { max-width: none; margin: 0; padding-right: calc(var(
|
||||
.lobbycard .chip { align-self: flex-start; }
|
||||
|
||||
/* ---- A4 character sheet (fixed colours on purpose: it is printed on white paper) ---- */
|
||||
.papertools { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin: .4rem 0 .8rem; }
|
||||
.papertools { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin: 0 0 .5rem; }
|
||||
.papertools .toolright { margin-left: auto; display: flex; gap: .5rem; align-items: center; }
|
||||
.papertools button { padding: .3rem .8rem; }
|
||||
.seg { display: inline-flex; }
|
||||
.seg button { border-radius: 0; padding: .4rem .9rem; }
|
||||
.seg button:first-child { border-radius: var(--radius) 0 0 var(--radius); }
|
||||
.seg button:last-child { border-radius: 0 var(--radius) var(--radius) 0; margin-left: -1px; }
|
||||
.seg button.on { background: var(--noxium); color: #12150f; border-color: var(--noxium); }
|
||||
.paperfit { width: 100%; }
|
||||
.papers { display: grid; gap: 14px; justify-items: start; width: 210mm; }
|
||||
.paper { box-sizing: border-box; width: 210mm; min-height: 297mm; padding: 11mm 12mm 8mm; background: #fff; color: #1a1f14; border: 1px solid #cfd5c3; box-shadow: 0 2px 12px rgba(0, 0, 0, .14);
|
||||
.papers { display: grid; gap: 14px; justify-items: start; width: 210mm;
|
||||
--pi: #1a1f14; --pg: #2f6a0d; --pbar: #2f6a0d; --pm: #6a7260; --pl: #cfd5c3; --pd: #c3cab6; --pt: #eef3e6; --pb: #fff; --p2: #4d5645; --p3: #3b4433; --p4: #5c6553; --pn: #b9c1ab; }
|
||||
.papers.dark { --pi: #e4e9d8; --pg: #8fd44c; --pbar: #3c7a16; --pm: #98a28a; --pl: #343b2d; --pd: #353c2e; --pt: #252b1f; --pb: #161a12; --p2: #b4bda6; --p3: #c5cdb7; --p4: #a1aa93; --pn: #454d3a; }
|
||||
.papers.dark .paper { box-shadow: 0 2px 12px rgba(0, 0, 0, .5); }
|
||||
.paper { box-sizing: border-box; width: 210mm; min-height: 297mm; padding: 11mm 12mm 8mm; background: var(--pb); color: var(--pi); border: 1px solid var(--pl); box-shadow: 0 2px 12px rgba(0, 0, 0, .14);
|
||||
display: flex; flex-direction: column; gap: 3.5mm; font-size: 8.6pt; line-height: 1.25; break-after: page; }
|
||||
.paper * { color: inherit; }
|
||||
.paper .tipped { border-bottom: 0; text-decoration: none; cursor: default; }
|
||||
.paper p { margin: 0; }
|
||||
.phead { display: flex; justify-content: space-between; align-items: flex-end; gap: 8mm; border-bottom: 2px solid #2f6a0d; padding-bottom: 2mm; }
|
||||
.phead { display: flex; justify-content: space-between; align-items: flex-end; gap: 8mm; border-bottom: 2px solid var(--pg); padding-bottom: 2mm; }
|
||||
.pname { font-size: 24pt; font-weight: 800; line-height: 1.05; letter-spacing: .01em; overflow-wrap: anywhere; }
|
||||
.phead.small .pname { font-size: 15pt; }
|
||||
.pbrand { text-align: right; font-weight: 800; text-transform: uppercase; letter-spacing: .12em; font-size: 8.5pt; color: #2f6a0d !important; white-space: nowrap; }
|
||||
.pbrand span { display: block; font-weight: 500; letter-spacing: .08em; font-size: 7pt; color: #6a7260 !important; }
|
||||
.pbrand { text-align: right; font-weight: 800; text-transform: uppercase; letter-spacing: .12em; font-size: 8.5pt; color: var(--pg) !important; white-space: nowrap; }
|
||||
.pbrand span { display: block; font-weight: 500; letter-spacing: .08em; font-size: 7pt; color: var(--pm) !important; }
|
||||
.pfields { display: grid; grid-template-columns: 1fr 1.6fr 1.3fr 1fr .8fr; gap: 3mm; }
|
||||
.pfield { border-bottom: 1px solid #1a1f14; padding-bottom: .6mm; min-width: 0; }
|
||||
.pfield span { display: block; font-size: 6.2pt; text-transform: uppercase; letter-spacing: .1em; color: #6a7260 !important; }
|
||||
.pfield { border-bottom: 1px solid var(--pi); padding-bottom: .6mm; min-width: 0; }
|
||||
.pfield span { display: block; font-size: 6.2pt; text-transform: uppercase; letter-spacing: .1em; color: var(--pm) !important; }
|
||||
.pfield b { font-weight: 600; font-size: 9pt; overflow-wrap: anywhere; }
|
||||
.pattrs { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2.5mm; }
|
||||
.pattr { border: 1.4px solid #1a1f14; border-radius: 3mm; text-align: center; padding: 1.6mm 1mm 1.4mm; display: grid; gap: .2mm; }
|
||||
.pattr { border: 1.4px solid var(--pi); border-radius: 3mm; text-align: center; padding: 1.6mm 1mm 1.4mm; display: grid; gap: .2mm; }
|
||||
.pattr-n { font-size: 6.6pt; text-transform: uppercase; letter-spacing: .08em; font-weight: 700; }
|
||||
.pattr-v { font-size: 20pt; line-height: 1; }
|
||||
.pattr-m { font-size: 7pt; color: #4d5645 !important; border-top: 1px solid #cfd5c3; padding-top: .8mm; }
|
||||
.pattr-m { font-size: 7pt; color: var(--p2) !important; border-top: 1px solid var(--pl); padding-top: .8mm; }
|
||||
.pderived { display: grid; grid-template-columns: repeat(auto-fit, minmax(28mm, 1fr)); gap: 2.5mm; }
|
||||
.pstat { background: #eef3e6; border-radius: 2mm; padding: 1.4mm 2.4mm; display: flex; justify-content: space-between; align-items: baseline; font-weight: 600; font-size: 8pt; }
|
||||
.pstat { background: var(--pt); border-radius: 2mm; padding: 1.4mm 2.4mm; display: flex; justify-content: space-between; align-items: baseline; font-weight: 600; font-size: 8pt; }
|
||||
.pstat b { font-size: 13pt; }
|
||||
.psec h3 { margin: 0 0 1.6mm; font-size: 9pt; text-transform: uppercase; letter-spacing: .12em; color: #fff !important; background: #2f6a0d; padding: .8mm 2.4mm; border-radius: 1.4mm; display: flex; justify-content: space-between; align-items: center; gap: 3mm; }
|
||||
.psec h3 { margin: 0 0 1.6mm; font-size: 9pt; text-transform: uppercase; letter-spacing: .12em; color: #fff !important; background: var(--pbar); padding: .8mm 2.4mm; border-radius: 1.4mm; display: flex; justify-content: space-between; align-items: center; gap: 3mm; }
|
||||
.psec h3 * { color: #fff !important; }
|
||||
.psec h3 small { font-size: 6.6pt; font-weight: 500; letter-spacing: .04em; text-transform: none; }
|
||||
.pskills { column-count: 2; column-gap: 6mm; }
|
||||
.pgroup { break-inside: avoid; margin-bottom: 3mm; }
|
||||
.pgroup h4 { margin: 0 0 .6mm; font-size: 7.4pt; text-transform: uppercase; letter-spacing: .1em; color: #2f6a0d !important; border-bottom: 1px solid #2f6a0d; }
|
||||
.prow { display: grid; grid-template-columns: 6.5mm 1fr 11mm 6mm 8mm; align-items: center; gap: 1mm; padding: .35mm 0; border-bottom: 1px dotted #c3cab6; }
|
||||
.pgroup h4 { margin: 0 0 .6mm; font-size: 7.4pt; text-transform: uppercase; letter-spacing: .1em; color: var(--pg) !important; border-bottom: 1px solid var(--pg); }
|
||||
.prow { display: grid; grid-template-columns: 6.5mm 1fr 11mm 6mm 8mm; align-items: center; gap: 1mm; padding: .35mm 0; border-bottom: 1px dotted var(--pd); }
|
||||
.prow .pn { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
|
||||
.prow .pn em { font-size: 6pt; color: #6a7260 !important; font-style: normal; text-transform: uppercase; letter-spacing: .05em; margin-left: .8mm; }
|
||||
.prow .pa { font-size: 6.4pt; color: #6a7260 !important; text-align: center; }
|
||||
.prow .pp { font-size: 7pt; color: #4d5645 !important; text-align: right; }
|
||||
.prow .pt { text-align: center; border: 1px solid #1a1f14; border-radius: 1.4mm; font-size: 8.6pt; padding: .1mm 0; background: #fff; }
|
||||
.prow .pn em { font-size: 6pt; color: var(--pm) !important; font-style: normal; text-transform: uppercase; letter-spacing: .05em; margin-left: .8mm; }
|
||||
.prow .pa { font-size: 6.4pt; color: var(--pm) !important; text-align: center; }
|
||||
.prow .pp { font-size: 7pt; color: var(--p2) !important; text-align: right; }
|
||||
.prow .pt { text-align: center; border: 1px solid var(--pi); border-radius: 1.4mm; font-size: 8.6pt; padding: .1mm 0; background: var(--pb); }
|
||||
.prow.locked { opacity: .45; }
|
||||
.prow.locked .pn { text-decoration: line-through; }
|
||||
.ppips { display: inline-flex; gap: .7mm; }
|
||||
.ppips i { width: 2.1mm; height: 2.1mm; border: 1px solid #1a1f14; border-radius: 50%; background: #fff; display: inline-block; }
|
||||
.ppips i.on { background: #2f6a0d; border-color: #2f6a0d; }
|
||||
.ppips i { width: 2.1mm; height: 2.1mm; border: 1px solid var(--pi); border-radius: 50%; background: var(--pb); display: inline-block; }
|
||||
.ppips i.on { background: var(--pg); border-color: var(--pg); }
|
||||
.pcols { column-count: 2; column-gap: 6mm; }
|
||||
.pitem { break-inside: avoid; margin-bottom: 2.2mm; }
|
||||
.pitem b { font-size: 9pt; }
|
||||
.pitem p { font-size: 7.8pt; color: #3b4433 !important; }
|
||||
.ptag { font-size: 6.6pt; color: #6a7260 !important; text-transform: uppercase; letter-spacing: .05em; }
|
||||
.pnone { color: #6a7260 !important; }
|
||||
.pitem p { font-size: 7.8pt; color: var(--p3) !important; }
|
||||
.ptag { font-size: 6.6pt; color: var(--pm) !important; text-transform: uppercase; letter-spacing: .05em; }
|
||||
.pnone { color: var(--pm) !important; }
|
||||
.pgear { width: 100%; border-collapse: collapse; font-size: 8pt; }
|
||||
.pgear th { text-align: left; font-size: 6.4pt; text-transform: uppercase; letter-spacing: .08em; color: #6a7260 !important; border-bottom: 1px solid #1a1f14; padding: .6mm 1mm; }
|
||||
.pgear td { padding: .45mm 1mm; border-bottom: 1px dotted #c3cab6; vertical-align: top; }
|
||||
.pgear th { text-align: left; font-size: 6.4pt; text-transform: uppercase; letter-spacing: .08em; color: var(--pm) !important; border-bottom: 1px solid var(--pi); padding: .6mm 1mm; }
|
||||
.pgear td { padding: .45mm 1mm; border-bottom: 1px dotted var(--pd); vertical-align: top; }
|
||||
.pgear td:nth-child(n+4), .pgear th:nth-child(n+4) { text-align: right; white-space: nowrap; }
|
||||
.pnote { font-size: 6.6pt; color: #5c6553 !important; }
|
||||
.pnote { font-size: 6.6pt; color: var(--p4) !important; }
|
||||
.pother { margin-top: 1.6mm; font-size: 8pt; }
|
||||
.pbg { font-size: 8pt; white-space: pre-wrap; }
|
||||
.pnotes { flex: 1; display: flex; flex-direction: column; min-height: 18mm; }
|
||||
.plines { flex: 1; background-image: repeating-linear-gradient(to bottom, transparent 0, transparent 6.4mm, #b9c1ab 6.4mm, #b9c1ab calc(6.4mm + 1px)); border-radius: 1mm; }
|
||||
.pfoot { margin-top: auto; display: flex; justify-content: space-between; font-size: 6.4pt; color: #6a7260 !important; text-transform: uppercase; letter-spacing: .08em; border-top: 1px solid #cfd5c3; padding-top: 1.2mm; }
|
||||
.plines { flex: 1; background-image: repeating-linear-gradient(to bottom, transparent 0, transparent 6.4mm, var(--pn) 6.4mm, var(--pn) calc(6.4mm + 1px)); border-radius: 1mm; }
|
||||
.pfoot { margin-top: auto; display: flex; justify-content: space-between; font-size: 6.4pt; color: var(--pm) !important; text-transform: uppercase; letter-spacing: .08em; border-top: 1px solid var(--pl); padding-top: 1.2mm; }
|
||||
@media print { .paper { box-shadow: none; } }
|
||||
|
||||
/* lobby header: one slim line instead of three */
|
||||
.lobbyhead { display: flex; align-items: baseline; gap: .8rem; flex-wrap: wrap; margin: 0 0 .3rem; }
|
||||
.lobbyhead h1 { font-size: 1.3rem; margin: 0; }
|
||||
.lobbypage .tabs { margin: .4rem 0 .7rem; }
|
||||
.lobbypage .tabs button { padding: .3rem .8rem; }
|
||||
|
||||
Reference in New Issue
Block a user