🐛 fix(ui): 修复数据表头和侧栏滚动显示

- 修复虚拟数据表横向滚动后表头与数据列错位

- 修复亮色主题字段元数据悬浮提示可读性

- 优化 v2 侧栏外部 SQL 菜单和定位入口文案

- 使用 rc-tree 横向滚动宽度估算并加粗侧栏树滚动条
This commit is contained in:
Syngnat
2026-06-02 11:17:22 +08:00
parent 2afddf497b
commit e421662576
7 changed files with 1065 additions and 108 deletions

View File

@@ -501,10 +501,16 @@ describe('DataGrid layout', () => {
expect(source).toContain('type VirtualTableScrollReference = TableReference & {');
expect(source).toContain('const tableRef = useRef<VirtualTableScrollReference | null>(null);');
expect(source).toContain('resolveDataGridHorizontalWheelDelta({');
expect(source).toContain('const virtualHorizontalAlignmentRafRef = useRef<number | null>(null);');
expect(source).toContain('const scheduleVirtualHorizontalWheel = useCallback');
expect(source).toContain('pendingTableHorizontalDeltaRef.current += delta;');
expect(source).toContain('tableHorizontalWheelRafRef.current = requestAnimationFrame');
expect(source).toContain('const scheduleVirtualHorizontalAlignment = useCallback((preferredLeft?: number) => {');
expect(source).toContain('virtualHorizontalElementsRef.current = { tableContainer: null, holderEl: null, innerEl: null, headerEl: null };');
expect(source).toContain('applyVirtualHorizontalOffset(tableContainer, nextLeft, { forceInternalScroll: true });');
expect(source).toContain('}, [horizontalScrollVisible, scheduleVirtualHorizontalAlignment, tableRenderData, tableScrollX, virtualEditingCell]);');
expect(source).toContain('tableInstance.scrollTo({ left: clampedOffset, top: holderEl.scrollTop });');
expect(source).toContain('applyVirtualHorizontalOffset(tableContainer, latestExternalScroll.scrollLeft, { forceInternalScroll: true });');
expect(source).toContain('if (externalSyncRafRef.current !== null)');
expect(source).toContain('externalSyncRafRef.current = requestAnimationFrame');
expect(source).toContain('const scheduleSyncExternalScrollFromTargets = useCallback');

View File

@@ -1921,6 +1921,7 @@ const DataGrid: React.FC<DataGridProps> = ({
const externalSyncRafRef = useRef<number | null>(null);
const tableTargetSyncRafRef = useRef<number | null>(null);
const tableHorizontalWheelRafRef = useRef<number | null>(null);
const virtualHorizontalAlignmentRafRef = useRef<number | null>(null);
const pendingTableHorizontalDeltaRef = useRef(0);
const pendingTableTargetSyncSourceRef = useRef<HTMLElement | null>(null);
const scrollSnapshotRafRef = useRef<number | null>(null);
@@ -6311,7 +6312,7 @@ const DataGrid: React.FC<DataGridProps> = ({
return { holderEl, clampedOffset, currentOffset };
}, [resolveVirtualHorizontalElements, tableScrollX]);
const applyVirtualHorizontalOffset = useCallback((tableContainer: HTMLElement, nextOffset: number) => {
const applyVirtualHorizontalOffset = useCallback((tableContainer: HTMLElement, nextOffset: number, options?: { forceInternalScroll?: boolean }) => {
const synced = syncVirtualHorizontalVisualOffset(tableContainer, nextOffset);
if (!synced) {
return false;
@@ -6319,7 +6320,7 @@ const DataGrid: React.FC<DataGridProps> = ({
const { holderEl, clampedOffset, currentOffset } = synced;
const deltaX = clampedOffset - currentOffset;
if (Math.abs(deltaX) < 0.5) return true;
if (Math.abs(deltaX) < 0.5 && !options?.forceInternalScroll) return true;
const tableInstance = tableRef.current;
if (tableInstance && typeof tableInstance.scrollTo === 'function') {
@@ -6338,6 +6339,34 @@ const DataGrid: React.FC<DataGridProps> = ({
return true;
}, [syncVirtualHorizontalVisualOffset]);
const scheduleVirtualHorizontalAlignment = useCallback((preferredLeft?: number) => {
if (!enableVirtual || !isTableSurfaceActive) return;
if (virtualHorizontalAlignmentRafRef.current !== null) {
cancelAnimationFrame(virtualHorizontalAlignmentRafRef.current);
}
virtualHorizontalAlignmentRafRef.current = requestAnimationFrame(() => {
virtualHorizontalAlignmentRafRef.current = null;
const tableContainer = tableContainerRef.current;
if (!(tableContainer instanceof HTMLElement)) return;
virtualHorizontalElementsRef.current = { tableContainer: null, holderEl: null, innerEl: null, headerEl: null };
const externalScroll = externalHorizontalScrollRef.current;
const nextLeft = Math.max(0, preferredLeft ?? externalScroll?.scrollLeft ?? lastTableScrollLeftRef.current);
const applied = applyVirtualHorizontalOffset(tableContainer, nextLeft, { forceInternalScroll: true });
const resolvedLeft = applied ? readVirtualHorizontalOffset(tableContainer) : nextLeft;
lastTableScrollLeftRef.current = resolvedLeft;
if (externalScroll && Math.abs(externalScroll.scrollLeft - resolvedLeft) > 1) {
externalScroll.scrollLeft = resolvedLeft;
}
lastExternalScrollLeftRef.current = externalScroll?.scrollLeft ?? resolvedLeft;
requestAnimationFrame(() => {
const latestContainer = tableContainerRef.current;
if (!(latestContainer instanceof HTMLElement)) return;
syncVirtualHorizontalVisualOffset(latestContainer, resolvedLeft);
});
});
}, [applyVirtualHorizontalOffset, enableVirtual, isTableSurfaceActive, readVirtualHorizontalOffset, syncVirtualHorizontalVisualOffset]);
const flushVirtualHorizontalWheel = useCallback((tableContainer: HTMLElement) => {
tableHorizontalWheelRafRef.current = null;
const delta = pendingTableHorizontalDeltaRef.current;
@@ -6586,7 +6615,7 @@ const DataGrid: React.FC<DataGridProps> = ({
// 虚拟表格路径:通过合成 WheelEvent 驱动 rc-virtual-list 内部状态,
// rc-table 自动同步 header scrollLeft。
if (enableVirtual && tableContainer instanceof HTMLElement) {
const applied = applyVirtualHorizontalOffset(tableContainer, latestExternalScroll.scrollLeft);
const applied = applyVirtualHorizontalOffset(tableContainer, latestExternalScroll.scrollLeft, { forceInternalScroll: true });
if (applied) {
// WheelEvent 经 rc-virtual-list 处理后状态异步更新,延迟同步 ref
requestAnimationFrame(() => {
@@ -6875,6 +6904,17 @@ const DataGrid: React.FC<DataGridProps> = ({
return () => cancelAnimationFrame(rafId);
}, [isTableSurfaceActive, totalWidth, mergedDisplayData.length, pagination?.total, pagination?.pageSize, recalculateTableMetrics]);
useEffect(() => {
if (!horizontalScrollVisible) return;
scheduleVirtualHorizontalAlignment();
return () => {
if (virtualHorizontalAlignmentRafRef.current !== null) {
cancelAnimationFrame(virtualHorizontalAlignmentRafRef.current);
virtualHorizontalAlignmentRafRef.current = null;
}
};
}, [horizontalScrollVisible, scheduleVirtualHorizontalAlignment, tableRenderData, tableScrollX, virtualEditingCell]);
// 虚拟表列对齐antd 虚拟表 body 使用 <div>+<td>(非 <table>
// 不会自动拉伸列宽到视口。而 header <table> 会被 antd 的 CSS 或 JS
// 设置为 width:100% 自动拉伸。强制 header table 宽度等于 scroll.x

View File

@@ -5,7 +5,12 @@ import { describe, expect, it, vi } from 'vitest';
import DataGridColumnTitle from './DataGridColumnTitle';
vi.mock('antd', () => ({
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
Tooltip: ({ children, title, rootClassName }: { children: React.ReactNode; title?: React.ReactNode; rootClassName?: string }) => (
<>
<div data-tooltip-root-class={rootClassName}>{title}</div>
{children}
</>
),
}));
describe('DataGridColumnTitle', () => {
@@ -50,6 +55,43 @@ describe('DataGridColumnTitle', () => {
expect(markup).toContain('align-items:flex-start');
});
it('keeps column metadata tooltip readable in light theme', () => {
const markup = renderToStaticMarkup(
<DataGridColumnTitle
columnName="auth_type"
columnMeta={{ type: 'tinyint(4)', comment: '认证类型1企业2个人' }}
showColumnType
showColumnComment
metaFontSize={11}
columnMetaHintColor="#595959"
columnMetaTooltipColor="#262626"
darkMode={false}
/>,
);
expect(markup).toContain('data-tooltip-root-class="gn-data-grid-column-meta-tooltip"');
expect(markup).toContain('class="gn-data-grid-column-meta-tooltip-content"');
expect(markup).toContain('color:var(--gn-fg-1, #fff)');
expect(markup).not.toContain('color:#fff');
});
it('keeps the configured warm metadata tooltip color in dark theme', () => {
const markup = renderToStaticMarkup(
<DataGridColumnTitle
columnName="auth_type"
columnMeta={{ type: 'tinyint(4)', comment: '认证类型1企业2个人' }}
showColumnType
showColumnComment
metaFontSize={11}
columnMetaHintColor="rgba(255, 236, 179, 0.98)"
columnMetaTooltipColor="rgba(255, 236, 179, 0.98)"
darkMode
/>,
);
expect(markup).toContain('color:rgba(255, 236, 179, 0.98)');
});
it('renders foreign-key jump affordance when reference target exists', () => {
const markup = renderToStaticMarkup(
<DataGridColumnTitle

View File

@@ -150,22 +150,26 @@ const DataGridColumnTitle: React.FC<DataGridColumnTitleProps> = ({
return titleNode;
}
const tooltipTextColor = darkMode ? columnMetaTooltipColor : 'var(--gn-fg-1, #fff)';
return (
<Tooltip
title={(
<pre
className="gn-data-grid-column-meta-tooltip-content"
style={{
maxHeight: 260,
overflow: 'auto',
margin: 0,
fontSize: 12,
whiteSpace: 'pre-wrap',
color: darkMode ? columnMetaTooltipColor : '#fff',
color: tooltipTextColor,
}}
>
{hoverLines.join('\n')}
</pre>
)}
rootClassName="gn-data-grid-column-meta-tooltip"
styles={{ root: { maxWidth: 640 } }}
{...(!darkMode ? { color: 'rgba(0, 0, 0, 0.82)' } : {})}
>

View File

@@ -7,6 +7,7 @@ import Sidebar, {
buildSidebarTableChildrenForUi,
buildV2SidebarTableSectionedChildren,
buildV2RailConnectionGroups,
estimateV2TreeHorizontalScrollWidth,
filterV2ExplorerTreeByKind,
getV2RailConnectionGroupBadgeText,
hasSidebarLazyChildren,
@@ -176,6 +177,12 @@ vi.mock('../../wailsjs/go/app/App', () => ({
SelectSQLDirectory: mocks.noop,
ListSQLDirectory: mocks.noop,
ReadSQLFile: mocks.noop,
CreateSQLFile: mocks.noop,
CreateSQLDirectory: mocks.noop,
DeleteSQLFile: mocks.noop,
DeleteSQLDirectory: mocks.noop,
RenameSQLFile: mocks.noop,
RenameSQLDirectory: mocks.noop,
JVMProbeCapabilities: mocks.noop,
GetDriverStatusList: mocks.noop,
}));
@@ -368,10 +375,47 @@ describe('Sidebar locate toolbar', () => {
const locateActionIndex = markup.indexOf('data-sidebar-locate-current-tab-action="true"');
expect(markup).toContain('data-sidebar-locate-current-tab-action="true"');
expect(markup).toContain('aria-label="定位当前打开表"');
expect(markup).toContain('aria-label="定位当前标签页"');
expect(locateActionIndex).toBeGreaterThan(externalSqlActionIndex);
});
it('wires external SQL directory file actions to dedicated Wails APIs', () => {
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
const loadTablesSource = source.slice(
source.indexOf('const loadTables = async'),
source.indexOf('const locateObjectInSidebarRef'),
);
expect(source).toContain('CreateSQLFile(directoryPath, name)');
expect(source).toContain('RenameSQLFile(filePath, name)');
expect(source).toContain('DeleteSQLFile(filePath)');
expect(source).toContain('CreateSQLDirectory(directoryPath, name)');
expect(source).toContain('RenameSQLDirectory(directoryPath, name)');
expect(source).toContain('DeleteSQLDirectory(directoryPath)');
expect(source).toContain('refreshGlobalExternalSQLRootNode(false)');
expect(source).toContain("request.objectGroup === 'externalSqlFiles'");
expect(source).toContain('SQL 文件未在外部 SQL 目录中找到');
expect(source).toContain('filePath: data.filePath || undefined');
expect(source).toContain("key: 'add-external-sql-directory'");
expect(source).toContain("key: 'new-external-sql-file'");
expect(source).toContain("key: 'rename-external-sql-file'");
expect(source).toContain("key: 'delete-external-sql-file'");
expect(source).toContain("key: 'new-external-sql-directory'");
expect(source).toContain("key: 'rename-external-sql-directory'");
expect(source).toContain("key: 'delete-external-sql-directory'");
expect(source).toContain('新建 SQL 文件');
expect(source).toContain('重命名 SQL 文件');
expect(source).toContain('确认删除 SQL 文件');
expect(source).toContain('新建目录');
expect(source).toContain('重命名目录');
expect(source).toContain('确认删除目录');
expect(source).toContain('仅支持删除空目录');
expect(source).toContain('文件名不能包含路径分隔符');
expect(source).toContain('目录名不能包含路径分隔符');
expect(loadTablesSource).not.toContain('externalSQLRootNode');
expect(loadTablesSource).not.toContain('dbExternalSQLDirectories');
});
it('keeps the legacy sidebar toolbar on a stable five-column grid layout', () => {
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
const markup = renderToStaticMarkup(<Sidebar />);
@@ -509,14 +553,25 @@ describe('Sidebar locate toolbar', () => {
expect(css).not.toContain('.gn-v2-active-connection-trigger:hover');
});
it('keeps v2 tree status dots circular while truncating only the label text', () => {
it('keeps v2 tree status dots circular while using virtual horizontal scroll for long labels', () => {
const css = readFileSync(new URL('../v2-theme.css', import.meta.url), 'utf8');
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
expect(source).toContain('gn-v2-tree-status is-${status}');
expect(source).toContain('data-sidebar-tree-folder-icon="true"');
expect(source).toContain("overflow: 'hidden'");
expect(source).not.toContain("overflowX: isV2Ui ? 'auto' : 'hidden'");
expect(source).toContain('scrollWidth={isV2Ui ? v2TreeHorizontalScrollWidth : undefined}');
expect(css).toMatch(/\.gn-v2-explorer-tree-shell \{[^}]*overflow: hidden !important;/s);
expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree \{[^}]*width: 100%;[^}]*min-width: 0;/s);
expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list-holder-inner \{[^}]*width: 100%;[^}]*min-width: 100%;/s);
expect(css).not.toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list-holder-inner \{[^}]*width: max-content;/s);
expect(css).not.toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list-holder \{[^}]*overflow-x: auto !important;/s);
expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list-scrollbar-horizontal \{[^}]*height: 12px !important;/s);
expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-list-scrollbar-horizontal \.ant-tree-list-scrollbar-thumb \{[^}]*height: 8px !important;/s);
expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-node-content-wrapper \{[^}]*display: flex !important;/s);
expect(css).toMatch(/\.gn-v2-tree-title\.is-connection \{[^}]*align-items:\s*center;/s);
expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-title \{[^}]*overflow: visible;/s);
expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-title \{[^}]*flex: 1 1 auto;[^}]*overflow: visible;/s);
expect(css).toMatch(/\.gn-v2-explorer-tree-shell \.ant-tree-title > \.gn-v2-tree-title \{[^}]*overflow: visible;/s);
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);
@@ -526,6 +581,32 @@ describe('Sidebar locate toolbar', () => {
expect(css).not.toContain('.gn-v2-tree-connection-meta');
});
it('estimates a v2 tree scroll width only when content is wider than the viewport', () => {
const narrowWidth = estimateV2TreeHorizontalScrollWidth([
{
title: 'front_end_sys',
key: 'db-front-end',
type: 'database',
children: [{
title: 'com_vod_error_file_tmp_with_a_very_long_table_name',
key: 'table-long',
type: 'table',
}],
},
] as any, 260);
const wideWidth = estimateV2TreeHorizontalScrollWidth([
{
title: 'users',
key: 'table-users',
type: 'table',
},
] as any, 900);
expect(narrowWidth).toBeGreaterThan(260);
expect(narrowWidth).toBeLessThanOrEqual(960);
expect(wideWidth).toBeUndefined();
});
it('does not repeat the active connection as an object-tree root in v2', () => {
mocks.state.connections = [{
id: 'conn-local',

File diff suppressed because it is too large Load Diff

View File

@@ -2155,6 +2155,7 @@ body[data-ui-version="v2"] .gn-v2-explorer-tree-shell {
border: 0;
border-radius: 0;
background: transparent;
overflow: hidden !important;
}
body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .sidebar-tree-scroll-content {
@@ -2162,10 +2163,37 @@ body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .sidebar-tree-scroll-conte
}
body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .ant-tree {
width: 100%;
min-width: 0;
font-size: var(--gn-sidebar-tree-font-size, var(--gn-font-size-sm, 12px));
line-height: 1.2;
}
body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .ant-tree-list-holder {
scrollbar-width: thin;
}
body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .ant-tree-list-scrollbar-horizontal {
height: 12px !important;
bottom: 1px !important;
}
body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .ant-tree-list-scrollbar-horizontal .ant-tree-list-scrollbar-thumb {
height: 8px !important;
border-radius: 999px !important;
background: color-mix(in srgb, var(--gn-fg-4) 52%, transparent) !important;
}
body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .ant-tree-list-scrollbar-horizontal .ant-tree-list-scrollbar-thumb:hover,
body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .ant-tree-list-scrollbar-horizontal .ant-tree-list-scrollbar-thumb-moving {
background: color-mix(in srgb, var(--gn-fg-3) 72%, transparent) !important;
}
body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .ant-tree-list-holder-inner {
width: 100%;
min-width: 100%;
}
body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .ant-tree-treenode {
min-height: 26px !important;
padding: 0 6px !important;
@@ -2189,7 +2217,7 @@ body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .ant-tree-node-content-wra
min-width: 0;
min-height: 26px !important;
padding: 0 6px !important;
display: inline-flex !important;
display: flex !important;
align-items: center;
border-radius: 6px !important;
color: var(--gn-fg-3) !important;
@@ -2246,9 +2274,9 @@ body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .gn-v2-tree-folder-icon .a
body[data-ui-version="v2"] .gn-v2-explorer-tree-shell .ant-tree-title {
min-width: 0;
flex: 1 1 auto;
display: inline-flex;
align-items: center;
max-width: 100%;
overflow: visible;
}
@@ -2800,19 +2828,34 @@ body[data-ui-version="v2"] .gn-v2-main-tabs > .ant-tabs-nav {
background: var(--gn-bg-panel-2);
}
body[data-ui-version="v2"] .gn-v2-main-tabs-double > .ant-tabs-nav {
height: 46px;
flex: 0 0 46px;
}
body[data-ui-version="v2"] .gn-v2-main-tabs .ant-tabs-nav-wrap,
body[data-ui-version="v2"] .gn-v2-main-tabs .ant-tabs-nav-list {
min-height: 36px;
}
body[data-ui-version="v2"] .gn-v2-main-tabs-double .ant-tabs-nav-wrap,
body[data-ui-version="v2"] .gn-v2-main-tabs-double .ant-tabs-nav-list {
min-height: 46px;
}
body[data-ui-version="v2"] .gn-v2-main-tabs .ant-tabs-tab {
min-width: 140px;
width: 260px;
min-width: 260px;
max-width: 260px;
height: 36px;
margin: 0 !important;
padding: 0 !important;
}
body[data-ui-version="v2"] .gn-v2-main-tabs-double .ant-tabs-tab {
height: 46px;
}
body[data-ui-version="v2"] .gn-v2-main-tabs .ant-tabs-tab-btn {
width: 100%;
height: 100%;
@@ -2822,10 +2865,94 @@ body[data-ui-version="v2"] .gn-v2-tab-label {
position: relative;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 0 8px 0 10px;
gap: 7px;
}
body[data-ui-version="v2"] .gn-v2-tab-label-content {
min-width: 0;
flex: 1 1 auto;
display: flex;
align-items: center;
gap: 7px;
overflow: hidden;
}
body[data-ui-version="v2"] .gn-v2-tab-label-double {
padding-top: 4px;
padding-bottom: 3px;
}
body[data-ui-version="v2"] .gn-v2-tab-label-double .gn-v2-tab-label-content {
align-items: stretch;
justify-content: flex-start;
flex-direction: column;
gap: 0;
}
body[data-ui-version="v2"] .gn-v2-tab-label-double .gn-v2-tab-label-main {
flex: 0 1 auto;
gap: 5px;
line-height: 17px;
}
body[data-ui-version="v2"] .gn-v2-tab-label-main {
min-width: 0;
flex: 1 1 auto;
display: flex;
align-items: center;
gap: 7px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--gn-fg-2);
font-weight: 700;
line-height: 16px;
}
body[data-ui-version="v2"] .gn-v2-tab-label-secondary {
min-width: 0;
display: block;
overflow: hidden;
color: var(--gn-fg-5);
font-family: var(--gn-font-mono);
font-size: 10px;
font-weight: 650;
line-height: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
body[data-ui-version="v2"] .gn-v2-tab-label-part {
min-width: 0;
flex: 0 0 auto;
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
body[data-ui-version="v2"] .gn-v2-tab-label-part-object {
flex: 1 1 auto;
color: var(--gn-fg-2);
font-weight: 700;
}
body[data-ui-version="v2"] .gn-v2-tab-label-part-connection {
flex: 0 1 92px;
color: var(--gn-fg-2);
font-family: var(--gn-font-mono);
font-weight: 700;
}
body[data-ui-version="v2"] .gn-v2-tab-label-part-host,
body[data-ui-version="v2"] .gn-v2-tab-label-part-database,
body[data-ui-version="v2"] .gn-v2-tab-label-part-schema {
flex: 0 2 auto;
max-width: 112px;
}
body[data-ui-version="v2"] .gn-v2-tab-kind-icon {
display: inline-flex;
color: var(--gn-accent);
@@ -2834,6 +2961,7 @@ body[data-ui-version="v2"] .gn-v2-tab-kind-icon {
body[data-ui-version="v2"] .gn-v2-tab-kind {
height: 17px;
flex: 0 0 auto;
display: inline-flex;
align-items: center;
padding: 0 5px;
@@ -2858,7 +2986,6 @@ body[data-ui-version="v2"] .gn-v2-tab-conn {
body[data-ui-version="v2"] .gn-v2-tab-close {
width: 18px;
height: 18px;
margin-left: auto;
flex: 0 0 18px;
border: none;
border-radius: 4px;
@@ -2877,6 +3004,59 @@ body[data-ui-version="v2"] .gn-v2-tab-close:hover {
color: var(--gn-fg-1);
}
body[data-ui-version="v2"] .gn-v2-tab-context-menu-popup .ant-dropdown-menu {
min-width: 188px;
padding: 6px !important;
border: 0.5px solid var(--gn-br-2) !important;
border-radius: 12px !important;
background: color-mix(in srgb, var(--gn-bg-panel) 94%, transparent) !important;
box-shadow: 0 18px 44px rgba(15, 23, 42, 0.18), 0 0 0 1px rgba(15, 23, 42, 0.04) !important;
backdrop-filter: blur(12px);
}
body[data-ui-version="v2"] .gn-v2-tab-context-menu-popup .ant-dropdown-menu-item {
min-height: 34px !important;
padding: 7px 10px !important;
border-radius: 8px !important;
color: var(--gn-fg-2) !important;
font-size: 13px !important;
font-weight: 650 !important;
}
body[data-ui-version="v2"] .gn-v2-tab-context-menu-popup .ant-dropdown-menu-item:hover {
background: var(--gn-bg-active) !important;
color: var(--gn-fg-1) !important;
}
body[data-ui-version="v2"] .gn-v2-tab-context-menu-popup .ant-dropdown-menu-item-disabled {
color: var(--gn-fg-5) !important;
opacity: 0.5;
}
body[data-ui-version="v2"] .gn-v2-tab-context-menu-popup .ant-dropdown-menu-item-icon {
color: var(--gn-info);
font-size: 14px;
}
body[data-ui-version="v2"] .gn-v2-tab-context-menu-popup .ant-dropdown-menu-item-divider {
margin: 5px 4px !important;
background: var(--gn-br-1) !important;
}
body[data-ui-version="v2"] .gn-v2-tab-context-menu-popup .ant-dropdown-menu-title-content {
display: inline-flex;
align-items: center;
}
body[data-ui-version="v2"] .gn-v2-tab-context-menu-popup .ant-dropdown-menu-item:first-child {
color: var(--gn-fg-1) !important;
background: color-mix(in srgb, var(--gn-info) 8%, transparent) !important;
}
body[data-ui-version="v2"] .gn-v2-tab-context-menu-popup .ant-dropdown-menu-item:first-child:hover {
background: color-mix(in srgb, var(--gn-info) 14%, transparent) !important;
}
/* ─── V2 DataGrid: toolbar, smart filters, table, statusbar ─ */
body[data-ui-version="v2"] .gn-v2-data-viewer {
background: var(--gn-bg-panel);
@@ -4509,15 +4689,72 @@ body[data-ui-version="v2"] .gn-v2-query-results {
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav {
background: var(--gn-bg-panel-2);
padding-left: 8px;
min-height: 38px;
}
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-tab {
min-height: 34px;
padding: 4px 10px !important;
width: auto !important;
min-width: 0 !important;
max-width: 148px !important;
height: 30px !important;
min-height: 30px;
margin: 4px 6px 4px 0 !important;
padding: 0 9px !important;
border: 0.5px solid var(--gn-br-1) !important;
border-radius: 999px !important;
background: color-mix(in srgb, var(--gn-bg-panel) 72%, transparent) !important;
color: var(--gn-fg-3) !important;
align-items: center !important;
justify-content: center !important;
font-size: 14px;
}
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-nav-list {
align-items: stretch;
align-items: center;
width: auto;
}
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-tab-btn {
width: auto !important;
height: 100%;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
font-size: 14px !important;
line-height: 1 !important;
}
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tab-label {
height: 100%;
line-height: 1;
user-select: none;
-webkit-user-select: none;
}
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tab-text {
font-size: 14px;
font-weight: 700;
}
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-tab:hover {
background: var(--gn-bg-hover) !important;
color: var(--gn-fg-1) !important;
}
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-tab.ant-tabs-tab-active {
border-color: color-mix(in srgb, var(--gn-accent) 38%, var(--gn-br-1)) !important;
background: color-mix(in srgb, var(--gn-accent) 10%, var(--gn-bg-panel)) !important;
color: var(--gn-fg-1) !important;
}
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tabs > .ant-tabs-nav .ant-tabs-tab.ant-tabs-tab-active::after {
display: none;
}
body[data-ui-version="v2"] .gn-v2-query-results .query-result-tab-count {
background: color-mix(in srgb, var(--gn-accent) 12%, var(--gn-bg-active));
color: var(--gn-fg-2);
}
body[data-ui-version="v2"] .gn-v2-query-empty,