From 968d168ff1ae5a39b5a7a2157dfb5a8ff4922657 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 17:24:48 +0000 Subject: [PATCH] Sheet: light/dark switch, slimmer lobby header, remove wide view Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HJhXuLD2Z6hv3Wqeq9UL9K --- README.md | 2 +- frontend/src/Lobbies.jsx | 30 ++++++----------- frontend/src/PaperSheet.jsx | 17 +++++++--- frontend/src/SheetView.jsx | 57 ------------------------------- frontend/src/styles.css | 67 +++++++++++++++++++++---------------- 5 files changed, 64 insertions(+), 109 deletions(-) delete mode 100644 frontend/src/SheetView.jsx diff --git a/README.md b/README.md index 7fae45b..1c69659 100644 --- a/README.md +++ b/README.md @@ -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 / save as 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). In a lobby, **A4 sheet / Wide** switches between the compact page and the full-width table view (your choice is remembered). 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. **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. ### Lobbies diff --git a/frontend/src/Lobbies.jsx b/frontend/src/Lobbies.jsx index 3aa7eb6..0ec7bd8 100644 --- a/frontend/src/Lobbies.jsx +++ b/frontend/src/Lobbies.jsx @@ -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 = ( - - - - - ) return (
-
-

{data?.name || name || 'Nameless'}

- -
-

{owner ? `Played by ${owner}. ` : ''}Read-only, hover or tap a value to see how it is made up.

{err &&

{err}

} - {data && view === 'paper' && } - {data && view === 'wide' && <>
{viewSwitch}
} + {data && ( + + {owner ? `Played by ${owner}` : 'Your character'} ยท read-only, hover or tap a value for details + + + } /> + )}
) } @@ -392,10 +384,10 @@ function LobbyView({ id, rules, user, onOpen }) { return (
-

โ† All lobbies

-
+
+ โ† Lobbies

{lobby.name}

- DM: {lobby.dm}{lobby.is_dm ? ' (you)' : ''} + DM: {lobby.dm}{lobby.is_dm ? ' (you)' : ''}
{err &&

{err}

} {info &&

{info}

} diff --git a/frontend/src/PaperSheet.jsx b/frontend/src/PaperSheet.jsx index d6c95fc..1d80188 100644 --- a/frontend/src/PaperSheet.jsx +++ b/frontend/src/PaperSheet.jsx @@ -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(`${(title || 'Character').replace(/[<>&]/g, '')}${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
{children}
+ return
{children}
} 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 }) {
{tools} - + + + + + + +
- +
{name}
diff --git a/frontend/src/SheetView.jsx b/frontend/src/SheetView.jsx deleted file mode 100644 index 0e0444d..0000000 --- a/frontend/src/SheetView.jsx +++ /dev/null @@ -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

โ€“

- return ( - - {rows.map((r) => ( - - - - {r.extra !== undefined && } - - ))} -
{r.label}{r.desc &&
{r.desc}
}
{valueLabel ? valueLabel(r) : r.val}{r.extra}
- ) -} - -// 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 ( -
- h.k !== 'name')} /> -
-
-

Attributes

- -

Derived

- -
-
-

Skills

- - {rest.length > 0 && ( -
- All other skills ({rest.length}) - -
- )} -
-
-

Talents

- -

Mutations / implants {c.capacityUsed}/{c.capacityMax}

- -

Equipment {c.weight}/{c.carryMax} kg

- (r.val > 1 ? `x${r.val}` : '')} /> -
- ) -} diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 18e29f5..c998d54 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -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; }