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>
|
</Tooltip>
|
||||||
</div>
|
</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
|
<div
|
||||||
className={clsx(css.panelResize, draggingWidth && css.panelResizeActive)}
|
className={clsx(css.panelResize, draggingWidth && css.panelResizeActive)}
|
||||||
onPointerDown={(event) => {
|
onPointerDown={(event) => {
|
||||||
@@ -225,7 +232,9 @@ export function Sidebar(props: { ctx: Context }) {
|
|||||||
title={fullscreen ? t('restoreFullscreen') : t('expandFullscreen')}
|
title={fullscreen ? t('restoreFullscreen') : t('expandFullscreen')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const viewport = window.innerWidth
|
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 />
|
<IconFullscreenOutline16 />
|
||||||
@@ -254,7 +263,6 @@ export function Sidebar(props: { ctx: Context }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,27 @@
|
|||||||
background: var(--dsw-alias-bg-layer-1);
|
background: var(--dsw-alias-bg-layer-1);
|
||||||
border-left: 1px solid var(--dsw-alias-border-l2);
|
border-left: 1px solid var(--dsw-alias-border-l2);
|
||||||
box-shadow: var(--dsw-shadow-lv2);
|
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 {
|
.panelResize {
|
||||||
|
|||||||
@@ -114,8 +114,11 @@ function LeafView(props: {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{dropZone !== null && <div className={clsx(css.dropOverlay, css[`drop${dropZone[0]!.toUpperCase()}${dropZone.slice(1)}`])} />}
|
{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
|
<TabBar
|
||||||
paneId={leaf.id}
|
paneId={leaf.id}
|
||||||
tabs={leaf.tabs}
|
tabs={leaf.tabs}
|
||||||
@@ -129,8 +132,8 @@ function LeafView(props: {
|
|||||||
else actions.moveTabBefore(payload, leaf.id, before)
|
else actions.moveTabBefore(payload, leaf.id, before)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{leaf.tabs.length > 0 ? (
|
||||||
<div className={css.paneContent}>{activeTab !== undefined ? renderTab(activeTab) : null}</div>
|
<div className={css.paneContent}>{activeTab !== undefined ? renderTab(activeTab) : null}</div>
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<div className={css.paneEmpty}>{t('emptyPane')}</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_MIN = 280
|
||||||
export const PANEL_MAX = 640
|
export const PANEL_MAX = 640
|
||||||
export const PANEL_DEFAULT = 400
|
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 TAB_MAX_WIDTH = 160
|
||||||
export const TERMINAL_LIMIT = 3
|
export const TERMINAL_LIMIT = 3
|
||||||
|
|
||||||
@@ -372,9 +374,13 @@ function loadState(sessionId: string): SidebarState {
|
|||||||
} catch {
|
} catch {
|
||||||
// Corrupt or unavailable storage: fall through to the default.
|
// Corrupt or unavailable storage: fall through to the default.
|
||||||
}
|
}
|
||||||
// New sessions open at half the viewport (VSCode-like default split).
|
// New sessions open at half the CONTENT area (the viewport minus the
|
||||||
const half = typeof window !== 'undefined' ? Math.max(PANEL_MIN, Math.round(window.innerWidth / 2)) : PANEL_DEFAULT
|
// left sidebar's default width), so the panel never swallows half the
|
||||||
return makeDefaultState(half)
|
// 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. */
|
/** The session-scoped store: one state per conversation, localStorage-backed. */
|
||||||
|
|||||||
Reference in New Issue
Block a user