@@ -162,6 +162,7 @@ onMounted(() => {
162162onUnmounted (() => {
163163 document .removeEventListener (' keydown' , onGlobalKeydown , true );
164164 stopSpinner ();
165+ clearTimeout (sidebarSwapTimer );
165166});
166167
167168// Escape closes whichever transient right-side detail panel is open.
@@ -195,10 +196,12 @@ const SIDEBAR_COLLAPSED_KEY = 'pythinker-web.sidebar-collapsed';
195196const SIDEBAR_DEFAULT = 270 ;
196197const SIDEBAR_MIN = 170 ;
197198const SIDEBAR_MAX = 420 ;
198- const SIDEBAR_COLLAPSED_WIDTH = 36 ;
199+ const SIDEBAR_COLLAPSED_WIDTH = 90 ;
199200
200201const sessionColWidth = ref (SIDEBAR_DEFAULT );
201202const sidebarCollapsed = ref (false );
203+ const railVisible = ref (false );
204+ let sidebarSwapTimer: ReturnType <typeof setTimeout > | undefined ;
202205const sideWidth = computed (() =>
203206 sidebarCollapsed .value ? SIDEBAR_COLLAPSED_WIDTH : sessionColWidth .value ,
204207);
@@ -209,6 +212,7 @@ function loadSidebarCollapsed(): void {
209212 } catch {
210213 sidebarCollapsed .value = false ;
211214 }
215+ railVisible .value = sidebarCollapsed .value ;
212216}
213217
214218function saveSidebarCollapsed(): void {
@@ -219,9 +223,23 @@ function saveSidebarCollapsed(): void {
219223 }
220224}
221225
222- function toggleSidebarCollapse( ): void {
223- sidebarCollapsed .value = ! sidebarCollapsed . value ;
226+ function setSidebarCollapsed( collapsed : boolean ): void {
227+ sidebarCollapsed .value = collapsed ;
224228 saveSidebarCollapsed ();
229+ clearTimeout (sidebarSwapTimer );
230+ sidebarSwapTimer = setTimeout (() => {
231+ railVisible .value = sidebarCollapsed .value ;
232+ }, 150 );
233+ }
234+
235+ function toggleSidebarCollapse(): void {
236+ setSidebarCollapsed (! sidebarCollapsed .value );
237+ }
238+
239+ async function expandAndSearch(): Promise <void > {
240+ setSidebarCollapsed (false );
241+ await nextTick ();
242+ void sidebarRef .value ?.openSearch ();
225243}
226244
227245// ---------------------------------------------------------------------------
@@ -565,6 +583,7 @@ watch(client.activeSessionId, () => {
565583});
566584
567585// Reference to ConversationPane so we can imperatively switch tabs
586+ const sidebarRef = ref <InstanceType <typeof Sidebar > | null >(null );
568587const conversationPaneRef = ref <InstanceType <typeof ConversationPane > | null >(null );
569588
570589// Shift-multi-selected workspace ids; when >1 are selected the main pane
@@ -872,13 +891,17 @@ function openPr(url: string): void {
872891 <div
873892 v-else
874893 class =" app"
875- :class =" { mobile: isMobile, 'sidebar-collapsed': sidebarCollapsed && !isMobile }"
894+ :class =" {
895+ mobile: isMobile,
896+ 'sidebar-collapsed': sidebarCollapsed && !isMobile,
897+ 'rail-visible': railVisible && !isMobile,
898+ }"
876899 :style =" { '--side-w': sideWidth + 'px', '--preview-w': previewWidth + 'px' }"
877900 >
878901 <!-- Desktop navigation: workspace rail + resizable session column. -->
879902 <template v-if =" ! isMobile " >
880903 <Sidebar
881- v-show = " ! sidebarCollapsed "
904+ ref="sidebarRef "
882905 :col-width =" sessionColWidth "
883906 :active-workspace =" client .visibleWorkspace .value "
884907 :active-workspace-id =" client .activeWorkspaceId .value "
@@ -910,19 +933,63 @@ function openPr(url: string): void {
910933 :max =" SIDEBAR_MAX "
911934 @update :width =" sessionColWidth = $event "
912935 />
913- <div v-if = " sidebarCollapsed " class =" sidebar-rail" >
936+ <div class =" sidebar-rail" >
914937 <button
915938 type =" button"
916- class =" sidebar-expand- btn"
939+ class =" rail- btn rail-logo "
917940 :title =" t('sidebar.expandSidebar')"
918941 :aria-label =" t('sidebar.expandSidebar')"
919942 @click =" toggleSidebarCollapse"
920943 >
921- <svg viewBox =" 0 0 24 24" width =" 16" height =" 16" fill =" none" stroke =" currentColor" stroke-width =" 2" stroke-linecap =" round" stroke-linejoin =" round" aria-hidden =" true" >
922- <path d =" M4 6h9" />
923- <path d =" M4 12h9" />
924- <path d =" M4 18h9" />
925- <path d =" M17 9l3 3-3 3" />
944+ <PythinkerLogo size="sm" />
945+ </button >
946+ <button
947+ type =" button"
948+ class =" rail-btn"
949+ :title =" t('sidebar.newSession')"
950+ :aria-label =" t('sidebar.newSession')"
951+ @click =" handleCreateSession"
952+ >
953+ <svg viewBox =" 0 0 16 16" width =" 18" height =" 18" fill =" none" stroke =" currentColor" stroke-width =" 1.6" stroke-linecap =" round" aria-hidden =" true" >
954+ <circle cx =" 8" cy =" 8" r =" 6.5" />
955+ <path d =" M8 5v6M5 8h6" />
956+ </svg >
957+ </button >
958+ <button
959+ type =" button"
960+ class =" rail-btn"
961+ :title =" t('sidebar.newWorkspace')"
962+ :aria-label =" t('sidebar.newWorkspace')"
963+ @click =" showAddWorkspace = true"
964+ >
965+ <svg viewBox =" 0 0 16 16" width =" 18" height =" 18" fill =" none" stroke =" currentColor" stroke-width =" 1.35" stroke-linecap =" round" stroke-linejoin =" round" aria-hidden =" true" >
966+ <path d =" M1 4V2.5A1 1 0 0 1 2 1.5h3.5l1.3 2H13a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1z" />
967+ <path d =" M1 5.5h13M10.5 8v3M9 9.5h3" />
968+ </svg >
969+ </button >
970+ <button
971+ type =" button"
972+ class =" rail-btn"
973+ :title =" t('sidebar.searchSessions')"
974+ :aria-label =" t('sidebar.searchSessions')"
975+ @click =" expandAndSearch"
976+ >
977+ <svg viewBox =" 0 0 16 16" width =" 18" height =" 18" fill =" none" stroke =" currentColor" stroke-width =" 1.5" stroke-linecap =" round" aria-hidden =" true" >
978+ <circle cx =" 7" cy =" 7" r =" 4.5" />
979+ <path d =" m10.5 10.5 3 3" />
980+ </svg >
981+ </button >
982+ <div class =" rail-spacer" />
983+ <button
984+ type =" button"
985+ class =" rail-btn"
986+ :title =" t('settings.title')"
987+ :aria-label =" t('settings.title')"
988+ @click =" showSettings = true"
989+ >
990+ <svg viewBox =" 0 0 24 24" width =" 18" height =" 18" fill =" none" stroke =" currentColor" stroke-width =" 2" stroke-linecap =" round" stroke-linejoin =" round" aria-hidden =" true" >
991+ <circle cx =" 12" cy =" 12" r =" 3" />
992+ <path d =" M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l-.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09A1.65 1.65 0 0 0 15 4.6a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09A1.65 1.65 0 0 0 19.4 15z" />
926993 </svg >
927994 </button >
928995 </div >
@@ -1347,6 +1414,7 @@ function openPr(url: string): void {
13471414 when closed) — opening animates the aside's width, so the conversation
13481415 column is squeezed over smoothly instead of snapping to a new template. */
13491416 grid-template-columns : var (--side-w ) 0 minmax (0 , 1fr ) 0 auto ;
1417+ transition : grid-template-columns 0.3s cubic-bezier (0.4 , 0 , 0.2 , 1 );
13501418 background : var (--bg );
13511419 color : var (--ink );
13521420 overflow : hidden ;
@@ -1375,22 +1443,62 @@ function openPr(url: string): void {
13751443 min-height : 0 ;
13761444 min-width : 0 ;
13771445}
1446+ .app > .side {
1447+ grid-column : 1 ;
1448+ grid-row : 1 ;
1449+ overflow : hidden ;
1450+ }
1451+
1452+ /* Which layer is shown — swapped 150ms after the toggle. */
1453+ :global(.app.rail-visible > .side ) {
1454+ visibility : hidden ;
1455+ pointer-events : none ;
1456+ }
1457+ :global(.app :not (.rail-visible ) .sidebar-rail ) {
1458+ visibility : hidden ;
1459+ pointer-events : none ;
1460+ }
13781461
1379- /* Collapsed sidebar rail: keeps a slim, dedicated grid track so the expand
1380- button never overlaps the conversation header or squeezes the main pane. */
1462+ /* Phase 1a: collapsing — expanded content's children fade out. */
1463+ :global(.app.sidebar-collapsed :not (.rail-visible ) > .side .col > * ) {
1464+ opacity : 0 ;
1465+ transition : opacity 0.15s cubic-bezier (0.4 , 0 , 0.2 , 1 );
1466+ }
1467+ /* Phase 1b: expanding — rail's children fade out. */
1468+ :global(.app :not (.sidebar-collapsed ).rail-visible .sidebar-rail > * ) {
1469+ opacity : 0 ;
1470+ transition : opacity 0.15s cubic-bezier (0.4 , 0 , 0.2 , 1 );
1471+ }
1472+ /* Phase 2a: rail items animate in (collapse direction). */
1473+ :global(.app.sidebar-collapsed.rail-visible .sidebar-rail > .rail-btn ) {
1474+ animation : rail-in 0.15s cubic-bezier (0.4 , 0 , 0.2 , 1 ) backwards ;
1475+ }
1476+ /* Phase 2b: sidebar content animates back in (expand direction). */
1477+ :global(.app :not (.sidebar-collapsed ):not (.rail-visible ) > .side .col > * ) {
1478+ animation : content-in 0.15s cubic-bezier (0.4 , 0 , 0.2 , 1 ) backwards ;
1479+ }
1480+ @keyframes rail-in { 0% { opacity : 0 ; transform : translateX (66px ); } }
1481+ @keyframes content-in { 0% { opacity : 0 ; } }
1482+
1483+ /* Collapsed sidebar rail: keeps a dedicated grid track for the icon rail. */
13811484.sidebar-rail {
13821485 grid-column : 1 ;
1486+ grid-row : 1 ;
13831487 display : flex ;
1384- justify-content : center ;
1385- padding-top : 8px ;
1488+ flex-direction : column ;
1489+ align-items : center ;
1490+ gap : 6px ;
1491+ padding : 10px 0 10px ;
13861492 background : var (--panel );
13871493 border-right : 1px solid var (--line );
1494+ min-height : 0 ;
13881495}
1389- .sidebar-expand-btn {
1496+ .rail-spacer { flex : 1 ; }
1497+ .rail-btn {
13901498 flex : none ;
1391- width : 28 px ;
1392- height : 28 px ;
1393- border-radius : 6 px ;
1499+ width : 36 px ;
1500+ height : 36 px ;
1501+ border-radius : 8 px ;
13941502 background : none ;
13951503 border : none ;
13961504 color : var (--muted );
@@ -1400,14 +1508,18 @@ function openPr(url: string): void {
14001508 cursor : pointer ;
14011509 padding : 0 ;
14021510}
1403- .sidebar-expand-btn :hover {
1404- background : var (--soft );
1405- color : var (--ink );
1406- }
1407- .sidebar-expand-btn :focus-visible {
1511+ .rail-btn :hover { background : var (--soft ); color : var (--ink ); }
1512+ .rail-btn :focus-visible {
14081513 outline : 2px solid var (--blue );
14091514 outline-offset : -2px ;
14101515}
1516+ :global(html [data-desktop-platform = ' darwin' ] .sidebar-rail ) {
1517+ padding-top : 48px ;
1518+ -webkit-app-region : drag;
1519+ }
1520+ :global(html [data-desktop-platform = ' darwin' ] .sidebar-rail button ) {
1521+ -webkit-app-region : no-drag;
1522+ }
14111523
14121524/* The collapsed rail occupies track 1; keep the main pane pinned to the
14131525 conversation track even though the sidebar/handle are display:none. */
@@ -1423,6 +1535,20 @@ function openPr(url: string): void {
14231535 grid-template-rows : auto 1fr ;
14241536}
14251537
1538+ @media (prefers-reduced-motion: reduce) {
1539+ .app { transition : none ; }
1540+ .app > .side ,
1541+ .sidebar-rail { transition : none ; }
1542+ :global(.app.sidebar-collapsed :not (.rail-visible ) > .side .col > * ),
1543+ :global(.app :not (.sidebar-collapsed ).rail-visible .sidebar-rail > * ) {
1544+ transition : none ;
1545+ }
1546+ :global(.app.sidebar-collapsed.rail-visible .sidebar-rail > .rail-btn ),
1547+ :global(.app :not (.sidebar-collapsed ):not (.rail-visible ) > .side .col > * ) {
1548+ animation : none ;
1549+ }
1550+ }
1551+
14261552/* The right-side panel column: a permanent grid item whose width animates
14271553 0 ↔ var(--preview-w). The CONTENT keeps a fixed width (and carries the
14281554 left hairline) so it clips during the transition instead of reflowing. */
0 commit comments