From 91e23856b4daa7a215d90f48b9ba410af2de0933 Mon Sep 17 00:00:00 2001 From: david raistrick <1108844+keen99@users.noreply.github.com> Date: Wed, 8 Jul 2026 19:28:05 -0400 Subject: [PATCH] feat: tablet-friendly encounter layout - Combat controls always left column (narrow portrait, wide desktop) - Short labels below lg breakpoint, full labels at lg+ - Sticky controls all viewports - Collapsible Add Participants section (auto-collapse during combat) - Fullscreen encounter toggle - Modals via createPortal to escape stacking context - Reduced padding/margins in portrait orientation --- src/App.js | 164 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 98 insertions(+), 66 deletions(-) diff --git a/src/App.js b/src/App.js index 9fbb1fc..34f0457 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,5 @@ import React, { useState, useEffect, useRef, useMemo, createContext, useContext, useCallback } from 'react'; +import { createPortal } from 'react-dom'; import * as shared from '@ttrpg/shared'; import { initializeApp, getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken, getFirestore, where, orderBy, limit, offset, getStorage, getStorageMode } from './storage'; import { isDevToolsEnabled } from './config/devTools'; @@ -35,7 +36,7 @@ function ToastStack({ toasts, onDismiss }) { function InfoModal({ message, onClose }) { if (!message) return null; return ( -
+
@@ -346,8 +347,8 @@ function Modal({ onClose, title, children }) { return () => window.removeEventListener('keydown', handleEsc); }, [onClose]); - return ( -
+ return createPortal( +

{title}

@@ -357,7 +358,8 @@ function Modal({ onClose, title, children }) {
{children}
-
+
, + document.body ); } @@ -391,8 +393,8 @@ function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) { onClose(); }; - return ( -
+ return createPortal( +
@@ -419,7 +421,8 @@ function ConfirmationModal({ isOpen, onClose, onConfirm, title, message }) {
-
+
, + document.body ); } @@ -846,7 +849,7 @@ function CharacterManager({ campaignId, campaignCharacters, syncCharacters }) { return ( <> -
+
-
+ {(() => { + const open = addSectionOpen && !combatActive; + return ( +
+ { if (!combatActive) { e.preventDefault(); setAddSectionOpen(v => !v); } else { e.preventDefault(); } }} + className="cursor-pointer text-lg font-medium text-amber-200 font-cinzel tracking-wide flex items-center gap-2 select-none" + > + {open ? : } + Add Participants + {combatActive && (pause to add)} + + {open && ( + <> +
+ +
{/* Warning when combat is active */} {encounter.isStarted && !encounter.isPaused && ( @@ -1678,15 +1696,11 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
- - {lastRollDetails && ( -

- {lastRollDetails.manual - ? `${lastRollDetails.name} (${lastRollDetails.type === 'character' ? 'Character' : lastRollDetails.type === 'monster' ? 'Monster' : lastRollDetails.type === 'npc' ? 'NPC' : lastRollDetails.type}): Set initiative ${lastRollDetails.total}` - : `${lastRollDetails.name} (${lastRollDetails.type === 'character' ? 'Character' : lastRollDetails.type === 'monster' ? 'Monster' : lastRollDetails.type === 'npc' ? 'NPC' : lastRollDetails.type}): Rolled d20 (${lastRollDetails.roll}) ${formatInitMod(lastRollDetails.mod)} = ${lastRollDetails.total} Initiative` - } -

- )} + + )} + + ); + })()} {participants.length === 0 &&

No participants added yet.

} @@ -2017,6 +2031,16 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp })} + {lastRollDetails && ( +

+ {lastRollDetails.manual + ? `${lastRollDetails.name} (${lastRollDetails.type === 'character' ? 'Character' : lastRollDetails.type === 'monster' ? 'Monster' : lastRollDetails.type === 'npc' ? 'NPC' : lastRollDetails.type}): Set initiative ${lastRollDetails.total}` + : `${lastRollDetails.name} (${lastRollDetails.type === 'character' ? 'Character' : lastRollDetails.type === 'monster' ? 'Monster' : lastRollDetails.type === 'npc' ? 'NPC' : lastRollDetails.type}): Rolled d20 (${lastRollDetails.roll}) ${formatInitMod(lastRollDetails.mod)} = ${lastRollDetails.total} Initiative` + } +

+ )} + + {editingParticipant && ( -
-

Combat Controls

-
+
+

Combat Controls

+
{!encounter.isStarted ? ( ) : ( <> {/* Round Counter */} -
-

Round: {encounter.round}

+
+

Round: {encounter.round}

{encounter.isPaused && ( -

(Paused)

+

(Paused)

)}
)}
- {/* Undo / Redo — queried on click (no live logs subscription during combat) */} -
+ {/* Undo / Redo */} +
{/* Display Settings */} -
+
Player Display
@@ -2317,6 +2341,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac catch { return null; } }); const [showCreateModal, setShowCreateModal] = useState(false); + const [encounterFullscreen, setEncounterFullscreen] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [itemToDelete, setItemToDelete] = useState(null); const [draggedEncounterId, setDraggedEncounterId] = useState(null); @@ -2504,7 +2529,7 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac return ( <> -
+

Encounters @@ -2596,13 +2621,20 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac )} {selectedEncounter && ( -
-

- Managing Encounter: {selectedEncounter.name} +
+

+ Managing Encounter: {selectedEncounter.name} +

-
- {/* Combat Controls - Left Side (Sticky on large screens) */} -
+
+ {/* Combat Controls - always left */} +
-
+