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:
@@ -13,7 +13,7 @@
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { PropsRenderSlots, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { computeColumns } from './columns.ts'
|
||||
import { computeColumns, SIDEBAR_AUTO_COLLAPSE, SIDEBAR_DEFAULT } from './columns.ts'
|
||||
import type { createLayoutStore } from './stores.ts'
|
||||
import css from './AppFrame.module.css'
|
||||
|
||||
@@ -127,7 +127,19 @@ export function AppFrame({
|
||||
}
|
||||
}, [])
|
||||
|
||||
const cols = computeColumns(viewport, panels.sidebar, detailsSession === undefined ? 0 : panels.details)
|
||||
// Narrow viewports auto-collapse the sidebar; the store mirror keeps
|
||||
// toggleSidebar's semantics right (narrow toggles flip the manual
|
||||
// re-expand override, stores.ts). Collapsed is decided here, so the
|
||||
// solver stays breakpoint-free: a narrow re-expand passes the preference
|
||||
// (or the default when the wide preference is closed) and the center
|
||||
// absorbs the squeeze.
|
||||
const narrow = viewport < SIDEBAR_AUTO_COLLAPSE
|
||||
useEffect(() => { actions.setNarrow(narrow) }, [actions, narrow])
|
||||
const sidebarCollapsed = narrow ? !panels.narrowExpanded : panels.sidebar === 0
|
||||
const sidebarPreference = sidebarCollapsed
|
||||
? 0
|
||||
: panels.sidebar === 0 ? SIDEBAR_DEFAULT : panels.sidebar
|
||||
const cols = computeColumns(viewport, sidebarPreference, detailsSession === undefined ? 0 : panels.details)
|
||||
const colsRef = useRef(cols)
|
||||
colsRef.current = cols
|
||||
|
||||
@@ -154,7 +166,7 @@ export function AppFrame({
|
||||
ref={frameRef}
|
||||
className={css.frame}
|
||||
style={{ gridTemplateColumns: `${cols.sidebar}px minmax(0, 1fr) ${cols.details}px` }}
|
||||
data-sidebar-collapsed={panels.sidebar === 0 || undefined}
|
||||
data-sidebar-collapsed={sidebarCollapsed || undefined}
|
||||
data-details-collapsed={cols.details === 0 || undefined}
|
||||
data-dragging={dragging || undefined}
|
||||
>
|
||||
@@ -162,9 +174,10 @@ export function AppFrame({
|
||||
{/* Render-site slot call with live concession output: a closed
|
||||
sidebar keeps the mounted slot at the compact-rail width, and the
|
||||
component sees its rendered state as owner params decided here
|
||||
(collapsed follows the preference, not the resolved width). */}
|
||||
(collapsed follows the resolved rail, so a derived auto-collapse
|
||||
renders the rail UI too). */}
|
||||
{renderSlot('sidebar', {
|
||||
collapsed: panels.sidebar === 0,
|
||||
collapsed: sidebarCollapsed,
|
||||
width: cols.sidebar,
|
||||
})}
|
||||
</div>
|
||||
@@ -178,7 +191,7 @@ export function AppFrame({
|
||||
<DetailsColumn>{renderSlot('details', {})}</DetailsColumn>
|
||||
</>
|
||||
{/* The collapsed rail is fixed-width: no resize handle while closed. */}
|
||||
{panels.sidebar > 0 && <DragHandle side="sidebar" left={cols.sidebar} onStart={onSidebarStart} onDrag={onSidebarDrag} onEnd={onDragEnd} />}
|
||||
{!sidebarCollapsed && <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>
|
||||
)
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
* 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.
|
||||
* The SIDEBAR_AUTO_COLLAPSE breakpoint is consumed by AppFrame, which decides
|
||||
* the effective sidebar preference before solving; the solver itself stays
|
||||
* breakpoint-free.
|
||||
*/
|
||||
|
||||
/** Resolved widths for one frame; center may drop below CENTER_MIN only at the final fallback. */
|
||||
@@ -24,6 +27,10 @@ export const SIDEBAR_MAX = 420
|
||||
export const SIDEBAR_DEFAULT = 280
|
||||
/** Closed-sidebar rail: a 24px icon column between 16px horizontal paddings. */
|
||||
export const SIDEBAR_COLLAPSED = 56
|
||||
/** Viewport width below which the sidebar auto-collapses to the rail (deepsuite
|
||||
* LG breakpoint); a manual toggle below it re-expands over the squeezed center
|
||||
* (stores.ts narrowExpanded). */
|
||||
export const SIDEBAR_AUTO_COLLAPSE = 1024
|
||||
/** Details drag clamp floor. */
|
||||
export const DETAILS_MIN = 300
|
||||
/** Details drag clamp ceiling. */
|
||||
|
||||
@@ -13,8 +13,14 @@ import {
|
||||
SIDEBAR_DEFAULT, SIDEBAR_MAX, SIDEBAR_MIN,
|
||||
} from './columns.ts'
|
||||
|
||||
/** Layout store state: panel width preferences in px (0 = closed). */
|
||||
type LayoutState = { sidebar: number; details: number }
|
||||
/**
|
||||
* Layout store state: panel width preferences in px (0 = closed), plus the
|
||||
* narrow-viewport pair — `narrow` mirrors AppFrame's breakpoint reading
|
||||
* (viewport < SIDEBAR_AUTO_COLLAPSE) so toggleSidebar can pick semantics, and
|
||||
* `narrowExpanded` is the manual override that re-expands the auto-collapsed
|
||||
* sidebar over the squeezed center without rewriting the width preference.
|
||||
*/
|
||||
type LayoutState = { sidebar: number; details: number; narrow: boolean; narrowExpanded: boolean }
|
||||
|
||||
/**
|
||||
* Annotation twin of the actions literal below (the export needs a declared
|
||||
@@ -24,6 +30,7 @@ type LayoutActions = {
|
||||
setSidebar: (draft: LayoutState, px: number) => void
|
||||
setDetails: (draft: LayoutState, px: number) => void
|
||||
toggleSidebar: (draft: LayoutState) => void
|
||||
setNarrow: (draft: LayoutState, narrow: boolean) => void
|
||||
openDetails: (draft: LayoutState) => void
|
||||
closeDetails: (draft: LayoutState) => void
|
||||
}
|
||||
@@ -33,16 +40,30 @@ type LayoutActions = {
|
||||
* closing a panel forgets its drag width — reopening restores the contract
|
||||
* default. Actions are the complete write set: drag writes clamp
|
||||
* into the panel's contract range and never cross the open/closed line;
|
||||
* open/close transitions write 0 / the default explicitly.
|
||||
* open/close transitions write 0 / the default explicitly. Below the
|
||||
* auto-collapse breakpoint (AppFrame feeds setNarrow) the sidebar toggle
|
||||
* flips the narrowExpanded override instead of the preference.
|
||||
* @returns the store handle (spec + type + identity + factory in one).
|
||||
*/
|
||||
export function createLayoutStore(): EngineStoreHandle<LayoutState, LayoutActions> {
|
||||
const handle = defineStore({
|
||||
init: (): LayoutState => ({ sidebar: SIDEBAR_DEFAULT, details: 0 }),
|
||||
init: (): LayoutState => ({ sidebar: SIDEBAR_DEFAULT, details: 0, narrow: false, narrowExpanded: false }),
|
||||
actions: {
|
||||
setSidebar: (d, px: number) => { d.sidebar = clampWidth(px, SIDEBAR_MIN, SIDEBAR_MAX) },
|
||||
setDetails: (d, px: number) => { d.details = clampWidth(px, DETAILS_MIN, DETAILS_MAX) },
|
||||
toggleSidebar: (d) => { d.sidebar = d.sidebar === 0 ? SIDEBAR_DEFAULT : 0 },
|
||||
// Narrow toggles flip only the override: the width preference survives
|
||||
// untouched, so re-widening restores the pre-squeeze layout.
|
||||
toggleSidebar: (d) => {
|
||||
if (d.narrow) d.narrowExpanded = !d.narrowExpanded
|
||||
else d.sidebar = d.sidebar === 0 ? SIDEBAR_DEFAULT : 0
|
||||
},
|
||||
// Crossing the breakpoint in either direction drops the override: the
|
||||
// narrow default is auto-collapsed, the wide state is the preference.
|
||||
setNarrow: (d, narrow: boolean) => {
|
||||
if (d.narrow === narrow) return
|
||||
d.narrow = narrow
|
||||
d.narrowExpanded = false
|
||||
},
|
||||
openDetails: (d) => { if (d.details === 0) d.details = DETAILS_DEFAULT },
|
||||
closeDetails: (d) => { d.details = 0 },
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user