diff --git a/src/client/Sidebar.tsx b/src/client/Sidebar.tsx index a2449c2..ba5cc78 100644 --- a/src/client/Sidebar.tsx +++ b/src/client/Sidebar.tsx @@ -196,8 +196,15 @@ export function Sidebar(props: { ctx: Context }) { )} - {state.panelOpen && ( -
+ {/* + The panel stays mounted while collapsed (hidden off-screen) so the + slide in/out can animate; visibility hides it after the slide settles. + */} +
{ @@ -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)) }} > @@ -244,17 +253,16 @@ export function Sidebar(props: { ctx: Context }) {
-
- -
+
+
- )} +
) } diff --git a/src/client/sidebar.module.css b/src/client/sidebar.module.css index d112fd4..3b54ac8 100644 --- a/src/client/sidebar.module.css +++ b/src/client/sidebar.module.css @@ -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 { diff --git a/src/client/split-pane.tsx b/src/client/split-pane.tsx index abd8bcb..094340f 100644 --- a/src/client/split-pane.tsx +++ b/src/client/split-pane.tsx @@ -114,23 +114,26 @@ function LeafView(props: { }} > {dropZone !== null &&
} + {/* + 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. + */} + { actions.activateTab(leaf.id, tabId) }} + onClose={(tabId) => { actions.closeTab(leaf.id, tabId) }} + onNewTab={onNewTab} + newTabOptions={newTabOptions} + onDropTab={(payload, before) => { + if (before === null) actions.moveTabToEdge(payload, leaf.id, 'center') + else actions.moveTabBefore(payload, leaf.id, before) + }} + /> {leaf.tabs.length > 0 ? ( - <> - { actions.activateTab(leaf.id, tabId) }} - onClose={(tabId) => { actions.closeTab(leaf.id, tabId) }} - onNewTab={onNewTab} - newTabOptions={newTabOptions} - onDropTab={(payload, before) => { - if (before === null) actions.moveTabToEdge(payload, leaf.id, 'center') - else actions.moveTabBefore(payload, leaf.id, before) - }} - /> -
{activeTab !== undefined ? renderTab(activeTab) : null}
- +
{activeTab !== undefined ? renderTab(activeTab) : null}
) : (
{t('emptyPane')}
)} diff --git a/src/client/state.ts b/src/client/state.ts index 088f7c2..1ad90e3 100644 --- a/src/client/state.ts +++ b/src/client/state.ts @@ -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. */