mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-01 03:08:43 +08:00
🐛 fix(theme): 提升连接状态与 SQL 关键字辨识度
- 为亮暗主题和内置预设增加独立连接状态语义色 - 强化 Host、数据库状态点及双行页签字段对比度 - 为 Monaco SQL 关键字补充高对比加粗规则 - 增加基础主题与内置预设的对比度回归测试
This commit is contained in:
66
frontend/src/components/MonacoEditor.theme.test.ts
Normal file
66
frontend/src/components/MonacoEditor.theme.test.ts
Normal file
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
@@ -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;');
|
||||
|
||||
@@ -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">·</span>');
|
||||
expect(source).toContain("if (part.key === 'kind')");
|
||||
expect(source).toContain('className="gn-v2-tab-kind"');
|
||||
expect(source).toContain('hasDoubleLineTabLabel');
|
||||
|
||||
@@ -260,6 +260,13 @@ const renderV2TabDisplayPart = (part: TabDisplayPart) => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderV2TabSecondaryParts = (parts: TabDisplayPart[]) => parts.map((part, index) => (
|
||||
<React.Fragment key={part.key}>
|
||||
{index > 0 ? <span className="gn-v2-tab-label-separator" aria-hidden="true">·</span> : null}
|
||||
{renderV2TabDisplayPart(part)}
|
||||
</React.Fragment>
|
||||
));
|
||||
|
||||
const SortableTabLabel: React.FC<SortableTabLabelProps> = ({
|
||||
tab,
|
||||
displayModel,
|
||||
@@ -304,8 +311,12 @@ const SortableTabLabel: React.FC<SortableTabLabelProps> = ({
|
||||
: displayModel.primaryText}
|
||||
</span>
|
||||
{showSecondaryLine ? (
|
||||
<span className="gn-v2-tab-label-secondary" title={displayModel.secondaryText}>
|
||||
{displayModel.secondaryText}
|
||||
<span
|
||||
className="gn-v2-tab-label-secondary"
|
||||
title={displayModel.secondaryText}
|
||||
aria-label={displayModel.secondaryText}
|
||||
>
|
||||
{renderV2TabSecondaryParts(displayModel.secondaryParts)}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
|
||||
@@ -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']) {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
61
frontend/src/v2-theme.contrast.test.ts
Normal file
61
frontend/src/v2-theme.contrast.test.ts
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user