fix(gui): keep sidebar controls when collapsed

This commit is contained in:
Yichen Jiang
2026-07-22 20:28:40 +08:00
parent b51d2b3d67
commit 93f1eaa972
18 changed files with 240 additions and 76 deletions
@@ -27,13 +27,8 @@
border-left: 1px solid var(--dsw-alias-border-l2);
}
/* Collapsed columns keep children mounted; the border must not paint a 1px seam.
Flags live on the frame — DetailsColumn renders inside the provider body and
does not know its own width. */
.frame[data-sidebar-collapsed] .sidebarCol {
border-right: none;
}
/* The details subtree stays mounted at zero width, so its border must not paint
a 1px seam. The collapsed sidebar instead retains a bordered compact rail. */
.frame[data-details-collapsed] .detailsCol {
border-left: none;
}
@@ -137,12 +137,14 @@ export function AppFrame(props: AppFrameProps) {
ref={frameRef}
className={css.frame}
style={{ gridTemplateColumns: `${cols.sidebar}px minmax(0, 1fr) ${cols.details}px` }}
data-sidebar-collapsed={cols.sidebar === 0 || undefined}
data-sidebar-collapsed={!sidebar.open || undefined}
data-details-collapsed={cols.details === 0 || undefined}
>
<div className={css.sidebarCol}>{props.sidebar}</div>
{props.children}
{cols.sidebar > 0 && <DragHandle left={cols.sidebar} onStart={onSidebarStart} onDrag={onSidebarDrag} />}
{sidebar.open && cols.sidebar > 0
? <DragHandle left={cols.sidebar} onStart={onSidebarStart} onDrag={onSidebarDrag} />
: null}
{cols.details > 0 && <DragHandle left={viewport - cols.details} onStart={onDetailsStart} onDrag={onDetailsDrag} />}
</div>
)
+10 -10
View File
@@ -1,9 +1,9 @@
/**
* Pure concession-chain column solver for the three-column AppFrame.
* Chain order is fixed by contract: keep center >= CENTER_MIN by shrinking
* details first, then sidebar, then auto-closing details (derived zero width —
* persisted open/width preferences are never rewritten, so widening the window
* restores them). Center absorbs any remaining deficit as the last resort.
* details first, then sidebar, then auto-closing details. A closed sidebar
* keeps its compact rail; persisted open/width preferences are never rewritten,
* so widening the window restores them. Center absorbs any remaining deficit.
*/
/** Panel viewing state consumed by the solver (mirrors LayoutService PanelState). */
@@ -21,6 +21,8 @@ export const SIDEBAR_MIN = 240
export const SIDEBAR_MAX = 420
/** Sidebar width before any user drag. */
export const SIDEBAR_DEFAULT = 300
/** Closed-sidebar rail: one 28px control between 16px horizontal paddings. */
export const SIDEBAR_COLLAPSED = 60
/** Details drag clamp floor. */
export const DETAILS_MIN = 300
/** Details drag clamp ceiling. */
@@ -47,13 +49,11 @@ export function clampWidth(px: number, min: number, max: number): number {
* @param viewport - available frame width in px.
* @param sidebar - sidebar preference (open flag + persisted width).
* @param details - details preference (open flag + persisted width).
* @returns resolved widths; details 0 means visually closed (never unmounted).
* @returns resolved widths; details 0 means visually closed, while a closed sidebar keeps its compact rail.
*/
export function computeColumns(viewport: number, sidebar: PanelInput, details: PanelInput): Columns {
const want = (p: PanelInput, min: number, max: number): number =>
p.open ? clampWidth(p.width, min, max) : 0
const s0 = want(sidebar, SIDEBAR_MIN, SIDEBAR_MAX)
const d0 = want(details, DETAILS_MIN, DETAILS_MAX)
const s0 = sidebar.open ? clampWidth(sidebar.width, SIDEBAR_MIN, SIDEBAR_MAX) : SIDEBAR_COLLAPSED
const d0 = details.open ? clampWidth(details.width, DETAILS_MIN, DETAILS_MAX) : 0
// Step 1: everything fits at preferred widths.
if (s0 + d0 + CENTER_MIN <= viewport) return { sidebar: s0, center: viewport - s0 - d0, details: d0 }
@@ -63,14 +63,14 @@ export function computeColumns(viewport: number, sidebar: PanelInput, details: P
if (s0 + d1 + CENTER_MIN <= viewport) return { sidebar: s0, center: CENTER_MIN, details: d1 }
// Step 3: shrink sidebar toward its minimum.
const s1 = s0 === 0 ? 0 : Math.max(SIDEBAR_MIN, viewport - d1 - CENTER_MIN)
const s1 = sidebar.open ? Math.max(SIDEBAR_MIN, viewport - d1 - CENTER_MIN) : SIDEBAR_COLLAPSED
if (s1 + d1 + CENTER_MIN <= viewport) return { sidebar: s1, center: CENTER_MIN, details: d1 }
// Step 4: auto-close details (derived — preferences untouched). With the
// details pressure gone the sidebar concession is re-solved from preference.
if (d1 > 0) {
if (s0 + CENTER_MIN <= viewport) return { sidebar: s0, center: viewport - s0, details: 0 }
const s2 = s0 === 0 ? 0 : Math.max(SIDEBAR_MIN, viewport - CENTER_MIN)
const s2 = sidebar.open ? Math.max(SIDEBAR_MIN, viewport - CENTER_MIN) : SIDEBAR_COLLAPSED
return { sidebar: s2, center: Math.max(0, viewport - s2), details: 0 }
}
@@ -13,7 +13,8 @@ import { LayoutService } from './service.ts'
export { AppFrame, CenterColumn, DetailsColumn, type AppFrameProps } from './AppFrame.tsx'
export {
clampWidth, computeColumns,
CENTER_MIN, DETAILS_DEFAULT, DETAILS_MAX, DETAILS_MIN, SIDEBAR_DEFAULT, SIDEBAR_MAX, SIDEBAR_MIN,
CENTER_MIN, DETAILS_DEFAULT, DETAILS_MAX, DETAILS_MIN,
SIDEBAR_COLLAPSED, SIDEBAR_DEFAULT, SIDEBAR_MAX, SIDEBAR_MIN,
type Columns, type PanelInput,
} from './columns.ts'
export { LayoutService, type NavState, type PanelState, type ViewId } from './service.ts'