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:
Yif
2026-07-23 17:08:47 +08:00
parent 9c01696a7e
commit 20720ef238
17 changed files with 523 additions and 272 deletions

View File

@@ -86,11 +86,25 @@
height: 32px;
border-radius: 10px;
box-sizing: border-box;
background: var(--dsw-alias-bg-layer-2);
background: var(--dsw-alias-button-floating-fill);
border: 1px solid var(--dsw-alias-border-l2-darkmode-thin);
/* Hover affordance: the pill hides until the pointer is over the owning
column (data-side pairs handle and column), the strip itself, or a drag. */
opacity: 0;
transition:
opacity var(--ds-transition-duration-slow) var(--ds-ease-in-out),
background var(--ds-transition-duration-slow) var(--ds-ease-in-out);
}
.sidebarCol:hover ~ .handle[data-side='sidebar']::after,
.detailsCol:hover ~ .handle[data-side='details']::after,
.handle:hover::after,
.handle[data-dragging='true']::after {
opacity: 1;
}
.handle:hover::after,
.handle[data-dragging='true']::after {
background: var(--dsw-alias-button-floating-hover);
border-color: var(--dsw-alias-border-l3);
}

View File

@@ -34,8 +34,8 @@ function DetailsColumn(props: { children?: ReactNode }) {
return <div className={css.detailsCol}>{props.children}</div>
}
/** One drag handle: pointer capture, rAF-throttled dx reports against the drag-start origin. */
function DragHandle(props: { left: number; onStart: () => void; onDrag: (dx: number) => void; onEnd: () => void }) {
/** One drag handle: pointer capture, rAF-throttled dx reports against the drag-start origin. `side` keys the hover-reveal CSS to the owning column. */
function DragHandle(props: { side: 'sidebar' | 'details'; left: number; onStart: () => void; onDrag: (dx: number) => void; onEnd: () => void }) {
const [dragging, setDragging] = useState(false)
const origin = useRef(0)
const latest = useRef(0)
@@ -72,6 +72,7 @@ function DragHandle(props: { left: number; onStart: () => void; onDrag: (dx: num
<div
className={css.handle}
style={{ left: props.left }}
data-side={props.side}
data-dragging={dragging || undefined}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
@@ -161,8 +162,8 @@ export function AppFrame({ useStore, actions, renderSlot, SessionProvider }: App
)}
</SessionProvider>
{/* The collapsed rail is fixed-width: no resize handle while closed. */}
{panels.sidebar > 0 && <DragHandle left={cols.sidebar} onStart={onSidebarStart} onDrag={onSidebarDrag} onEnd={onDragEnd} />}
{cols.details > 0 && <DragHandle left={viewport - cols.details} onStart={onDetailsStart} onDrag={onDetailsDrag} onEnd={onDragEnd} />}
{panels.sidebar > 0 && <DragHandle side="sidebar" left={cols.sidebar} onStart={onSidebarStart} onDrag={onSidebarDrag} onEnd={onDragEnd} />}
{cols.details > 0 && <DragHandle side="details" left={viewport - cols.details} onStart={onDetailsStart} onDrag={onDetailsDrag} onEnd={onDragEnd} />}
</div>
)
}

View File

@@ -1,12 +1,13 @@
/**
* Pure concession-chain column solver for the three-column AppFrame.
* Chain order is fixed by contract: keep center >= CENTER_MIN by shrinking
* details first, then sidebar, then auto-closing details (derived zero width —
* persisted width preferences are never rewritten, so widening the window
* restores them). Center absorbs any remaining deficit as the last resort.
* Inputs are the layout store's plain width preferences (0 = closed); a
* closed sidebar resolves to the fixed SIDEBAR_COLLAPSED control rail while
* closed details resolve to zero width.
* details, then auto-closing it (derived zero width — persisted width
* preferences are never rewritten, so widening the window restores them).
* The sidebar never concedes: its rendered width is always the drag
* preference (or the collapsed rail), and center absorbs any remaining
* deficit as the last resort. Inputs are the layout store's plain width
* preferences (0 = closed); a closed sidebar resolves to the fixed
* SIDEBAR_COLLAPSED control rail while closed details resolve to zero width.
*/
/** Resolved widths for one frame; center may drop below CENTER_MIN only at the final fallback. */
@@ -16,11 +17,11 @@ export interface Columns { sidebar: number; center: number; details: number }
/** Center column floor; only the final fallback may go below it. */
export const CENTER_MIN = 640
/** Sidebar drag clamp floor. */
export const SIDEBAR_MIN = 240
export const SIDEBAR_MIN = 280
/** Sidebar drag clamp ceiling. */
export const SIDEBAR_MAX = 420
/** Sidebar width before any user drag. */
export const SIDEBAR_DEFAULT = 300
/** Sidebar width before any user drag (= the drag floor). */
export const SIDEBAR_DEFAULT = 280
/** Closed-sidebar rail: a 24px icon column between 16px horizontal paddings. */
export const SIDEBAR_COLLAPSED = 56
/** Details drag clamp floor. */
@@ -44,38 +45,26 @@ export function clampWidth(px: number, min: number, max: number): number {
/**
* Solve the three column widths for one viewport frame. Pure: no hysteresis —
* the output is a function of (viewport, preferences) only, so recovery on
* re-widening is automatic. After the auto-close step the details pressure is
* gone, so the sidebar returns to its preferred width when it fits.
* Preferences re-clamp here because they cross a durable boundary
* (localStorage rehydration may carry stale ranges).
* re-widening is automatic. Preferences re-clamp here because they cross a
* durable boundary (localStorage rehydration may carry stale ranges).
* @param viewport - available frame width in px.
* @param sidebar - sidebar width preference in px (0 = closed).
* @param details - details width preference in px (0 = closed).
* @returns resolved widths; details 0 means visually closed (never unmounted), while a closed sidebar keeps its compact rail.
*/
export function computeColumns(viewport: number, sidebar: number, details: number): Columns {
const s0 = sidebar === 0 ? SIDEBAR_COLLAPSED : clampWidth(sidebar, SIDEBAR_MIN, SIDEBAR_MAX)
// The sidebar is fixed at its preference (or the rail) — it never concedes.
const s = sidebar === 0 ? SIDEBAR_COLLAPSED : clampWidth(sidebar, SIDEBAR_MIN, SIDEBAR_MAX)
const d0 = details === 0 ? 0 : clampWidth(details, DETAILS_MIN, DETAILS_MAX)
// Step 1: everything fits at preferred widths.
if (s0 + d0 + CENTER_MIN <= viewport) return { sidebar: s0, center: viewport - s0 - d0, details: d0 }
if (s + d0 + CENTER_MIN <= viewport) return { sidebar: s, center: viewport - s - d0, details: d0 }
// Step 2: shrink details toward its minimum.
const d1 = d0 === 0 ? 0 : Math.max(DETAILS_MIN, viewport - s0 - CENTER_MIN)
if (s0 + d1 + CENTER_MIN <= viewport) return { sidebar: s0, center: CENTER_MIN, details: d1 }
const d1 = d0 === 0 ? 0 : Math.max(DETAILS_MIN, viewport - s - CENTER_MIN)
if (s + d1 + CENTER_MIN <= viewport) return { sidebar: s, center: CENTER_MIN, details: d1 }
// Step 3: shrink sidebar toward its minimum (the collapsed rail never shrinks).
const s1 = sidebar === 0 ? SIDEBAR_COLLAPSED : Math.max(SIDEBAR_MIN, viewport - d1 - CENTER_MIN)
if (s1 + d1 + CENTER_MIN <= viewport) return { sidebar: s1, center: CENTER_MIN, details: d1 }
// Step 4: auto-close details (derived — preferences untouched). With the
// details pressure gone the sidebar concession is re-solved from preference.
if (d1 > 0) {
if (s0 + CENTER_MIN <= viewport) return { sidebar: s0, center: viewport - s0, details: 0 }
const s2 = sidebar === 0 ? SIDEBAR_COLLAPSED : Math.max(SIDEBAR_MIN, viewport - CENTER_MIN)
return { sidebar: s2, center: Math.max(0, viewport - s2), details: 0 }
}
// Step 5: center absorbs the deficit (may drop below CENTER_MIN).
return { sidebar: s1, center: Math.max(0, viewport - s1 - d1), details: d1 }
// Step 3: auto-close details (derived — preferences untouched); center
// absorbs any remaining deficit (may drop below CENTER_MIN).
return { sidebar: s, center: Math.max(0, viewport - s), details: 0 }
}