|
11 | 11 | import NavLink from './NavLink.svelte'; |
12 | 12 | import UserAvatar from '../components/UserAvatar.svelte'; |
13 | 13 | import NotificationTray from '../features/notifications/NotificationTray.svelte'; |
| 14 | + import ScrollableSidebar from './ScrollableSidebar.svelte'; |
14 | 15 | import { |
15 | 16 | IconSearch, IconSettings, IconPlus, IconGridDots, IconUserScan, |
16 | 17 | IconFolders, IconLayoutSidebarLeftExpand, IconLayoutSidebarLeftCollapse, |
|
22 | 23 | onShowCommandPalette = () => {}, |
23 | 24 | onShowCreateModal = () => {}, |
24 | 25 | onShowChatPanel = () => {}, |
25 | | - onToggleTerminal = () => {} |
| 26 | + onToggleTerminal = () => {}, |
| 27 | + activeSurface = null, |
| 28 | + onSurfaceChange = () => {}, |
26 | 29 | } = $props(); |
27 | 30 |
|
28 | 31 | const isTauri = getIsTauri(); |
|
126 | 129 | function showCreateDropdown() { |
127 | 130 | onShowCreateModal(); |
128 | 131 | } |
| 132 | +
|
| 133 | + function setPopoverSurface(surface, open) { |
| 134 | + if (open) { |
| 135 | + onSurfaceChange(surface); |
| 136 | + } else if (activeSurface === surface) { |
| 137 | + onSurfaceChange(null); |
| 138 | + } |
| 139 | + } |
| 140 | +
|
| 141 | + function closePopoverSurface() { |
| 142 | + if (activeSurface === 'workspaces' || activeSurface === 'notifications' || activeSurface === 'profile') { |
| 143 | + onSurfaceChange(null); |
| 144 | + } |
| 145 | + } |
129 | 146 | </script> |
130 | 147 |
|
131 | | -<nav class="main-sidebar {$uiStore.navExpanded ? 'w-[200px]' : 'w-16'} shadow-lg border-r flex flex-col py-4 fixed h-full z-40 themed-nav transition-all duration-200 overflow-x-hidden" style="border-color: var(--ds-border);" aria-label={t('aria.mainNavigation')}> |
| 148 | +{#snippet sidebarHeader()} |
132 | 149 | <!-- Logo --> |
133 | 150 | <Tooltip content="Windshift" placement="right" disabled={$uiStore.navExpanded}> |
134 | 151 | <a |
135 | 152 | href="/" |
| 153 | + onclick={closePopoverSurface} |
136 | 154 | class="flex items-center {$uiStore.navExpanded ? 'px-4' : 'justify-center'} w-full h-10 mb-2 hover:opacity-80 transition-opacity cursor-pointer" |
137 | 155 | > |
138 | | - <img src="windshift-3.svg" alt="Windshift" class="w-8 h-8 flex-shrink-0" /> |
| 156 | + <img src="/windshift-3.svg" alt="Windshift" class="w-8 h-8 flex-shrink-0" /> |
139 | 157 | {#if $uiStore.navExpanded} |
140 | 158 | <span class="ml-3 font-semibold text-sm whitespace-nowrap">Windshift</span> |
141 | 159 | {/if} |
142 | 160 | </a> |
143 | 161 | </Tooltip> |
| 162 | +{/snippet} |
144 | 163 |
|
| 164 | +{#snippet sidebarContent()} |
145 | 165 | <!-- Main Navigation --> |
146 | | - <div class="flex mt-6 flex-col {$uiStore.navExpanded ? 'items-stretch px-2.5' : 'items-center'} space-y-1 flex-1"> |
| 166 | + <div class="flex flex-col {$uiStore.navExpanded ? 'items-stretch px-2.5' : 'items-center'} space-y-1 py-4"> |
147 | 167 |
|
148 | 168 | <!-- Workspaces --> |
149 | 169 | <Tooltip content={t('nav.workspaces')} placement="right" disabled={$uiStore.navExpanded}> |
|
154 | 174 | triggerGap="gap-3" |
155 | 175 | triggerText={$uiStore.navExpanded ? t('nav.workspaces') : ''} |
156 | 176 | triggerLabel={t('nav.workspaces')} |
157 | | - triggerClass="{$uiStore.navExpanded ? 'w-full px-3' : 'w-10'} h-10 rounded flex items-center {$uiStore.navExpanded ? '' : 'justify-center'} cursor-pointer nav-button nav-button-emphasized {isWorkspaceRoute($currentRoute.view) ? 'nav-button-selected' : ''} {!$workspacesStore.loaded ? 'opacity-50 cursor-wait' : ''}" |
| 177 | + triggerClass="{$uiStore.navExpanded ? 'w-full px-3' : 'w-10'} h-10 rounded flex items-center {$uiStore.navExpanded ? '' : 'justify-center'} cursor-pointer nav-button nav-button-emphasized {isWorkspaceRoute($currentRoute.view) || activeSurface === 'workspaces' ? 'nav-button-selected' : ''} {!$workspacesStore.loaded ? 'opacity-50 cursor-wait' : ''}" |
158 | 178 | triggerTestid="workspaces-dropdown-trigger" |
159 | 179 | items={workspacesDropdownItems} |
160 | 180 | maxWidth="max-w-xs" |
161 | 181 | showChevron={false} |
162 | 182 | placement="right-start" |
163 | 183 | iconOnly={!$uiStore.navExpanded} |
164 | 184 | triggerAlignment={$uiStore.navExpanded ? 'start' : 'center'} |
| 185 | + isOpen={activeSurface === 'workspaces'} |
| 186 | + onOpenChange={(open) => setPopoverSurface('workspaces', open)} |
165 | 187 | /> |
166 | 188 | </div> |
167 | 189 | </Tooltip> |
|
175 | 197 | href={item.href} |
176 | 198 | isActive={item.activeViews.includes($currentRoute.view)} |
177 | 199 | expanded={$uiStore.navExpanded} |
| 200 | + onclick={closePopoverSurface} |
178 | 201 | /> |
179 | 202 | {/each} |
180 | 203 |
|
181 | | - <!-- Top Actions Section - "Notch" style centered positioning --> |
182 | | - <div class="flex flex-col items-stretch space-y-2 my-6 py-4"> |
| 204 | + <!-- Global actions share the same rhythm as navigation, but remain a |
| 205 | + distinct task group instead of a second navigation section. --> |
| 206 | + <div class="sidebar-quick-actions flex flex-col items-stretch space-y-1 my-3 py-3 border-y"> |
183 | 207 | <NavLink |
184 | 208 | id="global-create-button" |
185 | 209 | icon={IconPlus} |
186 | 210 | label={t('nav.create')} |
187 | 211 | onclick={showCreateDropdown} |
188 | 212 | expanded={$uiStore.navExpanded} |
189 | | - variant="primary" |
190 | | - tooltipSuffix=" (C)" |
| 213 | + variant="accent" |
| 214 | + isActive={activeSurface === 'create'} |
| 215 | + shortcut={getShortcutDisplay('global', 'create')} |
| 216 | + tooltipSuffix=" ({getShortcutDisplay('global', 'create')})" |
191 | 217 | /> |
192 | 218 | <NavLink |
193 | 219 | icon={IconSearch} |
194 | 220 | label={t('nav.search')} |
195 | 221 | onclick={onShowCommandPalette} |
196 | 222 | expanded={$uiStore.navExpanded} |
| 223 | + isActive={activeSurface === 'search'} |
| 224 | + shortcut={getShortcutDisplay('global', 'commandPalette')} |
197 | 225 | tooltipSuffix=" ({getShortcutDisplay('global', 'commandPalette')} or Space Space)" |
198 | 226 | /> |
199 | 227 | {#if aiStore.chatAvailable} |
|
203 | 231 | label={t('nav.aiChat')} |
204 | 232 | onclick={onShowChatPanel} |
205 | 233 | expanded={$uiStore.navExpanded} |
| 234 | + isActive={activeSurface === 'chat'} |
| 235 | + shortcut={getShortcutDisplay('global', 'aiChat')} |
206 | 236 | tooltipSuffix=" ({getShortcutDisplay('global', 'aiChat')})" |
207 | 237 | /> |
208 | 238 | {/if} |
|
217 | 247 | {/if} |
218 | 248 | </div> |
219 | 249 | </div> |
| 250 | +{/snippet} |
220 | 251 |
|
| 252 | +{#snippet sidebarFooter()} |
221 | 253 | <!-- Bottom Section --> |
222 | | - <div class="flex flex-col {$uiStore.navExpanded ? 'items-stretch px-3' : 'items-center'} space-y-1 mt-auto"> |
| 254 | + <div class="flex flex-col {$uiStore.navExpanded ? 'items-stretch px-3' : 'items-center'} space-y-1 pt-2"> |
223 | 255 | <!-- Nav Toggle Button --> |
224 | 256 | <button |
225 | 257 | onclick={() => uiStore.toggleNavExpanded()} |
|
242 | 274 | href={item.href} |
243 | 275 | isActive={item.activeViews.includes($currentRoute.view)} |
244 | 276 | expanded={$uiStore.navExpanded} |
| 277 | + onclick={closePopoverSurface} |
245 | 278 | /> |
246 | 279 | {/each} |
247 | 280 |
|
248 | 281 | <!-- Notification Tray --> |
249 | 282 | <Tooltip content={t('nav.notifications')} placement="right" disabled={$uiStore.navExpanded}> |
250 | | - <NotificationTray expanded={$uiStore.navExpanded} label={t('nav.notifications')} /> |
| 283 | + <NotificationTray |
| 284 | + expanded={$uiStore.navExpanded} |
| 285 | + label={t('nav.notifications')} |
| 286 | + isOpen={activeSurface === 'notifications'} |
| 287 | + onOpenChange={(open) => setPopoverSurface('notifications', open)} |
| 288 | + /> |
251 | 289 | </Tooltip> |
252 | 290 |
|
253 | 291 | <!-- User Profile Avatar --> |
254 | 292 | <Tooltip content={t('nav.profile')} placement="right" disabled={$uiStore.navExpanded}> |
255 | | - <UserAvatar expanded={$uiStore.navExpanded} label={t('nav.profile')} /> |
| 293 | + <UserAvatar |
| 294 | + expanded={$uiStore.navExpanded} |
| 295 | + label={t('nav.profile')} |
| 296 | + isOpen={activeSurface === 'profile'} |
| 297 | + onOpenChange={(open) => setPopoverSurface('profile', open)} |
| 298 | + /> |
256 | 299 | </Tooltip> |
257 | 300 | </div> |
258 | | -</nav> |
| 301 | +{/snippet} |
| 302 | +
|
| 303 | +<ScrollableSidebar |
| 304 | + as="nav" |
| 305 | + class="main-sidebar {$uiStore.navExpanded ? 'w-[200px]' : 'w-16'} shadow-lg border-r py-4 fixed inset-y-0 z-40 themed-nav transition-all duration-200" |
| 306 | + style="border-color: var(--ds-border);" |
| 307 | + aria-label={t('aria.mainNavigation')} |
| 308 | + header={sidebarHeader} |
| 309 | + footer={sidebarFooter} |
| 310 | + scrollTestid="main-navigation-scroll" |
| 311 | +> |
| 312 | + {@render sidebarContent()} |
| 313 | +</ScrollableSidebar> |
259 | 314 |
|
260 | 315 | <style> |
| 316 | + :global(.main-sidebar) { |
| 317 | + height: 100vh; |
| 318 | + height: 100dvh; |
| 319 | + } |
| 320 | +
|
| 321 | + .sidebar-quick-actions { |
| 322 | + border-color: color-mix(in srgb, var(--ds-border) 75%, transparent); |
| 323 | + } |
| 324 | +
|
261 | 325 | @media (max-width: 767px) { |
262 | | - .main-sidebar { |
| 326 | + :global(.main-sidebar) { |
263 | 327 | width: 4rem; |
264 | 328 | } |
265 | 329 | } |
|
0 commit comments