Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
52bc63b
init prompt
vagkaratzas Jul 4, 2026
e694282
remove prompt file
vagkaratzas Jul 4, 2026
fe4578f
feat(ui): unified dark instrument design foundation
vagkaratzas Jul 4, 2026
4226195
feat(ui): open action panels as a side drawer, not full-scene overlay
vagkaratzas Jul 4, 2026
d5e342d
feat(ui): make panel content fit the drawer width
vagkaratzas Jul 4, 2026
18a3e81
feat(ui): redesign Home panel with hero, action cards, quiet citations
vagkaratzas Jul 4, 2026
6088be8
feat(ui): theme the Help panel tab bar and content for dark mode
vagkaratzas Jul 4, 2026
6dea96c
feat(ui): polish scene overlays and add a centered empty state
vagkaratzas Jul 4, 2026
19f0f69
feat(ui): responsive nav and drawer clearance
vagkaratzas Jul 4, 2026
8559a7e
docs: add REPORT_TASTE.md summarizing the UI/UX redesign
vagkaratzas Jul 4, 2026
e893d59
feat(ui): redesign nav help as a shortcut cheat-sheet with kbd keys
vagkaratzas Jul 4, 2026
9c64a5c
feat(ui): polish canvas controls table with hover states and themed s…
vagkaratzas Jul 4, 2026
f9b1224
fix(ui): stop nav-control button clipping and dead info scrollbar
vagkaratzas Jul 4, 2026
73355e2
feat(ui): real stop/play and up/down icons on nav toggle buttons
vagkaratzas Jul 4, 2026
93b3e68
feat(ui): add collapse handle for side-opening action panels
vagkaratzas Jul 4, 2026
ce5f11d
feat(network): seed node scatter so loads are reproducible
vagkaratzas Jul 4, 2026
8bf5e31
feat(ui): collapse the File drawer after a successful load
vagkaratzas Jul 4, 2026
19789a8
feat(ui): port v2 theme bar (Light/Dark/Gray) with e2e test
vagkaratzas Jul 4, 2026
652d808
fix(ui): keep theme floor color on layer hover-exit
vagkaratzas Jul 4, 2026
e37cda4
report removed
vagkaratzas Jul 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 0 additions & 102 deletions REPORT_THREEJS.md

This file was deleted.

47 changes: 47 additions & 0 deletions frontend/e2e/themes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { expect, test } from '@playwright/test'

// Theme bar (v2 port): buttons appear after a network loads; clicking a
// preset recolours the scene; undo restores the previous theme. The canvas
// is opaque to the DOM, so assertions read window.__arena.ctx (applyTheme
// sets ctx.edgeDefaultColor, distinct per theme).

function edgeColor() {
return () =>
(window as unknown as { __arena: { ctx: { edgeDefaultColor: string } } })
.__arena.ctx.edgeDefaultColor
}

test('theme buttons attach on load and switch scene theme (undoable)', async ({
page,
}) => {
await page.goto('/')
await page.waitForFunction(
() => '__arena' in (window as unknown as Record<string, unknown>)
)

// No buttons before a network exists.
await expect(page.locator('#themeDiv .themeButton')).toHaveCount(0)

await page.getByRole('tab', { name: 'File' }).click()
await page.getByRole('button', { name: 'Load Example' }).click()
await expect(page.locator('#file_status')).toContainText('Loaded network')

await expect(page.locator('#themeDiv .themeButton')).toHaveCount(3)

await page.locator('#lightThemeButton').click()
expect(await page.evaluate(edgeColor())).toBe('#5c5c5c')
// Regression: floor_color input is the picker-path source of truth, so a
// theme toggle must update it — otherwise hover-exit repaint reverts layers
// to the stale (dark) color.
await expect(page.locator('#floor_color')).toHaveValue('#8aa185')

await page.locator('#grayThemeButton').click()
expect(await page.evaluate(edgeColor())).toBe('#6e2a5a')

await page.locator('#darkThemeButton').click()
expect(await page.evaluate(edgeColor())).toBe('#ffffff')

// Undo walks back dark → gray.
await page.locator('#undoButton').click()
expect(await page.evaluate(edgeColor())).toBe('#6e2a5a')
})
14 changes: 13 additions & 1 deletion frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,20 @@
<div class="tab-pane fade" id="panel-help" role="tabpanel"></div>
</div>

<button
id="panelDrawerToggle"
type="button"
class="panel-drawer-toggle"
aria-label="Hide panel"
style="display: none"
></button>

<div id="navControlButtonsDiv"></div>
<div id="info">Waiting for Network to be uploaded.</div>
<div id="themeDiv"></div>
<div id="info">
No network loaded yet. Open the <b>File</b> tab to upload a network or
load an example.
</div>
<div id="loader"></div>
<div id="app"></div>
<div id="labelDiv"></div>
Expand Down
72 changes: 54 additions & 18 deletions frontend/src/actions/nav_controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,24 +183,26 @@ function axisRowsHtml(
}

const INFO_HTML = `
1. <b>Zoom</b>: Mouse Wheel<br/>
2. <b>Pan</b>: Click Drag Scene / Arrow Keys<br/>
3. <b>Orbit</b>: Mouse Middle Drag<br/>
4. <b>Drag Layer</b>: Click Drag<br/>
5. <b>Rotate Layer</b>: <span class='blue'>Z</span> / <span class='red'>X</span> / <span class='green'>C</span> + Click Drag<br/>
6. <b>Move Selected Nodes</b>: <span class='blue'>Z</span> / <span class='green'>C</span> + Click Drag<br/>
7. <b>Node/Layer Selection</b>: Double Click<br/>
8. <b>Lasso Nodes</b>: Shift + Click Drag<br/>
9. <b>Unselect All Nodes</b>: Double Click Scene
<ul id="navShortcuts">
<li><span>Zoom</span><span><kbd>Wheel</kbd></span></li>
<li><span>Pan</span><span><kbd>Drag</kbd> / <kbd>&#8592;&#8593;&#8594;&#8595;</kbd></span></li>
<li><span>Orbit</span><span><kbd>Middle Drag</kbd></span></li>
<li><span>Drag Layer</span><span><kbd>Drag</kbd></span></li>
<li><span>Rotate Layer</span><span><kbd class='blue'>Z</kbd><kbd class='red'>X</kbd><kbd class='green'>C</kbd> + <kbd>Drag</kbd></span></li>
<li><span>Move Selected Nodes</span><span><kbd class='blue'>Z</kbd><kbd class='green'>C</kbd> + <kbd>Drag</kbd></span></li>
<li><span>Select Node/Layer</span><span><kbd>Dbl Click</kbd></span></li>
<li><span>Lasso Nodes</span><span><kbd>Shift</kbd> + <kbd>Drag</kbd></span></li>
<li><span>Unselect All</span><span><kbd>Dbl Click</kbd> Scene</span></li>
</ul>
<table id="canvasControls_table"><tbody>
<tr><td colspan="4"><h5>Scene</h5></td></tr>
<tr><td colspan="4">Rotation Controls</td></tr>
<tr><td colspan="4" class="controlsSubLabel">Rotation Controls</td></tr>
${axisRowsHtml('sliderValue1', 'Angle: 5&#730;', 'sceneRotateSlider', { min: 1, max: 15, value: 5, step: 1 }, '')}
<tr><td colspan="4"><button id="recenterButton">Recenter Network</button></td></tr>
<tr class="border_tr"><td colspan="4"><h5>Layers</h5></td></tr>
<tr><td colspan="4">Rotation Controls</td></tr>
<tr><td colspan="4" class="controlsSubLabel">Rotation Controls</td></tr>
${axisRowsHtml('sliderValue2', 'Angle: 5&#730;', 'layerRotateSlider', { min: 1, max: 15, value: 5, step: 1 }, '')}
<tr><td colspan="4">Translation Controls</td></tr>
<tr><td colspan="4" class="controlsSubLabel">Translation Controls</td></tr>
<tr>
<td colspan="2" class="canvasControls image_expandLayers" data-nav="expandLayers"></td>
<td colspan="2" class="canvasControls image_collapseLayers" data-nav="collapseLayers"></td>
Expand All @@ -214,7 +216,7 @@ const INFO_HTML = `
<td id="sliderValue4">x1</td>
</tr>
<tr class="border_tr"><td colspan="4"><h5>Nodes</h5></td></tr>
<tr><td colspan="4">Translation Controls</td></tr>
<tr><td colspan="4" class="controlsSubLabel">Translation Controls</td></tr>
<tr>
<td colspan="2" class="canvasControls image_nodeExpand" data-nav="expandNodes"></td>
<td colspan="2" class="canvasControls image_nodeCollapse" data-nav="collapseNodes"></td>
Expand Down Expand Up @@ -242,6 +244,35 @@ function holdAction(name: string): (() => void) | null {
return map[name] ?? null
}

// Real stop/start affordance for the render-pause button (previously a
// static "Stop:Render..." label that never reflected the actual flag).
function setPauseButtonState(paused: boolean): void {
const btn = document.getElementById('interLayerEdgesRenderPauseButton')
if (!btn) return
btn.classList.toggle('is-paused', paused)
btn.innerHTML = paused
? '<span class="nav-toggle-icon">&#9654;</span>Render Inter-Layer Edges'
: '<span class="nav-toggle-icon">&#10074;&#10074;</span>Pause Inter-Layer Edges'
btn.setAttribute(
'aria-label',
paused
? 'Resume inter-layer edge rendering'
: 'Pause inter-layer edge rendering'
)
}

// Up/down chevron reflecting whether #info is currently expanded.
function setControlsToggleState(open: boolean): void {
const btn = document.getElementById('displayCanvasControlsButton')
if (!btn) return
btn.classList.toggle('is-open', open)
btn.innerHTML = `<span class="nav-toggle-icon">${open ? '&#9652;' : '&#9662;'}</span>Navigation Controls`
btn.setAttribute(
'aria-label',
open ? 'Hide navigation controls' : 'Show navigation controls'
)
}

// v2 attached the controls on first network build (canvasControlsAttached
// flag); #info keeps its "waiting for network" hint until then.
export function registerNavControls(): void {
Expand All @@ -257,20 +288,25 @@ function attachNavControls(): void {
if (!buttonsDiv || !info) return

buttonsDiv.innerHTML = `
<button id="interLayerEdgesRenderPauseButton" class="displayCanvasControls">Stop:Render Inter-Layer Edges</button><br/>
<button id="displayCanvasControlsButton" class="displayCanvasControls">Navigation Controls</button>`
<button id="interLayerEdgesRenderPauseButton" class="displayCanvasControls nav-toggle-btn" type="button"></button>
<button id="displayCanvasControlsButton" class="displayCanvasControls nav-toggle-btn" type="button"></button>`
document
.getElementById('interLayerEdgesRenderPauseButton')!
.addEventListener('click', toggleInterLayerEdgesRendering)
.addEventListener('click', () => {
setPauseButtonState(toggleInterLayerEdgesRendering())
})
document
.getElementById('displayCanvasControlsButton')!
.addEventListener('click', () => {
info.style.display =
info.style.display === 'none' ? 'inline-block' : 'none'
const open = info.style.display === 'none'
info.style.display = open ? 'inline-block' : 'none'
setControlsToggleState(open)
})

info.innerHTML = INFO_HTML
info.style.display = 'inline-block'
setPauseButtonState(ctx.interLayerEdgesRenderPauseFlag)
setControlsToggleState(true)

document
.getElementById('recenterButton')!
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/actions/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,23 @@ import {
findIndexByUuid,
getCaseInsensitiveIndices,
getRandomArbitrary,
setRandomSeed,
} from '../utils'
import { raycaster, setRaycaster } from './screen'

// v2 set.seed(123): fixed so every network load scatters to the same layout.
const SCRAMBLE_SEED = 123

// v2 node.js scrambleNodes — random y/z spread onto each node's layer plane.
// Re-seeds first so the same network always scatters identically (Load Example
// and uploads are reproducible).
export function scrambleNodes(
yMin = ctx.yBoundMin,
yMax = ctx.yBoundMax,
zMin = ctx.zBoundMin,
zMax = ctx.zBoundMax
): void {
setRandomSeed(SCRAMBLE_SEED)
for (const node of ctx.nodeObjects) {
node.translateY(getRandomArbitrary(yMin, yMax))
node.translateZ(getRandomArbitrary(zMin, zMax))
Expand Down
46 changes: 45 additions & 1 deletion frontend/src/actions/themes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
// /api/config (v2 CHANNEL_COLORS_LIGHT/DARK).

import { bus } from '../bus'
import { history } from '../commands/base'
import { ChangeThemeCommand } from '../commands/scene'
import { store } from '../store'
import { ctx } from '../three'
import { assignChannelColorsFromPalette, redrawIntraLayerEdges } from './edge'
Expand Down Expand Up @@ -61,8 +63,14 @@ export function applyTheme(name: string, fromInit = false): void {

if (!fromInit) {
// v2 repaintLayersFromPicker: switch layers to picker priority with the
// theme's floor color
// theme's floor color. The floor_color input is the source of truth on the
// picker path (repaintLayers()/Layer.getColor() read it on hover-exit), so
// update it too — otherwise hover-exit reverts to the stale (dark) color.
ctx.layerColorPrioritySource = 'picker'
const floorInput =
typeof document !== 'undefined' &&
(document.getElementById('floor_color') as HTMLInputElement | null)
if (floorInput) floorInput.value = theme.floor
repaintLayers(theme.floor)
redrawIntraLayerEdges()
ctx.renderInterLayerEdgesFlag = true
Expand All @@ -75,3 +83,39 @@ export function applyTheme(name: string, fromInit = false): void {
export function registerThemeListener(): void {
bus.on('theme:changed', ({ theme }) => applyTheme(theme))
}

// Port of v2 attachThemeButtons: the fixed top-right Light/Dark/Gray bar.
// Attached on first network load (themes only recolour a live scene), routed
// through ChangeThemeCommand so theme switches are undoable.
export function registerThemeButtons(): void {
const off = bus.on('network:loaded', () => {
attachThemeButtons()
off()
})
}

let currentTheme = 'dark' // matches the app's default dark chrome

function attachThemeButtons(): void {
const themeDiv = document.getElementById('themeDiv')
if (!themeDiv || themeDiv.childElementCount > 0) return
for (const name of Object.keys(THEMES)) {
const btn = document.createElement('button')
btn.type = 'button'
btn.id = `${name}ThemeButton`
btn.className = 'themeButton'
btn.textContent = name.charAt(0).toUpperCase() + name.slice(1)
btn.addEventListener('click', () => {
history.execute(
new ChangeThemeCommand(
name,
() => currentTheme,
(t) => {
currentTheme = t
}
)
)
})
themeDiv.appendChild(btn)
}
}
Loading
Loading