Skip to content

Fix #149: keep the footer consistent on the bubble view - #343

Merged
taoeffect merged 3 commits into
masterfrom
fix/149-consistent-footer
Sep 2, 2026
Merged

Fix #149: keep the footer consistent on the bubble view#343
taoeffect merged 3 commits into
masterfrom
fix/149-consistent-footer

Conversation

@pieer

@pieer pieer commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Closes #149

Problem

The footer sat in a different vertical position on the bubble view than on the table view (and every other page).

The bubble view claimed its vertical space with a viewport magic number in the page template's inline <style>:

#bubble-view-root {
    height: calc(100vh - 25rem);
    margin-bottom: calc(0px - var(--page-space-bottom));
    ...
}

25rem (400px) does not know the real height of the navbar, the repo header or the notices above the graph, and the negative bottom margin cancelled the --page-space-bottom gap that .full.height reserves above the footer everywhere else. Measured against a running instance (836px viewport):

document height footer top gap above footer
table view 836px (= viewport) 744px the usual 64px
bubble view (before) 858px 767px, below the fold 0

So the bubble page overflowed the viewport by ~20px, the footer had to be scrolled to, and it was flush against the graph.

Fix

Stop measuring the viewport; participate in the flex layout instead — the same solution web_src/css/home.css documents at length for the landing page (#316 / #242 / #276). .full.height already grows to fill the viewport via flex: 1 0 auto, so making it and the wrappers down to the graph flex columns gives the graph box real free space to grow into:

  • #bubble-view-root takes that space with flex: 1 1 0. flex-basis: 0 is deliberate — FishboneGraph sizes its canvas from the box it is given, so a content-based basis would be circular — floored by min-height: 320px, which is MIN_SVG_HEIGHT in the component (a higher floor would re-create the overflow).
  • .f-fishbone-graph loses its own calc(100vh - 25rem) and fills the box (flex: 1 1 0; min-height: 0), so anything left over scrolls inside the graph rather than growing the page past the footer.
  • The rules moved out of the template's inline <style> into web_src/css/features/bubble-graph.css, scoped with :has(… :not([hidden])) so they only apply while the bubble view is the visible one (the table/article sections stay untouched).

No new percentage heights, no 100vh arithmetic to keep in sync with the header.

Verification

Verified in Chrome against a running instance (localhost:3000, subject example-subject), by measuring the layout with and without the new rules applied:

  • Bubble view after the fix: document height 836px for an 836px viewport, footer at 744–836px — identical to the table view, with the standard 64px gap above it.
  • Forcing the graph taller than the free space (simulating a short viewport): the page grows, the footer moves down with the content and sits directly below it — no overlap, no floating mid-page.
  • The stale svg height: 800px seen when switching table → bubble is pre-existing: it reproduces identically on the unpatched build, and is unrelated to this change.

make lint-css passes. FishboneGraph.vue is outside its file glob; running stylelint on it directly reports the same 28 pre-existing errors before and after this change.

Model used: Claude Opus 5 (1M context).

The bubble view sized its canvas with a viewport magic number --
"#bubble-view-root { height: calc(100vh - 25rem) }" plus a negative bottom
margin that cancelled ".full.height"'s "--page-space-bottom". Neither number
knows the real height of the navbar, the repo header or the notices above the
graph, so the page came out taller than the viewport (~20px on a 836px-tall
viewport here): the footer was pushed below the fold and sat flush against the
graph, while the same subject in table view had its footer pinned to the bottom
of the viewport with the usual 64px gap above it.

Use the flex layout the landing and auth pages already use (documented at
length in web_src/css/home.css): ".full.height" grows to fill the viewport via
"flex: 1 0 auto", so making it and the wrappers down to the graph flex columns
gives the graph box real free space to grow into. The box takes that space with
"flex: 1 1 0" -- a content-based basis would be circular, since FishboneGraph
sizes its canvas from the box it is given -- floored by a 320px min-height
(MIN_SVG_HEIGHT). The page is then exactly as tall as the viewport, or exactly
as tall as its content when the viewport is too short, and the footer follows
the content in both cases.

The rules live in web_src/css/features/bubble-graph.css instead of the page
template's inline <style>, and are scoped with ":has(...:not([hidden]))" so
they only apply while the bubble view is the visible one.

Verified in the browser against a running instance: the bubble page now ends at
the viewport bottom (836px document height for an 836px viewport) with the
footer at the same offset as the table view, and with the graph forced past the
free space the footer moves down with it instead of overlapping.

Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four ":has(... :not([hidden]))" selectors were spelled out twice, once for
"display: flex" and once for "flex: 1 0 auto", so a rename of the bubble
section class had to be made in two places to stay correct. The only reason for
the split was ".full.height", which must not lose its own flex value -- but
that value is "flex: 1 0 auto" (web_src/css/base.css), the very declaration the
second block sets, so merging the blocks changes nothing on the page.

Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pieer

pieer commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator Author

/review


AI review started.

@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown

Advanced AI Review

  • Type: Agentic (crush)
  • Model: glm-5.3
Click to expand review

I'll review these changes. First, let me gather context on the affected files and how they interact with the rest of the codebase.Now let me examine the FishboneGraph component's structure and sizing logic, and what gets mounted into #bubble-view-root.Let me check the base layout structure, the home.css/user.css precedent this PR follows, and how repo-history.ts initializes the sections.Let me check whether the existing history-bubble-root class on that element is used anywhere, and look at the footer template for completeness.One more check: does the compare page (or any other page) render FishboneGraph outside #bubble-view-root?# Review

I verified the flex chain against the actual DOM (base/head.tmpl:29.page-content.ui.container#repo-history-app.history-view-app.history-view-section--bubble#bubble-view-root), the hidden-attribute toggling in repo-history.ts:323-328 (matches :not([hidden]), so no display: flex vs [hidden] conflict), that MIN_SVG_HEIGHT = 320 (FishboneGraph.vue:182) matches the CSS floor, and that FishboneGraph is mounted only inside #bubble-view-root (repo-bubble-view.ts:5), so the auto-height fallback path is never exercised. The core fix is sound; the remaining issues are consistency/cleanup items.

1. 🟡 Dead class history-bubble-root while the new CSS styles the element by ID

  • Addressed
  • Dismissed

Addressed in 200d542 — the rule now selects .history-bubble-root; the id stays as the mount point for repo-bubble-view.ts.

custom/templates/shared/repo/bubble.tmpl:3-4:

<div id="bubble-view-root"
    class="history-bubble-root"

class="history-bubble-root" is referenced nowhere in the codebase (templates, CSS, JS, tests), while the new rule styles the same element via the ID (web_src/css/features/bubble-graph.css:61):

#bubble-view-root {

Either the dead class should be deleted, or — better — the CSS should use it, since every other selector in the new rule block is class-based and an ID selector carries (1,0,0) specificity that will beat any future utility-class override on this element:

.history-bubble-root {
  /* ... (contents of the #bubble-view-root rule unchanged) */
}

The ID itself must stay: repo-bubble-view.ts:5 and the template's data-* attributes use it as the mount point. Only the CSS selector changes.

2. ⚪️ The 320px floor is defined twice, kept in sync only by comments

  • Addressed
  • Dismissed

Dismissed — the suggested getComputedStyle(...) || 320 still keeps a second copy of the constant, so the indirection buys machinery rather than a single source of truth. Only the CSS side carried a cross-reference, so the note is not in two places; 200d542 adds the matching one next to MIN_SVG_HEIGHT so a change to either is made with the other in view.

web_src/css/features/bubble-graph.css:72:

  min-height: 320px;

duplicates FishboneGraph.vue:182:

const MIN_SVG_HEIGHT = 320;          // Never collapse the canvas below this

Both comments cross-reference the other file, but nothing enforces the sync — a future change to MIN_SVG_HEIGHT silently re-introduces the overflow this PR fixes (a higher JS value would again push the footer below the fold; a higher CSS value would starve the canvas). A single source of truth would be a custom property next to --page-space-bottom in base.css (which also satisfies stylelint's csstools/value-no-unknown-custom-properties import list):

/* base.css */
--bubble-graph-min-height: 320px; /* the canvas never draws smaller; keep in step with FishboneGraph.vue */
/* bubble-graph.css */
.history-bubble-root {
  min-height: var(--bubble-graph-min-height);
}
// FishboneGraph.vue
const MIN_SVG_HEIGHT = Number.parseFloat(getComputedStyle(document.documentElement)
  .getPropertyValue('--bubble-graph-min-height')) || 320;

If that indirection is deemed heavier than the problem, keeping the duplicated constant with the existing comments is defensible — but then a note that the two must change together belongs in one place, not two.

3. ⚪️ overflow: auto on the root is now unreachable

  • Addressed
  • Dismissed

Addressed in 200d542overflow: auto dropped, with a comment saying why the box can never scroll and why a scroll container here would only clip overlays.

web_src/css/features/bubble-graph.css:73:

  overflow: auto;

This was needed by the old design, where #bubble-view-root had a fixed height that its content could exceed. In the new layout the root's only child is a flex: 1 1 0; min-height: 0 item (FishboneGraph.vue:2257-2258) that always sizes exactly to the root, and any surplus (the legend under a MIN_SVG_HEIGHT canvas) already scrolls inside .f-fishbone-graph, which has its own overflow: auto (FishboneGraph.vue:2259). The horizontal bleed is also exactly cancelled by width: calc(100% + 2rem) with -1rem side margins. Dropping the declaration removes a scroll container that can never scroll (and a second potential scrollbar for keyboard focus navigation):

.history-bubble-root {
  display: flex;
  flex-direction: column;
  flex: 1 1 0;
  min-height: var(--bubble-graph-min-height);
  /* bleed over the container's horizontal padding, as before */
  margin-top: -1rem;
  margin-left: -1rem;
  margin-right: -1rem;
  width: calc(100% + 2rem);
}

4. ⚪️ The same header is called three different names in the new comments

  • Addressed
  • Dismissed

Addressed in 200d542 — "the repo header" everywhere (.secondary-nav), and the top-of-file comment now names the file's second purpose: layout the bubble view needs from its ancestors, which a scoped style cannot reach.

web_src/css/features/bubble-graph.css:27 vs :31 vs custom/templates/explore/repo_history.tmpl:137:

   between the repo header and the footer. It used to claim that space with a
...
   Neither number knows the real height of the navbar, the article header or the
   fills the space between the header and the footer through the page's flex

All three refer to the same .secondary-nav block (subject title + #subject-view-tabs). "Repo header" and "the article header" appearing six lines apart in one comment block invites exactly the confusion the comment is meant to prevent — pick one term (the element is rendered by custom/templates/repo/header.tmpl, so "repo header" seems right). Relatedly, the file's top-of-file comment (bubble-graph.css:1-5) still says the file exists for "styles shared between the graph's overlay components ... a rule needed by more than one of them", which no longer describes the page-layout section added below it; a one-line mention of the second purpose would keep it accurate.


Review generated using glm-5.3 via Z.AI. Comment /review to re-run.

Review follow-ups on #343, all cleanup on the CSS the fix added:

- style the mount point through ".history-bubble-root" instead of
  "#bubble-view-root". The class was on the element and used nowhere; the ID
  stays for repo-bubble-view.ts to mount on. Selecting by class keeps the
  file's specificity uniform at (0,1,0), so a utility class can still win.
- drop "overflow: auto" from that box. It was needed when the box had a fixed
  height its content could exceed; now its only child is "flex: 1 1 0;
  min-height: 0" and scrolls its own leftover, so the declaration only
  established a scroll container that can never scroll -- and would clip any
  overlay painted outside the box.
- call the ".secondary-nav" block "the repo header" everywhere instead of
  alternating with "the article header" six lines apart, and say in the
  file's top-of-file comment that it now also carries page layout the bubble
  view needs from its ancestors, which a scoped style cannot reach.
- note next to MIN_SVG_HEIGHT that the CSS floor is the same number, so a
  change to one is not made without the other.

Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pieer

pieer commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Approved

pieer added a commit that referenced this pull request Sep 2, 2026
Both branches edit the "=== SVG LAYOUT ===" block of FishboneGraph.vue: #343
annotated MIN_SVG_HEIGHT with the page-layout floor it has to stay in sync
with, and this branch moved the constant out to graph-viewport.ts so it can be
unit-tested. Merging #343 in now so this PR goes onto master cleanly once #343
is merged.

Kept this branch's block, and moved #343's cross-reference to where the
constant actually lives now. The matching comment in bubble-graph.css pointed
at FishboneGraph.vue, which no longer declares it, so it now names
graph-viewport.ts too -- otherwise the "change the two together" warning sends
the reader to the wrong file.

vitest 32/32, vue-tsc, eslint and stylelint all clean on the result.

Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@taoeffect
taoeffect merged commit 9870937 into master Sep 2, 2026
33 checks passed
@taoeffect
taoeffect deleted the fix/149-consistent-footer branch September 2, 2026 18:56
pieer added a commit that referenced this pull request Sep 2, 2026
Both conflicts are the cross-reference comments between the bubble
graph's canvas floor and the page-layout floor that #343 (Fix #149) and
this branch each touched.

#343 introduced MIN_SVG_HEIGHT in FishboneGraph.vue and pointed the CSS
comment at that file. This branch had already moved the constant to
graph-viewport.ts so it could be unit-tested, carrying the same
"change the two together" note with it (graph-viewport.ts:11-15). Kept
this branch's side in both files: master's `const MIN_SVG_HEIGHT = 320`
would now be a duplicate of the exported one, and nothing in
FishboneGraph.vue references it directly any more -- canvasHeightFor()
applies the floor. The CSS comment now names graph-viewport.ts, which is
where the number actually lives after the merge.

This is the re-check that reviews/348.md finding 7 asked for once both
branches landed. The sizing chain is coherent: .history-bubble-root is
"flex: 1 1 0; min-height: 320px" and .f-fishbone-graph is
"flex: 1 1 0; min-height: 0; overflow: auto", so the container height
comes from the page's flex layout rather than from the canvas measured
off it, and the re-measure guards added here cannot drive a feedback
loop through it.

Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
taoeffect pushed a commit that referenced this pull request Sep 2, 2026
* Fix #149: keep the footer consistent on the bubble view

The bubble view sized its canvas with a viewport magic number --
"#bubble-view-root { height: calc(100vh - 25rem) }" plus a negative bottom
margin that cancelled ".full.height"'s "--page-space-bottom". Neither number
knows the real height of the navbar, the repo header or the notices above the
graph, so the page came out taller than the viewport (~20px on a 836px-tall
viewport here): the footer was pushed below the fold and sat flush against the
graph, while the same subject in table view had its footer pinned to the bottom
of the viewport with the usual 64px gap above it.

Use the flex layout the landing and auth pages already use (documented at
length in web_src/css/home.css): ".full.height" grows to fill the viewport via
"flex: 1 0 auto", so making it and the wrappers down to the graph flex columns
gives the graph box real free space to grow into. The box takes that space with
"flex: 1 1 0" -- a content-based basis would be circular, since FishboneGraph
sizes its canvas from the box it is given -- floored by a 320px min-height
(MIN_SVG_HEIGHT). The page is then exactly as tall as the viewport, or exactly
as tall as its content when the viewport is too short, and the footer follows
the content in both cases.

The rules live in web_src/css/features/bubble-graph.css instead of the page
template's inline <style>, and are scoped with ":has(...:not([hidden]))" so
they only apply while the bubble view is the visible one.

Verified in the browser against a running instance: the bubble page now ends at
the viewport bottom (836px document height for an 836px viewport) with the
footer at the same offset as the table view, and with the graph forced past the
free space the footer moves down with it instead of overlapping.

Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: describe the bubble view's flex chain in one rule

The four ":has(... :not([hidden]))" selectors were spelled out twice, once for
"display: flex" and once for "flex: 1 0 auto", so a rename of the bubble
section class had to be made in two places to stay correct. The only reason for
the split was ".full.height", which must not lose its own flex value -- but
that value is "flex: 1 0 auto" (web_src/css/base.css), the very declaration the
second block sets, so merging the blocks changes nothing on the page.

Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fix #348: re-measure the bubble graph when its container resizes

The <svg> was sized from the container at mount and then only corrected by
a ResizeObserver whose bookkeeping could not tell a real resize from a
redelivery: it stored the CLAMPED layout width (min(w, 1100)) as "the last
width seen" and compared the next raw measurement against it, so every
delivery on a container wider than 1100px re-ran the layout and reset the
user's pan/zoom, while the mount path adopted the raw width and drew the
first frame at a width no later frame would use.

Measurement is now one path. graph-viewport.ts holds the pure functions
(canvas height, clamped layout width, measurable/changed tests) with unit
tests; the component keeps the RAW box it last measured, so the bail-out
compares like with like and an unchanged container costs one rect read and
stops. The observer stays on the CONTAINER, never on the <svg> it sizes,
so a taller canvas cannot enlarge what is observed, and it is disconnected
(with any pending rAF cancelled) on unmount.

Two triggers are added for the deliveries the observer cannot make. A
window resize and the tab returning to the foreground re-measure, because
a background tab's rendering is throttled and the resize that happened
there is coalesced away. And repo-history.ts dispatches
`repo:bubble-visible` after the switch to bubble view: the component
mounts as part of that switch, when its section can still be the hidden
0-height placeholder, so the box it measured at mount is not the box it is
drawn in. Both run the same bail-out and do nothing when nothing moved.

Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: style the bubble view root by class and drop its dead overflow

Review follow-ups on #343, all cleanup on the CSS the fix added:

- style the mount point through ".history-bubble-root" instead of
  "#bubble-view-root". The class was on the element and used nowhere; the ID
  stays for repo-bubble-view.ts to mount on. Selecting by class keeps the
  file's specificity uniform at (0,1,0), so a utility class can still win.
- drop "overflow: auto" from that box. It was needed when the box had a fixed
  height its content could exceed; now its only child is "flex: 1 1 0;
  min-height: 0" and scrolls its own leftover, so the declaration only
  established a scroll container that can never scroll -- and would clip any
  overlay painted outside the box.
- call the ".secondary-nav" block "the repo header" everywhere instead of
  alternating with "the article header" six lines apart, and say in the
  file's top-of-file comment that it now also carries page layout the bubble
  view needs from its ancestors, which a scoped style cannot reach.
- note next to MIN_SVG_HEIGHT that the CSS floor is the same number, so a
  change to one is not made without the other.

Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fix #348: compare the width the layout runs at, and keep a moved view

Review follow-up on #352.

sizeChanged() compared RAW widths, but nothing downstream consumes a raw
width: every layout input comes from the CLAMPED one. So a horizontal
resize between two widths above the cap (1200px -> 1300px) reported
"changed" and cost a full re-layout plus resetView() for a change no
layout input can see. Both sides now go through layoutWidthFor().

A genuine resize no longer re-frames a view the user has moved. The
re-measure path re-centres at 1:1, which is right for a view still where
the graph put it and wrong for a panned or zoomed one — and this branch
added three more ways to reach it. A d3 zoom event carrying a sourceEvent
marks the view as the user's; resetView() takes it back. The kept branch
still refreshes the pan bound and the zoom floor, which resetView() would
otherwise have done on its way past.

The re-measure triggers move into graph-viewport.ts
(registerRemeasureTriggers / observeContainerResize) and are registered at
the top of onMounted, before any await. The repo:bubble-visible handoff no
longer depends on this mount's awaits resolving ahead of the dispatcher's
— it worked only by microtask FIFO and would have broken silently the
first time an await was added above. The wiring now has unit tests, which
is where the bug lived; the event name is one exported constant.

Also: WIDTH_BREAKPOINT_MAX is MAX_LAYOUT_WIDTH, since every path clamps
now and a breakpoint above the clamp is unreachable (the widest screen
saturated at 0.86 and the wide end of every dial never engaged);
ensureBubbleView() claims the mount before its await, so the two callers
racing on the first switch cannot both get through; resetView()'s
zero-box retry goes through pendingRaf so unmount cancels it; and the
stale comment about the empty state in scheduleRemeasure() is corrected.

Co-authored-by: Pierre Schweiger <schweiger.pierre@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ensure footer is consistent across pages

2 participants