2026-07-05 — v28 workspace delight + responsive + state-persistence polish
The v27 shell restructure gave us the right bones — hero chromatogram, collapsible tabbed sidebar, two-column layout. This pass adds the finish that makes it genuinely frictionless: motion that respects user preferences, a narrower density that fits more tool surface without crowding, a sidebar that gracefully adapts at tablet widths, and a permalink that actually reproduces the exact workspace layout the sender was looking at.
What changed
Motion / prefers-reduced-motion
A @media (prefers-reduced-motion: reduce) guard now zeros out transition and
animation durations across the entire workspace shell subtree
(.app-shell, descendants, and pseudo-elements). The existing guards for the
clone-screen were already in place; this pass extends the same discipline to the full
workspace shell so users who opt out of motion get a completely still UI, not a partially
still one.
Density refinements
- Sidebar tab height reduced from 36 px to 32 px — still above the 24 px design floor, and the row is noticeably thinner on desktop where the chromatogram is long.
-
Panel gap tightened from
var(--space-3)tovar(--space-2)so tool sections sit closer together and more panel content fits without scrolling. -
Sidebar scrollbar set to
scrollbar-width: thinwith a muted token colour — the overflow scroller on the Analyze panel (which has many items) now looks intentional rather than accidental. -
Tabs row is now sticky inside the scrollable
.sidebar-inner(position: sticky; top: 0). You can scroll deep into the Analyze panel and switch tabs without having to scroll back to the top. -
Hover indicator on tabs — a
border-bottominvar(--color-border-strong)now appears on hover so the tab feels interactive before you click, consistent with the active-tab underline.
Narrow-viewport (tablet) refinements
At ≤ 900 px the sidebar already stacks below the hero. This pass tightens the UX:
-
The sidebar toggle is now full-width (
align-self: stretch; width: 100%) with centred text, so it reads as a clear section expander rather than a floating button. -
overflow-y: autois explicitly set on.sidebar-innerat this breakpoint, ensuring the 60 vh cap scrolls the panel rather than clips it. - The sticky tabs row works at this breakpoint too — tabs stay pinned at the top of the inner panel while the content underneath scrolls.
Permalink state persistence for sidebar UI
Before this pass, copying a link and sending it to a colleague would restore the trace state
but drop them onto the default Inspect tab regardless of which tab the sender had open. Now
PermalinkStateV1 carries an optional ui field:
ui?: { sidebarOpen: boolean; activeTab: 'inspect' | 'map' | 'analyze' | 'share' }
getPermalinkState() reads the live DOM to capture sidebar-open state and active
tab; applyPermalinkState() restores both when hydrating from a hash.
The sidebar toggle and tab clicks now call schedulePermalinkPersist() so the
hash updates immediately on interaction — no reload required to keep the link current.
Links without a ui key (old links) continue to work and default to
sidebar-open + Inspect tab.
Tests
tests/e2e/workspace-polish.e2e.test.ts contains ten genuine behavior assertions:
- Three reduced-motion tests asserting
transitionDuration === "0s"on each shell element. - Three narrow-viewport tests (toggle full-width; 60 vh cap; sticky tabs).
- Five permalink round-trip tests: closed sidebar restores, Map tab restores, Analyze tab restores, toggling sidebar updates hash and reloads correctly, switching to Share tab round-trips.
Zero capability regressions
The entire change is additive: new CSS media query blocks, a new optional field on the
permalink schema, and two lines in applyPermalinkState. Every existing test
anchor, control ID, and E2E selector is unchanged.