diff --git a/docs/ui-polish-assets/2026-07-01/after-activity.png b/docs/ui-polish-assets/2026-07-01/after-activity.png new file mode 100644 index 0000000..e4f73b1 Binary files /dev/null and b/docs/ui-polish-assets/2026-07-01/after-activity.png differ diff --git a/docs/ui-polish-assets/2026-07-01/after-browser-inbox.png b/docs/ui-polish-assets/2026-07-01/after-browser-inbox.png new file mode 100644 index 0000000..d53fb8f Binary files /dev/null and b/docs/ui-polish-assets/2026-07-01/after-browser-inbox.png differ diff --git a/docs/ui-polish-assets/2026-07-01/after-files.png b/docs/ui-polish-assets/2026-07-01/after-files.png new file mode 100644 index 0000000..bf4028a Binary files /dev/null and b/docs/ui-polish-assets/2026-07-01/after-files.png differ diff --git a/docs/ui-polish-assets/2026-07-01/after-journal-modal.png b/docs/ui-polish-assets/2026-07-01/after-journal-modal.png new file mode 100644 index 0000000..d170e85 Binary files /dev/null and b/docs/ui-polish-assets/2026-07-01/after-journal-modal.png differ diff --git a/docs/ui-polish-assets/2026-07-01/after-mobile-workspace.png b/docs/ui-polish-assets/2026-07-01/after-mobile-workspace.png new file mode 100644 index 0000000..e5e50b9 Binary files /dev/null and b/docs/ui-polish-assets/2026-07-01/after-mobile-workspace.png differ diff --git a/docs/ui-polish-assets/2026-07-01/after-search-empty.png b/docs/ui-polish-assets/2026-07-01/after-search-empty.png new file mode 100644 index 0000000..88b8733 Binary files /dev/null and b/docs/ui-polish-assets/2026-07-01/after-search-empty.png differ diff --git a/docs/ui-polish-assets/2026-07-01/after-search-results.png b/docs/ui-polish-assets/2026-07-01/after-search-results.png new file mode 100644 index 0000000..4ace9df Binary files /dev/null and b/docs/ui-polish-assets/2026-07-01/after-search-results.png differ diff --git a/docs/ui-polish-assets/2026-07-01/after-today.png b/docs/ui-polish-assets/2026-07-01/after-today.png new file mode 100644 index 0000000..6cb7fc6 Binary files /dev/null and b/docs/ui-polish-assets/2026-07-01/after-today.png differ diff --git a/docs/ui-polish-milestone.md b/docs/ui-polish-milestone.md new file mode 100644 index 0000000..867deb7 --- /dev/null +++ b/docs/ui-polish-milestone.md @@ -0,0 +1,96 @@ +# UI Polish Milestone + +## Assumptions + +- This milestone is visual polish only: no backend API changes, no plugin contract changes, and no feature removal. +- English remains the baseline UI language for this pass. Hardcoded Russian strings that are visible in core UI should be converted to English where touched. +- Official plugins are plain JS bundles with injected CSS. Introducing a new shared UI package would be larger than this milestone, so shared primitives will be expressed as global design tokens/classes plus consistent local CSS in each bundle. +- Desktop workbench density should stay compact. The goal is consistency, not a marketing-style redesign. + +## Audit Findings + +### Existing Tokens + +- `frontend/src/App.svelte` already defines partial global button classes: `btn-primary`, `btn-secondary`, `btn-danger`, `btn-ghost`, `btn-icon`, plus global button focus/disabled behavior. +- Most shell and plugin surfaces hardcode the same dark palette directly: `#1a1a2e`, `#16213e`, `#0f3460`, `#4ecca3`, `#e94560`, `#8b8ba8`. +- There are no named CSS variables for semantic surface, selected row, border, muted text, warning, or focus ring. +- Plugin bundles repeat their own toolbar/button/list/empty/error styles instead of reusing shell-level primitives. + +### Repeated Patterns + +- Page/workspace headers: `WorkspaceHost.svelte`, `WorkbenchHost.svelte`, `ViewContainer.svelte`, plugin toolbars. +- Tab bars: workspace tabs use a custom active style separate from selected rows and sidebar active states. +- Toolbars: Files, Notes, Search, Activity, Journal, Browser Inbox, and default editor use similar but inconsistent dark toolbar rules. +- List rows: Files rows, Notes rows, Activity rows, Journal rows, Secrets list items, Browser Inbox rows, Global Search results, WorkspaceTree rows. +- Cards/panels: Today panels, worklog suggestions, Secrets detail/form, Browser Inbox detail text, plugin manager cards. +- Empty/error states: many are plain centered text or show technical details as primary text. + +### Candidate Primitives + +Implement as global classes/tokens first, then apply locally: + +- PageHeader / WorkspaceHeader: compact title, metadata, right actions/search. +- TabBar / TabButton: consistent active tab, hover, focus. +- Toolbar / ToolbarGroup: grouped controls with separators and predictable height. +- Button variants: primary, secondary, ghost, danger, icon. +- ListRow: hover, selected, disabled, metadata line, actions zone. +- Card: flat desktop panel with 6px radius and subtle border. +- SplitPane: list/detail grid with shared border and selected item states. +- EmptyState: centered title, hint, optional action. +- InlineAlert: readable message plus optional details/debug text. +- StatusBadge: compact semantic label for type, status, warning/error. +- Menu: context menu with same surface, hover, danger item, separator. + +## Implementation Plan + +1. Add semantic CSS variables and global primitive classes in `frontend/src/App.svelte`. + - Colors: background, surface, surface-muted, surface-hover, surface-selected, border, border-strong, text-primary, text-secondary, text-muted, accent, accent-muted, danger, warning, success. + - Spacing: 4/8/12/16/24/32 via variables. + - Radius, font sizes, focus ring, and modest elevation tokens. + +2. Normalize shell surfaces. + - Update `WorkspaceHost.svelte`, `WorkbenchHost.svelte`, `ViewContainer.svelte`, `Sidebar.svelte`, `WorkspaceTree.svelte`, `GlobalSearch.svelte`, and `TodaySurface.svelte` to use tokens and shared state rules. + - Convert visible Russian shell search/empty strings to English. + - Keep plugin manager architecture untouched; only global tokens should make it less visually divergent. + +3. Normalize Files. + - Group toolbar into Navigation, Create, Selection, Clipboard/More, and filter/sort areas. + - Keep existing context menu and operations, restyle menu with shared surface/hover/danger states. + - Make single row click open when no modifier is used; keep ctrl/meta/shift selection behavior and keyboard selection. + - Reduce actions column visual noise by showing secondary row actions on hover/selection and using a compact More/action group. + +4. Normalize Notes. + - Make New Note the primary page action. + - Keep filter/sort inside the toolbar group. + - Make entire note row readable and clickable; row actions remain visible enough and use icon-button states. + - Use shared empty/status/error styling. + +5. Normalize Search. + - Render results as compact rows/cards with type badge/icon, title/path, secondary match reason, and compact Open button. + - Show provider errors as inline alert-style secondary information, not as raw main status text. + +6. Normalize Activity and Journal. + - Add human-readable event labels such as "Workspace selected", "File opened", "Note edited", and "Work session detected". + - Move raw event names/source IDs into a details block or secondary metadata. + - Style plugin errors through inline alert patterns. + - Present worklog suggestions as cards with consistent title/summary/minutes actions. + +7. Normalize Secrets and Browser Inbox. + - Keep split-pane list/detail layout. + - Use shared list rows, detail card/form surfaces, and empty states. + - Render "Value hidden" as password-like hidden field state. + - Use button variants: primary for save/create, secondary for copy/edit/remove where appropriate, danger for delete/clear/remove destructive actions. + +8. Verify. + - Run desktop frontend build and relevant Playwright checks if available. + - Run official plugin smoke/check script. + - Capture before/after screenshots or save available after screenshots under `docs/ui-polish-assets/`. + +## Out of Scope + +- Backend API or Wails method changes. +- Plugin manifest/contribution contract changes. +- New heavy UI dependencies. +- Full i18n extraction. +- Redesigning the product information architecture. +- Rebuilding official plugins into Svelte packages. diff --git a/frontend/e2e/activity.spec.js b/frontend/e2e/activity.spec.js index a43e0bf..4e95537 100644 --- a/frontend/e2e/activity.spec.js +++ b/frontend/e2e/activity.spec.js @@ -49,6 +49,15 @@ test.describe('Activity workflow', () => { sourcePluginId: 'verstak.files', workspaceRootPath: 'Project', }, + { + activityId: 'activity-e2e-open', + occurredAt: '2026-06-30T08:30:00.000Z', + type: 'file.opened', + title: 'Selected file', + summary: 'Project/Notes/Research Capture.md', + sourcePluginId: 'verstak.files', + workspaceRootPath: 'Project', + }, ], }); }); @@ -59,6 +68,7 @@ test.describe('Activity workflow', () => { await expect(activity.locator('.activity-count')).toHaveText('2 events'); await expect(activity.locator('[data-activity-id="activity-e2e-capture"]')).toContainText('Research Capture'); await expect(activity.locator('[data-activity-id="activity-e2e-note"]')).toContainText('Saved note'); + await expect(activity.locator('[data-activity-id="activity-e2e-open"]')).toHaveCount(0); await expect(activity.locator('[data-activity-section="worklog-suggestions"]')).toContainText('Worklog suggestions'); await expect(activity.locator('[data-worklog-suggestion="worklog:Project:2026-06-30"]')).toContainText('Project work on 2026-06-30'); await expect(activity.locator('[data-worklog-suggestion="worklog:Project:2026-06-30"]')).toContainText('50 min'); diff --git a/frontend/e2e/ux-p0.spec.js b/frontend/e2e/ux-p0.spec.js index 77ec9fb..1c2c319 100644 --- a/frontend/e2e/ux-p0.spec.js +++ b/frontend/e2e/ux-p0.spec.js @@ -97,9 +97,9 @@ test.describe('UX quick wins', () => { 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 expect(page.getByText('Choose a vault to start working')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Create vault' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Open existing' })).toBeVisible(); await page.close(); }); }); diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 6357562..1e7a007 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -416,6 +416,41 @@ box-sizing: border-box; } + :global(:root) { + --vt-color-background: #101020; + --vt-color-surface: #15152c; + --vt-color-surface-muted: #111629; + --vt-color-surface-hover: #1b2440; + --vt-color-surface-selected: rgba(78, 204, 163, 0.14); + --vt-color-border: #202b46; + --vt-color-border-strong: #2c456a; + --vt-color-text-primary: #f4f7fb; + --vt-color-text-secondary: #b7c0d4; + --vt-color-text-muted: #7f8aa3; + --vt-color-accent: #4ecca3; + --vt-color-accent-muted: rgba(78, 204, 163, 0.14); + --vt-color-danger: #e94560; + --vt-color-danger-muted: rgba(233, 69, 96, 0.14); + --vt-color-warning: #ffc857; + --vt-color-warning-muted: rgba(255, 200, 87, 0.14); + --vt-color-success: #4ecca3; + --vt-space-1: 0.25rem; + --vt-space-2: 0.5rem; + --vt-space-3: 0.75rem; + --vt-space-4: 1rem; + --vt-space-6: 1.5rem; + --vt-space-8: 2rem; + --vt-radius-sm: 4px; + --vt-radius-md: 6px; + --vt-radius-lg: 8px; + --vt-font-xs: 0.72rem; + --vt-font-sm: 0.8rem; + --vt-font-md: 0.88rem; + --vt-font-lg: 1rem; + --vt-focus-ring: 0 0 0 2px rgba(78, 204, 163, 0.34); + --vt-elevation-menu: 0 14px 32px rgba(0, 0, 0, 0.42); + } + :global(html), :global(body), :global(#app) { @@ -425,8 +460,8 @@ :global(body) { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background: #1a1a2e; - color: #e0e0f0; + background: var(--vt-color-background); + color: var(--vt-color-text-primary); overflow: hidden; } @@ -437,12 +472,12 @@ gap: 0.35rem; min-height: 2rem; padding: 0.4rem 0.85rem; - border: 1px solid #1a3a5c; - border-radius: 6px; - background: #0f3460; - color: #e0e0f0; + border: 1px solid var(--vt-color-border-strong); + border-radius: var(--vt-radius-md); + background: var(--vt-color-surface-hover); + color: var(--vt-color-text-primary); font: inherit; - font-size: 0.85rem; + font-size: var(--vt-font-sm); font-weight: 600; line-height: 1.2; cursor: pointer; @@ -450,14 +485,14 @@ } :global(button:hover:not(:disabled)) { - background: #1a3a5c; - border-color: #4ecca3; + background: #203050; + border-color: var(--vt-color-accent); color: #ffffff; } :global(button:focus-visible) { - outline: 2px solid #4ecca3; - outline-offset: 2px; + outline: 0; + box-shadow: var(--vt-focus-ring); } :global(button:disabled) { @@ -466,8 +501,8 @@ } :global(.btn-primary) { - background: #4ecca3; - border-color: #4ecca3; + background: var(--vt-color-accent); + border-color: var(--vt-color-accent); color: #101827; } @@ -478,14 +513,14 @@ } :global(.btn-secondary) { - background: #0f3460; - border-color: #533483; - color: #e0e0f0; + background: var(--vt-color-surface-hover); + border-color: var(--vt-color-border-strong); + color: var(--vt-color-text-primary); } :global(.btn-danger) { - background: #e94560; - border-color: #e94560; + background: var(--vt-color-danger); + border-color: var(--vt-color-danger); color: #ffffff; } @@ -497,13 +532,13 @@ :global(.btn-ghost) { background: transparent; border-color: transparent; - color: #a0a0b8; + color: var(--vt-color-text-secondary); } :global(.btn-ghost:hover:not(:disabled)) { - background: rgba(15, 52, 96, 0.55); - border-color: #0f3460; - color: #e0e0f0; + background: var(--vt-color-surface-hover); + border-color: var(--vt-color-border); + color: var(--vt-color-text-primary); } :global(.btn-icon) { @@ -512,6 +547,236 @@ padding: 0; } + :global(.vt-page) { + min-width: 0; + min-height: 0; + height: 100%; + display: flex; + flex-direction: column; + background: var(--vt-color-background); + color: var(--vt-color-text-primary); + } + + :global(.vt-page-header), + :global(.vt-toolbar) { + min-height: 2.75rem; + display: flex; + align-items: center; + gap: var(--vt-space-2); + padding: var(--vt-space-2) var(--vt-space-3); + border-bottom: 1px solid var(--vt-color-border); + background: var(--vt-color-surface-muted); + flex-shrink: 0; + } + + :global(.vt-page-title) { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--vt-color-text-primary); + font-size: var(--vt-font-lg); + font-weight: 650; + } + + :global(.vt-page-subtitle) { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--vt-color-text-muted); + font-size: var(--vt-font-sm); + } + + :global(.vt-toolbar-group) { + display: inline-flex; + align-items: center; + gap: var(--vt-space-1); + padding: 0 var(--vt-space-2) 0 0; + border-right: 1px solid var(--vt-color-border); + } + + :global(.vt-toolbar-group:last-child) { + border-right: 0; + padding-right: 0; + } + + :global(.vt-button) { + min-height: 2rem; + border-radius: var(--vt-radius-md); + } + + :global(.vt-icon-button) { + width: 2rem; + min-width: 2rem; + height: 2rem; + min-height: 2rem; + padding: 0; + } + + :global(.vt-tabbar) { + display: flex; + align-items: center; + gap: var(--vt-space-1); + padding: var(--vt-space-1) var(--vt-space-3) 0; + border-bottom: 1px solid var(--vt-color-border); + background: #12162a; + overflow-x: auto; + flex-shrink: 0; + } + + :global(.vt-tab) { + flex-shrink: 0; + min-height: 2.1rem; + padding: 0.38rem 0.78rem; + border: 1px solid transparent; + border-bottom: 0; + border-radius: var(--vt-radius-md) var(--vt-radius-md) 0 0; + background: transparent; + color: var(--vt-color-text-muted); + font-size: var(--vt-font-sm); + } + + :global(.vt-tab:hover:not(:disabled)) { + background: var(--vt-color-surface-hover); + border-color: transparent; + color: var(--vt-color-text-primary); + } + + :global(.vt-tab.is-active), + :global(.vt-tab.active) { + background: var(--vt-color-background); + border-color: var(--vt-color-border); + color: var(--vt-color-accent); + } + + :global(.vt-list-row) { + border-bottom: 1px solid rgba(32, 43, 70, 0.72); + color: var(--vt-color-text-primary); + } + + :global(.vt-list-row:hover) { + background: var(--vt-color-surface-hover); + } + + :global(.vt-list-row.selected), + :global(.vt-list-row.active), + :global(.vt-list-row.is-selected) { + background: var(--vt-color-surface-selected); + box-shadow: inset 2px 0 0 var(--vt-color-accent); + } + + :global(.vt-card) { + border: 1px solid var(--vt-color-border); + border-radius: var(--vt-radius-lg); + background: var(--vt-color-surface); + } + + :global(.vt-split-pane) { + display: grid; + grid-template-columns: minmax(16rem, 22rem) minmax(0, 1fr); + min-height: 0; + height: 100%; + background: var(--vt-color-background); + } + + :global(.vt-split-list) { + min-height: 0; + overflow: auto; + border-right: 1px solid var(--vt-color-border); + background: var(--vt-color-surface-muted); + } + + :global(.vt-split-detail) { + min-width: 0; + min-height: 0; + overflow: auto; + padding: var(--vt-space-4); + } + + :global(.vt-empty-state) { + min-height: 9rem; + display: flex; + flex: 1; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--vt-space-2); + padding: var(--vt-space-8); + color: var(--vt-color-text-muted); + font-size: var(--vt-font-sm); + line-height: 1.45; + text-align: center; + } + + :global(.vt-empty-title) { + color: var(--vt-color-text-secondary); + font-size: var(--vt-font-md); + font-weight: 650; + } + + :global(.vt-inline-alert) { + border: 1px solid var(--vt-color-border-strong); + border-radius: var(--vt-radius-md); + background: var(--vt-color-surface-muted); + color: var(--vt-color-text-secondary); + padding: var(--vt-space-3); + font-size: var(--vt-font-sm); + line-height: 1.45; + } + + :global(.vt-inline-alert.error) { + border-color: rgba(233, 69, 96, 0.55); + background: var(--vt-color-danger-muted); + color: #ffc6ce; + } + + :global(.vt-badge) { + display: inline-flex; + align-items: center; + min-height: 1.25rem; + padding: 0.1rem 0.4rem; + border: 1px solid var(--vt-color-border); + border-radius: var(--vt-radius-sm); + background: var(--vt-color-surface-muted); + color: var(--vt-color-text-muted); + font-size: var(--vt-font-xs); + font-weight: 650; + line-height: 1; + } + + :global(.vt-badge.accent) { + border-color: rgba(78, 204, 163, 0.4); + background: var(--vt-color-accent-muted); + color: var(--vt-color-accent); + } + + :global(.vt-menu) { + border: 1px solid var(--vt-color-border-strong); + border-radius: var(--vt-radius-md); + background: var(--vt-color-surface); + color: var(--vt-color-text-primary); + box-shadow: var(--vt-elevation-menu); + } + + :global(.vt-menu-item) { + display: flex; + align-items: center; + gap: var(--vt-space-2); + padding: 0.42rem 0.7rem; + color: var(--vt-color-text-secondary); + cursor: pointer; + } + + :global(.vt-menu-item:hover) { + background: var(--vt-color-surface-hover); + color: var(--vt-color-text-primary); + } + + :global(.vt-menu-item.danger) { + color: #ff9aaa; + } + :global(.scroll-surface) { min-width: 0; min-height: 0; @@ -521,7 +786,7 @@ :global(*) { scrollbar-width: thin; - scrollbar-color: #0f3460 #1a1a2e; + scrollbar-color: var(--vt-color-border-strong) var(--vt-color-background); } :global(*::-webkit-scrollbar) { @@ -530,12 +795,12 @@ } :global(*::-webkit-scrollbar-track) { - background: #1a1a2e; + background: var(--vt-color-background); } :global(*::-webkit-scrollbar-thumb) { - background: #0f3460; - border-radius: 4px; + background: var(--vt-color-border-strong); + border-radius: var(--vt-radius-sm); } :global(*::-webkit-scrollbar-thumb:hover) { @@ -547,8 +812,8 @@ align-items: center; justify-content: center; height: 100vh; - background: #1a1a2e; - color: #a0a0b8; + background: var(--vt-color-background); + color: var(--vt-color-text-muted); font-size: 1rem; } @@ -556,7 +821,7 @@ display: flex; height: 100vh; width: 100%; - background: #1a1a2e; + background: var(--vt-color-background); overflow: hidden; } @@ -574,7 +839,7 @@ min-height: 0; display: flex; flex-direction: column; - padding: clamp(1rem, 2vw, 1.5rem); + padding: 0; } @media (max-width: 720px) { diff --git a/frontend/src/lib/shell/GlobalSearch.svelte b/frontend/src/lib/shell/GlobalSearch.svelte index 863761c..73842b0 100644 --- a/frontend/src/lib/shell/GlobalSearch.svelte +++ b/frontend/src/lib/shell/GlobalSearch.svelte @@ -161,7 +161,7 @@ next.push({ type: 'Workspace', title: workspaceTitle(node), - subtitle: 'Рабочее пространство', + subtitle: 'Workspace', keywords: `${node.id || ''} ${node.rootPath || ''}`, rank: 10, action: 'workspace', @@ -293,8 +293,8 @@ on:focus={handleFocus} on:blur={() => setTimeout(() => focused = false, 120)} type="search" - placeholder={loading ? 'Индексируем...' : 'Поиск'} - aria-label="Глобальный поиск" + placeholder={loading ? 'Indexing...' : 'Search'} + aria-label="Global search" data-global-search-input /> @@ -314,7 +314,7 @@ {/each} {:else} -