Phase 1: PWA auto-update, player-view map fit fix, encounter picker
- PWA registerType autoUpdate + skipWaiting/clientsClaim/cleanupOutdatedCaches so
a fresh deploy reaches browsers without a manual hard-reload (the old 'prompt'
served the stale cached bundle, hiding shipped fixes like the polygon outline).
- MapCanvas: always mount the outer container so the ResizeObserver binds even when
the map image arrives late over WebSocket — fixes the networked player view
showing the map at 100% instead of fit ("3x magnified").
- TokenPalette: encounter picker for planned AND active encounters (was active-only),
with per-encounter "+ All" and counts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import type { Character, Combatant, Encounter, MapToken } from '@/lib/schemas';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Select } from '@/components/ui/Input';
|
||||
import { KIND_COLOR, baseSpec, unplacedPcs, type TokenSpec } from './tokens';
|
||||
|
||||
export type { TokenSpec };
|
||||
@@ -14,7 +16,12 @@ export function TokenPalette({ characters, encounters, existingTokens, onPlace,
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const pcs = characters.filter((c) => c.kind === 'pc');
|
||||
const activeEnc = encounters.find((e) => e.status === 'active') ?? encounters.find((e) => e.combatants.length > 0) ?? null;
|
||||
// Any encounter that has combatants — planned OR active — can supply tokens.
|
||||
const withCombatants = encounters.filter((e) => e.combatants.length > 0);
|
||||
const [encId, setEncId] = useState('');
|
||||
const selectedEnc = withCombatants.find((e) => e.id === encId)
|
||||
?? encounters.find((e) => e.status === 'active' && e.combatants.length > 0)
|
||||
?? withCombatants[0] ?? null;
|
||||
|
||||
const hasPc = (id: string) => existingTokens.some((t) => t.characterId === id);
|
||||
const hasCombatant = (cb: Combatant) => existingTokens.some((t) => t.combatantId === cb.id || (!t.combatantId && !t.characterId && t.label === cb.name));
|
||||
@@ -58,11 +65,28 @@ export function TokenPalette({ characters, encounters, existingTokens, onPlace,
|
||||
)}
|
||||
</section>
|
||||
|
||||
{activeEnc && activeEnc.combatants.length > 0 && (
|
||||
{selectedEnc && (
|
||||
<section>
|
||||
<div className="mb-1 truncate text-xs font-medium text-ink" title={activeEnc.name}>{activeEnc.name}</div>
|
||||
<div className="mb-1 flex items-center gap-1">
|
||||
<span className="text-xs font-medium text-ink">Encounter</span>
|
||||
<Button size="sm" variant="ghost" className="ml-auto h-6 px-1.5"
|
||||
disabled={selectedEnc.combatants.every((cb) => hasCombatant(cb))}
|
||||
onClick={() => onPlaceMany(selectedEnc.combatants.filter((cb) => !hasCombatant(cb)).map((cb, i) => baseSpec({
|
||||
label: cb.name, color: KIND_COLOR[cb.kind], kind: cb.kind, hp: cb.hp, combatantId: cb.id, col: i, row: 1,
|
||||
...(cb.characterId ? { characterId: cb.characterId } : {}),
|
||||
})))}>+ All</Button>
|
||||
</div>
|
||||
{withCombatants.length > 1 ? (
|
||||
<Select className="mb-1 h-7 w-full text-xs" value={selectedEnc.id} onChange={(e) => setEncId(e.target.value)} aria-label="Encounter">
|
||||
{withCombatants.map((e) => (
|
||||
<option key={e.id} value={e.id}>{e.status === 'active' ? '● ' : ''}{e.name} ({e.combatants.length})</option>
|
||||
))}
|
||||
</Select>
|
||||
) : (
|
||||
<div className="mb-1 truncate text-[11px] text-muted" title={selectedEnc.name}>{selectedEnc.status === 'active' ? '● ' : ''}{selectedEnc.name}</div>
|
||||
)}
|
||||
<ul className="space-y-1">
|
||||
{activeEnc.combatants.map((cb) => {
|
||||
{selectedEnc.combatants.map((cb) => {
|
||||
const placed = hasCombatant(cb);
|
||||
return (
|
||||
<li key={cb.id}>
|
||||
|
||||
Reference in New Issue
Block a user