fix: keep restored mobile drawer closed
This commit is contained in:
committed by
Menghuan1918
parent
41da8cb101
commit
2d2e8534a5
@@ -51,7 +51,7 @@
|
||||
|
||||
### 4.4 行为修正
|
||||
|
||||
- `state.ts` `loadState`:窄屏新会话默认 `panelOpen=false`(首开才生效;用户手动展开后照常持久化)。
|
||||
- `state.ts` `loadState`:窄屏加载一律以 `panelOpen=false` 开始(包括持久化布局恢复);仅覆盖抽屉可见性,右侧/底部 pane、tab 与自由窗口状态原样恢复。宽屏仍按持久化值或新会话偏好决定展开状态。
|
||||
- `service.ts` `openTab`:窄屏且 seed 带 `path`/`url` 且面板收起 → 自动展开抽屉(dedupe-focus 分支同样生效);纯类型打开不展开;宽屏行为不变。
|
||||
|
||||
### 4.5 样式(`sidebar.module.css`)
|
||||
@@ -61,7 +61,7 @@
|
||||
### 4.6 测试
|
||||
|
||||
- `tests/breakpoints.spec.ts`:断点边界(767 窄 / 768 宽;1024 明确为宽)。
|
||||
- `tests/unit.spec.ts`:`migrateBottomTabs` 4 例(并入第一 leaf 且底部清空/面板关/activePane 重指、右树为 split 时并入最左 leaf、幂等性、空底部树但 activePane 在底部树时重指)+ 窄屏新会话默认收起(stub `window.innerWidth=390`)。
|
||||
- `tests/unit.spec.ts`:`migrateBottomTabs` 4 例(并入第一 leaf 且底部清空/面板关/activePane 重指、右树为 split 时并入最左 leaf、幂等性、空底部树但 activePane 在底部树时重指)+ 窄屏新会话默认收起;`tests/state.spec.ts` 守护窄屏恢复强制收起且 pane / free-window 状态不丢,768px 宽屏仍恢复持久化展开值。
|
||||
- `tests/service.spec.ts`:窄屏 auto-expand 5 例(390 < 768 仍窄)。
|
||||
|
||||
## 5. 边界情况
|
||||
|
||||
+12
-5
@@ -1142,6 +1142,7 @@ function resetRequested(): boolean {
|
||||
|
||||
function loadState(sessionId: string, prefs: SidebarPrefs): SidebarState {
|
||||
const reset = resetRequested()
|
||||
const viewport = typeof window !== 'undefined' ? window.innerWidth : undefined
|
||||
if (reset) {
|
||||
try {
|
||||
localStorage.removeItem(`${STORAGE_PREFIX}:${sessionId}`)
|
||||
@@ -1164,7 +1165,15 @@ function loadState(sessionId: string, prefs: SidebarPrefs): SidebarState {
|
||||
nextIdCounter = maxCounterId(parsed)
|
||||
const sanitized = sanitizeState(parsed)
|
||||
if (sanitized !== undefined) {
|
||||
return globalWidth === undefined ? sanitized : { ...sanitized, width: globalWidth }
|
||||
const restored = globalWidth === undefined ? sanitized : { ...sanitized, width: globalWidth }
|
||||
// A narrow panel is a full-screen drawer, so every page/session load
|
||||
// starts it closed even when the prior desktop/mobile state persisted
|
||||
// `panelOpen: true`. Preserve the entire restored workbench (panes,
|
||||
// tabs, bottom panel, and free windows); visibility is the sole
|
||||
// viewport-derived override.
|
||||
return viewport !== undefined && isNarrowWidth(viewport) && restored.panelOpen
|
||||
? { ...restored, panelOpen: false }
|
||||
: restored
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -1179,10 +1188,8 @@ function loadState(sessionId: string, prefs: SidebarPrefs): SidebarState {
|
||||
// disabled editor type seeds nothing. On a NARROW viewport a brand-new
|
||||
// session starts collapsed instead — the panel is a full-screen drawer
|
||||
// there, and auto-opening it on first paint would cover the conversation
|
||||
// before the user asked. Only the first seeding is affected: once the
|
||||
// user expands the drawer, `panelOpen: true` persists like any other
|
||||
// state.
|
||||
const viewport = typeof window !== 'undefined' ? window.innerWidth : undefined
|
||||
// before the user asked. Persisted layouts follow the same narrow-load
|
||||
// visibility rule above, while their workbench contents remain intact.
|
||||
const width = globalWidth ?? (viewport === undefined
|
||||
? PANEL_DEFAULT
|
||||
: defaultWidthFor(viewport, prefs.defaultWidthPercent))
|
||||
|
||||
+2
-2
@@ -306,8 +306,8 @@ describe('side card preferences', () => {
|
||||
}
|
||||
try {
|
||||
const store = createSidebarStore()
|
||||
// The narrow viewport keeps a fresh session collapsed for the FIRST
|
||||
// seeding only (a later user expansion persists).
|
||||
// The narrow viewport keeps a fresh session collapsed. Persisted
|
||||
// sessions follow the same load-time visibility rule in state.spec.ts.
|
||||
store.setSession('narrow-fresh')
|
||||
expect(store.getSnapshot().state?.panelOpen).toBe(false)
|
||||
// The width seeding still follows the window (clamped to the floor).
|
||||
|
||||
@@ -1328,6 +1328,105 @@ describe('pinned terminals (v0.17.0)', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('narrow persisted-layout visibility (issue #162)', () => {
|
||||
const persistedLayout = JSON.stringify({
|
||||
panelOpen: true,
|
||||
width: 420,
|
||||
nextTerminal: 2,
|
||||
nextBrowser: 2,
|
||||
activePane: 'pane:2',
|
||||
expanded: ['/workspace/src'],
|
||||
splits: {
|
||||
kind: 'split',
|
||||
id: 'split:1',
|
||||
dir: 'row',
|
||||
sizes: [0.5, 0.5],
|
||||
children: [
|
||||
{
|
||||
kind: 'leaf',
|
||||
id: 'pane:1',
|
||||
active: 'editor:readme',
|
||||
tabs: [{ id: 'editor:readme', type: 'editor', title: 'README.md', path: '/workspace/README.md' }],
|
||||
},
|
||||
{
|
||||
kind: 'leaf',
|
||||
id: 'pane:2',
|
||||
active: 'terminal:1',
|
||||
tabs: [{ id: 'terminal:1', type: 'terminal', title: 'Terminal 1' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
bottomOpen: true,
|
||||
bottomHeight: 300,
|
||||
bottomOpenedOnce: true,
|
||||
bottomSplits: {
|
||||
kind: 'leaf',
|
||||
id: 'pane:3',
|
||||
active: 'browser:1',
|
||||
tabs: [{ id: 'browser:1', type: 'browser', title: 'Docs', path: 'https://example.com' }],
|
||||
},
|
||||
floats: [{
|
||||
id: 'float:4',
|
||||
tab: { id: 'notes:1', type: 'plugin:notes', title: 'Notes', meta: { draft: true } },
|
||||
x: 16,
|
||||
y: 24,
|
||||
w: 320,
|
||||
h: 240,
|
||||
}],
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
const g = globalThis as Record<string, unknown>
|
||||
delete g.window
|
||||
delete g.localStorage
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ width: 390, expectedOpen: false },
|
||||
{ width: 768, expectedOpen: true },
|
||||
])('restores panelOpen=$expectedOpen at $width px without changing pane or free-window state', ({ width, expectedOpen }) => {
|
||||
const g = globalThis as Record<string, unknown>
|
||||
g.window = {
|
||||
clearTimeout: () => {},
|
||||
setTimeout: () => 0,
|
||||
innerWidth: width,
|
||||
innerHeight: 800,
|
||||
location: { search: '' },
|
||||
}
|
||||
g.localStorage = {
|
||||
getItem: (key: string) => key === 'dsh-sidebar:v1:restored' ? persistedLayout : null,
|
||||
setItem: () => {},
|
||||
removeItem: () => {},
|
||||
}
|
||||
|
||||
const store = createSidebarStore()
|
||||
store.setSession('restored')
|
||||
const state = store.getSnapshot().state!
|
||||
|
||||
expect(state.panelOpen).toBe(expectedOpen)
|
||||
expect(state.activePane).toBe('pane:2')
|
||||
expect(allLeaves(state.splits).map(leaf => ({
|
||||
id: leaf.id,
|
||||
active: leaf.active,
|
||||
tabs: leaf.tabs.map(tab => tab.id),
|
||||
}))).toEqual([
|
||||
{ id: 'pane:1', active: 'editor:readme', tabs: ['editor:readme'] },
|
||||
{ id: 'pane:2', active: 'terminal:1', tabs: ['terminal:1'] },
|
||||
])
|
||||
expect(state.bottomOpen).toBe(true)
|
||||
expect(allLeaves(state.bottomSplits).map(leaf => ({ id: leaf.id, tabs: leaf.tabs.map(tab => tab.id) })))
|
||||
.toEqual([{ id: 'pane:3', tabs: ['browser:1'] }])
|
||||
expect(state.floats).toEqual([{
|
||||
id: 'float:4',
|
||||
tab: { id: 'notes:1', type: 'plugin:notes', title: 'Notes', meta: { draft: true } },
|
||||
x: 16,
|
||||
y: 24,
|
||||
w: 320,
|
||||
h: 240,
|
||||
}])
|
||||
})
|
||||
})
|
||||
|
||||
describe('URL reset escape hatch (issue #369)', () => {
|
||||
// Same browser-global stubs as the v0.12.0 block above; loadState reads
|
||||
// window.location.search (reset param) and localStorage (persisted state).
|
||||
|
||||
Reference in New Issue
Block a user