Skip to content
Development

Navigation redesign: topbar tabs and a Settings dialog

By Victor Da Luz
sveltetauriaccessibilitydev-loggreenhouse

Greenhouse’s dashboard footer had turned into a junk drawer. Eight controls in a row: Vault count pill, Harvest count pill, the vault path, a Board button, Switch vault, a “Why these rules?” link, and three locale pills. Nothing distinguished “go to a different view” from “change a setting” - they all sat at the same visual weight. Worse, one i18n key was doing double duty: dashboard_vault_label rendered “Vault:” both as the count pill’s label and as the prefix on the path row. In Spanish that’s “Banco de semillas:” sitting in front of a filesystem path, which reads as nonsense.

The fix split those two concerns apart: a persistent topbar tab row for the four peer views (Today/Board/Vault/Harvest, with count badges on Vault/Harvest), and a single gear-icon Settings dialog for everything else (vault path, Switch vault, Language, Why these rules?). The path row got its own key, dashboard_vault_path_label, so it can finally just say “Folder:” without dragging along a mistranslated noun.

Design calls worth slowing down for

Two things came up that weren’t really engineering decisions - they were “what does this app want to be” decisions, so I made them deliberately instead of on autopilot. Settings entry point: gear button only, or also a native “Preferences…” (Cmd+,) item under the app menu? Went with both - it’s the same dialog either way, and Cmd+, is the macOS-native muscle memory for this. Per-view Back buttons: once there’s a persistent tab row, do the Vault/Harvest/Board views still need their own Back button? This one actually has a documented answer: macOS distinguishes peer navigation (switch via a persistent selector, no Back button - you don’t “back out” of a tab) from hierarchical drill-in (Back is for going one level deeper, like a project detail from a worklist card). Keeping Back buttons on the peer views would’ve been the actual inconsistency. Removed them; the project detail keeps its own, since it really is a drill-in.

What I’d have gotten wrong without a second pass

My first draft of the plan wanted to collapse the four boolean view-flags (vaultOpen, harvestOpen, boardOpen, selectedItem) into a single currentView enum before touching the UI - it felt like the “correct” foundation for a tab row. A second look talked me out of it: the existing flags already have a proven clean-handoff helper (closeAllViews(), built for the native View menu in an earlier issue), and an enum refactor would’ve meant touching every branch of a 280-line template and rewriting menu-routing tests that were already passing. I kept the booleans and just added a small switchView() wrapper shared by the tab row and the menu handler. Smaller diff, same result, nothing destabilized.

Two ARIA/tooling potholes

First pass at the tab row used role="tablist" because these are, semantically, view-switcher tabs. The project’s own automated a11y test (axe, run against a real render) caught it immediately: a tablist role requires tab-role children and keyboard arrow-navigation semantics I hadn’t built. What these actually are is a toggle-button group - exactly what the existing locale switcher already used (role="group" + aria-pressed). Matched that instead of inventing a half-finished ARIA pattern.

Second: verifying the Settings dialog against the real running app (via the WebDriver harness) came back with screenshots that looked like the dialog wasn’t there at all - just a faint shadow at the bottom of the frame. Turns out native <dialog> elements opened via showModal() render in the browser’s “top layer,” and the screenshot endpoint doesn’t capture that layer. The dialog was genuinely open and correctly positioned (confirmed via getBoundingClientRect() and text-content assertions instead of pixels) - the screenshot was just blind to it. Already a known gotcha from an earlier session, but it’s the kind of thing you rediscover the hard way if you don’t check first.

Result

158 frontend tests green (32 of them in Dashboard.test.ts alone), 223 Rust tests green, cargo clippy/fmt clean, and a dedicated e2e script drives the real app through every tab and every Settings sub-dialog before I called it done.

Related reading

Development

Toast + undo for Greenhouse's vault action

The app's first transient-feedback surface, the aria-live rule that makes a conditionally-rendered toast silently unannounced, and a prop that goes stale the moment you rename.

Read