From 00473e7ac0f81fbe8280f7229a3cc9e47b4267ca Mon Sep 17 00:00:00 2001 From: Syngnat Date: Mon, 13 Jul 2026 09:54:23 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(theme):=20=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5=E7=8A=B6=E6=80=81=E4=B8=8E=20SQL=20=E5=85=B3?= =?UTF-8?q?=E9=94=AE=E5=AD=97=E8=BE=A8=E8=AF=86=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为亮暗主题和内置预设增加独立连接状态语义色 - 强化 Host、数据库状态点及双行页签字段对比度 - 为 Monaco SQL 关键字补充高对比加粗规则 - 增加基础主题与内置预设的对比度回归测试 --- .../src/components/MonacoEditor.theme.test.ts | 66 +++++++++++++++++++ frontend/src/components/MonacoEditor.tsx | 16 ++++- .../Sidebar.locate-toolbar.test.tsx | 4 +- .../src/components/TabManager.hover.test.tsx | 3 + frontend/src/components/TabManager.tsx | 15 ++++- frontend/src/utils/customThemePresets.test.ts | 11 ++++ frontend/src/utils/customThemePresets.ts | 21 ++++-- frontend/src/v2-theme.contrast.test.ts | 61 +++++++++++++++++ frontend/src/v2-theme.css | 62 ++++++++++++----- 9 files changed, 229 insertions(+), 30 deletions(-) create mode 100644 frontend/src/components/MonacoEditor.theme.test.ts create mode 100644 frontend/src/v2-theme.contrast.test.ts diff --git a/frontend/src/components/MonacoEditor.theme.test.ts b/frontend/src/components/MonacoEditor.theme.test.ts new file mode 100644 index 00000000..4fc49d24 --- /dev/null +++ b/frontend/src/components/MonacoEditor.theme.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { BUILTIN_CUSTOM_THEME_PRESETS } from '../utils/customThemePresets'; +import { registerGonaviMonacoThemes } from './MonacoEditor'; + +const readHexProperty = (css: string, property: string): string => { + const escaped = property.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const match = css.match(new RegExp(`${escaped}\\s*:\\s*(#[0-9a-f]{6})`, 'i')); + if (!match?.[1]) throw new Error(`Missing hexadecimal custom property: ${property}`); + return match[1]; +}; + +const relativeLuminance = (hex: string): number => { + const channels = [1, 3, 5].map((offset) => Number.parseInt(hex.slice(offset, offset + 2), 16) / 255); + const [red, green, blue] = channels.map((channel) => ( + channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4 + )); + return 0.2126 * red + 0.7152 * green + 0.0722 * blue; +}; + +const contrastRatio = (foreground: string, background: string): number => { + const light = Math.max(relativeLuminance(foreground), relativeLuminance(background)); + const dark = Math.min(relativeLuminance(foreground), relativeLuminance(background)); + return (light + 0.05) / (dark + 0.05); +}; + +describe('GoNavi Monaco themes', () => { + it('defines bold SQL keyword colors that remain AA-readable across every built-in preset', () => { + const defineTheme = vi.fn(); + registerGonaviMonacoThemes({ editor: { defineTheme } } as never); + + const definitions = new Map( + defineTheme.mock.calls.map(([name, definition]) => [name, definition]), + ); + const keywordTokens = [ + 'keyword.sql', + 'keyword.try.sql', + 'keyword.catch.sql', + 'keyword.block.sql', + 'keyword.choice.sql', + ]; + const lightRules = definitions.get('transparent-light')?.rules ?? []; + const darkRules = definitions.get('transparent-dark')?.rules ?? []; + + for (const token of keywordTokens) { + expect(lightRules.find((rule: any) => rule.token === token)).toMatchObject({ + foreground: '6D28D9', + fontStyle: 'bold', + }); + expect(darkRules.find((rule: any) => rule.token === token)).toMatchObject({ + foreground: 'C792EA', + fontStyle: 'bold', + }); + } + + for (const preset of BUILTIN_CUSTOM_THEME_PRESETS) { + const keyword = (preset.baseMode === 'dark' ? darkRules : lightRules) + .find((rule: any) => rule.token === 'keyword.sql'); + const background = readHexProperty(preset.css, '--gn-bg-input'); + expect( + contrastRatio(`#${keyword.foreground}`, background), + `${preset.id} SQL keyword must contrast with its editor background`, + ).toBeGreaterThanOrEqual(4.5); + } + }); +}); diff --git a/frontend/src/components/MonacoEditor.tsx b/frontend/src/components/MonacoEditor.tsx index 6d0fdf53..6383642a 100644 --- a/frontend/src/components/MonacoEditor.tsx +++ b/frontend/src/components/MonacoEditor.tsx @@ -495,7 +495,13 @@ export const registerGonaviMonacoThemes: BeforeMount = (monaco) => { monaco.editor.defineTheme('transparent-dark', { base: 'vs-dark', inherit: true, - rules: [], + rules: [ + { token: 'keyword.sql', foreground: 'C792EA', fontStyle: 'bold' }, + { token: 'keyword.try.sql', foreground: 'C792EA', fontStyle: 'bold' }, + { token: 'keyword.catch.sql', foreground: 'C792EA', fontStyle: 'bold' }, + { token: 'keyword.block.sql', foreground: 'C792EA', fontStyle: 'bold' }, + { token: 'keyword.choice.sql', foreground: 'C792EA', fontStyle: 'bold' }, + ], colors: { 'editor.background': '#00000000', 'editor.lineHighlightBackground': '#ffffff10', @@ -507,7 +513,13 @@ export const registerGonaviMonacoThemes: BeforeMount = (monaco) => { monaco.editor.defineTheme('transparent-light', { base: 'vs', inherit: true, - rules: [], + rules: [ + { token: 'keyword.sql', foreground: '6D28D9', fontStyle: 'bold' }, + { token: 'keyword.try.sql', foreground: '6D28D9', fontStyle: 'bold' }, + { token: 'keyword.catch.sql', foreground: '6D28D9', fontStyle: 'bold' }, + { token: 'keyword.block.sql', foreground: '6D28D9', fontStyle: 'bold' }, + { token: 'keyword.choice.sql', foreground: '6D28D9', fontStyle: 'bold' }, + ], colors: { 'editor.background': '#00000000', 'editor.lineHighlightBackground': '#00000010', diff --git a/frontend/src/components/Sidebar.locate-toolbar.test.tsx b/frontend/src/components/Sidebar.locate-toolbar.test.tsx index 38f1b79a..dc82b158 100644 --- a/frontend/src/components/Sidebar.locate-toolbar.test.tsx +++ b/frontend/src/components/Sidebar.locate-toolbar.test.tsx @@ -1260,8 +1260,8 @@ describe('Sidebar locate toolbar', () => { expect(v2TreeTitleCss).toContain('min-width: 100%;'); expect(v2TreeTitleCss).toContain('overflow: visible;'); expect(css).toMatch(/\.gn-v2-tree-status \{[^}]*width: 14px;[^}]*height: 14px;[^}]*flex: 0 0 14px;[^}]*overflow: visible;/s); - expect(css).toMatch(/\.gn-v2-tree-status::before \{[^}]*width: 7px;[^}]*height: 7px;[^}]*border-radius: 50%;/s); - expect(css).toMatch(/\.gn-v2-tree-status\.is-success::before \{[^}]*background: #22c55e;[^}]*box-shadow: 0 0 0 4px rgba\(34, 197, 94, 0\.18\);/s); + expect(css).toMatch(/\.gn-v2-tree-status::before \{[^}]*width: 9px;[^}]*height: 9px;[^}]*border: 1\.5px solid var\(--gn-fg-4\);[^}]*border-radius: 50%;/s); + expect(css).toMatch(/\.gn-v2-tree-status\.is-success::before \{[^}]*border: 0;[^}]*background: var\(--gn-status-connected\);[^}]*box-shadow: 0 0 0 3px color-mix\(in srgb, var\(--gn-status-connected\) 22%, transparent\);/s); const treeLabelCss = readCssRuleBlock(css, 'body[data-ui-version="v2"] .gn-v2-tree-label'); expect(treeLabelCss).toContain('flex: 0 0 auto;'); expect(treeLabelCss).toContain('overflow: visible;'); diff --git a/frontend/src/components/TabManager.hover.test.tsx b/frontend/src/components/TabManager.hover.test.tsx index b0968cb6..013b80cf 100644 --- a/frontend/src/components/TabManager.hover.test.tsx +++ b/frontend/src/components/TabManager.hover.test.tsx @@ -289,6 +289,9 @@ describe('TabManager hover info', () => { expect(source).toContain('buildTabDisplayModel(tab, connection, appearance.tabDisplay, t)'); expect(source).toContain('displayModel={displayModel}'); expect(source).toContain('displayModel.primaryParts.map(renderV2TabDisplayPart)'); + expect(source).toContain('renderV2TabSecondaryParts(displayModel.secondaryParts)'); + expect(source).toContain('aria-label={displayModel.secondaryText}'); + expect(source).toContain('className="gn-v2-tab-label-separator" aria-hidden="true">·'); expect(source).toContain("if (part.key === 'kind')"); expect(source).toContain('className="gn-v2-tab-kind"'); expect(source).toContain('hasDoubleLineTabLabel'); diff --git a/frontend/src/components/TabManager.tsx b/frontend/src/components/TabManager.tsx index 3f4d6545..5593b5a6 100644 --- a/frontend/src/components/TabManager.tsx +++ b/frontend/src/components/TabManager.tsx @@ -260,6 +260,13 @@ const renderV2TabDisplayPart = (part: TabDisplayPart) => { ); }; +const renderV2TabSecondaryParts = (parts: TabDisplayPart[]) => parts.map((part, index) => ( + + {index > 0 ? : null} + {renderV2TabDisplayPart(part)} + +)); + const SortableTabLabel: React.FC = ({ tab, displayModel, @@ -304,8 +311,12 @@ const SortableTabLabel: React.FC = ({ : displayModel.primaryText} {showSecondaryLine ? ( - - {displayModel.secondaryText} + + {renderV2TabSecondaryParts(displayModel.secondaryParts)} ) : null} diff --git a/frontend/src/utils/customThemePresets.test.ts b/frontend/src/utils/customThemePresets.test.ts index 4ceca9cd..d07bcda4 100644 --- a/frontend/src/utils/customThemePresets.test.ts +++ b/frontend/src/utils/customThemePresets.test.ts @@ -50,6 +50,9 @@ describe('built-in custom theme presets', () => { expect(preset.css).toContain('--gn-ant-on-primary:'); expect(preset.css).toContain('--gn-settings-card-bg:'); expect(preset.css).toContain('--gn-explain-critical:'); + expect(preset.css).toContain('--gn-status-connected:'); + expect(preset.css).toContain('.gn-v2-tab-label-part-host'); + expect(preset.css).toContain('.gn-v2-tab-label-part-database'); } expect(BUILTIN_CUSTOM_THEME_PRESETS.filter((preset) => preset.baseMode === 'dark')).toHaveLength(4); expect(BUILTIN_CUSTOM_THEME_PRESETS.filter((preset) => preset.baseMode === 'light')).toHaveLength(2); @@ -72,6 +75,7 @@ describe('built-in custom theme presets', () => { it('keeps preset text and solid-button colors at WCAG AA contrast', () => { for (const preset of BUILTIN_CUSTOM_THEME_PRESETS) { const panel = readHexProperty(preset.css, '--gn-bg-panel'); + const panel2 = readHexProperty(preset.css, '--gn-bg-panel-2'); for (const property of [ '--gn-fg-1', '--gn-fg-2', @@ -84,6 +88,7 @@ describe('built-in custom theme presets', () => { '--gn-warn', '--gn-danger', '--gn-purple', + '--gn-status-connected', ]) { const color = readHexProperty(preset.css, property); expect( @@ -91,6 +96,12 @@ describe('built-in custom theme presets', () => { `${preset.id} ${property} must contrast with --gn-bg-panel`, ).toBeGreaterThanOrEqual(4.5); } + for (const property of ['--gn-info', '--gn-accent', '--gn-status-connected']) { + expect( + contrastRatio(readHexProperty(preset.css, property), panel2), + `${preset.id} ${property} must contrast with --gn-bg-panel-2`, + ).toBeGreaterThanOrEqual(4.5); + } const onAccent = readHexProperty(preset.css, '--gn-on-accent'); for (const property of ['--gn-accent', '--gn-accent-2']) { diff --git a/frontend/src/utils/customThemePresets.ts b/frontend/src/utils/customThemePresets.ts index 8714dd1e..67065def 100644 --- a/frontend/src/utils/customThemePresets.ts +++ b/frontend/src/utils/customThemePresets.ts @@ -62,7 +62,7 @@ export type BuiltinCustomThemePreset = CustomThemeDefinition & { }; }; -const BUILTIN_THEME_REVISION = 2026071101; +const BUILTIN_THEME_REVISION = 2026071301; const createBuiltinThemeCss = (id: string, palette: BuiltinThemePalette): string => `/* GoNavi built-in theme: ${id} */ body[data-custom-theme], @@ -107,6 +107,7 @@ body[data-custom-theme][data-ui-version="v2"] { --gn-danger-strong: ${palette.dangerStrong}; --gn-danger-strong-hover: ${palette.dangerHover}; --gn-on-danger: ${palette.onDanger}; + --gn-status-connected: ${palette.mode === 'dark' ? '#4ade80' : '#15803d'}; --gn-purple: ${palette.purple}; --gn-purple-soft: ${palette.purpleSoft}; @@ -217,8 +218,8 @@ body[data-custom-theme][data-ui-version="v2"] .gn-v2-ai-quick-card.tone-purple . body[data-custom-theme][data-ui-version="v2"] .gn-v2-live-dot.is-live, body[data-custom-theme][data-ui-version="v2"] .gn-v2-rail-status.is-live, body[data-custom-theme][data-ui-version="v2"] .gn-v2-tree-status.is-success::before { - background: var(--gn-accent-text, var(--gn-accent)) !important; - box-shadow: 0 0 0 3px var(--gn-accent-soft) !important; + background: var(--gn-status-connected) !important; + box-shadow: 0 0 0 3px color-mix(in srgb, var(--gn-status-connected) 22%, transparent) !important; } body[data-custom-theme][data-ui-version="v2"] .gn-v2-live-dot.is-loading, @@ -227,6 +228,14 @@ body[data-custom-theme][data-ui-version="v2"] .gn-v2-tree-status.is-loading::bef border-top-color: var(--gn-info) !important; } +body[data-custom-theme][data-ui-version="v2"] .gn-v2-tab-label-part-host { + color: var(--gn-info) !important; +} + +body[data-custom-theme][data-ui-version="v2"] .gn-v2-tab-label-part-database { + color: var(--gn-accent) !important; +} + body[data-custom-theme][data-ui-version="v2"] .monaco-editor, body[data-custom-theme][data-ui-version="v2"] .monaco-editor-background, body[data-custom-theme][data-ui-version="v2"] .monaco-editor .margin, @@ -309,7 +318,7 @@ export const BUILTIN_CUSTOM_THEME_PRESETS: readonly BuiltinCustomThemePreset[] = fg1: '#eceff4', fg2: '#d8dee9', fg3: '#c1cad8', fg4: '#9aa7b8', fg5: '#929ead', border1: 'rgba(216, 222, 233, 0.07)', border2: 'rgba(216, 222, 233, 0.12)', border3: 'rgba(216, 222, 233, 0.20)', accent: '#88c0d0', accent2: '#6faabb', accentSoft: 'rgba(136, 192, 208, 0.16)', accentSoftHover: 'rgba(136, 192, 208, 0.24)', accentOutline: 'rgba(136, 192, 208, 0.42)', onAccent: '#17252a', - info: '#81a1c1', infoSoft: 'rgba(129, 161, 193, 0.17)', onInfo: '#17212b', warn: '#ebcb8b', warnSoft: 'rgba(235, 203, 139, 0.16)', danger: '#df858d', dangerStrong: '#a94f59', dangerHover: '#943f49', onDanger: '#ffffff', purple: '#b993b2', purpleSoft: 'rgba(185, 147, 178, 0.17)', + info: '#89add0', infoSoft: 'rgba(137, 173, 208, 0.17)', onInfo: '#17212b', warn: '#ebcb8b', warnSoft: 'rgba(235, 203, 139, 0.16)', danger: '#df858d', dangerStrong: '#a94f59', dangerHover: '#943f49', onDanger: '#ffffff', purple: '#b993b2', purpleSoft: 'rgba(185, 147, 178, 0.17)', shadowSm: '0 1px 2px rgba(20, 24, 31, 0.24)', shadowMd: '0 4px 14px rgba(20, 24, 31, 0.32)', shadowLg: '0 12px 38px rgba(20, 24, 31, 0.42)', shadowCard: '0 0 0 0.5px rgba(216, 222, 233, 0.08), 0 1px 3px rgba(20, 24, 31, 0.25)', kbdBg: '#3b4351', kbdFg: '#d8dee9', }, @@ -340,8 +349,8 @@ export const BUILTIN_CUSTOM_THEME_PRESETS: readonly BuiltinCustomThemePreset[] = hover: 'rgba(67, 59, 49, 0.05)', active: 'rgba(67, 59, 49, 0.09)', selected: 'rgba(47, 125, 104, 0.14)', fg1: '#292722', fg2: '#45413a', fg3: '#655f55', fg4: '#766e63', fg5: '#7a7267', border1: 'rgba(67, 59, 49, 0.08)', border2: 'rgba(67, 59, 49, 0.13)', border3: 'rgba(67, 59, 49, 0.20)', - accent: '#2f7d68', accent2: '#236553', accentSoft: '#dcede6', accentSoftHover: '#cce4da', accentOutline: 'rgba(47, 125, 104, 0.30)', onAccent: '#ffffff', - info: '#3976a8', infoSoft: '#ddeaf4', onInfo: '#ffffff', warn: '#9f5f1d', warnSoft: '#f2e4cf', danger: '#b94a4a', dangerStrong: '#a83c3c', dangerHover: '#923333', onDanger: '#ffffff', purple: '#765b93', purpleSoft: '#eae1f2', + accent: '#2d7864', accent2: '#236553', accentSoft: '#dcede6', accentSoftHover: '#cce4da', accentOutline: 'rgba(45, 120, 100, 0.30)', onAccent: '#ffffff', + info: '#3572a4', infoSoft: '#ddeaf4', onInfo: '#ffffff', warn: '#9f5f1d', warnSoft: '#f2e4cf', danger: '#b94a4a', dangerStrong: '#a83c3c', dangerHover: '#923333', onDanger: '#ffffff', purple: '#765b93', purpleSoft: '#eae1f2', shadowSm: '0 1px 2px rgba(67, 59, 49, 0.07)', shadowMd: '0 4px 14px rgba(67, 59, 49, 0.10)', shadowLg: '0 12px 36px rgba(67, 59, 49, 0.16)', shadowCard: '0 0 0 0.5px rgba(67, 59, 49, 0.10), 0 1px 3px rgba(67, 59, 49, 0.07)', kbdBg: '#ece5d9', kbdFg: '#45413a', }, diff --git a/frontend/src/v2-theme.contrast.test.ts b/frontend/src/v2-theme.contrast.test.ts new file mode 100644 index 00000000..3312f1cd --- /dev/null +++ b/frontend/src/v2-theme.contrast.test.ts @@ -0,0 +1,61 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +const source = readFileSync(new URL('./v2-theme.css', import.meta.url), 'utf8'); + +const readRuleBlock = (selector: string): string => { + const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const match = source.match(new RegExp(`${escaped}\\s*\\{([^}]*)\\}`)); + if (!match?.[1]) throw new Error(`Missing CSS rule: ${selector}`); + return match[1]; +}; + +const readHexProperty = (block: string, property: string): string => { + const escaped = property.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const match = block.match(new RegExp(`${escaped}\\s*:\\s*(#[0-9a-f]{6})`, 'i')); + if (!match?.[1]) throw new Error(`Missing hexadecimal custom property: ${property}`); + return match[1]; +}; + +const relativeLuminance = (hex: string): number => { + const channels = [1, 3, 5].map((offset) => Number.parseInt(hex.slice(offset, offset + 2), 16) / 255); + const [red, green, blue] = channels.map((channel) => ( + channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4 + )); + return 0.2126 * red + 0.7152 * green + 0.0722 * blue; +}; + +const contrastRatio = (foreground: string, background: string): number => { + const light = Math.max(relativeLuminance(foreground), relativeLuminance(background)); + const dark = Math.min(relativeLuminance(foreground), relativeLuminance(background)); + return (light + 0.05) / (dark + 0.05); +}; + +describe('v2 theme connection identity contrast', () => { + it('uses dedicated connected-state tokens and distinct Host/DB tab colors', () => { + const light = readRuleBlock('body[data-ui-version="v2"][data-theme="light"]'); + const dark = readRuleBlock('body[data-ui-version="v2"][data-theme="dark"]'); + const host = readRuleBlock('body[data-ui-version="v2"] .gn-v2-tab-label-part-host'); + const database = readRuleBlock('body[data-ui-version="v2"] .gn-v2-tab-label-part-database'); + const liveDot = readRuleBlock('body[data-ui-version="v2"] .gn-v2-live-dot'); + const treeDot = readRuleBlock('body[data-ui-version="v2"] .gn-v2-tree-status::before'); + + expect(host).toContain('color: var(--gn-info);'); + expect(database).toContain('color: var(--gn-accent);'); + expect(liveDot).toContain('width: 10px;'); + expect(liveDot).toContain('height: 10px;'); + expect(treeDot).toContain('width: 9px;'); + expect(treeDot).toContain('height: 9px;'); + expect(source).toContain('background: var(--gn-status-connected);'); + + for (const [mode, block] of [['light', light], ['dark', dark]] as const) { + const panel2 = readHexProperty(block, '--gn-bg-panel-2'); + for (const property of ['--gn-info', '--gn-accent', '--gn-status-connected']) { + expect( + contrastRatio(readHexProperty(block, property), panel2), + `${mode} ${property} must contrast with --gn-bg-panel-2`, + ).toBeGreaterThanOrEqual(4.5); + } + } + }); +}); diff --git a/frontend/src/v2-theme.css b/frontend/src/v2-theme.css index 59dabdef..7930957a 100644 --- a/frontend/src/v2-theme.css +++ b/frontend/src/v2-theme.css @@ -43,16 +43,17 @@ body[data-ui-version="v2"][data-theme="light"] { --gn-br-2: rgba(15, 23, 42, 0.12); --gn-br-3: rgba(15, 23, 42, 0.18); - --gn-accent: #16a34a; - --gn-accent-2: #15803d; + --gn-accent: #15803d; + --gn-accent-2: #166534; --gn-accent-soft: #dcfce7; - --gn-info: #0284c7; + --gn-info: #0369a1; --gn-info-soft: #e0f2fe; --gn-warn: #d97706; --gn-warn-soft: rgba(245, 158, 11, 0.14); --gn-danger: #dc2626; --gn-danger-strong: #dc2626; --gn-danger-strong-hover: #b91c1c; + --gn-status-connected: #15803d; --gn-shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.05); --gn-shadow-md: 0 4px 14px rgba(15, 23, 42, 0.08); @@ -97,6 +98,7 @@ body[data-ui-version="v2"][data-theme="dark"] { --gn-danger: #f87171; --gn-danger-strong: #dc2626; --gn-danger-strong-hover: #b91c1c; + --gn-status-connected: #4ade80; --gn-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); --gn-shadow-md: 0 4px 14px rgba(0, 0, 0, 0.45); @@ -2073,7 +2075,7 @@ body[data-ui-version="v2"] .gn-v2-rail-status { } body[data-ui-version="v2"] .gn-v2-rail-status.is-live { - background: var(--gn-accent); + background: var(--gn-status-connected); } body[data-ui-version="v2"] .gn-v2-rail-status.is-error { @@ -2150,16 +2152,19 @@ body[data-ui-version="v2"] .gn-v2-active-connection-trigger { } body[data-ui-version="v2"] .gn-v2-live-dot { - width: 7px; - height: 7px; + width: 10px; + height: 10px; + box-sizing: border-box; + border: 1.5px solid var(--gn-fg-4); border-radius: 50%; flex-shrink: 0; - background: var(--gn-fg-5); + background: transparent; } body[data-ui-version="v2"] .gn-v2-live-dot.is-live { - background: #22c55e; - box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.18); + border: 0; + background: var(--gn-status-connected); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--gn-status-connected) 22%, transparent); } body[data-ui-version="v2"] .gn-v2-live-dot.is-loading { @@ -2172,7 +2177,9 @@ body[data-ui-version="v2"] .gn-v2-live-dot.is-loading { } body[data-ui-version="v2"] .gn-v2-live-dot.is-error { + border: 0; background: var(--gn-danger); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--gn-danger) 20%, transparent); } body[data-ui-version="v2"] .gn-v2-active-connection-copy { @@ -2856,16 +2863,19 @@ body[data-ui-version="v2"] .gn-v2-tree-status { body[data-ui-version="v2"] .gn-v2-tree-status::before { content: ''; - width: 7px; - height: 7px; + width: 9px; + height: 9px; + box-sizing: border-box; + border: 1.5px solid var(--gn-fg-4); border-radius: 50%; - flex: 0 0 7px; - background: #b8c0cc; + flex: 0 0 9px; + background: transparent; } body[data-ui-version="v2"] .gn-v2-tree-status.is-success::before { - background: #22c55e; - box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.18); + border: 0; + background: var(--gn-status-connected); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--gn-status-connected) 22%, transparent); } body[data-ui-version="v2"] .gn-v2-tree-status.is-loading::before { @@ -2880,8 +2890,9 @@ body[data-ui-version="v2"] .gn-v2-tree-status.is-loading::before { } body[data-ui-version="v2"] .gn-v2-tree-status.is-error::before { + border: 0; background: var(--gn-danger); - box-shadow: 0 0 0 4px rgba(239, 68, 68, 0.16); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--gn-danger) 20%, transparent); } @keyframes gn-v2-tree-status-spin { @@ -3586,9 +3597,11 @@ body[data-ui-version="v2"] .gn-v2-tab-label-main { body[data-ui-version="v2"] .gn-v2-tab-label-secondary { min-width: 0; - display: block; + display: flex; + align-items: center; + gap: 4px; overflow: hidden; - color: var(--gn-fg-5); + color: var(--gn-fg-4); font-family: var(--gn-font-mono); font-size: 10px; font-weight: 650; @@ -3626,6 +3639,19 @@ body[data-ui-version="v2"] .gn-v2-tab-label-part-schema { max-width: 112px; } +body[data-ui-version="v2"] .gn-v2-tab-label-part-host { + color: var(--gn-info); +} + +body[data-ui-version="v2"] .gn-v2-tab-label-part-database { + color: var(--gn-accent); +} + +body[data-ui-version="v2"] .gn-v2-tab-label-separator { + flex: 0 0 auto; + color: var(--gn-fg-5); +} + body[data-ui-version="v2"] .gn-v2-tab-kind-icon { display: inline-flex; color: var(--gn-accent);