polish(web): 打磨 Web 会话界面的布局、自适应与组件一致性
1. 统一会话列宽度轴:新增 --dsh-chat-content-width(748px),输入框、todo、goal、queue、approval、plan review、ask question 等容器的宽度与边距全部由该变量推导,消除各面板之间的像素漂移,窄视口下的边缘留白也保持一致。 2. 输入框自适应与细节:控制行改为容器查询(460px 阈值以下权限选择器只显示图标+下拉,隐藏文字);卡片圆角 20→22、行内边距调整并整体下移 2px(发送按钮除外);permission/model 触发器统一 24px 圆角;Plan 与 Read Only 间距 +8。 3. 修复浮层菜单溢出:slash 菜单与命令弹层钳制到输入卡片宽度,超长行以省略号截断;Tooltip 增加 12px 视口边缘安全距离。 4. 增加与替换图标:Add provider 改用输入框同款加号图标(IconPlusOutline16),统一图标尺寸与字号。 5. 统一 Settings → Models 组件:补齐按钮 hover 态、select 下拉箭头不再贴边、标题区与 provider 卡片间距 +12。 6. 侧边栏交互:add workspace / group by / create session / 收起侧边栏四个图标按钮增加 500ms 延迟 tooltip(前两个向下弹出);展开态的 New Session 不再重复显示 tooltip;侧边栏窄屏自适应收起逻辑微调。 7. 其他:hero 区 workspace 徽章右移对齐;附带 Agent Note(中英双语)记录共享宽度轴与容器查询的设计取舍。
This commit is contained in:
@@ -1,18 +1,19 @@
|
||||
// Hover/focus label bubble (figma tooltip pill: dark plate, white text).
|
||||
// TODO: interaction is a placeholder (no flip on viewport collision or
|
||||
// arrow) — visuals and behavior get a proper pass later.
|
||||
// TODO: interaction is a placeholder (horizontal overflow clamps, but there
|
||||
// is no vertical flip on viewport collision and 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, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { cloneElement, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import type { FocusEventHandler, MouseEventHandler, MutableRefObject, ReactElement, Ref } from 'react'
|
||||
import css from './Tooltip.module.css'
|
||||
|
||||
/** Bubble placement relative to the anchor. */
|
||||
export type TooltipSide = 'right' | 'bottom'
|
||||
export type TooltipSide = 'right' | 'bottom' | 'top'
|
||||
|
||||
/** Props Tooltip injects into its anchor child; the child's own handlers are chained ahead of the tooltip's. */
|
||||
interface AnchorProps {
|
||||
@@ -44,6 +45,21 @@ export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false,
|
||||
else if (childRef != null) (childRef as MutableRefObject<HTMLElement | null>).current = el
|
||||
}, [childRef])
|
||||
const [pos, setPos] = useState<{ x: number; y: number } | null>(null)
|
||||
const bubble = useRef<HTMLSpanElement | null>(null)
|
||||
// Horizontal viewport clamp: fixed positioning knows nothing about edges, so
|
||||
// a centered bubble near the right edge would clip. Measured after paint and
|
||||
// written straight to the style (no state), so it cannot re-trigger itself.
|
||||
// EDGE_MARGIN keeps the bubble 12px off the viewport edges instead of flush.
|
||||
useLayoutEffect(() => {
|
||||
const el = bubble.current
|
||||
if (el === null || pos === null) return
|
||||
const EDGE_MARGIN = 12
|
||||
const r = el.getBoundingClientRect()
|
||||
let dx = 0
|
||||
if (r.right > window.innerWidth - EDGE_MARGIN) dx = window.innerWidth - EDGE_MARGIN - r.right
|
||||
if (r.left + dx < EDGE_MARGIN) dx = EDGE_MARGIN - r.left
|
||||
if (dx !== 0) el.style.left = `${pos.x + dx}px`
|
||||
}, [pos])
|
||||
const showTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
// Hover and focus are independent triggers: the bubble hides only after
|
||||
// BOTH clear (hovering away from a focused anchor must not drop it).
|
||||
@@ -73,7 +89,9 @@ export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false,
|
||||
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 })
|
||||
: side === 'top'
|
||||
? { x: r.left + r.width / 2, y: r.top - 8 }
|
||||
: { x: r.left + r.width / 2, y: r.bottom + 8 })
|
||||
}
|
||||
const showAfterHoverDelay = () => {
|
||||
cancelShow()
|
||||
@@ -101,7 +119,7 @@ export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false,
|
||||
onBlur: (e) => { children.props.onBlur?.(e); triggers.current.focus = false; hide() },
|
||||
})}
|
||||
{pos !== null && (
|
||||
<span className={css.bubble} data-side={side} style={{ left: pos.x, top: pos.y }} role="tooltip">
|
||||
<span ref={bubble} className={css.bubble} data-side={side} style={{ left: pos.x, top: pos.y }} role="tooltip">
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user