mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-11 09:13:36 +08:00
🐛 fix(query-editor): 统一主题背景与结果区布局
让 Monaco、SQL 工具栏、数据网格和嵌入日志统一使用当前主题的工作台表面色,并兼容自定义主题覆盖。 统一结果 Tab 的高度、圆角与间距,移除嵌入日志重复标题,将清空日志和隐藏结果区操作并排放到 Tab 栏右侧。 补充主题、工具栏、结果 Tab 和日志面板回归测试。
This commit is contained in:
@@ -714,7 +714,7 @@ const renderDataTableView = () => (
|
||||
onClearFiltersAndSorts={clearAllFiltersAndSorts}
|
||||
/>
|
||||
|
||||
<div ref={containerRef} style={{ flex: 1, overflow: 'hidden', position: 'relative', minHeight: 0, display: 'flex', flexDirection: 'column', background: bgContent, borderRadius: panelRadius, border: `1px solid ${panelFrameColor}`, boxSizing: 'border-box' }}>
|
||||
<div ref={containerRef} style={{ flex: 1, overflow: 'hidden', position: 'relative', minHeight: 0, display: 'flex', flexDirection: 'column', background: `var(--gn-bg-panel, ${bgContent})`, borderRadius: panelRadius, border: `1px solid ${panelFrameColor}`, boxSizing: 'border-box' }}>
|
||||
{contextHolder}
|
||||
{exportProgressModal}
|
||||
<DataGridModals
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { act, create, type ReactTestRenderer } from "react-test-renderer";
|
||||
|
||||
import { I18nProvider } from "../i18n/provider";
|
||||
import { readV2ThemeCss } from "../test/readV2ThemeCss";
|
||||
import LogPanel from "./LogPanel";
|
||||
|
||||
const storeState = {
|
||||
@@ -109,6 +110,8 @@ describe("LogPanel i18n", () => {
|
||||
beforeEach(() => {
|
||||
storeState.sqlLogs = [];
|
||||
storeState.clearSqlLogs.mockClear();
|
||||
storeState.theme = "light";
|
||||
storeState.appearance = { enabled: true, opacity: 1, blur: 0, uiVersion: "legacy" };
|
||||
});
|
||||
|
||||
it("renders log panel chrome in the active language", () => {
|
||||
@@ -198,4 +201,43 @@ describe("LogPanel i18n", () => {
|
||||
});
|
||||
expect(onDiagnoseExecutionError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("omits duplicate log chrome from the embedded log tab", () => {
|
||||
storeState.sqlLogs = [{
|
||||
id: "log-embedded",
|
||||
timestamp: Date.UTC(2026, 6, 30, 8, 0, 0),
|
||||
sql: "SELECT 1",
|
||||
status: "success",
|
||||
duration: 3,
|
||||
}];
|
||||
|
||||
const renderer = renderLogPanel({ variant: "embedded" });
|
||||
const renderedText = textContent(renderer.toJSON());
|
||||
|
||||
expect(renderedText).not.toContain("SQL execution log");
|
||||
expect(renderedText).not.toContain("Track execution status, duration, and errors for quick review.");
|
||||
expect(renderer.root.findAll((node) => node.props?.title === "Clear logs")).toHaveLength(0);
|
||||
expect(renderedText).toContain("SELECT 1");
|
||||
});
|
||||
|
||||
it("uses the shared SQL workbench background for the embedded log surface", () => {
|
||||
storeState.appearance = { enabled: true, opacity: 1, blur: 0, uiVersion: "v2" };
|
||||
|
||||
const renderer = renderLogPanel({ variant: "embedded" });
|
||||
const embeddedPanel = renderer.root.findByProps({ className: "log-panel-embedded" });
|
||||
const scrollPanel = renderer.root.findByProps({ className: "log-panel-scroll" });
|
||||
const css = readV2ThemeCss();
|
||||
const embeddedTableCss = css.slice(
|
||||
css.indexOf('body[data-ui-version="v2"] .log-panel-embedded .log-panel-table .ant-table {'),
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-result-panel-header .query-result-panel-header-title'),
|
||||
);
|
||||
|
||||
expect(embeddedPanel.props.style.background).toBe(
|
||||
"var(--gn-query-workbench-bg, var(--gn-bg-panel-2))",
|
||||
);
|
||||
expect(scrollPanel.props.style.padding).toBe("0 0 12px");
|
||||
expect(embeddedTableCss).toContain(
|
||||
"background: var(--gn-query-workbench-bg, var(--gn-bg-panel-2)) !important;",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,7 +71,6 @@ const LogPanel: React.FC<LogPanelProps> = ({
|
||||
? `rgba(255, 255, 255, ${Math.max(0.28, opacity * 0.48)})`
|
||||
: `rgba(0, 0, 0, ${Math.max(0.18, opacity * 0.36)})`;
|
||||
const isEmbedded = variant === 'embedded';
|
||||
const logCountLabel = sqlLogs.length.toLocaleString();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@@ -118,7 +117,7 @@ const LogPanel: React.FC<LogPanelProps> = ({
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
padding: isEmbedded ? '0 12px 12px' : '8px 10px 10px',
|
||||
padding: isEmbedded ? '0 0 12px' : '8px 10px 10px',
|
||||
}}
|
||||
>
|
||||
{sqlLogs.length === 0 ? (
|
||||
@@ -187,60 +186,18 @@ const LogPanel: React.FC<LogPanelProps> = ({
|
||||
if (isEmbedded) {
|
||||
return (
|
||||
<div
|
||||
className="log-panel-embedded"
|
||||
style={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
background: isV2Ui
|
||||
? 'var(--gn-query-workbench-bg, var(--gn-bg-panel-2))'
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: '0 0 auto',
|
||||
padding: '8px 12px',
|
||||
borderBottom: `1px solid ${panelDividerColor}`,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
minHeight: 40,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 8,
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
background: panelAccentSoftBg,
|
||||
color: panelAccentColor,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<BugOutlined />
|
||||
</div>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: panelPrimaryTextColor }}>
|
||||
{t('log_panel.description')}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: panelMutedTextColor }}>
|
||||
{logCountLabel}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip title={t('log_panel.action.clear')}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<ClearOutlined />}
|
||||
onClick={clearSqlLogs}
|
||||
style={{ color: panelMutedTextColor }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{executionError && (
|
||||
<div style={{ padding: '12px 12px 0' }}>
|
||||
<div style={{
|
||||
|
||||
@@ -25,7 +25,7 @@ const contrastRatio = (foreground: string, background: string): number => {
|
||||
};
|
||||
|
||||
describe('GoNavi Monaco themes', () => {
|
||||
it('defines bold SQL keyword colors that remain AA-readable across every built-in preset', () => {
|
||||
it('defines bold SQL keyword colors that remain AA-readable on the shared workbench surface', () => {
|
||||
const defineTheme = vi.fn();
|
||||
registerGonaviMonacoThemes({ editor: { defineTheme } } as never);
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('GoNavi Monaco themes', () => {
|
||||
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');
|
||||
const background = readHexProperty(preset.css, '--gn-bg-panel-2');
|
||||
expect(
|
||||
contrastRatio(`#${keyword.foreground}`, background),
|
||||
`${preset.id} SQL keyword must contrast with its editor background`,
|
||||
|
||||
@@ -3487,11 +3487,30 @@ describe('QueryEditor external SQL save', () => {
|
||||
expect(textContent(renderer!.toJSON())).not.toContain('结果 1 (2)');
|
||||
});
|
||||
|
||||
it('keeps query result tabs compact, centered, and readable in v2 UI', () => {
|
||||
it('keeps query result tabs flush, full-height, and readable in v2 UI', () => {
|
||||
const source = readFileSync(new URL('./QueryEditorResultsPanel.tsx', import.meta.url), 'utf8');
|
||||
const css = readV2ThemeCss();
|
||||
const resultNavCss = css.slice(
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav {'),
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-extra-content {'),
|
||||
);
|
||||
const resultTabCss = css.slice(
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-tab {'),
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-nav-list {'),
|
||||
);
|
||||
expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-tab {');
|
||||
expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-tab-btn {');
|
||||
expect(resultNavCss).toContain('padding: 0 8px 0 0;');
|
||||
expect(resultNavCss).toContain('min-height: 46px;');
|
||||
expect(resultTabCss).toContain('height: 46px !important;');
|
||||
expect(resultTabCss).toContain('margin: 0 !important;');
|
||||
expect(resultTabCss).toContain('border-radius: 8px !important;');
|
||||
expect(css).toContain([
|
||||
'body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-nav-wrap,',
|
||||
'body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-nav-list {',
|
||||
' min-height: 46px;',
|
||||
'}',
|
||||
].join('\n'));
|
||||
expect(css).toContain('user-select: none;');
|
||||
expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-results .query-result-tab-text {');
|
||||
});
|
||||
@@ -3767,6 +3786,25 @@ describe('QueryEditor external SQL save', () => {
|
||||
const v2Renderer = renderResultsPanel(true);
|
||||
expect(v2Renderer.root.findAll((node) => node.props?.['data-log-panel'] === 'true')).toHaveLength(1);
|
||||
expect(v2Renderer.root.findAll((node) => node.props?.['data-tab-key'] === '__gonavi_sql_execution_log__')).toHaveLength(1);
|
||||
const tabActions = v2Renderer.root.findByProps({ className: 'query-result-panel-tab-actions' });
|
||||
const actionButtons = tabActions.findAll((node) => node.type === 'button');
|
||||
const resultPanelStyles = v2Renderer.root.findAll((node) => node.type === 'style')
|
||||
.map((node) => textContent(node))
|
||||
.join('\n');
|
||||
expect(actionButtons.map((node) => node.props.className)).toEqual([
|
||||
'query-result-panel-clear query-result-panel-tab-action',
|
||||
'query-result-panel-hide query-result-panel-tab-action',
|
||||
]);
|
||||
expect(resultPanelStyles).toContain(
|
||||
'.query-result-panel-tab-actions { display: inline-flex; flex-direction: row;',
|
||||
);
|
||||
expect(resultPanelStyles).toContain(
|
||||
'.query-result-tabs .ant-tabs-extra-content .query-result-panel-tab-action { width: 28px; min-width: 28px; height: 28px !important; min-height: 28px !important; padding: 0 !important;',
|
||||
);
|
||||
act(() => {
|
||||
actionButtons[0].props.onClick();
|
||||
});
|
||||
expect(storeState.clearSqlLogs).toHaveBeenCalledTimes(1);
|
||||
v2Renderer.unmount();
|
||||
|
||||
const emptyV2Renderer = renderResultsPanel(true, 0);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Dropdown, Tabs, Tooltip, message, type MenuProps } from 'antd';
|
||||
import { BugOutlined, CloseOutlined, CopyOutlined, EyeInvisibleOutlined, PushpinOutlined, RobotOutlined } from '@ant-design/icons';
|
||||
import { BugOutlined, ClearOutlined, CloseOutlined, CopyOutlined, EyeInvisibleOutlined, PushpinOutlined, RobotOutlined } from '@ant-design/icons';
|
||||
|
||||
import { useStore } from '../store';
|
||||
import type { EditRowLocator } from '../utils/rowLocator';
|
||||
import type { GridSortInfoItem } from '../utils/dataGridSort';
|
||||
import type { QueryResultPaginationState } from '../utils/queryResultPagination';
|
||||
@@ -164,6 +165,7 @@ const QueryEditorResultsPanel: React.FC<QueryEditorResultsPanelProps> = ({
|
||||
}) => {
|
||||
const i18n = useOptionalI18n();
|
||||
const t = i18n?.t ?? defaultTranslate;
|
||||
const clearSqlLogs = useStore(state => state.clearSqlLogs);
|
||||
const globalHiddenColumns = useGlobalHiddenColumns();
|
||||
const [draggingResultKey, setDraggingResultKey] = useState<string | null>(null);
|
||||
const [detachDragPreview, setDetachDragPreview] = useState<DetachDragPreviewState | null>(null);
|
||||
@@ -696,11 +698,24 @@ const QueryEditorResultsPanel: React.FC<QueryEditorResultsPanelProps> = ({
|
||||
|
||||
const tabsHideButton = (
|
||||
<Tooltip title={hideTooltipTitle}>
|
||||
<Button aria-label={t('query_editor.results_panel.aria.hide')} className="query-result-panel-hide query-result-panel-hide-compact" type="text" size="small" icon={<EyeInvisibleOutlined />} onClick={onHide} />
|
||||
<Button aria-label={t('query_editor.results_panel.aria.hide')} className="query-result-panel-hide query-result-panel-tab-action" type="text" size="small" icon={<EyeInvisibleOutlined />} onClick={onHide} />
|
||||
</Tooltip>
|
||||
);
|
||||
const tabsClearButton = (
|
||||
<Tooltip title={t('log_panel.action.clear')}>
|
||||
<Button aria-label={t('log_panel.action.clear')} className="query-result-panel-clear query-result-panel-tab-action" type="text" size="small" icon={<ClearOutlined />} onClick={clearSqlLogs} />
|
||||
</Tooltip>
|
||||
);
|
||||
const isSqlLogActive = resolvedActiveResultKey === QUERY_EDITOR_SQL_LOG_TAB_KEY;
|
||||
const tabsExtraContent = !activeResultUsesDataGrid
|
||||
? { right: <div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>{tabsHideButton}</div> }
|
||||
? {
|
||||
right: (
|
||||
<div className="query-result-panel-tab-actions">
|
||||
{isSqlLogActive && tabsClearButton}
|
||||
{tabsHideButton}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
@@ -736,7 +751,8 @@ const QueryEditorResultsPanel: React.FC<QueryEditorResultsPanelProps> = ({
|
||||
.query-result-panel-header { flex: 0 0 auto; min-height: 38px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 0 12px; border-bottom: 1px solid rgba(0, 0, 0, 0.06); background: rgba(255, 255, 255, 0.9); }
|
||||
.query-result-panel-header-title { font-size: 13px; font-weight: 600; color: #666; }
|
||||
.query-result-panel-hide { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.query-result-panel-hide-compact { min-width: 28px; padding: 0 6px; justify-content: center; }
|
||||
.query-result-panel-tab-actions { display: inline-flex; flex-direction: row; align-items: center; gap: 4px; }
|
||||
.query-result-tabs .ant-tabs-extra-content .query-result-panel-tab-action { width: 28px; min-width: 28px; height: 28px !important; min-height: 28px !important; padding: 0 !important; display: inline-flex; align-items: center; justify-content: center; }
|
||||
`}</style>
|
||||
<div data-gonavi-close-shortcut-scope="result" className={isV2Ui ? 'gn-v2-query-results' : undefined} style={{ position: 'relative', flex: 1, minHeight: 0, overflow: 'hidden', padding: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
{tabItems.length > 0 ? (
|
||||
|
||||
@@ -32,6 +32,38 @@ describe('QueryEditorToolbar layout', () => {
|
||||
expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-toolbar-action-pair {');
|
||||
});
|
||||
|
||||
it('shares the active theme surface across the SQL toolbar and Monaco editor', () => {
|
||||
const css = readV2ThemeCss();
|
||||
const defaultMonacoCss = css.slice(
|
||||
css.indexOf('body[data-ui-version="v2"]:not([data-custom-theme]) {'),
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-editor {'),
|
||||
);
|
||||
const editorCss = css.slice(
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-editor {'),
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-editor-pane {'),
|
||||
);
|
||||
const toolbarCss = css.slice(
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-toolbar {'),
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-toolbar-main {'),
|
||||
);
|
||||
const toolbarSelectCss = css.slice(
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-toolbar .ant-select-selector {'),
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-toolbar .ant-select-selection-item,'),
|
||||
);
|
||||
const monacoStageCss = css.slice(
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-monaco-stage {'),
|
||||
css.indexOf('body[data-ui-version="v2"] .gn-v2-query-monaco-stage:has('),
|
||||
);
|
||||
|
||||
expect(defaultMonacoCss).toContain('--gn-monaco-bg: var(--gn-bg-panel-2);');
|
||||
expect(editorCss).toContain('--gn-query-workbench-bg: var(--gn-bg-panel-2);');
|
||||
expect(toolbarCss).toContain('background: var(--gn-query-workbench-bg) !important;');
|
||||
expect(toolbarSelectCss).toContain('background: var(--gn-query-workbench-bg) !important;');
|
||||
expect(monacoStageCss).toContain(
|
||||
'background: var(--gn-monaco-bg, var(--gn-query-workbench-bg));',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps run and stop buttons separated in the v2 toolbar action group', () => {
|
||||
const toolbarSource = readFileSync(new URL('./QueryEditorToolbar.tsx', import.meta.url), 'utf8');
|
||||
const css = readV2ThemeCss();
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
/* ─── V2 Query editor ─ */
|
||||
/* Base themes follow the sidebar surface; custom themes keep an explicit Monaco override. */
|
||||
body[data-ui-version="v2"]:not([data-custom-theme]) {
|
||||
--gn-monaco-bg: var(--gn-bg-panel-2);
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-editor {
|
||||
--gn-query-workbench-bg: var(--gn-bg-panel-2);
|
||||
background: var(--gn-bg-app);
|
||||
}
|
||||
|
||||
@@ -95,7 +101,7 @@ body[data-ui-version="v2"] .gn-v2-query-toolbar {
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
background: var(--gn-bg-panel) !important;
|
||||
background: var(--gn-query-workbench-bg) !important;
|
||||
border-bottom: 0.5px solid var(--gn-br-1) !important;
|
||||
}
|
||||
|
||||
@@ -198,6 +204,7 @@ body[data-ui-version="v2"] .gn-v2-query-toolbar-select-icon {
|
||||
body[data-ui-version="v2"] .gn-v2-query-toolbar .ant-select-selector {
|
||||
height: 32px !important;
|
||||
padding: 0 10px !important;
|
||||
background: var(--gn-query-workbench-bg) !important;
|
||||
border-radius: 9px !important;
|
||||
}
|
||||
|
||||
@@ -356,8 +363,7 @@ body[data-ui-version="v2"] .gn-v2-query-monaco-stage {
|
||||
overflow: hidden;
|
||||
border-top: 0.5px solid var(--gn-br-1);
|
||||
border-bottom: 0.5px solid var(--gn-br-1);
|
||||
/* Same token as Monaco surface */
|
||||
background: var(--gn-monaco-bg, var(--gn-bg-panel));
|
||||
background: var(--gn-monaco-bg, var(--gn-query-workbench-bg));
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-monaco-stage:has(.monaco-editor .find-widget.visible:not(.hiddenEditor)) {
|
||||
@@ -367,7 +373,7 @@ body[data-ui-version="v2"] .gn-v2-query-monaco-stage:has(.monaco-editor .find-wi
|
||||
body[data-ui-version="v2"] .gn-v2-query-monaco-shell {
|
||||
min-height: 0;
|
||||
overflow: visible;
|
||||
background: var(--gn-monaco-bg, var(--gn-bg-panel));
|
||||
background: var(--gn-monaco-bg, var(--gn-query-workbench-bg));
|
||||
}
|
||||
|
||||
.gn-query-execution-statusbar {
|
||||
@@ -423,24 +429,25 @@ body[data-ui-version="v2"] .gn-v2-query-results {
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav {
|
||||
box-sizing: border-box;
|
||||
background: var(--gn-bg-panel-2);
|
||||
padding-left: 8px;
|
||||
min-height: 38px;
|
||||
padding: 0 8px 0 0;
|
||||
min-height: 46px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-extra-content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-right: 8px;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-tab {
|
||||
width: auto !important;
|
||||
min-width: 0 !important;
|
||||
max-width: 148px !important;
|
||||
height: 30px !important;
|
||||
min-height: 30px;
|
||||
margin: 4px 6px 4px 0 !important;
|
||||
height: 46px !important;
|
||||
min-height: 46px;
|
||||
margin: 0 !important;
|
||||
padding: 0 9px !important;
|
||||
border: 0.5px solid var(--gn-br-1) !important;
|
||||
border-radius: 8px !important;
|
||||
@@ -451,6 +458,11 @@ body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-n
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-nav-wrap,
|
||||
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-nav-list {
|
||||
min-height: 46px;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-nav-list {
|
||||
align-items: center;
|
||||
width: auto;
|
||||
@@ -503,6 +515,10 @@ body[data-ui-version="v2"] .gn-v2-query-result-panel-header {
|
||||
border-bottom-color: var(--gn-br-1);
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .log-panel-embedded .log-panel-table .ant-table {
|
||||
background: var(--gn-query-workbench-bg, var(--gn-bg-panel-2)) !important;
|
||||
}
|
||||
|
||||
body[data-ui-version="v2"] .gn-v2-query-result-panel-header .query-result-panel-header-title {
|
||||
color: var(--gn-fg-3);
|
||||
}
|
||||
|
||||
@@ -51,11 +51,18 @@ describe('custom theme CSS validation', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('injects --gn-monaco-bg for hand-written themes that only set panel', () => {
|
||||
it('injects --gn-monaco-bg for hand-written themes that only set theme surfaces', () => {
|
||||
const css = 'body[data-custom-theme] {\n --gn-bg-panel: #f7faf8;\n --gn-bg-panel-2: #eef3f0;\n}';
|
||||
const next = ensureCustomThemeMonacoSurfaceVars(css);
|
||||
expect(next).toContain('--gn-monaco-bg: var(--gn-bg-panel-2)');
|
||||
expect(next).toContain('--gn-bg-panel: #f7faf8');
|
||||
});
|
||||
|
||||
it('keeps legacy hand-written themes aligned when they only define panel', () => {
|
||||
const css = 'body[data-custom-theme] {\n --gn-bg-panel: #f7faf8;\n}';
|
||||
const next = ensureCustomThemeMonacoSurfaceVars(css);
|
||||
|
||||
expect(next).toContain('--gn-monaco-bg: var(--gn-bg-panel)');
|
||||
expect(next).toContain('--gn-bg-panel: #f7faf8');
|
||||
});
|
||||
|
||||
it('does not override an explicit --gn-monaco-bg in custom theme CSS', () => {
|
||||
|
||||
@@ -220,19 +220,20 @@ export const createCustomThemeId = (): string => {
|
||||
|
||||
/**
|
||||
* Ensure Monaco surface token exists for hand-written custom themes.
|
||||
* Presets already define it; user CSS that only sets --gn-bg-panel still needs this.
|
||||
* Presets already define it; hand-written CSS still needs the shared theme surface default.
|
||||
* If the theme already declares --gn-monaco-bg, leave it alone (user override wins).
|
||||
*/
|
||||
export const ensureCustomThemeMonacoSurfaceVars = (css: string): string => {
|
||||
const source = String(css || '');
|
||||
if (/--gn-monaco-bg\s*:/.test(source)) return source;
|
||||
const surfaceVar = /--gn-bg-panel-2\s*:/.test(source) ? '--gn-bg-panel-2' : '--gn-bg-panel';
|
||||
const trimmed = source.trimEnd();
|
||||
const appendix = [
|
||||
'',
|
||||
'/* GoNavi: Monaco surface follows panel unless the theme overrides --gn-monaco-bg */',
|
||||
`/* GoNavi: Monaco surface follows ${surfaceVar} unless the theme overrides --gn-monaco-bg */`,
|
||||
'body[data-custom-theme],',
|
||||
'body[data-custom-theme][data-ui-version="v2"] {',
|
||||
' --gn-monaco-bg: var(--gn-bg-panel);',
|
||||
` --gn-monaco-bg: var(${surfaceVar});`,
|
||||
'}',
|
||||
'',
|
||||
].join('\n');
|
||||
@@ -396,7 +397,7 @@ body[data-custom-theme][data-ui-version="v2"] {
|
||||
--gn-bg-chrome: #171a23;
|
||||
--gn-bg-panel: #1d202b;
|
||||
--gn-bg-panel-2: #232733;
|
||||
--gn-monaco-bg: var(--gn-bg-panel);
|
||||
--gn-monaco-bg: var(--gn-bg-panel-2);
|
||||
--gn-bg-hover: rgba(255, 255, 255, 0.06);
|
||||
--gn-bg-active: rgba(255, 255, 255, 0.10);
|
||||
--gn-bg-selected: rgba(139, 92, 246, 0.18);
|
||||
|
||||
@@ -48,11 +48,15 @@ describe('built-in custom theme presets', () => {
|
||||
expect(sanitizeCustomThemeDefinition(preset)).toEqual(expect.objectContaining({ id: preset.id }));
|
||||
expect(preset.css).toContain('--gn-ant-primary:');
|
||||
expect(preset.css).toContain('--gn-ant-on-primary:');
|
||||
expect(preset.css).toContain('--gn-monaco-bg: var(--gn-bg-panel-2);');
|
||||
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(preset.css).toContain(
|
||||
'background-color: var(--gn-monaco-bg, var(--gn-bg-panel-2)) !important;',
|
||||
);
|
||||
}
|
||||
expect(BUILTIN_CUSTOM_THEME_PRESETS.filter((preset) => preset.baseMode === 'dark')).toHaveLength(4);
|
||||
expect(BUILTIN_CUSTOM_THEME_PRESETS.filter((preset) => preset.baseMode === 'light')).toHaveLength(2);
|
||||
@@ -70,6 +74,7 @@ describe('built-in custom theme presets', () => {
|
||||
expect(comfortDark.css).toContain('.gn-v2-query-toolbar-save-action');
|
||||
expect(comfortDark.css).toContain('.gn-v2-ai-panel .ai-logo');
|
||||
expect(comfortDark.css).toContain('.monaco-editor-background');
|
||||
expect(comfortDark.css).not.toContain('background-color: var(--gn-bg-input) !important;');
|
||||
});
|
||||
|
||||
it('keeps preset text and solid-button colors at WCAG AA contrast', () => {
|
||||
|
||||
@@ -254,7 +254,7 @@ body[data-custom-theme][data-ui-version="v2"] {
|
||||
--gn-bg-chrome: ${palette.chrome};
|
||||
--gn-bg-panel: ${palette.panel};
|
||||
--gn-bg-panel-2: ${palette.panel2};
|
||||
--gn-monaco-bg: var(--gn-bg-panel);
|
||||
--gn-monaco-bg: var(--gn-bg-panel-2);
|
||||
--gn-bg-input: ${palette.input};
|
||||
--gn-bg-subtle: ${palette.panel2};
|
||||
--gn-bg-hover: ${palette.hover};
|
||||
@@ -455,7 +455,7 @@ 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,
|
||||
body[data-custom-theme][data-ui-version="v2"] .monaco-editor .sticky-widget {
|
||||
background-color: var(--gn-bg-input) !important;
|
||||
background-color: var(--gn-monaco-bg, var(--gn-bg-panel-2)) !important;
|
||||
}`;
|
||||
|
||||
const createPreset = (
|
||||
|
||||
Reference in New Issue
Block a user