mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
fix(web): stop session row from shifting on hover (#1319)
The session row swapped the relative time for the kebab on hover using display:none, which dropped the time from layout and appended the kebab at the end. Because the time is variable-width (2h / 5m / just now) and the kebab is a fixed 26px, the right region reflowed: status badges shifted and the title's truncation changed, causing visible jitter. Place the time and kebab in one inline-grid cell (grid-area:1/1) and swap them via visibility instead, so the slot width stays max(time width, 26px) across hover. The badges and title no longer reflow. Documented in the design system (section 07 Session row) as a trailing action slot rule.
This commit is contained in:
parent
5322c63889
commit
e8ab7ca786
4 changed files with 80 additions and 30 deletions
5
.changeset/fix-web-session-row-hover.md
Normal file
5
.changeset/fix-web-session-row-hover.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Fix the sidebar session row shifting its title and status badges when hovered.
|
||||
|
|
@ -2188,20 +2188,35 @@ export function auth(token: string) {
|
|||
</div></div>
|
||||
|
||||
<h3 class="sub">Session row</h3>
|
||||
<p>A session row is an inset rounded pill, structured as: <code>status slot → title → time → attention Badge → kebab</code>.</p>
|
||||
<p>A session row is an inset rounded pill, structured as: <code>status slot → title → attention Badge → trailing action slot</code>.</p>
|
||||
<table class="dt">
|
||||
<thead><tr><th>Part</th><th>Rule</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Container</td><td><code>margin: 1px 6px; padding: 7px 10px; radius-md</code>; hover = <code>surface-sunken</code>; active = <code>accent-soft</code> + <code>inset 0 0 0 1px accent-bd</code></td></tr>
|
||||
<tr><td>Status slot (lead)</td><td>fixed <code>--sb-gutter</code> width; running = <code>Spinner</code> sm, otherwise unread = 7px accent dot</td></tr>
|
||||
<tr><td>Title</td><td>flex:1 with truncation; double-click enters inline rename (compact input, not Input)</td></tr>
|
||||
<tr><td>Time</td><td>mono xs, <code>fg-faint</code>; yields to the kebab on hover</td></tr>
|
||||
<tr><td>Attention Badge</td><td><code>Badge</code> sm: info (needs answer) / warning (needs approval) / danger (aborted)</td></tr>
|
||||
<tr><td>kebab</td><td><code>IconButton</code> sm, shown on hover; dropdown uses <code>Menu/MenuItem</code></td></tr>
|
||||
<tr><td>Attention Badge</td><td><code>Badge</code> sm: info (needs answer) / warning (needs approval) / danger (aborted); <b>always shown</b>, not toggled on hover</td></tr>
|
||||
<tr><td>Trailing action slot (trail)</td><td>fixed slot shared by the time ↔ kebab (see below)</td></tr>
|
||||
<tr><td>Archive confirmation</td><td>replaces the title area, <code>Button</code> sm (danger confirm / secondary cancel)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4 class="mini">Trailing action slot</h4>
|
||||
<p>The session row's rightmost is a <b>fixed-width</b> action slot: at rest it shows the relative time (mono xs, <code>fg-faint</code>); on hover it swaps <b>in place</b> to the kebab (<code>IconButton</code> sm).</p>
|
||||
<ul class="clean">
|
||||
<li>The time and kebab are stacked with <code>display:inline-grid</code> + <code>grid-area:1/1</code> and swapped on hover via <code>visibility</code> — <b>never <code>display:none</code></b>.</li>
|
||||
<li>The slot width is always <code>max(time width, IconButton sm 26px)</code>, so the right region's total width is unchanged across hover — badges don't shift, the title's visible character count doesn't change, <b>no jitter</b>.</li>
|
||||
<li>A session awaiting an answer (info Badge) likewise shows no time on hover: the slot holds only the kebab.</li>
|
||||
</ul>
|
||||
<div class="code"><div class="code-bar"><span class="d"></span><span class="d"></span><span class="d"></span><span class="fn">SessionRow.vue</span></div><pre>.act { display: inline-grid; flex: none; align-items: center; justify-items: center; }
|
||||
.act .ts, .act .kebab { grid-area: 1 / 1; }
|
||||
.act .kebab { visibility: hidden; }
|
||||
.se:hover .act .kebab, .act:has(.kebab.open) .kebab { visibility: visible; }
|
||||
.se:hover .act .ts, .act:has(.kebab.open) .ts { visibility: hidden; }</pre></div>
|
||||
<div class="callout info"><span class="ico">i</span><div>
|
||||
<b>Why the time and kebab share a slot and use <code>visibility</code> rather than <code>display:none</code>:</b> the time is variable-width (<code>2h</code> / <code>5m</code> / <code>just now</code> …), the kebab a fixed 26px. If hover used <code>display:none</code> to drop the time and append the kebab, the right region would reflow — badges shift and the title's truncation changes, causing visible jitter. Sharing one slot and swapping via <code>visibility</code> keeps the slot width constant; this is a hard rule for sidebar list-row stability.
|
||||
</div></div>
|
||||
|
||||
<h3 class="sub">Workspace group</h3>
|
||||
<p>The group head and session rows share <code>--sb-*</code>: folder icon (open/closed) → name → path subtitle, with the kebab and "+" revealed on hover.</p>
|
||||
<ul class="clean">
|
||||
|
|
|
|||
|
|
@ -2188,20 +2188,35 @@ export function auth(token: string) {
|
|||
</div></div>
|
||||
|
||||
<h3 class="sub">Session row</h3>
|
||||
<p>A session row is an inset rounded pill, structured as: <code>status slot → title → time → attention Badge → kebab</code>.</p>
|
||||
<p>A session row is an inset rounded pill, structured as: <code>status slot → title → attention Badge → trailing action slot</code>.</p>
|
||||
<table class="dt">
|
||||
<thead><tr><th>Part</th><th>Rule</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Container</td><td><code>margin: 1px 6px; padding: 7px 10px; radius-md</code>; hover = <code>surface-sunken</code>; active = <code>accent-soft</code> + <code>inset 0 0 0 1px accent-bd</code></td></tr>
|
||||
<tr><td>Status slot (lead)</td><td>fixed <code>--sb-gutter</code> width; running = <code>Spinner</code> sm, otherwise unread = 7px accent dot</td></tr>
|
||||
<tr><td>Title</td><td>flex:1 with truncation; double-click enters inline rename (compact input, not Input)</td></tr>
|
||||
<tr><td>Time</td><td>mono xs, <code>fg-faint</code>; yields to the kebab on hover</td></tr>
|
||||
<tr><td>Attention Badge</td><td><code>Badge</code> sm: info (needs answer) / warning (needs approval) / danger (aborted)</td></tr>
|
||||
<tr><td>kebab</td><td><code>IconButton</code> sm, shown on hover; dropdown uses <code>Menu/MenuItem</code></td></tr>
|
||||
<tr><td>Attention Badge</td><td><code>Badge</code> sm: info (needs answer) / warning (needs approval) / danger (aborted); <b>always shown</b>, not toggled on hover</td></tr>
|
||||
<tr><td>Trailing action slot (trail)</td><td>fixed slot shared by the time ↔ kebab (see below)</td></tr>
|
||||
<tr><td>Archive confirmation</td><td>replaces the title area, <code>Button</code> sm (danger confirm / secondary cancel)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4 class="mini">Trailing action slot</h4>
|
||||
<p>The session row's rightmost is a <b>fixed-width</b> action slot: at rest it shows the relative time (mono xs, <code>fg-faint</code>); on hover it swaps <b>in place</b> to the kebab (<code>IconButton</code> sm).</p>
|
||||
<ul class="clean">
|
||||
<li>The time and kebab are stacked with <code>display:inline-grid</code> + <code>grid-area:1/1</code> and swapped on hover via <code>visibility</code> — <b>never <code>display:none</code></b>.</li>
|
||||
<li>The slot width is always <code>max(time width, IconButton sm 26px)</code>, so the right region's total width is unchanged across hover — badges don't shift, the title's visible character count doesn't change, <b>no jitter</b>.</li>
|
||||
<li>A session awaiting an answer (info Badge) likewise shows no time on hover: the slot holds only the kebab.</li>
|
||||
</ul>
|
||||
<div class="code"><div class="code-bar"><span class="d"></span><span class="d"></span><span class="d"></span><span class="fn">SessionRow.vue</span></div><pre>.act { display: inline-grid; flex: none; align-items: center; justify-items: center; }
|
||||
.act .ts, .act .kebab { grid-area: 1 / 1; }
|
||||
.act .kebab { visibility: hidden; }
|
||||
.se:hover .act .kebab, .act:has(.kebab.open) .kebab { visibility: visible; }
|
||||
.se:hover .act .ts, .act:has(.kebab.open) .ts { visibility: hidden; }</pre></div>
|
||||
<div class="callout info"><span class="ico">i</span><div>
|
||||
<b>Why the time and kebab share a slot and use <code>visibility</code> rather than <code>display:none</code>:</b> the time is variable-width (<code>2h</code> / <code>5m</code> / <code>just now</code> …), the kebab a fixed 26px. If hover used <code>display:none</code> to drop the time and append the kebab, the right region would reflow — badges shift and the title's truncation changes, causing visible jitter. Sharing one slot and swapping via <code>visibility</code> keeps the slot width constant; this is a hard rule for sidebar list-row stability.
|
||||
</div></div>
|
||||
|
||||
<h3 class="sub">Workspace group</h3>
|
||||
<p>The group head and session rows share <code>--sb-*</code>: folder icon (open/closed) → name → path subtitle, with the kebab and "+" revealed on hover.</p>
|
||||
<ul class="clean">
|
||||
|
|
|
|||
|
|
@ -205,8 +205,6 @@ defineExpose({ closeMenu });
|
|||
<span v-else class="t" @dblclick.stop="startRename">{{ session.title }}</span>
|
||||
</div>
|
||||
|
||||
<span class="ts">{{ session.time }}</span>
|
||||
|
||||
<!-- Pending tags — coloured per kind, shown even when the row isn't
|
||||
active. "Answer" = an askUserQuestion is waiting; "Approve" = a
|
||||
permission request is waiting. The session's lifecycle status drives
|
||||
|
|
@ -241,18 +239,24 @@ defineExpose({ closeMenu });
|
|||
</Badge>
|
||||
</Tooltip>
|
||||
|
||||
<!-- Kebab button (visible on hover) -->
|
||||
<IconButton
|
||||
ref="kebabRef"
|
||||
v-if="!renaming"
|
||||
class="kebab"
|
||||
:class="{ open: menuOpen }"
|
||||
size="sm"
|
||||
:label="t('sidebar.options')"
|
||||
@click.stop="toggleMenu($event)"
|
||||
>
|
||||
<Icon name="dots-horizontal" size="sm" />
|
||||
</IconButton>
|
||||
<!-- Trailing action slot: the relative time and the kebab share one grid
|
||||
cell and swap via `visibility` (never display:none), so the slot
|
||||
width is identical in hover and rest. The badges and title therefore
|
||||
don't reflow on hover — see design-system §07 "Session row". -->
|
||||
<span class="act">
|
||||
<span class="ts">{{ session.time }}</span>
|
||||
<IconButton
|
||||
ref="kebabRef"
|
||||
v-if="!renaming"
|
||||
class="kebab"
|
||||
:class="{ open: menuOpen }"
|
||||
size="sm"
|
||||
:label="t('sidebar.options')"
|
||||
@click.stop="toggleMenu($event)"
|
||||
>
|
||||
<Icon name="dots-horizontal" size="sm" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Kebab dropdown — teleported to <body> and position:fixed so it escapes
|
||||
|
|
@ -350,16 +354,27 @@ defineExpose({ closeMenu });
|
|||
color: var(--color-text-faint);
|
||||
font-size: var(--text-xs);
|
||||
font-family: var(--font-mono);
|
||||
flex: none;
|
||||
}
|
||||
.se:hover .ts { display: none; }
|
||||
|
||||
/* Kebab button — hidden until hover. Sits at the RIGHT of the timestamp
|
||||
and attention badge so it is the right-most element. `.se .kebab` out-
|
||||
specificities IconButton's display so the hidden default actually wins. */
|
||||
.se .kebab { display: none; }
|
||||
.se:hover .kebab,
|
||||
.kebab.open { display: inline-flex; }
|
||||
/* Trailing action slot: time and kebab share one grid cell (grid-area:1/1).
|
||||
Both stay in the layout and swap via `visibility` (never display:none), so
|
||||
the slot width = max(time width, IconButton sm 26px) is identical in hover
|
||||
and rest — the badges and title don't reflow, eliminating hover jitter.
|
||||
`.act .kebab` out-specificities IconButton's own display so the hidden
|
||||
default wins. */
|
||||
.act {
|
||||
display: inline-grid;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
}
|
||||
.act .ts,
|
||||
.act .kebab { grid-area: 1 / 1; }
|
||||
.act .kebab { visibility: hidden; }
|
||||
.se:hover .act .kebab,
|
||||
.act:has(.kebab.open) .kebab { visibility: visible; }
|
||||
.se:hover .act .ts,
|
||||
.act:has(.kebab.open) .ts { visibility: hidden; }
|
||||
.kebab.open { color: var(--color-text); background: var(--color-surface-sunken); }
|
||||
|
||||
/* Fixed + anchored to the ⋯ button via inline style (see positionMenu); the menu
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue