From e8bab17c2fed26d54781b0eb73374ed18122f13b Mon Sep 17 00:00:00 2001 From: Sanaei Date: Tue, 15 Sep 2026 22:04:00 +0200 Subject: [PATCH] fix(clients): stop the Edit Client modal showing a stray light scrollbar The client form body is capped at the viewport and scrolls internally (49ef1449). Every tab ends with a Form.Item that keeps antd's 24px bottom margin, so when the fields themselves fit, that empty margin alone pushed the body past the cap: 752px of content in 740px at a 900px window. The last item of each tab now drops the margin, so the body scrolls only when real content overflows. When it does scroll, the bar was painted light inside the dark modal: the dark themes set body.dark and data-theme but never color-scheme, which is what native scrollbars read. applyDom (panel, login and subscription bundles) and the Storybook decorator now set it on the root element. --- frontend/.storybook/preview.tsx | 1 + frontend/src/hooks/useTheme.tsx | 2 ++ .../src/pages/clients/ClientFormModal.css | 5 +++ .../src/pages/clients/ClientFormModal.tsx | 2 ++ frontend/src/test/storybook-theme.test.tsx | 32 +++++++++++++++++-- 5 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 frontend/src/pages/clients/ClientFormModal.css diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx index c5281cb16..8737aebfa 100644 --- a/frontend/.storybook/preview.tsx +++ b/frontend/.storybook/preview.tsx @@ -22,6 +22,7 @@ export const withTheme: Decorator = (Story, context) => { useLayoutEffect(() => { document.body.classList.remove('dark', 'light'); document.body.classList.add(dark ? 'dark' : 'light'); + document.documentElement.style.colorScheme = dark ? 'dark' : 'light'; document.documentElement.removeAttribute('data-theme'); }, [dark]); return ( diff --git a/frontend/src/hooks/useTheme.tsx b/frontend/src/hooks/useTheme.tsx index c64be0427..d55b5ab0a 100644 --- a/frontend/src/hooks/useTheme.tsx +++ b/frontend/src/hooks/useTheme.tsx @@ -15,6 +15,8 @@ function readBool(key: string, fallback: boolean): boolean { function applyDom(isDark: boolean, isUltra: boolean) { document.body.classList.remove('dark', 'light'); document.body.classList.add(isDark ? 'dark' : 'light'); + // Native scrollbars read color-scheme, not the body class. + document.documentElement.style.colorScheme = isDark ? 'dark' : 'light'; if (isUltra) { document.documentElement.setAttribute('data-theme', 'ultra-dark'); } else { diff --git a/frontend/src/pages/clients/ClientFormModal.css b/frontend/src/pages/clients/ClientFormModal.css new file mode 100644 index 000000000..3e5803424 --- /dev/null +++ b/frontend/src/pages/clients/ClientFormModal.css @@ -0,0 +1,5 @@ +/* The body is capped at the viewport and scrolls; a trailing item margin + alone must not push it past the cap and summon a scrollbar. */ +.client-form-modal .ant-tabs-content > .ant-form-item:last-child { + margin-bottom: 0; +} diff --git a/frontend/src/pages/clients/ClientFormModal.tsx b/frontend/src/pages/clients/ClientFormModal.tsx index 2ef3fc423..68a78c33a 100644 --- a/frontend/src/pages/clients/ClientFormModal.tsx +++ b/frontend/src/pages/clients/ClientFormModal.tsx @@ -49,6 +49,7 @@ import type { } from '@/hooks/useClients'; import { useFail2banStatusQuery, getLimitIpNotice } from '@/api/queries/useFail2banStatusQuery'; import { ClientFormSchema, ClientCreateFormSchema, type ClientFormValues } from '@/schemas/client'; +import './ClientFormModal.css'; const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL); const VMESS_SECURITY_OPTIONS = ['auto', 'aes-128-gcm', 'chacha20-poly1305'] as const; @@ -803,6 +804,7 @@ export default function ClientFormModal({ open={open} title={isEdit ? t('pages.clients.editClient') : t('pages.clients.addClient')} destroyOnHidden + className="client-form-modal" width={720} zIndex={CLIENT_FORM_MODAL_Z_INDEX} style={{ top: 20 }} diff --git a/frontend/src/test/storybook-theme.test.tsx b/frontend/src/test/storybook-theme.test.tsx index 5ebd6aa76..275545c4e 100644 --- a/frontend/src/test/storybook-theme.test.tsx +++ b/frontend/src/test/storybook-theme.test.tsx @@ -1,8 +1,8 @@ -import { render } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import { afterEach, expect, test } from 'vitest'; import { withTheme } from '../../.storybook/preview'; -import { ThemeProvider } from '@/hooks/useTheme'; +import { ThemeProvider, useTheme } from '@/hooks/useTheme'; function Story() { return
Story
; @@ -14,9 +14,37 @@ function StorybookTheme({ theme }: { theme: 'light' | 'dark' }) { > as Parameters[1]); } +function ThemeToggle() { + const { toggleTheme } = useTheme(); + return ; +} + afterEach(() => { document.body.className = ''; document.documentElement.removeAttribute('data-theme'); + document.documentElement.style.colorScheme = ''; +}); + +// Without color-scheme the browser paints native scrollbars light inside dark +// modals, e.g. the Edit Client body. +test('native scrollbars follow the panel theme', () => { + const { getByRole } = render( + + + , + ); + expect(document.documentElement.style.colorScheme).toBe('dark'); + + fireEvent.click(getByRole('button')); + expect(document.documentElement.style.colorScheme).toBe('light'); +}); + +test('native scrollbars follow the Storybook theme', () => { + const { rerender } = render(); + expect(document.documentElement.style.colorScheme).toBe('light'); + + rerender(); + expect(document.documentElement.style.colorScheme).toBe('dark'); }); test('preserves unrelated body classes when applying the Storybook theme', () => {