feat(editor): 文件窗口与资源管理器二合一 + 声明式设置 select 行 (#151)
* feat(editor): merged explorer mode — path input, docked file tree, global name search
New editorExplorer pref (default on, editor card gear): the editor tab gains
a path-input header and a toggleable right-docked tree panel (per-tab state
in tab.meta.treeOpen; file tabs default closed, the seeded path-less home
tab defaults open). Fresh sessions seed that empty editor tab instead of
the explorer tab when merged mode is on; the standalone explorer tab stays
in the + menu unchanged.
- Extract FileTree.tsx from ExplorerView (shared by both hosts)
- Host fs.search route: recursive case-insensitive name search rooted at
the session cwd, budget-capped (200 matches / 100k visited), skips .git,
never descends symlink dirs
- EditorHost reads the pref reactively (boolean snapshot) and keeps the
pre-merge layout verbatim when off
* feat(settings): select row type for declarative settings; editorExplorer as iconed select
- SidebarSettingToggle gains type 'select' with options {value,title,desc,icon}
and multi (default false; stores the picked values as an array in options
order). Any option carrying an icon renders big-icon option cards in the
dropdown and the icon in the closed anchor; icon-less rows are one-line text.
- SelectRow in SideCardSection rides the primitives Menu (portal); single-pick
commits and closes, multi toggles and stays open. Wired for both host-prefs
toggles and plugin-owned pluginToggles via the new onSelectValue seam.
- The editorExplorer merged-mode pref is now an iconed select (merged vs
separate) instead of a switch; the stored value stays boolean.
- Advertise the capability as 'settingSelect' in SIDEBAR_FEATURES.
* feat(editor): unify explorer into the editor surface, resizable tree dock, hoisted toolbar
- One tree implementation, two presentations: the shared TreePanel (global
name search + refresh + FileTree) docks as the editor tab's right panel
and — in merged mode — fills the explorer tab's window; the pref-off
explorer keeps its legacy header shell.
- The docked panel drag-resizes from its left edge (160-480px clamp),
persisted as tab.meta.treeWidth alongside treeOpen.
- The text editor's preview/edit/save toolbar moves into the path-input
header row: FileViewerProps gains internal toolbar fields ('host' mode
skips the viewer's own row, reports EditorToolbarState and registers
EditorToolbarControls); non-merged mode is byte-identical to before.
* feat(editor): single files window with in-place switching; explorer descriptor removed
- editor tab replaces explorer on the + menu (order 10, Files); persisted
explorer tabs migrate to the editor home tab via sanitizeState
- editorExplorer now only controls open behavior: merged = in-place
updateTab, independent = openSidebarFile; both modes share the merged
chrome (path input + hoisted preview/edit/save toolbar + tree toggle)
- file tree context menu gains 'open in new tab' / 'open to the side'
(mintTabId avoids the id safety net; insertLeafAt splits right)
- tree dock resize uses pointer capture (touch-action none, 6px handle)
- defaults: openByDefault off, sidebar width 35%
- file tree clips horizontal overflow (no sideways scroll)
- e2e: mount lane arms the editor-chunk wait before goto and clicks the
tree row's left edge (hover @-reference button sits mid-row); drag lane
expands the panel first (collapsed push writes 0px), waits for the
slide-in to settle, and picks the leftmost col-resize strip; 'Files'
title assertions require draggable to skip the bottom panel's welcome
cards
* feat(editor): split mode turns the path-less window into the standalone explorer
In independent (split) mode a path-less files window renders ONLY the
tree panel (search + FileTree, full-window form) — no path input, no
hoisted toolbar, no tree toggle. File clicks open per-path editor tabs
(full chrome, tree docked closed by default) through openSidebarFile,
unchanged. Merged mode is untouched (chrome + docked tree, in-place
switching). TreePanel's previously reserved 'full' prop now has its
consumer.
This commit is contained in:
@@ -83,28 +83,8 @@ interface FrameSample {
|
||||
test('width drag tracks the shell 1:1 with transitions disabled (issue #92)', async ({ page }) => {
|
||||
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' })
|
||||
await expect(page.locator('#root > *')).not.toHaveCount(0, { timeout: 90_000 })
|
||||
// The seeded session opens the right panel by default: the layout push
|
||||
// variable becomes live once the panel mounts (session activation lags
|
||||
// the shell render).
|
||||
await expect
|
||||
.poll(async () => (
|
||||
await page.evaluate(() => document.documentElement.style.getPropertyValue('--dsh-sidebar-width'))
|
||||
), { timeout: 90_000 })
|
||||
.not.toBe('')
|
||||
|
||||
// The width drag strip is the panel's left-edge hit strip. There is no
|
||||
// dedicated hook (the skinning contract is token-driven), so locate it
|
||||
// semantically: the only `cursor: col-resize` element in the sidebar.
|
||||
const stripBox = await page.evaluate(() => {
|
||||
const host = document.querySelector('[data-dsh-better-sidebar]')
|
||||
if (host === null) return null
|
||||
const found = [...host.querySelectorAll<HTMLElement>('*')]
|
||||
.find(el => getComputedStyle(el).cursor === 'col-resize')
|
||||
if (found === undefined) return null
|
||||
const r = found.getBoundingClientRect()
|
||||
return { x: r.x, y: r.y, width: r.width, height: r.height }
|
||||
})
|
||||
expect(stripBox, 'the width drag strip must be present (cursor: col-resize)').not.toBeNull()
|
||||
const sidebar = page.locator('[data-dsh-better-sidebar]')
|
||||
await expect(sidebar).toBeAttached({ timeout: 90_000 })
|
||||
|
||||
// Dismiss whatever onboarding takeover is present (same dance as the mount
|
||||
// lane), so the pointer can reach the strip without a masking overlay.
|
||||
@@ -131,12 +111,72 @@ test('width drag tracks the shell 1:1 with transitions disabled (issue #92)', as
|
||||
if (!dismissed) break
|
||||
}
|
||||
|
||||
// openByDefault defaults OFF: a fresh session's panel starts collapsed, and
|
||||
// the collapsed layout push still writes `--dsh-sidebar-width: 0px` — so the
|
||||
// geometry checks below are meaningless until the panel is expanded through
|
||||
// the toggle cluster.
|
||||
const expandButton = sidebar.getByRole('button', { name: 'Expand sidebar' })
|
||||
await expect(expandButton, 'the collapsed toggle cluster must offer the expand button').toHaveCount(1)
|
||||
await expandButton.click()
|
||||
|
||||
// The layout push variable becomes live (non-zero) once the panel opens
|
||||
// (session activation lags the shell render).
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const value = await page.evaluate(() => document.documentElement.style.getPropertyValue('--dsh-sidebar-width'))
|
||||
return value !== '' && value !== '0px'
|
||||
}, { timeout: 90_000 })
|
||||
.toBe(true)
|
||||
|
||||
// The width drag strip is the panel's left-edge hit strip. There is no
|
||||
// dedicated hook (the skinning contract is token-driven), so locate it
|
||||
// semantically among the sidebar's `cursor: col-resize` elements — the files
|
||||
// window's tree-dock handle matches too, but the panel strip is always the
|
||||
// LEFTMOST one (the dock sits at the panel's right edge).
|
||||
const locateStrip = `(() => {
|
||||
const host = document.querySelector('[data-dsh-better-sidebar]')
|
||||
if (host === null) return null
|
||||
const boxes = [...host.querySelectorAll('*')]
|
||||
.filter(el => getComputedStyle(el).cursor === 'col-resize')
|
||||
.map(el => el.getBoundingClientRect())
|
||||
.filter(r => r.width > 0 && r.height > 0)
|
||||
.sort((a, b) => a.x - b.x)
|
||||
const r = boxes[0]
|
||||
if (r === undefined) return null
|
||||
const varWidth = parseFloat(document.documentElement.style.getPropertyValue('--dsh-sidebar-width'))
|
||||
return {
|
||||
x: r.x, y: r.y, width: r.width, height: r.height,
|
||||
varWidth: Number.isNaN(varWidth) ? 0 : varWidth,
|
||||
innerWidth: window.innerWidth,
|
||||
}
|
||||
})()`
|
||||
type StripBox = { x: number; y: number; width: number; height: number; varWidth: number; innerWidth: number }
|
||||
// The panel SLIDES IN from the right on expand: locating the strip before
|
||||
// the open transition settles captures a mid-animation box at the viewport
|
||||
// edge, and the drag lands off-panel. Wait until the strip sits at the
|
||||
// pushed layout edge (its right edge ≈ innerWidth - the push variable).
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const box = await page.evaluate<StripBox | null>(locateStrip)
|
||||
if (box === null) return false
|
||||
return Math.abs((box.x + box.width) - (box.innerWidth - box.varWidth)) <= 8
|
||||
}, { timeout: 30_000 })
|
||||
.toBe(true)
|
||||
const stripBox = await page.evaluate<StripBox | null>(locateStrip)
|
||||
expect(stripBox, 'the width drag strip must be present (cursor: col-resize)').not.toBeNull()
|
||||
|
||||
// Instrument a per-frame sampler BEFORE the drag begins.
|
||||
await page.evaluate(() => {
|
||||
type Sample = FrameSample
|
||||
const samples: Sample[] = []
|
||||
const strip = [...document.querySelectorAll<HTMLElement>('*')]
|
||||
.find(el => getComputedStyle(el).cursor === 'col-resize')
|
||||
// Same rule as the strip locator above: the LEFTMOST col-resize element
|
||||
// inside the sidebar host (the tree-dock handle is further right).
|
||||
const host = document.querySelector('[data-dsh-better-sidebar]')
|
||||
const strip = host === null
|
||||
? null
|
||||
: [...host.querySelectorAll<HTMLElement>('*')]
|
||||
.filter(el => getComputedStyle(el).cursor === 'col-resize')
|
||||
.sort((a, b) => a.getBoundingClientRect().x - b.getBoundingClientRect().x)[0]
|
||||
// The conversation column: the grid item the layout push squeezes (the
|
||||
// same nth-child(2) layout.css targets for the vertical push; its right
|
||||
// edge is where the width push lands).
|
||||
|
||||
Reference in New Issue
Block a user