Add responsive mobile layout for the Play screen and nav
The stylesheet had no real mobile design (one media query that only hid a nav hint), so on phones the Play screen's four side-by-side columns overflowed horizontally. Add a max-width:720px layer that collapses the Play layout to a single story column, turns the World/Script State rails into left-edge tabs that open as slide-in overlays, and makes the Plot/Memory/Scripts/Insights side-panel a full-screen overlay. Also add a hamburger dropdown for the top nav, switch the play-layout heights from vh to dvh (fixes the mobile address-bar jump), and bump inputs to 16px to stop iOS zoom-on-focus. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWVyFKvqJGjfbXdibLgkMe
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
141eec7c64
commit
8fc6d4d2e0
+39
-28
@@ -7,6 +7,7 @@ export default function App() {
|
||||
// null until /auth/me resolves; in local mode multi_user=false hides all auth UI.
|
||||
const [me, setMe] = useState(null)
|
||||
const [authMode, setAuthMode] = useState(null) // 'register' | 'login' | null
|
||||
const [navOpen, setNavOpen] = useState(false) // mobile hamburger menu
|
||||
|
||||
useEffect(() => {
|
||||
api.getMe().then(setMe).catch(() => {})
|
||||
@@ -31,34 +32,44 @@ export default function App() {
|
||||
<>
|
||||
<nav className="topnav">
|
||||
<span className="brand">⚔ AI D&D</span>
|
||||
<NavLink to="/" end className={({ isActive }) => `navlink${isActive ? ' active' : ''}`}>
|
||||
Adventures
|
||||
</NavLink>
|
||||
<NavLink to="/scenarios" className={({ isActive }) => `navlink${isActive ? ' active' : ''}`}>
|
||||
Scenarios
|
||||
</NavLink>
|
||||
<NavLink to="/scripts" className={({ isActive }) => `navlink${isActive ? ' active' : ''}`}>
|
||||
Scripts
|
||||
</NavLink>
|
||||
<NavLink to="/settings" className={({ isActive }) => `navlink${isActive ? ' active' : ''}`}>
|
||||
Settings
|
||||
</NavLink>
|
||||
{me?.multi_user && (
|
||||
<div className="nav-account">
|
||||
{me.is_guest ? (
|
||||
<>
|
||||
<span className="guest-nudge">Playing as guest — sign up to keep your adventures</span>
|
||||
<button onClick={() => setAuthMode('login')}>Log in</button>
|
||||
<button className="primary" onClick={() => setAuthMode('register')}>Sign up</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="account-email" title={me.email}>{me.email}</span>
|
||||
<button onClick={logout}>Log out</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="nav-hamburger"
|
||||
aria-label="Menu"
|
||||
aria-expanded={navOpen}
|
||||
onClick={() => setNavOpen((o) => !o)}
|
||||
>
|
||||
{navOpen ? '✕' : '☰'}
|
||||
</button>
|
||||
<div className={`nav-links${navOpen ? ' open' : ''}`} onClick={() => setNavOpen(false)}>
|
||||
<NavLink to="/" end className={({ isActive }) => `navlink${isActive ? ' active' : ''}`}>
|
||||
Adventures
|
||||
</NavLink>
|
||||
<NavLink to="/scenarios" className={({ isActive }) => `navlink${isActive ? ' active' : ''}`}>
|
||||
Scenarios
|
||||
</NavLink>
|
||||
<NavLink to="/scripts" className={({ isActive }) => `navlink${isActive ? ' active' : ''}`}>
|
||||
Scripts
|
||||
</NavLink>
|
||||
<NavLink to="/settings" className={({ isActive }) => `navlink${isActive ? ' active' : ''}`}>
|
||||
Settings
|
||||
</NavLink>
|
||||
{me?.multi_user && (
|
||||
<div className="nav-account">
|
||||
{me.is_guest ? (
|
||||
<>
|
||||
<span className="guest-nudge">Playing as guest — sign up to keep your adventures</span>
|
||||
<button onClick={() => setAuthMode('login')}>Log in</button>
|
||||
<button className="primary" onClick={() => setAuthMode('register')}>Sign up</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="account-email" title={me.email}>{me.email}</span>
|
||||
<button onClick={logout}>Log out</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
<Outlet context={{ me, setMe }} />
|
||||
{authMode && (
|
||||
|
||||
+127
-4
@@ -94,6 +94,21 @@ a:hover { color: var(--accent-bright); }
|
||||
}
|
||||
.topnav a.navlink:hover { color: var(--text); }
|
||||
|
||||
/* On desktop the wrapper is transparent — its children (nav links + account)
|
||||
participate directly in the .topnav flex row, exactly as before. The mobile
|
||||
query below turns it into a dropdown. */
|
||||
.nav-links { display: contents; }
|
||||
.nav-hamburger {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
font-size: 1.2rem;
|
||||
padding: 6px 8px;
|
||||
margin-left: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Phase 8: account area (hosted multi-user mode only) */
|
||||
.nav-account {
|
||||
margin-left: auto;
|
||||
@@ -286,7 +301,7 @@ button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: n
|
||||
|
||||
/* ---------- Play page ---------- */
|
||||
|
||||
.play-page { display: flex; flex-direction: column; min-height: calc(100vh - 56px); }
|
||||
.play-page { display: flex; flex-direction: column; min-height: calc(100dvh - 56px); }
|
||||
|
||||
/* Keep the Plot/Memory/Scripts/Insights toggles reachable while the story
|
||||
scrolls — pin the header just below the 56px top nav. */
|
||||
@@ -425,7 +440,7 @@ button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: n
|
||||
|
||||
/* ---------- Side panel (Plot / Insights) ---------- */
|
||||
|
||||
.play-layout { flex: 1; display: flex; min-height: calc(100vh - 56px); }
|
||||
.play-layout { flex: 1; display: flex; min-height: calc(100dvh - 56px); }
|
||||
.play-layout .play-page { max-width: 960px; }
|
||||
.play-layout.with-panel .play-page { max-width: none; }
|
||||
|
||||
@@ -443,7 +458,7 @@ button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: n
|
||||
background: var(--bg-panel);
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 56px);
|
||||
height: calc(100dvh - 56px);
|
||||
position: sticky;
|
||||
top: 56px;
|
||||
animation: panel-in 0.25s ease both;
|
||||
@@ -494,7 +509,7 @@ button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: n
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
top: 56px;
|
||||
height: calc(100vh - 56px);
|
||||
height: calc(100dvh - 56px);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
border-right: 1px solid var(--border);
|
||||
@@ -1131,3 +1146,111 @@ button:disabled { opacity: 0.45; cursor: default; transform: none; box-shadow: n
|
||||
from { opacity: 0; transform: translate(-50%, 12px); }
|
||||
to { opacity: 1; transform: translate(-50%, 0); }
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Mobile / narrow screens (≤ 720px)
|
||||
The desktop Play screen is a horizontal row of up to four columns
|
||||
(two left rails + story + right side-panel). Phones can't fit that,
|
||||
so here we collapse to a single story column and turn the rails and
|
||||
side-panel into full-height overlays. Everything above this query is
|
||||
the untouched desktop design.
|
||||
============================================================ */
|
||||
@media (max-width: 720px) {
|
||||
|
||||
/* ---------- Top nav → hamburger dropdown ---------- */
|
||||
.topnav { gap: 12px; padding: 0 16px; }
|
||||
.nav-hamburger { display: block; }
|
||||
.nav-links {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 56px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
background: var(--bg-panel);
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.55);
|
||||
padding: 4px 0;
|
||||
}
|
||||
.nav-links.open { display: flex; }
|
||||
.topnav a.navlink {
|
||||
padding: 13px 20px;
|
||||
border-bottom: none;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
.topnav a.navlink.active {
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: var(--accent);
|
||||
}
|
||||
.nav-account {
|
||||
margin-left: 0;
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid var(--border);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.account-email { max-width: 100%; }
|
||||
|
||||
/* ---------- Global page padding / typography ---------- */
|
||||
.page { padding: 18px 14px; }
|
||||
/* 16px inputs stop iOS Safari auto-zooming when a field is focused. */
|
||||
input[type="text"], input[type="password"], input[type="number"], textarea, select {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* ---------- Play screen → single column ---------- */
|
||||
.play-layout { flex-direction: column; }
|
||||
.play-layout .play-page,
|
||||
.play-layout.with-panel .play-page { max-width: 100%; }
|
||||
.play-page .page-header h1 { font-size: 1.25rem; }
|
||||
.panel-toggles { flex-wrap: wrap; }
|
||||
.story { font-size: 1.06rem; }
|
||||
/* Reserve a left gutter so the fixed World/State edge-tabs never sit on
|
||||
top of the story text as it scrolls past them. */
|
||||
.story, .play-page .empty { padding-left: 26px; }
|
||||
.story .action { padding-right: 0; }
|
||||
/* No hover on touch — keep per-action edit/delete tools visible. */
|
||||
.story .action-tools { opacity: 1; pointer-events: auto; }
|
||||
|
||||
/* ---------- Left rails (World State / Script State) → edge tabs + overlay ---------- */
|
||||
/* Closed: a compact tab pinned to the left edge instead of a full-height
|
||||
column that steals the story's width. World State sits above Script State.
|
||||
(World State only renders for RPG adventures, so it may be absent.) */
|
||||
.status-drawer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
z-index: 40;
|
||||
}
|
||||
.status-drawer:not(.open) {
|
||||
top: 170px;
|
||||
height: auto;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-left: none;
|
||||
border-radius: 0 10px 10px 0;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.ws-drawer:not(.open) { top: 170px; }
|
||||
.status-drawer:not(.ws-drawer):not(.open) { top: 260px; }
|
||||
.status-drawer:not(.open) .status-toggle { height: auto; padding: 14px 4px; }
|
||||
|
||||
/* Open: slide-in overlay covering most of the screen. */
|
||||
.status-drawer.open {
|
||||
top: 56px;
|
||||
height: calc(100dvh - 56px);
|
||||
width: min(88vw, 320px);
|
||||
box-shadow: 0 0 50px rgba(0, 0, 0, 0.65);
|
||||
border-right: 1px solid var(--border-bright);
|
||||
}
|
||||
|
||||
/* ---------- Right side-panel (Plot/Memory/Scripts/Insights) → full overlay ---------- */
|
||||
.side-panel {
|
||||
position: fixed;
|
||||
inset: 56px 0 0 0;
|
||||
width: auto;
|
||||
height: auto;
|
||||
z-index: 45;
|
||||
border-left: none;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user