@@ -78,7 +78,6 @@ const AppSidebar: React.FC<AppSidebarProps> = ({
7878 const [ showDbPicker , setShowDbPicker ] = useState ( false ) ;
7979 const [ latencyMs , setLatencyMs ] = useState < number | null > ( null ) ;
8080 const [ collectionSort , setCollectionSort ] = useState < 'name_asc' | 'name_desc' | 'count_desc' | 'count_asc' > ( 'name_asc' ) ;
81- const [ isSwitching , setIsSwitching ] = useState ( false ) ;
8281 const chipRef = useRef < HTMLDivElement > ( null ) ;
8382
8483 useEffect ( ( ) => {
@@ -92,11 +91,6 @@ const AppSidebar: React.FC<AppSidebarProps> = ({
9291 return ( ) => document . removeEventListener ( 'mousedown' , handleClick ) ;
9392 } , [ showDbPicker ] ) ;
9493
95- // Reset loading state whenever the connection actually changes
96- useEffect ( ( ) => {
97- setIsSwitching ( false ) ;
98- } , [ accountId , databaseName ] ) ;
99-
10094 // Measure API latency by pinging /health every 30s
10195 useEffect ( ( ) => {
10296 const measure = async ( ) => {
@@ -139,145 +133,128 @@ const AppSidebar: React.FC<AppSidebarProps> = ({
139133 } } > QueryPal</ span >
140134 </ Link >
141135
142- { /* Connection chip */ }
143- { ( accountName || isSwitching ) && (
144- < div ref = { chipRef } style = { { position : 'relative' } } >
145- < style > { `
146- @keyframes qp-spin { to { transform: rotate(360deg); } }
147- @keyframes qp-pulse { 0%,100% { opacity: 0.45; } 50% { opacity: 0.9; } }
148- ` } </ style >
149- { isSwitching ? (
150- < div style = { { display : 'flex' , alignItems : 'center' , gap : 7 , padding : '5px 9px' , background : 'var(--soft)' , borderRadius : 7 } } >
151- < div style = { { width : 14 , height : 14 , borderRadius : '50%' , border : '2px solid var(--border)' , borderTopColor : 'var(--accent)' , animation : 'qp-spin 0.7s linear infinite' , flexShrink : 0 } } />
136+ { /* Connection chip — always rendered so the brand section never changes height */ }
137+ < div ref = { chipRef } style = { { position : 'relative' } } >
138+ { ! accountName ? (
139+ /* Placeholder keeps the same height as the real chip */
140+ < div style = { { display : 'flex' , alignItems : 'center' , gap : 7 , padding : '5px 9px' , background : 'var(--soft)' , borderRadius : 7 } } >
141+ < span style = { { width : 14 , height : 14 , borderRadius : 4 , background : 'var(--border)' , flexShrink : 0 } } />
142+ < div style = { { minWidth : 0 , flex : 1 } } >
143+ < div style = { { height : 11 , borderRadius : 3 , background : 'var(--border)' , width : '65%' , marginBottom : 4 } } />
144+ < div style = { { height : 9 , borderRadius : 3 , background : 'var(--border)' , width : '45%' } } />
145+ </ div >
146+ </ div >
147+ ) : (
148+ < >
149+ < div
150+ onClick = { ( ) => ( ( availableAccounts && availableAccounts . length > 1 ) || ( availableDbs && availableDbs . length > 1 ) ) ? setShowDbPicker ( v => ! v ) : undefined }
151+ style = { {
152+ display : 'flex' , alignItems : 'center' , gap : 7 ,
153+ padding : '5px 9px' , background : 'var(--soft)' , borderRadius : 7 ,
154+ cursor : ( ( availableAccounts && availableAccounts . length > 1 ) || ( availableDbs && availableDbs . length > 1 ) ) ? 'pointer' : 'default' ,
155+ } }
156+ >
157+ < span style = { { width : 14 , height : 14 , borderRadius : 4 , background : '#1d6cf2' , flexShrink : 0 } } />
152158 < div style = { { minWidth : 0 , flex : 1 } } >
153- < div style = { { fontSize : 11.5 , fontWeight : 500 , color : 'var(--muted )' , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } >
154- Connecting…
159+ < div style = { { fontSize : 11.5 , fontWeight : 500 , color : 'var(--fg )' , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } >
160+ { accountName }
155161 </ div >
156- < div style = { { height : 9 , marginTop : 3 , borderRadius : 4 , background : 'var(--border)' , animation : 'qp-pulse 1.4s ease-in-out infinite' , width : '70%' } } />
162+ { databaseName && (
163+ < div style = { { fontSize : 10.5 , color : 'var(--muted)' , fontFamily : 'var(--font-mono)' , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } >
164+ { databaseName }
165+ </ div >
166+ ) }
157167 </ div >
168+ { ( ( availableAccounts && availableAccounts . length > 1 ) || ( availableDbs && availableDbs . length > 1 ) ) && (
169+ < svg width = "10" height = "10" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "1.8" style = { { color : 'var(--muted)' , flexShrink : 0 } } >
170+ < path d = "M4 6l4 4 4-4" />
171+ </ svg >
172+ ) }
158173 </ div >
159- ) : ( ( ) => {
160- const hasMultipleAccounts = availableAccounts && availableAccounts . length > 1 ;
161- const hasMultipleDbs = availableDbs && availableDbs . length > 1 ;
162- const isPickerEnabled = hasMultipleAccounts || hasMultipleDbs ;
163- return (
164- < >
165- < div
166- onClick = { ( ) => isPickerEnabled ? setShowDbPicker ( v => ! v ) : undefined }
167- style = { {
168- display : 'flex' , alignItems : 'center' , gap : 7 ,
169- padding : '5px 9px' , background : 'var(--soft)' , borderRadius : 7 ,
170- cursor : isPickerEnabled ? 'pointer' : 'default' ,
171- } }
172- >
173- < span style = { { width : 14 , height : 14 , borderRadius : 4 , background : '#1d6cf2' , flexShrink : 0 } } />
174- < div style = { { minWidth : 0 , flex : 1 } } >
175- < div style = { { fontSize : 11.5 , fontWeight : 500 , color : 'var(--fg)' , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } >
176- { accountName }
177- </ div >
178- { databaseName && (
179- < div style = { { fontSize : 10.5 , color : 'var(--muted)' , fontFamily : 'var(--font-mono)' , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } >
180- { databaseName }
181- </ div >
182- ) }
183- </ div >
184- { isPickerEnabled && (
185- < svg width = "10" height = "10" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "1.8" style = { { color : 'var(--muted)' , flexShrink : 0 } } >
186- < path d = "M4 6l4 4 4-4" />
187- </ svg >
188- ) }
189- </ div >
190-
191- { showDbPicker && (
192- < div style = { {
193- position : 'absolute' , top : 'calc(100% + 4px)' , left : 0 , right : 0 , zIndex : 100 ,
194- background : 'var(--panel)' , border : '1px solid var(--border)' , borderRadius : 8 ,
195- boxShadow : '0 4px 16px rgba(0,0,0,0.10)' , overflow : 'hidden' ,
196- } } >
197- { /* Accounts section */ }
198- { hasMultipleAccounts && availableAccounts && (
199- < >
200- < div style = { { padding : '6px 10px 4px' , fontSize : 10.5 , textTransform : 'uppercase' , letterSpacing : '0.06em' , color : 'var(--muted)' , fontWeight : 500 } } >
201- Cosmos account
202- </ div >
203- { availableAccounts . map ( acc => {
204- const isCurrent = acc . id === accountId ;
205- return (
206- < button
207- key = { acc . id }
208- onClick = { ( ) => { if ( ! isCurrent ) { setIsSwitching ( true ) ; setShowDbPicker ( false ) ; onSwitchAccount ?.( acc ) ; } } }
209- style = { {
210- width : '100%' , display : 'flex' , alignItems : 'center' , gap : 8 ,
211- padding : '7px 10px' , border : 'none' , textAlign : 'left' ,
212- cursor : isCurrent ? 'default' : 'pointer' ,
213- background : isCurrent ? 'var(--accent-soft)' : 'transparent' ,
214- color : isCurrent ? 'var(--accent)' : 'var(--fg)' ,
215- fontSize : 12.5 , fontFamily : 'var(--font-body)' ,
216- } }
217- onMouseEnter = { ( e ) => { if ( ! isCurrent ) ( e . currentTarget as HTMLElement ) . style . background = 'var(--soft)' ; } }
218- onMouseLeave = { ( e ) => { if ( ! isCurrent ) ( e . currentTarget as HTMLElement ) . style . background = 'transparent' ; } }
219- >
220- < span style = { { width : 10 , height : 10 , borderRadius : 3 , background : isCurrent ? '#1d6cf2' : 'var(--muted)' , flexShrink : 0 } } />
221- < span style = { { flex : 1 , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } > { acc . name } </ span >
222- { isCurrent && (
223- < svg width = "11" height = "11" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "2" style = { { color : 'var(--accent)' , flexShrink : 0 } } >
224- < path d = "M3 8l4 4 6-6" />
225- </ svg >
226- ) }
227- </ button >
228- ) ;
229- } ) }
230- </ >
231- ) }
232174
233- { /* Divider between sections */ }
234- { hasMultipleAccounts && hasMultipleDbs && (
235- < div style = { { height : 1 , background : 'var(--border)' , margin : '4px 0' } } />
236- ) }
237-
238- { /* Databases section */ }
239- { hasMultipleDbs && availableDbs && (
240- < >
241- < div style = { { padding : '6px 10px 4px' , fontSize : 10.5 , textTransform : 'uppercase' , letterSpacing : '0.06em' , color : 'var(--muted)' , fontWeight : 500 } } >
242- Database
243- </ div >
244- { availableDbs . map ( db => {
245- const isCurrent = db . name === databaseName ;
246- return (
247- < button
248- key = { db . name }
249- onClick = { ( ) => { if ( ! isCurrent ) { setIsSwitching ( true ) ; setShowDbPicker ( false ) ; onSwitchDatabase ?.( db ) ; } } }
250- style = { {
251- width : '100%' , display : 'flex' , alignItems : 'center' , gap : 8 ,
252- padding : '7px 10px' , border : 'none' , cursor : 'pointer' , textAlign : 'left' ,
253- background : isCurrent ? 'var(--accent-soft)' : 'transparent' ,
254- color : isCurrent ? 'var(--accent)' : 'var(--fg)' ,
255- fontSize : 12.5 , fontFamily : 'var(--font-mono)' ,
256- } }
257- onMouseEnter = { ( e ) => { if ( ! isCurrent ) ( e . currentTarget as HTMLElement ) . style . background = 'var(--soft)' ; } }
258- onMouseLeave = { ( e ) => { if ( ! isCurrent ) ( e . currentTarget as HTMLElement ) . style . background = 'transparent' ; } }
259- >
260- < svg width = "11" height = "11" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "1.3" style = { { color : isCurrent ? 'var(--accent)' : 'var(--muted)' , flexShrink : 0 } } >
261- < ellipse cx = "8" cy = "4" rx = "6" ry = "2" /> < path d = "M2 4v8c0 1.1 2.7 2 6 2s6-.9 6-2V4M2 8c0 1.1 2.7 2 6 2s6-.9 6-2" />
262- </ svg >
263- { db . name }
264- { isCurrent && (
265- < svg width = "11" height = "11" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "2" style = { { marginLeft : 'auto' , color : 'var(--accent)' , flexShrink : 0 } } >
266- < path d = "M3 8l4 4 6-6" />
267- </ svg >
268- ) }
269- </ button >
270- ) ;
271- } ) }
272- </ >
273- ) }
274- </ div >
175+ { showDbPicker && (
176+ < div style = { {
177+ position : 'absolute' , top : 'calc(100% + 4px)' , left : 0 , right : 0 , zIndex : 100 ,
178+ background : 'var(--panel)' , border : '1px solid var(--border)' , borderRadius : 8 ,
179+ boxShadow : '0 4px 16px rgba(0,0,0,0.10)' , overflow : 'hidden' ,
180+ } } >
181+ { availableAccounts && availableAccounts . length > 1 && (
182+ < >
183+ < div style = { { padding : '6px 10px 4px' , fontSize : 10.5 , textTransform : 'uppercase' , letterSpacing : '0.06em' , color : 'var(--muted)' , fontWeight : 500 } } >
184+ Cosmos account
185+ </ div >
186+ { availableAccounts . map ( acc => {
187+ const isCurrent = acc . id === accountId ;
188+ return (
189+ < button
190+ key = { acc . id }
191+ onClick = { ( ) => { if ( ! isCurrent ) { setShowDbPicker ( false ) ; onSwitchAccount ?.( acc ) ; } } }
192+ style = { {
193+ width : '100%' , display : 'flex' , alignItems : 'center' , gap : 8 ,
194+ padding : '7px 10px' , border : 'none' , textAlign : 'left' ,
195+ cursor : isCurrent ? 'default' : 'pointer' ,
196+ background : isCurrent ? 'var(--accent-soft)' : 'transparent' ,
197+ color : isCurrent ? 'var(--accent)' : 'var(--fg)' ,
198+ fontSize : 12.5 , fontFamily : 'var(--font-body)' ,
199+ } }
200+ onMouseEnter = { ( e ) => { if ( ! isCurrent ) ( e . currentTarget as HTMLElement ) . style . background = 'var(--soft)' ; } }
201+ onMouseLeave = { ( e ) => { if ( ! isCurrent ) ( e . currentTarget as HTMLElement ) . style . background = 'transparent' ; } }
202+ >
203+ < span style = { { width : 10 , height : 10 , borderRadius : 3 , background : isCurrent ? '#1d6cf2' : 'var(--muted)' , flexShrink : 0 } } />
204+ < span style = { { flex : 1 , overflow : 'hidden' , textOverflow : 'ellipsis' , whiteSpace : 'nowrap' } } > { acc . name } </ span >
205+ { isCurrent && (
206+ < svg width = "11" height = "11" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "2" style = { { color : 'var(--accent)' , flexShrink : 0 } } >
207+ < path d = "M3 8l4 4 6-6" />
208+ </ svg >
209+ ) }
210+ </ button >
211+ ) ;
212+ } ) }
213+ </ >
275214 ) }
276- </ >
277- ) ;
278- } ) ( ) }
279- </ div >
280- ) }
215+ { availableAccounts && availableAccounts . length > 1 && availableDbs && availableDbs . length > 1 && (
216+ < div style = { { height : 1 , background : 'var(--border)' , margin : '4px 0' } } />
217+ ) }
218+ { availableDbs && availableDbs . length > 1 && (
219+ < >
220+ < div style = { { padding : '6px 10px 4px' , fontSize : 10.5 , textTransform : 'uppercase' , letterSpacing : '0.06em' , color : 'var(--muted)' , fontWeight : 500 } } >
221+ Database
222+ </ div >
223+ { availableDbs . map ( db => {
224+ const isCurrent = db . name === databaseName ;
225+ return (
226+ < button
227+ key = { db . name }
228+ onClick = { ( ) => { if ( ! isCurrent ) { setShowDbPicker ( false ) ; onSwitchDatabase ?.( db ) ; } } }
229+ style = { {
230+ width : '100%' , display : 'flex' , alignItems : 'center' , gap : 8 ,
231+ padding : '7px 10px' , border : 'none' , cursor : 'pointer' , textAlign : 'left' ,
232+ background : isCurrent ? 'var(--accent-soft)' : 'transparent' ,
233+ color : isCurrent ? 'var(--accent)' : 'var(--fg)' ,
234+ fontSize : 12.5 , fontFamily : 'var(--font-mono)' ,
235+ } }
236+ onMouseEnter = { ( e ) => { if ( ! isCurrent ) ( e . currentTarget as HTMLElement ) . style . background = 'var(--soft)' ; } }
237+ onMouseLeave = { ( e ) => { if ( ! isCurrent ) ( e . currentTarget as HTMLElement ) . style . background = 'transparent' ; } }
238+ >
239+ < svg width = "11" height = "11" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "1.3" style = { { color : isCurrent ? 'var(--accent)' : 'var(--muted)' , flexShrink : 0 } } >
240+ < ellipse cx = "8" cy = "4" rx = "6" ry = "2" /> < path d = "M2 4v8c0 1.1 2.7 2 6 2s6-.9 6-2V4M2 8c0 1.1 2.7 2 6 2s6-.9 6-2" />
241+ </ svg >
242+ { db . name }
243+ { isCurrent && (
244+ < svg width = "11" height = "11" viewBox = "0 0 16 16" fill = "none" stroke = "currentColor" strokeWidth = "2" style = { { marginLeft : 'auto' , color : 'var(--accent)' , flexShrink : 0 } } >
245+ < path d = "M3 8l4 4 6-6" />
246+ </ svg >
247+ ) }
248+ </ button >
249+ ) ;
250+ } ) }
251+ </ >
252+ ) }
253+ </ div >
254+ ) }
255+ </ >
256+ ) }
257+ </ div >
281258 </ div >
282259
283260 { /* Navigation */ }
@@ -347,23 +324,7 @@ const AppSidebar: React.FC<AppSidebarProps> = ({
347324 } ) }
348325 </ div >
349326
350- { /* Collections — skeleton while switching, real list when loaded */ }
351- { isSwitching && (
352- < div style = { { flex : 1 , overflow : 'hidden' , display : 'flex' , flexDirection : 'column' , marginTop : 12 } } >
353- < div style = { { padding : '0 8px 6px 8px' , fontSize : 10.5 , fontWeight : 500 , textTransform : 'uppercase' , letterSpacing : '0.08em' , color : 'var(--muted)' } } >
354- Collections
355- </ div >
356- < div style = { { padding : '0 8px' , display : 'flex' , flexDirection : 'column' , gap : 4 } } >
357- { [ 60 , 80 , 50 ] . map ( ( w , i ) => (
358- < div key = { i } style = { { display : 'flex' , alignItems : 'center' , gap : 7 , padding : '5px 8px' } } >
359- < div style = { { width : 11 , height : 11 , borderRadius : 3 , background : 'var(--border)' , flexShrink : 0 , animation : 'qp-pulse 1.4s ease-in-out infinite' } } />
360- < div style = { { height : 10 , borderRadius : 4 , background : 'var(--border)' , width : `${ w } %` , animation : 'qp-pulse 1.4s ease-in-out infinite' , animationDelay : `${ i * 0.15 } s` } } />
361- </ div >
362- ) ) }
363- </ div >
364- </ div >
365- ) }
366- { ! isSwitching && collections && collections . length > 0 && (
327+ { collections && collections . length > 0 && (
367328 < div style = { { flex : 1 , overflow : 'hidden' , display : 'flex' , flexDirection : 'column' , marginTop : 12 } } >
368329 < div style = { {
369330 display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' ,
0 commit comments