diff --git a/frontend/src/components/DataGridModals.tsx b/frontend/src/components/DataGridModals.tsx index be4d1653..4e53d560 100644 --- a/frontend/src/components/DataGridModals.tsx +++ b/frontend/src/components/DataGridModals.tsx @@ -244,6 +244,7 @@ const DataGridModals: React.FC = ({ {cellEditorOpen && ( = ({ {jsonEditorOpen && ( = ({ {ddlModalOpen && ( = ({
= ({ tab }) => { { 'editor.background': '#00000000', 'editor.lineHighlightBackground': '#ffffff10', 'editorGutter.background': '#00000000', - 'editorStickyScroll.background': '#1e1e1e', - 'editorStickyScrollHover.background': '#2a2a2a', + // Transparent sticky scroll so panel/theme bg shows through (CSS may also paint --gn-bg-panel). + 'editorStickyScroll.background': '#00000000', + 'editorStickyScrollHover.background': '#ffffff12', }, }); monaco.editor.defineTheme('transparent-light', { @@ -802,8 +808,8 @@ export const registerGonaviMonacoThemes: BeforeMount = (monaco) => { 'editor.background': '#00000000', 'editor.lineHighlightBackground': '#00000010', 'editorGutter.background': '#00000000', - 'editorStickyScroll.background': '#ffffff', - 'editorStickyScrollHover.background': '#f5f5f5', + 'editorStickyScroll.background': '#00000000', + 'editorStickyScrollHover.background': '#00000010', }, }); @@ -850,9 +856,11 @@ const MonacoEditor: React.FC = ({ loading, onMount, options, + theme, ...props }) => { const [ready, setReady] = useState(isTestRuntime); + const appTheme = useStore((state) => state.theme); const uiVersion = useStore((state) => state.appearance.uiVersion); const dataTableFontSize = useStore((state) => state.appearance.dataTableFontSize); const dataTableFontSizeFollowGlobal = useStore((state) => state.appearance.dataTableFontSizeFollowGlobal); @@ -860,6 +868,9 @@ const MonacoEditor: React.FC = ({ const sqlEditorFontSizeFollowGlobal = useStore((state) => state.appearance.sqlEditorFontSizeFollowGlobal); const monoFontFamily = useStore((state) => state.appearance.customMonoFontFamily); const globalFontSize = useStore((state) => state.fontSize); + // Monaco theme is process-global; never fall back to "light" or other editors get polluted. + const resolvedTheme = theme + ?? (appTheme === 'dark' ? 'transparent-dark' : 'transparent-light'); useEffect(() => { let cancelled = false; @@ -895,11 +906,21 @@ const MonacoEditor: React.FC = ({ onMount?.(editor, monaco); }, [onMount]); + // Unified surface: all call sites inherit panel via --gn-monaco-bg (no per-page bg). + const surfaceStyle: React.CSSProperties = { + height: props.height || '100%', + width: props.width || '100%', + minHeight: 0, + minWidth: 0, + background: `var(${GONAVI_MONACO_BG_CSS_VAR}, var(--gn-bg-panel, transparent))`, + }; + const loadingFallback = (
{loading || null}
@@ -959,13 +980,18 @@ const MonacoEditor: React.FC = ({ } return ( - +
+ +
); }; diff --git a/frontend/src/components/common/ResizableDraggableModal.css b/frontend/src/components/common/ResizableDraggableModal.css index f3bab5cf..3ffc84fb 100644 --- a/frontend/src/components/common/ResizableDraggableModal.css +++ b/frontend/src/components/common/ResizableDraggableModal.css @@ -32,8 +32,57 @@ height: var(--gn-modal-resized-height); } +/* Header/footer stay put; body fills remaining space when modal is resized. */ +.gn-resizable-draggable-modal .ant-modal-header { + flex: 0 0 auto; +} + +/* Close alignment is handled in v2-theme.css (content-absolute, top ≈ header center). */ + .gn-resizable-draggable-modal .ant-modal-body { + flex: 1 1 auto; + /* Prefer content size when unresized; only collapse min when height is fixed by resize. */ + min-height: auto; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.gn-resizable-draggable-modal[data-has-resized-height='true'] .ant-modal-body { min-height: 0; + overflow: hidden; +} + +.gn-resizable-draggable-modal .ant-modal-footer { + flex: 0 0 auto; +} + +/* + * Monaco fill host: + * - Unresized: explicit min-height so height:100% / abs fill has a real box (avoids 0-height). + * - Resized: parent content has fixed height → host flexes and Monaco fills it. + */ +.gn-resizable-draggable-modal .gn-modal-fill-body { + position: relative; + flex: 1 1 auto; + width: 100%; + min-height: 450px; + height: 450px; + overflow: hidden; +} + +.gn-resizable-draggable-modal[data-has-resized-height='true'] .gn-modal-fill-body { + height: auto; + min-height: 0; + flex: 1 1 auto; +} + +.gn-resizable-draggable-modal .gn-modal-fill-body > .gn-monaco-surface { + position: absolute; + inset: 0; + width: auto !important; + height: auto !important; + min-height: 0 !important; } .gn-resizable-draggable-modal[data-draggable='true'] .ant-modal-confirm-title { diff --git a/frontend/src/components/theme/CustomThemeStyleHost.test.ts b/frontend/src/components/theme/CustomThemeStyleHost.test.ts index ea8f404c..8c9d7e8c 100644 --- a/frontend/src/components/theme/CustomThemeStyleHost.test.ts +++ b/frontend/src/components/theme/CustomThemeStyleHost.test.ts @@ -61,13 +61,15 @@ describe('CustomThemeStyleHost runtime', () => { it('uses one style element, updates textContent, and cleans body state', () => { const fake = createFakeDocument(); syncCustomThemeStyle(buildTheme('theme-one', 'body { color: red; }'), fake.documentRef); - expect(fake.getStyle().textContent).toBe('body { color: red; }'); + expect(fake.getStyle().textContent).toContain('body { color: red; }'); + expect(fake.getStyle().textContent).toContain('--gn-monaco-bg: var(--gn-bg-panel)'); expect(fake.attributes.get('data-custom-theme')).toBe('active'); expect(fake.attributes.get('data-custom-theme-id')).toBe('theme-one'); expect(fake.getAppendCount()).toBe(1); syncCustomThemeStyle(buildTheme('theme-two', 'body { color: blue; }'), fake.documentRef); - expect(fake.getStyle().textContent).toBe('body { color: blue; }'); + expect(fake.getStyle().textContent).toContain('body { color: blue; }'); + expect(fake.getStyle().textContent).toContain('--gn-monaco-bg: var(--gn-bg-panel)'); expect(fake.attributes.get('data-custom-theme-id')).toBe('theme-two'); expect(fake.getAppendCount()).toBe(1); diff --git a/frontend/src/components/theme/CustomThemeStyleHost.tsx b/frontend/src/components/theme/CustomThemeStyleHost.tsx index e0fbda82..8d164846 100644 --- a/frontend/src/components/theme/CustomThemeStyleHost.tsx +++ b/frontend/src/components/theme/CustomThemeStyleHost.tsx @@ -2,6 +2,7 @@ import { useEffect, useLayoutEffect, useMemo } from 'react'; import { CUSTOM_THEME_STORAGE_KEY, useCustomThemeStore } from '../../customThemeStore'; import { CUSTOM_THEME_STYLE_ID, + ensureCustomThemeMonacoSurfaceVars, extractComputedCustomThemeAntTokens, extractCustomThemeAntTokens, type CustomThemeAntTokens, @@ -77,7 +78,8 @@ export const syncCustomThemeStyle = ( style.id = CUSTOM_THEME_STYLE_ID; style.setAttribute('data-gonavi-custom-theme', theme.id); // textContent is intentional: imported CSS must never pass through HTML. - style.textContent = theme.css; + // Inject --gn-monaco-bg for hand-written themes that only set --gn-bg-panel. + style.textContent = ensureCustomThemeMonacoSurfaceVars(theme.css); if (!style.isConnected) documentRef.head.appendChild(style); documentRef.body.setAttribute('data-custom-theme', 'active'); documentRef.body.setAttribute('data-custom-theme-id', theme.id); diff --git a/frontend/src/styles/v2-theme-workbench.css b/frontend/src/styles/v2-theme-workbench.css index 573fe451..50e7e26b 100644 --- a/frontend/src/styles/v2-theme-workbench.css +++ b/frontend/src/styles/v2-theme-workbench.css @@ -356,7 +356,8 @@ 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); - background: var(--gn-bg-panel); + /* Same token as Monaco surface */ + background: var(--gn-monaco-bg, var(--gn-bg-panel)); } body[data-ui-version="v2"] .gn-v2-query-monaco-stage:has(.monaco-editor .find-widget.visible:not(.hiddenEditor)) { @@ -366,6 +367,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)); } body[data-ui-version="v2"] .gn-v2-query-resizer { diff --git a/frontend/src/utils/customTheme.test.ts b/frontend/src/utils/customTheme.test.ts index bb99c9f7..c7a7ef81 100644 --- a/frontend/src/utils/customTheme.test.ts +++ b/frontend/src/utils/customTheme.test.ts @@ -1,8 +1,10 @@ import { describe, expect, it } from 'vitest'; import { CUSTOM_THEME_MAX_BYTES, + ensureCustomThemeMonacoSurfaceVars, extractComputedCustomThemeAntTokens, extractCustomThemeAntTokens, + sanitizeCustomThemeDefinition, sanitizeCustomThemeList, validateCustomThemeCss, } from './customTheme'; @@ -49,6 +51,32 @@ describe('custom theme CSS validation', () => { ); }); + it('injects --gn-monaco-bg for hand-written themes that only set 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', () => { + const css = 'body[data-custom-theme] {\n --gn-bg-panel: #111;\n --gn-monaco-bg: #222;\n}'; + expect(ensureCustomThemeMonacoSurfaceVars(css)).toBe(css); + }); + + it('sanitizes custom themes and ensures Monaco surface token', () => { + const theme = sanitizeCustomThemeDefinition({ + schemaVersion: 1, + id: 'hand-written', + name: 'Hand', + sourceFileName: 'hand.css', + baseMode: 'dark', + css: 'body[data-custom-theme] { --gn-bg-panel: #1d202b; --gn-accent: #8b5cf6; }', + createdAt: 1, + updatedAt: 1, + }); + expect(theme?.css).toContain('--gn-monaco-bg: var(--gn-bg-panel)'); + }); + it('extracts safe CSS color tokens for Ant Design without accepting arbitrary values', () => { const tokens = extractCustomThemeAntTokens(` body[data-custom-theme] { diff --git a/frontend/src/utils/customTheme.ts b/frontend/src/utils/customTheme.ts index d148d2a5..7521d660 100644 --- a/frontend/src/utils/customTheme.ts +++ b/frontend/src/utils/customTheme.ts @@ -218,6 +218,27 @@ export const createCustomThemeId = (): string => { return `theme-${randomId.toLowerCase().replace(/[^a-z0-9_-]/g, '-')}`.slice(0, 80); }; +/** + * 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. + * 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 trimmed = source.trimEnd(); + const appendix = [ + '', + '/* GoNavi: Monaco surface follows panel 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);', + '}', + '', + ].join('\n'); + return trimmed ? `${trimmed}\n${appendix}` : appendix.trimStart(); +}; + export const sanitizeCustomThemeDefinition = (value: unknown): CustomThemeDefinition | null => { if (!value || typeof value !== 'object') return null; const raw = value as Record; @@ -242,7 +263,7 @@ export const sanitizeCustomThemeDefinition = (value: unknown): CustomThemeDefini name: sanitizeCustomThemeName(raw.name, deriveCustomThemeName(sourceFileName)), sourceFileName, baseMode: sanitizeCustomThemeBaseMode(raw.baseMode), - css: validation.css, + css: ensureCustomThemeMonacoSurfaceVars(validation.css), createdAt, updatedAt, }; @@ -375,6 +396,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-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); diff --git a/frontend/src/utils/customThemePresets.ts b/frontend/src/utils/customThemePresets.ts index db0458cc..9811545b 100644 --- a/frontend/src/utils/customThemePresets.ts +++ b/frontend/src/utils/customThemePresets.ts @@ -254,6 +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-bg-input: ${palette.input}; --gn-bg-subtle: ${palette.panel2}; --gn-bg-hover: ${palette.hover}; diff --git a/frontend/src/v2-theme.css b/frontend/src/v2-theme.css index 273a751c..b316d01c 100644 --- a/frontend/src/v2-theme.css +++ b/frontend/src/v2-theme.css @@ -28,6 +28,8 @@ body[data-ui-version="v2"][data-theme="light"] { --gn-bg-chrome: #ececea; --gn-bg-panel: #ffffff; --gn-bg-panel-2: #fafaf8; + /* All Monaco editors share this surface token (see .gn-monaco-surface). */ + --gn-monaco-bg: var(--gn-bg-panel); --gn-bg-hover: rgba(15, 23, 42, 0.045); --gn-bg-active: rgba(15, 23, 42, 0.075); --gn-bg-selected: rgba(34, 197, 94, 0.10); @@ -79,6 +81,7 @@ body[data-ui-version="v2"][data-theme="dark"] { --gn-bg-chrome: #14171c; --gn-bg-panel: #161a21; --gn-bg-panel-2: #1b1f27; + --gn-monaco-bg: var(--gn-bg-panel); --gn-bg-hover: rgba(255, 255, 255, 0.05); --gn-bg-active: rgba(255, 255, 255, 0.08); --gn-bg-selected: rgba(34, 197, 94, 0.14); @@ -385,6 +388,28 @@ body[data-ui-version="v2"] .ant-picker { transition: border-color 0.08s, background 0.08s !important; } +/* + * allowClear / prefix / suffix wraps input in .ant-input-affix-wrapper. + * Only the wrapper should draw the border — otherwise it looks double-layered. + */ +body[data-ui-version="v2"] .ant-input-affix-wrapper > input.ant-input, +body[data-ui-version="v2"] .ant-input-affix-wrapper .ant-input { + border: none !important; + border-radius: 0 !important; + box-shadow: none !important; + background: transparent !important; + outline: none !important; +} + +body[data-ui-version="v2"] .ant-input-affix-wrapper > input.ant-input:focus, +body[data-ui-version="v2"] .ant-input-affix-wrapper > input.ant-input:focus-visible, +body[data-ui-version="v2"] .ant-input-affix-wrapper .ant-input:focus, +body[data-ui-version="v2"] .ant-input-affix-wrapper .ant-input:focus-visible { + border: none !important; + box-shadow: none !important; + outline: none !important; +} + body[data-ui-version="v2"] .ant-input::placeholder, body[data-ui-version="v2"] .ant-input-affix-wrapper input::placeholder { color: var(--gn-fg-5) !important; @@ -399,6 +424,13 @@ body[data-ui-version="v2"] .ant-picker-focused { box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.12) !important; } +/* Focus ring only on the wrapper when affix is present (not also on inner input). */ +body[data-ui-version="v2"] .ant-input-affix-wrapper-focused > input.ant-input, +body[data-ui-version="v2"] .ant-input-affix-wrapper-focused .ant-input { + border: none !important; + box-shadow: none !important; +} + body[data-ui-version="v2"] .ant-input-search-button { background: var(--gn-accent) !important; border-color: var(--gn-accent-2) !important; @@ -559,7 +591,8 @@ body[data-ui-version="v2"] .ant-modal .ant-modal-content { body[data-ui-version="v2"] .ant-modal .ant-modal-header { background: var(--gn-bg-panel-2) !important; border-bottom: 0.5px solid var(--gn-br-1) !important; - padding: 12px 18px !important; + /* Right padding reserves room for absolute close on content */ + padding: 14px 48px 14px 18px !important; margin: 0 !important; } @@ -567,6 +600,39 @@ body[data-ui-version="v2"] .ant-modal .ant-modal-title { font-size: 14px !important; font-weight: 600 !important; color: var(--gn-fg-1) !important; + line-height: 22px !important; +} + +/* + * Ant mounts .ant-modal-close on .ant-modal-content (absolute). + * Center X against the title row: header padding 14 + title 22 ≈ 50 → top (50-28)/2 ≈ 11. + */ +body[data-ui-version="v2"] .ant-modal .ant-modal-close { + top: 11px !important; + inset-inline-end: 12px !important; + width: 28px !important; + height: 28px !important; + margin: 0 !important; + padding: 0 !important; + transform: none !important; + color: var(--gn-fg-3) !important; + background: transparent !important; + border-radius: 6px !important; +} + +body[data-ui-version="v2"] .ant-modal .ant-modal-close:hover { + color: var(--gn-fg-1) !important; + background: var(--gn-bg-hover) !important; +} + +body[data-ui-version="v2"] .ant-modal .ant-modal-close-x { + width: 28px !important; + height: 28px !important; + line-height: 28px !important; + font-size: 14px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; } body[data-ui-version="v2"] .ant-modal .ant-modal-body { @@ -1348,11 +1414,48 @@ body[data-ui-version="v2"] [class*="AISend"] { color: #fff !important; } -/* ─── Code / SQL editor surfaces ───────────────────────── */ +/* ─── Code / SQL editor surfaces (unified via MonacoEditor .gn-monaco-surface) ─ + * Consumers must not set per-page Monaco bg; only --gn-monaco-bg (defaults to panel). + * Covers SQL workbench, Redis field modals, Nacos, DataGrid cell/JSON editors, etc. + * --gn-monaco-bg is set on light/dark roots and injected into custom themes. + */ +body[data-ui-version="v2"] .gn-monaco-surface { + background: var(--gn-monaco-bg, var(--gn-bg-panel)) !important; + background-color: var(--gn-monaco-bg, var(--gn-bg-panel)) !important; +} + +/* Center line numbers in the gutter so single digits don't look left/right skewed */ +body[data-ui-version="v2"] .gn-monaco-surface .monaco-editor .margin-view-overlays .line-numbers, +body[data-ui-version="v2"] .gn-monaco-surface .monaco-editor .line-numbers { + text-align: center !important; + /* Monaco defaults to right-align + padding-right; reset so digits sit in the middle */ + padding-right: 0 !important; + box-sizing: border-box; +} + +body[data-ui-version="v2"] .gn-monaco-surface .monaco-editor, +body[data-ui-version="v2"] .gn-monaco-surface .monaco-editor-background, +body[data-ui-version="v2"] .gn-monaco-surface .monaco-editor .margin, +body[data-ui-version="v2"] .gn-monaco-surface .monaco-editor .overflow-guard, +body[data-ui-version="v2"] .gn-monaco-surface .monaco-editor .monaco-scrollable-element, +body[data-ui-version="v2"] .gn-monaco-surface .monaco-editor .lines-content, +body[data-ui-version="v2"] .gn-monaco-surface .monaco-editor .inputarea, +body[data-ui-version="v2"] .gn-monaco-surface .monaco-editor .view-overlays, +body[data-ui-version="v2"] .gn-monaco-surface .monaco-editor .margin-view-overlays, +body[data-ui-version="v2"] .gn-monaco-surface .monaco-editor .sticky-widget, +body[data-ui-version="v2"] .gn-monaco-surface .monaco-editor .sticky-widget .sticky-line-content, +body[data-ui-version="v2"] .gn-monaco-surface .monaco-editor .sticky-widget .sticky-line-number { + background: var(--gn-monaco-bg, var(--gn-bg-panel)) !important; + background-color: var(--gn-monaco-bg, var(--gn-bg-panel)) !important; +} + +/* Fallback when editor is not wrapped (legacy/direct monaco mounts). */ body[data-ui-version="v2"] .monaco-editor, body[data-ui-version="v2"] .monaco-editor-background, -body[data-ui-version="v2"] .monaco-editor .margin { - background-color: var(--gn-bg-panel) !important; +body[data-ui-version="v2"] .monaco-editor .margin, +body[data-ui-version="v2"] .monaco-editor .overflow-guard { + background: var(--gn-monaco-bg, var(--gn-bg-panel)) !important; + background-color: var(--gn-monaco-bg, var(--gn-bg-panel)) !important; } body[data-ui-version="v2"] .monaco-editor,