Skip to content

Commit e48ec36

Browse files
ndemiancclaude
andcommitted
fix(levelcode): stop the session card jumping on hover — float the actions
The hover detail (files · model · turns · actions) expanded in-flow, which grew the row and shoved every card below it down — a distracting "jump" as the mouse moved over the list. Now the detail is an absolutely-positioned drawer anchored to the card, so the resting row height never changes: hovering only fades the drawer in over the neighbouring rows, nothing reflows. - media/chat.html (the /sessions modal) and media/sessionsView.html (the sidebar): .sessrich is position:absolute in both states (zero flow space), so the card height is fixed; it fades/opens downward on hover, and the last row opens upward so the scroll container never clips it. Hovered card gets a z-index bump; the list carries a little bottom runway for a down-opening drawer near the end; reduced-motion drops the slide. - The shared SESSIONS-PURE render block is untouched — this is CSS only, so no drift and the card markup/tests are unchanged. - test/sessionsUi.test.js: a NO-JUMP invariant on both surfaces (the drawer is absolute, the last row opens upward, and it is not the old display:none expand). Full suite 30 files green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 383b0d4 commit e48ec36

3 files changed

Lines changed: 35 additions & 9 deletions

File tree

‎extensions/levelcode-ai/media/chat.html‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,28 +1078,36 @@
10781078
--cc-warn: var(--vscode-editorWarning-foreground, var(--vscode-foreground)); --cc-danger: var(--vscode-errorForeground); }
10791079
.sesspanel .sessbar { display: flex; align-items: center; gap: 9px; padding: 12px 14px 10px; border-bottom: 1px solid var(--cc-line); }
10801080
.sesspanel .sesstitle { font-weight: 600; font-size: 14px; }
1081-
.sesspanel .sesslist { flex: 1 1 auto; overflow-y: auto; padding: 2px 12px 14px; }
1081+
.sesspanel .sesslist { flex: 1 1 auto; overflow-y: auto; padding: 2px 12px 60px; } /* runway for a down-opening drawer near the end */
10821082
.sesspanel .sessbucket { font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--cc-text3); margin: 12px 4px 7px; }
10831083
.sesspanel .sessempty { padding: 44px 18px; text-align: center; color: var(--cc-text3); font-size: 13px; line-height: 1.6; }
10841084
.sesscard { position: relative; background: var(--cc-bg); border: 1px solid var(--cc-line); border-radius: 7px; padding: 9px 11px; margin-bottom: 7px; cursor: pointer; transition: border-color .12s, transform .12s, box-shadow .12s; }
1085-
.sesscard:hover, .sesscard:focus-visible, .sesscard:focus-within { border-color: var(--cc-accent); transform: translateY(-1px); box-shadow: 0 6px 18px rgba(0,0,0,.22); outline: none; }
1085+
.sesscard:hover, .sesscard:focus-visible, .sesscard:focus-within { border-color: var(--cc-accent); transform: translateY(-1px); box-shadow: 0 6px 18px rgba(0,0,0,.22); outline: none; z-index: 20; }
10861086
.sesscard.warn { border-left: 2px solid var(--cc-warn); }
10871087
.sesscard.err { border-left: 2px solid var(--cc-danger); }
10881088
.sesscard.res { border-left: 2px solid var(--cc-accent); }
10891089
.sesscard .sesstop { display: flex; align-items: center; gap: 8px; }
10901090
.sesscard .sessname { font-weight: 600; font-size: 13.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; }
10911091
.sesscard .sessstar { color: var(--cc-accent); font-size: 13px; }
10921092
.sesscard .sesssub { margin-top: 4px; font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 11.5px; color: var(--cc-text3); }
1093-
.sesscard .sessrich { display: none; margin-top: 8px; }
1094-
.sesscard:hover .sessrich, .sesscard:focus-within .sessrich { display: block; }
1093+
/* Actions/detail float as a hover DRAWER anchored to the card, so the card height never changes and the
1094+
list no longer jumps. Opens downward; the last card opens upward so the scroll container never clips it. */
1095+
.sesscard .sessrich { position: absolute; left: -1px; right: -1px; top: calc(100% - 3px); z-index: 30;
1096+
margin: 0; padding: 9px 11px 10px; background: var(--cc-bg); border: 1px solid var(--cc-accent);
1097+
border-radius: 8px; box-shadow: 0 12px 28px rgba(0,0,0,.34);
1098+
opacity: 0; visibility: hidden; transform: translateY(-4px); pointer-events: none;
1099+
transition: opacity .12s ease, transform .12s ease, visibility 0s linear .13s; }
1100+
.sesscard:hover .sessrich, .sesscard:focus-within .sessrich { opacity: 1; visibility: visible; transform: none; pointer-events: auto; transition: opacity .12s ease, transform .12s ease; }
1101+
.sesscard:last-child .sessrich { top: auto; bottom: calc(100% - 3px); transform: translateY(4px); }
1102+
.sesscard:last-child:hover .sessrich, .sesscard:last-child:focus-within .sessrich { transform: none; }
10951103
.sesscard .sessfiles { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
10961104
.sesscard .sesschip { font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 11px; color: var(--cc-text2); background: var(--cc-surface); border: 1px solid var(--cc-line); border-radius: 4px; padding: 1px 6px; }
10971105
.sesscard .sessmeta { display: flex; align-items: center; gap: 7px; margin-top: 6px; font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 11.5px; color: var(--cc-text3); }
10981106
.sesscard .sessacts { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 9px; }
10991107
.sesscard .sessbtn { font: inherit; font-size: 11.5px; color: var(--cc-text); background: var(--cc-surface); border: 1px solid var(--cc-line); border-radius: 5px; padding: 3px 9px; cursor: pointer; }
11001108
.sesscard .sessbtn.pri { color: #fff; background: var(--cc-accent); border-color: transparent; }
11011109
.sesscard .sessbtn:hover { border-color: var(--cc-accent); }
1102-
@media (prefers-reduced-motion: reduce) { .sesscard { transition: none; } .sesscard:hover, .sesscard:focus-visible, .sesscard:focus-within { transform: none; } }
1110+
@media (prefers-reduced-motion: reduce) { .sesscard, .sesscard .sessrich { transition: none; } .sesscard:hover, .sesscard:focus-visible, .sesscard:focus-within, .sesscard .sessrich, .sesscard:last-child .sessrich { transform: none; } }
11031111
</style>
11041112
</head>
11051113
<body>

‎extensions/levelcode-ai/media/sessionsView.html‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
.sessview { display: flex; flex-direction: column; height: 100vh; }
1919
.sessnew { margin: 2px 10px 8px; padding: 7px 10px; background: var(--cc-bg); border: 1px solid var(--cc-line); border-radius: 6px; color: var(--cc-text); font: inherit; font-size: 12.5px; cursor: pointer; text-align: center; }
2020
.sessnew:hover { border-color: var(--cc-accent); }
21-
.sesslist { flex: 1 1 auto; overflow-y: auto; padding: 0 10px 12px; }
21+
.sesslist { flex: 1 1 auto; overflow-y: auto; padding: 0 10px 76px; } /* runway so a down-opening drawer near the end is not clipped */
2222
.sessbucket { display: flex; justify-content: space-between; align-items: baseline; font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--cc-text3); margin: 12px 4px 7px; }
2323
.sessbucket .cnt { letter-spacing: 0; }
2424
.sessempty { padding: 44px 16px; text-align: center; color: var(--cc-text3); font-size: 13px; line-height: 1.6; }
@@ -34,16 +34,26 @@
3434
.sesscard .sessname { font-weight: 400; font-size: 13.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1; }
3535
.sesscard .sessstar { flex: 0 0 auto; color: var(--cc-accent); font-size: 12px; }
3636
.sesscard .sesssub { margin: 2px 0 0 16px; font-size: 12px; color: var(--cc-text3); }
37-
.sesscard .sessrich { display: none; margin: 6px 0 0 16px; }
38-
.sesscard:hover .sessrich, .sesscard:focus-within .sessrich { display: block; }
37+
/* Actions/detail float as a hover DRAWER anchored to the card, so the row height never changes and the
38+
list no longer jumps when the mouse moves over it. Opens downward; the last row opens upward so the
39+
scroll container never clips it. */
40+
.sesscard .sessrich { position: absolute; left: 6px; right: 6px; top: calc(100% - 3px); z-index: 30;
41+
margin: 0; padding: 8px 10px 9px; background: var(--cc-bg); border: 1px solid var(--cc-line);
42+
border-radius: 7px; box-shadow: 0 10px 24px rgba(0,0,0,.30);
43+
opacity: 0; visibility: hidden; transform: translateY(-4px); pointer-events: none;
44+
transition: opacity .11s ease, transform .11s ease, visibility 0s linear .12s; }
45+
.sesscard:hover, .sesscard:focus-within { z-index: 20; }
46+
.sesscard:hover .sessrich, .sesscard:focus-within .sessrich { opacity: 1; visibility: visible; transform: none; pointer-events: auto; transition: opacity .11s ease, transform .11s ease; }
47+
.sesscard:last-child .sessrich { top: auto; bottom: calc(100% - 3px); transform: translateY(4px); }
48+
.sesscard:last-child:hover .sessrich, .sesscard:last-child:focus-within .sessrich { transform: none; }
3949
.sesscard .sessfiles { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
4050
.sesscard .sesschip { font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 11px; color: var(--cc-text2); background: color-mix(in srgb, var(--cc-text3) 12%, transparent); border-radius: 4px; padding: 1px 6px; }
4151
.sesscard .sessmeta { display: flex; align-items: center; gap: 7px; margin-top: 6px; font-family: var(--vscode-editor-font-family, ui-monospace, monospace); font-size: 11px; color: var(--cc-text3); }
4252
.sesscard .sessacts { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 9px; }
4353
.sesscard .sessbtn { font: inherit; font-size: 11px; color: var(--cc-text2); background: transparent; border: 1px solid var(--cc-line); border-radius: 5px; padding: 2px 8px; cursor: pointer; }
4454
.sesscard .sessbtn.pri { color: #fff; background: var(--cc-accent); border-color: transparent; }
4555
.sesscard .sessbtn:hover { border-color: var(--cc-accent); color: var(--cc-text); }
46-
@media (prefers-reduced-motion: reduce) { .sesscard { transition: none; } }
56+
@media (prefers-reduced-motion: reduce) { .sesscard, .sesscard .sessrich { transition: none; } .sesscard .sessrich, .sesscard:last-child .sessrich { transform: none; } }
4757
</style>
4858
</head>
4959
<body>

‎extensions/levelcode-ai/test/sessionsUi.test.js‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,4 +186,12 @@ test('VIEW: theme-true across the three kinds and reduced-motion-safe', () => {
186186
assert.match(view, /@media \(prefers-reduced-motion: reduce\)[\s\S]{0,120}\.sesscard[\s\S]{0,80}transition:\s*none/);
187187
});
188188

189+
test('NO-JUMP: the hover detail is an absolute drawer on BOTH surfaces (stable row height, not an in-flow expand)', () => {
190+
for (const [name, src] of [['modal', html], ['sidebar', view]]) {
191+
assert.match(src, /\.sesscard \.sessrich \{[^}]*position: absolute/, name + ': the drawer is absolutely positioned, so revealing it never reflows the row');
192+
assert.match(src, /\.sesscard:last-child \.sessrich \{[^}]*bottom:/, name + ': the last row opens upward so the scroll container never clips it');
193+
assert.ok(!/\.sesscard \.sessrich \{ display: none;/.test(src), name + ': not the old display:none → block expand that pushed the list around');
194+
}
195+
});
196+
189197
console.log('sessionsUi: ' + n + ' tests passed');

0 commit comments

Comments
 (0)