iv: wire Shell into App.jsx, replace tab navigation with context dropdown

This commit is contained in:
otivm
2026-05-03 15:10:43 +00:00
parent a80368c8de
commit 6745e522a7

View File

@@ -2,18 +2,21 @@ import { useState, useEffect } from 'react'
import { generateToken, loadState, saveState } from './api.js' import { generateToken, loadState, saveState } from './api.js'
import { createState } from './gameState.js' import { createState } from './gameState.js'
import { BACKGROUNDS } from './constants.js' import { BACKGROUNDS } from './constants.js'
import Ledger from './screens/Ledger.jsx' import Shell from './components/Shell.jsx'
import Actor from './screens/Actor.jsx'
import Forum from './screens/Forum.jsx'
import Map from './screens/Map.jsx' import Map from './screens/Map.jsx'
import Prologue from './screens/Prologue.jsx'
import './App.css' import './App.css'
import contextsJson from './config/contexts.json'
const TOKEN_KEY = 'otivm_token' const TOKEN_KEY = 'otivm_token'
export default function App() { export default function App() {
const [state, setState] = useState(null) const [state, setState] = useState(null)
const [token, setToken] = useState(null) const [token, setToken] = useState(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [screen, setScreen] = useState('prologue') const [context, setContext] = useState('actor')
useEffect(() => { useEffect(() => {
async function bootstrap() { async function bootstrap() {
@@ -26,13 +29,12 @@ export default function App() {
const saved = await loadState(tok) const saved = await loadState(tok)
if (saved) { if (saved) {
setState(saved) setState(saved)
// If background already chosen, open on ledger setContext(saved.background_id && saved.background_id !== 'unknown' ? 'forum' : 'actor')
setScreen(saved.background_id ? 'ledger' : 'prologue')
} else { } else {
const fresh = createState(tok) const fresh = createState(tok)
setState(fresh) setState(fresh)
await saveState(tok, fresh) await saveState(tok, fresh)
setScreen('prologue') setContext('actor')
} }
setLoading(false) setLoading(false)
} }
@@ -44,8 +46,6 @@ export default function App() {
await saveState(token, newState) await saveState(token, newState)
} }
// Called from Prologue when player confirms a background.
// Seeds background_id into state, saves, switches to Ledger.
async function onSelectBackground(backgroundId) { async function onSelectBackground(backgroundId) {
const bg = BACKGROUNDS.find(b => b.id === backgroundId) const bg = BACKGROUNDS.find(b => b.id === backgroundId)
if (!bg) return if (!bg) return
@@ -56,15 +56,9 @@ export default function App() {
} }
setState(updated) setState(updated)
await saveState(token, updated) await saveState(token, updated)
setScreen('ledger') setContext('forum')
} }
// Session abandonment — forward-looking lifecycle handler.
// Does not delete the old save. Appends a terminal event so the record
// is complete. The old save becomes a historical artefact on disk.
// In the Simulator this event will have social and ecological consequences
// for the clan — a Constructor who stops participating leaves a gap.
// For now: mark abandoned, generate new token, bootstrap fresh in-place.
async function onNewGame() { async function onNewGame() {
if (state && token) { if (state && token) {
const abandoned = { const abandoned = {
@@ -82,53 +76,93 @@ export default function App() {
const fresh = createState(newTok) const fresh = createState(newTok)
setState(fresh) setState(fresh)
await saveState(newTok, fresh) await saveState(newTok, fresh)
setScreen('prologue') setContext('actor')
} }
if (loading) { if (loading) {
return ( return <div className="otivm-loading"><span>Consulting the ledger...</span></div>
<div className="loading">
<span>Consulting the ledger...</span>
</div>
)
} }
return ( // Active context config for Shell
<div> const activeCtx = contextsJson.find(c => c.id === context) || contextsJson[0]
<nav className="main-nav"> const subitems = activeCtx.subitems || []
<span className="nav-title">OTIVM</span>
<div className="nav-links"> // Context header text
<button const ctxHeader = (
className={`nav-btn${screen === 'prologue' ? ' active' : ''}`} <div className="otivm-ctx-header">
onClick={() => setScreen('prologue')} <div>
> <span className="otivm-ctx-eyebrow">Context</span>
Prologue <span className="otivm-ctx-name">{activeCtx.name} {activeCtx.subtitle}</span>
</button>
<button
className={`nav-btn${screen === 'ledger' ? ' active' : ''}`}
onClick={() => setScreen('ledger')}
>
Ledger
</button>
<button
className={`nav-btn${screen === 'map' ? ' active' : ''}`}
onClick={() => setScreen('map')}
>
Map
</button>
</div>
</nav>
<div className="screen-wrap">
<div style={{ display: screen === 'prologue' ? 'block' : 'none' }}>
<Prologue state={state} onSelectBackground={onSelectBackground} />
</div>
<div style={{ display: screen === 'ledger' ? 'block' : 'none' }}>
<Ledger state={state} onStateChange={onStateChange} onNewGame={onNewGame} />
</div>
<div style={{ display: screen === 'map' ? 'block' : 'none' }}>
<Map state={state} />
</div>
</div> </div>
</div> </div>
) )
// Screen content
let screen
if (context === 'actor') {
screen = (
<Actor
state={state}
onSelectBackground={onSelectBackground}
/>
)
} else if (context === 'forum') {
screen = (
<Forum
state={state}
onStateChange={onStateChange}
onNewGame={onNewGame}
/>
)
} else if (context === 'map') {
screen = <Map state={state} />
}
return (
<Shell
contexts={contextsJson}
activeId={context}
onContext={setContext}
subitems={subitems}
layout={activeCtx.layout}
token={token}
onNewGame={onNewGame}
>
{ctxHeader}
<div className="otivm-layout-wrap">
{context === 'actor' && (
<div className={activeCtx.layout === 'three-col' ? 'otivm-layout-three-col' : ''}>
{activeCtx.layout === 'three-col' ? (
<>
<div>{filterCol(screen, 'left')}</div>
<div>{filterCol(screen, 'center')}</div>
<div>{filterCol(screen, 'right')}</div>
</>
) : screen}
</div>
)}
{context === 'forum' && (
<div className={activeCtx.layout === 'two-col' ? 'otivm-layout-two-col' : ''}>
{activeCtx.layout === 'two-col' ? (
<>
<div>{filterCol(screen, 'left')}</div>
<div>{filterCol(screen, 'right')}</div>
</>
) : screen}
</div>
)}
{context === 'map' && screen}
</div>
</Shell>
)
}
// Extract children from a screen component by their col prop.
// Actor and Forum return arrays of Section elements with col props.
function filterCol(screen, col) {
if (!screen) return null
const children = screen.props?.children
if (!children) return null
const arr = Array.isArray(children) ? children : [children]
return arr.filter(c => c?.props?.col === col)
} }