feat(ui): navigation renders the whole ancestor chain, not one level - #574
Merged
Conversation
The single-level back link under-reported a product three levels deep, and on the batch gallery its label and its destination disagreed outright: it read `road-signs` — the project's name — while landing on the project's Batches tab. Both halves were right on their own; only the chain says both. `patterns/Breadcrumb.tsx` replaces `patterns/BackLink.tsx`. Ancestors only, so the current page stays the `<h1>` and no crumb carries `aria-current`. Below `lg` the same items array collapses to `← <immediate parent>` through CSS on one DOM node per crumb — never a second list, so nothing is read twice aloud. The host spells every URL and the screen supplies every label: `PARENT` holds ids and does not fetch, while a project's name is behind a query `ui-core` makes. A screen omits a level it has no callback for rather than deadening it, which is what keeps an empty chain meaning "nothing to offer". `DatasetScreen.onBack` is deleted rather than migrated — the trunk is a project tab and no mount has passed it since the move.
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.
Closes #570.
Navigation renders the page's whole ancestor chain instead of the step above it.
What shipped
frontend/ui-core/src/patterns/Breadcrumb.tsxreplacespatterns/BackLink.tsx, in the sameslot above the page header.
← ProjectsProjects← road-signsProjects / road-signs← road-signs, landing on the Batches tabProjects / road-signs / BatchesThe gallery row is the defect. The label named the project while the destination was the
project's Batches tab, so the control said one place and went to another. Both halves were
right on their own — the tab is where somebody leaving a batch belongs, and naming the project
is the most one level can say — and only the chain says both.
<h1>, so no crumb carriesaria-current.lgthe same items array collapses to← <immediate parent>, the old look. It is CSSon one DOM node per crumb, never a second list, so nothing is announced twice.
title; the row never wraps at any width.<nav aria-label="Breadcrumb">, real buttons, keyboard-reachable. The focus ring is the baselayer's
:focus-visible— a per-component ring would be a second answer to one question.still means nothing to offer and a host that wires nothing renders no control.
Projectsand
Inferenceare unchanged: rail destinations get no breadcrumb.DatasetScreen.onBackis deleted rather than migrated. The trunk is a project tab and nomount has passed it since that move, so the prop — and the now-unused
useProjectquery behindits label — are gone.
Two premises in the dispatch that HEAD contradicted
PARENT.batchesalready carried?tab=batches,GalleryScreenalready used it, ande2e/navigation.spec.tsalready asserted it. Theresidual defect is the label/destination disagreement above. The step-8 audit for the same
class of bug on other sub-views found nothing else: ingest's parent is the project page,
which has no tab-specific home, and the project's is the list.
arrow,
PinBadge, an 8-char content hash and● annotated · Saved, in aminmax(0,1fr)grid track that already truncates to hold the navigation cluster on the bar's geometric
centre. Decision (Armando, 2026-08-12): leave it alone — the arrow already means up, and
crumbs would be paid for out of the frame's identity readout.
AnnotationPage.tsxisuntouched.
Three deviations from the dispatch, deliberate
BreadcrumbItem.onNavigateis required, not optional. Nothing in this repo produces anancestor it cannot navigate to; a screen omits such a level instead, which is what keeps the
empty-list rule true. A dead-text crumb has no producer, so it would ship untested. Widening
it later is one line.
PARENT. A crumb needs a label as well as a URL, and a project'sname is behind a query
ui-coremakes whileroutes.tsxholds ids and does not fetch (itsown module rule). So the host spells every URL and the screen supplies every label. What can
silently drift is a URL, and a URL still has exactly one spelling.
no pure function to call; the destinations are what the browser suite asserts, which is also
the only place a route change is a real thing.
Test plan
navigation.test.tsxrewritten (15 cases): the full chain in order with each crumb calling itsown destination; the collapsed slot naming the immediate parent, not the root; an empty list
rendering nothing; the truncation title; and per screen — chain drawn, parent callable, nothing
at all when the host wires nothing.
e2e/navigation.spec.tsgained three scenarios: every crumb of the gallery's chain reaching theURL it names; the
Batchescrumb landing withtab-batchesactuallyaria-selected(theredirect-that-moved-only-the-URL failure); and the
lgcollapse at 900px wide. That last oneasserts through both locator kinds —
getByRolereads the accessibility tree, which adisplay: nonecrumb is not in, andlocatorreads the DOM, which every crumb is still in. Thepair is what says the collapse is one list presented twice rather than two lists.
Mutation verification
Committed first, each step unconditional, each revert by its recorded patch with a clean-tree
assertion between cases.
index === 0instead ofindex === lastcollapses to the IMMEDIATE parent, not to the root, and the gallery's and ingest'sputs its immediate parent in the collapsed slot. The project row stayed green, honestly — with one crumb, index 0 is last.PARENT.batchesdrops?tab=batchesthe batch gallery returns to its parent from a cold openandthe Batches crumb lands with the Batches tab actually selected.Gate
bash scripts/check.shrun in stages against the harness's ~10-minute ceiling. Every stage'sexit code, verbatim:
docsfirst exited 2 ondocs-site/node_modules is missing— a fresh worktree, fixed bypnpm --dir docs-site install, not a code failure. #555 did not fire, so no baselinereproduction was needed.
Documentation
DESIGN.md→ Navigation rules rewritten: the chain replaces the single parent, plus theancestors-only rule and why, tabs-are-levels, the
lgcollapse, the host/screen split, andthe annotator's exemption.
navigate(-1)stays banned. The project-header list now names thebreadcrumb.
.agents/skills/frontend/information-architecture/SKILL.md: the back-link invariant becomesthe chain model, with a per-route table recording the batch route's tab-qualified ancestor.
Sitemap change: none — no screen moved, no tab or nav entry was added or removed; only the
navigation contract between existing routes changed.
docs/ui.mddescribes tabs,?asset=and the rail but never the back affordance, so it isuntouched and uncontradicted.
Found, not fixed
Nothing.
Flags for Armando
900 as a representative width below
lg; the app has no declared minimum viewport and therail's own behaviour at phone widths is out of this task's scope.
parentLabel's noun fallback now appears mid-chain, so a gallery whose project query isstill in flight reads
Projects / Project / Batchesfor one paint. That is the existingrule (never change width under a cursor) applied at a new position; if the doubled word reads
badly in practice, the fix is a per-level noun rather than a spinner.