Skip to content

Commit 8ce563a

Browse files
rezailmiReza Ilmi
andauthored
feat(world-scene): tidy HUD buttons — collapse zoom cluster, swap arrange icon (#65)
- Remove the top-right slider toggle button; rely on the existing DevPalette command "Show/Hide world controls" (Cmd+K) as the entry. - Collapse the three zoom buttons (Zoom in / Zoom out / Reset view) into a single "Reset view" button (RotateCcw icon). The +/-/0 keyboard shortcuts still zoom/reset and are advertised in the button's aria-label. - Swap the arrange-mode icon from Move (4-arrow) to Pencil to match the edit-mode semantic. - Update HUD test: panel-only controls (rain/music/bird) now seeded via a Map-backed localStorage stub (matches DevPalette.test.tsx pattern). Adds a regression guard asserting the legacy slider toggle is absent. Co-authored-by: Reza Ilmi <reza.ilmi@gt.tech.gov.sg>
1 parent 15b519d commit 8ce563a

3 files changed

Lines changed: 51 additions & 42 deletions

File tree

src/components/IslandProgressionOverlay.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Check, Move } from 'lucide-react'
1+
import { Check, Pencil } from 'lucide-react'
22
import { useEffect, useState } from 'react'
33
import { WorldIconButton } from '~/components/student-space/hud/StudentSpaceHud'
44
import type { Game } from '~/engine/student-space/Game'
@@ -189,7 +189,7 @@ export function IslandProgressionOverlay({ game }: { game: Game }) {
189189
{editMode ? (
190190
<Check aria-hidden="true" className="size-4" />
191191
) : (
192-
<Move aria-hidden="true" className="size-4" />
192+
<Pencil aria-hidden="true" className="size-4" />
193193
)}
194194
</WorldIconButton>
195195

src/components/student-space/hud/StudentSpaceHud.tsx

Lines changed: 7 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,4 @@
1-
import {
2-
CloudRain,
3-
Gauge,
4-
Music2,
5-
RotateCcw,
6-
SlidersHorizontal,
7-
Sparkles,
8-
Volume2,
9-
VolumeX,
10-
X,
11-
ZoomIn,
12-
ZoomOut,
13-
} from 'lucide-react'
1+
import { CloudRain, Gauge, Music2, RotateCcw, Sparkles, Volume2, VolumeX, X } from 'lucide-react'
142
import type { ButtonHTMLAttributes, ReactNode } from 'react'
153
import { useCallback, useEffect, useState } from 'react'
164
import { Hud } from '~/components/ui/hud'
@@ -129,25 +117,11 @@ export function StudentSpaceHud({ game }: { game: unknown }) {
129117
return (
130118
<>
131119
<ZoomHud game={typedGame} />
132-
{visible ? (
133-
<WorldControlsPanel game={typedGame} onClose={() => setVisible(false)} />
134-
) : (
135-
<WorldControlsToggle onToggle={() => setVisible(true)} />
136-
)}
120+
{visible ? <WorldControlsPanel game={typedGame} onClose={() => setVisible(false)} /> : null}
137121
</>
138122
)
139123
}
140124

141-
function WorldControlsToggle({ onToggle }: { onToggle: () => void }) {
142-
return (
143-
<div className="fixed top-[calc(var(--inset-frame)+12px)] right-[calc(var(--inset-frame)+12px)] z-30">
144-
<WorldIconButton label="Show world controls" onClick={onToggle}>
145-
<SlidersHorizontal aria-hidden className="size-4" />
146-
</WorldIconButton>
147-
</div>
148-
)
149-
}
150-
151125
function WorldControlsPanel({ game, onClose }: { game: GameLike; onClose: () => void }) {
152126
return (
153127
<Hud
@@ -399,13 +373,11 @@ function ZoomHud({ game }: { game: GameLike }) {
399373

400374
return (
401375
<div className="fixed right-[calc(var(--inset-frame)+12px)] bottom-[calc(var(--inset-frame)+16px)] z-30 flex flex-col gap-2">
402-
<WorldIconButton label="Zoom in" onClick={() => dispatch('zoom-in')}>
403-
<ZoomIn aria-hidden className="size-4" />
404-
</WorldIconButton>
405-
<WorldIconButton label="Zoom out" onClick={() => dispatch('zoom-out')}>
406-
<ZoomOut aria-hidden className="size-4" />
407-
</WorldIconButton>
408-
<WorldIconButton label="Reset view" onClick={() => dispatch('reset')}>
376+
<WorldIconButton
377+
label="Reset view (+ / − / 0 to zoom)"
378+
title="Reset view (+ / − / 0 to zoom)"
379+
onClick={() => dispatch('reset')}
380+
>
409381
<RotateCcw aria-hidden className="size-4" />
410382
</WorldIconButton>
411383
<WorldIconButton label="Toggle sound" pressed={!muted} onClick={() => dispatch('sound')}>

test/components/student-space/hud/student-space-hud.test.tsx

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
11
import { render, screen } from '@testing-library/react'
22
import userEvent from '@testing-library/user-event'
3-
import { afterEach, describe, expect, it, vi } from 'vitest'
3+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
44
import { StudentSpaceHud } from '~/components/student-space/hud/StudentSpaceHud'
55
import { EngineOverlayProvider } from '~/lib/student-space/use-engine-overlay'
6+
import { WORLD_CONTROLS_STORAGE_KEY } from '~/lib/student-space/world-controls-visibility'
67

7-
function renderHud(engine = makeFakeEngine()) {
8+
let originalStorageDescriptor: PropertyDescriptor | undefined
9+
10+
function createStorageStub() {
11+
const map = new Map<string, string>()
12+
return {
13+
getItem(key: string) {
14+
return map.has(key) ? (map.get(key) ?? null) : null
15+
},
16+
setItem(key: string, value: string) {
17+
map.set(key, String(value))
18+
},
19+
removeItem(key: string) {
20+
map.delete(key)
21+
},
22+
}
23+
}
24+
25+
function renderHud(engine = makeFakeEngine(), { panelOpen = false } = {}) {
26+
if (panelOpen) window.localStorage.setItem(WORLD_CONTROLS_STORAGE_KEY, '1')
827
render(
928
<EngineOverlayProvider>
1029
<StudentSpaceHud game={engine} />
@@ -61,16 +80,29 @@ function makeFakeEngine() {
6180
}
6281
}
6382

83+
beforeEach(() => {
84+
originalStorageDescriptor = Object.getOwnPropertyDescriptor(window, 'localStorage')
85+
Object.defineProperty(window, 'localStorage', {
86+
configurable: true,
87+
value: createStorageStub(),
88+
})
89+
})
90+
6491
afterEach(() => {
6592
document.body.className = ''
93+
if (originalStorageDescriptor) {
94+
Object.defineProperty(window, 'localStorage', originalStorageDescriptor)
95+
} else {
96+
delete (window as { localStorage?: unknown }).localStorage
97+
}
6698
})
6799

68100
describe('StudentSpaceHud', () => {
69101
it('renders React HUD controls and dispatches engine actions', async () => {
70-
const engine = renderHud()
102+
const engine = renderHud(makeFakeEngine(), { panelOpen: true })
71103

72-
await userEvent.click(screen.getByRole('button', { name: 'Zoom in' }))
73-
expect(engine.view.camera.zoomBy).toHaveBeenCalledWith(0.85)
104+
await userEvent.click(screen.getByRole('button', { name: /Reset view/ }))
105+
expect(engine.view.camera.resetToDefault).toHaveBeenCalled()
74106

75107
await userEvent.click(screen.getByRole('button', { name: 'Toggle sound' }))
76108
expect(engine.view.sound.toggleMuted).toHaveBeenCalled()
@@ -87,6 +119,11 @@ describe('StudentSpaceHud', () => {
87119
expect(engine.view.kira.cycleSpecies).toHaveBeenCalledWith(1)
88120
})
89121

122+
it('does not render the legacy slider toggle button', () => {
123+
renderHud()
124+
expect(screen.queryByRole('button', { name: /show world controls/i })).not.toBeInTheDocument()
125+
})
126+
90127
it('hides dev-only status and fps controls behind the DevPalette body class', () => {
91128
document.body.classList.add('is-dev-overlay-hidden')
92129
renderHud()

0 commit comments

Comments
 (0)