From 7c3cc269bcc80f6b715f7efdcea2c1d771e8730e Mon Sep 17 00:00:00 2001 From: blankll Date: Wed, 21 Oct 2026 16:01:38 +0800 Subject: [PATCH 1/4] feat(ui): unify graphite theme and semantic colors --- AGENTS.md | 4 +- DESIGN.md | 111 +++++++------- eslint.config.js | 1 + src/__tests__/dataGridPaging.test.ts | 4 +- src/__tests__/theme.test.ts | 117 +++++++++++++++ src/assets/index.css | 103 ++++++++----- src/components/DeviceReplaceDialog.vue | 2 +- src/components/SQLEditor.vue | 10 +- src/components/UpdateNotification.vue | 12 +- src/components/agent-message-bubble.vue | 38 ++--- src/components/chat-panel.vue | 6 +- .../connections/ConnectingModal.vue | 6 +- .../connections/DatabaseFileField.vue | 10 +- src/components/connections/ServerCard.vue | 8 +- .../connections/ServerFormDialog.vue | 80 +++++----- .../connections/SshProfileDialog.vue | 6 +- .../connections/SshTunnelConfigPanel.vue | 20 +-- .../connections/ssl/SslCertFields.vue | 6 +- .../connections/ssl/SslModeSelect.vue | 4 +- .../database-browser/DataTableView.vue | 30 ++-- .../database-browser/ForeignKeysTab.vue | 2 +- .../database-browser/IndexesTab.vue | 6 +- .../database-browser/ListingTab.vue | 4 +- .../database-browser/QueryResultPanel.vue | 6 +- src/components/database-browser/QueryTabs.vue | 11 +- src/components/database-browser/SchemaTab.vue | 8 +- .../database-browser/ServerInfoView.vue | 2 +- .../database-browser/TriggersTab.vue | 2 +- .../er-diagram/EngineeringEntity.vue | 10 +- src/components/er-diagram/ErDiagramView.vue | 2 +- src/components/er-diagram/TableCard.vue | 4 +- .../explain-plan/ExplainPlanNodeTree.vue | 17 +-- .../explain-plan/ExplainPlanPanel.vue | 2 +- src/components/grid/DataGrid.vue | 19 ++- src/components/grid/EditRowDialog.vue | 6 +- src/components/grid/FilterBar.vue | 17 +-- src/components/layout/AppHeader.vue | 8 +- src/components/layout/AppSidebar.vue | 10 +- src/components/layout/TaskSidebar.vue | 2 +- src/components/layout/UserMenu.vue | 6 +- src/components/layout/WindowControls.vue | 2 +- src/components/markdown-render.vue | 2 +- src/components/model-picker.vue | 10 +- src/components/settings/SettingsFeedback.vue | 4 +- src/components/sidebar/ConnectionSelector.vue | 10 +- .../sidebar/CreateDatabaseDialog.vue | 2 +- src/components/sidebar/CreateTableDialog.vue | 2 +- .../sidebar/DatabaseSelectorRow.vue | 6 +- src/components/sidebar/DeleteQueryDialog.vue | 2 +- src/components/sidebar/DropDatabaseDialog.vue | 2 +- src/components/sidebar/SavedQueryItem.vue | 6 +- src/components/sidebar/SchemaTree.vue | 90 ++++++------ src/components/sidebar/TreeGroup.vue | 2 +- .../transfer/export/ExportPreviewStep.vue | 2 +- .../transfer/export/ExportWizard.vue | 8 +- .../transfer/import/ImportMappingStep.vue | 2 +- .../transfer/import/ImportWizard.vue | 8 +- .../transfer/shared/ConnectionSelector.vue | 4 +- .../transfer/shared/FileDropZone.vue | 2 +- .../transfer/shared/MultiTableSelector.vue | 12 +- .../transfer/shared/ProgressPanel.vue | 8 +- .../transfer/shared/ResultPanel.vue | 10 +- .../transfer/shared/ScopeSelector.vue | 2 +- .../transfer/shared/TableSelector.vue | 2 +- src/components/transfer/tasks/TaskCard.vue | 16 +- .../transfer/tasks/TaskManagerPanel.vue | 2 +- src/components/ui/badge/index.ts | 14 +- src/components/ui/button/index.ts | 2 +- .../ui/combobox/SearchableSelect.vue | 2 +- .../DestructiveConfirmDialog.vue | 6 +- .../ui/notification/AppNotifications.vue | 14 +- .../ui/radio-group/RadioGroupItem.vue | 2 +- src/components/ui/spinner/Spinner.vue | 2 +- .../upgrade/FeaturePoster/AiPoster.vue | 8 +- .../upgrade/FeaturePoster/McpPoster.vue | 2 +- .../upgrade/FeaturePoster/TransferPoster.vue | 10 +- src/components/upgrade/PaidGate.vue | 6 +- src/components/upgrade/ProBadge.vue | 2 +- src/components/upgrade/UpgradeDialog.vue | 10 +- src/composables/editorTheme/index.ts | 57 ++++++++ src/composables/useDatabaseIcon.ts | 138 +++++++++--------- src/composables/useMonacoEditor.ts | 5 +- src/pages/ConnectionsPage.vue | 30 ++-- src/pages/DataStudioPage.vue | 14 +- src/pages/HistoryPage.vue | 8 +- src/pages/QueriesPage.vue | 29 ++-- src/pages/SettingsPage.vue | 83 +++-------- src/pages/TransferPage.vue | 2 +- src/store/appStore.ts | 2 +- .../components/modify-source-modal.vue | 22 +-- .../components/session-history-panel.vue | 2 +- .../components/tool-confirmation-card.vue | 8 +- src/views/setting/ai-settings.vue | 6 +- src/views/setting/mcp-bridge.vue | 6 +- src/views/setting/plan-section.vue | 16 +- src/views/setting/provider-form-dialog.vue | 6 +- src/views/setting/provider-list.vue | 8 +- tests/store/appStore.test.ts | 8 +- uno.config.ts | 18 ++- 99 files changed, 830 insertions(+), 658 deletions(-) create mode 100644 src/__tests__/theme.test.ts create mode 100644 src/composables/editorTheme/index.ts diff --git a/AGENTS.md b/AGENTS.md index bf6e1ab4..bf1992aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,11 +8,11 @@ Tauri-based cross-platform SQL database GUI client with Vue 3 + TypeScript front **Before adding or changing client UI, read [DESIGN.md](DESIGN.md).** It is the canonical client design specification; feature design documents describe behavior within that system. -- Preserve SQLKit's existing **blue primary**, light/dark/system themes, compact desktop workspace, and shared shadcn-vue/Radix Vue components. Learn restraint and semantic color ownership from console-serverlessinsight; do not copy its violet brand or monochrome primary buttons. +- Preserve SQLKit's existing **blue primary**, light/dark/system themes, compact desktop workspace, and shared shadcn-vue/Radix Vue components. Keep semantic color ownership consistent and retain SQLKit's blue brand and primary actions. - Use neutral surfaces, typography, spacing, and borders for hierarchy. Keep accent areas scarce (roughly 10% of application chrome); use at most one solid primary action per operation context. Secondary toolbar actions stay neutral. - Colors have fixed roles: `primary` = primary actions/interaction; `success` = actual success/connected/completed; `destructive` = errors/destructive actions. Ordinary information, counts, boolean values, database properties, and file formats are neutral. Do not assign a color to each feature or add a separate AI palette. - Reuse semantic CSS tokens from `src/assets/index.css` and their `uno.config.ts` mappings. Do not add palette classes or literal colors for existing UI semantics. `accent` is a neutral interaction surface, not a second brand color. -- `warning` and `info` are **not currently registered** semantic colors. Any new semantic token must include light/dark definitions, necessary foreground pairs, UnoCSS mappings, shared variants where needed, contrast verification, and a DESIGN.md update in the same change. +- `warning` is registered with light/dark foreground, muted-background, and border pairs; `info` is **not registered**. Use `primary-text` and `destructive-text` for readable colored text, reserving the corresponding base tokens for solid actions. Any new semantic token must include light/dark definitions, necessary foreground pairs, UnoCSS mappings, shared variants where needed, contrast verification, and a DESIGN.md update in the same change. - Non-interactive badges explicitly use `outline`/`secondary` by default; status colors require real state and accompanying text or icons. Syntax highlighting, data charts, diffs, and small vendor logos have the bounded exceptions documented in DESIGN.md. - Provide visible keyboard focus, accessible icon-button names, loading/empty/error states, readable contrast, and localized copy. Reuse runtime platform detection for shortcuts and native window behavior. - Existing hardcoded colors and component defaults are migration debt, not permission to repeat them. Distinguish documented targets from implemented behavior, and verify the affected themes, window sizes, and interactions when changing UI. diff --git a/DESIGN.md b/DESIGN.md index 1bed51ef..838e282b 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -3,13 +3,13 @@ name: SQLKit description: A restrained, dense, cross-platform database workspace centered on SQL and data colors: primary-light: "hsl(221.2 83.2% 45%)" - primary-dark: "hsl(217.2 91.2% 45%)" + primary-dark: "hsl(221.21 83.19% 53.33%)" background-light: "hsl(0 0% 100%)" - background-dark: "hsl(222.2 84% 4.9%)" + background-dark: "hsl(220.00 12.00% 9.80%)" foreground-light: "hsl(222.2 84% 4.9%)" - foreground-dark: "hsl(210 40% 98%)" + foreground-dark: "hsl(217.50 17.39% 90.98%)" card-light: "hsl(0 0% 100%)" - card-dark: "hsl(222.2 47% 8%)" + card-dark: "hsl(220.00 11.39% 15.49%)" rounded: lg: "8px" md: "6px" @@ -22,22 +22,17 @@ rounded: **Design direction: A quiet SQL workspace.** Users should notice connection context, SQL, and query results first, actions second, and brand expression last. Support long sessions of writing, inspecting, and comparing data through high information density, clear hierarchy, stable layouts, and predictable interactions. -This file defines the design constraints for new and modified SQLKit client UI, alongside [AGENTS.md](AGENTS.md). Preserve the existing blue theme, light / dark / system modes, compact desktop layout, and shadcn-vue / Radix Vue component system. Establishing this specification does not mean existing screens have already been migrated. +This file defines the design constraints for new and modified SQLKit client UI, alongside [AGENTS.md](AGENTS.md). Preserve the existing blue theme, light / dark / system modes, compact desktop layout, and shadcn-vue / Radix Vue component system. The graphite theme and semantic color migration apply to the client shell, shared controls, SQL editor, connections, grids, transfers, settings, and AI surfaces. -### Sources and adaptations +### Design principles -The reference project is `console-serverlessinsight`, reviewed on 2026-10-19. The main sources are its `AGENTS.md` section on color rules (V3 · Mono + Violet Accent), `apps/console/src/assets/css/themes/violet.css`, `apps/console/uno.config.ts`, `docs/console-ui-design.md`, and `apps/console/PRODUCT.md`. These paths record provenance only; SQLKit has no runtime dependency on the reference repository. +The client uses a restrained desktop workspace with softly cool graphite surfaces in dark mode and blue primary actions in both themes. -| Reference principle | SQLKit adaptation | -| --- | --- | -| Give colors fixed semantics rather than assigning them to modules | Blue for primary actions and interaction, green for success, red for errors or danger; neutrals for ordinary information | -| Let neutral surfaces dominate and keep accents scarce | Preserve SQLKit's slightly cool neutral tokens rather than copying the reference project's zero-chroma grays | -| Restrict brand color to small areas | Keep SQLKit's blue primary buttons, limiting their count and area; do not copy violet or monochrome primary buttons | -| Use semantic status tokens across light and dark themes | Reuse success / destructive; fill gaps such as warning through the process below | -| Use progressive disclosure, a stable app shell, and keyboard access | Apply these to the connection tree, query tabs, editor, result grids, and desktop window behavior | -| Require non-color signals and readable contrast | Treat these as acceptance criteria; existing tokens do not prove compliance | - -The reference project's early colorful diagrams, login gradients, and CSS glass and ambient-light effects are not design references for SQLKit's workspace. +- Give colors fixed semantics: blue for interaction, green for actual success, red for errors and danger, amber for actual warnings, and neutrals for ordinary information. +- Keep accents scarce and let typography, spacing, and surface hierarchy organize the workspace. +- Use shared semantic tokens for light, dark, and system appearance, including the SQL editor and overlays. +- Preserve compact layouts, progressive disclosure, predictable keyboard access, and readable contrast. +- Keep decorative gradients, ambient effects, and glass treatments out of the database workspace. ### Specification and implementation @@ -54,33 +49,49 @@ The table records CSS HSL components, used as `hsl(var(--token))`. The frontmatt | Token | Light | Dark | Role | | --- | --- | --- | --- | -| `--primary` | `221.2 83.2% 45%` | `217.2 91.2% 45%` | Primary actions, links, interaction emphasis | -| `--primary-foreground` | `0 0% 100%` | `0 0% 100%` | Text on solid primary backgrounds | -| `--background` | `0 0% 100%` | `222.2 84% 4.9%` | Main workspace | -| `--foreground` | `222.2 84% 4.9%` | `210 40% 98%` | Body text and important data | -| `--card` / `--popover` | `0 0% 100%` | `222.2 47% 8%` | Containers and overlays | -| `--secondary` / `--muted` | `210 40% 96.1%` | `217.2 32.6% 16%` | Secondary surfaces and neutral states | -| `--muted-foreground` | `215.4 16.3% 46.9%` | `215 20.2% 65.1%` | Supporting text; never the sole carrier of critical information | -| `--accent` | `210 40% 96.1%` | `217.2 32.6% 17.5%` | Neutral interaction surface, **not a second brand color** | -| `--border` | `214.3 31.8% 91.4%` | `217.2 32.6% 17.5%` | Structural separation | -| `--input` | `214.3 31.8% 80%` | `217.2 32.6% 28%` | Input boundaries | -| `--ring` | `220 13% 60%` | `212.7 26.8% 68%` | Keyboard focus; retain the current neutral color | -| `--success` | `142.1 70.6% 35.5%` | `142.1 70% 45%` | Success, connected, completed | -| `--success-muted` | `138 76% 95%` | `142 35% 16%` | Local success feedback background | -| `--success-border` | `140 58% 85%` | `142 30% 25%` | Success feedback boundary | -| `--destructive` | `0 84.2% 60.2%` | `0 62.8% 30.6%` | Errors, failures, destructive actions | +| `--background` | `0 0% 100%` | `220.00 12.00% 9.80%` | Workspace and editor | +| `--chrome` | `210.00 40.00% 98.04%` | `217.50 12.50% 12.55%` | Title bar, navigation, sidebar, toolbars | +| `--foreground` | `222.2 84% 4.9%` | `217.50 17.39% 90.98%` | Body text and data | +| `--card` | `0 0% 100%` | `220.00 11.39% 15.49%` | Containers | +| `--popover` | `0 0% 100%` | `220.00 11.39% 15.49%` | Menus, dialogs and editor widgets | +| `--primary` | `221.2 83.2% 45%` | `221.21 83.19% 53.33%` | Solid primary actions | +| `--primary-foreground` | `0 0% 100%` | `0.00 0.00% 100.00%` | Text on primary fills | +| `--primary-text` | `224.28 76.33% 48.04%` | `218.46 100.00% 77.06%` | Links and small active indicators | +| `--selection` | `220.00 100.00% 95.29%` | `215.71 35.59% 23.14%` | Text and grid selection | +| `--secondary` | `210 40% 96.1%` | `220.00 11.39% 15.49%` | Secondary surfaces | +| `--muted` | `210 40% 96.1%` | `220.00 11.39% 15.49%` | Subdued surfaces | +| `--muted-foreground` | `213.33 16.82% 41.96%` | `217.89 11.80% 68.43%` | Supporting text | +| `--accent` | `210 40% 96.1%` | `216.92 13.13% 19.41%` | Neutral hover and selected controls | +| `--border` | `214.3 31.8% 91.4%` | `217.50 13.33% 23.53%` | Structural separation | +| `--input` | `213.75 13.91% 54.90%` | `215.56 11.79% 44.90%` | Control boundaries | +| `--ring` | `215.38 16.32% 46.86%` | `217.89 11.80% 68.43%` | Keyboard focus | +| `--destructive` | `0.00 73.71% 41.76%` | `0.00 53.53% 47.25%` | Solid destructive actions | +| `--destructive-foreground` | `0.00 0.00% 100.00%` | `0.00 0.00% 100.00%` | Text on destructive fills | +| `--destructive-text` | `0.00 73.71% 41.76%` | `4.82 81.16% 72.94%` | Readable error text and icons | +| `--destructive-muted` | `0.00 85.71% 97.25%` | `352.94 19.10% 17.45%` | Subdued error feedback | +| `--destructive-border` | `0.00 96.30% 89.41%` | `3.40 29.61% 35.10%` | Error feedback boundary | +| `--success` | `142.43 71.81% 29.22%` | `143.02 47.25% 64.31%` | Success text and connected/completed signals | +| `--success-foreground` | `0.00 0.00% 100.00%` | `220.00 12.00% 9.80%` | Text on success fills | +| `--success-muted` | `138.46 76.47% 96.67%` | `148.42 22.89% 16.27%` | Success feedback surface | +| `--success-border` | `141.00 78.95% 85.10%` | `141.67 23.08% 30.59%` | Success feedback boundary | +| `--warning` | `35.65 100.00% 27.06%` | `36.61 79.73% 70.98%` | Actual warnings and risk signals | +| `--warning-foreground` | `0.00 0.00% 100.00%` | `220.00 12.00% 9.80%` | Text on warning fills | +| `--warning-muted` | `39.00 100.00% 96.08%` | `36.67 20.45% 17.25%` | Warning feedback surface | +| `--warning-border` | `39.10 67.94% 74.31%` | `37.36 32.12% 32.35%` | Warning feedback boundary | + +Dark mode uses the approved softly cool graphite palette. Preserve blue primary actions. Use `primary-text` for blue text and icons, and `destructive-text` for error text and icons; solid fill tokens are not readable-text tokens. `success` and `warning` are paired with dark foregrounds in dark mode and white foregrounds in light mode. ### Semantic ownership | Visual signal | Allowed meaning | Must not represent | | --- | --- | --- | -| Blue `primary` | Primary actions, links, selected controls, small active indicators | Success, ordinary badges, large panel backgrounds | +| Blue `primary / primary-text` | Primary actions, links, selected controls, small active indicators | Success, ordinary badges, large panel backgrounds | | Neutrals `foreground / muted / secondary / accent` | Body text, supporting information, toolbars, navigation backgrounds, counts, ordinary attributes, disconnected states | Important errors or risks hidden through low contrast | | Green `success` | Successful connection tests, connected states, successful execution, completed transfers | Execute buttons, boolean true, auto-increment attributes, decorative selling points | -| Red `destructive` | Execution failures, connection failures, deletion confirmations | Ordinary task counts, disconnected states, boolean false, decoration | -| Amber `warning` (not yet implemented) | Actual warnings, risks, exceptional states requiring attention | JSON format, primary keys, ordinary unsaved indicators, arbitrary toolbar buttons | +| Red `destructive / destructive-text` | Execution failures, connection failures, deletion confirmations | Ordinary task counts, disconnected states, boolean false, decoration | +| Amber `warning` | Actual warnings, risks, exceptional states requiring attention | JSON format, primary keys, ordinary unsaved indicators, arbitrary toolbar buttons | -Use neutral icons and text for ordinary information; do not introduce a separate sky-blue palette. SQLKit's `primary` already owns blue interaction emphasis. There are currently no `--info` or `--warning` mappings. Add tokens only for semantics that require a distinct role. +Use neutral icons and text for ordinary information; do not introduce a separate sky-blue palette. SQLKit's `primary` already owns blue interaction emphasis. Warning includes foreground, muted-background, and border pairs in both themes. There is no `--info` mapping; ordinary information remains neutral. Add tokens only for semantics that require a distinct role. ### Color budget @@ -114,8 +125,8 @@ Use neutral icons and text for ordinary information; do not introduce a separate 1. Check whether an existing role can express the meaning before considering opacity variants. Opacity does not waive contrast requirements: text must remain readable against the actual composited background. 2. Do not add hex / RGB / HSL literals or palette utilities such as `blue-500`, `green-600`, and `violet-*` in business components to replace existing semantic roles. Isolate chart, syntax, and vendor-asset exceptions in their respective modules. 3. When adding a semantic token, include all of the following in the same change: `:root` and `.dark` definitions in `src/assets/index.css`, necessary foreground / subdued-background pairs, `uno.config.ts` mappings, shared component variants where needed, light/dark verification, and this document's update. -4. Do not submit UI that depends on unimplemented utilities such as `text-warning` or `bg-info` before their mappings exist. Warning is a known gap; do not substitute `primary` or `success` for it. -5. Check all consumers before changing shared tokens. Fixing semantic color readability must not also replace the blue brand or reset the entire neutral palette. +4. Do not submit UI that depends on unimplemented utilities such as `text-warning` or `bg-info` before their mappings exist. Use `warning` for actual risk; ordinary attributes remain neutral. +5. Check all consumers before changing shared tokens. Preserve the approved graphite surface hierarchy and blue brand when adjusting individual semantic colors. ## Typography @@ -227,8 +238,8 @@ Interactive components must cover default, hover, focus-visible, active / select ### Badges, notifications, and task states -- Non-interactive attributes default to `Badge variant="outline"` or `secondary`. Status uses a dot / icon with text. The shared Badge still defaults to solid primary; callers must explicitly select an appropriate variant. -- Use success for connected / completed, neutrals for disconnected / not started, and destructive for failed. Standardize warning treatment once shared tokens exist. +- Non-interactive attributes default to `Badge variant="outline"` or `secondary`. Status uses a dot / icon with text. The shared Badge defaults to neutral secondary. Explicitly use success / warning / destructive variants only for real states; those variants use subdued semantic pairs. +- Use success for connected / completed, neutrals for disconnected / not started, and destructive for failed. Use the shared warning tokens for actual risks. - Count badges are neutral by default. Task count is not failure count; `taskCount > 0` alone does not justify red. - Toasts use neutral backgrounds and status icons. Errors requiring action need readable explanations and Retry / View details paths rather than a brief flash. Prefer keeping execution feedback in the relevant result region. - Do not duplicate toasts for normal progress and success. Avoid simultaneously repeating the same event in local status, notifications, and the task panel. @@ -239,7 +250,7 @@ Interactive components must cover default, hover, focus-visible, active / select - Result column headers show the database-provided type on the same line as the name when metadata is available, using small neutral text. Keep numeric headers right-aligned, truncate long types with the full value available on hover, and omit type labels when metadata is absent. Sortable columns always show a neutral sort indicator, with directional arrows for active sorting; do not hide it until hover. - Keep the result-set selector inside the existing result toolbar. Collapse secondary export actions into a menu at narrow panel widths to preserve a single toolbar row. - Use the shared pagination footer and state model in table browsing and query result grids, starting at 100 rows per page. Keep first / previous / next / last, direct page entry, common page-size presets, custom sizes, and the row range consistent. Wrap footer groups at narrow widths; keep navigation reachable for empty results and disable it while loading or showing an error. Table browsing fetches one page at a time; query grids page returned rows locally and additionally offer all returned rows. Copy / export retains its stated data scope. See [RESULT_PAGINATION_DESIGN.md](docs/RESULT_PAGINATION_DESIGN.md). -- Keep Monaco's theme integration (currently `vs / vs-dark`) and user font-size settings. Syntax highlighting is outside the application decoration color budget; reducing colors must not damage syntax recognition. +- Use the token-driven `sqlkit-light / sqlkit-dark` Monaco themes and preserve user font-size settings. Both inherit Monaco's built-in grammar support; editor, gutter, active-line, selection, completion and hover surfaces read the active CSS theme. Syntax hues live in `--syntax-keyword / string / number / comment` in the same CSS token source. Syntax highlighting is outside the application decoration color budget; reducing colors must not damage syntax recognition. - Keep data central: neutral headers, subtle hover, stable selection, and clear sorting / filtering indicators. Preserve virtual scrolling; do not rebuild the whole grid for animation. - Distinguish `NULL`, empty strings, and booleans. Show true / false or equivalent text / symbols; do not default to red / green boolean mapping. - Primary keys, unique constraints, and auto-increment are attributes, not warnings or success. Use neutral identifiers such as a key icon or PK / UNIQUE / identity text. @@ -306,16 +317,10 @@ Check each affected surface. Documentation-only changes need source, link, and i - [ ] Data scope, sorting scope, and execution status match actual capabilities; visual changes preserve functional semantics. - [ ] UI implementation changes run `npm run lint:check` and `npm run build`; behavior changes also run relevant tests, and layout changes include actual screenshots at relevant themes and window sizes. -### Existing UI migration priorities (not implemented in this change) +### Theme maintenance -| Priority | Current state | Follow-up | -| --- | --- | --- | -| P1 | Existing foreground pairs such as success / destructive need individual verification | Check contrast on actual backgrounds; adjust semantic pairs when needed while retaining the blue brand | -| P1 | Success / warning in `ui/badge` and notifications still use fixed palettes; a global warning token is missing | Add light/dark semantic tokens and mappings first, then migrate shared components and check all consumers | -| P2 | Some `QueriesPage.vue` toolbar buttons use purple / orange | Use neutral toolbars and primary for the main action | -| P2 | `grid/FilterBar.vue` assigns colors to operators; grid booleans and some attributes use red / green / amber | Use neutral text, symbols, and attribute icons; color only actual status | -| P2 | `AppHeader.vue` uses red for ordinary task counts; some connection-page icon containers use multiple colors | Make counts neutral; distinguish connection cards by name / engine icon / actual status | -| P2 | Shared Badge defaults to solid primary; some close buttons appear only on hover | Explicitly select neutral variants for non-interactive badges and make close buttons visible on keyboard focus | -| P2 | Narrow windows and reduced-motion behavior have not been fully checked against this specification | Verify and fix affected workflows; do not claim the current client already satisfies every requirement | - -Migrate incrementally by shared component or business area as it changes. Check each change against this specification; establishing the rules does not include a bulk rewrite of business UI. +- Keep the CSS token source, UnoCSS mappings, Monaco adapter, and this specification synchronized. +- Shared badges default to neutral secondary. Connection and transfer status signals use semantic tokens; database attributes, booleans, filter operators, task counts, feature icons, and vendor icon containers remain neutral. +- Theme preview tiles scope the same light/dark tokens instead of maintaining a second miniature palette. +- Automated contrast checks cover text on workspace, chrome, raised, selection, and semantic feedback surfaces, plus foregrounds on solid actions. Editor theme tests cover light/dark switching and token inheritance. +- Continue to verify real content, narrow windows, keyboard focus, system appearance changes, and native window controls on the target operating systems. Theme tests do not replace native-platform interaction checks. diff --git a/eslint.config.js b/eslint.config.js index 9f0f19f9..25b754e5 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -23,6 +23,7 @@ export default antfu({ '**/dist/**', '**/node_modules/**', '**/.tauri/**', + '.impeccable/hook.cache.json', 'AGENTS.md', 'docs/**', 'src-tauri/gen/**', diff --git a/src/__tests__/dataGridPaging.test.ts b/src/__tests__/dataGridPaging.test.ts index 34ee1616..f00440bf 100644 --- a/src/__tests__/dataGridPaging.test.ts +++ b/src/__tests__/dataGridPaging.test.ts @@ -92,13 +92,13 @@ describe('paged result grid', () => { const firstRow = wrapper.get('[data-index="0"]') firstRow.findComponent({ name: 'Checkbox' }).vm.$emit('update:checked', true) await wrapper.vm.$nextTick() - expect(wrapper.get('[data-index="0"]').classes()).toContain('bg-primary/[0.08]') + expect(wrapper.get('[data-index="0"]').classes()).toContain('bg-selection') const scroller = wrapper.find('.overflow-auto').element as HTMLDivElement scroller.scrollTop = 320 wrapper.findComponent({ name: 'PaginationControls' }).vm.$emit('pageChange', 2) await wrapper.vm.$nextTick() expect(scroller.scrollTop).toBe(0) - expect(wrapper.get('[data-index="0"]').classes()).not.toContain('bg-primary/[0.08]') + expect(wrapper.get('[data-index="0"]').classes()).not.toContain('bg-selection') expect(wrapper.get('[data-index="0"]').text()).toContain('135') }) diff --git a/src/__tests__/theme.test.ts b/src/__tests__/theme.test.ts new file mode 100644 index 00000000..5c9b9902 --- /dev/null +++ b/src/__tests__/theme.test.ts @@ -0,0 +1,117 @@ +/** @jest-environment jsdom */ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { applyEditorTheme } from '@/composables/editorTheme' + +const css = readFileSync(join(__dirname, '../assets/index.css'), 'utf8') +const parseTokens = (block: string) => Object.fromEntries( + [...block.matchAll(/--([\w-]+): ([^;]+);/g)].map(([, name, value]) => [name, value]), +) +const themes = { + light: parseTokens(css.slice(css.indexOf('{') + 1, css.indexOf('}'))), + dark: parseTokens(css.match(/\.dark \{([^}]+)\}/)![1]), +} + +const luminance = (hsl: string) => { + const [h, s, l] = hsl.match(/[\d.]+/g)!.map(Number) + const saturation = s / 100 + const light = l / 100 + const chroma = (1 - Math.abs(2 * light - 1)) * saturation + const x = chroma * (1 - Math.abs((h / 60) % 2 - 1)) + const m = light - chroma / 2 + const channels = [ + [chroma, x, 0], + [x, chroma, 0], + [0, chroma, x], + [0, x, chroma], + [x, 0, chroma], + [chroma, 0, x], + ][Math.floor(h / 60) % 6] + return channels.map(value => value + m) + .map(value => value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4) + .reduce((sum, value, index) => sum + value * [0.2126, 0.7152, 0.0722][index], 0) +} + +const contrast = (a: string, b: string) => { + const values = [luminance(a), luminance(b)].sort((a, b) => a - b) + return (values[1] + 0.05) / (values[0] + 0.05) +} + +describe.each(['light', 'dark'] as const)('%s theme', (name) => { + const tokens = themes[name] + + it.each(['background', 'chrome', 'card', 'popover', 'accent', 'selection'])( + 'keeps normal and supporting text readable on %s', + (surface) => { + ;['foreground', 'muted-foreground', 'primary-text', 'destructive-text'].forEach((text) => { + expect(contrast(tokens[text], tokens[surface])).toBeGreaterThanOrEqual(4.5) + }) + }, + ) + + it.each(['primary', 'destructive', 'success', 'warning'])( + 'provides a readable foreground on solid %s', + (role) => { + expect(contrast(tokens[`${role}-foreground`], tokens[role])).toBeGreaterThanOrEqual(4.5) + }, + ) + + it.each(['success', 'warning', 'destructive'])( + 'keeps %s feedback readable on its subdued surface', + (role) => { + const text = role === 'destructive' ? 'destructive-text' : role + expect(contrast(tokens[text], tokens[`${role}-muted`])).toBeGreaterThanOrEqual(4.5) + }, + ) + + it.each(['keyword', 'string', 'number', 'comment'])( + 'keeps SQL %s tokens readable', + (token) => { + expect(contrast(tokens[`syntax-${token}`], tokens.background)).toBeGreaterThanOrEqual(4.5) + }, + ) +}) + +describe('monaco theme integration', () => { + const monaco = { editor: { defineTheme: jest.fn(), setTheme: jest.fn() } } + const applyTheme = (dark: boolean) => { + document.documentElement.removeAttribute('style') + Object.entries(dark ? themes.dark : themes.light).forEach(([key, value]) => { + document.documentElement.style.setProperty(`--${key}`, value) + }) + return applyEditorTheme(monaco as unknown as Parameters[0], dark) + } + + afterEach(() => { + document.documentElement.removeAttribute('style') + jest.clearAllMocks() + }) + + it('coordinates editor, gutter and completion surfaces with the active dark tokens', () => { + expect(applyTheme(true)).toBe('sqlkit-dark') + expect(monaco.editor.defineTheme).toHaveBeenCalledWith('sqlkit-dark', expect.objectContaining({ + base: 'vs-dark', + inherit: true, + colors: expect.objectContaining({ + 'editor.background': '#16181c', + 'editorGutter.background': '#16181c', + 'editor.foreground': '#e4e7ec', + 'editorSuggestWidget.background': '#23262c', + 'editorSuggestWidget.selectedBackground': '#263750', + }), + })) + expect(monaco.editor.setTheme).toHaveBeenLastCalledWith('sqlkit-dark') + }) + + it('rebuilds from the current CSS values when appearance switches in either direction', () => { + applyTheme(true) + applyTheme(false) + expect(monaco.editor.defineTheme).toHaveBeenLastCalledWith('sqlkit-light', expect.objectContaining({ + base: 'vs', + colors: expect.objectContaining({ 'editor.background': '#ffffff' }), + })) + expect(monaco.editor.setTheme).toHaveBeenLastCalledWith('sqlkit-light') + applyTheme(true) + expect(monaco.editor.setTheme).toHaveBeenLastCalledWith('sqlkit-dark') + }) +}) diff --git a/src/assets/index.css b/src/assets/index.css index ae562b1d..d6d1ebbc 100644 --- a/src/assets/index.css +++ b/src/assets/index.css @@ -1,4 +1,6 @@ -:root { +:root, +.theme-preview.light { + color-scheme: light; --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; @@ -10,55 +12,84 @@ --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 46.9%; + --muted-foreground: 213.33 16.82% 41.96%; --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - --success: 142.1 70.6% 35.5%; - --success-foreground: 0 0% 100%; - --success-muted: 138 76% 95%; - --success-border: 140 58% 85%; + --destructive: 0.00 73.71% 41.76%; + --destructive-foreground: 0.00 0.00% 100.00%; + --success: 142.43 71.81% 29.22%; + --success-foreground: 0.00 0.00% 100.00%; + --success-muted: 138.46 76.47% 96.67%; + --success-border: 141.00 78.95% 85.10%; --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 80%; - --ring: 220 13% 60%; + --input: 213.75 13.91% 54.90%; + --ring: 215.38 16.32% 46.86%; --radius: 0.5rem; --chart-1: 12 76% 61%; --chart-2: 173 58% 39%; --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; + --chrome: 210.00 40.00% 98.04%; + --primary-text: 224.28 76.33% 48.04%; + --selection: 220.00 100.00% 95.29%; + --destructive-text: 0.00 73.71% 41.76%; + --destructive-muted: 0.00 85.71% 97.25%; + --destructive-border: 0.00 96.30% 89.41%; + --warning: 35.65 100.00% 27.06%; + --warning-foreground: 0.00 0.00% 100.00%; + --warning-muted: 39.00 100.00% 96.08%; + --warning-border: 39.10 67.94% 74.31%; + --syntax-keyword: 224.28 76.33% 48.04%; + --syntax-string: 23.16 60.64% 36.86%; + --syntax-number: 270.65 39.66% 45.49%; + --syntax-comment: 98.18 12.09% 35.69%; } .dark { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --card: 222.2 47% 8%; - --card-foreground: 210 40% 98%; - --popover: 222.2 47% 8%; - --popover-foreground: 210 40% 98%; - --primary: 217.2 91.2% 45%; - --primary-foreground: 0 0% 100%; - --secondary: 217.2 32.6% 16%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 16%; - --muted-foreground: 215 20.2% 65.1%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 210 40% 98%; - --success: 142.1 70% 45%; - --success-foreground: 0 0% 100%; - --success-muted: 142 35% 16%; - --success-border: 142 30% 25%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 28%; - --ring: 212.7 26.8% 68%; + color-scheme: dark; + --background: 220.00 12.00% 9.80%; + --foreground: 217.50 17.39% 90.98%; + --card: 220.00 11.39% 15.49%; + --card-foreground: 217.50 17.39% 90.98%; + --popover: 220.00 11.39% 15.49%; + --popover-foreground: 217.50 17.39% 90.98%; + --primary: 221.21 83.19% 53.33%; + --primary-foreground: 0.00 0.00% 100.00%; + --secondary: 220.00 11.39% 15.49%; + --secondary-foreground: 217.50 17.39% 90.98%; + --muted: 220.00 11.39% 15.49%; + --muted-foreground: 217.89 11.80% 68.43%; + --accent: 216.92 13.13% 19.41%; + --accent-foreground: 217.50 17.39% 90.98%; + --destructive: 0.00 53.53% 47.25%; + --destructive-foreground: 0.00 0.00% 100.00%; + --success: 143.02 47.25% 64.31%; + --success-foreground: 220.00 12.00% 9.80%; + --success-muted: 148.42 22.89% 16.27%; + --success-border: 141.67 23.08% 30.59%; + --border: 217.50 13.33% 23.53%; + --input: 215.56 11.79% 44.90%; + --ring: 217.89 11.80% 68.43%; --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; + --chrome: 217.50 12.50% 12.55%; + --primary-text: 218.46 100.00% 77.06%; + --selection: 215.71 35.59% 23.14%; + --destructive-text: 4.82 81.16% 72.94%; + --destructive-muted: 352.94 19.10% 17.45%; + --destructive-border: 3.40 29.61% 35.10%; + --warning: 36.61 79.73% 70.98%; + --warning-foreground: 220.00 12.00% 9.80%; + --warning-muted: 36.67 20.45% 17.25%; + --warning-border: 37.36 32.12% 32.35%; + --syntax-keyword: 218.46 100.00% 77.06%; + --syntax-string: 26.40 47.77% 69.22%; + --syntax-number: 264.49 51.58% 81.37%; + --syntax-comment: 112.50 7.55% 58.43%; } * { @@ -99,7 +130,6 @@ button:not(:disabled), @supports not (border-width: 0.5px) { .border { border-width: 1px !important; - opacity: 0.6; } } @@ -127,3 +157,8 @@ button:not(:disabled), scrollbar-width: thin; scrollbar-color: hsl(var(--muted-foreground) / 0.25) transparent; } + +::selection { + background-color: hsl(var(--selection)); + color: hsl(var(--foreground)); +} diff --git a/src/components/DeviceReplaceDialog.vue b/src/components/DeviceReplaceDialog.vue index dbbc822e..f8c5ba76 100644 --- a/src/components/DeviceReplaceDialog.vue +++ b/src/components/DeviceReplaceDialog.vue @@ -224,7 +224,7 @@ function handleOpenChange(open: boolean) { .replace-error { font-size: 13px; - color: hsl(var(--destructive)); + color: hsl(var(--destructive-text)); margin-top: 8px; } diff --git a/src/components/SQLEditor.vue b/src/components/SQLEditor.vue index f664fe54..c6f8debb 100644 --- a/src/components/SQLEditor.vue +++ b/src/components/SQLEditor.vue @@ -269,15 +269,9 @@ defineExpose({ getValue, setValue, handleFormat }) diff --git a/src/composables/editorTheme/index.ts b/src/composables/editorTheme/index.ts new file mode 100644 index 00000000..e65ca00f --- /dev/null +++ b/src/composables/editorTheme/index.ts @@ -0,0 +1,57 @@ +import type * as Monaco from 'monaco-editor/esm/vs/editor/editor.api' + +const colorToHex = (value: string): string => { + const [hue, saturation, lightness] = value.match(/[\d.]+/g)!.map(Number) + const light = lightness / 100 + const amplitude = saturation / 100 * Math.min(light, 1 - light) + const channel = (offset: number) => { + const position = (offset + hue / 30) % 12 + const value = light - amplitude * Math.max(-1, Math.min(position - 3, 9 - position, 1)) + return Math.round(value * 255).toString(16).padStart(2, '0') + } + return `#${channel(0)}${channel(8)}${channel(4)}` +} + +export const applyEditorTheme = (monaco: typeof Monaco, dark: boolean): string => { + const styles = getComputedStyle(document.documentElement) + const color = (token: string) => colorToHex(styles.getPropertyValue(`--${token}`)) + const name = dark ? 'sqlkit-dark' : 'sqlkit-light' + monaco.editor.defineTheme(name, { + base: dark ? 'vs-dark' : 'vs', + inherit: true, + rules: [ + { token: 'keyword', foreground: color('syntax-keyword').slice(1) }, + { token: 'string', foreground: color('syntax-string').slice(1) }, + { token: 'number', foreground: color('syntax-number').slice(1) }, + { token: 'comment', foreground: color('syntax-comment').slice(1) }, + ].flatMap(rule => [rule, { ...rule, token: `${rule.token}.sql` }]), + colors: { + 'editor.background': color('background'), + 'editor.foreground': color('foreground'), + 'editorGutter.background': color('background'), + 'editorLineNumber.foreground': color('muted-foreground'), + 'editorLineNumber.activeForeground': color('foreground'), + 'editor.lineHighlightBackground': color('chrome'), + 'editor.lineHighlightBorder': color('chrome'), + 'editor.selectionBackground': color('selection'), + 'editor.inactiveSelectionBackground': color('accent'), + 'editorCursor.foreground': color('primary-text'), + 'editorIndentGuide.background1': color('border'), + 'editorIndentGuide.activeBackground1': color('input'), + 'editorWidget.background': color('popover'), + 'editorWidget.border': color('border'), + 'editorSuggestWidget.background': color('popover'), + 'editorSuggestWidget.foreground': color('popover-foreground'), + 'editorSuggestWidget.border': color('border'), + 'editorSuggestWidget.selectedBackground': color('selection'), + 'editorHoverWidget.background': color('popover'), + 'editorHoverWidget.foreground': color('popover-foreground'), + 'editorHoverWidget.border': color('border'), + 'editorError.foreground': color('destructive-text'), + 'editorWarning.foreground': color('warning'), + 'focusBorder': color('ring'), + }, + }) + monaco.editor.setTheme(name) + return name +} diff --git a/src/composables/useDatabaseIcon.ts b/src/composables/useDatabaseIcon.ts index 52d33a92..6e6baedb 100644 --- a/src/composables/useDatabaseIcon.ts +++ b/src/composables/useDatabaseIcon.ts @@ -70,73 +70,73 @@ type DatabaseIconConfig = { } const databaseIcons: Record = { - POSTGRESQL: { icon: postgresqlLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - MYSQL: { icon: mysqlLogo, color: 'bg-orange-100 dark:bg-orange-900/30' }, - MARIADB: { icon: mariadbLogo, color: 'bg-purple-100 dark:bg-purple-900/30' }, - SQLITE: { icon: sqliteLogo, color: 'bg-green-100 dark:bg-green-900/30' }, - SQLCIPHER: { icon: sqlcipherLogo, color: 'bg-green-100 dark:bg-green-900/30' }, - SQLSERVER: { icon: sqlserverLogo, color: 'bg-red-100 dark:bg-red-900/30' }, - DUCKDB: { icon: duckdbLogo, color: 'bg-yellow-100 dark:bg-yellow-900/30' }, - CLICKHOUSE: { icon: clickhouseLogo, color: 'bg-red-100 dark:bg-red-900/30' }, - COCKROACHDB: { icon: cockroachdbLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - REDSHIFT: { icon: redshiftLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - YUGABYTEDB: { icon: yugabytedbLogo, color: 'bg-orange-100 dark:bg-orange-900/30' }, - TIMESCALEDB: { icon: timescaledbLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - KINGBASEES: { icon: kingbaseesLogo, color: 'bg-purple-100 dark:bg-purple-900/30' }, - GAUSSDB: { icon: gaussdbLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - HIGHGO: { icon: highgoLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - UXDB: { icon: uxdbLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - OPENGAUSS: { icon: opengaussLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - GBASE8C: { icon: gbaseLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - QUESTDB: { icon: questdbLogo, color: 'bg-green-100 dark:bg-green-900/30' }, - VASTBASE: { icon: vastbaseLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - YASHANDB: { icon: yashandbLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - TIDB: { icon: tidbLogo, color: 'bg-red-100 dark:bg-red-900/30' }, - OCEANBASE: { icon: oceanbaseLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - OCEANBASE_ORACLE: { icon: oceanbaseLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - TDSQL: { icon: tdsqlLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - POLARDB: { icon: polardbLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - DAMENG: { icon: damengLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - DORIS: { icon: dorisLogo, color: 'bg-orange-100 dark:bg-orange-900/30' }, - SELECTDB: { icon: selectdbLogo, color: 'bg-cyan-100 dark:bg-cyan-900/30' }, - STARROCKS: { icon: starrocksLogo, color: 'bg-yellow-100 dark:bg-yellow-900/30' }, - DATABEND: { icon: databendLogo, color: 'bg-orange-100 dark:bg-orange-900/30' }, - GOLDENDB: { icon: goldendbLogo, color: 'bg-yellow-100 dark:bg-yellow-900/30' }, - MANTICORESEARCH: { icon: manticoreLogo, color: 'bg-amber-100 dark:bg-amber-900/30' }, - ORACLE: { icon: oracleLogo, color: 'bg-red-100 dark:bg-red-900/30' }, - DB2: { icon: db2Logo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - H2: { icon: h2Logo, color: 'bg-green-100 dark:bg-green-900/30' }, - SNOWFLAKE: { icon: snowflakeLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - XUGUDB: { icon: xugudbLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - GBASE8A: { icon: gbaseLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - TRINO: { icon: trinoLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - PRESTO: { icon: prestoLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - DERBY: { icon: derbyLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - HIVE: { icon: hiveLogo, color: 'bg-orange-100 dark:bg-orange-900/30' }, - DATABRICKS: { icon: databricksLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - HANA: { icon: hanaLogo, color: 'bg-teal-100 dark:bg-teal-900/30' }, - TERADATA: { icon: teradataLogo, color: 'bg-red-100 dark:bg-red-900/30' }, - VERTICA: { icon: verticaLogo, color: 'bg-green-100 dark:bg-green-900/30' }, - EXASOL: { icon: exasolLogo, color: 'bg-amber-100 dark:bg-amber-900/30' }, - BIGQUERY: { icon: bigqueryLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - INFORMIX: { icon: informixLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - KYLIN: { icon: kylinLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - CASSANDRA: { icon: cassandraLogo, color: 'bg-gray-100 dark:bg-gray-800/30' }, - IRIS: { icon: irisLogo, color: 'bg-teal-100 dark:bg-teal-900/30' }, - ACCESS: { icon: accessLogo, color: 'bg-red-100 dark:bg-red-900/30' }, - FIREBIRD: { icon: firebirdLogo, color: 'bg-purple-100 dark:bg-purple-900/30' }, - RQLITE: { icon: rqliteLogo, color: 'bg-green-100 dark:bg-green-900/30' }, - TURSO: { icon: tursoLogo, color: 'bg-cyan-100 dark:bg-cyan-900/30' }, - TDENGINE: { icon: tdengineLogo, color: 'bg-amber-100 dark:bg-amber-900/30' }, - ALLOYDB: { icon: alloydbLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - CRATEDB: { icon: cratedbLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - CLOUDSQLPG: { icon: cloudsqlpgLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - ENTERPRISEDB: { icon: enterprisedbLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - GREENPLUM: { icon: greenplumLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - MATERIALIZE: { icon: materializeLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - FUJITSUPG: { icon: postgresqlLogo, color: 'bg-blue-100 dark:bg-blue-900/30' }, - SINGLESTOREMEMSQL: { icon: singlestoreLogo, color: 'bg-orange-100 dark:bg-orange-900/30' }, - CLOUDSQLMYSQL: { icon: mysqlLogo, color: 'bg-orange-100 dark:bg-orange-900/30' }, + POSTGRESQL: { icon: postgresqlLogo, color: 'bg-muted' }, + MYSQL: { icon: mysqlLogo, color: 'bg-muted' }, + MARIADB: { icon: mariadbLogo, color: 'bg-muted' }, + SQLITE: { icon: sqliteLogo, color: 'bg-muted' }, + SQLCIPHER: { icon: sqlcipherLogo, color: 'bg-muted' }, + SQLSERVER: { icon: sqlserverLogo, color: 'bg-muted' }, + DUCKDB: { icon: duckdbLogo, color: 'bg-muted' }, + CLICKHOUSE: { icon: clickhouseLogo, color: 'bg-muted' }, + COCKROACHDB: { icon: cockroachdbLogo, color: 'bg-muted' }, + REDSHIFT: { icon: redshiftLogo, color: 'bg-muted' }, + YUGABYTEDB: { icon: yugabytedbLogo, color: 'bg-muted' }, + TIMESCALEDB: { icon: timescaledbLogo, color: 'bg-muted' }, + KINGBASEES: { icon: kingbaseesLogo, color: 'bg-muted' }, + GAUSSDB: { icon: gaussdbLogo, color: 'bg-muted' }, + HIGHGO: { icon: highgoLogo, color: 'bg-muted' }, + UXDB: { icon: uxdbLogo, color: 'bg-muted' }, + OPENGAUSS: { icon: opengaussLogo, color: 'bg-muted' }, + GBASE8C: { icon: gbaseLogo, color: 'bg-muted' }, + QUESTDB: { icon: questdbLogo, color: 'bg-muted' }, + VASTBASE: { icon: vastbaseLogo, color: 'bg-muted' }, + YASHANDB: { icon: yashandbLogo, color: 'bg-muted' }, + TIDB: { icon: tidbLogo, color: 'bg-muted' }, + OCEANBASE: { icon: oceanbaseLogo, color: 'bg-muted' }, + OCEANBASE_ORACLE: { icon: oceanbaseLogo, color: 'bg-muted' }, + TDSQL: { icon: tdsqlLogo, color: 'bg-muted' }, + POLARDB: { icon: polardbLogo, color: 'bg-muted' }, + DAMENG: { icon: damengLogo, color: 'bg-muted' }, + DORIS: { icon: dorisLogo, color: 'bg-muted' }, + SELECTDB: { icon: selectdbLogo, color: 'bg-muted' }, + STARROCKS: { icon: starrocksLogo, color: 'bg-muted' }, + DATABEND: { icon: databendLogo, color: 'bg-muted' }, + GOLDENDB: { icon: goldendbLogo, color: 'bg-muted' }, + MANTICORESEARCH: { icon: manticoreLogo, color: 'bg-muted' }, + ORACLE: { icon: oracleLogo, color: 'bg-muted' }, + DB2: { icon: db2Logo, color: 'bg-muted' }, + H2: { icon: h2Logo, color: 'bg-muted' }, + SNOWFLAKE: { icon: snowflakeLogo, color: 'bg-muted' }, + XUGUDB: { icon: xugudbLogo, color: 'bg-muted' }, + GBASE8A: { icon: gbaseLogo, color: 'bg-muted' }, + TRINO: { icon: trinoLogo, color: 'bg-muted' }, + PRESTO: { icon: prestoLogo, color: 'bg-muted' }, + DERBY: { icon: derbyLogo, color: 'bg-muted' }, + HIVE: { icon: hiveLogo, color: 'bg-muted' }, + DATABRICKS: { icon: databricksLogo, color: 'bg-muted' }, + HANA: { icon: hanaLogo, color: 'bg-muted' }, + TERADATA: { icon: teradataLogo, color: 'bg-muted' }, + VERTICA: { icon: verticaLogo, color: 'bg-muted' }, + EXASOL: { icon: exasolLogo, color: 'bg-muted' }, + BIGQUERY: { icon: bigqueryLogo, color: 'bg-muted' }, + INFORMIX: { icon: informixLogo, color: 'bg-muted' }, + KYLIN: { icon: kylinLogo, color: 'bg-muted' }, + CASSANDRA: { icon: cassandraLogo, color: 'bg-muted' }, + IRIS: { icon: irisLogo, color: 'bg-muted' }, + ACCESS: { icon: accessLogo, color: 'bg-muted' }, + FIREBIRD: { icon: firebirdLogo, color: 'bg-muted' }, + RQLITE: { icon: rqliteLogo, color: 'bg-muted' }, + TURSO: { icon: tursoLogo, color: 'bg-muted' }, + TDENGINE: { icon: tdengineLogo, color: 'bg-muted' }, + ALLOYDB: { icon: alloydbLogo, color: 'bg-muted' }, + CRATEDB: { icon: cratedbLogo, color: 'bg-muted' }, + CLOUDSQLPG: { icon: cloudsqlpgLogo, color: 'bg-muted' }, + ENTERPRISEDB: { icon: enterprisedbLogo, color: 'bg-muted' }, + GREENPLUM: { icon: greenplumLogo, color: 'bg-muted' }, + MATERIALIZE: { icon: materializeLogo, color: 'bg-muted' }, + FUJITSUPG: { icon: postgresqlLogo, color: 'bg-muted' }, + SINGLESTOREMEMSQL: { icon: singlestoreLogo, color: 'bg-muted' }, + CLOUDSQLMYSQL: { icon: mysqlLogo, color: 'bg-muted' }, } export function useDatabaseIcon() { @@ -145,11 +145,11 @@ export function useDatabaseIcon() { } const getDatabaseColor = (type: DatabaseType): string => { - return databaseIcons[type]?.color ?? 'bg-gray-100 dark:bg-gray-900/30' + return databaseIcons[type]?.color ?? 'bg-muted' } const getDatabaseConfig = (type: DatabaseType): DatabaseIconConfig => { - return databaseIcons[type] ?? { icon: sqliteLogo, color: 'bg-gray-100 dark:bg-gray-900/30' } + return databaseIcons[type] ?? { icon: sqliteLogo, color: 'bg-muted' } } return { diff --git a/src/composables/useMonacoEditor.ts b/src/composables/useMonacoEditor.ts index b8773990..fcac9acd 100644 --- a/src/composables/useMonacoEditor.ts +++ b/src/composables/useMonacoEditor.ts @@ -6,6 +6,7 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api' import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker' import { onBeforeUnmount } from 'vue' import { ThemeType, useAppStore } from '@/store/appStore' +import { applyEditorTheme } from './editorTheme' import { hasGrammar } from './sqlCompletion/dialects' import { getCompletionProvider } from './sqlCompletion/provider' import { @@ -101,7 +102,7 @@ export function useMonacoEditor(containerRef: Ref, initialVa editor = monaco.editor.create(containerRef.value, { value: initialValue.value, language: resolveEditorLanguage(options.language || 'sql'), - theme: appStore.uiThemeType === ThemeType.DARK ? 'vs-dark' : 'vs', + theme: applyEditorTheme(monaco, appStore.uiThemeType === ThemeType.DARK), automaticLayout: true, readOnly: options.readOnly || false, minimap: { enabled: options.minimap !== false }, @@ -215,7 +216,7 @@ export function useMonacoEditor(containerRef: Ref, initialVa } const updateTheme = (dark: boolean) => { - monaco.editor.setTheme(dark ? 'vs-dark' : 'vs') + applyEditorTheme(monaco, dark) } const updateLanguage = (id: SQLDialect) => { diff --git a/src/pages/ConnectionsPage.vue b/src/pages/ConnectionsPage.vue index eafeb73a..1f60a415 100644 --- a/src/pages/ConnectionsPage.vue +++ b/src/pages/ConnectionsPage.vue @@ -370,8 +370,8 @@ function getConnectionStatus(connectionId: string | undefined): ConnectionStatus {{ stats.total }}

-
- +
+ @@ -388,8 +388,8 @@ function getConnectionStatus(connectionId: string | undefined): ConnectionStatus {{ stats.sshProfiles }}

-
- +
+
@@ -403,8 +403,8 @@ function getConnectionStatus(connectionId: string | undefined): ConnectionStatus {{ stats.active }}

-
- +
+
@@ -412,22 +412,22 @@ function getConnectionStatus(connectionId: string | undefined): ConnectionStatus
-
+
- +
-

+

{{ t('pages.connections.error.title') }}

-

+

{{ connectError }}

-
@@ -701,7 +701,7 @@ function syncAllProviderModels() { left: 8px; top: 50%; transform: translateY(-50%); - color: hsl(var(--muted-foreground) / 0.6); + color: hsl(var(--muted-foreground)); pointer-events: none; } @@ -721,7 +721,7 @@ function syncAllProviderModels() { } .add-source-search::placeholder { - color: hsl(var(--muted-foreground) / 0.5); + color: hsl(var(--muted-foreground)); } .add-source-list { @@ -953,7 +953,7 @@ function syncAllProviderModels() { font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; - color: hsl(var(--muted-foreground) / 0.6); + color: hsl(var(--muted-foreground)); } .permission-menu-item { @@ -982,7 +982,7 @@ function syncAllProviderModels() { } .permission-menu-item--active .permission-menu-icon { - color: hsl(var(--primary)); + color: hsl(var(--primary-text)); } .permission-menu-label { @@ -990,7 +990,7 @@ function syncAllProviderModels() { } .permission-check { - color: hsl(var(--primary)); + color: hsl(var(--primary-text)); flex-shrink: 0; } diff --git a/src/pages/HistoryPage.vue b/src/pages/HistoryPage.vue index 92351e9a..37b513c4 100644 --- a/src/pages/HistoryPage.vue +++ b/src/pages/HistoryPage.vue @@ -221,7 +221,7 @@ function handleToggleFavorite(id: string) { @@ -229,8 +229,8 @@ function handleToggleFavorite(id: string) {
- diff --git a/src/views/setting/mcp-bridge.vue b/src/views/setting/mcp-bridge.vue index cc73a0a4..7e6174a5 100644 --- a/src/views/setting/mcp-bridge.vue +++ b/src/views/setting/mcp-bridge.vue @@ -76,8 +76,8 @@ const permissionModeDesc = computed(() => { const statusDotClass = computed(() => { if (restartPhase.value === 'shutting-down' || restartPhase.value === 'starting') - return 'bg-yellow-500 animate-pulse' - return status.value.running ? 'bg-green-500' : 'bg-red-500' + return 'bg-warning animate-pulse' + return restartPhase.value === 'failed' ? 'bg-destructive-text' : status.value.running ? 'bg-success' : 'bg-muted-foreground' }) const statusText = computed(() => { @@ -495,7 +495,7 @@ async function onActionToggle(id: string, action: McpAction) { type="button" class="text-xs px-2 py-1 border rounded-md cursor-pointer transition-all" :class="connectionActions(String(conn.id)).includes(action.value) - ? 'border-primary bg-primary/10 text-primary font-medium' + ? 'border-primary bg-primary/10 text-primary-text font-medium' : 'border-input text-muted-foreground hover:border-primary/50'" @click="onActionToggle(String(conn.id), action.value)" > diff --git a/src/views/setting/plan-section.vue b/src/views/setting/plan-section.vue index 20654b77..24046da3 100644 --- a/src/views/setting/plan-section.vue +++ b/src/views/setting/plan-section.vue @@ -97,27 +97,27 @@ const handleLogout = () => runAccountAction(() => entitlementStore.clearSessionS {{ expiryText }} - + {{ t('plan.section.cancelScheduled') }} - + {{ t('plan.section.sessionExpired') }} {{ t('plan.section.deviceActivationFailed') }} {{ t('plan.section.checkFailed') }} {{ t('plan.section.notLoggedIn') }} - @@ -248,17 +248,17 @@ const handleLogout = () => runAccountAction(() => entitlementStore.clearSessionS padding: 1px 8px; border-radius: 999px; background-color: hsl(var(--primary) / 0.12); - color: hsl(var(--primary)); + color: hsl(var(--primary-text)); font-size: 9px; font-weight: 700; white-space: nowrap; } .compare-yes { - color: hsl(var(--primary)); + color: hsl(var(--primary-text)); } .compare-no { - color: hsl(var(--muted-foreground) / 0.55); + color: hsl(var(--muted-foreground)); } diff --git a/src/views/setting/provider-form-dialog.vue b/src/views/setting/provider-form-dialog.vue index 07b157ed..e59bda8f 100644 --- a/src/views/setting/provider-form-dialog.vue +++ b/src/views/setting/provider-form-dialog.vue @@ -317,7 +317,7 @@ watch(() => props.open, (open) => { :placeholder="t('pages.settings.ai.form.namePlaceholder')" :class="{ 'border-destructive': formErrors.name }" /> -

+

{{ formErrors.name }}

@@ -335,7 +335,7 @@ watch(() => props.open, (open) => { 'cursor-not-allowed opacity-60': isApiCompatibilityReadonly, }" /> -

+

{{ formErrors.apiCompatibility }}

@@ -408,7 +408,7 @@ watch(() => props.open, (open) => { :placeholder="t('pages.settings.ai.form.baseUrlPlaceholder')" :class="{ 'border-destructive': formErrors.baseUrl }" /> -

+

{{ formErrors.baseUrl }}

diff --git a/src/views/setting/provider-list.vue b/src/views/setting/provider-list.vue index a2604552..fe8812c4 100644 --- a/src/views/setting/provider-list.vue +++ b/src/views/setting/provider-list.vue @@ -124,7 +124,7 @@ const handleSync = async (id: string) => { >
{{ provider.name }} @@ -218,7 +218,7 @@ const handleSync = async (id: string) => { /> diff --git a/src/components/sidebar/SchemaTree.vue b/src/components/sidebar/SchemaTree.vue index 22fb1325..36141072 100644 --- a/src/components/sidebar/SchemaTree.vue +++ b/src/components/sidebar/SchemaTree.vue @@ -421,8 +421,8 @@ defineExpose({ refresh }) @@ -435,7 +435,7 @@ defineExpose({ refresh }) @dblclick="handleDoubleClick(table, schema)" @contextmenu="handleContextMenu($event, table, schema)" > - + {{ table.name }}
@@ -447,7 +447,7 @@ defineExpose({ refresh }) {{ view.name }}
@@ -477,7 +477,7 @@ defineExpose({ refresh }) - + {{ proc.name }}
@@ -502,7 +502,7 @@ defineExpose({ refresh }) - + {{ fn.name }}
@@ -530,8 +530,8 @@ defineExpose({ refresh })