feat(web): URL-sync filters + sticky table headers + scroll-shadow indicators#82
Merged
github-actions[bot] merged 2 commits intodevelopfrom May 8, 2026
Merged
Conversation
Three small but high-frequency wins for data-table UX. No logic changes.
URL-sync filters (apps/web/lib/useUrlParam.ts)
- New ``useUrlParam(key, default)`` and ``useUrlNumber`` hooks: two-way
binding between a query-string key and React state. Uses
``router.replace`` with ``scroll: false`` so chip clicks don't scroll
the page or pollute the back-button stack. Defaults are dropped from
the URL (clean copy/paste).
- Applied to:
/benchmark stage, k, eval_set
/jobs status
/proteins tab
- Refresh / share-link now lands on the same view.
Sticky data-table headers (.protea-thead-sticky in globals.css)
- New utility: ``position: sticky; top: 4rem`` (clears the h-16 chrome
header) + white background + 1px shadow.
- Applied to /benchmark matrix table, /proteins browse grid header,
/embeddings configs grid header.
Scroll-shadow indicator (.protea-scroll-shadow)
- Roman Komarov's local/scroll background-attachment trick: faint
shadows at the left/right edges of horizontally scrollable
containers, masked by white covers anchored to the content. Tells
the user "more content this way" without a separate JS observer.
- Applied to the same wrappers above.
CI: next build green; backend untouched.
6 tasks
github-actions Bot
pushed a commit
that referenced
this pull request
May 8, 2026
## Why UX audit flagged: **only 3 of 14 pages used aria/role/label htmlFor**. Drive-by: when PRs #81 (heatmap) and #82 (URL-sync) auto-rebased onto each other, the `viewMode` state on `/benchmark` was dropped — develop currently fails to build that page. Folded the trivial fix into this PR so it can land. ## What ### A11y - **Skip-to-content** link in the locale layout: hidden until focused, lands focus on `<main id="main" tabIndex={-1}>`. Reachable on the first Tab press. - **NavLinks** dropdowns now expose `aria-haspopup="menu"`, `aria-expanded`, `aria-current="page"` on active groups. Inner items carry `role="menuitem"`. Mobile menu locks body scroll while open and restores prior overflow on close (no more dual-scroll trap). - **StatusBadge** gains a redundant leading glyph per state (clock / pulse / check / ✕ / slash-circle). Color-blind users still parse the badge at a glance. Wrapper carries `role="status"` and a verbose `aria-label`. - **Benchmark filter chips** (Pipeline stage, Neighbours K) gain `aria-pressed` plus `role="group"` with descriptive `aria-label`. - **Heatmap | Table toggle** already ships `role="tablist"` / `aria-selected` from #81. ### Drive-by build fix - Restore `viewMode` state and the `BenchmarkHeatmap` import on `/benchmark`. PR #81's toggle UI references `viewMode` / `setViewMode`, but the auto-rebase ladder against #82 dropped the hook line on develop, leaving the page broken at build time. Adds the lines back; default `"heatmap"`. ## Test plan - [x] `next build` green; 18 routes; no TS errors (was failing on develop before this PR) - [x] Backend untouched - [ ] Press Tab on first load — "Skip to main content" link appears top-left, focusing it jumps to `<main>` - [ ] Use a screen reader on `/jobs` — StatusBadge announces "Status: succeeded" etc. - [ ] On a small viewport: open the mobile menu, body underneath stops scrolling; close and underneath scrolls again - [ ] Tab into Pipeline-stage chips — focus ring appears, `aria-pressed` flips on selection
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Three small frictions, fixed in one PR.
/benchmark, refresh — the chip resets. Worse: you can't share a deep-link to "PROTEA at stage=reranker, K=5".h-16chrome header above,<thead>should stay visible too — currently it scrolls away with the body, and you forget which column is which.What
URL-sync filters —
apps/web/lib/useUrlParam.tsuseUrlParam(key, default)anduseUrlNumber(key, default)hooks: two-way bind a query-string key to React state.router.replacewithscroll: falseso chip clicks don't pollute history or scroll the page. Defaults are dropped from the URL (clean copy/paste)./benchmark→stage,k,eval_set/jobs→status/proteins→tabSticky data-table headers —
.protea-thead-stickyposition: sticky; top: 4rem(clears theh-16chrome header) + white background + 1px shadow./benchmarkmatrix table<thead>/proteinsbrowse grid header/embeddingsconfigs grid headerScroll-shadow indicator —
.protea-scroll-shadowbackground-attachmenttrick: faint shadows at the left/right edges of horizontally scrollable containers, masked by white covers anchored to the content. Communicates "there's more this way" without an extra JS observer.Test plan
next buildgreen; 18 routes; no TS errors/benchmark, pickstage=rerankerandk=5, refresh — chips remain selected, URL contains?stage=reranker&k=5/benchmark(viewMode=table when PR feat(web): benchmark heatmap small-multiples — visual ranking per cell #81 merges; or current matrix view) —<thead>stays pinned below the chrome header/proteinstab and/jobsstatus persist on refresh