diff --git a/frontend/e2e/command-palette.spec.js b/frontend/e2e/command-palette.spec.js index 0de6428..c91fe7d 100644 --- a/frontend/e2e/command-palette.spec.js +++ b/frontend/e2e/command-palette.spec.js @@ -43,6 +43,6 @@ test.describe('Command Palette', () => { await page.keyboard.press('Escape'); await expect(page.locator('.command-palette')).not.toBeVisible(); - await expect(page.locator('.plugin-manager')).toBeVisible(); + await expect(page.locator('.workspace-host')).toBeVisible(); }); }); diff --git a/frontend/e2e/default-editor.spec.js b/frontend/e2e/default-editor.spec.js index f9ae676..497eee5 100644 --- a/frontend/e2e/default-editor.spec.js +++ b/frontend/e2e/default-editor.spec.js @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test'; -import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js'; +import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js'; test.describe('F: Default Editor Plugin', () => { let consoleCollector; @@ -134,14 +134,14 @@ test.describe('F: Default Editor Plugin', () => { }); test('default-editor plugin is listed as loaded in plugin manager', async ({ page }) => { - await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click(); + await openPluginManager(page); const card = page.locator('.plugin-card').filter({ hasText: 'verstak.default-editor' }); await expect(card).toBeVisible({ timeout: 10000 }); await expect(card.locator('.status-badge')).toHaveText('loaded'); }); test('disable default-editor plugin removes its providers', async ({ page }) => { - await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click(); + await openPluginManager(page); const card = page.locator('.plugin-card').filter({ hasText: 'verstak.default-editor' }); await card.locator('button.btn-disable').click(); await expect(card.locator('button.btn-enable')).toBeVisible({ timeout: 10000 }); @@ -161,7 +161,7 @@ test.describe('F: Default Editor Plugin', () => { }); test('default-editor plugin card shows openProviders contribution count', async ({ page }) => { - await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click(); + await openPluginManager(page); const card = page.locator('.plugin-card').filter({ hasText: 'verstak.default-editor' }); await expect(card).toBeVisible({ timeout: 10000 }); await expect(card.locator('.meta-row').filter({ hasText: 'Contributions:' })).toContainText('3 openProviders'); diff --git a/frontend/e2e/files-plugin.spec.js b/frontend/e2e/files-plugin.spec.js index 9311862..1c75e5d 100644 --- a/frontend/e2e/files-plugin.spec.js +++ b/frontend/e2e/files-plugin.spec.js @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test'; -import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js'; +import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js'; test.describe('G: Files Plugin', () => { let consoleCollector; @@ -16,7 +16,7 @@ test.describe('G: Files Plugin', () => { }); test('files plugin appears in Plugin Manager as loaded', async ({ page }) => { - await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click(); + await openPluginManager(page); const card = page.locator('.plugin-card').filter({ hasText: 'verstak.files' }); await expect(card).toBeVisible({ timeout: 10000 }); await expect(card.locator('.status-badge')).toHaveText('loaded'); @@ -88,7 +88,7 @@ test.describe('G: Files Plugin', () => { await expect(page.locator('.files-breadcrumb')).not.toContainText('Daily'); }); - test('files explorer uses icon controls and no row New Here action', async ({ page }) => { + test('files explorer uses labeled controls and no row New Here action', async ({ page }) => { await page.locator('.wt-label').filter({ hasText: 'Project' }).click(); await expect(page.locator('.files-breadcrumb')).toContainText('Project', { timeout: 10000 }); @@ -96,13 +96,13 @@ test.describe('G: Files Plugin', () => { const button = page.locator(`[data-files-action="${action}"]`); await expect(button).toHaveAttribute('title', /.+/); await expect(button.locator('svg')).toBeVisible(); - await expect(button).not.toHaveText(/\S/); + await expect(button).toHaveText(/\S/); } await expect(page.locator('.files-row-btn').filter({ hasText: 'New here' })).toHaveCount(0); const firstRowButton = page.locator('[data-file-name="Notes"] .files-row-btn').first(); await expect(firstRowButton).toBeVisible(); - await expect(firstRowButton).not.toHaveText(/\S/); + await expect(firstRowButton).toHaveText(/\S/); expect(await firstRowButton.evaluate((node) => node.innerHTML)).toContain(' { let consoleCollector; @@ -26,7 +26,7 @@ test.describe('D: Plugin API bridge', () => { await page.locator('.pt-save-setting').click(); await expect(saved).toHaveText('Saved setting: persisted through bridge', { timeout: 10000 }); - await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click(); + await openPluginManager(page); await expect.poll(() => page.evaluate(() => Object.keys(window.__VERSTAK_COMMAND_HANDLERS__ || {}).length)).toBe(0); await expect.poll(() => page.evaluate(() => (window.__VERSTAK_EVENT_HANDLERS__?.['verstak.platform-test.echo'] || []).length)).toBe(0); await page.locator('button.reload-btn').click(); @@ -155,7 +155,7 @@ test.describe('D: Plugin API bridge', () => { await expect.poll(() => page.evaluate(() => Object.keys(window.__VERSTAK_COMMAND_HANDLERS__ || {}).length)).toBe(1); await expect.poll(() => page.evaluate(() => (window.__VERSTAK_EVENT_HANDLERS__?.['verstak.platform-test.echo'] || []).length)).toBe(1); - await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click(); + await openPluginManager(page); await expect.poll(() => page.evaluate(() => Object.keys(window.__VERSTAK_COMMAND_HANDLERS__ || {}).length)).toBe(0); await expect.poll(() => page.evaluate(() => (window.__VERSTAK_EVENT_HANDLERS__?.['verstak.platform-test.echo'] || []).length)).toBe(0); @@ -165,7 +165,7 @@ test.describe('D: Plugin API bridge', () => { await page.locator('.sidebar .plugin-item').filter({ hasText: 'Platform Test' }).click(); await expect(page.locator('.pt-command-result')).toContainText('Command: handled', { timeout: 10000 }); - await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click(); + await openPluginManager(page); const pluginCard = page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' }); await pluginCard.locator('button.btn-disable').click(); await expect(pluginCard.locator('button.btn-enable')).toBeVisible({ timeout: 10000 }); @@ -175,7 +175,7 @@ test.describe('D: Plugin API bridge', () => { }); test('platform-test settings panel loads bundle content returned as raw string', async ({ page }) => { - await page.locator('.sidebar .nav-item').filter({ hasText: 'Plugin Manager' }).click(); + await openPluginManager(page); const pluginCard = page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' }); await pluginCard.locator('button.btn-settings').click(); diff --git a/frontend/e2e/plugin-manager-disable-enable.spec.js b/frontend/e2e/plugin-manager-disable-enable.spec.js index ffa77b2..58e46cb 100644 --- a/frontend/e2e/plugin-manager-disable-enable.spec.js +++ b/frontend/e2e/plugin-manager-disable-enable.spec.js @@ -12,7 +12,7 @@ * 8. Verify plugin sidebar item returns */ import { test, expect } from '@playwright/test'; -import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js'; +import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js'; test.describe('A: Plugin Manager Disable/Enable refresh', () => { let consoleCollector; @@ -22,6 +22,7 @@ test.describe('A: Plugin Manager Disable/Enable refresh', () => { await resetMockState(page); await page.goto('/'); await waitForAppReady(page); + await openPluginManager(page); }); test.afterEach(async () => { diff --git a/frontend/e2e/plugin-manager-layout.spec.js b/frontend/e2e/plugin-manager-layout.spec.js index b0e8a08..f9132da 100644 --- a/frontend/e2e/plugin-manager-layout.spec.js +++ b/frontend/e2e/plugin-manager-layout.spec.js @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test'; -import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js'; +import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js'; test.describe('E: Plugin Manager layout', () => { let consoleCollector; @@ -16,6 +16,7 @@ test.describe('E: Plugin Manager layout', () => { }); test('plugin list scrolls through the global main scroll surface and stays responsive', async ({ page }) => { + await openPluginManager(page); const basePluginCount = await page.locator('.plugin-card').count(); await page.evaluate(() => window.__wailsMock.addSyntheticPlugins(18)); await page.locator('button.reload-btn').click(); diff --git a/frontend/e2e/reload-updates-state.spec.js b/frontend/e2e/reload-updates-state.spec.js index eef9f7f..cd6ddd6 100644 --- a/frontend/e2e/reload-updates-state.spec.js +++ b/frontend/e2e/reload-updates-state.spec.js @@ -7,7 +7,7 @@ * 3. Verify UI reflects the updated state */ import { test, expect } from '@playwright/test'; -import { waitForAppReady, setupConsoleCollector, resetMockState, setPluginStatus } from './helpers.js'; +import { waitForAppReady, setupConsoleCollector, resetMockState, setPluginStatus, openPluginManager } from './helpers.js'; test.describe('C: Reload updates UI state', () => { let consoleCollector; @@ -17,6 +17,7 @@ test.describe('C: Reload updates UI state', () => { await resetMockState(page); await page.goto('/'); await waitForAppReady(page); + await openPluginManager(page); }); test.afterEach(async () => { diff --git a/frontend/e2e/status-bar.spec.js b/frontend/e2e/status-bar.spec.js index 68ccf07..0416106 100644 --- a/frontend/e2e/status-bar.spec.js +++ b/frontend/e2e/status-bar.spec.js @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test'; -import { waitForAppReady, setupConsoleCollector, resetMockState } from './helpers.js'; +import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js'; test.describe('Status Bar host', () => { let consoleCollector; @@ -39,6 +39,7 @@ test.describe('Status Bar host', () => { test('refreshes statusBarItems after disabling plugin', async ({ page }) => { const pluginCard = page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' }); await expect(page.locator('[data-status-item-id="verstak.platform-test.status"]')).toBeVisible(); + await openPluginManager(page); await pluginCard.locator('button.btn-disable').click(); diff --git a/frontend/e2e/ux-followup.spec.js b/frontend/e2e/ux-followup.spec.js new file mode 100644 index 0000000..4fe1e62 --- /dev/null +++ b/frontend/e2e/ux-followup.spec.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test'; +import { waitForAppReady, resetMockState, openPluginManager } from './helpers.js'; + +test.describe('UX follow-up fixes', () => { + test.beforeEach(async ({ page }) => { + await resetMockState(page); + await page.goto('/'); + await waitForAppReady(page); + }); + + test('global search stays available after opening tool sidebar views', async ({ page }) => { + const search = page.locator('[data-global-search-input]'); + await expect(search).toBeVisible(); + + await page.locator('.sidebar .nav-item').filter({ hasText: 'Activity' }).click(); + await expect(page.locator('.activity-root')).toBeVisible({ timeout: 10000 }); + await expect(search).toBeVisible(); + + await page.locator('.sidebar .nav-item').filter({ hasText: 'Browser Inbox' }).click(); + await expect(page.locator('.browser-inbox-root')).toBeVisible({ timeout: 10000 }); + await expect(search).toBeVisible(); + }); + + test('global search types ahead across workspaces and file contents with keyboard layout fallback', async ({ page }) => { + const search = page.locator('[data-global-search-input]'); + + await search.fill('Зкщоусе'); + await expect(page.locator('[data-global-search-results]')).toContainText('Project', { timeout: 10000 }); + + await search.fill('project file'); + await expect(page.locator('[data-global-search-results]')).toContainText('project-only.txt', { timeout: 10000 }); + }); + + test('plugin settings modal gives complex panels enough space', async ({ page }) => { + await openPluginManager(page); + await page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' }).getByRole('button', { name: 'Settings' }).click(); + + const modal = page.locator('.modal[aria-label="Plugin Settings"]'); + await expect(modal).toBeVisible({ timeout: 10000 }); + const box = await modal.boundingBox(); + expect(box.width).toBeGreaterThanOrEqual(760); + expect(box.height).toBeGreaterThanOrEqual(560); + }); +}); diff --git a/frontend/e2e/ux-p0.spec.js b/frontend/e2e/ux-p0.spec.js new file mode 100644 index 0000000..579c68e --- /dev/null +++ b/frontend/e2e/ux-p0.spec.js @@ -0,0 +1,97 @@ +import { test, expect } from '@playwright/test'; +import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js'; + +test.describe('UX P0 shell flow', () => { + let consoleCollector; + + test.beforeEach(async ({ page }) => { + consoleCollector = setupConsoleCollector(page); + await resetMockState(page); + await page.goto('/'); + await waitForAppReady(page); + }); + + test.afterEach(async () => { + consoleCollector.assertNoErrors(); + }); + + test('starts in the first workspace instead of Plugin Manager', async ({ page }) => { + await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 }); + await expect(page.locator('.plugin-manager')).toHaveCount(0); + await expect(page.locator('.wt-node.selected .wt-label')).toHaveText('Project'); + await expect(page.locator('.workspace-title')).toHaveText('Project'); + }); + + test('workspace selection and main content stay in sync across plugin manager round trip', async ({ page }) => { + await page.locator('.wt-label').filter({ hasText: 'Test' }).click(); + + await expect(page.locator('.workspace-title')).toHaveText('Test', { timeout: 10000 }); + await expect(page.locator('.wt-node.selected .wt-label')).toHaveText('Test'); + await expect(page.locator('.plugin-manager')).toHaveCount(0); + + await openPluginManager(page); + await expect(page.locator('.plugin-manager')).toBeVisible(); + await expect(page.locator('.wt-node.selected .wt-label')).toHaveCount(0); + + await page.locator('.wt-label').filter({ hasText: 'Project' }).click(); + await expect(page.locator('.workspace-title')).toHaveText('Project', { timeout: 10000 }); + await expect(page.locator('.plugin-manager')).toHaveCount(0); + }); + + test('status bar plugin contribution failures do not render large error panels', async ({ page }) => { + await expect(page.locator('.workspace-host')).toBeVisible({ timeout: 10000 }); + await expect(page.getByText('Plugin View Error')).toHaveCount(0); + await expect(page.locator('.status-bar [data-status-item-id]')).toHaveCount(2); + }); + + test('Plugin Manager remains reachable from the settings menu', async ({ page }) => { + await openPluginManager(page); + + await expect(page.locator('.plugin-manager')).toBeVisible(); + await expect(page.locator('.plugin-card').filter({ hasText: 'verstak.platform-test' })).toBeVisible(); + }); +}); + +test.describe('UX quick wins', () => { + test('Files screen uses readable dates and understandable action controls', async ({ page }) => { + await resetMockState(page); + await page.goto('/'); + await waitForAppReady(page); + await page.locator('.wt-label').filter({ hasText: 'Project' }).click(); + + const files = page.locator('.files-root'); + await expect(files).toBeVisible({ timeout: 10000 }); + await expect(files.getByText(/T\d{2}:\d{2}:\d{2}/)).toHaveCount(0); + + const actions = ['New folder', 'New markdown file', 'New text file']; + for (const action of actions) { + const button = page.locator(`[data-files-action]`).filter({ hasText: action }).first(); + await expect(button).toBeVisible(); + await expect(button).toHaveAttribute('title', action); + } + }); + + test('Vault Selection is localized and has a clear primary action', async ({ browser }) => { + const page = await browser.newPage(); + await page.addInitScript(() => { + window.go = { api: { App: { + GetAppSettings: async () => ({ currentVaultPath: '', recentVaults: ['/tmp/verstak-recent-vault'] }), + GetVaultStatus: async () => ({ status: 'closed', path: '', vaultId: '' }), + SelectDirectory: async () => '', + SelectVaultForOpen: async () => '', + CreateVault: async () => null, + OpenVault: async () => null, + SetCurrentVault: async () => '', + WriteFrontendLog: async () => {}, + } } }; + }); + + await page.goto('/'); + await page.waitForSelector('.vault-selection', { timeout: 10000 }); + + await expect(page.getByText('Выберите vault для начала работы')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Создать vault' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Открыть существующий' })).toBeVisible(); + await page.close(); + }); +}); diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 764a9c2..3d7ce0a 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -12,7 +12,7 @@ import { onMount } from 'svelte'; import { tick } from 'svelte'; - let currentView = 'plugin-manager'; + let currentView = 'workspace'; let vaultStatus = { status: 'unknown', path: '', vaultId: '' }; let needsVaultSelection = false; let loading = true; @@ -35,6 +35,75 @@ App.WriteFrontendLog('App', msg); } + function resultOrError(response, fallbackValue) { + return typeof response === 'string' ? [fallbackValue, response] : [response, '']; + } + + function workspaceName(workspace) { + return String(workspace?.name || workspace?.rootPath || workspace?.id || ''); + } + + function workspaceAsNode(workspace, order) { + const name = workspaceName(workspace); + return { + id: name, + type: workspace?.type || 'space', + title: workspace?.title || name, + name, + rootPath: workspace?.rootPath || name, + status: workspace?.status || 'active', + order, + }; + } + + function emitWorkspaceActive(name) { + window.dispatchEvent(new CustomEvent('verstak:workspace-active-changed', { + detail: { workspaceName: name || '' } + })); + } + + function clearWorkspaceSelection() { + selectedWorkspaceName = ''; + emitWorkspaceActive(''); + } + + async function openDefaultWorkspaceRoute() { + try { + const [workspaces, err] = resultOrError(await App.ListWorkspaces(), []); + if (err || !workspaces || workspaces.length === 0) { + workspaceNodes = []; + selectedWorkspaceName = ''; + currentView = 'workspace-empty'; + emitWorkspaceActive(''); + return; + } + + workspaceNodes = workspaces.map(workspaceAsNode); + let currentWorkspace = null; + try { + currentWorkspace = await App.GetCurrentWorkspace(); + } catch { + currentWorkspace = null; + } + const currentName = workspaceName(currentWorkspace); + const selected = workspaces.find((workspace) => workspaceName(workspace) === currentName) || workspaces[0]; + selectedWorkspaceName = workspaceName(selected); + if (selectedWorkspaceName) { + try { await App.SetCurrentWorkspace(selectedWorkspaceName); } catch {} + currentView = 'workspace'; + } else { + currentView = 'workspace-empty'; + } + emitWorkspaceActive(selectedWorkspaceName); + } catch (e) { + debug.log('[App] openDefaultWorkspaceRoute ERROR', String(e)); + workspaceNodes = []; + selectedWorkspaceName = ''; + currentView = 'workspace-empty'; + emitWorkspaceActive(''); + } + } + function currentSnapshot() { return { currentView, @@ -70,6 +139,7 @@ activeSettingsPanelId = snapshot.activeSettingsPanelId; openedResource = snapshot.openedResource; selectedWorkspaceName = snapshot.selectedWorkspaceName; + emitWorkspaceActive(currentView === 'workspace' ? selectedWorkspaceName : ''); applyingNavigation = false; } @@ -153,6 +223,7 @@ debug.log('[App] checkVault: vault open, needsVaultSelection=false'); flog('checkVault: needsVaultSelection=false'); needsVaultSelection = false; + await openDefaultWorkspaceRoute(); } } catch (e) { debug.log('[App] checkVault: ERROR', String(e)); @@ -166,15 +237,18 @@ flog('checkVault: END, loading=false'); } - function onVaultOpened() { + async function onVaultOpened() { debug.log('[App] onVaultOpened'); needsVaultSelection = false; vaultStatus = { status: 'open', path: '', vaultId: '' }; + await openDefaultWorkspaceRoute(); + pushNavigation(); } function onNav(e) { debug.log('[App] onNav:', e.detail.viewId); currentView = e.detail.viewId; + if (currentView !== 'workspace') clearWorkspaceSelection(); pushNavigation(); } @@ -183,6 +257,7 @@ activeView = e.detail.viewId; activeViewPluginId = e.detail.pluginId || ''; currentView = 'plugin-view'; + clearWorkspaceSelection(); pushNavigation(); } @@ -191,6 +266,7 @@ activeSettingsPluginId = e.detail.pluginId; activeSettingsPanelId = e.detail.panelId || ''; currentView = 'plugin-manager'; + clearWorkspaceSelection(); pushNavigation(); } @@ -206,7 +282,13 @@ selectedWorkspaceName = e.detail?.workspaceName || ''; workspaceNodes = e.detail?.nodes || workspaceNodes; if (selectedWorkspaceName) { + activeView = null; + activeViewPluginId = ''; + activeSettingsPluginId = ''; + activeSettingsPanelId = ''; + openedResource = null; currentView = 'workspace'; + emitWorkspaceActive(selectedWorkspaceName); pushNavigation(); } } @@ -302,7 +384,7 @@ {:else if currentView === 'workbench'} - {:else if currentView === 'workspace'} + {:else if currentView === 'workspace' || currentView === 'workspace-empty'} {:else} diff --git a/frontend/src/lib/plugin-manager/PluginCard.svelte b/frontend/src/lib/plugin-manager/PluginCard.svelte index 0c03a72..bdc5142 100644 --- a/frontend/src/lib/plugin-manager/PluginCard.svelte +++ b/frontend/src/lib/plugin-manager/PluginCard.svelte @@ -97,35 +97,39 @@ Name: {m.name || '-'} -
- API Version: - {m.apiVersion || '-'} -
Source: {m.source || 'unknown'}
-
- Root: - {p.rootPath || '-'} -
Contributions: {contribSummary}
- -
- Provides -
- {#each m.provides || [] as cap} - {cap} - {/each} +
+ Technical details +
+
+ API Version: + {m.apiVersion || '-'} +
+
+ Root: + {p.rootPath || '-'} +
-
- {#if m.requires && m.requires.length > 0} +
+ Provides +
+ {#each m.provides || [] as cap} + {cap} + {/each} +
+
+ + {#if m.requires && m.requires.length > 0}
Requires
@@ -141,9 +145,9 @@

Missing required capabilities: {missingRequired.join(', ')}

{/if}
- {/if} + {/if} - {#if m.optionalRequires && m.optionalRequires.length > 0} + {#if m.optionalRequires && m.optionalRequires.length > 0}
Optional Requires
@@ -159,10 +163,9 @@

Optional capabilities not available — plugin running in degraded mode

{/if}
- {/if} + {/if} - - {#if m.permissions && m.permissions.length > 0} + {#if m.permissions && m.permissions.length > 0}
Permissions
@@ -175,7 +178,8 @@ {/each}
- {/if} + {/if} + {#if p.error} @@ -289,6 +293,24 @@ font-size: 0.8rem; } + .technical-meta { + margin-top: 0.55rem; + } + + .plugin-details { + margin: 0.6rem 0; + padding: 0.45rem 0; + border-top: 1px solid rgba(15, 52, 96, 0.75); + border-bottom: 1px solid rgba(15, 52, 96, 0.75); + } + + .plugin-details summary { + cursor: pointer; + color: #8b8ba8; + font-size: 0.78rem; + font-weight: 600; + } + .meta-row { display: flex; gap: 0.5rem; diff --git a/frontend/src/lib/plugin-manager/PluginManager.svelte b/frontend/src/lib/plugin-manager/PluginManager.svelte index df0d41a..8aba0db 100644 --- a/frontend/src/lib/plugin-manager/PluginManager.svelte +++ b/frontend/src/lib/plugin-manager/PluginManager.svelte @@ -514,7 +514,7 @@ } .modal { background: #16213e; border: 1px solid #0f3460; border-radius: 8px; - width: 480px; max-width: 90vw; max-height: 80vh; display: flex; flex-direction: column; + width: min(880px, calc(100vw - 4rem)); max-width: calc(100vw - 4rem); height: min(680px, calc(100vh - 4rem)); max-height: calc(100vh - 4rem); display: flex; flex-direction: column; } .modal-header { display: flex; align-items: center; justify-content: space-between; @@ -523,7 +523,8 @@ .modal-header h3 { margin: 0; color: #e0e0f0; font-size: 1.1rem; } .modal-close { background: none; border: none; color: #a0a0b8; font-size: 1.2rem; cursor: pointer; padding: 0.2rem 0.5rem; } .modal-close:hover { color: #e94560; } - .modal-body { padding: 1rem; overflow-y: auto; } + .modal-body { padding: 1rem; overflow: auto; min-height: 0; flex: 1; display: flex; flex-direction: column; } + .modal-body :global(.plugin-bundle-host) { flex: 1; min-height: 0; display: flex; flex-direction: column; } .settings-hint { color: #666; font-size: 0.8rem; margin: 0.25rem 0; } .settings-hint code { color: #4ecca3; } @@ -542,7 +543,8 @@ } .modal { - width: min(480px, calc(100vw - 2rem)); + width: min(880px, calc(100vw - 2rem)); + height: min(680px, calc(100vh - 2rem)); max-height: calc(100vh - 2rem); } } diff --git a/frontend/src/lib/shell/GlobalSearch.svelte b/frontend/src/lib/shell/GlobalSearch.svelte new file mode 100644 index 0000000..48d1567 --- /dev/null +++ b/frontend/src/lib/shell/GlobalSearch.svelte @@ -0,0 +1,349 @@ + + + + + diff --git a/frontend/src/lib/shell/Sidebar.svelte b/frontend/src/lib/shell/Sidebar.svelte index d454b6a..f539559 100644 --- a/frontend/src/lib/shell/Sidebar.svelte +++ b/frontend/src/lib/shell/Sidebar.svelte @@ -2,6 +2,7 @@ import { onDestroy, onMount } from 'svelte'; import * as App from '../../../wailsjs/go/api/App'; import WorkspaceTree from './WorkspaceTree.svelte'; + import GlobalSearch from './GlobalSearch.svelte'; import Icon from '../ui/Icon.svelte'; import { debug } from '../log/debug.js'; @@ -76,6 +77,10 @@ Verstak
+ {#if vaultOpen} + + {/if} + {#if sidebarItems.length > 0}