Files
deepseek-harness/packages/client/ui-sidebar/src/client/SidebarRoot.module.css
imccyu ea8b1178cd feat(web): session list one-list, hover card, row menus, rename, manual ordering
Sidebar session list grows the figma 239-10458 feature set and the
workspace/session browsing region moves wholesale into ui-workspace:

- Group-by menu (WorkSpace / In one list): flat mode lists every session
  top-level, strictly newest-first; the choice persists across reloads.
- Session rows get a 500ms hover detail card (title / relative time /
  status line) and a ... menu (Rename / Fork session / Delete session,
  visual-only for now); workspace headers get ... with Rename (wired) and
  Delete workspace (visual-only).
- workspace.rename RPC: trims, rejects duplicate titles on the create
  chain (workspace-name-conflict), no-op on same title; modal dialog with
  client-side duplicate pre-check.
- workspace.insertSessionBefore RPC (DOM-insertBefore semantics, omitted
  anchor appends): HTML5 drag reorder of root sessions inside a workspace
  group; order truth stays host-side, the view refreshes from the
  response/changed frame.
- Activity pinning removed: the session/event touchSession chain is gone;
  workspace accounts are manually owned (new sessions prepend, explicit
  reordering only). Contracts and tests updated, api catalog regenerated.
- ui-sidebar reduced to the column shell (brand, fold state machine, New
  Session, Settings) exposing one sidebar.workspaces hole with a two-fact
  owner share {wide, expandSidebar}; ui-workspace owns the whole region
  (header, search, grouped/flat lists, dialogs, drag) plus the picker via
  a shared WorkspaceCreateFlow. The old sidebar.workspace picker slot and
  its deferral indirection are gone.
- ui-primitives: Menu gains label entries, danger rows, and
  closeOnPointerLeave; new HoverCard (portaled, open-delay, disabled
  guard). Hover card and row menu never coexist.
2026-07-26 00:55:17 +08:00

247 lines
5.9 KiB
CSS

/* Sidebar column (figma 133:7629): vertical stack, padding 12/6, sidebar
fill + 1px right border painted by the layout column. Collapse is a
slide + crossfade, not a morph: the content holds its frozen expanded
layout (inline width set by the component) and fades in place (.fading)
while the sliding column (AppFrame grid tracks) clips it; the rail layout
(.collapsed) only applies after the fade settles, so nothing reflows
mid-slide. */
.root {
display: flex;
flex-direction: column;
height: 100%;
padding: 6px 12px;
box-sizing: border-box;
background: var(--dsw-specific-sidebar-fill);
color: var(--dsw-alias-label-primary);
font-size: 14px;
}
/* Rail geometry (figma rail spec): 36x36 control boxes centered in the 56px
rail (10px side padding), 12px vertical rhythm, 18px from the rail top to
the whale's box (24px to the 24-wide whale glyph itself). */
.root.collapsed {
padding: 18px 10px 6px;
}
/* Collapse phase 1: the whole frozen-width content fades out in place over
150ms; at settle the children unmount/snap to the rail layout. */
.fading > * {
opacity: 0;
transition: opacity 150ms var(--ds-ease-in-out);
}
/* Wide-only content fades back in on expand remount. */
.wide {
animation: wide-in 200ms var(--ds-ease-in-out);
}
@keyframes wide-in {
from { opacity: 0; }
}
/* Rail controls hold hidden while the column slides shut, then fade in over
the slide's tail: .railIn applies at settle (150ms into the 0.3s AppFrame
track transition), so a 100ms delay + 150ms fade starts just before the
slide ends (250ms) and finishes at 400ms; `backwards` keeps them at
opacity 0 through the delay. Only a live collapse gets .railIn — a
refresh straight into the collapsed state renders statically. */
.railIn .iconButton,
.railIn .newSession,
.railIn .foot {
animation: rail-in 150ms var(--ds-ease-in-out) 100ms backwards;
}
@keyframes rail-in {
from { opacity: 0; }
}
/* Logo row (figma pad (4,8,4,8)): brand left, panel toggle right-anchored —
the toggle is the rail's expand control and slides in with the right edge. */
.logoRow {
flex: none;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
height: 60px;
padding: 8px 0 8px 4px;
margin-bottom: 16px;
box-sizing: border-box;
overflow: hidden;
}
.collapsed .logoRow {
height: 36px;
padding: 0;
margin-bottom: 12px;
}
/* Brand group (figma I133:7632): the full wordmark rides the text ink
(figma-flows ruling: main-screen instance is black; blue is brand
emphasis only). */
.brand {
flex: 1;
min-width: 0;
display: inline-flex;
align-items: center;
overflow: hidden;
}
.iconButton {
flex: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 50%;
padding: 0;
background: transparent;
cursor: pointer;
color: var(--dsw-alias-label-secondary);
}
.iconButton:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
.collapsed .iconButton {
width: 36px;
height: 36px;
}
/* Rail logo swap: collapsed, the toggle rests as the whale mark (brand ink,
no hover circle) and hovering reveals the panel icon — the expand
affordance (figma sidebar-hover flow). Expanded it is a plain panel icon. */
.collapsed .toggle .panelIcon {
display: none;
}
.collapsed .toggle:hover .panelIcon {
display: inline;
}
.collapsed .toggle:hover .railFish {
display: none;
}
/* Rail icons ride the primary ink (figma rail spec); expanded keeps the
secondary icon-button ink. */
.collapsed .iconButton {
color: var(--dsw-alias-label-primary);
}
/* New Session: 38px capsule (figma 133:7634); collapsed it renders as the
rail's plain icon control. */
.newSession {
flex: none;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 38px;
padding: 8px 16px;
margin: 0 2px 20px; /* bottom: former headerBlock padBottom 12 + root gap 8 */
box-sizing: border-box;
border: 1px solid var(--dsw-alias-border-l2);
border-radius: 24px;
background: var(--dsw-alias-button-elevated-fill);
color: var(--dsw-alias-label-primary);
font-size: 14px;
font-weight: 500;
line-height: 22px;
cursor: pointer;
overflow: hidden;
}
.newSession:hover {
background: var(--dsw-alias-button-floating-hover);
}
.collapsed .newSession {
height: 36px;
padding: 0;
margin: 0 0 12px;
gap: 0;
border-color: transparent;
background: transparent;
}
.collapsed .newSession:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
.newSessionLabel {
max-width: 200px;
overflow: hidden;
white-space: nowrap;
}
.collapsed .newSessionLabel {
max-width: 0;
}
/* Region seat: always mounted so the foot never moves; the browser inside
handles its own wide/rail content. */
.regionArea {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Foot: settings entry (figma 133:7668, 49 hug): the former 18/10 vertical
margins fold into the row so the hover pill spans the full 49px. */
.foot {
flex: none;
display: flex;
align-items: center;
gap: 8px;
height: 49px;
margin: 8px 0 0; /* + 49px row + root padBottom 6 keeps the old 57px band */
padding: 0 2px 0 6px;
border-radius: 12px;
cursor: pointer;
overflow: hidden;
color: var(--dsw-alias-label-primary);
}
.foot:hover {
background: var(--dsw-alias-interactive-bg-hover);
}
/* Rail settings: the same 36x36 circle box as the other rail controls. */
.collapsed .foot {
width: 36px;
height: 36px;
margin: 18px 0 10px;
justify-content: center;
gap: 0;
padding: 0;
border-radius: 50%;
}
.footLabel {
max-width: 120px;
overflow: hidden;
white-space: nowrap;
}
.collapsed .footLabel {
max-width: 0;
}
@media (prefers-reduced-motion: reduce) {
.wide,
.fading > *,
.railIn .iconButton,
.railIn .newSession,
.railIn .foot {
transition: none;
animation: none;
}
}