// Section.jsx — OTIVM-IV // Generic panel renderer. Receives a section definition and renders // the appropriate sub-component based on the "type" field. // Does not know what it is showing — delegates entirely to type. // // Section definition shape (from context-{id}.json): // title — string, shown in panel header // type — one of the section types below // col — layout column assignment (read by Shell.jsx LayoutGrid) // [data] — type-specific data array or object // // Section types: // status-block — key/value pairs // parameter-list — actor_parameters rows // auctoritas — three-face auctoritas panel // cost-table — cost items with amounts and sources // drift-log — parameter_drift_log entries // action-bar — action buttons // route-list — trade route cards with cost breakdown // text-block — narrative text, optionally collapsible // map-canvas — TESSERA fog-of-war map placeholder // // The "col" prop is consumed by Shell.jsx LayoutGrid for column placement. // Section.jsx itself ignores it. import { useState } from 'react' export default function Section({ title, type, col, ...props }) { return (
{title}
) } function SectionBody({ type, ...props }) { switch (type) { case 'status-block': return case 'parameter-list': return case 'auctoritas': return case 'cost-table': return case 'drift-log': return case 'action-bar': return case 'route-list': return case 'text-block': return case 'map-canvas': return default: return
Unknown section type: {type}
} } // ── Status block ────────────────────────────────────────────────────────── // props.items: [{ key, value, cls }] // cls: 'good' | 'warn' | 'bad' | '' function StatusBlock({ items = [] }) { return (
{items.map((item, i) => (
{item.key} {item.value}
))}
) } // ── Parameter list ──────────────────────────────────────────────────────── // props.items: [{ token, name, true_val, perceived, conf }] // Shows a gap indicator when true_val !== perceived. function ParameterList({ items = [] }) { return (
{items.map((item, i) => { const gap = item.true_val !== item.perceived return (
{item.token}
{item.name}
{item.perceived} {gap && (
true: {item.true_val}
)} {item.conf}
) })}
) } // ── Auctoritas ──────────────────────────────────────────────────────────── // props.faces: [{ label, value, discrepant }] // props.gap_note: string shown when discrepancy exists function Auctoritas({ faces = [], gap_note }) { const hasGap = faces.some(f => f.discrepant) return (
{faces.map((face, i) => (
{face.label}
{face.value}
))}
{hasGap && gap_note && (
{gap_note}
)}
) } // ── Cost table ──────────────────────────────────────────────────────────── // props.note: optional note shown above table // props.items: [{ label, amount, period, source, conf, debit }] function CostTable({ note, items = [] }) { return (
{note && (
{note}
)} {items.map((item, i) => ( ))}
Item Amount Source · Conf.
{item.label} {item.period &&
{item.period}
}
{item.debit ? '−' : '+'}{item.amount} {item.source} · {item.conf}
) } // ── Drift log ───────────────────────────────────────────────────────────── // props.items: [{ param, delta, trigger, note, positive }] function DriftLog({ items = [] }) { return (
{items.map((item, i) => (
{item.param} {item.delta}
{item.trigger}
{item.note &&
{item.note}
}
))}
) } // ── Action bar ──────────────────────────────────────────────────────────── // props.buttons: [{ label, style, action, disabled }] // props.onAction: fn(action) called when a button is clicked function ActionBar({ buttons = [], onAction }) { return (
{buttons.map((btn, i) => ( ))}
) } // ── Route list ──────────────────────────────────────────────────────────── // props.routes: [{ id, name, goods, mode, days, cost, profit, // vectura, portoria, other, locked, lock_reason }] // props.selectedRoute: id of currently selected route // props.onSelect: fn(id) called when a route is clicked function RouteList({ routes = [], selectedRoute, onSelect }) { return (
{routes.map(route => { if (route.locked) { return (
{route.name} locked
{route.goods} · {route.mode}
{route.lock_reason && (
Requires {route.lock_reason}
)}
) } const net = route.profit - route.cost const isSelected = selectedRoute === route.id return (
onSelect && onSelect(route.id)} >
{route.name} +{net} dn net
{route.goods} · {route.mode} · {route.days} sim. days
Vectura
{route.vectura} dn
Portoria
{route.portoria} dn
Other
{route.other} dn
Revenue
{route.profit} dn
) })}
) } // ── Text block ──────────────────────────────────────────────────────────── // props.entries: [{ date, text }] // props.collapsible: boolean function TextBlock({ entries = [], collapsible = false }) { const [open, setOpen] = useState(!collapsible) return (
{collapsible && ( )} {open && entries.map((entry, i) => (
{entry.date}
{entry.text}
))}
) } // ── Map canvas ──────────────────────────────────────────────────────────── // Placeholder — the real Map.jsx component is rendered by the MAP context screen. // This type exists so Section.jsx can reference it; in production the MAP // context screen passes the real Map.jsx output as a child instead. function MapCanvas() { return (
TESSERA H7 · roman_14bce Map renders here — src/screens/Map.jsx
) } // ── Helpers ─────────────────────────────────────────────────────────────── function bandClass(val) { if (!val) return 'otivm-band-low' const v = val.toLowerCase() if (v === 'high' || v === 'distinguished') return 'otivm-band-high' if (v === 'medium' || v === 'neutral') return 'otivm-band-medium' return 'otivm-band-low' }