Character portrait

Crop-and-zoom picture editor on the Concept page, stored (re-encoded to a
256 px WebP data URL) inside the character; shown on the sheet header, in
the lobby player bar and as token icon on the map.

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-23 19:33:28 +00:00
parent 03ebc3a138
commit d4d7b4d22e
11 changed files with 181 additions and 9 deletions
+2 -2
View File
@@ -432,7 +432,7 @@ function CharBar({ lobby, sel, onSel }) {
return (
<div key={m.username} role="listitem" className={`pcard${sel === m.username ? ' sel' : ''}${can ? ' can' : ''}`} style={{ '--pc': m.color || 'var(--line)' }}
onClick={() => can && onSel(m.username)} tabIndex={can ? 0 : undefined} onKeyDown={(e) => can && e.key === 'Enter' && onSel(m.username)}>
<span className="pavatar">{(c?.name || m.username).slice(0, 1).toUpperCase()}</span>
<span className="pavatar">{c?.portrait ? <img src={`/api/lobbies/${lobby.id}/portrait/${encodeURIComponent(m.username)}?v=${c.portrait}`} alt="" /> : (c?.name || m.username).slice(0, 1).toUpperCase()}</span>
<span className="pinfo">
<b>{c ? c.name : 'No character'}</b>
<span className="muted small">{m.username}{m.me ? ' (you)' : ''}{c?.class ? ` · ${c.class}` : ''}</span>
@@ -762,7 +762,7 @@ function LobbyView({ id, rules, user }) {
const selTok = lobby.is_dm ? mapState.tokens.find((t) => t.id === mapSel && t.kind === 'npc') : null
const walls = full && showWalls ? mapState.walls || [] : []
return (
<MapView map={mapState.map} image={mapImg} tokens={mapState.tokens} grid={showGrid}
<MapView map={mapState.map} image={mapImg} portraitUrl={(t) => t.portrait ? `/api/lobbies/${id}/portrait/${encodeURIComponent(t.username)}?v=${t.portrait}` : null} tokens={mapState.tokens} grid={showGrid}
onMove={moveToken} selected={mapSel} onSelect={setMapSel}
overlay={walls.length ? <WallLayer map={mapState.map} walls={walls} onDoor={(w) => act(`/api/lobbies/${id}/doors/${w.id}`, 'PUT', { open: !w.open }, () => loadMap())} /> : null}>
{mapState.vision?.empty && <div className="mapempty">{lobby.is_dm ? 'This player is not on the map yet. Switch to the full view and drag their token onto it.' : 'Waiting for the DM to put your token on the map.'}</div>}