---
phase: 01-federation-mesh-hardening
plan: 12
type: execute
wave: 7
depends_on: []
files_modified:
- neode-ui/src/views/web5/Web5ConnectedNodes.vue
- neode-ui/src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts
autonomous: true
requirements: [UIFIX-02]
gap_closure: true
must_haves:
truths:
- "On a wide viewport the connected-nodes card's height is set by its row sibling, not by how many nodes are in the list — adding nodes makes the inner list scroll instead of making the row taller (UIFIX-02)"
- "The inner list scrolls within the matched height: with more rows than fit, a scrollbar appears inside the card and the card stays put"
- "With a short sibling the card still has a usable list height rather than collapsing to its header and tabs (UIFIX-02 empty edge, sibling half)"
- "With zero connected nodes the card renders its existing empty/loading row and does not collapse (UIFIX-02 empty edge, list half)"
- "All three tabs — trusted, observers, requests — share the same scroll behaviour, so switching tabs never changes the card's height (UIFIX-02 adjacency edge)"
- "The stacked single-column layout below the row breakpoint is unchanged: the list keeps its existing capped height and its existing scroll"
prohibitions:
- statement: "Nothing outside the connected-nodes card's own height and overflow behaviour may change — the card's glass styling, padding, header, tab strip, row markup, counts, and every animation stay byte-identical, and no sibling card in any Web5 row is restyled to make the fix work"
category: safety
artifacts:
- path: neode-ui/src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts
provides: "Structural pin on the scroll contract for all three tab panes"
min_lines: 30
key_links:
- from: neode-ui/src/views/web5/Web5ConnectedNodes.vue
to: neode-ui/src/views/web5/Web5.vue
via: "the card is a min-height-zero flex column whose scroll pane contributes no intrinsic height at the row breakpoint, so the grid row is sized by the sibling and stretch gives the card that height"
pattern: "overflow-y-auto"
---
Make the connected-nodes list obey the row: its height tracks the taller sibling beside it and the
list scrolls inside that height, instead of growing until every node fits.
Purpose: UIFIX-02 is a BLOCKER, and it is a regression of an earlier request ("was still meant to
scroll"). Quick task 260729-je5 made the list fill the card's height; what is missing is the other
half — the list must not *drive* the card's height. Today all three tab panes carry
`max-h-72 xl:max-h-none`, so at the `xl` breakpoint where the row becomes two columns the cap is
lifted and nothing bounds the list: it grows to fit every row, stretches the grid row, and the
scrollbar the user expects never appears.
Output: a bounded, sibling-matched card with an internal scroll at the row breakpoint, an unchanged
stacked layout below it, and a test that pins the contract so a future cleanup cannot undo it again.
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
@.planning/PROJECT.md
@.planning/STATE.md
@.planning/phases/01-federation-mesh-hardening/01-UI-SPEC.md
@neode-ui/src/views/web5/Web5.vue
## Artifacts this phase produces
Created or changed by **this plan**:
| Symbol | Kind | File |
|---|---|---|
| scroll-contract classes on the three tab panes | changed template classes | `neode-ui/src/views/web5/Web5ConnectedNodes.vue` |
| row-breakpoint height floor on the card root | changed template classes | same |
| `neode-ui/src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts` | new vitest suite | new file |
Task 1: End-to-end — the trusted pane scrolls at a sibling-matched height
neode-ui/src/views/web5/Web5ConnectedNodes.vue, neode-ui/src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts
- `neode-ui/src/views/web5/Web5ConnectedNodes.vue` lines 1-135: the card root
(`glass-card p-6 scroll-mt-24 flex flex-col`), the desktop and mobile header blocks, the four-tab
strip, and the three `v-show` tab panes at lines 57, 90 and 120 — all three currently carrying
`space-y-2 flex-auto min-h-0 overflow-y-auto max-h-72 xl:max-h-none`. Also read the loading and
empty rows inside the trusted pane so you know what renders when the list is empty.
- `neode-ui/src/views/web5/Web5.vue` lines 57-74: the three `grid grid-cols-1 xl:grid-cols-2 gap-6`
rows. The connected-nodes card is the left item of the first row and `Web5NodeVisibility` is its
right sibling. Confirm no `items-start`/`self-start` is applied anywhere on that row — grid's
default `align-items: stretch` is what makes the sibling-matched height work, and this plan must
not add or remove alignment utilities on the row.
- `neode-ui/src/views/web5/Web5NodeVisibility.vue` — read only far enough to see roughly how tall
it renders (it is the sibling whose height the card must adopt). Do not modify it.
- `neode-ui/src/views/dashboard/__tests__/keepAliveTabs.test.ts` — the house convention for a
structural DOM/class pin test in this repo (this is the file the standing rule names as
must-stay-green; read it for its mounting and assertion style, do not change it).
- Mounting the component and reading the trusted pane's class list: it has `overflow-y-auto`, has
`min-h-0`, and has no class that removes its height bound at the row breakpoint.
- The same three assertions hold for the observers pane and the requests pane.
- The pane keeps a capped height below the row breakpoint (the stacked layout is unchanged).
- The card root is a flex column with a height floor at the row breakpoint, so a short sibling
cannot collapse the list area.
- With an empty node list the pane still renders (the existing empty/loading row is present) and
the pane element is still in the tree.
Write the test file first and confirm it fails.
In `Web5ConnectedNodes.vue`, change only the height/overflow contract:
On each of the three tab panes, replace the current sizing classes so that below the row
breakpoint nothing changes (keep the existing capped height and `overflow-y-auto`, keep basis
`auto` so the auto-height stacked column still sizes to content), and at the row breakpoint the
pane becomes a zero-basis growing flex child with no height cap — `xl:flex-1 xl:basis-0
xl:max-h-none` alongside the existing `min-h-0 overflow-y-auto`. Zero basis is the whole trick:
it makes the pane contribute nothing to the card's intrinsic height, so the grid row is sized by
the sibling alone, `align-items: stretch` gives the card that row height, and `flex-1` then hands
the leftover height to the pane, which scrolls inside it.
On the card root, keep `glass-card p-6 scroll-mt-24 flex flex-col` exactly as it is and add
`min-h-0` plus a row-breakpoint height floor (`xl:min-h-[20rem]`) so a sibling shorter than the
header-plus-tabs block still leaves a usable, scrolling list area rather than a collapsed strip.
Choose the floor to sit close to today's stacked cap so the visual weight of the card is familiar.
Change nothing else. Do not touch the header blocks, the tab strip, the per-row markup, the count
badges, the pulse dot on the requests tab, any `v-show`/`v-if` condition, any script logic, or any
class on `Web5.vue`'s grid rows. Do not add a scrollbar style — the list already scrolls with the
house default below the breakpoint and must look identical above it.
cd neode-ui && test -f src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts && npx vitest run src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts
- The test file exists and `cd neode-ui && npx vitest run src/views/web5/__tests__/Web5ConnectedNodesScroll.test.ts` exits 0 (the `test -f` guard is required — `vitest.config.ts` sets `passWithNoTests: true`, so a missing file would pass vacuously).
- `grep -c 'xl:max-h-none' neode-ui/src/views/web5/Web5ConnectedNodes.vue` equals 3 and each of those three lines also matches `xl:basis-0`.
- `grep -c 'flex-auto' neode-ui/src/views/web5/Web5ConnectedNodes.vue` equals 0.
- `grep -c 'max-h-72' neode-ui/src/views/web5/Web5ConnectedNodes.vue` equals 3 — the stacked cap is untouched.
- `grep -c 'xl:min-h-' neode-ui/src/views/web5/Web5ConnectedNodes.vue` equals 1.
- `git diff --stat -- neode-ui/src/views/web5/Web5.vue` reports no change.
- `git diff -- neode-ui/src/views/web5/Web5ConnectedNodes.vue | grep -c '^[-+].*