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 files changed
+523 -272

No files matched your search

@@ -0,0 +1,72 @@
// Hover/focus label bubble (figma tooltip pill: dark plate, white text).
// TODO: interaction is a placeholder (no show delay, no flip on viewport
// collision, no arrow) — visuals and behavior get a proper pass later.
// The anchor is the child element itself (cloneElement, no wrapper node), so
// attaching a tooltip never changes the anchor's layout context. The bubble is
// position:fixed and coordinates come from the anchor's rect at show time, so
// it escapes ancestor overflow clipping (the sidebar rail clips its column)
// without a portal.
import { cloneElement, useEffect, useRef, useState } from 'react'
import type { FocusEventHandler, MouseEventHandler, ReactElement, Ref } from 'react'
import css from './Tooltip.module.css'
/** Bubble placement relative to the anchor. */
export type TooltipSide = 'right' | 'bottom'
/** Props Tooltip injects into its anchor child; the child's own handlers are chained ahead of the tooltip's. */
interface AnchorProps {
ref?: Ref<HTMLElement> | undefined
onMouseEnter?: MouseEventHandler | undefined
onMouseLeave?: MouseEventHandler | undefined
onFocus?: FocusEventHandler | undefined
onBlur?: FocusEventHandler | undefined
}
/**
* Attach a hover/focus tooltip to an anchor element.
* @param props.label - bubble text.
* @param props.side - placement relative to the anchor (default 'right').
* @param props.disabled - suppress the bubble while true; the anchor renders identically so toggling never remounts it (which would cut its CSS transitions).
* @param props.children - a single anchor element. Tooltip owns its ref (no current consumer passes one).
* @returns the cloned anchor plus a fixed-position bubble while hovered/focused.
*/
export function Tooltip({ label, side = 'right', disabled = false, children }: { label: string; side?: TooltipSide; disabled?: boolean; children: ReactElement<AnchorProps> }) {
const anchor = useRef<HTMLElement | null>(null)
const [pos, setPos] = useState<{ x: number; y: number } | null>(null)
// Disabling mid-hover (e.g. clicking a rail control expands the sidebar)
// must drop an already-visible bubble: no mouseleave fires.
useEffect(() => {
if (disabled) setPos(null)
}, [disabled])
const show = () => {
if (disabled) return
const el = anchor.current
/* v8 ignore next -- the ref is attached by event time: events fire on the cloned anchor. */
if (el === null) return
const r = el.getBoundingClientRect()
setPos(side === 'right'
? { x: r.right + 10, y: r.top + r.height / 2 }
: { x: r.left + r.width / 2, y: r.bottom + 8 })
}
const hide = () => { setPos(null) }
return (
<>
{cloneElement(children, {
ref: anchor,
onMouseEnter: (e) => { children.props.onMouseEnter?.(e); show() },
onMouseLeave: (e) => { children.props.onMouseLeave?.(e); hide() },
onFocus: (e) => { children.props.onFocus?.(e); show() },
onBlur: (e) => { children.props.onBlur?.(e); hide() },
})}
{pos !== null && (
<span className={css.bubble} data-side={side} style={{ left: pos.x, top: pos.y }} role="tooltip">
{label}
</span>
)}
</>
)
}