mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-23 14:01:32 +00:00
refactor: new settings page design and new pill component
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
41
src/shared/view/ui/PillBar.tsx
Normal file
41
src/shared/view/ui/PillBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user