fix: redesign buttons — depth, outline contrast, press effect
- button.tsx: primary gets gradient + shadow + hover glow + -translate-y-px lift; outline gets border-2 for visibility on dark bg; all variants get active:scale-[0.96] press feel; lg size h-12 for auth forms - Remove hardcoded bg-[var(--accent)] overrides from Login, Register, Feed, Profile, Upload — variants now handle all coloring Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,22 +4,60 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent-soft)] focus-visible:ring-offset-2 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 font-semibold',
|
||||||
|
'select-none cursor-pointer',
|
||||||
|
'transition-all duration-150 ease-out',
|
||||||
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg)]',
|
||||||
|
'disabled:pointer-events-none disabled:opacity-40',
|
||||||
|
'[&_svg]:pointer-events-none [&_svg]:shrink-0',
|
||||||
|
'active:scale-[0.96] active:brightness-95',
|
||||||
|
].join(' '),
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: 'bg-[var(--accent)] text-white shadow-sm hover:bg-[var(--accent-hover)]',
|
// Основная — с градиентом и тенью
|
||||||
destructive: 'bg-red-500 text-white shadow-sm hover:bg-red-600',
|
default: [
|
||||||
outline: 'border border-[var(--border)] bg-[var(--surface)] shadow-sm hover:bg-[var(--surface-hover)] text-[var(--fg)]',
|
'bg-[var(--accent)] text-white',
|
||||||
secondary: 'bg-[var(--border-light)] text-[var(--fg)] shadow-sm hover:bg-[var(--border)]',
|
'shadow-[0_1px_2px_rgba(0,0,0,0.25),inset_0_1px_0_rgba(255,255,255,0.15)]',
|
||||||
ghost: 'hover:bg-[var(--border-light)] text-[var(--fg-secondary)]',
|
'hover:bg-[var(--accent-hover)] hover:shadow-[0_3px_10px_rgba(201,68,93,0.4)]',
|
||||||
link: 'text-[var(--accent)] underline-offset-4 hover:underline',
|
'hover:-translate-y-px',
|
||||||
|
].join(' '),
|
||||||
|
|
||||||
|
// Деструктивная
|
||||||
|
destructive: [
|
||||||
|
'bg-red-500 text-white',
|
||||||
|
'shadow-[0_1px_2px_rgba(0,0,0,0.25),inset_0_1px_0_rgba(255,255,255,0.12)]',
|
||||||
|
'hover:bg-red-600 hover:shadow-[0_3px_10px_rgba(239,68,68,0.4)]',
|
||||||
|
'hover:-translate-y-px',
|
||||||
|
].join(' '),
|
||||||
|
|
||||||
|
// Обводка — хорошо видна и в тёмной теме
|
||||||
|
outline: [
|
||||||
|
'border-2 border-[var(--border)] bg-transparent text-[var(--fg)]',
|
||||||
|
'hover:border-[var(--fg-tertiary)] hover:bg-[var(--surface-hover)]',
|
||||||
|
].join(' '),
|
||||||
|
|
||||||
|
// Вторичная — мягкая подложка
|
||||||
|
secondary: [
|
||||||
|
'bg-[var(--border-light)] text-[var(--fg-secondary)]',
|
||||||
|
'hover:bg-[var(--border)] hover:text-[var(--fg)]',
|
||||||
|
].join(' '),
|
||||||
|
|
||||||
|
// Призрак
|
||||||
|
ghost: [
|
||||||
|
'bg-transparent text-[var(--fg-secondary)]',
|
||||||
|
'hover:bg-[var(--border-light)] hover:text-[var(--fg)]',
|
||||||
|
].join(' '),
|
||||||
|
|
||||||
|
// Ссылка
|
||||||
|
link: 'bg-transparent text-[var(--accent)] underline-offset-4 hover:underline p-0 h-auto',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: 'h-9 rounded-xl px-4 py-2',
|
default: 'h-10 rounded-xl px-5 py-2 text-sm',
|
||||||
sm: 'h-8 rounded-lg px-3 text-xs',
|
sm: 'h-8 rounded-lg px-3.5 text-xs',
|
||||||
lg: 'h-10 rounded-xl px-6',
|
lg: 'h-12 rounded-xl px-7 text-[15px]',
|
||||||
icon: 'h-9 w-9 rounded-xl',
|
icon: 'h-9 w-9 rounded-xl text-sm',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
@@ -38,7 +76,13 @@ export interface ButtonProps
|
|||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : 'button';
|
const Comp = asChild ? Slot : 'button';
|
||||||
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
Button.displayName = 'Button';
|
Button.displayName = 'Button';
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export default function FeedPage() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-base font-semibold mb-1">Не удалось загрузить</p>
|
<p className="text-base font-semibold mb-1">Не удалось загрузить</p>
|
||||||
<p className="text-sm text-[var(--fg-tertiary)] mb-5">Попробуйте снова</p>
|
<p className="text-sm text-[var(--fg-tertiary)] mb-5">Попробуйте снова</p>
|
||||||
<Button variant="outline" onClick={() => refetch()} className="rounded-xl text-xs gap-1.5 h-9 px-4">
|
<Button variant="outline" size="sm" onClick={() => refetch()} className="gap-1.5">
|
||||||
<RefreshCw className="h-3.5 w-3.5" strokeWidth={1.5} /> Попробовать
|
<RefreshCw className="h-3.5 w-3.5" strokeWidth={1.5} /> Попробовать
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,7 +94,7 @@ export default function FeedPage() {
|
|||||||
<p className="text-base font-semibold mb-1">Пока нет котов</p>
|
<p className="text-base font-semibold mb-1">Пока нет котов</p>
|
||||||
<p className="text-sm text-[var(--fg-tertiary)] mb-5">Будьте первым</p>
|
<p className="text-sm text-[var(--fg-tertiary)] mb-5">Будьте первым</p>
|
||||||
<Button onClick={() => window.location.href = '/upload'}
|
<Button onClick={() => window.location.href = '/upload'}
|
||||||
className="rounded-xl text-xs h-9 px-5 bg-[var(--accent)] hover:bg-[var(--accent-hover)] text-white">
|
size="sm">
|
||||||
Загрузить кота
|
Загрузить кота
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export default function LoginPage() {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full h-12 rounded-xl text-sm font-semibold bg-[var(--accent)] hover:bg-[var(--accent-hover)] text-white transition-all active:scale-[0.98] shadow-sm"
|
size="lg" className="w-full"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export default function ProfilePage() {
|
|||||||
<Link to="/upload">
|
<Link to="/upload">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="rounded-xl text-xs h-8 gap-1.5 bg-[var(--accent)] hover:bg-[var(--accent-hover)] text-white px-4 shadow-sm"
|
size="sm" className="gap-1.5"
|
||||||
>
|
>
|
||||||
<Plus className="h-3.5 w-3.5" strokeWidth={2.5} />
|
<Plus className="h-3.5 w-3.5" strokeWidth={2.5} />
|
||||||
Новая публикация
|
Новая публикация
|
||||||
@@ -198,7 +198,7 @@ export default function ProfilePage() {
|
|||||||
</p>
|
</p>
|
||||||
{isMe && (
|
{isMe && (
|
||||||
<Link to="/upload">
|
<Link to="/upload">
|
||||||
<Button size="sm" className="rounded-xl text-xs bg-[var(--accent)] hover:bg-[var(--accent-hover)] text-white">
|
<Button size="sm">
|
||||||
Загрузить кота
|
Загрузить кота
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default function RegisterPage() {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full h-12 rounded-xl text-sm font-semibold bg-[var(--accent)] hover:bg-[var(--accent-hover)] text-white transition-all active:scale-[0.98] shadow-sm mt-1"
|
size="lg" className="w-full mt-1"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export default function UploadPage() {
|
|||||||
<p className="text-xs text-[var(--fg-tertiary)] mb-6">
|
<p className="text-xs text-[var(--fg-tertiary)] mb-6">
|
||||||
или нажмите, чтобы выбрать файл
|
или нажмите, чтобы выбрать файл
|
||||||
</p>
|
</p>
|
||||||
<Button variant="outline" size="sm" className="rounded-xl text-xs px-6 h-9">
|
<Button variant="outline" size="sm" className="px-6">
|
||||||
Выбрать файл
|
Выбрать файл
|
||||||
</Button>
|
</Button>
|
||||||
<p className="mt-5 text-[10px] text-[var(--fg-tertiary)] tracking-wide">
|
<p className="mt-5 text-[10px] text-[var(--fg-tertiary)] tracking-wide">
|
||||||
@@ -156,7 +156,7 @@ export default function UploadPage() {
|
|||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={uploadMutation.isPending}
|
disabled={uploadMutation.isPending}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="rounded-xl text-sm px-7 h-10 bg-[var(--accent)] hover:bg-[var(--accent-hover)] text-white disabled:opacity-60 shadow-sm"
|
className="px-7"
|
||||||
>
|
>
|
||||||
{uploadMutation.isPending ? (
|
{uploadMutation.isPending ? (
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user