From ebd91ca42a92842691c8b289fe1983e3d78dfdde Mon Sep 17 00:00:00 2001 From: david raistrick <1108844+keen99@users.noreply.github.com> Date: Wed, 8 Jul 2026 14:05:55 -0400 Subject: [PATCH] Inline editable fields (init/hp/maxhp/ac) with editing overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Participant card fields now editable in place (D-style): transparent bg, underline on hover/focus, no spinners. Covers Initiative, Current HP, Max HP, and AC badge value. Click any value to edit. Blur/Enter saves. Escape cancels. Status recomputes on HP change (conscious/dying/dead/down per ruleset). Editing overlay: when a field is focused, amber ring highlights the card and a centered pointer-events-none label shows '✎ Editing {field}'. Label floats over card middle without blocking input — taps pass through to field. Handlers: handleInlineCurrentHp (recomputes status), handleInlineMaxHp, handleInlineAc. Keys prefixed to avoid React key collisions when values match. Tests: selectors updated to use element id (monsterMaxHp) since inline aria-labels now match form label queries. --- TODO.md | 3 +- src/App.js | 112 +++++++++++++++++++++++++++-- src/tests/InitiativeReslot.test.js | 2 +- src/tests/ReslotAllPaths.test.js | 2 +- src/tests/testHelpers.js | 2 +- 5 files changed, 110 insertions(+), 11 deletions(-) diff --git a/TODO.md b/TODO.md index ee69e02..5fa5bae 100644 --- a/TODO.md +++ b/TODO.md @@ -11,7 +11,7 @@ also better vert tab layout - labelt friendly x needs AC for players dude -and quick entry hp +x and quick entry hp hp do not carry from encounter to ecnounter!!! ^feature to add back to campaign character after encounter? @@ -20,6 +20,7 @@ maybe campaign toggle in charc section (choosing each end is DM overload will be hp wont go over max and no temp hp support +refresh/reload/code update causes UI to update to top unselected cambpaign have to drill all the way back down to active encounter....every time. siemtmes? diff --git a/src/App.js b/src/App.js index 844689e..576fe10 100644 --- a/src/App.js +++ b/src/App.js @@ -1085,6 +1085,17 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp ]; const participants = encounter.participants || []; + const [editingField, setEditingField] = useState(null); + const [editingId, setEditingId] = useState(null); + const blurTimeoutRef = useRef(null); + const handleFieldFocus = (id, label) => { + if (blurTimeoutRef.current) { clearTimeout(blurTimeoutRef.current); blurTimeoutRef.current = null; } + setEditingId(id); + setEditingField(label); + }; + const handleFieldBlur = () => { + blurTimeoutRef.current = setTimeout(() => { setEditingField(null); setEditingId(null); }, 150); + }; useEffect(() => { if (participantType === 'character' && selectedCharacterId) { @@ -1283,6 +1294,53 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp } }; + const handleInlineCurrentHp = async (participantId, value) => { + let n = parseInt(value, 10); + if (isNaN(n)) return; + const p = participants.find(pp => pp.id === participantId); + if (!p || n === p.currentHp) return; + try { + const isGeneric = (encounter.ruleset === 'generic'); + let patch; + if (!isGeneric && n < 0) n = 0; + if (isGeneric) { + patch = { currentHp: n, status: n > 0 ? 'conscious' : 'down' }; + } else { + patch = { currentHp: n, deathSaveSuccesses: 0, deathSaveFailures: 0 }; + if (n > 0) { patch.status = 'conscious'; } + else if (p.type === 'monster') { patch.status = 'dead'; patch.isActive = false; } + else { patch.status = 'dying'; } + } + await updateParticipant(encounter, participantId, patch, buildCtx(encounterPath)); + } catch (err) { + showToast('Failed to update HP.'); + } + }; + + const handleInlineMaxHp = async (participantId, value) => { + const n = parseInt(value, 10); + if (isNaN(n)) return; + const p = participants.find(pp => pp.id === participantId); + if (!p || n === p.maxHp || n <= 0) return; + try { + await updateParticipant(encounter, participantId, { maxHp: n }, buildCtx(encounterPath)); + } catch (err) { + showToast('Failed to update Max HP.'); + } + }; + + const handleInlineAc = async (participantId, value) => { + const n = parseInt(value, 10); + if (isNaN(n)) return; + const p = participants.find(pp => pp.id === participantId); + if (!p || n === p.ac) return; + try { + await updateParticipant(encounter, participantId, { ac: n }, buildCtx(encounterPath)); + } catch (err) { + showToast('Failed to update AC.'); + } + }; + const handleDeathSaveChange = async (participantId, outcome) => { if (!db) return; try { @@ -1602,6 +1660,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp {participants.length === 0 &&

No participants added yet.

} +