refactor(ui): decouple settings modal trigger from modal logic

extract SettingsTrigger component to separate modal open control from SettingsModal.
update HomeContent to manage modal state and pass open/onOpenChange props.
remove redundant trigger button from SettingsModal for improved composability.

removes embedded trigger logic from SettingsModal, enabling external state
management and better integration with various UI layouts. also cleans up
playwright config by centralizing testIgnore for unit tests.
This commit is contained in:
Richard R 2026-05-30 15:13:15 -06:00
parent 8b45a4f6cd
commit 60c1540744
3 changed files with 45 additions and 31 deletions

View file

@ -16,6 +16,7 @@ if (process.env.CI === 'true') {
*/
export default defineConfig({
testDir: './tests',
testIgnore: '**/unit/**',
tsconfig: './tsconfig.json',
timeout: 30 * 1000,
outputDir: './tests/results',
@ -60,7 +61,6 @@ export default defineConfig({
{
name: 'firefox',
testIgnore: '**/unit/**',
use: {
...devices['Desktop Firefox'],
extraHTTPHeaders: { 'x-openreader-test-namespace': 'firefox' },
@ -69,7 +69,6 @@ export default defineConfig({
{
name: 'webkit',
testIgnore: '**/unit/**',
use: {
...devices['Desktop Safari'],
extraHTTPHeaders: { 'x-openreader-test-namespace': 'webkit' },

View file

@ -1,7 +1,8 @@
'use client';
import { useState } from 'react';
import { DocumentList } from '@/components/doclist/DocumentList';
import { SettingsModal } from '@/components/SettingsModal';
import { SettingsModal, SettingsTrigger } from '@/components/SettingsModal';
import { UserMenu } from '@/components/auth/UserMenu';
const Brand = () => (
@ -14,20 +15,24 @@ const Brand = () => (
</div>
);
const AppActions = () => (
<div className="flex flex-col gap-0.5 w-full">
<SettingsModal
triggerLabel="Settings"
className="w-full justify-start gap-2 px-2 py-1 text-[12px] border-transparent hover:border-accent"
/>
<UserMenu variant="sidebar" />
</div>
);
export function HomeContent() {
const [settingsOpen, setSettingsOpen] = useState(false);
const appActions = (
<div className="flex flex-col gap-0.5 w-full">
<SettingsTrigger
triggerLabel="Settings"
className="w-full justify-start gap-2 px-2 py-1 text-[12px] border-transparent hover:border-accent"
onOpen={() => setSettingsOpen(true)}
/>
<UserMenu variant="sidebar" />
</div>
);
return (
<div className="w-full h-full">
<DocumentList brand={<Brand />} appActions={<AppActions />} />
<DocumentList brand={<Brand />} appActions={appActions} />
<SettingsModal open={settingsOpen} onOpenChange={setSettingsOpen} />
</div>
);
}

View file

@ -130,19 +130,42 @@ const SIDEBAR_SECTIONS: SidebarSection[] = [
type AdminSubTab = 'providers' | 'features';
export function SettingsModal({
export function SettingsTrigger({
className = '',
triggerLabel,
onOpen,
}: {
className?: string;
triggerLabel?: string;
onOpen: () => void;
}) {
return (
<Button
onClick={onOpen}
className={`inline-flex items-center py-1 px-2 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase hover:text-accent transition-transform transition-colors duration-200 ease-out hover:scale-[1.01] ${className}`}
aria-label="Settings"
tabIndex={0}
>
<SettingsIcon className="w-4 h-4 transition-transform duration-200 ease-out hover:scale-[1.01] hover:rotate-45" />
{triggerLabel && <span className="ml-2">{triggerLabel}</span>}
</Button>
);
}
export function SettingsModal({
open,
onOpenChange,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const runtimeConfig = useRuntimeConfig();
const enableDestructiveDelete = runtimeConfig.enableDestructiveDeleteActions;
const showAllProviderModels = runtimeConfig.showAllProviderModels;
const enableTTSProvidersTab = runtimeConfig.enableTtsProvidersTab;
const restrictUserApiKeys = runtimeConfig.restrictUserApiKeys;
const [isOpen, setIsOpen] = useState(false);
const isOpen = open;
const setIsOpen = onOpenChange;
const [isChangelogOpen, setIsChangelogOpen] = useState(false);
const [activeSection, setActiveSection] = useState<SectionId>(enableTTSProvidersTab ? 'api' : 'theme');
@ -215,7 +238,7 @@ export function SettingsModal({
if (changelogOpenSignal <= 0) return;
setIsOpen(true);
setIsChangelogOpen(true);
}, [changelogOpenSignal]);
}, [changelogOpenSignal, setIsOpen]);
useEffect(() => {
setLocalApiKey(apiKey);
@ -409,7 +432,7 @@ export function SettingsModal({
} else {
setCustomModelInput('');
}
}, [apiKey, baseUrl, providerRef, providerType, ttsModel, ttsInstructions, ttsModels]);
}, [apiKey, baseUrl, providerRef, providerType, ttsModel, ttsInstructions, ttsModels, setIsOpen]);
const [systemIsDark, setSystemIsDark] = useState(
typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches
@ -489,19 +512,6 @@ export function SettingsModal({
return (
<>
<Button
onClick={() => {
setIsOpen(true);
setIsChangelogOpen(false);
}}
className={`inline-flex items-center py-1 px-2 rounded-md border border-offbase bg-base text-foreground text-xs hover:bg-offbase hover:text-accent transition-transform transition-colors duration-200 ease-out hover:scale-[1.01] ${className}`}
aria-label="Settings"
tabIndex={0}
>
<SettingsIcon className="w-4 h-4 transition-transform duration-200 ease-out hover:scale-[1.01] hover:rotate-45" />
{triggerLabel && <span className="ml-2">{triggerLabel}</span>}
</Button>
<Transition appear show={isOpen} as={Fragment}>
<Dialog
as="div"