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. */