feat(ui): 自由窗口——tab 拖出侧边栏成为可移动/缩放/置顶的浮动窗口 (v0.16.0)
- 拖出:标签栏 tab 拖到主会话列(HTML5 DnD 检测 + 虚线提示浮层)或右键菜单 「移动到自由窗口」(右/底面板共用 TabBar),tab 从 pane 移动进 SidebarState.floats - 窗口(FreeWindow,渲染于 [data-dsh-panel-host] z-42):头部 pointer 拖动 (rAF 直写 + 松手提交)、拖到 pane 高亮停靠(center 合并)、SE 角缩放、 点击置顶、头部右键(回到侧边栏/关闭)、X = closeTab 走正常生命周期 - 服务语义:openTab dedupe/id 聚焦浮动 tab = 置顶窗口(不展开面板); closeTab/activateTab 兼顾 floats;agent 终端 reconcile 覆盖浮窗; features 单调新增 'floatWindows',版本 0.16.0 - 持久化:floats 随会话 localStorage,sanitizeState 宽容校验 + 几何钳制, uid 播种覆盖 float 前缀;i18n 三键 19 语言同步 - 关键修复(e2e 发现):portal 覆盖层(头部 Menu)的合成事件沿 React 树 冒泡回拖拽表面,劫持菜单行点击——拖拽起点加 DOM 同源守卫(jsdom 不复现) - e2e-mount.sh:缺省 tarball 改取 mtime 最新(旧字典序选择会把冒烟挂到 过期产物);新增 tests/e2e/float-window.e2e.ts 双用例
This commit is contained in:
+11
-4
@@ -4,8 +4,9 @@
|
||||
* button cluster, and the + menu that opens new tabs (explorer / git /
|
||||
* terminal). Tabs are draggable; dropping onto another tab inserts before it,
|
||||
* dropping on the strip background appends to this pane. Right-clicking a
|
||||
* tab opens the tab context menu (close / close others / close to the left /
|
||||
* close to the right, all scoped to this pane).
|
||||
* tab opens the tab context menu (float as a free window / close / close
|
||||
* others / close to the left / close to the right, the close ones scoped to
|
||||
* this pane).
|
||||
*/
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||||
import clsx from 'clsx'
|
||||
@@ -63,6 +64,9 @@ export function TabBar(props: {
|
||||
newTabOptions: NewTabOption[]
|
||||
/** Drop of a tab from any pane: (payload, insertBeforeTabId | null). */
|
||||
onDropTab: (payload: TabDragPayload, before: string | null) => void
|
||||
/** Float a tab out as a free window (the tab context menu's entry; the
|
||||
* drag-to-conversation gesture is handled at the Sidebar shell level). */
|
||||
onFloatTab: (tabId: string) => void
|
||||
/** Icon resolver for tab labels (reads from the tab descriptor registry). */
|
||||
getTabIcon?: (tab: SidebarTab) => ReactNode
|
||||
/** Badge resolver for tab labels (reads the descriptor's `badge`; the
|
||||
@@ -70,7 +74,7 @@ export function TabBar(props: {
|
||||
getTabBadge?: (tab: SidebarTab) => ReactNode
|
||||
}) {
|
||||
const {
|
||||
paneId, tabs, active, onActivate, onClose, onNewTab, newTabOptions, onDropTab, getTabIcon, getTabBadge,
|
||||
paneId, tabs, active, onActivate, onClose, onNewTab, newTabOptions, onDropTab, onFloatTab, getTabIcon, getTabBadge,
|
||||
} = props
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
// The tab right-click context menu: the target tab plus the cursor
|
||||
@@ -271,6 +275,7 @@ export function TabBar(props: {
|
||||
open={tabMenu !== null && tabMenuIndex >= 0}
|
||||
onClose={() => { setTabMenu(null) }}
|
||||
items={[
|
||||
{ id: 'float', label: t('moveToFreeWindow') },
|
||||
{ id: 'close', label: t('close') },
|
||||
{ id: 'closeOthers', label: t('closeOtherTabs'), ...(tabs.length <= 1 ? { disabled: true } : {}) },
|
||||
{ id: 'closeLeft', label: t('closeLeftTabs'), ...(tabMenuIndex <= 0 ? { disabled: true } : {}) },
|
||||
@@ -282,7 +287,9 @@ export function TabBar(props: {
|
||||
setTabMenu(null)
|
||||
const index = tabs.findIndex(tab => tab.id === target.tabId)
|
||||
if (index < 0) return
|
||||
if (id === 'close') {
|
||||
if (id === 'float') {
|
||||
onFloatTab(target.tabId)
|
||||
} else if (id === 'close') {
|
||||
onClose(target.tabId)
|
||||
} else if (id === 'closeOthers') {
|
||||
for (const tab of tabs) {
|
||||
|
||||
Reference in New Issue
Block a user