Lobby: maps, map editor and tokens

DM map library (upload re-encoded with Pillow), map editor with prepared
enemies/NPCs, map on the lobby table with personal pan/zoom, draggable
tokens (players move their own, DM all), full/player view, live sync via
the WebSocket.

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 13:59:21 +00:00
parent 162d8c9224
commit e8755582d5
8 changed files with 679 additions and 9 deletions
+45
View File
@@ -396,3 +396,48 @@ body:has(.lobbychat) main { padding: .5rem calc(var(--chatw) + .5rem) 0 .5rem; }
.editor .tabs { margin-top: .6rem; }
.settings.inline { position: static; max-width: none; margin: 0; border: 0; background: transparent; padding: 0; }
/* ---- maps */
.table { background: color-mix(in srgb, var(--panel) 70%, #000); }
.mapbox { position: absolute; inset: 0; overflow: hidden; touch-action: none; cursor: grab; background: #111; z-index: 1; }
.mapbox:active { cursor: grabbing; }
.mapworld { position: absolute; left: 0; top: 0; transform-origin: 0 0; will-change: transform; }
.mapworld img { display: block; max-width: none; user-select: none; -webkit-user-drag: none; }
.mapgrid { position: absolute; inset: 0; pointer-events: none; background-image: linear-gradient(to right, rgba(255,255,255,.28) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,255,255,.28) 1px, transparent 1px); }
.maptoken { position: absolute; border-radius: 50%; background: var(--tc); border: 3px solid #fff; box-shadow: 0 0 0 2px rgba(0,0,0,.6), 0 2px 6px rgba(0,0,0,.6); display: grid; place-items: center; color: #fff; font-weight: 700; text-transform: uppercase; user-select: none; touch-action: none; cursor: default; }
.maptoken.mov { cursor: move; }
.maptoken.player { border-color: var(--tc); background: color-mix(in srgb, var(--tc) 55%, #000); box-shadow: 0 0 0 2px #fff, 0 2px 6px rgba(0,0,0,.6); }
.maptoken.hid { opacity: .55; border-style: dashed; }
.maptoken.sel { outline: 3px solid var(--accent, gold); outline-offset: 3px; }
.maptoken span { pointer-events: none; text-shadow: 0 1px 2px #000; }
.maptoken em { position: absolute; top: 100%; left: 50%; transform: translateX(-50%); margin-top: 2px; font-style: normal; font-weight: 600; text-transform: none; white-space: nowrap; color: #fff; text-shadow: 0 0 3px #000, 0 0 3px #000; pointer-events: none; }
.maptray { position: absolute; top: .5rem; left: .5rem; display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; padding: .3rem .5rem; border-radius: 8px; background: color-mix(in srgb, var(--panel) 90%, transparent); border: 1px solid var(--line); z-index: 4; max-width: 70%; }
.traytoken { border-radius: 999px; border: 2px solid var(--tc); background: color-mix(in srgb, var(--tc) 40%, var(--panel)); color: var(--text); padding: .15rem .7rem; cursor: grab; touch-action: none; }
.mapzoom { position: absolute; right: .5rem; bottom: .5rem; display: grid; gap: .25rem; z-index: 4; }
.mapzoom button, .maptools button { background: color-mix(in srgb, var(--panel) 90%, transparent); }
.maptools { position: absolute; top: .5rem; right: .5rem; display: flex; gap: .3rem; z-index: 4; }
.tokenpanel { position: absolute; right: 3rem; bottom: .5rem; z-index: 5; width: 17rem; max-width: 60%; padding: .6rem; border-radius: 10px; background: var(--panel); border: 1px solid var(--line); cursor: default; }
.tokenform { display: grid; gap: .45rem; }
.tokenform input[type=number] { width: 4.5rem; }
.swatches { display: flex; gap: .3rem; flex-wrap: wrap; }
.swatch { width: 1.4rem; height: 1.4rem; padding: 0; border-radius: 50%; border: 2px solid transparent; }
.swatch.on { border-color: var(--text); }
.maplib { display: grid; gap: .5rem; margin-bottom: 1rem; }
.uploadbtn { display: inline-block; padding: .4rem .8rem; border: 1px dashed var(--line); border-radius: 8px; cursor: pointer; text-align: center; }
.maplist { list-style: none; padding: 0; margin: 0; display: grid; gap: .4rem; }
.maplist li { padding: .5rem; border: 1px solid var(--line); border-radius: 8px; display: grid; gap: .3rem; }
.maplist li.active { border-color: var(--accent, gold); }
.mapeditor { position: fixed; inset: 0; z-index: 100; display: grid; grid-template-columns: 20rem 1fr; background: var(--bg); }
.mapeditor aside { overflow-y: auto; border-radius: 0; display: grid; gap: .6rem; align-content: start; }
.mapstage { position: relative; }
.tokenlist { list-style: none; padding: 0; margin: 0; display: grid; gap: .25rem; }
.tokenlist button { width: 100%; text-align: left; }
.tokenlist i { display: inline-block; width: .8rem; height: .8rem; border-radius: 50%; vertical-align: middle; }
@media (max-width: 800px) { .mapeditor { grid-template-columns: 1fr; grid-template-rows: 45% 1fr; } }
.mapeditor, .mapeditor * { box-sizing: border-box; }
.mapeditor { z-index: 1000; }
.mapeditor .mapstage { min-width: 0; }
label.chk { display: flex; align-items: center; gap: .45rem; }
label.chk input[type=checkbox] { width: auto; margin: 0; }
.mapeditor label.small:not(.chk) { display: grid; gap: .2rem; }
.editactions { position: sticky; bottom: 0; background: var(--panel); padding: .5rem 0; }