refactor: new settings page design and new pill component

This commit is contained in:
simosmik
2026-03-10 21:02:32 +00:00
parent f4777c139f
commit 8ddeeb0ce8
30 changed files with 781 additions and 587 deletions

View File

@@ -4,24 +4,24 @@ import { cn } from '../../../lib/utils';
// Keep visual variants centralized so all button usages stay consistent.
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium touch-manipulation transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90 active:bg-primary/80',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90 active:bg-destructive/80',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground active:bg-accent/80',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80 active:bg-secondary/70',
ghost: 'hover:bg-accent hover:text-accent-foreground active:bg-accent/80',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3 text-sm',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10',
},
},
defaultVariants: {

View File

@@ -1,5 +1,6 @@
import { Moon, Sun } from 'lucide-react';
import { useTheme } from '../../../contexts/ThemeContext';
import { cn } from '../../../lib/utils';
type DarkModeToggleProps = {
checked?: boolean;
@@ -13,7 +14,6 @@ function DarkModeToggle({
ariaLabel = 'Toggle dark mode',
}: DarkModeToggleProps) {
const { isDarkMode, toggleDarkMode } = useTheme();
// Support controlled usage while keeping ThemeContext as the default source of truth.
const isControlled = typeof checked === 'boolean' && typeof onToggle === 'function';
const isEnabled = isControlled ? checked : isDarkMode;
@@ -29,21 +29,26 @@ function DarkModeToggle({
return (
<button
onClick={handleToggle}
className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-gray-700 dark:focus:ring-offset-gray-900"
className={cn(
'relative inline-flex h-7 w-12 flex-shrink-0 touch-manipulation cursor-pointer items-center rounded-full border-2 transition-colors duration-200',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background',
isEnabled ? 'border-primary bg-primary' : 'border-border bg-muted',
)}
role="switch"
aria-checked={isEnabled}
aria-label={ariaLabel}
>
<span className="sr-only">{ariaLabel}</span>
<span
className={`${
isEnabled ? 'translate-x-7' : 'translate-x-1'
} flex h-6 w-6 transform items-center justify-center rounded-full bg-white shadow-lg transition-transform duration-200`}
className={cn(
'flex h-5 w-5 transform items-center justify-center rounded-full shadow-sm transition-transform duration-200',
isEnabled ? 'translate-x-[22px] bg-white' : 'translate-x-[2px] bg-foreground/60 dark:bg-foreground/80',
)}
>
{isEnabled ? (
<Moon className="h-3.5 w-3.5 text-gray-700" />
<Moon className="h-3 w-3 text-primary" />
) : (
<Sun className="h-3.5 w-3.5 text-yellow-500" />
<Sun className="h-3 w-3 text-white dark:text-background" />
)}
</span>
</button>

View File

@@ -28,15 +28,15 @@ export default function LanguageSelector({ compact = false }: LanguageSelectorPr
// Compact style for QuickSettingsPanel
if (compact) {
return (
<div className="flex items-center justify-between rounded-lg border border-transparent bg-gray-50 p-3 transition-colors hover:border-gray-300 hover:bg-gray-100 dark:bg-gray-800 dark:hover:border-gray-600 dark:hover:bg-gray-700">
<span className="flex items-center gap-2 text-sm text-gray-900 dark:text-white">
<Languages className="h-4 w-4 text-gray-600 dark:text-gray-400" />
<div className="flex items-center justify-between rounded-lg border border-transparent bg-muted/50 p-3 transition-colors hover:border-border hover:bg-accent">
<span className="flex items-center gap-2 text-sm text-foreground">
<Languages className="h-4 w-4 text-muted-foreground" />
{t('account.language')}
</span>
<select
value={i18n.language}
onChange={handleLanguageChange}
className="w-[100px] rounded-lg border border-gray-300 bg-gray-50 p-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:focus:ring-blue-400"
className="w-[100px] rounded-lg border border-input bg-card p-2 text-sm text-foreground focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary"
>
{languages.map((lang) => (
<option key={lang.value} value={lang.value}>
@@ -50,28 +50,26 @@ export default function LanguageSelector({ compact = false }: LanguageSelectorPr
// Full style for Settings page
return (
<div className="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/50">
<div className="flex items-center justify-between">
<div>
<div className="mb-1 font-medium text-gray-900 dark:text-gray-100">
{t('account.languageLabel')}
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
{t('account.languageDescription')}
</div>
<div className="flex items-center justify-between px-4 py-3.5">
<div>
<div className="text-sm font-medium text-foreground">
{t('account.languageLabel')}
</div>
<div className="mt-0.5 text-xs text-muted-foreground">
{t('account.languageDescription')}
</div>
<select
value={i18n.language}
onChange={handleLanguageChange}
className="w-36 rounded-lg border border-gray-300 bg-gray-50 p-2 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100"
>
{languages.map((lang) => (
<option key={lang.value} value={lang.value}>
{lang.nativeName}
</option>
))}
</select>
</div>
<select
value={i18n.language}
onChange={handleLanguageChange}
className="w-36 rounded-lg border border-input bg-card p-2 text-sm text-foreground focus:border-primary focus:ring-1 focus:ring-primary"
>
{languages.map((lang) => (
<option key={lang.value} value={lang.value}>
{lang.nativeName}
</option>
))}
</select>
</div>
);
}

View File

@@ -0,0 +1,41 @@
import type { ReactNode } from 'react';
import { cn } from '../../../lib/utils';
/* ── Container ─────────────────────────────────────────────────── */
type PillBarProps = {
children: ReactNode;
className?: string;
};
export function PillBar({ children, className }: PillBarProps) {
return (
<div className={cn('inline-flex items-center gap-[2px] rounded-lg bg-muted/60 p-[3px]', className)}>
{children}
</div>
);
}
/* ── Individual pill button ────────────────────────────────────── */
type PillProps = {
isActive: boolean;
onClick: () => void;
children: ReactNode;
className?: string;
};
export function Pill({ isActive, onClick, children, className }: PillProps) {
return (
<button
onClick={onClick}
className={cn(
'flex touch-manipulation items-center gap-1.5 rounded-md px-3 py-2 text-sm font-medium transition-all duration-150',
isActive
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground active:bg-background/50',
className,
)}
>
{children}
</button>
);
}

View File

@@ -4,3 +4,4 @@ export { default as DarkModeToggle } from './DarkModeToggle';
export { Input } from './Input';
export { ScrollArea } from './ScrollArea';
export { default as Tooltip } from './Tooltip';
export { PillBar, Pill } from './PillBar';