fix(route): overflow-y: visible — hover cards never clip vertically again

The root cause of the hover-clipping we kept band-aiding with larger
track-heights: a scroll container with overflow-x: auto implicitly
clips on the perpendicular axis too. Explicit overflow-y: visible
lets cards expand above and below the track freely.

Implementation matches the spec's belt-and-braces pattern. New layered
markup:

  .rr-wrap          → position: relative, anchors fades
  .rr-scroll        → overflow-x: auto + overflow-y: visible,
                      padding 60/60/80, scroll-padding 60/60 sides
  .rr-scroll-inner  → structural, no layout effect
  .rr-track         → positioned at the inner wrapper

The padding-top: 60px / padding-bottom: 80px on .rr-scroll gives cards
room to grow above and below the track without ever hitting a clip
boundary, even on browsers that mis-handle the overflow-x/y mix.

Edge fades reposition: top: 60px / bottom: 80px (was 0 / 16) so they
only cover the track itself, not the overflow padding zones above and
below where hover-expanded cards now live.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Hvid 2026-05-12 14:35:02 +02:00
parent b4df8e10f1
commit 83503fe7a3
2 changed files with 20 additions and 9 deletions

View file

@ -45,7 +45,8 @@
"Bash(pnpm db:seed)", "Bash(pnpm db:seed)",
"Bash(curl -s -c /tmp/jar.txt -d \"email=lars@virk2.dk&password=cab123\" http://localhost:4321/login -o /dev/null -i)", "Bash(curl -s -c /tmp/jar.txt -d \"email=lars@virk2.dk&password=cab123\" http://localhost:4321/login -o /dev/null -i)",
"Bash(curl -s -c /tmp/jar.txt -d \"email=lars@virk2.dk&password=cab123\" http://localhost:4321/login -o /dev/null)", "Bash(curl -s -c /tmp/jar.txt -d \"email=lars@virk2.dk&password=cab123\" http://localhost:4321/login -o /dev/null)",
"Bash(curl -s -b /tmp/jar.txt http://localhost:4321/roadmap)" "Bash(curl -s -b /tmp/jar.txt http://localhost:4321/roadmap)",
"Bash(grep -nE \"\\\\.rr-fade-left, \\\\.rr-fade-right|rr-fade-left \\\\{|rr-fade-right \\\\{\" src/components/RoadmapRoute.astro)"
] ]
} }
} }

View file

@ -72,6 +72,7 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex
<!-- The route — desktop horizontal --> <!-- The route — desktop horizontal -->
<div class="rr-wrap rr-desktop"> <div class="rr-wrap rr-desktop">
<div class="rr-scroll" id="rr-scroll"> <div class="rr-scroll" id="rr-scroll">
<div class="rr-scroll-inner">
<div class="rr-track" style={`width: ${layout.trackWidth}px; height: 420px;`}> <div class="rr-track" style={`width: ${layout.trackWidth}px; height: 420px;`}>
<svg class="rr-path" width={layout.trackWidth} height="420" aria-hidden="true"> <svg class="rr-path" width={layout.trackWidth} height="420" aria-hidden="true">
<defs> <defs>
@ -115,6 +116,7 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex
))} ))}
</div> </div>
</div> </div>
</div>
<div class="rr-fade-left" id="rr-fade-l" aria-hidden="true"></div> <div class="rr-fade-left" id="rr-fade-l" aria-hidden="true"></div>
<div class="rr-fade-right" id="rr-fade-r" aria-hidden="true"></div> <div class="rr-fade-right" id="rr-fade-r" aria-hidden="true"></div>
@ -239,18 +241,24 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex
/* ── Desktop route ──────────────────────────────────────────────── */ /* ── Desktop route ──────────────────────────────────────────────── */
.rr-wrap { position: relative; } .rr-wrap { position: relative; }
.rr-scroll { .rr-scroll {
/* overflow-x: auto + overflow-y: visible is the only thing that lets
hovered cards expand above/below the track without being clipped.
The previous fix bandaged it with extra trackHeight; this is the
real fix. The .rr-scroll-inner wrapper is the spec-recommended
belt-and-braces in case a browser misbehaves on this combination. */
overflow-x: auto; overflow-x: auto;
overflow-y: visible;
scroll-snap-type: x mandatory; scroll-snap-type: x mandatory;
scroll-behavior: smooth; scroll-behavior: smooth;
scrollbar-width: none; scrollbar-width: none;
/* 60px is enough for a 220px card centred under a dot 60px from /* Top/bottom give cards room to grow above/below the track. The 60px
the edge — card extends 50px past the dot, sits inside the sides give the first/last cards room when fully scrolled. */
padded container. */ padding: 60px 60px 80px;
padding: 0 60px 8px;
scroll-padding-left: 60px; scroll-padding-left: 60px;
scroll-padding-right: 60px; scroll-padding-right: 60px;
} }
.rr-scroll::-webkit-scrollbar { display: none; } .rr-scroll::-webkit-scrollbar { display: none; }
.rr-scroll-inner { /* structural — keeps the track on its own layer */ }
.rr-track { position: relative; } .rr-track { position: relative; }
.rr-path { position: absolute; top: 0; left: 0; pointer-events: none; } .rr-path { position: absolute; top: 0; left: 0; pointer-events: none; }
@ -376,11 +384,13 @@ const initialShippingX = lastShippingIndex >= 0 ? layout.itemX[lastShippingIndex
margin: 0; margin: 0;
} }
/* Edge fades */ /* Edge fades cover only the track itself — the top/bottom padding
zones (60/80) on .rr-scroll exist so hover cards can overflow there
without clipping, so the fades shouldn't paint over them. */
.rr-fade-left, .rr-fade-right { .rr-fade-left, .rr-fade-right {
position: absolute; position: absolute;
top: 0; top: 60px;
bottom: 16px; bottom: 80px;
pointer-events: none; pointer-events: none;
transition: opacity .25s ease; transition: opacity .25s ease;
} }