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', () => {