feat(ui): 文件二级页面间距规整(8px 网格)+ 浮窗默认尺寸改手机竖屏比例

- 文件页 chrome 行统一:路径行与树搜索行同为 41px 高(路径输入框改
  border-box 28px 总高)、gap 统一 6px、内边距统一 6px 8px
- 树体侧距 6→8px 对齐 chrome 行;markdown 预览 10/14→12/16(与错误
  文案块一致);CodeMirror scroller 新增同款 12/16 内边距(预览/编辑
  切换文字位置不跳);截断横幅 12→8px
- 浮窗默认尺寸 480×360 → 390×780(1:2 手机竖屏比例),创建时先按
  视口减 24px 钳制再居中,矮视口得到更矮的窗而非溢出
- 测试/文档同步:state/free-window 预期重算(含 clampMove 回推的
  y=24 语义)、e2e 断言宽度上界、AGENTS §7.5 与设计文档
This commit is contained in:
Menghuan1918
2026-08-24 00:59:27 +08:00
parent f648b0c006
commit d15645bf23
7 changed files with 59 additions and 36 deletions
+1 -1
View File
@@ -174,7 +174,7 @@ test('float a tab, move the window, reload restores it, dock it back', async ({
const leftBefore = await floatWindow.evaluate((node) => Number.parseFloat((node as HTMLElement).style.left))
expect(Number.isFinite(leftBefore), `style.left must be numeric, got ${leftBefore}`).toBe(true)
expect(leftBefore).toBeGreaterThan(0)
expect(leftBefore).toBeLessThan(1440 - 480)
expect(leftBefore).toBeLessThan(1440 - 390)
await assertNoCrash(page)
// 2. The header drag moves the window (pointer capture + commit).
+22 -17
View File
@@ -213,9 +213,10 @@ describe('free windows: the window', () => {
const state = store.getSnapshot().state!
expect(state.floats).toHaveLength(1)
expect(state.floats[0]!.tab.id).toBe('notes')
// Born at the conversation column's center, minus half the default
// size (480x360), clamped into the 1024x768 viewport.
expect(state.floats[0]).toMatchObject({ x: 400 - 240, y: 325 - 180, w: 480, h: 360 })
// Born at the conversation column's center with the phone-ratio
// default (390x780), created capped to the jsdom viewport (h 744) and
// clamped in (the centered y would be negative).
expect(state.floats[0]).toMatchObject({ x: 400 - 195, y: 0, w: 390, h: 744 })
expect(container.querySelector('[data-dsh-float-window]')).not.toBeNull()
} finally {
unmount()
@@ -230,20 +231,22 @@ describe('free windows: the window', () => {
act(() => { store.reduce(s => floatTabReducer(s, 'notes', 512, 384)) })
const win = container.querySelector<HTMLElement>('[data-dsh-float-window]')!
const header = win.querySelector<HTMLElement>('[class*="floatHeader"]')!
expect(win.style.left).toBe('272px')
expect(win.style.left).toBe('317px')
// Grab at (300, 220) — the drag records the window's own origin, so
// moving the pointer to (200, 250) lands the window at (172, 234).
// moving the pointer to (200, 250) lands the window at (217, 42) —
// clamped back up to y 24 (the 744-tall window must stay in the
// 768-tall viewport: 768 - 744 = 24).
act(() => {
header.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, clientX: 300, clientY: 220, button: 0 }))
header.dispatchEvent(new MouseEvent('pointermove', { bubbles: true, clientX: 200, clientY: 250 }))
})
await flushFrame()
expect(win.style.left).toBe('172px')
expect(win.style.top).toBe('234px')
expect(win.style.left).toBe('217px')
expect(win.style.top).toBe('24px')
// The store is untouched mid-drag (DOM is the only record).
expect(store.getSnapshot().state!.floats[0]!.x).toBe(272)
expect(store.getSnapshot().state!.floats[0]!.x).toBe(317)
act(() => { header.dispatchEvent(new MouseEvent('pointerup', { bubbles: true, clientX: 200, clientY: 250, button: 0 })) })
expect(store.getSnapshot().state!.floats[0]).toMatchObject({ x: 172, y: 234 })
expect(store.getSnapshot().state!.floats[0]).toMatchObject({ x: 217, y: 24 })
} finally {
unmount()
}
@@ -258,14 +261,16 @@ describe('free windows: the window', () => {
const win = container.querySelector<HTMLElement>('[data-dsh-float-window]')!
const handle = win.querySelector<HTMLElement>('[class*="floatResize"]')!
act(() => {
handle.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, clientX: 752, clientY: 564, button: 0 }))
handle.dispatchEvent(new MouseEvent('pointermove', { bubbles: true, clientX: 852, clientY: 614 }))
handle.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, clientX: 700, clientY: 750, button: 0 }))
handle.dispatchEvent(new MouseEvent('pointermove', { bubbles: true, clientX: 800, clientY: 800 }))
})
await flushFrame()
expect(win.style.width).toBe('580px')
expect(win.style.height).toBe('410px')
act(() => { handle.dispatchEvent(new MouseEvent('pointerup', { bubbles: true, clientX: 852, clientY: 614, button: 0 })) })
expect(store.getSnapshot().state!.floats[0]).toMatchObject({ w: 580, h: 410 })
// +100/+50 from the created 390x744: height clamps to the viewport
// ceiling (768 - y 12 = 756).
expect(win.style.width).toBe('490px')
expect(win.style.height).toBe('756px')
act(() => { handle.dispatchEvent(new MouseEvent('pointerup', { bubbles: true, clientX: 800, clientY: 800, button: 0 })) })
expect(store.getSnapshot().state!.floats[0]).toMatchObject({ w: 490, h: 756 })
} finally {
unmount()
}
@@ -279,7 +284,7 @@ describe('free windows: the window', () => {
act(() => { store.reduce(s => floatTabReducer(s, 'notes', 512, 384)) })
const win = container.querySelector<HTMLElement>('[data-dsh-float-window]')!
const header = win.querySelector<HTMLElement>('[class*="floatHeader"]')!
expect(win.style.left).toBe('272px')
expect(win.style.left).toBe('317px')
act(() => { header.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true, clientX: 300, clientY: 220 })) })
// The menu list is PORTALED to document.body, yet its events still
// bubble through the React tree into the header's onPointerDown —
@@ -293,7 +298,7 @@ describe('free windows: the window', () => {
header.dispatchEvent(new MouseEvent('pointermove', { bubbles: true, clientX: 200, clientY: 250 }))
})
await flushFrame()
expect(win.style.left).toBe('272px')
expect(win.style.left).toBe('317px')
act(() => { dockRow.dispatchEvent(new MouseEvent('click', { bubbles: true })) })
expect(store.getSnapshot().state!.floats).toHaveLength(0)
expect(paneTabs(store)).toContain('notes')
+6 -4
View File
@@ -919,8 +919,9 @@ describe('free windows (v0.16.0)', () => {
expect(leaf.active).toBe('term:1')
expect(floated.floats).toHaveLength(1)
expect(floated.floats[0]!.tab).toEqual(git)
// Default size centered on the drop point (no viewport → floors only).
expect(floated.floats[0]).toMatchObject({ x: 300 - 240, y: 200 - 180, w: 480, h: 360 })
// Phone-ratio default (390x780) centered on the drop point (no viewport
// → size cap never bites; only the negative y clamps to 0).
expect(floated.floats[0]).toMatchObject({ x: 300 - 195, y: 0, w: 390, h: 780 })
expect(floated.activePane).toBe(leaf.id)
})
@@ -961,8 +962,9 @@ describe('free windows (v0.16.0)', () => {
expect(moved.floats[0]).toMatchObject({ x: 0, y: 0 })
// Beyond the right/bottom edge clamps the top-left so the window fits.
moved = moveFloat(moved, id, 5000, 5000)
expect(moved.floats[0]!.x).toBe(1024 - 480)
expect(moved.floats[0]!.y).toBe(768 - 360)
// The float was CREATED capped to the viewport: w 390, h 768-24=744.
expect(moved.floats[0]!.x).toBe(1024 - 390)
expect(moved.floats[0]!.y).toBe(768 - 744)
// No-op move returns the same reference (no persist churn).
expect(moveFloat(moved, id, moved.floats[0]!.x, moved.floats[0]!.y)).toBe(moved)
// Resize: floors, viewport ceiling, and the SE-corner anchor (x/y keep).