fix(ui): scale WORLD MAP to fill its tab pane without an internal scrollbar (#36)
CI / build + unit + e2e (pull_request) Failing after 13m30s
CI / build + unit + e2e (pull_request) Failing after 13m30s
This commit is contained in:
+30
-13
@@ -11,10 +11,12 @@ import type { WorldState } from '@/lib/game/save'
|
||||
// pan + zoom (d3-zoom), per #28. Clicking a system adjacent to the current one jumps there
|
||||
// (same POST /api/jump flow as before); the current and unreachable systems are dimmed and not
|
||||
// clickable, and every node is locked while a jump is in flight. Turn-based: fetch on mount,
|
||||
// refetch via the jump response — no polling.
|
||||
// refetch via the jump response — no polling. In wide mode (#36) the chart fills the whole tab
|
||||
// pane (no internal scrollbar); narrow keeps a fixed fallback height since the page scrolls naturally there.
|
||||
|
||||
interface WorldMapPanelProps {
|
||||
height?: number
|
||||
height?: number // fallback chart height when the pane gives no definite space (narrow mode)
|
||||
narrow?: boolean
|
||||
onJumped?: () => void // #30 — let the parent refresh /api/system after a successful jump
|
||||
}
|
||||
|
||||
@@ -44,7 +46,9 @@ const STATUS_COLOR: Record<NodeStatus, string> = {
|
||||
offline: 'var(--border)',
|
||||
}
|
||||
|
||||
export function WorldMapPanel({ height = 380, onJumped }: WorldMapPanelProps) {
|
||||
export function WorldMapPanel({ height = 380, narrow = false, onJumped }: WorldMapPanelProps) {
|
||||
// Wide mode: the flex chain (Tabs → TabsContent → this panel) provides a definite pane size to fill (#36).
|
||||
const filling = !narrow
|
||||
const [world, setWorld] = useState<WorldState | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [jumpingId, setJumpingId] = useState<number | null>(null)
|
||||
@@ -99,7 +103,10 @@ export function WorldMapPanel({ height = 380, onJumped }: WorldMapPanelProps) {
|
||||
const container = containerRef.current
|
||||
if (!container || !world) return
|
||||
|
||||
// Measured pane size (#36): the flex chain gives a definite box in wide mode; `height` is the
|
||||
// fallback when the container has no resolved size yet (narrow flow, or tab hidden on load).
|
||||
const W = container.clientWidth || 800
|
||||
const H = container.clientHeight || height
|
||||
const current = world.systems.find((s) => s.current)!
|
||||
// Systems one jump away — the only clickable chart fields (the same set as before).
|
||||
const reachableIds = new Set(
|
||||
@@ -116,15 +123,17 @@ export function WorldMapPanel({ height = 380, onJumped }: WorldMapPanelProps) {
|
||||
? (s.discovered ? 'DISCOVERED // JUMP' : 'UNKNOWN // JUMP')
|
||||
: s.discovered ? 'DISCOVERED' : 'UNKNOWN',
|
||||
x: (s.x / 100) * W, // seed from the API coordinates so the layout settles near the old chart
|
||||
y: (s.y / 100) * height,
|
||||
y: (s.y / 100) * H,
|
||||
}))
|
||||
const links: ChartLink[] = world.edges.map(([a, b]) => ({ source: a, target: b }))
|
||||
|
||||
// width/height 100% + fixed viewBox: the scene scales with the pane on window resizes without
|
||||
// rebuilding (pan/zoom stay accurate — d3 maps pointers through the SVG transform matrix).
|
||||
const svg = select(container)
|
||||
.append('svg')
|
||||
.attr('width', '100%')
|
||||
.attr('height', height)
|
||||
.attr('viewBox', `0 0 ${W} ${height}`)
|
||||
.attr('height', '100%')
|
||||
.attr('viewBox', `0 0 ${W} ${H}`)
|
||||
.style('display', 'block')
|
||||
|
||||
const root = svg.append('g')
|
||||
@@ -162,9 +171,9 @@ export function WorldMapPanel({ height = 380, onJumped }: WorldMapPanelProps) {
|
||||
maxY = Math.max(maxY, (n.y ?? 0) + NODE_H / 2)
|
||||
}
|
||||
const pad = 16
|
||||
const scale = Math.min(W / (maxX - minX + pad * 2), height / (maxY - minY + pad * 2))
|
||||
const scale = Math.min(W / (maxX - minX + pad * 2), H / (maxY - minY + pad * 2))
|
||||
const tx = W / 2 - scale * ((minX + maxX) / 2)
|
||||
const ty = height / 2 - scale * ((minY + maxY) / 2)
|
||||
const ty = H / 2 - scale * ((minY + maxY) / 2)
|
||||
svg.call(zoomBehavior.transform, zoomIdentity.translate(tx, ty).scale(scale))
|
||||
}
|
||||
|
||||
@@ -220,7 +229,7 @@ export function WorldMapPanel({ height = 380, onJumped }: WorldMapPanelProps) {
|
||||
const simulation = forceSimulation(nodes)
|
||||
.force('link', forceLink<ChartNode, ChartLink>(links).id((d) => d.id).distance(130).strength(0.25))
|
||||
.force('charge', forceManyBody().strength(-420))
|
||||
.force('center', forceCenter(W / 2, height / 2))
|
||||
.force('center', forceCenter(W / 2, H / 2))
|
||||
.force('collide', forceCollide(60))
|
||||
.on('end', fitToNodes)
|
||||
.on('tick', () => {
|
||||
@@ -257,9 +266,17 @@ export function WorldMapPanel({ height = 380, onJumped }: WorldMapPanelProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||
{/* Same wrapper + title bar as the old hand-rolled chart; d3 owns everything inside the box. */}
|
||||
<div style={{ border: '1px solid var(--border)', background: 'var(--surface)', fontFamily: 'var(--font-mono)', overflow: 'hidden' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', ...(filling ? { height: '100%' } : {}) }}>
|
||||
{/* Wrapper + title bar; in wide mode the box flexes to fill the whole pane (#36). d3 owns everything inside it. */}
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid var(--border)',
|
||||
background: 'var(--surface)',
|
||||
fontFamily: 'var(--font-mono)',
|
||||
overflow: 'hidden',
|
||||
...(filling ? { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' } : {}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: '0.4rem 0.75rem',
|
||||
@@ -273,7 +290,7 @@ export function WorldMapPanel({ height = 380, onJumped }: WorldMapPanelProps) {
|
||||
>
|
||||
GALACTIC CHART // JUMP ROUTES — TURN {world.turn}
|
||||
</div>
|
||||
<div ref={containerRef} style={{ width: '100%', height: `${height}px` }} />
|
||||
<div ref={containerRef} style={{ width: '100%', ...(filling ? { flex: 1, minHeight: 0 } : { height: `${height}px` }) }} />
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
|
||||
Reference in New Issue
Block a user