diff --git a/apps/probe-viewer/src/App.css b/apps/probe-viewer/src/App.css index c57495d..3029cc0 100644 --- a/apps/probe-viewer/src/App.css +++ b/apps/probe-viewer/src/App.css @@ -28,14 +28,14 @@ background: linear-gradient(180deg, var(--bg) 0%, var(--bg-2) 100%); } -.app-sidebar { +.probe-selection-menu { width: 320px; border-right: 1px solid var(--line); background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(6px); } -.app-main { +.probe-display { flex: 1; min-width: 0; /* allow the main area to shrink to the frame instead of overflowing */ min-height: 0; @@ -45,7 +45,7 @@ justify-content: center; } -.sidebar { +.probe-selection-menu-content { display: flex; flex-direction: column; gap: 1.25rem; @@ -54,13 +54,13 @@ color: var(--ink); } -.sidebar-header { +.probe-selection-menu-header { display: flex; flex-direction: column; gap: 0.5rem; } -.sidebar-title { +.probe-selection-menu-title { font-size: 1.4rem; margin: 0; font-weight: 700; @@ -68,26 +68,26 @@ color: var(--ink); } -.sidebar-subtitle { +.probe-selection-menu-subtitle { margin: 0; color: var(--ink-2); font-size: 0.95rem; } -.sidebar-control { +.probe-filter { display: flex; flex-direction: column; gap: 0.5rem; } -.sidebar-label { +.probe-filter-label { font-size: 0.85rem; font-weight: 600; color: var(--ink-2); } -.sidebar select, -.sidebar input { +.probe-selection-menu-content select, +.probe-selection-menu-content input { font: inherit; padding: 0.5rem 0.75rem; border-radius: var(--radius); @@ -96,20 +96,20 @@ transition: border-color 0.2s ease, box-shadow 0.2s ease; } -.sidebar select:focus, -.sidebar input:focus { +.probe-selection-menu-content select:focus, +.probe-selection-menu-content input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); outline: none; } -.sidebar select:disabled, -.sidebar input:disabled { +.probe-selection-menu-content select:disabled, +.probe-selection-menu-content input:disabled { opacity: 0.6; cursor: not-allowed; } -.sidebar-list { +.probe-list { flex: 1; min-height: 0; /* let the list shrink to its slot so overflow-y: auto can scroll it */ overflow-y: auto; @@ -119,7 +119,7 @@ padding-right: 0.25rem; } -.sidebar-item { +.probe-list-item { display: flex; flex-direction: column; align-items: flex-start; @@ -133,37 +133,37 @@ transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; } -.sidebar-item:hover { +.probe-list-item:hover { border-color: var(--line-strong); background: var(--accent-soft); } /* Active probe: a graphite left bar (inset shadow, so no layout shift) plus a darker border. Monochrome, distinct from the lighter hover fill. */ -.sidebar-item--active { +.probe-list-item--active { border-color: var(--accent-strong); background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent-strong), var(--shadow-sm); } -.sidebar-item-name { +.probe-list-item-name { font-weight: 600; color: var(--ink); } -.sidebar-item-meta { +.probe-list-item-meta { font-size: 0.8rem; color: var(--ink-2); } /* Neuropixels hierarchy grouping */ -.sidebar-group { +.probe-list-group { display: flex; flex-direction: column; gap: 0.4rem; } -.sidebar-group-header { +.probe-list-group-header { display: flex; align-items: center; gap: 0.5rem; @@ -178,21 +178,21 @@ border-bottom: 1px solid var(--line); } -.sidebar-group-header:hover { +.probe-list-group-header:hover { color: var(--accent-strong); } -.sidebar-group-caret { +.probe-list-group-caret { width: 1rem; color: var(--muted); } -.sidebar-group-title { +.probe-list-group-title { flex: 1; text-align: left; } -.sidebar-group-count { +.probe-list-group-count { font-size: 0.75rem; font-weight: 600; color: var(--ink-2); @@ -201,14 +201,14 @@ padding: 0.05rem 0.5rem; } -.sidebar-subgroup { +.probe-list-subgroup { display: flex; flex-direction: column; gap: 0.35rem; padding-left: 0.6rem; } -.sidebar-subgroup-header { +.probe-list-subgroup-header { display: flex; align-items: center; gap: 0.4rem; @@ -219,11 +219,11 @@ cursor: pointer; } -.sidebar-subgroup-header:hover .sidebar-subgroup-title { +.probe-list-subgroup-header:hover .probe-list-subgroup-title { color: var(--accent-strong); } -.sidebar-subgroup-title { +.probe-list-subgroup-title { flex: 1; text-align: left; font-size: 0.74rem; @@ -233,14 +233,14 @@ color: var(--muted); } -.sidebar-subdivision { +.probe-list-subdivision { display: flex; flex-direction: column; gap: 0.35rem; } /* Static (non-collapsible) length divider inside the Non-human-primate family. */ -.sidebar-subgroup-divider { +.probe-list-subgroup-divider { margin: 0.3rem 0 0.05rem 0.9rem; font-size: 0.68rem; font-weight: 600; @@ -249,20 +249,20 @@ color: var(--muted); } -.sidebar-hint { +.probe-list-hint { font-size: 0.9rem; color: var(--muted); margin: 0.5rem 0; } -.sidebar-error { +.probe-list-error { font-size: 0.9rem; color: #dc2626; font-weight: 600; margin: 0.5rem 0; } -.viewer-panel { +.probe-view { flex: 1; display: flex; flex-direction: column; @@ -276,14 +276,14 @@ box-shadow: var(--shadow); } -.viewer-header { +.probe-view-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; } -.viewer-title { +.probe-view-title { margin: 0; font-size: 1.7rem; font-weight: 700; @@ -294,7 +294,7 @@ /* "JSON" link inline in the subtitle metadata line. It uses the blue link color and a persistent underline (on the word, not the icon) so it clearly reads as a clickable link rather than just emphasized text. */ -.viewer-json-link { +.probe-view-json-link { display: inline-flex; align-items: center; gap: 0.25rem; @@ -305,16 +305,16 @@ transition: color 0.2s ease; } -.viewer-json-link-text { +.probe-view-json-link-text { text-decoration: underline; text-underline-offset: 2px; } -.viewer-json-link:hover { +.probe-view-json-link:hover { color: var(--accent-strong); } -.viewer-json-download { +.probe-view-json-download { display: inline-flex; align-items: center; vertical-align: middle; @@ -325,17 +325,17 @@ transition: color 0.2s ease, background 0.2s ease; } -.viewer-json-download svg { +.probe-view-json-download svg { width: 14px; height: 14px; } -.viewer-json-download:hover { +.probe-view-json-download:hover { color: var(--accent-strong); background: var(--accent-soft); } -.viewer-subtitle { +.probe-view-subtitle { margin: 0.25rem 0 0; color: #475569; font-size: 0.95rem; @@ -343,7 +343,7 @@ /* Map-style overlay clusters floating in the canvas corners, so the probe owns the frame and the chrome sits out of the way until you reach for it. */ -.canvas-controls { +.probe-drawing-controls { position: absolute; top: 0.75rem; z-index: 4; @@ -352,11 +352,11 @@ gap: 0.4rem; } -.canvas-controls--nav { +.probe-drawing-controls--zoom { left: 0.75rem; } -.canvas-controls button { +.probe-drawing-controls button { display: inline-flex; align-items: center; justify-content: center; @@ -373,7 +373,7 @@ transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease; } -.canvas-controls button:hover { +.probe-drawing-controls button:hover { border-color: var(--accent); color: var(--accent-strong); background: var(--accent-soft); @@ -382,14 +382,14 @@ /* Control bands above (framing) and below (display preferences) the canvas, so the canvas itself carries only the zoom overlay and nothing occludes the probe. */ -.viewer-toolbar { +.probe-view-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; } -.viewer-toolbar button { +.probe-view-toolbar button { display: inline-flex; align-items: center; justify-content: center; @@ -405,7 +405,7 @@ transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease; } -.viewer-toolbar button:hover { +.probe-view-toolbar button:hover { border-color: var(--accent); color: var(--accent-strong); background: var(--accent-soft); @@ -413,7 +413,7 @@ /* Toggles styled as part of the button family: an outline chip that fills with the accent when on, so they sit alongside the action buttons consistently. */ -.viewer-toggle { +.probe-view-toggle { display: inline-flex; align-items: center; gap: 0.45rem; @@ -428,26 +428,26 @@ transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease; } -.viewer-toggle:hover { +.probe-view-toggle:hover { border-color: var(--accent); color: var(--accent-strong); } -.viewer-toggle:has(input:checked) { +.probe-view-toggle:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); color: var(--accent-strong); } /* Keyboard focus ring on the chip, since the real checkbox is visually hidden. */ -.viewer-toggle:has(input:focus-visible) { +.probe-view-toggle:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; } /* The checkbox stays in the DOM (and keyboard-focusable) for accessibility, but is visually hidden: the chip's pressed fill is what conveys on/off. */ -.viewer-toggle input { +.probe-view-toggle input { position: absolute; width: 1px; height: 1px; @@ -460,13 +460,13 @@ } /* Double-sided probe layout controls */ -.viewer-controls-sides { +.probe-view-sides-control { display: flex; align-items: center; gap: 0.6rem; } -.viewer-controls-label { +.probe-view-sides-label { font-size: 0.9rem; font-weight: 600; color: #1e293b; @@ -474,52 +474,52 @@ /* Segmented button group: adjacent buttons read as one control, the active segment highlighted. */ -.viewer-segmented { +.probe-view-segmented { display: inline-flex; gap: 0; } -.viewer-segmented button { +.probe-view-segmented button { border-radius: 0; text-transform: capitalize; } -.viewer-segmented button:first-child { +.probe-view-segmented button:first-child { border-top-left-radius: 0.75rem; border-bottom-left-radius: 0.75rem; } -.viewer-segmented button:last-child { +.probe-view-segmented button:last-child { border-top-right-radius: 0.75rem; border-bottom-right-radius: 0.75rem; } -.viewer-segmented button:not(:first-child) { +.probe-view-segmented button:not(:first-child) { border-left: none; } -.viewer-segmented button.is-active { +.probe-view-segmented button.is-active { background: #2563eb; color: #fff; } /* Color key matching the canvas: gold front, steel-blue back. */ -.viewer-side-swatch { +.probe-view-side-swatch { width: 0.7rem; height: 0.7rem; border-radius: 50%; display: inline-block; } -.viewer-side-swatch--front { +.probe-view-side-swatch--front { background: rgb(212, 175, 55); } -.viewer-side-swatch--back { +.probe-view-side-swatch--back { background: rgb(70, 130, 180); } -.viewer-canvas { +.probe-drawing-window { position: relative; min-height: 360px; border-radius: 1.1rem; @@ -528,7 +528,7 @@ background: #f8f9fa; } -.viewer-canvas-surface { +.probe-drawing-area { width: 100%; height: 100%; display: flex; @@ -536,17 +536,18 @@ justify-content: stretch; } -.viewer-canvas canvas { +.probe-drawing-window canvas { width: 100%; height: 100%; cursor: grab; + touch-action: none; /* a touch drag pans the probe instead of scrolling the page */ } -.viewer-canvas canvas:active { +.probe-drawing-window canvas:active { cursor: grabbing; } -.probe-overview { +.probe-minimap { position: absolute; bottom: 12px; right: 12px; @@ -556,11 +557,11 @@ transition: box-shadow 0.2s ease; } -.probe-overview:hover { +.probe-minimap:hover { box-shadow: 0 6px 16px rgba(15, 23, 42, 0.25); } -.viewer-download { +.probe-view-action { display: inline-flex; align-items: center; gap: 0.35rem; @@ -575,26 +576,26 @@ cursor: pointer; } -.viewer-header-actions { +.probe-view-header-actions { display: flex; gap: 0.5rem; align-items: center; } -.viewer-download:hover { +.probe-view-action:hover { border-color: var(--accent); color: var(--accent-strong); background: var(--accent-soft); } -.viewer-snippet { +.probe-view-snippet { position: relative; display: inline-flex; vertical-align: middle; } /* Looks like the JSON link, with a chevron to say it opens something. */ -.viewer-snippet-toggle { +.probe-view-snippet-toggle { display: inline-flex; align-items: center; gap: 0.15rem; @@ -607,11 +608,11 @@ cursor: pointer; } -.viewer-snippet-toggle:hover { +.probe-view-snippet-toggle:hover { color: var(--accent-strong); } -.viewer-snippet-popover { +.probe-view-snippet-popover { position: absolute; top: calc(100% + 0.4rem); left: 0; @@ -627,7 +628,7 @@ } /* Language label on the left, copy action on the right, as in docs code blocks. */ -.viewer-snippet-header { +.probe-view-snippet-header { display: flex; align-items: center; justify-content: space-between; @@ -636,7 +637,7 @@ background: var(--bg); } -.viewer-snippet-language { +.probe-view-snippet-language { font-size: 0.72rem; font-weight: 600; letter-spacing: 0.04em; @@ -644,7 +645,7 @@ color: var(--muted); } -.viewer-snippet-copy { +.probe-view-snippet-copy { display: inline-flex; align-items: center; gap: 0.3rem; @@ -658,12 +659,12 @@ cursor: pointer; } -.viewer-snippet-copy:hover { +.probe-view-snippet-copy:hover { color: var(--accent-strong); background: var(--accent-soft); } -.viewer-snippet-code { +.probe-view-snippet-code { display: block; padding: 0.8rem 1rem; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; @@ -675,20 +676,20 @@ } /* Syntax highlighting kept monochrome: the chrome carries no hue (see :root). */ -.viewer-snippet-token--keyword { +.probe-view-snippet-token--keyword { font-weight: 700; color: var(--accent-strong); } -.viewer-snippet-token--function { +.probe-view-snippet-token--function { color: var(--ink); } -.viewer-snippet-token--string { +.probe-view-snippet-token--string { color: var(--muted); } -.viewer-canvas-placeholder { +.probe-drawing-placeholder { height: 360px; border-radius: 1rem; background: repeating-linear-gradient( @@ -707,12 +708,12 @@ padding: 1rem; } -.viewer-issue-link { +.probe-view-issue-link { text-align: right; font-size: 0.85rem; } -.viewer-issue-link a { +.probe-view-issue-link a { color: #6b7280; text-decoration: none; display: inline-flex; @@ -720,12 +721,12 @@ gap: 0.35rem; } -.viewer-issue-link a:hover { +.probe-view-issue-link a:hover { color: var(--accent-strong); text-decoration: underline; } -.viewer-placeholder { +.probe-view-placeholder { flex: 1; display: flex; align-items: center; @@ -736,22 +737,46 @@ box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.2); } -.viewer-placeholder--error { +.probe-view-placeholder--error { color: #dc2626; } +/* Too narrow for the probe selection menu beside the probe display, or too short + for the whole probe view: the document scrolls instead of being locked to the + viewport. */ +@media (max-width: 960px), (max-height: 680px) { + .app-shell { + height: auto; + overflow: visible; + } + + .probe-selection-menu-content { + height: auto; + } + + .probe-list { + flex: none; + max-height: 40dvh; + } +} + @media (max-width: 960px) { .app-shell { flex-direction: column; } - .app-sidebar { + .probe-view-header, + .probe-view-header-actions { + flex-wrap: wrap; + } + + .probe-selection-menu { width: 100%; border-right: none; border-bottom: 1px solid rgba(15, 23, 42, 0.08); } - .app-main { + .probe-display { padding: 1rem; } } @@ -944,7 +969,7 @@ color: #64748b; } -.sidebar-home { +.probe-selection-menu-home { display: inline-flex; align-items: center; gap: 0.35rem; @@ -961,7 +986,7 @@ transition: color 0.15s ease; } -.sidebar-home:hover { +.probe-selection-menu-home:hover { color: var(--ink); } @@ -1038,7 +1063,7 @@ } /* Cold /local empty state. */ -.viewer-panel--empty { +.probe-view--empty { align-items: center; justify-content: center; } diff --git a/apps/probe-viewer/src/App.tsx b/apps/probe-viewer/src/App.tsx index 0cb1608..13aabee 100644 --- a/apps/probe-viewer/src/App.tsx +++ b/apps/probe-viewer/src/App.tsx @@ -3,7 +3,7 @@ import { useLocation, useParams } from "react-router-dom"; import { ProbeIndex } from "./components/ProbeIndex"; import { ProbeViewer } from "./components/ProbeViewer"; -import { Sidebar } from "./components/Sidebar"; +import { ProbeSelectionMenu } from "./components/ProbeSelectionMenu"; import { useAppStore } from "./state/useAppStore"; import { useProbeRouteSync } from "./state/useProbeRouteSync"; import { useRestoreCameraFromUrl } from "./state/useRestoreCameraFromUrl"; @@ -43,10 +43,10 @@ function App() { return (
No planar geometry available for this probe.
No planar geometry available for this probe.
{node.label}
+{node.label}
)} {open && (node.children @@ -195,11 +195,11 @@ export function Sidebar() { }; return ( -+
Browse available probe layouts and inspect their geometry.