🎨 style(context-menu): 统一工作台右键菜单视觉规范

- 复用表对象菜单的上下文标题、功能分组与快捷键布局
- 统一查询标签、结果标签、工具栏、顶栏、Redis 与 Monaco 菜单
- 补充菜单结构、交互和视觉规范回归测试

Refs #879
This commit is contained in:
Syngnat
2026-08-09 14:10:59 +08:00
parent 0a238c551c
commit bf5a1b7681
12 changed files with 752 additions and 231 deletions

View File

@@ -435,6 +435,10 @@ vi.mock('@ant-design/icons', () => {
ClearOutlined: Icon,
CopyOutlined: Icon,
DiffOutlined: Icon,
ExportOutlined: Icon,
TableOutlined: Icon,
ArrowLeftOutlined: Icon,
ArrowRightOutlined: Icon,
PlayCircleOutlined: Icon,
SaveOutlined: Icon,
FormatPainterOutlined: Icon,
@@ -504,16 +508,26 @@ vi.mock('antd', () => {
Input,
Segmented: () => null,
Form,
Dropdown: ({ children, menu }: any) => (
<>
{children}
{menu?.items?.map((item: any) => (
item?.type === 'divider'
? null
: <button key={item.key} type="button" disabled={item.disabled} onClick={item.onClick}>{item.label}</button>
))}
</>
),
Dropdown: ({ children, menu }: any) => {
const renderMenuItems = (items: any[] = []): React.ReactNode => items.map((item: any) => {
if (item?.type === 'divider') return null;
if (item?.type === 'group') {
return (
<React.Fragment key={item.key}>
<span>{item.label}</span>
{renderMenuItems(item.children)}
</React.Fragment>
);
}
return <button key={item.key} type="button" disabled={item.disabled} onClick={item.onClick}>{item.label}</button>;
});
return (
<>
{children}
{renderMenuItems(menu?.items)}
</>
);
},
Tooltip: ({ children }: any) => <>{children}</>,
Select: () => null,
Tabs: ({ activeKey, items, onChange, tabBarExtraContent }: any) => {

View File

@@ -122,6 +122,7 @@ import {
} from '../utils/resultDiff/viewDataVerify';
import { SQL_EDITOR_AUTO_COMMIT_DELAY_OPTIONS } from './QueryEditorTransactionSettings';
import QueryEditorTransactionToolbar from './QueryEditorTransactionToolbar';
import { decorateV2MonacoContextMenu } from './common/V2ActionMenuPopup';
import QueryEditorToolbar, {
formatQueryExecutionElapsed,
resolveQueryExecutionSpeedIcon,
@@ -4793,6 +4794,28 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
inlineSuggest: { enabled: false },
} : mountedEditorOptions);
if (isV2Ui && typeof editor.onContextMenu === 'function') {
editor.onContextMenu(() => {
const decorateContextMenu = () => {
const connectionName = connectionsRef.current.find(
(connection) => connection.id === currentConnectionIdRef.current,
)?.name;
const contextMeta = [connectionName, currentDbRef.current]
.filter(Boolean)
.join(' · ');
decorateV2MonacoContextMenu(
translate('tab_manager.kind_badge.query'),
contextMeta || translate('query_editor.placeholder.database'),
);
};
// Monaco appends the menu asynchronously after dispatching onContextMenu.
// Retry across the next paint window so the first open is decorated too.
window.setTimeout(decorateContextMenu, 0);
window.setTimeout(decorateContextMenu, 48);
window.setTimeout(decorateContextMenu, 120);
});
}
aiInlineGhostVisibleContextKeyRef.current = editor.createContextKey?.(
QUERY_EDITOR_AI_INLINE_CONTEXT_KEY,
false,
@@ -7630,35 +7653,47 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
};
const formatSettingsMenu: MenuProps['items'] = [
{
key: 'upper',
label: translate('query_editor.format.keyword_upper'),
icon: sqlFormatOptions.keywordCase === 'upper' ? '✓' : undefined,
onClick: () => setSqlFormatOptions({ keywordCase: 'upper' })
},
{
key: 'lower',
label: translate('query_editor.format.keyword_lower'),
icon: sqlFormatOptions.keywordCase === 'lower' ? '✓' : undefined,
onClick: () => setSqlFormatOptions({ keywordCase: 'lower' })
},
{ type: 'divider' },
{
key: 'restore-last-format',
label: translate('query_editor.format.restore_last_format'),
disabled: !tab.formatRestoreSnapshot?.query,
onClick: handleRestoreLastFormat,
},
{ type: 'divider' },
{
key: 'snippet-settings',
label: translate('query_editor.format.snippet_settings'),
onClick: () => window.dispatchEvent(new CustomEvent('gonavi:open-snippet-settings')),
type: 'group',
key: 'format-actions',
label: translate('query_editor.action.format_sql'),
children: [
{
key: 'upper',
label: translate('query_editor.format.keyword_upper'),
icon: sqlFormatOptions.keywordCase === 'upper' ? '✓' : undefined,
onClick: () => setSqlFormatOptions({ keywordCase: 'upper' }),
},
{
key: 'lower',
label: translate('query_editor.format.keyword_lower'),
icon: sqlFormatOptions.keywordCase === 'lower' ? '✓' : undefined,
onClick: () => setSqlFormatOptions({ keywordCase: 'lower' }),
},
{
key: 'restore-last-format',
label: translate('query_editor.format.restore_last_format'),
disabled: !tab.formatRestoreSnapshot?.query,
onClick: handleRestoreLastFormat,
},
],
},
{
key: 'shortcut-settings',
label: translate('query_editor.format.shortcut_settings'),
onClick: () => window.dispatchEvent(new CustomEvent('gonavi:open-shortcut-settings')),
type: 'group',
key: 'format-settings',
label: translate('settings.title'),
children: [
{
key: 'snippet-settings',
label: translate('query_editor.format.snippet_settings'),
onClick: () => window.dispatchEvent(new CustomEvent('gonavi:open-snippet-settings')),
},
{
key: 'shortcut-settings',
label: translate('query_editor.format.shortcut_settings'),
onClick: () => window.dispatchEvent(new CustomEvent('gonavi:open-shortcut-settings')),
},
],
},
];
@@ -10415,60 +10450,73 @@ const QueryEditor: React.FC<{ tab: TabData; isActive?: boolean }> = ({ tab, isAc
), [currentDb, elasticsearchServerMajor, insertElasticsearchConsoleTemplate]);
const saveMoreMenuItems: MenuProps['items'] = [
...(currentSavedQuery && !tab.filePath ? [{
key: 'save-query-as',
label: (
<span>
{translate('query_editor.action.save_as')}
{saveQueryAsShortcutBinding?.enabled && saveQueryAsShortcutBinding.combo && (
<span style={{ marginLeft: 8, color: 'var(--gn-text-muted, #6c757d)', fontSize: 11 }}>
{getShortcutDisplayLabel(saveQueryAsShortcutBinding.combo, activeShortcutPlatform)}
</span>
)}
</span>
),
onClick: handleSaveQueryAs,
}] : []),
{
key: 'rename-query',
label: translate('query_editor.action.rename_query'),
disabled: !!tab.filePath,
onClick: handleRenameQuery,
type: 'group',
key: 'query-actions',
label: translate('tab_manager.kind_badge.query'),
children: [
...(currentSavedQuery && !tab.filePath ? [{
key: 'save-query-as',
label: (
<span className="gn-v2-context-menu-item-title">
{translate('query_editor.action.save_as')}
{saveQueryAsShortcutBinding?.enabled && saveQueryAsShortcutBinding.combo && (
<span className="gn-v2-context-menu-kbd">
{getShortcutDisplayLabel(saveQueryAsShortcutBinding.combo, activeShortcutPlatform)}
</span>
)}
</span>
),
onClick: handleSaveQueryAs,
}] : []),
{
key: 'rename-query',
label: translate('query_editor.action.rename_query'),
disabled: !!tab.filePath,
onClick: handleRenameQuery,
},
{
key: 'export-sql-file',
label: translate('query_editor.action.export_sql_file'),
onClick: () => void handleExportSQLFile(),
},
],
},
{
key: 'export-sql-file',
label: translate('query_editor.action.export_sql_file'),
onClick: () => void handleExportSQLFile(),
},
{ type: 'divider' },
{
key: 'diagnose-query',
label: (
<span>
{translate('app.shortcuts.action.diagnoseQuery.label' as any)}
{diagnoseQueryShortcutBinding?.enabled && diagnoseQueryShortcutBinding.combo && (
<span style={{ marginLeft: 8, color: 'var(--gn-text-muted, #6c757d)', fontSize: 11 }}>
{getShortcutDisplayLabel(diagnoseQueryShortcutBinding.combo, activeShortcutPlatform)}
type: 'group',
key: 'analysis-actions',
label: translate('tab_manager.kind_badge.sql_analysis'),
children: [
{
key: 'diagnose-query',
label: (
<span className="gn-v2-context-menu-item-title">
{translate('app.shortcuts.action.diagnoseQuery.label' as any)}
{diagnoseQueryShortcutBinding?.enabled && diagnoseQueryShortcutBinding.combo && (
<span className="gn-v2-context-menu-kbd">
{getShortcutDisplayLabel(diagnoseQueryShortcutBinding.combo, activeShortcutPlatform)}
</span>
)}
</span>
)}
</span>
),
disabled: !currentConnectionCapabilities.supportsExplainDiagnosis,
onClick: () => openSqlAnalysisWorkbench('diagnose', getCurrentQuery()),
},
{
key: 'show-slow-queries',
label: (
<span>
{translate('app.shortcuts.action.showSlowQueries.label' as any)}
{showSlowQueriesShortcutBinding?.enabled && showSlowQueriesShortcutBinding.combo && (
<span style={{ marginLeft: 8, color: 'var(--gn-text-muted, #6c757d)', fontSize: 11 }}>
{getShortcutDisplayLabel(showSlowQueriesShortcutBinding.combo, activeShortcutPlatform)}
),
disabled: !currentConnectionCapabilities.supportsExplainDiagnosis,
onClick: () => openSqlAnalysisWorkbench('diagnose', getCurrentQuery()),
},
{
key: 'show-slow-queries',
label: (
<span className="gn-v2-context-menu-item-title">
{translate('app.shortcuts.action.showSlowQueries.label' as any)}
{showSlowQueriesShortcutBinding?.enabled && showSlowQueriesShortcutBinding.combo && (
<span className="gn-v2-context-menu-kbd">
{getShortcutDisplayLabel(showSlowQueriesShortcutBinding.combo, activeShortcutPlatform)}
</span>
)}
</span>
)}
</span>
),
onClick: () => openSqlAnalysisWorkbench('slow-query'),
),
onClick: () => openSqlAnalysisWorkbench('slow-query'),
},
],
},
];

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Dropdown, Segmented, Tag, Tabs, Tooltip, message, type MenuProps } from 'antd';
import { BugOutlined, ClearOutlined, CloseOutlined, CopyOutlined, EyeInvisibleOutlined, PushpinOutlined, RobotOutlined } from '@ant-design/icons';
import { ArrowLeftOutlined, ArrowRightOutlined, BugOutlined, ClearOutlined, CloseOutlined, CopyOutlined, DiffOutlined, ExportOutlined, EyeInvisibleOutlined, PushpinOutlined, RobotOutlined, TableOutlined } from '@ant-design/icons';
import { useStore } from '../store';
import type { EditRowLocator } from '../utils/rowLocator';
@@ -22,6 +22,7 @@ import DetachDragPreview, {
} from './DetachDragPreview';
import DataGrid from './DataGrid';
import LogPanel from './LogPanel';
import { renderV2ActionMenuPopup } from './common/V2ActionMenuPopup';
export type OpenResultInWindowPreferred = Partial<Pick<DetachedWindowBounds, 'x' | 'y' | 'width' | 'height'>>;
@@ -496,40 +497,67 @@ const QueryEditorResultsPanel: React.FC<QueryEditorResultsPanelProps> = ({
const hasClosableResult = resultSets.some((item) => !item.pinned);
return [
{
key: result?.pinned ? 'unpin' : 'pin',
label: t(result?.pinned
? 'query_editor.results_panel.menu.unpin'
: 'query_editor.results_panel.menu.pin'),
onClick: () => onResultPinnedChange(key, !result?.pinned),
type: 'group',
key: 'result-actions',
label: t('query_editor.action.results'),
children: [
{
key: result?.pinned ? 'unpin' : 'pin',
icon: <PushpinOutlined />,
label: t(result?.pinned
? 'query_editor.results_panel.menu.unpin'
: 'query_editor.results_panel.menu.pin'),
onClick: () => onResultPinnedChange(key, !result?.pinned),
},
...(onOpenResultInWindow
? [{
key: 'open-in-window',
icon: <ExportOutlined />,
label: t('query_editor.results_panel.menu.open_in_window'),
onClick: () => onOpenResultInWindow(key),
}]
: []),
...(onCompareResult
? [{
key: 'compare-results',
icon: <DiffOutlined />,
label: t('query_editor.results_panel.menu.compare_results'),
disabled: comparableCount < 2,
onClick: () => onCompareResult(key),
}]
: []),
],
},
{
type: 'group',
key: 'close-actions',
label: t('common.close'),
children: [
{ key: 'close-other', icon: <CloseOutlined />, label: t('query_editor.results_panel.menu.close_other'), disabled: !hasClosableOtherResult, onClick: () => onCloseOtherResultTabs(key) },
{ key: 'close-left', icon: <ArrowLeftOutlined />, label: t('query_editor.results_panel.menu.close_left'), disabled: !hasClosableResultToLeft, onClick: () => onCloseResultTabsToLeft(key) },
{ key: 'close-right', icon: <ArrowRightOutlined />, label: t('query_editor.results_panel.menu.close_right'), disabled: !hasClosableResultToRight, onClick: () => onCloseResultTabsToRight(key) },
{ key: 'close-all', icon: <CloseOutlined />, label: t('query_editor.results_panel.menu.close_all'), disabled: !hasClosableResult, onClick: onCloseAllResultTabs },
],
},
{ type: 'divider' as const },
...(onOpenResultInWindow
? [{
key: 'open-in-window',
label: t('query_editor.results_panel.menu.open_in_window'),
onClick: () => onOpenResultInWindow(key),
}, { type: 'divider' as const }]
: []),
...(onCompareResult
? [{
key: 'compare-results',
label: t('query_editor.results_panel.menu.compare_results'),
disabled: comparableCount < 2,
onClick: () => onCompareResult(key),
}, { type: 'divider' as const }]
: []),
{ key: 'close-other', label: t('query_editor.results_panel.menu.close_other'), disabled: !hasClosableOtherResult, onClick: () => onCloseOtherResultTabs(key) },
{ key: 'close-left', label: t('query_editor.results_panel.menu.close_left'), disabled: !hasClosableResultToLeft, onClick: () => onCloseResultTabsToLeft(key) },
{ key: 'close-right', label: t('query_editor.results_panel.menu.close_right'), disabled: !hasClosableResultToRight, onClick: () => onCloseResultTabsToRight(key) },
{ type: 'divider' },
{ key: 'close-all', label: t('query_editor.results_panel.menu.close_all'), disabled: !hasClosableResult, onClick: onCloseAllResultTabs },
];
}
const resultTabItems = resultSets.map((rs, idx) => ({
key: rs.key,
label: (
<Dropdown menu={{ items: buildResultTabMenuItems(rs.key, idx) }} trigger={['contextMenu']} rootClassName={isV2Ui ? 'gn-v2-tab-context-menu-popup' : undefined}>
<Dropdown
menu={{ items: buildResultTabMenuItems(rs.key, idx) }}
trigger={['contextMenu']}
rootClassName={isV2Ui ? 'gn-v2-tab-context-menu-popup' : undefined}
popupRender={(menu) => renderV2ActionMenuPopup(menu, isV2Ui, {
title: rs.resultType === 'message'
? t('query_editor.results_panel.tab.message', { index: idx + 1 })
: t('query_editor.results_panel.tab.result', { index: idx + 1 }),
meta: Array.isArray(rs.rows) ? `${rs.rows.length} × ${rs.columns.length}` : currentDb,
icon: <TableOutlined />,
badge: currentDb || undefined,
})}
>
<div
className={`query-result-tab-label${onOpenResultInWindow ? ' is-detachable' : ''}${draggingResultKey === rs.key ? ' is-dragging-detach' : ''}`}
title={onOpenResultInWindow ? t('query_editor.results_panel.menu.open_in_window') : undefined}

View File

@@ -32,6 +32,47 @@ describe('QueryEditorToolbar layout', () => {
expect(css).toContain('body[data-ui-version="v2"] .gn-v2-query-toolbar-action-pair {');
});
it('uses the table context-menu density across v2 action menus', () => {
const css = readV2ThemeCss();
const baseTokensCss = css.slice(
css.indexOf('body[data-ui-version="v2"] {'),
css.indexOf('body[data-ui-version="v2"][data-platform="darwin"] {'),
);
const dropdownItemCss = css.slice(
css.indexOf('/* Compact action menus share the table context-menu geometry. */'),
css.indexOf('/* Icon + label share one baseline/vertical center'),
);
const titlebarMenuCss = css.slice(
css.indexOf('/* Title-bar more menu: leaf rows and submenu rows share the same hover fill + alignment */'),
css.indexOf('body[data-ui-version="v2"] .gn-v2-titlebar-quick-dropdown .ant-dropdown-menu-item:hover'),
);
const tabMenuCss = css.slice(
css.indexOf('body[data-ui-version="v2"] .gn-v2-tab-context-menu-popup .ant-dropdown-menu {'),
css.indexOf('/* ─── V2 DataGrid: toolbar, smart filters, table, statusbar ─ */'),
);
const tableMenuItemCss = css.slice(
css.indexOf('body[data-ui-version="v2"] .gn-v2-context-menu-item {'),
css.indexOf('body[data-ui-version="v2"] .gn-v2-context-menu-item:hover {'),
);
const monacoMenuCss = css.slice(
css.indexOf('/* Monaco context menu follows the same compact action-menu surface. */'),
css.indexOf('/* Nested dropdown menus: collapse double border, no glow/halo */'),
);
expect(baseTokensCss).toContain('--gn-v2-menu-row-height: 28px;');
expect(baseTokensCss).toContain('--gn-v2-menu-item-radius: 5px;');
expect(dropdownItemCss).toContain('height: var(--gn-v2-menu-row-height) !important;');
expect(dropdownItemCss).toContain('min-height: var(--gn-v2-menu-row-height) !important;');
expect(dropdownItemCss).toContain('box-sizing: border-box !important;');
expect(dropdownItemCss).toContain('padding: 4px 8px !important;');
expect(titlebarMenuCss).toContain('min-height: var(--gn-v2-menu-row-height) !important;');
expect(tabMenuCss).not.toContain('min-height: 34px');
expect(tabMenuCss).not.toContain('.ant-dropdown-menu-item:first-child');
expect(tableMenuItemCss).toContain('height: var(--gn-v2-menu-row-height);');
expect(monacoMenuCss).toContain('min-height: var(--gn-v2-menu-row-height) !important;');
expect(monacoMenuCss).toContain('background: var(--gn-bg-panel) !important;');
});
it('shares the active theme surface across the SQL toolbar and Monaco editor', () => {
const css = readV2ThemeCss();
const defaultMonacoCss = css.slice(
@@ -268,4 +309,20 @@ describe('QueryEditorToolbar layout', () => {
expect(css).toContain('text-overflow: ellipsis;');
expect(css).toContain('white-space: nowrap;');
});
it('uses the table context-menu visual grammar for every v2 action popup', () => {
const toolbarSource = readFileSync(new URL('./QueryEditorToolbar.tsx', import.meta.url), 'utf8');
const queryEditorSource = readFileSync(new URL('./QueryEditor.tsx', import.meta.url), 'utf8');
const sharedPopupSource = readFileSync(new URL('./common/V2ActionMenuPopup.tsx', import.meta.url), 'utf8');
const css = readV2ThemeCss();
expect(toolbarSource).toContain('renderV2ActionMenuPopup');
expect(queryEditorSource).toContain('decorateV2MonacoContextMenu');
expect(sharedPopupSource).toContain('gn-v2-context-menu-header gn-v2-action-menu-header');
expect(sharedPopupSource).toContain('gn-v2-context-menu-engine-pill');
expect(css).toContain('.gn-v2-action-menu-surface {');
expect(css).toContain('.gn-v2-action-menu-body .ant-dropdown-menu-item-group-title {');
expect(css).toContain('.gn-v2-action-menu-body .ant-dropdown-menu-item:not(:has(.ant-dropdown-menu-item-icon))::before');
expect(css).toContain('.monaco-menu > .gn-v2-monaco-context-menu-header {');
});
});

View File

@@ -26,6 +26,7 @@ import {
import QueryEditorTransactionSettings, {
type SqlEditorCommitMode,
} from "./QueryEditorTransactionSettings";
import { renderV2ActionMenuPopup } from './common/V2ActionMenuPopup';
export type QueryEditorMode = "sql" | "elasticsearch";
@@ -304,6 +305,11 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
? t("query_editor.elasticsearch.action.ai")
: `AI · ${t("query_editor.action.more")}`;
const formatSettingsTitle = `${t("query_editor.action.format_sql")} · ${t("settings.title")}`;
const activeConnectionLabel = queryCapableConnections.find(
(connection) => connection.id === currentConnectionId,
)?.name;
const menuContextMeta = [activeConnectionLabel, currentDb].filter(Boolean).join(' · ')
|| t('tab_manager.kind_badge.query');
const aiMenuItems: MenuProps["items"] = isElasticsearchMode
? [
{
@@ -350,16 +356,20 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
const moreMenuItems: MenuProps["items"] = isV2Ui
? [
...baseMoreMenuItems,
...(baseMoreMenuItems.length > 0 ? [{ type: "divider" as const }] : []),
{
key: "toggle-result-panel",
label: toggleResultPanelTitle,
icon: isResultPanelVisible ? (
<EyeInvisibleOutlined />
) : (
<EyeOutlined />
),
onClick: onToggleResultPanelVisibility,
type: 'group',
key: 'result-visibility',
label: t('query_editor.action.results'),
children: [{
key: "toggle-result-panel",
label: toggleResultPanelTitle,
icon: isResultPanelVisible ? (
<EyeInvisibleOutlined />
) : (
<EyeOutlined />
),
onClick: onToggleResultPanelVisibility,
}],
},
]
: baseMoreMenuItems;
@@ -437,11 +447,17 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
open={isV2Ui && openToolbarMenu === "templates" ? false : undefined}
>
<span className={isV2Ui ? "gn-v2-query-toolbar-menu-trigger" : undefined}>
<Dropdown
menu={{ items: templateMenuItems }}
placement="bottomLeft"
trigger={["click"]}
open={isV2Ui ? openToolbarMenu === "templates" : undefined}
<Dropdown
menu={{ items: templateMenuItems }}
placement="bottomLeft"
trigger={["click"]}
rootClassName={isV2Ui ? 'gn-v2-action-menu-popup-host' : undefined}
popupRender={(menu) => renderV2ActionMenuPopup(menu, isV2Ui, {
title: t('query_editor.elasticsearch.action.templates'),
meta: menuContextMeta,
icon: <DownOutlined />,
})}
open={isV2Ui ? openToolbarMenu === "templates" : undefined}
onOpenChange={isV2Ui ? (open) => updateToolbarMenuOpen("templates", open) : undefined}
>
<Button
@@ -618,6 +634,13 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
menu={{ items: aiMenuItems }}
placement="bottomRight"
trigger={["click"]}
rootClassName={isV2Ui ? 'gn-v2-action-menu-popup-host' : undefined}
popupRender={(menu) => renderV2ActionMenuPopup(menu, isV2Ui, {
title: aiMoreTitle,
meta: menuContextMeta,
icon: <RobotOutlined />,
badge: 'AI',
})}
open={isV2Ui ? openToolbarMenu === "ai" : undefined}
onOpenChange={isV2Ui ? (open) => updateToolbarMenuOpen("ai", open) : undefined}
>
@@ -659,6 +682,13 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
menu={{ items: aiMenuItems }}
placement="bottomRight"
trigger={["click"]}
rootClassName={isV2Ui ? 'gn-v2-action-menu-popup-host' : undefined}
popupRender={(menu) => renderV2ActionMenuPopup(menu, isV2Ui, {
title: aiMoreTitle,
meta: menuContextMeta,
icon: <RobotOutlined />,
badge: 'AI',
})}
open={isV2Ui ? openToolbarMenu === "ai" : undefined}
onOpenChange={isV2Ui ? (open) => updateToolbarMenuOpen("ai", open) : undefined}
>
@@ -683,6 +713,13 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
menu={{ items: moreMenuItems }}
placement="bottomRight"
trigger={["click"]}
rootClassName={isV2Ui ? 'gn-v2-action-menu-popup-host' : undefined}
popupRender={(menu) => renderV2ActionMenuPopup(menu, isV2Ui, {
title: t('query_editor.action.more'),
meta: menuContextMeta,
icon: <EllipsisOutlined />,
badge: t('tab_manager.kind_badge.query'),
})}
open={isV2Ui ? openToolbarMenu === "more" : undefined}
onOpenChange={isV2Ui ? (open) => updateToolbarMenuOpen("more", open) : undefined}
>
@@ -766,6 +803,13 @@ const QueryEditorToolbar: React.FC<QueryEditorToolbarProps> = ({
menu={{ items: formatSettingsMenu }}
placement="bottomRight"
trigger={["click"]}
rootClassName={isV2Ui ? 'gn-v2-action-menu-popup-host' : undefined}
popupRender={(menu) => renderV2ActionMenuPopup(menu, isV2Ui, {
title: formatSettingsTitle,
meta: menuContextMeta,
icon: <FormatPainterOutlined />,
badge: t('tab_manager.kind_badge.query'),
})}
open={isV2Ui ? openToolbarMenu === "format" : undefined}
onOpenChange={isV2Ui ? (open) => updateToolbarMenuOpen("format", open) : undefined}
>

View File

@@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import RedisViewer from './RedisViewer';
const appCss = readFileSync(new URL('../App.css', import.meta.url), 'utf8');
const redisViewerSource = readFileSync(new URL('./RedisViewer.tsx', import.meta.url), 'utf8');
const storeState = vi.hoisted(() => ({
connections: [
@@ -204,6 +205,18 @@ const findFirstLeafNode = (nodes: any[]): any | null => {
};
describe('RedisViewer tree interactions', () => {
it('uses the shared compact context-menu item class for v2 key actions', () => {
const contextMenuStart = redisViewerSource.indexOf('{treeContextMenu && typeof document');
const contextMenuSource = redisViewerSource.slice(
contextMenuStart,
redisViewerSource.indexOf('), document.body)}', contextMenuStart),
);
expect(contextMenuSource).toContain("'gn-v2-context-menu-item'");
expect(contextMenuSource).toContain('style={isV2Ui ? undefined : {');
expect(contextMenuSource).not.toContain("style={{ width: '100%', justifyContent: 'flex-start', height: 40");
});
beforeEach(() => {
vi.clearAllMocks();
antdState.treeProps = null;

View File

@@ -2841,7 +2841,8 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
>
<Button
type="text"
style={{ width: '100%', justifyContent: 'flex-start', height: 40, borderRadius: 10, color: workbenchTheme.textPrimary, fontWeight: 600 }}
className={isV2Ui ? 'gn-v2-context-menu-item' : undefined}
style={isV2Ui ? undefined : { width: '100%', justifyContent: 'flex-start', height: 40, borderRadius: 10, color: workbenchTheme.textPrimary, fontWeight: 600 }}
icon={<EditOutlined />}
onClick={() => openRenameKeyModal(treeContextMenu.rawKey)}
>
@@ -2849,7 +2850,8 @@ const RedisViewer: React.FC<RedisViewerProps> = ({ connectionId, redisDB }) => {
</Button>
<Button
type="text"
style={{ width: '100%', justifyContent: 'flex-start', height: 40, borderRadius: 10, color: workbenchTheme.textPrimary, fontWeight: 600 }}
className={isV2Ui ? 'gn-v2-context-menu-item' : undefined}
style={isV2Ui ? undefined : { width: '100%', justifyContent: 'flex-start', height: 40, borderRadius: 10, color: workbenchTheme.textPrimary, fontWeight: 600 }}
icon={<CopyOutlined />}
onClick={async () => {
try {

View File

@@ -1,7 +1,7 @@
import Modal from './common/ResizableDraggableModal';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { Button, Dropdown, message, Tabs, Tooltip } from 'antd';
import { CloseOutlined, ConsoleSqlOutlined, DatabaseOutlined, EditOutlined, FileTextOutlined, FolderOpenOutlined, HistoryOutlined, PlusOutlined, PushpinOutlined, RightOutlined, RobotOutlined, SearchOutlined, SettingOutlined } from '@ant-design/icons';
import { ArrowLeftOutlined, ArrowRightOutlined, CloseCircleOutlined, CloseOutlined, ConsoleSqlOutlined, DatabaseOutlined, EditOutlined, ExportOutlined, FileTextOutlined, FolderOpenOutlined, HistoryOutlined, PlusOutlined, PushpinOutlined, RightOutlined, RobotOutlined, SearchOutlined, SettingOutlined } from '@ant-design/icons';
import type { MenuProps, TabsProps } from 'antd';
import { DndContext, PointerSensor, closestCenter, useSensor, useSensors } from '@dnd-kit/core';
import type { DragEndEvent, DragMoveEvent, DragStartEvent } from '@dnd-kit/core';
@@ -57,6 +57,7 @@ import { QUERY_TAB_RENAME_REQUEST_EVENT } from '../utils/queryTabTitle';
import { getDbIcon } from './DatabaseIcons';
import { resolveConnectionAccentColor, resolveConnectionIconType } from '../utils/connectionVisual';
import { dispatchSidebarLocateConnection } from '../utils/sidebarLocate';
import { renderV2ActionMenuPopup } from './common/V2ActionMenuPopup';
const getTabKindLabel = (tab: TabData): string => {
if (tab.type === 'query') return t('tab_manager.kind_badge.query');
@@ -632,6 +633,12 @@ const SortableTabLabel: React.FC<SortableTabLabelProps> = ({
trigger={['contextMenu']}
onOpenChange={handleTabMenuOpenChange}
rootClassName={isV2Ui ? 'gn-v2-tab-context-menu-popup' : undefined}
popupRender={(menu) => renderV2ActionMenuPopup(menu, Boolean(isV2Ui), {
title: displayTitle,
meta: displayModel.secondaryText || getTabKindLabel(tab),
icon: tab.type === 'query' ? <ConsoleSqlOutlined /> : <FileTextOutlined />,
badge: getTabKindLabel(tab),
})}
>
{wrappedLabel}
</Dropdown>
@@ -1337,57 +1344,74 @@ const TabManager: React.FC<TabManagerProps> = React.memo<TabManagerProps>(({ onF
const renameQueryMenuState = resolveQueryTabRenameMenuState(tab);
const menuItems: MenuProps['items'] = [
...(renameQueryMenuState.visible ? [{
key: 'rename-query',
icon: <EditOutlined />,
label: t('query_editor.action.rename_query'),
disabled: renameQueryMenuState.disabled,
onClick: () => {
setActiveTab(tab.id);
window.setTimeout(() => {
window.dispatchEvent(new CustomEvent(QUERY_TAB_RENAME_REQUEST_EVENT, {
detail: { tabId: tab.id },
}));
}, 0);
},
}] : []),
{
key: 'tab-display-settings',
icon: <SettingOutlined />,
label: t('tab_manager.menu.tab_display_settings'),
onClick: openTabDisplaySettings,
type: 'group',
key: 'tab-actions',
label: t('tab_manager.kind_badge.fallback'),
children: [
...(renameQueryMenuState.visible ? [{
key: 'rename-query',
icon: <EditOutlined />,
label: t('query_editor.action.rename_query'),
disabled: renameQueryMenuState.disabled,
onClick: () => {
setActiveTab(tab.id);
window.setTimeout(() => {
window.dispatchEvent(new CustomEvent(QUERY_TAB_RENAME_REQUEST_EVENT, {
detail: { tabId: tab.id },
}));
}, 0);
},
}] : []),
{
key: 'tab-display-settings',
icon: <SettingOutlined />,
label: t('tab_manager.menu.tab_display_settings'),
onClick: openTabDisplaySettings,
},
{
key: 'open-in-window',
icon: <ExportOutlined />,
label: t('tab_manager.menu.open_in_window'),
disabled: isBackgroundTaskWorkbenchTab(tab),
onClick: () => detachTabToWindow(tab.id),
},
],
},
{
key: 'open-in-window',
label: t('tab_manager.menu.open_in_window'),
disabled: isBackgroundTaskWorkbenchTab(tab),
onClick: () => detachTabToWindow(tab.id),
},
{ type: 'divider' },
{
key: 'close-other',
label: t('tab_manager.menu.close_other'),
disabled: tabs.length <= 1,
onClick: () => closeTabsWithSQLFilePrompt(getCloseOtherTabIds(tabs, tab.id), () => closeOtherTabs(tab.id)),
},
{
key: 'close-left',
label: t('tab_manager.menu.close_left'),
disabled: index === 0,
onClick: () => closeTabsWithSQLFilePrompt(getCloseTabsToLeftIds(dockedTabs, tab.id), () => closeTabsToLeft(tab.id)),
},
{
key: 'close-right',
label: t('tab_manager.menu.close_right'),
disabled: index === dockedTabs.length - 1,
onClick: () => closeTabsWithSQLFilePrompt(getCloseTabsToRightIds(dockedTabs, tab.id), () => closeTabsToRight(tab.id)),
},
{ type: 'divider' },
{
key: 'close-all',
label: t('tab_manager.menu.close_all'),
disabled: tabs.length === 0,
onClick: () => closeTabsWithSQLFilePrompt(tabs.map((item) => item.id), () => closeAllTabs()),
type: 'group',
key: 'close-actions',
label: t('common.close'),
children: [
{
key: 'close-other',
icon: <CloseCircleOutlined />,
label: t('tab_manager.menu.close_other'),
disabled: tabs.length <= 1,
onClick: () => closeTabsWithSQLFilePrompt(getCloseOtherTabIds(tabs, tab.id), () => closeOtherTabs(tab.id)),
},
{
key: 'close-left',
icon: <ArrowLeftOutlined />,
label: t('tab_manager.menu.close_left'),
disabled: index === 0,
onClick: () => closeTabsWithSQLFilePrompt(getCloseTabsToLeftIds(dockedTabs, tab.id), () => closeTabsToLeft(tab.id)),
},
{
key: 'close-right',
icon: <ArrowRightOutlined />,
label: t('tab_manager.menu.close_right'),
disabled: index === dockedTabs.length - 1,
onClick: () => closeTabsWithSQLFilePrompt(getCloseTabsToRightIds(dockedTabs, tab.id), () => closeTabsToRight(tab.id)),
},
{
key: 'close-all',
icon: <CloseOutlined />,
label: t('tab_manager.menu.close_all'),
disabled: tabs.length === 0,
onClick: () => closeTabsWithSQLFilePrompt(tabs.map((item) => item.id), () => closeAllTabs()),
},
],
},
];

View File

@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { Dropdown, Tooltip } from 'antd';
import type { MenuProps } from 'antd';
import { MoreOutlined } from '@ant-design/icons';
import { renderV2ActionMenuPopup } from './common/V2ActionMenuPopup';
export interface TitleBarQuickAction {
key: string;
@@ -65,7 +66,12 @@ const TitleBarQuickActions: React.FC<TitleBarQuickActionsProps> = ({ label, more
menu={{ items: buildMenuItems(action.menu), className: 'gn-v2-titlebar-quick-menu' }}
trigger={['click']}
placement="bottomLeft"
rootClassName="gn-v2-titlebar-quick-dropdown"
rootClassName="gn-v2-titlebar-quick-dropdown gn-v2-action-menu-popup-host"
popupRender={(menu) => renderV2ActionMenuPopup(menu, true, {
title: action.label,
meta: label,
icon: action.icon,
})}
open={openMenuKey === action.key}
onOpenChange={(open) => handleMenuOpenChange(action.key, open)}
>
@@ -108,7 +114,12 @@ const TitleBarQuickActions: React.FC<TitleBarQuickActionsProps> = ({ label, more
menu={dropdownMenuProps}
trigger={['click']}
placement="bottomLeft"
rootClassName="gn-v2-titlebar-quick-dropdown"
rootClassName="gn-v2-titlebar-quick-dropdown gn-v2-action-menu-popup-host"
popupRender={(menu) => renderV2ActionMenuPopup(menu, true, {
title: moreLabel,
meta: label,
icon: <MoreOutlined />,
})}
open={openMenuKey === '__more__'}
onOpenChange={(open) => handleMenuOpenChange('__more__', open)}
>

View File

@@ -0,0 +1,173 @@
import React from 'react';
export interface V2ActionMenuPopupProps {
title: React.ReactNode;
meta?: React.ReactNode;
icon?: React.ReactNode;
badge?: React.ReactNode;
children: React.ReactNode;
}
const V2ActionMenuPopup: React.FC<V2ActionMenuPopupProps> = ({
title,
meta,
icon,
badge,
children,
}) => (
<div className="gn-v2-action-menu-surface">
<div className="gn-v2-context-menu-header gn-v2-action-menu-header">
<span className="gn-v2-context-menu-table-icon gn-v2-action-menu-header-icon" aria-hidden="true">
{icon}
</span>
<span className="gn-v2-context-menu-heading">
<strong>{title}</strong>
{meta ? <small>{meta}</small> : null}
</span>
{badge ? <span className="gn-v2-context-menu-engine-pill">{badge}</span> : null}
</div>
<div className="gn-v2-action-menu-body">{children}</div>
</div>
);
export const renderV2ActionMenuPopup = (
menu: React.ReactNode,
enabled: boolean,
props: Omit<V2ActionMenuPopupProps, 'children'>,
): React.ReactNode => (
enabled ? <V2ActionMenuPopup {...props}>{menu}</V2ActionMenuPopup> : menu
);
const MONACO_CONTEXT_MENU_STYLE_ID = 'gn-v2-monaco-context-menu-styles';
const MONACO_CONTEXT_MENU_STYLES = `
.monaco-menu {
min-width: 264px !important;
overflow: hidden !important;
padding: 4px !important;
border: 0.5px solid var(--gn-br-2) !important;
border-radius: var(--gn-v2-menu-surface-radius, 10px) !important;
background: var(--gn-bg-panel) !important;
color: var(--gn-fg-1) !important;
box-shadow: var(--gn-shadow-lg) !important;
}
.monaco-menu > .gn-v2-monaco-context-menu-header {
display: grid;
grid-template-columns: 16px minmax(0, 1fr);
column-gap: 6px;
row-gap: 3px;
align-items: center;
margin: -4px -4px 4px;
padding: 10px 12px;
border-bottom: 0.5px solid var(--gn-br-1);
border-radius: 9px 9px 0 0;
background: var(--gn-bg-panel-2);
font-family: var(--gn-font-sans);
}
.gn-v2-monaco-context-menu-header .gn-v2-action-menu-header-icon {
width: 16px;
color: var(--gn-info);
font-family: var(--gn-font-mono);
font-size: 8px;
font-weight: 700;
letter-spacing: -0.04em;
}
.gn-v2-monaco-context-menu-header .gn-v2-context-menu-heading { display: contents; }
.gn-v2-monaco-context-menu-header strong {
grid-column: 2;
min-width: 0;
overflow: hidden;
color: var(--gn-fg-1);
font-family: var(--gn-font-mono);
font-size: 12.5px;
font-weight: 600;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.gn-v2-monaco-context-menu-header small {
grid-column: 1 / -1;
overflow: hidden;
color: var(--gn-fg-4);
font-family: var(--gn-font-mono);
font-size: 10.5px;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.monaco-menu .monaco-action-bar.vertical .action-menu-item,
.monaco-menu .monaco-action-bar.vertical .action-label:not(.separator) {
min-height: var(--gn-v2-menu-row-height, 28px) !important;
border-radius: var(--gn-v2-menu-item-radius, 5px) !important;
font-size: 12.5px !important;
line-height: var(--gn-v2-menu-row-height, 28px) !important;
}
.monaco-menu .monaco-action-bar.vertical .action-label:not(.separator) {
padding: 0 8px !important;
}
.monaco-menu .monaco-action-bar.vertical .action-menu-item:focus {
background: var(--gn-bg-active) !important;
color: var(--gn-fg-1) !important;
outline: none !important;
}
.monaco-menu .monaco-action-bar .action-item .action-label.separator {
width: auto !important;
height: 1px !important;
min-height: 1px !important;
margin: 4px !important;
padding: 0 !important;
border: 0 !important;
background: var(--gn-br-1) !important;
}
`;
export const decorateV2MonacoContextMenu = (title: string, meta?: string): void => {
const roots: Array<Document | ShadowRoot> = [document];
document.querySelectorAll<HTMLElement>('*').forEach((element) => {
if (element.shadowRoot) roots.push(element.shadowRoot);
});
const menus = roots.flatMap((root) => {
if (root instanceof ShadowRoot && !root.getElementById(MONACO_CONTEXT_MENU_STYLE_ID)) {
const style = document.createElement('style');
style.id = MONACO_CONTEXT_MENU_STYLE_ID;
style.textContent = MONACO_CONTEXT_MENU_STYLES;
root.append(style);
}
return Array.from(root.querySelectorAll<HTMLElement>('.monaco-menu'));
}).filter((menu) => menu.getClientRects().length > 0);
const menu = menus[menus.length - 1];
if (!menu) return;
const existing = menu.querySelector<HTMLElement>(':scope > .gn-v2-monaco-context-menu-header');
if (existing) {
const titleElement = existing.querySelector<HTMLElement>('.gn-v2-context-menu-heading strong');
if (titleElement) titleElement.textContent = title;
const metaElement = existing.querySelector<HTMLElement>('.gn-v2-context-menu-heading small');
if (metaElement) metaElement.textContent = meta || '';
return;
}
const header = document.createElement('div');
header.className = 'gn-v2-context-menu-header gn-v2-action-menu-header gn-v2-monaco-context-menu-header';
const icon = document.createElement('span');
icon.className = 'gn-v2-context-menu-table-icon gn-v2-action-menu-header-icon gn-v2-monaco-context-menu-icon';
icon.setAttribute('aria-hidden', 'true');
icon.textContent = 'SQL';
const heading = document.createElement('span');
heading.className = 'gn-v2-context-menu-heading';
const strong = document.createElement('strong');
strong.textContent = title;
heading.appendChild(strong);
if (meta) {
const small = document.createElement('small');
small.textContent = meta;
heading.appendChild(small);
}
header.append(icon, heading);
menu.prepend(header);
};
export default V2ActionMenuPopup;

View File

@@ -989,7 +989,7 @@ body[data-ui-version="v2"] .gn-v2-redis-data-section .ant-table-wrapper {
body[data-ui-version="v2"] .gn-v2-redis-context-menu {
width: 208px !important;
min-width: 188px !important;
padding: 6px !important;
padding: var(--gn-v2-menu-surface-padding) !important;
}
/* ─── V2 Nacos workbench (flat split + aligned headers, match Redis) ─ */

View File

@@ -130,6 +130,10 @@ body[data-ui-version="v2"]:not([data-custom-theme]) {
/* ─── Base typography & body ──────────────────────────── */
body[data-ui-version="v2"] {
--gn-v2-menu-row-height: 28px;
--gn-v2-menu-item-radius: 5px;
--gn-v2-menu-surface-radius: 10px;
--gn-v2-menu-surface-padding: 4px;
font-family: var(--gn-font-sans) !important;
font-size: var(--gn-font-size, var(--gonavi-font-size, 14px));
-webkit-font-smoothing: antialiased;
@@ -429,9 +433,9 @@ body[data-ui-version="v2"] .ant-dropdown-menu,
body[data-ui-version="v2"] .ant-picker-dropdown .ant-picker-panel-container {
background: var(--gn-bg-panel) !important;
border: 0.5px solid var(--gn-br-2) !important;
border-radius: 10px !important;
border-radius: var(--gn-v2-menu-surface-radius) !important;
box-shadow: var(--gn-shadow-lg) !important;
padding: 4px !important;
padding: var(--gn-v2-menu-surface-padding) !important;
}
body[data-ui-version="v2"] .ant-dropdown-menu-item,
@@ -452,6 +456,17 @@ body[data-ui-version="v2"] .ant-select-item {
outline: none !important;
}
/* Compact action menus share the table context-menu geometry. */
body[data-ui-version="v2"] .ant-dropdown-menu-item,
body[data-ui-version="v2"] .ant-dropdown-menu-submenu-title {
height: var(--gn-v2-menu-row-height) !important;
min-height: var(--gn-v2-menu-row-height) !important;
box-sizing: border-box !important;
padding: 4px 8px !important;
border-radius: var(--gn-v2-menu-item-radius) !important;
font-weight: 500 !important;
}
/* Icon + label share one baseline/vertical center (leaf and submenu rows) */
body[data-ui-version="v2"] .ant-dropdown-menu-title-content {
display: inline-flex !important;
@@ -501,12 +516,12 @@ body[data-ui-version="v2"] .ant-dropdown-menu-submenu-title .ant-dropdown-menu-i
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
flex: 0 0 16px !important;
width: 16px !important;
height: 16px !important;
flex: 0 0 14px !important;
width: 14px !important;
height: 14px !important;
margin-inline-end: 8px !important;
margin-right: 8px !important;
font-size: 14px !important;
font-size: 13px !important;
line-height: 1 !important;
}
@@ -536,6 +551,63 @@ body[data-ui-version="v2"] .ant-dropdown-menu-item-danger:hover {
background: rgba(220, 38, 38, 0.08) !important;
}
/* Monaco context menu follows the same compact action-menu surface. */
body[data-ui-version="v2"] .monaco-menu {
min-width: 264px !important;
overflow: hidden !important;
padding: var(--gn-v2-menu-surface-padding) !important;
border: 0.5px solid var(--gn-br-2) !important;
border-radius: var(--gn-v2-menu-surface-radius) !important;
background: var(--gn-bg-panel) !important;
box-shadow: var(--gn-shadow-lg) !important;
}
body[data-ui-version="v2"] .monaco-menu > .gn-v2-monaco-context-menu-header {
margin: calc(var(--gn-v2-menu-surface-padding) * -1) calc(var(--gn-v2-menu-surface-padding) * -1) 4px;
border-radius: calc(var(--gn-v2-menu-surface-radius) - 1px) calc(var(--gn-v2-menu-surface-radius) - 1px) 0 0;
font-family: var(--gn-font-sans);
}
body[data-ui-version="v2"] .gn-v2-monaco-context-menu-icon {
width: 18px;
color: var(--gn-info);
font-family: var(--gn-font-mono);
font-size: 8px;
font-weight: 700;
letter-spacing: -0.04em;
}
body[data-ui-version="v2"] .monaco-menu-option,
body[data-ui-version="v2"] .monaco-menu .monaco-action-bar.vertical .action-menu-item,
body[data-ui-version="v2"] .monaco-menu .monaco-action-bar.vertical .action-label:not(.separator) {
min-height: var(--gn-v2-menu-row-height) !important;
border-radius: var(--gn-v2-menu-item-radius) !important;
font-size: 12.5px !important;
line-height: var(--gn-v2-menu-row-height) !important;
}
body[data-ui-version="v2"] .monaco-menu-option,
body[data-ui-version="v2"] .monaco-menu .monaco-action-bar.vertical .action-label:not(.separator) {
padding: 0 8px !important;
}
body[data-ui-version="v2"] .monaco-menu-option.active,
body[data-ui-version="v2"] .monaco-menu .monaco-action-bar.vertical .action-menu-item:focus {
background: var(--gn-bg-active) !important;
color: var(--gn-fg-1) !important;
outline: none !important;
}
body[data-ui-version="v2"] .monaco-menu .monaco-action-bar .action-item .action-label.separator {
width: auto !important;
height: 1px !important;
min-height: 1px !important;
margin: 4px !important;
padding: 0 !important;
border: 0 !important;
background: var(--gn-br-1) !important;
}
/* Nested dropdown menus: collapse double border, no glow/halo */
body[data-ui-version="v2"] .ant-dropdown-menu-submenu-popup,
body[data-ui-version="v2"] .gn-v2-titlebar-quick-submenu,
@@ -575,7 +647,7 @@ body[data-ui-version="v2"] .gn-v2-titlebar-quick-menu .ant-dropdown-menu-submenu
background: transparent !important;
border: none !important;
box-shadow: none !important;
min-height: 32px !important;
min-height: var(--gn-v2-menu-row-height) !important;
}
body[data-ui-version="v2"] .gn-v2-titlebar-quick-dropdown .ant-dropdown-menu-item:hover,
@@ -4314,26 +4386,6 @@ body[data-ui-version="v2"] .gn-v2-tab-close:hover {
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 {
@@ -4341,30 +4393,11 @@ body[data-ui-version="v2"] .gn-v2-tab-context-menu-popup .ant-dropdown-menu-item
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);
@@ -5760,11 +5793,85 @@ body[data-ui-version="v2"] .gn-v2-data-grid-pagination-wrap .data-grid-paginatio
}
/* ─── V2 context menus, including portal-rendered custom menus ─ */
body[data-ui-version="v2"] .gn-v2-action-menu-surface {
width: 264px;
max-width: calc(100vw - 24px);
overflow: hidden;
box-sizing: border-box;
border: 0.5px solid var(--gn-br-2);
border-radius: var(--gn-v2-menu-surface-radius);
background: var(--gn-bg-panel);
color: var(--gn-fg-1);
box-shadow: var(--gn-shadow-lg);
font-size: 12.5px;
animation: gnV2ContextMenuFadeIn 0.08s ease-out;
}
body[data-ui-version="v2"] .gn-v2-action-menu-surface .gn-v2-action-menu-header {
border-radius: calc(var(--gn-v2-menu-surface-radius) - 1px) calc(var(--gn-v2-menu-surface-radius) - 1px) 0 0;
}
body[data-ui-version="v2"] .gn-v2-action-menu-header-icon:empty::before {
content: "\2022";
color: var(--gn-fg-5);
}
body[data-ui-version="v2"] .gn-v2-action-menu-body {
overflow: hidden;
padding: 4px 4px 8px;
}
body[data-ui-version="v2"] .gn-v2-action-menu-body > .ant-dropdown-menu {
width: 100%;
min-width: 0;
padding: 0 !important;
border: 0 !important;
border-radius: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
body[data-ui-version="v2"] .gn-v2-action-menu-body .ant-dropdown-menu-item,
body[data-ui-version="v2"] .gn-v2-action-menu-body .ant-dropdown-menu-submenu-title {
padding: 0 8px 0 10px !important;
}
body[data-ui-version="v2"] .gn-v2-action-menu-body .ant-dropdown-menu-item-group-title {
padding: 8px 12px 2px !important;
color: var(--gn-fg-5) !important;
font-size: 9.5px !important;
font-weight: 700 !important;
line-height: 1.25 !important;
letter-spacing: 0.08em;
text-transform: uppercase;
}
body[data-ui-version="v2"] .gn-v2-action-menu-body .ant-dropdown-menu-item-group-list {
margin: 0 !important;
}
body[data-ui-version="v2"] .gn-v2-action-menu-body .ant-dropdown-menu-title-content > .gn-v2-context-menu-item-title {
width: 100%;
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
body[data-ui-version="v2"] .gn-v2-action-menu-body .ant-dropdown-menu-item:not(:has(.ant-dropdown-menu-item-icon))::before,
body[data-ui-version="v2"] .gn-v2-action-menu-body .ant-dropdown-menu-submenu-title:not(:has(.ant-dropdown-menu-item-icon))::before {
content: "";
width: 14px;
height: 14px;
flex: 0 0 14px;
margin-inline-end: 8px;
}
body[data-ui-version="v2"] .gn-v2-context-menu {
width: 260px !important;
min-width: 220px !important;
border: 0.5px solid var(--gn-br-2) !important;
border-radius: 10px !important;
border-radius: var(--gn-v2-menu-surface-radius) !important;
background: var(--gn-bg-panel) !important;
box-shadow: var(--gn-shadow-lg) !important;
color: var(--gn-fg-1) !important;
@@ -5807,7 +5914,7 @@ body[data-ui-version="v2"] .gn-v2-table-context-menu {
overflow: hidden;
padding: 0;
border: 0.5px solid var(--gn-br-2);
border-radius: 10px;
border-radius: var(--gn-v2-menu-surface-radius);
background: var(--gn-bg-panel);
color: var(--gn-fg-1);
box-shadow: var(--gn-shadow-lg);
@@ -5926,12 +6033,12 @@ body[data-ui-version="v2"] .gn-v2-context-menu-divider {
body[data-ui-version="v2"] .gn-v2-context-menu-item {
width: 100%;
height: 28px;
height: var(--gn-v2-menu-row-height);
display: flex;
gap: 8px;
align-items: center;
border: 0;
border-radius: 5px;
border-radius: var(--gn-v2-menu-item-radius);
padding: 0 8px 0 10px;
background: transparent;
color: var(--gn-fg-1);