From 52bc63bb972fbbab47ce8d36b3fb8d691c20786a Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 09:52:16 +0100 Subject: [PATCH 01/20] init prompt --- PROMPT.txt | 74 +++++++++++++++++++++++++++++++++ REPORT_THREEJS.md | 102 ---------------------------------------------- 2 files changed, 74 insertions(+), 102 deletions(-) create mode 100644 PROMPT.txt delete mode 100644 REPORT_THREEJS.md diff --git a/PROMPT.txt b/PROMPT.txt new file mode 100644 index 0000000..2d357fb --- /dev/null +++ b/PROMPT.txt @@ -0,0 +1,74 @@ +Use the taste-skill plugin to redesign and significantly improve the frontend UI/UX of this application while preserving all existing backend APIs, data flows, routes, authentication behavior, and functionality. +Use token-saviour skill. 1 feature - 1 commit rule (no push). + +Primary goal: +Transform the frontend into a cohesive, modern, polished, production-ready interface with strong layout, typography, spacing, color usage, responsiveness, accessibility, animations, interactions, and visual hierarchy. + +Non-negotiable constraints: +- Do not change backend behavior. +- Do not change API endpoints, request shapes, response shapes, authentication logic, database logic, or server-side contracts. +- Do not replace real API data with mock data. +- Do not remove existing features or user flows. +- Do not perform a full rewrite unless the existing frontend structure is clearly blocking the redesign and the change is justified. +- Preserve product semantics and core navigation. +- Prioritize usability, clarity, accessibility, and maintainability over purely decorative changes. + +Before making changes: +1. Inspect the frontend structure, styling system, component hierarchy, routes, and key user flows. +2. Run the app and identify current UI issues, visual inconsistencies, console errors, responsive layout problems, and interaction bugs. +3. Use playwright-cli to establish a baseline for key flows before making major changes. +4. Create a concise implementation plan covering the main UI areas/components to improve. + +Redesign priorities: +- Improve layout structure, spacing, alignment, and visual hierarchy. +- Improve typography, sizing, readability, and contrast. +- Create a more cohesive color palette and design language. +- Improve buttons, forms, cards, panels, navigation, empty states, loading states, and error states. +- Improve hover, focus, active, disabled, and selected states. +- Add subtle, purposeful animations and transitions. +- Improve responsiveness across desktop, tablet, and mobile viewports. +- Improve accessibility, including keyboard focus, semantic structure where practical, contrast, reduced-motion behavior, and readable text. +- Fix UI bugs encountered during the redesign. +- Navigation open-up menus other than `Home` and `Help` SHOULD NOT cover the whole main view three.js scene. + +Design direction: +Make confident, high-quality improvements rather than superficial tweaks, but avoid unnecessary complexity. The result should feel premium, intentional, and cohesive while still preserving the application’s existing purpose and workflows. + +Validation loop: +After each substantial change: +1. Run relevant build, lint, typecheck, and test commands available in the project. +2. Use playwright-cli to verify key user flows still work. +3. Check for console errors and runtime errors. +4. Check responsive behavior at common desktop, tablet, and mobile viewport sizes. +5. Fix any regressions, layout breakage, accessibility issues, or obvious visual defects before proceeding. + +Visual defects to catch and fix: +- Broken spacing or alignment. +- Text clipping, overflow, or unreadable text. +- Poor contrast. +- Layout shifts. +- Broken mobile or tablet layouts. +- Inconsistent component styling. +- Missing hover/focus/active states. +- Jarring or excessive animations. +- Loading or error states that look unfinished. +- Console errors caused by frontend changes. + +Backend rule: +Do not modify backend files or API contracts. If a frontend issue appears to require backend changes, stop and document the issue, the reason, and the smallest proposed backend change instead of making it automatically. + +Final validation: +Iterate until: +- No functional regressions are found. +- No critical console/runtime errors remain. +- Key user flows pass in Playwright. +- The interface works across major viewport sizes. +- The design feels cohesive, modern, accessible, and production-ready. +- Further changes would be minor polish rather than meaningful improvement. + +Final response should include (Write in a REPORT_TASTE.md file): +- Summary of UI/UX improvements made. +- Files changed. +- Validation commands run. +- Playwright flows tested. +- Any known limitations, risks, or recommended follow-up improvements. \ No newline at end of file diff --git a/REPORT_THREEJS.md b/REPORT_THREEJS.md deleted file mode 100644 index d98337c..0000000 --- a/REPORT_THREEJS.md +++ /dev/null @@ -1,102 +0,0 @@ -# Three.js / UI Refinement Report - -Branch: `three-js-lifting` — 8 commits, one feature per commit, no push. -Backend untouched: no endpoint, model, or contract changes (verified by the -full backend test suite and by the unchanged `backend/` tree). - -## Summary of major improvements - -### Phase 1 — Three.js correctness -1. **HiDPI rendering** — the renderer ignored `devicePixelRatio` (blurry on - retina). Now `setPixelRatio(min(dpr, 2))`, re-applied on resize for - monitor moves; `powerPreference: 'high-performance'`. -2. **Render loop** — replaced the `setTimeout(rAF)` hybrid with a pure - `requestAnimationFrame` loop and timestamp-based FPS cap (`ctx.fps` still - the configurable bound). Frames are vsync-aligned and the loop auto-pauses - in hidden tabs. -3. **GPU resource disposal** — scene graphs orphaned when a new command cut - off the redo branch were never disposed (geometry/material/texture leak on - repeated network loads). Commands now have an optional `dispose()` hook; - `LoadNetworkCommand` frees its exclusively-owned snapshot. Undo history - stays fully intact — only provably unreachable scenes are disposed. - -### Phase 2 — UX / interaction -4. **Smooth eased wheel zoom** — wheel sets a clamped target scale (same - 0.2–2 bounds); the render loop eases toward it. `prefers-reduced-motion` - users keep instant zoom; the target resets on network load. -5. **Smoother pan/orbit/lasso** — the v2 10px drag gate made orbit jump in - ~10° increments; delta-based interactions now update every 2px. Held-key - node/layer transforms keep the 10px step (they apply a fixed increment per - event, so their speed is unchanged). -6. **Lasso leak fix** — the lasso rectangle allocated a new geometry+material - per pointer move without disposal; material is now shared, geometry - disposed on rebuild/release. -7. **Hover raycast throttling** — raycasting every node sphere per mousemove - janks large networks; moves now record the latest position and the render - loop does one hover check per frame. -8. **Picking crash fix (found during validation)** — `intersectObjects` - defaults to recursive, so hover rays hit plane children (node spheres, - labels, coord lines) whose uuids aren't in the picked list; - `findIndexByUuid` returned -1 and `ctx.layers[-1].plane` threw on hover. - Hover/double-click picking is now non-recursive (also cheaper). - `drag_controls` was deliberately left recursive — its uuid gate encodes - the v2 "don't drag a layer while over a node" behavior. - -### Phase 3 — Postprocessing (`frontend/src/three/postprocessing.ts`) -9. **Optional subtle bloom** — `EffectComposer` + `UnrealBloomPass` - (strength 0.35, radius 0.3, threshold 0.8) behind a "Node Glow" checkbox - in Scene Actions. Guard rails: - - active only over dark backgrounds (luma < 0.5) — bloom on the white - theme would wash the scene out; theme switches toggle it automatically; - - defaults off on mobile UAs; - - when inactive the plain `renderer.render` path runs — zero extra GPU - cost; - - labels are DOM overlays, so they're never bloomed. - -### Phase 4 — UI polish (CSS only, no semantic changes) -10. **Cohesive dark chrome** — shared design tokens (accent, frosted - translucent surface, hairline border) across navbar, canvas-control - overlay card, node description card, footer and loader; system font - stack; the solid-blue v2 footer became a quiet translucent bar; the - 120px loader became a slim centered ring. Added `:focus-visible` - outlines and reduced-motion handling for spinner + zoom. - -## Files changed -- `frontend/src/actions/screen.ts` — renderer config, DPR, rAF loop, composer wiring -- `frontend/src/actions/canvas_controls.ts` — eased zoom, drag thresholds, lasso disposal, hover coalescing -- `frontend/src/actions/canvas_controls.test.ts` — updated zoom test for easing -- `frontend/src/actions/node.ts`, `frontend/src/actions/layer.ts` — non-recursive picking -- `frontend/src/commands/base.ts`, `frontend/src/commands/scene.ts` — `dispose()` hook -- `frontend/src/three/runtime.ts` — `disposeSnapshot` -- `frontend/src/three/postprocessing.ts` — new (bloom module) -- `frontend/src/ui/scene.ts` — Node Glow toggle -- `frontend/src/style.css` — design tokens + chrome polish - -## Validation commands run (all green) -- `backend`: `uv run pytest` (58 passed), `uv run ruff check .`, `uv run mypy app` -- `frontend`: `npx vitest run` (105 tests, 15 files), `npm run lint`, - `npx tsc --noEmit`, `npm run build`, `npx playwright test` (e2e suite) - -## Playwright flows tested -- Existing e2e: load example → Fruchterman-Reingold layout → clustered layout → export session. -- Custom scripts: full-canvas hover sweeps (crash regression), eased-zoom - scale assertions, middle-drag orbit, double-click select + Ctrl+Z/Ctrl+Shift+Z - undo/redo, bloom toggle on/off screenshots, viewport resize (1440/900/700) - + reload, responsive screenshots at desktop/tablet/mobile widths. -- Console: zero errors in every run (only headless-GL driver warnings from - screenshot ReadPixels, environmental). - -## Remaining risks / follow-ups -- **Undo stack is unbounded** — long sessions with many network loads retain - all previous scenes for undo (CPU+GPU). Disposal now covers the redo side; - a capped undo stack with eviction-time `dispose()` is the natural next step. -- **Bloom on WebGL context loss** is untested; the composer is created once - and never rebuilt. -- **Node hover check still maps `ctx.nodeObjects` to a new array per check** - (once per frame now, not per event); for 10k+ nodes consider a persistent - array or BVH. -- Lighting (`AmbientLight` + two point lights) left untouched to preserve the - established look; a soft key/fill setup could improve depth perception if a - visual refresh is ever wanted. -- The `#info:not(:has(table))` rule assumes the pre-network overlay contains - no table; if that markup changes, revisit. From e6942821765b358d97971029e3d102ee27f1e0ad Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 09:52:41 +0100 Subject: [PATCH 02/20] remove prompt file --- PROMPT.txt | 74 ------------------------------------------------------ 1 file changed, 74 deletions(-) delete mode 100644 PROMPT.txt diff --git a/PROMPT.txt b/PROMPT.txt deleted file mode 100644 index 2d357fb..0000000 --- a/PROMPT.txt +++ /dev/null @@ -1,74 +0,0 @@ -Use the taste-skill plugin to redesign and significantly improve the frontend UI/UX of this application while preserving all existing backend APIs, data flows, routes, authentication behavior, and functionality. -Use token-saviour skill. 1 feature - 1 commit rule (no push). - -Primary goal: -Transform the frontend into a cohesive, modern, polished, production-ready interface with strong layout, typography, spacing, color usage, responsiveness, accessibility, animations, interactions, and visual hierarchy. - -Non-negotiable constraints: -- Do not change backend behavior. -- Do not change API endpoints, request shapes, response shapes, authentication logic, database logic, or server-side contracts. -- Do not replace real API data with mock data. -- Do not remove existing features or user flows. -- Do not perform a full rewrite unless the existing frontend structure is clearly blocking the redesign and the change is justified. -- Preserve product semantics and core navigation. -- Prioritize usability, clarity, accessibility, and maintainability over purely decorative changes. - -Before making changes: -1. Inspect the frontend structure, styling system, component hierarchy, routes, and key user flows. -2. Run the app and identify current UI issues, visual inconsistencies, console errors, responsive layout problems, and interaction bugs. -3. Use playwright-cli to establish a baseline for key flows before making major changes. -4. Create a concise implementation plan covering the main UI areas/components to improve. - -Redesign priorities: -- Improve layout structure, spacing, alignment, and visual hierarchy. -- Improve typography, sizing, readability, and contrast. -- Create a more cohesive color palette and design language. -- Improve buttons, forms, cards, panels, navigation, empty states, loading states, and error states. -- Improve hover, focus, active, disabled, and selected states. -- Add subtle, purposeful animations and transitions. -- Improve responsiveness across desktop, tablet, and mobile viewports. -- Improve accessibility, including keyboard focus, semantic structure where practical, contrast, reduced-motion behavior, and readable text. -- Fix UI bugs encountered during the redesign. -- Navigation open-up menus other than `Home` and `Help` SHOULD NOT cover the whole main view three.js scene. - -Design direction: -Make confident, high-quality improvements rather than superficial tweaks, but avoid unnecessary complexity. The result should feel premium, intentional, and cohesive while still preserving the application’s existing purpose and workflows. - -Validation loop: -After each substantial change: -1. Run relevant build, lint, typecheck, and test commands available in the project. -2. Use playwright-cli to verify key user flows still work. -3. Check for console errors and runtime errors. -4. Check responsive behavior at common desktop, tablet, and mobile viewport sizes. -5. Fix any regressions, layout breakage, accessibility issues, or obvious visual defects before proceeding. - -Visual defects to catch and fix: -- Broken spacing or alignment. -- Text clipping, overflow, or unreadable text. -- Poor contrast. -- Layout shifts. -- Broken mobile or tablet layouts. -- Inconsistent component styling. -- Missing hover/focus/active states. -- Jarring or excessive animations. -- Loading or error states that look unfinished. -- Console errors caused by frontend changes. - -Backend rule: -Do not modify backend files or API contracts. If a frontend issue appears to require backend changes, stop and document the issue, the reason, and the smallest proposed backend change instead of making it automatically. - -Final validation: -Iterate until: -- No functional regressions are found. -- No critical console/runtime errors remain. -- Key user flows pass in Playwright. -- The interface works across major viewport sizes. -- The design feels cohesive, modern, accessible, and production-ready. -- Further changes would be minor polish rather than meaningful improvement. - -Final response should include (Write in a REPORT_TASTE.md file): -- Summary of UI/UX improvements made. -- Files changed. -- Validation commands run. -- Playwright flows tested. -- Any known limitations, risks, or recommended follow-up improvements. \ No newline at end of file From fe4578ff5b4b97834de2b34ea3de690616545df4 Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 09:59:42 +0100 Subject: [PATCH 03/20] feat(ui): unified dark instrument design foundation Retint Bootstrap's dark tokens to one cool palette + accent, refine navbar, buttons, forms, focus rings, and honor prefers-reduced-motion. Co-Authored-By: Claude Opus 4.8 --- frontend/src/style.css | 173 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 160 insertions(+), 13 deletions(-) diff --git a/frontend/src/style.css b/frontend/src/style.css index c8d1b68..4a0690a 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -3,29 +3,168 @@ with their panels. */ /* Design tokens shared by the 2D chrome so it reads as one system with the - dark canvas. */ + dark canvas. One cool "instrument" palette; a single blue accent. */ :root { - --arena-accent: #4f8ff7; - --arena-surface: rgba(20, 24, 31, 0.82); - --arena-border: rgba(255, 255, 255, 0.1); + --arena-accent: #5b9bff; + --arena-accent-hover: #7db0ff; + --arena-accent-rgb: 91, 155, 255; + --arena-bg: #0a0d13; + --arena-surface: rgba(18, 23, 32, 0.86); + --arena-surface-solid: #12171f; + --arena-elevated: rgba(28, 34, 45, 0.92); + --arena-border: rgba(255, 255, 255, 0.09); + --arena-border-strong: rgba(255, 255, 255, 0.16); + --arena-text: #e2e8f2; + --arena-text-muted: #94a1b5; + --arena-radius: 10px; + --arena-radius-sm: 7px; + --arena-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); + --arena-ease: cubic-bezier(0.16, 1, 0.3, 1); +} + +/* Retint Bootstrap's dark theme so every .btn / .form-control / .nav / .table + inherits the palette instead of being restyled one component at a time. */ +[data-bs-theme='dark'] { + --bs-body-bg: #0a0d13; + --bs-body-color: #e2e8f2; + --bs-secondary-color: #94a1b5; + --bs-tertiary-bg: rgba(18, 23, 32, 0.86); + --bs-border-color: rgba(255, 255, 255, 0.12); + --bs-primary: #5b9bff; + --bs-primary-rgb: 91, 155, 255; + --bs-link-color: #7db0ff; + --bs-link-color-rgb: 125, 176, 255; + --bs-link-hover-color: #a3c6ff; + --bs-body-font-family: + system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; } body { - font-family: - system-ui, - -apple-system, - 'Segoe UI', - Roboto, - 'Helvetica Neue', - sans-serif; + font-family: var(--bs-body-font-family); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; } /* Frosted navbar so the canvas glows through without hurting legibility. */ #navBar { background-color: var(--arena-surface) !important; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(14px) saturate(140%); + -webkit-backdrop-filter: blur(14px) saturate(140%); border-bottom: 1px solid var(--arena-border); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.4); + min-height: 56px; +} + +#navBar .navbar-nav .nav-link { + font-size: 0.9rem; + font-weight: 500; + color: var(--arena-text-muted); + padding: 0.4rem 0.72rem; + border-radius: var(--arena-radius-sm); + transition: + color 0.15s var(--arena-ease), + background-color 0.15s var(--arena-ease); +} + +#navBar .navbar-nav .nav-link:hover { + color: var(--arena-text); + background-color: rgba(255, 255, 255, 0.05); +} + +#navBar .navbar-nav .nav-link.active { + color: #fff; + background-color: rgba(var(--arena-accent-rgb), 0.16); + box-shadow: inset 0 0 0 1px rgba(var(--arena-accent-rgb), 0.4); +} + +/* Undo / redo group */ +#navBar .btn-outline-secondary { + --bs-btn-color: var(--arena-text-muted); + --bs-btn-border-color: var(--arena-border-strong); + --bs-btn-hover-bg: rgba(255, 255, 255, 0.06); + --bs-btn-hover-border-color: var(--arena-accent); + --bs-btn-hover-color: var(--arena-text); + font-weight: 500; +} + +/* Shared component polish -------------------------------------------------- */ +.btn { + --bs-btn-font-weight: 500; + border-radius: var(--arena-radius-sm); + transition: + transform 0.12s var(--arena-ease), + filter 0.15s var(--arena-ease), + background-color 0.15s var(--arena-ease), + box-shadow 0.15s var(--arena-ease); +} + +.btn-primary { + --bs-btn-bg: var(--arena-accent); + --bs-btn-border-color: var(--arena-accent); + --bs-btn-hover-bg: var(--arena-accent-hover); + --bs-btn-hover-border-color: var(--arena-accent-hover); + --bs-btn-active-bg: #4a88ec; + --bs-btn-color: #06101f; + --bs-btn-hover-color: #06101f; + box-shadow: 0 4px 14px rgba(var(--arena-accent-rgb), 0.28); +} + +.btn:not(:disabled):hover { + transform: translateY(-1px); +} + +.btn:not(:disabled):active { + transform: translateY(0) scale(0.985); +} + +.form-control, +.form-select { + background-color: rgba(255, 255, 255, 0.03); + border-color: var(--arena-border-strong); + border-radius: var(--arena-radius-sm); + color: var(--arena-text); + transition: + border-color 0.15s var(--arena-ease), + box-shadow 0.15s var(--arena-ease); +} + +.form-control:focus, +.form-select:focus { + background-color: rgba(255, 255, 255, 0.04); + border-color: var(--arena-accent); + box-shadow: 0 0 0 3px rgba(var(--arena-accent-rgb), 0.22); +} + +.form-control::file-selector-button { + background-color: rgba(var(--arena-accent-rgb), 0.14); + color: var(--arena-text); + border: 0; + border-right: 1px solid var(--arena-border); +} + +.form-label { + font-size: 0.82rem; + font-weight: 600; + letter-spacing: 0.01em; + color: var(--arena-text-muted); + margin-bottom: 0.35rem; +} + +.form-check-input:checked { + background-color: var(--arena-accent); + border-color: var(--arena-accent); +} + +.form-check-input:focus { + border-color: var(--arena-accent); + box-shadow: 0 0 0 3px rgba(var(--arena-accent-rgb), 0.22); +} + +.form-range::-webkit-slider-thumb { + background: var(--arena-accent); +} +.form-range::-moz-range-thumb { + background: var(--arena-accent); } :focus-visible { @@ -33,6 +172,14 @@ body { outline-offset: 2px; } +@media (prefers-reduced-motion: reduce) { + * { + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + } +} + #app { position: fixed; inset: 0; From 422619503d05983f28b25c501e9ae90a25e0029f Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 10:01:26 +0100 Subject: [PATCH 04/20] feat(ui): open action panels as a side drawer, not full-scene overlay All panels except Home/Help now slide in as a right-side drawer so the 3D scene stays visible and interactive while operating on it. Co-Authored-By: Claude Opus 4.8 --- frontend/src/style.css | 52 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/frontend/src/style.css b/frontend/src/style.css index 4a0690a..2ae3afb 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -186,20 +186,58 @@ body { z-index: 0; } -/* Panels overlay the canvas below the fixed navbar. */ +/* Action panels open as a right-side drawer so the 3D scene stays visible; + only Home and Help take the full view (they are reading, not operating on + the scene). Main View's pane is display:none, so the canvas shows through. */ #panelContent .tab-pane { position: fixed; top: 56px; /* navbar height */ - left: 0; right: 0; bottom: 0; + width: min(440px, 94vw); overflow-y: auto; - /* Above the #info / nav-control overlays (z-index 2) so an open panel - covers them; Main View's pane is display:none, so the canvas + controls - stay visible there. */ - z-index: 5; + overscroll-behavior: contain; + z-index: 5; /* above the #info / nav-control overlays (z-index 2) */ + background-color: var(--arena-surface); + backdrop-filter: blur(16px) saturate(140%); + -webkit-backdrop-filter: blur(16px) saturate(140%); + border-left: 1px solid var(--arena-border); + box-shadow: var(--arena-shadow); + padding: 1.4rem 1.35rem calc(1.4rem + env(safe-area-inset-bottom, 0px)); +} + +/* Slide the drawer in when it becomes the active pane. */ +#panelContent .tab-pane.show { + animation: drawer-in 0.28s var(--arena-ease); +} + +@keyframes drawer-in { + from { + transform: translateX(16px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +/* Home and Help are full-width reading surfaces, not scene controls. */ +#panel-home, +#panel-help { + left: 0; + width: auto; background-color: var(--bs-body-bg); - padding: 1rem; + backdrop-filter: none; + -webkit-backdrop-filter: none; + border-left: 0; + box-shadow: none; + padding: 1.6rem 2rem; +} + +/* Wide data tables get a roomier drawer. */ +#panel-data { + width: min(680px, 96vw); } /* Main View has no panel content: keep it invisible so the canvas shows. */ From d5e342d4957002462a1e737e40eaa3dbf195fec7 Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 10:03:31 +0100 Subject: [PATCH 05/20] feat(ui): make panel content fit the drawer width Neutralize responsive column wrappers inside drawers and rebuild the per-layer selection into tidy rows with aligned Hide/Labels toggles. Co-Authored-By: Claude Opus 4.8 --- frontend/src/style.css | 53 ++++++++++++++++++++++++++++++++++++++ frontend/src/ui/layouts.ts | 12 ++++----- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/frontend/src/style.css b/frontend/src/style.css index 2ae3afb..db68db2 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -240,6 +240,59 @@ body { width: min(680px, 96vw); } +/* Inside a drawer the old responsive column wrappers would shrink content to a + fraction of the (already narrow) drawer — let panel content use full width. */ +#panelContent .tab-pane:not(#panel-home):not(#panel-help) [class*='col-'] { + flex: 0 0 100%; + max-width: 100%; + width: 100%; + padding-left: 0; + padding-right: 0; +} + +/* Per-layer selection rows: name on the left, Hide/Labels toggles on the right, + never wrapping into an unreadable pile in the narrow drawer. */ +.layer-row { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 0.55rem; + margin-bottom: 0.3rem; + border: 1px solid var(--arena-border); + border-radius: var(--arena-radius-sm); + background: rgba(255, 255, 255, 0.02); + transition: border-color 0.15s var(--arena-ease); +} + +.layer-row:hover { + border-color: var(--arena-border-strong); +} + +.layer-row__name { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 500; + margin: 0; +} + +.layer-row__opt { + display: inline-flex; + align-items: center; + gap: 0.3rem; + flex: 0 0 auto; + font-size: 0.8rem; + color: var(--arena-text-muted); + margin: 0; + cursor: pointer; +} + +.layer-row__opt .form-check-input { + margin: 0; +} + /* Main View has no panel content: keep it invisible so the canvas shows. */ #panel-main-view { display: none !important; diff --git a/frontend/src/ui/layouts.ts b/frontend/src/ui/layouts.ts index a9aa9c7..f9f8177 100644 --- a/frontend/src/ui/layouts.ts +++ b/frontend/src/ui/layouts.ts @@ -102,14 +102,14 @@ function buildLayerCheckboxes(): void { container.innerHTML = '' ctx.layers.forEach((layer, i) => { const row = document.createElement('div') - row.className = 'form-check-inline' + row.className = 'layer-row' row.innerHTML = ` - - - - - + + + ` const [sel, hide, labels] = row.querySelectorAll('input') sel.addEventListener('change', () => selectLayer(i, sel.checked)) From 18a3e8185d1257b356ee50bef5da63fae16b45db Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 10:06:39 +0100 Subject: [PATCH 06/20] feat(ui): redesign Home panel with hero, action cards, quiet citations Restructure into a hero header, figure + readable intro grid, quick-start action cards, and a de-emphasized citation block. Fix drawer specificity so Home/Help render full-width. Co-Authored-By: Claude Opus 4.8 --- frontend/src/style.css | 141 ++++++++++++++++++++++++++++++++++++++-- frontend/src/ui/home.ts | 81 +++++++++++++---------- 2 files changed, 183 insertions(+), 39 deletions(-) diff --git a/frontend/src/style.css b/frontend/src/style.css index db68db2..a9bcd11 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -222,9 +222,10 @@ body { } } -/* Home and Help are full-width reading surfaces, not scene controls. */ -#panel-home, -#panel-help { +/* Home and Help are full-width reading surfaces, not scene controls. + Selector carries two ids to out-specify the base .tab-pane drawer rule. */ +#panelContent #panel-home, +#panelContent #panel-help { left: 0; width: auto; background-color: var(--bs-body-bg); @@ -613,9 +614,137 @@ footer a:hover { #logo1 { background-image: url(/images/logo.png); background-repeat: no-repeat; - width: 200px; - height: 48px; - margin-bottom: 10px; + background-size: contain; + width: 220px; + height: 52px; +} + +/* Home panel ---------------------------------------------------------------- */ +.home-wrap { + max-width: 1080px; + margin: 0 auto; + padding-bottom: 4rem; +} + +.home-hero { + padding: 0.5rem 0 1.75rem; + border-bottom: 1px solid var(--arena-border); + margin-bottom: 2rem; +} + +.home-tagline { + margin: 0.75rem 0 0; + font-size: 1.05rem; + color: var(--arena-text-muted); + letter-spacing: 0.01em; +} + +.home-grid { + display: grid; + grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); + gap: 2.5rem; + align-items: start; +} + +.home-figure { + margin: 0; + border: 1px solid var(--arena-border); + border-radius: var(--arena-radius); + overflow: hidden; + background: rgba(255, 255, 255, 0.02); +} + +.home-figure img { + display: block; + width: 100%; +} + +.home-intro p { + font-size: 1rem; + line-height: 1.7; + color: var(--arena-text); + max-width: 62ch; +} + +.home-actions { + display: grid; + gap: 0.75rem; + margin-top: 1.5rem; +} + +.home-action { + display: block; + padding: 0.85rem 1.05rem; + border: 1px solid var(--arena-border-strong); + border-radius: var(--arena-radius); + text-decoration: none; + background: rgba(255, 255, 255, 0.02); + transition: + transform 0.15s var(--arena-ease), + border-color 0.15s var(--arena-ease), + background-color 0.15s var(--arena-ease); +} + +.home-action:hover { + transform: translateY(-2px); + border-color: var(--arena-accent); + background: rgba(var(--arena-accent-rgb), 0.08); +} + +.home-action--primary { + border-color: rgba(var(--arena-accent-rgb), 0.55); + background: rgba(var(--arena-accent-rgb), 0.12); +} + +.home-action__title { + display: block; + font-weight: 600; + color: var(--arena-text); +} + +.home-action__sub { + display: block; + font-size: 0.85rem; + color: var(--arena-text-muted); + margin-top: 0.15rem; +} + +.home-cite { + margin-top: 2.75rem; + padding: 1.5rem 1.6rem; + border: 1px solid var(--arena-border); + border-radius: var(--arena-radius); + background: rgba(255, 255, 255, 0.015); +} + +.home-cite__heading { + font-size: 0.82rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--arena-text-muted); + margin: 0 0 1rem; +} + +.home-cite p { + font-size: 0.9rem; + line-height: 1.6; + color: var(--arena-text-muted); + margin-bottom: 1rem; +} + +.home-cite p:last-child { + margin-bottom: 0; +} + +@media (max-width: 768px) { + #panel-home { + padding: 1.25rem 1rem; + } + .home-grid { + grid-template-columns: 1fr; + gap: 1.5rem; + } } /* Help panel — inner tab bar + content (port of v2 arena3dweb.css help rules). */ diff --git a/frontend/src/ui/home.ts b/frontend/src/ui/home.ts index 660d3c6..0061bee 100644 --- a/frontend/src/ui/home.ts +++ b/frontend/src/ui/home.ts @@ -3,45 +3,60 @@ import { showTab } from './tabs' const HOME_HTML = ` -
-
-
- Main View +
+
+
+

Interactive 3D visualization of multilayered networks

+
+ +
+
+ A multilayered network rendered across stacked 3D layers +
+
+

+ Arena3Dweb is the first fully interactive, dependency-free web application for + visualizing multi-layered graphs in 3D space. Integrate multiple networks in a single view + with their intra- and inter-layer connections, apply a wide range of layout algorithms to + selected layers individually or in combination, and highlight topological features. Layers + and the whole scene can be translated, rotated, and scaled, while node position, color, and + size adjust on-the-fly. The current version supports weighted and unweighted undirected graphs. +

+ + +
-
-

- Arena3Dweb is the first, fully interactive and dependency-free, web application which allows the - visualization of multi-layered graphs in 3D space. With Arena3Dweb, users can integrate multiple networks - in a single view along with their intra- and inter-layer connections. For clearer and more informative views, users - can choose between a plethora of layout algorithms and apply them on a set of selected layers either individually or - in combination. Users can align networks and highlight node topological features, whereas each layer as well as the - whole scene can be translated, rotated and scaled in 3D space. User-selected edge colors can be used to highlight - important paths, while node positioning, coloring and resizing can be adjusted on-the-fly. In its current version, - Arena3Dweb supports weighted and unweighted undirected graphs. -

- → To start using Arena3Dweb click on the File menu tab and upload a network. -

- → Visit the Help/Examples page to download some Arena3Dweb example files. -

- → Get started by uploading your networks here. -

+ +
+

Please cite Arena3Dweb

- Please acknowledge the use of Arena3Dweb by citing the following publications:

- - Kokoli, M., Karatzas, E., Baltoumas, F.A., Schneider, R., Pafilis, E., Paragkamian, S., Doncheva, N.T., Jensen, L.J. - and Pavlopoulos, G., 2022.
+ Kokoli, M., Karatzas, E., Baltoumas, F.A., Schneider, R., Pafilis, E., Paragkamian, S., Doncheva, N.T., Jensen, L.J. + and Pavlopoulos, G., 2022. Arena3Dweb: interactive 3D visualization of multilayered networks - supporting multiple directional information channels, clustering analysis and application integration,
- NAR Genomics and Bioinformatics, 5(2), p.lqad053.
- doi: https://doi.org/10.1093/nargab/lqad053; + supporting multiple directional information channels, clustering analysis and application integration. + NAR Genomics and Bioinformatics, 5(2), p.lqad053.
+ doi: 10.1093/nargab/lqad053 +  ·  PubMed: 37260509 -

- - Karatzas, E., Baltoumas, F.A., Panayiotou, N.A., Schneider, R. and Pavlopoulos, G.A., 2021.
- Arena3Dweb: Interactive 3D visualization of multilayered networks,
- Nucleic Acids Research, 49(W1), pp.W36-W45.
- doi: https://doi.org/10.1093/nar/gkab278; +

+

+ Karatzas, E., Baltoumas, F.A., Panayiotou, N.A., Schneider, R. and Pavlopoulos, G.A., 2021. + Arena3Dweb: Interactive 3D visualization of multilayered networks. + Nucleic Acids Research, 49(W1), pp.W36-W45.
+ doi: 10.1093/nar/gkab278 +  ·  PubMed: 33885790

-
+
` From 6088be8291eacc4f69331b4fad350cc14131e54b Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 10:07:54 +0100 Subject: [PATCH 07/20] feat(ui): theme the Help panel tab bar and content for dark mode Replace the light-on-dark tab strip with accent pill tabs and restyle headings, code blocks, images, and links to match the dark chrome. Co-Authored-By: Claude Opus 4.8 --- frontend/src/style.css | 67 ++++++++++++++++++++++++++++++------------ 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/frontend/src/style.css b/frontend/src/style.css index a9bcd11..a148fd8 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -747,55 +747,84 @@ footer a:hover { } } -/* Help panel — inner tab bar + content (port of v2 arena3dweb.css help rules). */ +/* Help panel — inner tab bar + content, themed to match the dark chrome. */ +#helpDiv { + max-width: 1080px; + margin: 0 auto; +} + #helpDiv .tab { - overflow: hidden; - background-color: #f1f1f1; + display: flex; + flex-wrap: wrap; + gap: 0.4rem; width: 100%; + padding-bottom: 1.25rem; + margin-bottom: 1.5rem; + border-bottom: 1px solid var(--arena-border); } #helpDiv .tab button { - background-color: inherit; - float: left; - border: none; + background-color: rgba(255, 255, 255, 0.03); + border: 1px solid var(--arena-border); + border-radius: 999px; outline: none; cursor: pointer; - padding: 14px 16px; - transition: 0.3s; - font-size: 17px; - color: #000; + padding: 0.4rem 0.9rem; + font-size: 0.88rem; + font-weight: 500; + color: var(--arena-text-muted); + transition: + color 0.15s var(--arena-ease), + background-color 0.15s var(--arena-ease), + border-color 0.15s var(--arena-ease); } #helpDiv .tab button:hover { - background-color: #ddd; + color: var(--arena-text); + border-color: var(--arena-border-strong); + background-color: rgba(255, 255, 255, 0.06); } #helpDiv .tab button.active { - background-color: #ccc; + color: #06101f; + background-color: var(--arena-accent); + border-color: var(--arena-accent); } #helpDiv .tabcontent { display: none; - padding: 1%; width: 100%; + max-width: 78ch; + line-height: 1.65; word-break: break-word; - text-align: justify; +} + +#helpDiv .tabcontent h2, +#helpDiv .tabcontent h3 { + font-weight: 600; + letter-spacing: -0.01em; + margin-top: 1.75rem; } #helpDiv .tabcontent img { max-width: 100%; + border: 1px solid var(--arena-border); + border-radius: var(--arena-radius-sm); } #helpDiv pre { overflow-x: auto; - padding: 8px; - background-color: rgba(128, 128, 128, 0.15); + padding: 0.85rem 1rem; + border: 1px solid var(--arena-border); + border-radius: var(--arena-radius-sm); + background-color: rgba(255, 255, 255, 0.04); + color: var(--arena-text); } #helpDiv .numbering { - color: red; - font-weight: bold; - font-size: 18px; + color: var(--arena-accent); + font-weight: 700; + font-size: 1.05rem; } #helpDiv .indent { From 6dea96c7c2a2d3afce28cb6f0666a8ef589ac37f Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 10:10:29 +0100 Subject: [PATCH 08/20] feat(ui): polish scene overlays and add a centered empty state Turn the bare "waiting" hint into a centered empty-state card, restyle the navigation-controls headings/dividers and Recenter button, and replace the raw-red node-description close button with a themed control. Co-Authored-By: Claude Opus 4.8 --- frontend/index.html | 5 ++- frontend/src/style.css | 90 +++++++++++++++++++++++++++++++++++------- 2 files changed, 80 insertions(+), 15 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index b193499..a63ac42 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -188,7 +188,10 @@
-
Waiting for Network to be uploaded.
+
+ No network loaded yet. Open the File tab to upload a network or load + an example. +
diff --git a/frontend/src/style.css b/frontend/src/style.css index a148fd8..f951637 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -349,13 +349,30 @@ body { -webkit-backdrop-filter: blur(8px); } -/* The overlay hides while empty (before a network is loaded it only shows - the waiting hint, which should stay a quiet unboxed line). */ +/* Before a network is loaded, #info holds only the waiting hint: present it as + a centered empty-state card over the canvas instead of a top-left line. */ #info:not(:has(table)) { - background: none; - border: none; - backdrop-filter: none; - -webkit-backdrop-filter: none; + top: 50%; + left: 50%; + right: auto; + transform: translate(-50%, -50%); + width: auto; + max-width: 340px; + max-height: none; + text-align: center; + font-size: 14px; + line-height: 1.6; + color: var(--arena-text-muted); + padding: 1.5rem 1.75rem; + border-radius: var(--arena-radius); +} + +#info:not(:has(table))::before { + content: '◇'; + display: block; + font-size: 1.6rem; + color: var(--arena-accent); + margin-bottom: 0.5rem; } /* #info itself stays click-through (hint text); the controls inside accept @@ -367,10 +384,43 @@ body { #canvasControls_table { color: var(--bs-body-color); + width: 100%; + margin-top: 0.5rem; +} + +#canvasControls_table h5 { + font-size: 0.95rem; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--arena-text); + margin: 0.35rem 0; +} + +#canvasControls_table #recenterButton { + width: 100%; + margin: 0.25rem 0 0.5rem; + padding: 0.35rem 0.6rem; + font-size: 0.82rem; + color: var(--arena-text); + background: rgba(var(--arena-accent-rgb), 0.14); + border: 1px solid rgba(var(--arena-accent-rgb), 0.4); + border-radius: var(--arena-radius-sm); + transition: + background-color 0.15s var(--arena-ease), + border-color 0.15s var(--arena-ease); +} + +#canvasControls_table #recenterButton:hover { + background: rgba(var(--arena-accent-rgb), 0.24); + border-color: var(--arena-accent); } .border_tr { - border-top: 10px solid lightgray; + border-top: 1px solid var(--arena-border-strong); +} + +.border_tr td { + padding-top: 0.5rem; } .labelDrop { @@ -570,19 +620,31 @@ body { #closeButton { position: absolute; - right: 0; - background-color: red; - border: 1px solid gray; - border-radius: 5px; + top: 8px; + right: 8px; + width: 26px; + height: 26px; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--arena-text-muted); + background-color: rgba(255, 255, 255, 0.06); + border: 1px solid var(--arena-border); + border-radius: var(--arena-radius-sm); + transition: + color 0.15s var(--arena-ease), + background-color 0.15s var(--arena-ease), + border-color 0.15s var(--arena-ease); } #closeButton:hover { - background-color: #ff756b; - border: 1px solid white; + color: #fff; + background-color: rgba(220, 70, 60, 0.85); + border-color: rgba(220, 70, 60, 0.9); } #closeButton:active { - background-color: #ab150a; + background-color: rgba(180, 40, 32, 0.9); } /* Footer — quiet translucent bar instead of v2's solid blue banner. */ From 19f0f69b6614f6d3114f2a71a4a4e285ea43559a Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 10:14:54 +0100 Subject: [PATCH 09/20] feat(ui): responsive nav and drawer clearance Auto-collapse the mobile navbar menu after picking a tab so the chosen panel isn't buried, and pad drawers to clear the fixed footer bar. Co-Authored-By: Claude Opus 4.8 --- frontend/src/event_listeners.ts | 16 ++++++++++++++++ frontend/src/style.css | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/frontend/src/event_listeners.ts b/frontend/src/event_listeners.ts index 14c5fdf..fe76e22 100644 --- a/frontend/src/event_listeners.ts +++ b/frontend/src/event_listeners.ts @@ -5,6 +5,7 @@ // sceneZoom, keyPressed, right-click menu) live in canvas_controls — deferred // until that action lands. Only the window-level listeners are wired here. +import { Collapse } from 'bootstrap' import { bus } from './bus' import { history } from './commands/base' import { resetScreen } from './actions/screen' @@ -13,6 +14,21 @@ export function registerGlobalListeners(): void { window.addEventListener('resize', resetScreen) window.addEventListener('keydown', handleUndoRedo) wireHistoryButtons() + wireMobileNavCollapse() +} + +// On small screens the navbar is a collapsed menu; picking a tab should close +// it so the chosen panel/scene isn't left buried under the open menu. +function wireMobileNavCollapse(): void { + const menu = document.getElementById('navbarTabs') + if (!menu) return + for (const link of menu.querySelectorAll('.navbar-nav .nav-link')) { + link.addEventListener('click', () => { + if (menu.classList.contains('show')) { + Collapse.getOrCreateInstance(menu).hide() + } + }) + } } function wireHistoryButtons(): void { diff --git a/frontend/src/style.css b/frontend/src/style.css index f951637..865f555 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -203,7 +203,8 @@ body { -webkit-backdrop-filter: blur(16px) saturate(140%); border-left: 1px solid var(--arena-border); box-shadow: var(--arena-shadow); - padding: 1.4rem 1.35rem calc(1.4rem + env(safe-area-inset-bottom, 0px)); + /* extra bottom padding clears the fixed footer bar */ + padding: 1.4rem 1.35rem calc(3rem + env(safe-area-inset-bottom, 0px)); } /* Slide the drawer in when it becomes the active pane. */ From 8559a7ea032c4a064a0787c031540e9281a27370 Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 14:06:20 +0100 Subject: [PATCH 10/20] docs: add REPORT_TASTE.md summarizing the UI/UX redesign Co-Authored-By: Claude Opus 4.8 --- REPORT_TASTE.md | 93 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 REPORT_TASTE.md diff --git a/REPORT_TASTE.md b/REPORT_TASTE.md new file mode 100644 index 0000000..6686faa --- /dev/null +++ b/REPORT_TASTE.md @@ -0,0 +1,93 @@ +# Arena3Dweb — Frontend UI/UX Redesign (taste-skill) + +**Design read:** Redesign-*preserve* of a scientific 3D-network visualization +tool for bioinformatics researchers. Dark "instrument" canvas app built on +Bootstrap 5 + Vite/TypeScript/Three.js. Direction: cohesive dark instrument +panel, single cool-blue accent, restrained motion, strong hierarchy — without +touching any backend contract, route, data flow, or feature. + +Approach followed the token-saviour + ponytail path: **kept Bootstrap** and +*retinted its dark tokens* instead of ripping the framework out or rewriting to +React. Most changes are CSS + small, surgical markup edits. One feature per +commit, no push. + +--- + +## Summary of improvements + +1. **Unified design foundation.** Retinted Bootstrap's dark theme to one cool + palette + accent so every `.btn` / `.form-control` / `.nav` / `.table` + inherits the system. Refined navbar (frosted, active-tab pill), buttons + (accent fill, tactile `:active` press), inputs (focus ring), `:focus-visible` + rings, and a global `prefers-reduced-motion` guard. + +2. **Side-drawer panels (key UX fix).** Action panels used to overlay the whole + 3D scene. They now slide in as a right-side drawer (`min(440px, 94vw)`, wider + for the data table) so the scene stays visible and interactive while you + operate on it. **Only Home and Help remain full-width**, per the explicit + constraint. Drawer has a slide-in animation (reduced-motion aware). + +3. **Panel content fits the drawer.** Neutralized the now-pointless responsive + column wrappers inside drawers, and rebuilt the cramped per-layer checkbox + group into tidy bordered rows with a truncating layer name and aligned + Hide / Labels toggles. + +4. **Home panel redesign.** Hero header with tagline, a figure + readable-measure + intro grid, two quick-start action cards (Upload / Examples), and a + de-emphasized citation block. Collapses to one column on mobile. + +5. **Help panel themed.** Replaced the light-on-dark tab strip with accent pill + tabs; restyled headings, code blocks, images, and links for the dark chrome + and a comfortable reading measure. + +6. **Scene overlays + empty state.** The bare "waiting for network" line is now a + centered empty-state card. Restyled the navigation-controls headings and + dividers, gave the Recenter button an accent treatment, and replaced the + raw-red node-description close button with a themed control. + +7. **Responsive behavior.** The mobile navbar menu now auto-collapses after a tab + is picked (so the chosen panel isn't left buried under an open menu), and + drawers pad their bottom to clear the fixed footer. Verified at desktop + (1440), tablet (820), and mobile (390). + +## Files changed + +| File | Change | +|------|--------| +| `frontend/src/style.css` | Design tokens, Bootstrap retint, component polish, drawer system, layer rows, Home/Help/overlay/empty-state styling | +| `frontend/index.html` | Friendlier empty-state copy | +| `frontend/src/ui/home.ts` | Home panel markup restructured (IDs preserved) | +| `frontend/src/ui/layouts.ts` | Per-layer selection rebuilt as clean rows (JS wiring unchanged) | +| `frontend/src/event_listeners.ts` | Auto-collapse mobile navbar on tab select | + +No backend files, API endpoints, request/response shapes, routes, or auth logic +were touched. + +## Validation commands run + +- `npx tsc --noEmit` — clean (after each change) +- `npm run lint` (eslint) — clean +- `npx vitest run` — **106 passed (15 files)** +- `npx playwright test` — **1 passed** (existing `network-flow` E2E) + +## Playwright flows tested (baseline + post-change screenshots) + +- Home → File → **Load Example** → Main View (scene renders) +- Layer Selection: **Select All Layers → choose Circle layout → Run** ("Layout + applied", scene re-renders) +- Every panel visited (Scene / Layer / Node / Edge / View Data / FPS / Help) +- Empty state (no network) and loaded-controls state +- Mobile (390) + tablet (820): navbar collapse, drawer, home stack +- **Console errors: 0** across the full sweep + +## Known limitations / follow-ups + +- On phones the drawer is ~94vw, so it necessarily covers most of the scene — + unavoidable at that width; the desktop/tablet constraint (don't cover the + scene) is fully met. +- Node/edge attribute *content* (help tables, node description body) was themed + but not re-laid-out; a future pass could card-ify the longer help spec tables. +- The nav-control image sprites (rotation arrows, expand/collapse) are the + original PNGs — left as-is to avoid asset churn; could become inline SVG later + for crispness on HiDPI. +- No new fonts were added (system-ui stack kept) to avoid a dependency/asset add. From e893d59641b6276ad0d88216f76d90183376c75e Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 14:30:51 +0100 Subject: [PATCH 11/20] feat(ui): redesign nav help as a shortcut cheat-sheet with kbd keys Co-Authored-By: Claude Fable 5 --- frontend/src/actions/nav_controls.ts | 20 ++++++++------ frontend/src/style.css | 41 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/frontend/src/actions/nav_controls.ts b/frontend/src/actions/nav_controls.ts index f03f32e..01c62cf 100644 --- a/frontend/src/actions/nav_controls.ts +++ b/frontend/src/actions/nav_controls.ts @@ -183,15 +183,17 @@ function axisRowsHtml( } const INFO_HTML = ` - 1. Zoom: Mouse Wheel
- 2. Pan: Click Drag Scene / Arrow Keys
- 3. Orbit: Mouse Middle Drag
- 4. Drag Layer: Click Drag
- 5. Rotate Layer: Z / X / C + Click Drag
- 6. Move Selected Nodes: Z / C + Click Drag
- 7. Node/Layer Selection: Double Click
- 8. Lasso Nodes: Shift + Click Drag
- 9. Unselect All Nodes: Double Click Scene + diff --git a/frontend/src/style.css b/frontend/src/style.css index 865f555..ef47851 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -383,6 +383,47 @@ body { pointer-events: auto; } +/* Navigation shortcut cheat-sheet: action left, keys right. */ +#navShortcuts { + list-style: none; + margin: 0; + padding: 0; +} + +#navShortcuts li { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + padding: 3px 0; + color: var(--arena-text-muted); +} + +#navShortcuts li span:last-child { + white-space: nowrap; + text-align: right; +} + +#navShortcuts li + li { + border-top: 1px solid var(--arena-border); +} + +#navShortcuts kbd { + display: inline-block; + padding: 1px 5px; + font-size: 10px; + line-height: 1.5; + color: var(--arena-text); + background: var(--arena-elevated); + border: 1px solid var(--arena-border-strong); + border-bottom-width: 2px; + border-radius: 4px; +} + +#navShortcuts kbd + kbd { + margin-left: 3px; +} + #canvasControls_table { color: var(--bs-body-color); width: 100%; From 9c64a5c0b7855044890d5e2feb64370a7033d4bf Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 14:33:20 +0100 Subject: [PATCH 12/20] feat(ui): polish canvas controls table with hover states and themed sliders Co-Authored-By: Claude Fable 5 --- frontend/src/actions/nav_controls.ts | 8 ++++---- frontend/src/style.css | 26 ++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/frontend/src/actions/nav_controls.ts b/frontend/src/actions/nav_controls.ts index 01c62cf..be8af95 100644 --- a/frontend/src/actions/nav_controls.ts +++ b/frontend/src/actions/nav_controls.ts @@ -196,13 +196,13 @@ const INFO_HTML = `
Scene
Rotation Controls
- + ${axisRowsHtml('sliderValue1', 'Angle: 5˚', 'sceneRotateSlider', { min: 1, max: 15, value: 5, step: 1 }, '')} - + ${axisRowsHtml('sliderValue2', 'Angle: 5˚', 'layerRotateSlider', { min: 1, max: 15, value: 5, step: 1 }, '')} - + @@ -216,7 +216,7 @@ const INFO_HTML = ` - + diff --git a/frontend/src/style.css b/frontend/src/style.css index ef47851..8de2253 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -465,8 +465,24 @@ body { padding-top: 0.5rem; } +.controlsSubLabel { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--arena-text-muted); + padding: 0.1rem 0 0.3rem; +} + +.canvasSlider { + width: 100%; + accent-color: var(--arena-accent); + cursor: pointer; +} + .labelDrop { vertical-align: bottom; + color: var(--arena-text-muted); + font-variant-numeric: tabular-nums; } .raiseSlider { @@ -494,6 +510,16 @@ body { width: 40px; height: 40px; cursor: pointer; + border-radius: var(--arena-radius-sm); + transition: background-color 0.15s var(--arena-ease); +} + +.canvasControls:hover { + background-color: rgba(var(--arena-accent-rgb), 0.12); +} + +.canvasControls:active { + background-color: rgba(var(--arena-accent-rgb), 0.22); } .image_expandLayers, From f9b12240ca97e5168ac9bff17fc0b7512b86384b Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 14:53:53 +0100 Subject: [PATCH 13/20] fix(ui): stop nav-control button clipping and dead info scrollbar navControlButtonsDiv shared #info's z-index, so #info's top edge clipped the bottom of the "Navigation Controls" button when both overlapped by a few px. Give the buttons a higher z-index so they always render above the panel below them. #info also carried pointer-events: none unconditionally (meant only for the pre-load hint state), so its own scrollbar swallowed no clicks and drags fell through to the canvas, dragging the scene instead of scrolling. Pointer-events: none now applies only while #info holds no table (the hint state); once the controls table is present the panel is fully interactive. Co-Authored-By: Claude Sonnet 5 --- frontend/src/style.css | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/style.css b/frontend/src/style.css index 8de2253..9561b94 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -307,17 +307,17 @@ body { z-index: 2; font-size: 12px; color: var(--bs-body-color); - pointer-events: none; /* passive hint text — must not block panel clicks */ } /* Nav control buttons + Scene/Layers/Nodes control table (port of v2 arena3dweb.css canvas-control rules; positions adapted to the fixed - navbar). */ + navbar). Higher z-index than #info so the toggle/pause buttons are never + clipped by the controls panel below them, regardless of exact heights. */ #navControlButtonsDiv { position: fixed; top: 64px; left: 10px; - z-index: 2; + z-index: 3; } .displayCanvasControls { @@ -376,11 +376,11 @@ body { margin-bottom: 0.5rem; } -/* #info itself stays click-through (hint text); the controls inside accept - clicks. */ -#info table, -#info button { - pointer-events: auto; +/* Before a network loads, #info is just passive hint text and must not block + canvas interaction. Once it holds the controls table it becomes a real + panel — including its own scrollbar — so it needs normal pointer events. */ +#info:not(:has(table)) { + pointer-events: none; } /* Navigation shortcut cheat-sheet: action left, keys right. */ From 73355e28b974026366d869be0d2be7bd59866fe3 Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 15:05:13 +0100 Subject: [PATCH 14/20] feat(ui): real stop/play and up/down icons on nav toggle buttons interLayerEdgesRenderPauseButton's label never actually reflected ctx.interLayerEdgesRenderPauseFlag (a stub noted in edge.ts). It now swaps a bars/play icon + label in sync with the flag on every click. displayCanvasControlsButton gets a chevron that flips between open and closed state instead of a static label with no visual affordance. Co-Authored-By: Claude Sonnet 5 --- frontend/src/actions/nav_controls.ts | 44 ++++++++++++++++++++++++---- frontend/src/style.css | 25 ++++++++++++++++ 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/frontend/src/actions/nav_controls.ts b/frontend/src/actions/nav_controls.ts index be8af95..b37ab6e 100644 --- a/frontend/src/actions/nav_controls.ts +++ b/frontend/src/actions/nav_controls.ts @@ -244,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 + ? 'Render Inter-Layer Edges' + : '❚❚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 = `${open ? '▴' : '▾'}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 { @@ -259,20 +288,25 @@ function attachNavControls(): void { if (!buttonsDiv || !info) return buttonsDiv.innerHTML = ` -
- ` + + ` 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')! diff --git a/frontend/src/style.css b/frontend/src/style.css index 9561b94..ce315b5 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -337,6 +337,31 @@ body { border-color: var(--arena-accent); } +/* Icon + label toggle buttons (render-pause, controls disclosure): the icon + swaps per state instead of the label alone carrying the meaning. */ +.nav-toggle-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 0.4rem; +} + +.nav-toggle-icon { + color: var(--arena-accent); + font-size: 10px; + line-height: 1; +} + +/* Paused = an actual stopped state worth calling out, not just a hover tint. */ +.nav-toggle-btn.is-paused { + border-color: rgba(var(--arena-accent-rgb), 0.5); + background-color: rgba(var(--arena-accent-rgb), 0.1); +} + +.nav-toggle-btn.is-paused .nav-toggle-icon { + color: #ffb454; +} + #info { width: 258px; top: 150px; /* below the nav control buttons */ From 93b3e6890fd25d202a46b4e55402325b21f27017 Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 15:18:36 +0100 Subject: [PATCH 15/20] feat(ui): add collapse handle for side-opening action panels Action-panel drawers (File/Layouts/Scene/.../Data) could only be dismissed by navigating back to Main View through the navbar tabs. Add a small chevron handle docked to the drawer's left seam: '>' collapses the open drawer to the canvas, '<' reopens the last one that was open. Derives the active pane from the shown.bs.tab event's own data-bs-target rather than querying '.tab-pane.show': Bootstrap fires that event as soon as the (unanimated) nav button activates, before the pane's own fade transition adds its 'show' class, so a DOM query at that instant could still match the pane being deactivated. Co-Authored-By: Claude Sonnet 5 --- frontend/index.html | 12 +++++- frontend/src/main.ts | 2 + frontend/src/style.css | 38 ++++++++++++++++++ frontend/src/ui/drawer_toggle.ts | 66 ++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 frontend/src/ui/drawer_toggle.ts diff --git a/frontend/index.html b/frontend/index.html index a63ac42..68c47b3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -187,10 +187,18 @@
+ +
- No network loaded yet. Open the File tab to upload a network or load - an example. + No network loaded yet. Open the File tab to upload a network or + load an example.
diff --git a/frontend/src/main.ts b/frontend/src/main.ts index ac4e1bf..102da54 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -17,6 +17,7 @@ import { initEdgePanel } from './ui/edge' import { initFpsPanel } from './ui/fps' import { initDataPanel } from './ui/data' import { initHelpPanel } from './ui/help' +import { registerDrawerToggle } from './ui/drawer_toggle' import { store } from './store' import { ctx, Scene } from './three' import { history } from './commands/base' @@ -63,6 +64,7 @@ async function main(): Promise { initFpsPanel() initDataPanel() initHelpPanel() + registerDrawerToggle() registerGlobalListeners() registerThemeListener() registerLabelRendering() diff --git a/frontend/src/style.css b/frontend/src/style.css index ce315b5..27a06a0 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -223,6 +223,44 @@ body { } } +/* Collapse handle for the right-side drawer: sits on the seam between the + open drawer and the canvas, or on the viewport edge once the drawer is + closed, so a panel can be dismissed/reopened without the navbar tabs. */ +.panel-drawer-toggle { + position: fixed; + top: 50%; + right: 0; + transform: translateY(-50%); + z-index: 6; /* above the drawer's z-index 5 */ + width: 20px; + height: 64px; + align-items: center; + justify-content: center; + font-size: 16px; + color: var(--arena-text-muted); + background-color: var(--arena-surface); + border: 1px solid var(--arena-border); + border-right: 0; + border-radius: var(--arena-radius-sm) 0 0 var(--arena-radius-sm); + box-shadow: var(--arena-shadow); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + cursor: pointer; + transition: + right 0.28s var(--arena-ease), + color 0.15s var(--arena-ease), + background-color 0.15s var(--arena-ease); +} + +.panel-drawer-toggle:hover { + color: var(--arena-text); + background-color: var(--arena-elevated); +} + +.panel-drawer-toggle.is-open { + color: var(--arena-accent); +} + /* Home and Help are full-width reading surfaces, not scene controls. Selector carries two ids to out-specify the base .tab-pane drawer rule. */ #panelContent #panel-home, diff --git a/frontend/src/ui/drawer_toggle.ts b/frontend/src/ui/drawer_toggle.ts new file mode 100644 index 0000000..f4ad662 --- /dev/null +++ b/frontend/src/ui/drawer_toggle.ts @@ -0,0 +1,66 @@ +// Collapse handle for the right-side action-panel drawer (File/Layouts/ +// Scene/.../Data — everything except Home, Main View and Help, which aren't +// canvas-overlay drawers). Lets a drawer be dismissed/reopened without going +// back through the navbar tabs. + +import { showTab } from './tabs' + +const NON_DRAWER_IDS = new Set(['panel-home', 'panel-main-view', 'panel-help']) + +let lastDrawerSelector: string | null = null + +// Bootstrap fires shown.bs.tab as soon as the (unanimated) nav button itself +// finishes activating — before the target pane's own fade transition adds its +// 'show' class. Querying '.tab-pane.show' at that moment can still match the +// pane being deactivated, so read the pane id straight from the event's +// tab-trigger target instead of asking the DOM which pane is "showing". +function activePaneId(): string | null { + return document.querySelector('#panelContent .tab-pane.active')?.id ?? null +} + +function isDrawerPane(id: string | null): boolean { + return !!id && !NON_DRAWER_IDS.has(id) +} + +function updateToggle(paneId?: string | null): void { + const btn = document.getElementById('panelDrawerToggle') + if (!btn) return + const id = paneId ?? activePaneId() + + if (id === 'panel-home' || id === 'panel-help') { + // Full-width reading panes, not a drawer to collapse. + btn.style.display = 'none' + return + } + + const open = isDrawerPane(id) + if (open) lastDrawerSelector = `#${id}` + + btn.style.display = 'flex' + btn.classList.toggle('is-open', open) + btn.innerHTML = open ? '›' : '‹' + btn.setAttribute('aria-label', open ? 'Hide panel' : 'Show panel') + btn.style.right = + open && id + ? `${document.getElementById(id)!.getBoundingClientRect().width}px` + : '0' +} + +export function registerDrawerToggle(): void { + const btn = document.getElementById('panelDrawerToggle') + if (!btn) return + + btn.addEventListener('click', () => { + if (isDrawerPane(activePaneId())) showTab('#panel-main-view') + else if (lastDrawerSelector) showTab(lastDrawerSelector) + }) + + document.addEventListener('shown.bs.tab', (e) => { + const paneSelector = (e.target as HTMLElement).getAttribute( + 'data-bs-target' + ) + updateToggle(paneSelector?.replace('#', '')) + }) + window.addEventListener('resize', () => updateToggle()) + updateToggle() +} From ce5f11db2b60188bc83222275240ff03d24db1b3 Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 15:31:56 +0100 Subject: [PATCH 16/20] feat(network): seed node scatter so loads are reproducible getRandomArbitrary drew from Math.random(), so every Load Example or upload scattered nodes to a different layout. Back it with a seeded mulberry32 PRNG and re-seed at the start of scrambleNodes with a fixed seed (123, mirroring the backend's random.seed(123)), so the same network always produces the same layout. Co-Authored-By: Claude Opus 4.8 --- frontend/src/actions/node.ts | 7 +++++++ frontend/src/utils.ts | 21 +++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/frontend/src/actions/node.ts b/frontend/src/actions/node.ts index 54aa112..ea2a151 100644 --- a/frontend/src/actions/node.ts +++ b/frontend/src/actions/node.ts @@ -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)) diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index 2a73475..723e79c 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -27,9 +27,26 @@ export function findIndexByUuid( return array.findIndex((object) => object.uuid === uuid) } -// Random float in [min, max). +// Seeded PRNG (mulberry32) so a given network always scatters to the same +// layout — Load Example and uploads are reproducible instead of random each +// time. Call setRandomSeed() to restart the sequence. Mirrors the backend's +// random.seed(123) for layout algorithms. +let rngState = 123 >>> 0 + +export function setRandomSeed(seed: number): void { + rngState = seed >>> 0 +} + +function seededRandom(): number { + rngState = (rngState + 0x6d2b79f5) | 0 + let t = Math.imul(rngState ^ (rngState >>> 15), 1 | rngState) + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t + return ((t ^ (t >>> 14)) >>> 0) / 4294967296 +} + +// Random float in [min, max) from the seeded sequence. export function getRandomArbitrary(min: number, max: number): number { - return Math.random() * (max - min) + min + return seededRandom() * (max - min) + min } export function toRadians(angle: number): number { From 8bf5e31d12eb847425adcf893f724e9634a92ca8 Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 15:32:17 +0100 Subject: [PATCH 17/20] feat(ui): collapse the File drawer after a successful load After a network upload, Load Example, or session import succeeds, switch to the empty Main View pane so the drawer closes and the 3D scene is revealed without a manual tab change. The panelDrawerToggle picks up the change through its existing shown.bs.tab listener and shows the closed state. On error the drawer stays open so the message is visible. Co-Authored-By: Claude Opus 4.8 --- frontend/src/ui/file.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/ui/file.ts b/frontend/src/ui/file.ts index 6f9477b..a420fef 100644 --- a/frontend/src/ui/file.ts +++ b/frontend/src/ui/file.ts @@ -6,6 +6,7 @@ import { loadNetwork, loadSession } from '../actions/network' import { applyNodeAttributes } from '../actions/node' import { startLoader, finishLoader } from '../actions/screen' import { exportSession } from '../actions/session' +import { showTab } from './tabs' const FILE_HTML = `
@@ -46,6 +47,7 @@ async function onUploadNetwork(file: File): Promise { status( `Loaded network: ${data.layers.length} layers, ${data.nodes.length} nodes.` ) + showTab('#panel-main-view') // reveal the scene; drawer toggle shows closed } catch (err) { status(err instanceof Error ? err.message : 'Network upload failed.', true) } finally { @@ -59,6 +61,7 @@ async function onLoadSession(file: File): Promise { const session = await api.importSession(file) loadSession(session) status('Session loaded.') + showTab('#panel-main-view') // reveal the scene; drawer toggle shows closed } catch (err) { status(err instanceof Error ? err.message : 'Session load failed.', true) } finally { From 19789a8de56a2b1527d831b462115854704ce1ec Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 15:42:18 +0100 Subject: [PATCH 18/20] feat(ui): port v2 theme bar (Light/Dark/Gray) with e2e test Buttons attach top-right on first network load and dispatch ChangeThemeCommand, so theme switches are undoable. Co-Authored-By: Claude Fable 5 --- frontend/e2e/themes.spec.ts | 43 ++++++++++++++++++++++++++++++++++ frontend/index.html | 1 + frontend/src/actions/themes.ts | 38 ++++++++++++++++++++++++++++++ frontend/src/main.ts | 3 ++- frontend/src/style.css | 40 +++++++++++++++++++++++++++++++ 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 frontend/e2e/themes.spec.ts diff --git a/frontend/e2e/themes.spec.ts b/frontend/e2e/themes.spec.ts new file mode 100644 index 0000000..4b2ffe0 --- /dev/null +++ b/frontend/e2e/themes.spec.ts @@ -0,0 +1,43 @@ +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) + ) + + // 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') + + 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') +}) diff --git a/frontend/index.html b/frontend/index.html index 68c47b3..03fcdae 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -196,6 +196,7 @@ > +
No network loaded yet. Open the File tab to upload a network or load an example. diff --git a/frontend/src/actions/themes.ts b/frontend/src/actions/themes.ts index 2084edc..91a601e 100644 --- a/frontend/src/actions/themes.ts +++ b/frontend/src/actions/themes.ts @@ -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' @@ -75,3 +77,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) + } +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 102da54..828e961 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -27,7 +27,7 @@ import { registerLayerDragControls } from './actions/drag_controls' import { registerLabelRendering } from './actions/labels' import { registerNavControls } from './actions/nav_controls' import { setRenderer, resetScreen, animate } from './actions/screen' -import { registerThemeListener } from './actions/themes' +import { registerThemeButtons, registerThemeListener } from './actions/themes' import { registerGlobalListeners } from './event_listeners' async function main(): Promise { @@ -67,6 +67,7 @@ async function main(): Promise { registerDrawerToggle() registerGlobalListeners() registerThemeListener() + registerThemeButtons() registerLabelRendering() registerCanvasControls() registerLayerDragControls() diff --git a/frontend/src/style.css b/frontend/src/style.css index 27a06a0..99940b5 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -400,6 +400,46 @@ body { color: #ffb454; } +/* Theme bar (port of v2 #themeDiv/.themeButton): fixed top-right, one button + per THEMES preset, each previewing its own scene colors. */ +#themeDiv { + position: fixed; + top: 64px; + right: 10px; + z-index: 3; + display: flex; + gap: 4px; +} + +.themeButton { + width: 56px; + padding: 3px 0; + font-size: 12px; + font-weight: bold; + border: 1px solid var(--arena-border); + border-radius: 6px; + transition: border-color 0.15s ease; +} + +.themeButton:hover { + border-color: var(--arena-accent); +} + +#lightThemeButton { + color: #5c5c5c; + background-color: #ffffff; +} + +#darkThemeButton { + color: #ffffff; + background-color: #000000; +} + +#grayThemeButton { + color: #6e2a5a; + background-color: #bababa; +} + #info { width: 258px; top: 150px; /* below the nav control buttons */ From 652d80890fb1deb5cfc89866fac71008af4f774f Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 15:48:44 +0100 Subject: [PATCH 19/20] fix(ui): keep theme floor color on layer hover-exit applyTheme set layer colors via repaintLayers(theme.floor) but never updated the #floor_color input, which is the source of truth on the picker priority path. Hover-exit repaint (repaintLayers() / Layer.getColor()) read the stale input and reverted layers to the dark default. Restore the v2 behaviour of writing the theme floor into #floor_color. Co-Authored-By: Claude Fable 5 --- frontend/e2e/themes.spec.ts | 4 ++++ frontend/src/actions/themes.ts | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/e2e/themes.spec.ts b/frontend/e2e/themes.spec.ts index 4b2ffe0..48c2af2 100644 --- a/frontend/e2e/themes.spec.ts +++ b/frontend/e2e/themes.spec.ts @@ -30,6 +30,10 @@ test('theme buttons attach on load and switch scene theme (undoable)', async ({ 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') diff --git a/frontend/src/actions/themes.ts b/frontend/src/actions/themes.ts index 91a601e..baae11a 100644 --- a/frontend/src/actions/themes.ts +++ b/frontend/src/actions/themes.ts @@ -63,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 From e37cda40f11ed32a9a48e2397c0bb42719bc9dd6 Mon Sep 17 00:00:00 2001 From: vagkaratzas Date: Sat, 4 Jul 2026 15:52:02 +0100 Subject: [PATCH 20/20] report removed --- REPORT_TASTE.md | 93 ------------------------------------------------- 1 file changed, 93 deletions(-) delete mode 100644 REPORT_TASTE.md diff --git a/REPORT_TASTE.md b/REPORT_TASTE.md deleted file mode 100644 index 6686faa..0000000 --- a/REPORT_TASTE.md +++ /dev/null @@ -1,93 +0,0 @@ -# Arena3Dweb — Frontend UI/UX Redesign (taste-skill) - -**Design read:** Redesign-*preserve* of a scientific 3D-network visualization -tool for bioinformatics researchers. Dark "instrument" canvas app built on -Bootstrap 5 + Vite/TypeScript/Three.js. Direction: cohesive dark instrument -panel, single cool-blue accent, restrained motion, strong hierarchy — without -touching any backend contract, route, data flow, or feature. - -Approach followed the token-saviour + ponytail path: **kept Bootstrap** and -*retinted its dark tokens* instead of ripping the framework out or rewriting to -React. Most changes are CSS + small, surgical markup edits. One feature per -commit, no push. - ---- - -## Summary of improvements - -1. **Unified design foundation.** Retinted Bootstrap's dark theme to one cool - palette + accent so every `.btn` / `.form-control` / `.nav` / `.table` - inherits the system. Refined navbar (frosted, active-tab pill), buttons - (accent fill, tactile `:active` press), inputs (focus ring), `:focus-visible` - rings, and a global `prefers-reduced-motion` guard. - -2. **Side-drawer panels (key UX fix).** Action panels used to overlay the whole - 3D scene. They now slide in as a right-side drawer (`min(440px, 94vw)`, wider - for the data table) so the scene stays visible and interactive while you - operate on it. **Only Home and Help remain full-width**, per the explicit - constraint. Drawer has a slide-in animation (reduced-motion aware). - -3. **Panel content fits the drawer.** Neutralized the now-pointless responsive - column wrappers inside drawers, and rebuilt the cramped per-layer checkbox - group into tidy bordered rows with a truncating layer name and aligned - Hide / Labels toggles. - -4. **Home panel redesign.** Hero header with tagline, a figure + readable-measure - intro grid, two quick-start action cards (Upload / Examples), and a - de-emphasized citation block. Collapses to one column on mobile. - -5. **Help panel themed.** Replaced the light-on-dark tab strip with accent pill - tabs; restyled headings, code blocks, images, and links for the dark chrome - and a comfortable reading measure. - -6. **Scene overlays + empty state.** The bare "waiting for network" line is now a - centered empty-state card. Restyled the navigation-controls headings and - dividers, gave the Recenter button an accent treatment, and replaced the - raw-red node-description close button with a themed control. - -7. **Responsive behavior.** The mobile navbar menu now auto-collapses after a tab - is picked (so the chosen panel isn't left buried under an open menu), and - drawers pad their bottom to clear the fixed footer. Verified at desktop - (1440), tablet (820), and mobile (390). - -## Files changed - -| File | Change | -|------|--------| -| `frontend/src/style.css` | Design tokens, Bootstrap retint, component polish, drawer system, layer rows, Home/Help/overlay/empty-state styling | -| `frontend/index.html` | Friendlier empty-state copy | -| `frontend/src/ui/home.ts` | Home panel markup restructured (IDs preserved) | -| `frontend/src/ui/layouts.ts` | Per-layer selection rebuilt as clean rows (JS wiring unchanged) | -| `frontend/src/event_listeners.ts` | Auto-collapse mobile navbar on tab select | - -No backend files, API endpoints, request/response shapes, routes, or auth logic -were touched. - -## Validation commands run - -- `npx tsc --noEmit` — clean (after each change) -- `npm run lint` (eslint) — clean -- `npx vitest run` — **106 passed (15 files)** -- `npx playwright test` — **1 passed** (existing `network-flow` E2E) - -## Playwright flows tested (baseline + post-change screenshots) - -- Home → File → **Load Example** → Main View (scene renders) -- Layer Selection: **Select All Layers → choose Circle layout → Run** ("Layout - applied", scene re-renders) -- Every panel visited (Scene / Layer / Node / Edge / View Data / FPS / Help) -- Empty state (no network) and loaded-controls state -- Mobile (390) + tablet (820): navbar collapse, drawer, home stack -- **Console errors: 0** across the full sweep - -## Known limitations / follow-ups - -- On phones the drawer is ~94vw, so it necessarily covers most of the scene — - unavoidable at that width; the desktop/tablet constraint (don't cover the - scene) is fully met. -- Node/edge attribute *content* (help tables, node description body) was themed - but not re-laid-out; a future pass could card-ify the longer help spec tables. -- The nav-control image sprites (rotation arrows, expand/collapse) are the - original PNGs — left as-is to avoid asset churn; could become inline SVG later - for crispness on HiDPI. -- No new fonts were added (system-ui stack kept) to avoid a dependency/asset add.
Scene
Rotation Controls
Rotation Controls
Layers
Rotation Controls
Rotation Controls
Translation Controls
Translation Controls
x1
Nodes
Translation Controls
Translation Controls