From 87dd8cb09df1c20cbdf5129666ee0e01007d2ed1 Mon Sep 17 00:00:00 2001 From: Hibryda Date: Sun, 8 Mar 2026 00:10:16 +0100 Subject: [PATCH] feat(v3): redesign UI from top tab bar to VSCode-style left sidebar Replace horizontal tab bar + right-side settings drawer with vertical icon rail (36px) + expandable drawer panel (28em) + always-visible workspace. GlobalTabBar now renders 4 SVG icon buttons. Settings is a regular sidebar tab. Keyboard: Alt+1..4, Ctrl+B toggle, Ctrl+, settings. --- v2/src/App.svelte | 155 ++++++++++-------- .../components/Workspace/GlobalTabBar.svelte | 124 ++++++-------- .../components/Workspace/SettingsTab.svelte | 2 +- v2/src/lib/stores/workspace.svelte.ts | 2 +- 4 files changed, 145 insertions(+), 138 deletions(-) diff --git a/v2/src/App.svelte b/v2/src/App.svelte index 1485fff..9991c07 100644 --- a/v2/src/App.svelte +++ b/v2/src/App.svelte @@ -25,13 +25,21 @@ let detachedConfig = getDetachedConfig(); let paletteOpen = $state(false); - let settingsOpen = $state(false); + let drawerOpen = $state(false); let loaded = $state(false); let activeTab = $derived(getActiveTab()); - function toggleSettings() { - settingsOpen = !settingsOpen; + // Panel titles + const panelTitles: Record = { + sessions: 'Sessions', + docs: 'Documentation', + context: 'Context', + settings: 'Settings', + }; + + function toggleDrawer() { + drawerOpen = !drawerOpen; } onMount(() => { @@ -50,11 +58,20 @@ return; } - // Alt+1..3 — switch workspace tab - if (e.altKey && !e.ctrlKey && e.key >= '1' && e.key <= '3') { + // Alt+1..4 — switch sidebar tab (and open drawer) + if (e.altKey && !e.ctrlKey && e.key >= '1' && e.key <= '4') { e.preventDefault(); - const tabs = ['sessions', 'docs', 'context'] as const; - setActiveTab(tabs[parseInt(e.key) - 1]); + const tabs = ['sessions', 'docs', 'context', 'settings'] as const; + const idx = parseInt(e.key) - 1; + if (idx < tabs.length) { + const tab = tabs[idx]; + if (getActiveTab() === tab && drawerOpen) { + drawerOpen = false; + } else { + setActiveTab(tab); + drawerOpen = true; + } + } return; } @@ -69,17 +86,29 @@ return; } - // Ctrl+, — toggle settings drawer + // Ctrl+, — toggle settings panel if (e.ctrlKey && e.key === ',') { e.preventDefault(); - toggleSettings(); + if (getActiveTab() === 'settings' && drawerOpen) { + drawerOpen = false; + } else { + setActiveTab('settings'); + drawerOpen = true; + } return; } - // Escape — close settings drawer - if (e.key === 'Escape' && settingsOpen) { + // Ctrl+B — toggle sidebar + if (e.ctrlKey && !e.shiftKey && e.key === 'b') { e.preventDefault(); - settingsOpen = false; + drawerOpen = !drawerOpen; + return; + } + + // Escape — close drawer + if (e.key === 'Escape' && drawerOpen) { + e.preventDefault(); + drawerOpen = false; return; } } @@ -111,35 +140,36 @@ {:else if loaded}
- +
+ -
-
- {#if activeTab === 'sessions'} - - {:else if activeTab === 'docs'} - - {:else if activeTab === 'context'} - - {/if} -
- - {#if settingsOpen} - - -
settingsOpen = false}>
- {/if} + +
+ +
@@ -166,62 +196,45 @@ overflow: hidden; } - .content-area { + .main-row { flex: 1; - position: relative; + display: flex; overflow: hidden; } - .tab-content { - height: 100%; - overflow: hidden; - } - - .drawer-backdrop { - position: absolute; - inset: 0; - background: rgba(0, 0, 0, 0.3); - z-index: 50; - } - - .settings-drawer { - position: absolute; - top: 0; - right: 0; - bottom: 0; - width: 32em; - max-width: 90%; - background: var(--ctp-base); - border-left: 1px solid var(--ctp-surface1); - box-shadow: -4px 0 24px rgba(0, 0, 0, 0.3); - z-index: 51; + .sidebar-panel { + width: 28em; + max-width: 50%; display: flex; flex-direction: column; + background: var(--ctp-base); + border-right: 1px solid var(--ctp-surface1); overflow: hidden; + flex-shrink: 0; } - .drawer-header { + .panel-header { display: flex; align-items: center; justify-content: space-between; - padding: 10px 16px; + padding: 8px 12px; border-bottom: 1px solid var(--ctp-surface0); flex-shrink: 0; } - .drawer-header h2 { - font-size: 0.9rem; + .panel-header h2 { + font-size: 0.8rem; font-weight: 600; color: var(--ctp-text); margin: 0; } - .drawer-close { + .panel-close { display: flex; align-items: center; justify-content: center; - width: 24px; - height: 24px; + width: 22px; + height: 22px; background: transparent; border: none; border-radius: 4px; @@ -229,11 +242,21 @@ cursor: pointer; } - .drawer-close:hover { + .panel-close:hover { color: var(--ctp-text); background: var(--ctp-surface0); } + .panel-content { + flex: 1; + overflow: hidden; + } + + .workspace { + flex: 1; + overflow: hidden; + } + .loading { display: flex; align-items: center; diff --git a/v2/src/lib/components/Workspace/GlobalTabBar.svelte b/v2/src/lib/components/Workspace/GlobalTabBar.svelte index 7239cd2..9920668 100644 --- a/v2/src/lib/components/Workspace/GlobalTabBar.svelte +++ b/v2/src/lib/components/Workspace/GlobalTabBar.svelte @@ -2,89 +2,74 @@ import { getActiveTab, setActiveTab, type WorkspaceTab } from '../../stores/workspace.svelte'; interface Props { - settingsOpen?: boolean; - ontoggleSettings?: () => void; + expanded?: boolean; + ontoggle?: () => void; } - let { settingsOpen = false, ontoggleSettings }: Props = $props(); + let { expanded = false, ontoggle }: Props = $props(); const tabs: { id: WorkspaceTab; label: string; shortcut: string }[] = [ { id: 'sessions', label: 'Sessions', shortcut: 'Alt+1' }, { id: 'docs', label: 'Docs', shortcut: 'Alt+2' }, { id: 'context', label: 'Context', shortcut: 'Alt+3' }, + { id: 'settings', label: 'Settings', shortcut: 'Ctrl+,' }, ]; + + // SVG icon paths for each tab + const icons: Record = { + sessions: 'M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z', + docs: 'M6 2h9l5 5v15H4V2h2zm8 0v5h5M8 12h8M8 16h5', + context: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 5v5l3 3', + settings: 'M10.3 2L9.9 4.4a7 7 0 0 0-1.8 1l-2.2-.9-1.7 3 1.8 1.5a7 7 0 0 0 0 2l-1.8 1.5 1.7 3 2.2-.9a7 7 0 0 0 1.8 1L10.3 18h3.4l.4-2.4a7 7 0 0 0 1.8-1l2.2.9 1.7-3-1.8-1.5a7 7 0 0 0 0-2l1.8-1.5-1.7-3-2.2.9a7 7 0 0 0-1.8-1L13.7 2h-3.4zM12 8a4 4 0 1 1 0 8 4 4 0 0 1 0-8z', + }; + + function handleTabClick(id: WorkspaceTab) { + const current = getActiveTab(); + if (current === id && expanded) { + // Clicking active tab again collapses + ontoggle?.(); + } else { + setActiveTab(id); + if (!expanded) ontoggle?.(); + } + } -