Skip to content

feat(web): URL-sync filters + sticky table headers + scroll-shadow indicators#82

Merged
github-actions[bot] merged 2 commits intodevelopfrom
feat/web-data-tables-ux
May 8, 2026
Merged

feat(web): URL-sync filters + sticky table headers + scroll-shadow indicators#82
github-actions[bot] merged 2 commits intodevelopfrom
feat/web-data-tables-ux

Conversation

@frapercan
Copy link
Copy Markdown
Owner

Why

Three small frictions, fixed in one PR.

  1. Filters forget themselves on refresh. Pick a stage chip on /benchmark, refresh — the chip resets. Worse: you can't share a deep-link to "PROTEA at stage=reranker, K=5".
  2. Long tables lose their header on scroll. With the sticky h-16 chrome header above, <thead> should stay visible too — currently it scrolls away with the body, and you forget which column is which.
  3. No scroll affordance on overflowing tables. The user has to discover horizontal scroll by trial.

What

URL-sync filters — apps/web/lib/useUrlParam.ts

  • New useUrlParam(key, default) and useUrlNumber(key, default) hooks: two-way bind a query-string key to React state. router.replace with scroll: false so chip clicks don't pollute history or scroll the page. Defaults are dropped from the URL (clean copy/paste).
  • Wired in:
    • /benchmarkstage, k, eval_set
    • /jobsstatus
    • /proteinstab
  • Refresh / share-link now lands on the same view.

Sticky data-table headers — .protea-thead-sticky

  • position: sticky; top: 4rem (clears the h-16 chrome header) + white background + 1px shadow.
  • Applied to:
    • /benchmark matrix table <thead>
    • /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. Communicates "there's more this way" without an extra JS observer.
  • Applied to the three wrappers above.

Test plan

  • next build green; 18 routes; no TS errors
  • Backend untouched
  • Visual: open /benchmark, pick stage=reranker and k=5, refresh — chips remain selected, URL contains ?stage=reranker&k=5
  • Switch to default stage/K — those keys disappear from the URL
  • Scroll long table on /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
  • Narrow viewport — fading shadow appears on the right edge of the wrapper, fades as you scroll right
  • /proteins tab and /jobs status persist on refresh

frapercan added 2 commits May 8, 2026 18:18
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.
@github-actions github-actions Bot enabled auto-merge (squash) May 8, 2026 16:19
@github-actions github-actions Bot merged commit af48d96 into develop May 8, 2026
13 checks passed
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant