sidebar: + menu always reachable on empty panes, unified default width (content-area half), expand/collapse animation
- An empty pane (fresh split, or last tab dragged out) now keeps the tab strip with its + menu mounted, so a new tab can always be created or dragged in; the placeholder body still explains the state. - Default width unified: new sessions open at half the CONTENT area (viewport minus the left sidebar), so the panel never swallows half the whole window and old/new sessions no longer disagree (previously old persisted widths ~1/4 vs fresh sessions 1/2). Fullscreen restore targets the same content-half width. - Expand/collapse now animates: the panel stays mounted and slides off-screen (translateX + visibility after the slide); fullscreen width changes animate too, and drag-resizing disables the transition.
This commit is contained in:
+12
-4
@@ -196,8 +196,15 @@ export function Sidebar(props: { ctx: Context }) {
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{state.panelOpen && (
|
||||
<div className={css.panel} style={{ width: state.width }} data-dragging={draggingWidth || undefined}>
|
||||
{/*
|
||||
The panel stays mounted while collapsed (hidden off-screen) so the
|
||||
slide in/out can animate; visibility hides it after the slide settles.
|
||||
*/}
|
||||
<div
|
||||
className={clsx(css.panel, !state.panelOpen && css.panelHidden)}
|
||||
style={{ width: state.width }}
|
||||
data-dragging={draggingWidth || undefined}
|
||||
>
|
||||
<div
|
||||
className={clsx(css.panelResize, draggingWidth && css.panelResizeActive)}
|
||||
onPointerDown={(event) => {
|
||||
@@ -225,7 +232,9 @@ export function Sidebar(props: { ctx: Context }) {
|
||||
title={fullscreen ? t('restoreFullscreen') : t('expandFullscreen')}
|
||||
onClick={() => {
|
||||
const viewport = window.innerWidth
|
||||
sidebarStore.reduce(s => setWidth(s, fullscreen ? Math.round(viewport / 2) : viewport))
|
||||
sidebarStore.reduce(s => setWidth(s, fullscreen
|
||||
? Math.max(280, Math.round((viewport - 320) / 2))
|
||||
: viewport))
|
||||
}}
|
||||
>
|
||||
<IconFullscreenOutline16 />
|
||||
@@ -254,7 +263,6 @@ export function Sidebar(props: { ctx: Context }) {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -57,6 +57,27 @@
|
||||
background: var(--dsw-alias-bg-layer-1);
|
||||
border-left: 1px solid var(--dsw-alias-border-l2);
|
||||
box-shadow: var(--dsw-shadow-lv2);
|
||||
/* Expand/collapse slides the panel; fullscreen animates the width. */
|
||||
transition:
|
||||
transform var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
width var(--ds-transition-duration-slow) var(--ds-ease-in-out);
|
||||
}
|
||||
|
||||
/* Collapsed: slid off-screen, interactive nothing, hidden after the slide. */
|
||||
.panelHidden {
|
||||
transform: translateX(102%);
|
||||
pointer-events: none;
|
||||
box-shadow: none;
|
||||
visibility: hidden;
|
||||
transition:
|
||||
transform var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
width var(--ds-transition-duration-slow) var(--ds-ease-in-out),
|
||||
visibility 0s linear var(--ds-transition-duration-slow);
|
||||
}
|
||||
|
||||
/* Width drags write at pointer cadence; easing would detach the edge. */
|
||||
.panel[data-dragging] {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.panelResize {
|
||||
|
||||
@@ -114,8 +114,11 @@ function LeafView(props: {
|
||||
}}
|
||||
>
|
||||
{dropZone !== null && <div className={clsx(css.dropOverlay, css[`drop${dropZone[0]!.toUpperCase()}${dropZone.slice(1)}`])} />}
|
||||
{leaf.tabs.length > 0 ? (
|
||||
<>
|
||||
{/*
|
||||
The tab strip renders even for an empty pane: the + menu must stay
|
||||
reachable when the pane has no tabs (fresh split, or the last tab was
|
||||
dragged out), so a new tab can always be created or dragged in.
|
||||
*/}
|
||||
<TabBar
|
||||
paneId={leaf.id}
|
||||
tabs={leaf.tabs}
|
||||
@@ -129,8 +132,8 @@ function LeafView(props: {
|
||||
else actions.moveTabBefore(payload, leaf.id, before)
|
||||
}}
|
||||
/>
|
||||
{leaf.tabs.length > 0 ? (
|
||||
<div className={css.paneContent}>{activeTab !== undefined ? renderTab(activeTab) : null}</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={css.paneEmpty}>{t('emptyPane')}</div>
|
||||
)}
|
||||
|
||||
+9
-3
@@ -54,6 +54,8 @@ export interface SidebarState {
|
||||
export const PANEL_MIN = 280
|
||||
export const PANEL_MAX = 640
|
||||
export const PANEL_DEFAULT = 400
|
||||
/** Approximate default width of the host's left sidebar (columns.ts SIDEBAR_DEFAULT). */
|
||||
export const LEFT_SIDEBAR_DEFAULT = 320
|
||||
export const TAB_MAX_WIDTH = 160
|
||||
export const TERMINAL_LIMIT = 3
|
||||
|
||||
@@ -372,9 +374,13 @@ function loadState(sessionId: string): SidebarState {
|
||||
} catch {
|
||||
// Corrupt or unavailable storage: fall through to the default.
|
||||
}
|
||||
// New sessions open at half the viewport (VSCode-like default split).
|
||||
const half = typeof window !== 'undefined' ? Math.max(PANEL_MIN, Math.round(window.innerWidth / 2)) : PANEL_DEFAULT
|
||||
return makeDefaultState(half)
|
||||
// New sessions open at half the CONTENT area (the viewport minus the
|
||||
// left sidebar's default width), so the panel never swallows half the
|
||||
// whole window; the same default applies to every session.
|
||||
const contentHalf = typeof window !== 'undefined'
|
||||
? Math.max(PANEL_MIN, Math.round((window.innerWidth - LEFT_SIDEBAR_DEFAULT) / 2))
|
||||
: PANEL_DEFAULT
|
||||
return makeDefaultState(contentHalf)
|
||||
}
|
||||
|
||||
/** The session-scoped store: one state per conversation, localStorage-backed. */
|
||||
|
||||
Reference in New Issue
Block a user