mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(gui): polish sidebar layout, wordmark, tooltip, and fonts to figma
Fixed sidebar width (never concedes), figma session-row rail (16px twist + status slots, triangle arrows, mount fade), exact brand wordmark svg with tooltip'd rail controls, form controls inheriting the app font stack, and inlined the twist button reset since the tsdown CSS pipeline drops composes.
This commit is contained in:
@@ -24,12 +24,38 @@
|
||||
background: var(--dsw-alias-interactive-bg-active);
|
||||
}
|
||||
|
||||
/* Two-line row: the leading slot (folder/chevron), title, and trailing
|
||||
actions all top-align on the 20px first text line (figma cell) — content
|
||||
is 42px (20 + 2 + 20), so 6px vertical padding centers the block. */
|
||||
.projectRow {
|
||||
height: 54px;
|
||||
align-items: flex-start;
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.projectRow .rowActions {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Session cell (figma): pad 8, adjacent 16px twist + status slots, then a 4px
|
||||
gap to the title — the slots butt together, so the row gap is zeroed and
|
||||
the title carries its own margins. */
|
||||
.sessionRow {
|
||||
height: 34px;
|
||||
gap: 0;
|
||||
/* Mount fade: session rows appear by unfolding a group (or the tree
|
||||
mounting). Stable row keys keep already-visible rows from replaying it. */
|
||||
animation: row-in 150ms var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
.sessionRow .title {
|
||||
margin: 0 6px 0 4px;
|
||||
}
|
||||
|
||||
@keyframes row-in {
|
||||
from { opacity: 0; }
|
||||
}
|
||||
|
||||
.slot {
|
||||
@@ -47,11 +73,20 @@
|
||||
color: var(--dsw-alias-state-business-primary);
|
||||
}
|
||||
|
||||
/* Project leading slot: folder by default, chevron on row hover. */
|
||||
/* Project leading slot: folder by default, expand arrow on row hover. */
|
||||
.projectRow .chevron { display: none; }
|
||||
.projectRow:hover .chevron { display: inline-flex; }
|
||||
.projectRow:hover .folder { display: none; }
|
||||
|
||||
/* Expand arrow (filled triangle): points right closed, rotates to point down open. */
|
||||
.arrow {
|
||||
transition: transform 150ms var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
.arrowOpen {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.projectText {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -131,22 +166,25 @@
|
||||
}
|
||||
|
||||
/* Session expand twist occupies the leading 16px slot; keep a spacer when absent
|
||||
so titles align across sibling rows. */
|
||||
so titles align across sibling rows. Duplicates the .iconButton reset instead
|
||||
of `composes:` — the tsdown CSS-modules pipeline drops composes mappings, which
|
||||
left the raw UA button box showing. */
|
||||
.twist {
|
||||
composes: iconButton;
|
||||
width: 16px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* "L" connector slot (figma arrow 14:3071): 16x16, glyph right-aligned. */
|
||||
.cornerSlot {
|
||||
flex: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
color: var(--dsw-alias-label-caption);
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.twist:hover {
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
/* Chevrons and tree twists ride the caption grey (#ADB2B8); the folder glyph
|
||||
@@ -156,3 +194,11 @@
|
||||
.twist {
|
||||
color: var(--dsw-alias-label-caption);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.sessionRow,
|
||||
.arrow {
|
||||
animation: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,15 @@
|
||||
*/
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
IconChevronDownOutline14, IconChevronRightOutline14,
|
||||
IconEllipsisOutline16, IconFolderClose16, IconFolderOpen16, IconPlusOutline16,
|
||||
IconTreeCorner8x10, StateDot,
|
||||
IconTriangleRightFill14, StateDot,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { ProjectRow, SessionRow } from './tree.ts'
|
||||
import { formatRelativeTime } from './tree.ts'
|
||||
import css from './Rows.module.css'
|
||||
|
||||
/** Indent step per tree level: 16px slot + 6px gap (figma). */
|
||||
const INDENT_STEP = 22
|
||||
/** Indent step per tree level: one 16px slot (figma session cell). */
|
||||
const INDENT_STEP = 16
|
||||
|
||||
/**
|
||||
* Project (workspace) row: 54px, folder + title + session count; hover
|
||||
@@ -38,7 +37,7 @@ export function ProjectRowItem({ row, active, onToggle, onCreate }: {
|
||||
{row.expanded ? <IconFolderOpen16 /> : <IconFolderClose16 />}
|
||||
</span>
|
||||
<span className={clsx(css.slot, css.chevron)}>
|
||||
{row.expanded ? <IconChevronDownOutline14 /> : <IconChevronRightOutline14 />}
|
||||
<IconTriangleRightFill14 className={clsx(css.arrow, row.expanded && css.arrowOpen)} />
|
||||
</span>
|
||||
<span className={css.projectText}>
|
||||
<span className={css.title}>{row.label}</span>
|
||||
@@ -79,17 +78,16 @@ export function SessionRowItem({ row, selected, now, onOpen, onToggle }: {
|
||||
onOpen: () => void
|
||||
onToggle: () => void
|
||||
}) {
|
||||
// Rail (figma sub-cell slot sequence): twist slot, always-reserved state
|
||||
// slot (opacity-0 slots keep their 22px in figma, so titles align whether
|
||||
// or not the dot is lit), then the L connector on child rows. Extra depth
|
||||
// rides the left padding: indent spacers = depth - 1.
|
||||
// Rail (figma session cell: pad 8, twist slot 16, status slot 16, gap 4 to
|
||||
// the title): both slots are always reserved so titles align whether or not
|
||||
// the twist/dot is lit. Extra depth rides the left padding.
|
||||
return (
|
||||
<div
|
||||
className={clsx(css.sessionRow, selected && css.selected)}
|
||||
role="treeitem"
|
||||
aria-selected={selected}
|
||||
{...(row.hasChildren ? { 'aria-expanded': row.expanded } : {})}
|
||||
style={{ paddingLeft: 8 + Math.max(0, row.depth - 1) * INDENT_STEP }}
|
||||
style={{ paddingLeft: 8 + row.depth * INDENT_STEP }}
|
||||
onClick={onOpen}
|
||||
>
|
||||
{row.hasChildren
|
||||
@@ -100,16 +98,11 @@ export function SessionRowItem({ row, selected, now, onOpen, onToggle }: {
|
||||
aria-label={row.expanded ? 'Collapse' : 'Expand'}
|
||||
onClick={(e) => { e.stopPropagation(); onToggle() }}
|
||||
>
|
||||
{row.expanded ? <IconChevronDownOutline14 /> : <IconChevronRightOutline14 />}
|
||||
<IconTriangleRightFill14 className={clsx(css.arrow, row.expanded && css.arrowOpen)} />
|
||||
</button>
|
||||
)
|
||||
: <span className={css.slot} />}
|
||||
<span className={css.slot}>{row.running && <StateDot state="ongoing" />}</span>
|
||||
{row.depth > 0 && (
|
||||
<span className={css.cornerSlot} data-tree-corner="">
|
||||
<IconTreeCorner8x10 />
|
||||
</span>
|
||||
)}
|
||||
<span className={css.title}>{row.title}</span>
|
||||
<span className={css.time}>{formatRelativeTime(row.updatedAt, now)}</span>
|
||||
<span className={css.rowActions}>
|
||||
|
||||
@@ -1,41 +1,62 @@
|
||||
/* Sidebar column (figma 133:7629): vertical stack, padding 16/6, sidebar
|
||||
fill + 1px right border painted by the layout column. Collapse morphs in
|
||||
place: the four control rows persist into the 56px rail (one icon each,
|
||||
x-converged by the shrinking column), geometry rides the deepsuite curve
|
||||
while wide-only content cross-fades 200ms; explicit margins own the
|
||||
vertical rhythm in both states so every gap can transition. */
|
||||
/* 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 16px;
|
||||
padding: 6px 12px;
|
||||
box-sizing: border-box;
|
||||
background: var(--dsw-specific-sidebar-fill);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
font-size: 14px;
|
||||
transition: padding var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
/* 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-top: 14px;
|
||||
padding: 18px 10px 6px;
|
||||
}
|
||||
|
||||
/* Wide-only content: fades ahead of the geometry (200ms vs 300ms) and
|
||||
unmounts once the collapse settles; remounts fade back in. */
|
||||
/* 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);
|
||||
transition: opacity 200ms var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
.collapsed .wide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@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 .searchButton,
|
||||
.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 {
|
||||
@@ -45,23 +66,19 @@
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
height: 60px;
|
||||
padding: 8px 4px;
|
||||
padding: 8px 0 8px 4px;
|
||||
margin-bottom: 16px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
height var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
padding var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
margin var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
.collapsed .logoRow {
|
||||
height: 24px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Brand group (figma I133:7632): fish + wordmark ride the text ink
|
||||
/* 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 {
|
||||
@@ -69,28 +86,9 @@
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wordmark {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* HARNESS badge (figma 34:10358): 14px tall, mono 11/500 on primary fill. */
|
||||
.badge {
|
||||
flex: none;
|
||||
padding: 0 3px;
|
||||
border-radius: 2px;
|
||||
background: var(--dsw-alias-label-primary);
|
||||
color: var(--dsw-alias-label-primary-inverted);
|
||||
font-family: var(--ds-font-family-code);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.iconButton {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
@@ -104,9 +102,6 @@
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
transition:
|
||||
width var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
height var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
.iconButton:hover {
|
||||
@@ -114,12 +109,33 @@
|
||||
}
|
||||
|
||||
.collapsed .iconButton {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
/* New Session: 38px capsule (figma 133:7634) morphing into the rail's plain
|
||||
icon control — border and fill fade with the label. */
|
||||
/* 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;
|
||||
@@ -128,24 +144,17 @@
|
||||
gap: 6px;
|
||||
height: 38px;
|
||||
padding: 8px 16px;
|
||||
margin-bottom: 20px; /* former headerBlock padBottom 12 + root gap 8 */
|
||||
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: 510;
|
||||
font-weight: 500;
|
||||
line-height: 22px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
height var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
padding var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
margin var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
gap var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
border-color var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
background-color 200ms var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
.newSession:hover {
|
||||
@@ -153,9 +162,9 @@
|
||||
}
|
||||
|
||||
.collapsed .newSession {
|
||||
height: 24px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
margin-bottom: 8px;
|
||||
margin: 0 0 12px;
|
||||
gap: 0;
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
@@ -169,7 +178,6 @@
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
transition: max-width var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
.collapsed .newSessionLabel {
|
||||
@@ -191,16 +199,12 @@
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
color: var(--dsw-alias-label-tertiary);
|
||||
transition:
|
||||
height var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
padding var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
margin var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
.collapsed .sectionHeader {
|
||||
height: 24px;
|
||||
height: 36px;
|
||||
padding-left: 0;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sectionLabel {
|
||||
@@ -211,8 +215,8 @@
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
/* Search input: 38px capsule (figma 133:7649) morphing into the rail's
|
||||
search control. Upstream binds a dedicated design-system variable (light
|
||||
/* Search input: 38px capsule (figma 133:7649); collapsed it renders as the
|
||||
rail's search control. Upstream binds a dedicated design-system variable (light
|
||||
#F1F3F5 / dark #1B1B1C) matching no shipped alias — a component token
|
||||
pinned to the static scale mirrors it (ruled compliant: indirect via
|
||||
custom property, upstream-variable equivalent). */
|
||||
@@ -223,7 +227,7 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 38px;
|
||||
margin-bottom: 12px; /* former listArea gap 4 + own 8 (spec padB12 to the first cell) */
|
||||
margin: 0 2px 12px; /* bottom: former listArea gap 4 + own 8 (spec padB12 to the first cell) */
|
||||
padding: 0 14px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
@@ -231,13 +235,6 @@
|
||||
background: var(--dsh-search-input-fill);
|
||||
color: var(--dsw-alias-label-caption);
|
||||
overflow: hidden;
|
||||
transition:
|
||||
height var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
padding var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
margin var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
gap var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
border-color var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
background-color 200ms var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
:global(body[data-ds-dark-theme]) .search {
|
||||
@@ -245,9 +242,9 @@
|
||||
}
|
||||
|
||||
.collapsed .search {
|
||||
height: 24px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
margin-bottom: 8px;
|
||||
margin: 0 0 12px;
|
||||
gap: 0;
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
@@ -261,8 +258,6 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
padding: 0;
|
||||
@@ -272,9 +267,11 @@
|
||||
}
|
||||
|
||||
.collapsed .searchButton {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.collapsed .searchButton:hover {
|
||||
@@ -366,39 +363,41 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Foot: settings entry (figma 133:7668). Left padding lands the 14px glyph
|
||||
on the rail's icon axis when collapsed. */
|
||||
/* 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: 29px;
|
||||
margin: 18px 0 10px; /* former root gap 8 + own 10 above; root padBottom 6 below */
|
||||
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);
|
||||
transition:
|
||||
padding var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
gap var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
.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 0 0 5px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.footLabel {
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
transition: max-width var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
.collapsed .footLabel {
|
||||
@@ -406,16 +405,12 @@
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.root,
|
||||
.wide,
|
||||
.logoRow,
|
||||
.iconButton,
|
||||
.newSession,
|
||||
.newSessionLabel,
|
||||
.sectionHeader,
|
||||
.search,
|
||||
.foot,
|
||||
.footLabel {
|
||||
.fading > *,
|
||||
.railIn .iconButton,
|
||||
.railIn .newSession,
|
||||
.railIn .searchButton,
|
||||
.railIn .foot {
|
||||
transition: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@@ -6,28 +6,32 @@
|
||||
* state, and rows are derived in render via useMemo (slot design section 6:
|
||||
* derived data is a pure function, no materializing store).
|
||||
*
|
||||
* Collapse is a morph, not a swap: the four control rows persist into the
|
||||
* 56px rail (collapse/new session/new workspace/search, one icon each, same
|
||||
* top-down order as their expanded rows) and animate their geometry on the
|
||||
* deepsuite curve, while wide-only content (brand, labels, input, tree)
|
||||
* cross-fades out and unmounts once the collapse settles — dropping the
|
||||
* sessions subscription. Rail search expands and focuses the search box.
|
||||
* Collapse is a slide + crossfade: the content freezes at its expanded
|
||||
* width (inline style) and fades out in place while the sliding column
|
||||
* (AppFrame grid tracks) clips it — nothing reflows mid-slide. At settle
|
||||
* the wide-only content (brand, labels, input, tree) unmounts, dropping
|
||||
* the sessions subscription, and the control rows snap to the 56px rail
|
||||
* (one icon each, same top-down order) fading in as the slide ends. Rail
|
||||
* search expands and focuses the search box.
|
||||
*/
|
||||
import { Fragment, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
FishLogo,
|
||||
BrandWordmark, FishLogo,
|
||||
IconCloseFill14, IconNewChatOutline16, IconPanelLeftOutline16, IconPersonalizationOutline16,
|
||||
IconProjectAddOutline16, IconSearchOutline16, IconSettingsOutline14,
|
||||
Menu,
|
||||
Menu, Tooltip,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
import type { SidebarRootComponentProps } from './contract/slots.ts'
|
||||
import { deriveRows } from './tree.ts'
|
||||
import { ProjectRowItem, SessionRowItem } from './Rows.tsx'
|
||||
import css from './SidebarRoot.module.css'
|
||||
|
||||
/** Wide-content unmount delay; matches --ds-transition-duration-slow (0.3s). */
|
||||
const COLLAPSE_SETTLE_MS = 300
|
||||
/** Wide-content unmount delay; matches the 150ms wide-content fade-out. */
|
||||
const COLLAPSE_SETTLE_MS = 150
|
||||
|
||||
/** Column slide length (--ds-transition-duration-slow): rail-search focus waits it out — focus() forces a synchronous layout and would jank the slide. */
|
||||
const EXPAND_SLIDE_MS = 300
|
||||
|
||||
const GROUP_BY_ITEMS = [
|
||||
{ id: 'workspace', label: 'WorkSpace' },
|
||||
@@ -134,7 +138,7 @@ function SessionTree({ useSessions, onOpen, onCreate, query }: SessionTreeProps)
|
||||
* @param props - composed slot props (runtime share + injected callbacks, contract/slots.ts).
|
||||
* @returns the sidebar element tree.
|
||||
*/
|
||||
export function SidebarRoot({ collapsed, useSessions, onOpen, onCreate, onToggleSidebar }: SidebarRootComponentProps) {
|
||||
export function SidebarRoot({ collapsed, width, useSessions, onOpen, onCreate, onToggleSidebar }: SidebarRootComponentProps) {
|
||||
// The query outlives the tree and the input (both wide-only) so collapsing
|
||||
// does not silently drop an in-progress filter.
|
||||
const [query, setQuery] = useState('')
|
||||
@@ -150,72 +154,98 @@ export function SidebarRoot({ collapsed, useSessions, onOpen, onCreate, onToggle
|
||||
}, [collapsed])
|
||||
const wide = !collapsed || !settled
|
||||
|
||||
// Freeze the content at its expanded width while it fades out (collapsed
|
||||
// && wide): the sliding column then clips it instead of reflowing it. The
|
||||
// rail layout (.collapsed styles) only applies once the fade settles.
|
||||
const lastWideWidth = useRef(width)
|
||||
if (!collapsed) lastWideWidth.current = width
|
||||
|
||||
// Rail-in only crossfades a live collapse: a refresh straight into the
|
||||
// collapsed state renders the rail statically (no delay-hidden icons).
|
||||
const everWide = useRef(!collapsed)
|
||||
if (!collapsed) everWide.current = true
|
||||
|
||||
// Rail search = expand + land in the search box: the flag arms before the
|
||||
// expand toggle; once expanded the input is mounted and takes focus.
|
||||
const [searchOnExpand, setSearchOnExpand] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!collapsed && searchOnExpand) {
|
||||
searchInput.current?.focus()
|
||||
setSearchOnExpand(false)
|
||||
const timer = window.setTimeout(() => {
|
||||
searchInput.current?.focus({ preventScroll: true })
|
||||
setSearchOnExpand(false)
|
||||
}, EXPAND_SLIDE_MS)
|
||||
return () => { window.clearTimeout(timer) }
|
||||
}
|
||||
}, [collapsed, searchOnExpand])
|
||||
|
||||
return (
|
||||
<div className={clsx(css.root, collapsed && css.collapsed)}>
|
||||
<div
|
||||
className={clsx(css.root, !wide && css.collapsed, !wide && everWide.current && css.railIn, collapsed && wide && css.fading)}
|
||||
style={wide ? { width: collapsed ? lastWideWidth.current : width } : undefined}
|
||||
>
|
||||
<div className={css.logoRow}>
|
||||
{wide && (
|
||||
<span className={clsx(css.brand, css.wide)}>
|
||||
{/* Wordmark svg not extracted yet (figma 88:8932) — text stands in at the same ink. */}
|
||||
<FishLogo size={23} />
|
||||
<span className={css.wordmark}>deepseek</span>
|
||||
<span className={css.badge}>HARNESS</span>
|
||||
<BrandWordmark />
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
onClick={() => { onToggleSidebar() }}
|
||||
>
|
||||
<IconPanelLeftOutline16 />
|
||||
</button>
|
||||
{/* Rail resting state is the whale mark; hovering swaps in the panel
|
||||
icon (the expand affordance, figma sidebar-hover flow). */}
|
||||
<Tooltip label="Open sidebar" disabled={wide}>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(css.iconButton, css.toggle)}
|
||||
aria-label={collapsed ? 'Open sidebar' : 'Collapse sidebar'}
|
||||
onClick={() => { onToggleSidebar() }}
|
||||
>
|
||||
{!wide && <FishLogo className={css.railFish} size={24} />}
|
||||
{/* Rail icons render at 18 (figma rail spec); expanded keeps the glyph-native sizes. */}
|
||||
<IconPanelLeftOutline16 className={css.panelIcon} size={wide ? 16 : 18} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={css.newSession}
|
||||
aria-label="New session"
|
||||
onClick={() => { onCreate() }}
|
||||
>
|
||||
<IconNewChatOutline16 size={14} />
|
||||
{wide && <span className={clsx(css.newSessionLabel, css.wide)}>New Session</span>}
|
||||
</button>
|
||||
<Tooltip label="New session" disabled={wide}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.newSession}
|
||||
aria-label="New session"
|
||||
onClick={() => { onCreate() }}
|
||||
>
|
||||
<IconNewChatOutline16 size={wide ? 14 : 18} />
|
||||
{wide && <span className={clsx(css.newSessionLabel, css.wide)}>New Session</span>}
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<div className={css.sectionHeader}>
|
||||
{wide && <span className={clsx(css.sectionLabel, css.wide)}>WorkSpace</span>}
|
||||
{wide && <GroupByMenu />}
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
aria-label="New workspace"
|
||||
onClick={() => { onCreate() }}
|
||||
>
|
||||
<IconProjectAddOutline16 />
|
||||
</button>
|
||||
<Tooltip label="New Workspace" disabled={wide}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
aria-label="New workspace"
|
||||
onClick={() => { onCreate() }}
|
||||
>
|
||||
<IconProjectAddOutline16 size={wide ? 16 : 18} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* Expanded: the row is a click-to-focus field (the leading icon is
|
||||
decorative). Collapsed: the icon is the rail's search control. */}
|
||||
<div className={css.search} onClick={() => { if (!collapsed) searchInput.current?.focus() }}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.searchButton}
|
||||
aria-label="Search sessions"
|
||||
tabIndex={collapsed ? 0 : -1}
|
||||
onClick={() => { if (collapsed) { setSearchOnExpand(true); onToggleSidebar() } }}
|
||||
>
|
||||
<IconSearchOutline16 size={14} />
|
||||
</button>
|
||||
<Tooltip label="Search" disabled={wide}>
|
||||
<button
|
||||
type="button"
|
||||
className={css.searchButton}
|
||||
aria-label="Search sessions"
|
||||
tabIndex={collapsed ? 0 : -1}
|
||||
onClick={() => { if (collapsed) { setSearchOnExpand(true); onToggleSidebar() } }}
|
||||
>
|
||||
<IconSearchOutline16 size={wide ? 14 : 18} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{wide && (
|
||||
<input
|
||||
ref={searchInput}
|
||||
@@ -245,7 +275,7 @@ export function SidebarRoot({ collapsed, useSessions, onOpen, onCreate, onToggle
|
||||
</div>
|
||||
|
||||
<div className={css.foot} role="button" tabIndex={0} aria-label="Settings">
|
||||
<IconSettingsOutline14 />
|
||||
<IconSettingsOutline14 size={wide ? 14 : 18} />
|
||||
{wide && <span className={clsx(css.footLabel, css.wide)}>Settings</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user