refactor: system settings UI for consistent, compact layouts

Redesign the system settings interface to align with the rest of the console experience by using fixed header actions, removing redundant subtitles, respecting global content width, and standardizing responsive form layouts.

Introduce reusable settings layout primitives for forms, switch rows, grouped controls, nested control sections, title status indicators, and page action portals. Replace duplicated card-style switch markup with explicit compact components, improve nested switch readability, and reduce visual noise across authentication, billing, content, integrations, maintenance, models, and request-limit settings.

Also complete missing i18n translations, remove obsolete subtitle translation keys, refine i18n sync reporting, fix sidebar truncation for long labels, and verify the frontend with type checking and lint diagnostics.
This commit is contained in:
t0ng7u
2026-05-25 00:34:26 +08:00
parent 92a0959448
commit b08febaa3c
97 changed files with 2420 additions and 3032 deletions
@@ -16,9 +16,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { Info } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { SettingsPageTitleStatusPortal } from './settings-page-context'
type FormDirtyIndicatorProps = {
isDirty: boolean
@@ -26,7 +25,7 @@ type FormDirtyIndicatorProps = {
}
/**
* Visual indicator that the form has unsaved changes
* Compact page-title status indicator for unsaved form changes.
*
* @example
* ```tsx
@@ -41,14 +40,11 @@ export function FormDirtyIndicator({
if (!isDirty) return null
return (
<Alert
variant='default'
className='border-orange-500/50 bg-orange-50 dark:bg-orange-950/20'
>
<Info className='h-4 w-4 text-orange-600 dark:text-orange-500' />
<AlertDescription className='text-orange-800 dark:text-orange-400'>
{message ?? t('You have unsaved changes')}
</AlertDescription>
</Alert>
<SettingsPageTitleStatusPortal>
<span className='inline-flex h-5 items-center gap-1.5 rounded-full bg-amber-500/10 px-2 text-[11px] font-medium whitespace-nowrap text-amber-700 ring-1 ring-amber-500/20 ring-inset dark:bg-amber-400/10 dark:text-amber-300 dark:ring-amber-400/20'>
<span className='size-1.5 rounded-full bg-amber-500 dark:bg-amber-300' />
{message ? t(message) : t('Unsaved changes')}
</span>
</SettingsPageTitleStatusPortal>
)
}
@@ -16,6 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { cn } from '@/lib/utils'
import {
AccordionItem,
AccordionTrigger,
@@ -25,7 +26,6 @@ import {
type SettingsAccordionProps = {
value: string
title: string
description?: string
children: React.ReactNode
className?: string
}
@@ -33,18 +33,14 @@ type SettingsAccordionProps = {
export function SettingsAccordion({
value,
title,
description,
children,
className,
}: SettingsAccordionProps) {
return (
<AccordionItem value={value} className={className}>
<AccordionItem value={value} className={cn(className)}>
<AccordionTrigger className='hover:no-underline'>
<div className='flex flex-col gap-1 text-left'>
<div className='text-base font-semibold'>{title}</div>
{description && (
<div className='text-muted-foreground text-sm'>{description}</div>
)}
</div>
</AccordionTrigger>
<AccordionContent className='pt-4'>{children}</AccordionContent>
@@ -0,0 +1,182 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import type { ComponentProps, ReactNode } from 'react'
import { cn } from '@/lib/utils'
import { FormItem } from '@/components/ui/form'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
type SettingsFormGridProps = {
children: ReactNode
className?: string
}
type SettingsFormGridItemProps = SettingsFormGridProps & {
span?: 'default' | 'full'
}
type SettingsSwitchItemProps = ComponentProps<typeof FormItem>
type SettingsSwitchRowProps = ComponentProps<'div'>
type SettingsControlGroupProps = ComponentProps<'div'>
type SettingsControlChildrenProps = ComponentProps<'div'>
type SettingsSwitchFieldProps = SettingsSwitchRowProps & {
checked: boolean
onCheckedChange: (checked: boolean) => void
label: ReactNode
description?: ReactNode
disabled?: boolean
}
const settingsSwitchRowClassName =
'flex min-w-0 flex-row items-center justify-between gap-4 border-b py-2.5 last:border-b-0'
export function SettingsFormGrid(props: SettingsFormGridProps) {
return (
<div
data-settings-form-span='full'
className={cn(
'grid min-w-0 gap-x-5 gap-y-6 lg:grid-cols-2',
props.className
)}
>
{props.children}
</div>
)
}
export function SettingsFormGridItem(props: SettingsFormGridItemProps) {
return (
<div
data-settings-form-span={props.span === 'full' ? 'full' : undefined}
className={cn(
'min-w-0',
props.span === 'full' && 'lg:col-span-2',
props.className
)}
>
{props.children}
</div>
)
}
export function SettingsSwitchItem({
className,
...props
}: SettingsSwitchItemProps) {
return (
<FormItem
data-settings-form-span='full'
className={cn(settingsSwitchRowClassName, className)}
{...props}
/>
)
}
export function SettingsSwitchRow({
className,
...props
}: SettingsSwitchRowProps) {
return (
<div
data-settings-form-span='full'
className={cn(settingsSwitchRowClassName, className)}
{...props}
/>
)
}
export function SettingsSwitchField({
checked,
onCheckedChange,
label,
description,
disabled,
className,
...props
}: SettingsSwitchFieldProps) {
return (
<SettingsSwitchRow className={className} {...props}>
<SettingsSwitchContent>
<Label className='text-sm font-medium'>{label}</Label>
{description ? (
<p className='text-muted-foreground text-xs'>{description}</p>
) : null}
</SettingsSwitchContent>
<Switch
checked={checked}
onCheckedChange={onCheckedChange}
disabled={disabled}
/>
</SettingsSwitchRow>
)
}
export function SettingsSwitchContent(props: SettingsFormGridProps) {
return (
<div className={cn('min-w-0 space-y-0.5', props.className)}>
{props.children}
</div>
)
}
export function SettingsControlGroup({
className,
...props
}: SettingsControlGroupProps) {
return (
<div
data-settings-form-span='full'
className={cn(
'bg-muted/20 min-w-0 space-y-3 rounded-xl border px-3 py-2.5',
className
)}
{...props}
/>
)
}
export function SettingsControlChildren({
className,
...props
}: SettingsControlChildrenProps) {
return (
<div
className={cn('border-border/70 ml-2 min-w-0 border-l pl-3', className)}
{...props}
/>
)
}
export function SettingsForm({ className, ...props }: ComponentProps<'form'>) {
return (
<form
className={cn(
'grid min-w-0 gap-x-5 gap-y-6 lg:grid-cols-2',
'lg:[&>*:not([data-slot=form-item])]:col-span-2',
'lg:[&>[data-settings-form-span=full]]:col-span-2',
'lg:[&>[data-slot=alert]]:col-span-2',
'[&>[data-slot=form-item]]:min-w-0',
'lg:[&>[data-slot=form-item]:has(textarea)]:col-span-2',
'lg:[&>[data-slot=form-item]:has([data-slot=switch])]:col-span-2',
className
)}
{...props}
/>
)
}
@@ -0,0 +1,146 @@
/*
Copyright (C) 2023-2026 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import {
createContext,
useContext,
type ComponentProps,
type ReactNode,
type RefObject,
} from 'react'
import { RotateCcw, Save } from 'lucide-react'
import { createPortal } from 'react-dom'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
type SettingsPageContextValue = {
actionsContainer: HTMLDivElement | null
titleStatusContainer: HTMLSpanElement | null
suppressSectionHeader: boolean
}
const SettingsPageContext = createContext<SettingsPageContextValue>({
actionsContainer: null,
titleStatusContainer: null,
suppressSectionHeader: false,
})
type SettingsPageProviderProps = {
actionsContainer: HTMLDivElement | null
titleStatusContainer?: HTMLSpanElement | null
children: ReactNode
suppressSectionHeader?: boolean
}
export function SettingsPageProvider(props: SettingsPageProviderProps) {
return (
<SettingsPageContext.Provider
value={{
actionsContainer: props.actionsContainer,
titleStatusContainer: props.titleStatusContainer ?? null,
suppressSectionHeader: props.suppressSectionHeader ?? true,
}}
>
{props.children}
</SettingsPageContext.Provider>
)
}
export function useSuppressSettingsSectionHeader() {
return useContext(SettingsPageContext).suppressSectionHeader
}
type SettingsPageTitleStatusPortalProps = {
children: ReactNode
}
export function SettingsPageTitleStatusPortal(
props: SettingsPageTitleStatusPortalProps
) {
const { titleStatusContainer } = useContext(SettingsPageContext)
if (!titleStatusContainer) return null
return createPortal(props.children, titleStatusContainer)
}
type SettingsPageActionsPortalProps = {
children: ReactNode
}
export function SettingsPageActionsPortal(
props: SettingsPageActionsPortalProps
) {
const { actionsContainer } = useContext(SettingsPageContext)
if (!actionsContainer) return null
return createPortal(
<div className='flex flex-wrap items-center justify-end gap-2'>
{props.children}
</div>,
actionsContainer
)
}
type SettingsPageFormActionsProps = {
onSave: () => void
onReset?: () => void
isSaving?: boolean
isSaveDisabled?: boolean
isResetDisabled?: boolean
saveLabel?: string
savingLabel?: string
resetLabel?: string
resetVariant?: ComponentProps<typeof Button>['variant']
saveButtonRef?: RefObject<HTMLButtonElement | null>
}
export function SettingsPageFormActions(props: SettingsPageFormActionsProps) {
const { t } = useTranslation()
const saveLabel = props.isSaving
? (props.savingLabel ?? 'Saving...')
: (props.saveLabel ?? 'Save Changes')
return (
<SettingsPageActionsPortal>
{props.onReset && (
<Button
type='button'
size='sm'
variant={props.resetVariant ?? 'outline'}
onClick={props.onReset}
disabled={props.isResetDisabled || props.isSaving}
>
<RotateCcw data-icon='inline-start' />
<span>{t(props.resetLabel ?? 'Reset')}</span>
</Button>
)}
<Button
ref={props.saveButtonRef}
type='button'
size='sm'
onClick={props.onSave}
disabled={props.isSaving || props.isSaveDisabled}
>
<Save data-icon='inline-start' />
<span>{t(saveLabel)}</span>
</Button>
</SettingsPageActionsPortal>
)
}
@@ -16,13 +16,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useMemo, useState, type ReactNode } from 'react'
import { useParams } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { SectionPageLayout } from '@/components/layout'
import { useSystemOptions, getOptionValue } from '../hooks/use-system-options'
import type { SystemOption } from '../types'
import { SettingsPageProvider } from './settings-page-context'
type SettingsPageProps<
TSettings extends Record<string, string | number | boolean | unknown[]>,
TSectionId extends string,
TExtraArgs extends unknown[] = [],
> = {
routePath: string
defaultSettings: TSettings
@@ -30,9 +35,57 @@ type SettingsPageProps<
getSectionContent: (
sectionId: TSectionId,
settings: TSettings,
...extraArgs: unknown[]
) => React.ReactNode
extraArgs?: unknown[]
...extraArgs: TExtraArgs
) => ReactNode
getSectionMeta: (sectionId: TSectionId) => {
titleKey: string
}
extraArgs?: TExtraArgs
loadingMessage?: string
resolveSettings?: (
settings: TSettings,
raw: SystemOption[] | undefined
) => TSettings
}
type SettingsPageFrameProps = {
title: ReactNode
children: ReactNode
}
function SettingsPageFrame(props: SettingsPageFrameProps) {
const [actionsContainer, setActionsContainer] =
useState<HTMLDivElement | null>(null)
const [titleStatusContainer, setTitleStatusContainer] =
useState<HTMLSpanElement | null>(null)
return (
<SettingsPageProvider
actionsContainer={actionsContainer}
titleStatusContainer={titleStatusContainer}
>
<SectionPageLayout>
<SectionPageLayout.Title>
<span className='inline-flex max-w-full min-w-0 items-center gap-2 align-middle'>
<span className='truncate'>{props.title}</span>
<span
ref={setTitleStatusContainer}
className='inline-flex shrink-0'
/>
</span>
</SectionPageLayout.Title>
<SectionPageLayout.Actions>
<div
ref={setActionsContainer}
className='flex flex-wrap items-center justify-end gap-2'
/>
</SectionPageLayout.Actions>
<SectionPageLayout.Content>
<div className='flex w-full flex-col gap-4'>{props.children}</div>
</SectionPageLayout.Content>
</SectionPageLayout>
</SettingsPageProvider>
)
}
/**
@@ -42,39 +95,53 @@ type SettingsPageProps<
export function SettingsPage<
TSettings extends Record<string, string | number | boolean | unknown[]>,
TSectionId extends string,
TExtraArgs extends unknown[] = [],
>({
routePath,
defaultSettings,
defaultSection,
getSectionContent,
extraArgs = [],
}: SettingsPageProps<TSettings, TSectionId>) {
getSectionMeta,
extraArgs,
loadingMessage = 'Loading settings...',
resolveSettings,
}: SettingsPageProps<TSettings, TSectionId, TExtraArgs>) {
const { t } = useTranslation()
const { data, isLoading } = useSystemOptions()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const params = useParams({ from: routePath as any })
const activeSection = (params?.section ?? defaultSection) as TSectionId
const sectionMeta = getSectionMeta(activeSection)
const settings = useMemo(() => {
const baseSettings = getOptionValue(
data?.data,
defaultSettings
) as TSettings
return resolveSettings
? resolveSettings(baseSettings, data?.data)
: baseSettings
}, [data?.data, defaultSettings, resolveSettings])
if (isLoading) {
return (
<div className='flex items-center justify-center py-12'>
<div className='text-muted-foreground'>{t('Loading settings...')}</div>
</div>
<SettingsPageFrame title={t(sectionMeta.titleKey)}>
<div className='text-muted-foreground flex min-h-40 items-center justify-center text-sm'>
{t(loadingMessage)}
</div>
</SettingsPageFrame>
)
}
const settings = getOptionValue(data?.data, defaultSettings) as TSettings
const activeSection = (params?.section ?? defaultSection) as TSectionId
const sectionContent = getSectionContent(
activeSection,
settings,
...extraArgs
...((extraArgs ?? []) as TExtraArgs)
)
return (
<div className='flex h-full w-full flex-1 flex-col'>
<div className='faded-bottom h-full w-full overflow-y-auto scroll-smooth pe-4 pb-12'>
<div className='space-y-4'>{sectionContent}</div>
</div>
</div>
<SettingsPageFrame title={t(sectionMeta.titleKey)}>
{sectionContent}
</SettingsPageFrame>
)
}
@@ -16,10 +16,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { cn } from '@/lib/utils'
import { useSuppressSettingsSectionHeader } from './settings-page-context'
type SettingsSectionProps = {
title: string
titleProps?: React.HTMLAttributes<HTMLHeadingElement>
description?: string
children: React.ReactNode
className?: string
}
@@ -27,32 +29,23 @@ type SettingsSectionProps = {
export function SettingsSection({
title,
titleProps,
description,
children,
className,
}: SettingsSectionProps) {
const baseClassName = 'space-y-4'
const sectionClassName = className
? `${baseClassName} ${className}`
: baseClassName
const suppressHeader = useSuppressSettingsSectionHeader()
return (
<section className={sectionClassName}>
<div className='space-y-1'>
<h3
{...titleProps}
className={
titleProps?.className
? `text-base font-semibold ${titleProps.className}`
: 'text-base font-semibold'
}
>
{title}
</h3>
{description && (
<p className='text-muted-foreground text-sm'>{description}</p>
)}
</div>
<section className={cn('flex flex-col gap-4', className)}>
{!suppressHeader && (
<div className='flex flex-col gap-1'>
<h3
{...titleProps}
className={cn('text-base font-semibold', titleProps?.className)}
>
{title}
</h3>
</div>
)}
{children}
</section>
)