refactor(Button): distinct solid/outline variants, drop redundant secondary

This commit is contained in:
Ilia Mashkov
2026-07-14 18:02:21 +03:00
parent d479f3aabf
commit 611a9c1a5a
4 changed files with 17 additions and 25 deletions
@@ -56,7 +56,7 @@ export function ProjectCard({ title, year, description, tags, url, imageUrl, pri
))} ))}
</div> </div>
)} )}
<Button href={url} variant="primary" size="sm" className="self-start lg:w-full lg:self-auto text-center"> <Button href={url} variant="solid" size="sm" className="self-start lg:w-full lg:self-auto text-center">
View Project View Project
</Button> </Button>
</div> </div>
+6 -9
View File
@@ -13,11 +13,8 @@ type Story = StoryObj<typeof Button>;
export const AllVariants: Story = { export const AllVariants: Story = {
render: () => ( render: () => (
<div className="flex gap-4 flex-wrap p-8 bg-ochre-clay"> <div className="flex gap-4 flex-wrap p-8 bg-ochre-clay">
<Button variant="primary" size="md"> <Button variant="solid" size="md">
Primary Solid
</Button>
<Button variant="secondary" size="md">
Secondary
</Button> </Button>
<Button variant="outline" size="md"> <Button variant="outline" size="md">
Outline Outline
@@ -32,13 +29,13 @@ export const AllVariants: Story = {
export const Sizes: Story = { export const Sizes: Story = {
render: () => ( render: () => (
<div className="flex gap-4 items-center flex-wrap p-8 bg-ochre-clay"> <div className="flex gap-4 items-center flex-wrap p-8 bg-ochre-clay">
<Button variant="primary" size="sm"> <Button variant="solid" size="sm">
Small Small
</Button> </Button>
<Button variant="primary" size="md"> <Button variant="solid" size="md">
Medium Medium
</Button> </Button>
<Button variant="primary" size="lg"> <Button variant="solid" size="lg">
Large Large
</Button> </Button>
</div> </div>
@@ -47,7 +44,7 @@ export const Sizes: Story = {
export const Disabled: Story = { export const Disabled: Story = {
args: { args: {
variant: 'primary', variant: 'solid',
disabled: true, disabled: true,
children: 'Disabled', children: 'Disabled',
}, },
+2 -6
View File
@@ -14,14 +14,10 @@ describe('Button', () => {
}); });
}); });
describe('variants', () => { describe('variants', () => {
it('applies primary variant by default', () => { it('applies solid variant by default', () => {
render(<Button>Go</Button>); render(<Button>Go</Button>);
expect(screen.getByRole('button')).toHaveClass('bg-blue'); expect(screen.getByRole('button')).toHaveClass('bg-blue');
}); });
it('applies secondary variant', () => {
render(<Button variant="secondary">Go</Button>);
expect(screen.getByRole('button')).toHaveClass('bg-blue');
});
it('applies outline variant', () => { it('applies outline variant', () => {
render(<Button variant="outline">Go</Button>); render(<Button variant="outline">Go</Button>);
expect(screen.getByRole('button')).toHaveClass('bg-cream'); expect(screen.getByRole('button')).toHaveClass('bg-cream');
@@ -82,7 +78,7 @@ describe('Button', () => {
}); });
it('applies the same variant and size classes as button', () => { it('applies the same variant and size classes as button', () => {
render( render(
<Button href="/test" variant="primary" size="sm"> <Button href="/test" variant="solid" size="sm">
Go Go
</Button>, </Button>,
); );
+8 -9
View File
@@ -1,13 +1,13 @@
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from 'react'; import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from 'react';
import { cn } from '$shared/lib'; import { cn } from '$shared/lib';
export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost'; export type ButtonVariant = 'solid' | 'outline' | 'ghost';
export type ButtonSize = 'sm' | 'md' | 'lg'; export type ButtonSize = 'sm' | 'md' | 'lg';
type BaseProps = { type BaseProps = {
/** /**
* Visual variant * Visual variant
* @default 'primary' * @default 'solid'
*/ */
variant?: ButtonVariant; variant?: ButtonVariant;
/** /**
@@ -41,10 +41,9 @@ function isAnchorProps(props: RestButton | RestAnchor): props is RestAnchor {
} }
const VARIANTS = { const VARIANTS = {
primary: 'brutal-border bg-blue text-cream btn-shadow', solid: 'brutal-border bg-blue text-cream btn-shadow',
secondary: 'brutal-border bg-blue text-cream btn-shadow', // Reversed solid: same border + offset-block animation, cream fill instead of blue
outline: outline: 'brutal-border bg-cream text-blue btn-shadow',
'brutal-border border-blue/35 bg-cream text-blue hover:border-blue hover:bg-blue/10 active:bg-blue active:text-cream',
ghost: ghost:
'brutal-border bg-transparent text-blue btn-transition hover:-translate-x-0.5 hover:-translate-y-0.5 active:translate-x-0.5 active:translate-y-0.5', 'brutal-border bg-transparent text-blue btn-transition hover:-translate-x-0.5 hover:-translate-y-0.5 active:translate-x-0.5 active:translate-y-0.5',
} as const satisfies Record<ButtonVariant, string>; } as const satisfies Record<ButtonVariant, string>;
@@ -55,15 +54,15 @@ const SIZES = {
lg: 'px-8 py-4 text-lg', lg: 'px-8 py-4 text-lg',
} as const satisfies Record<ButtonSize, string>; } as const satisfies Record<ButtonSize, string>;
/* Elevation lives per-variant: primary/secondary use btn-shadow (static offset /* Elevation lives per-variant: solid uses btn-shadow (static offset block,
* block, button moves), ghost uses btn-transition + translate. */ * button moves), ghost uses btn-transition + translate. */
const BASE = 'cursor-pointer uppercase tracking-wider'; const BASE = 'cursor-pointer uppercase tracking-wider';
/** /**
* Brutalist button with variants and sizes. * Brutalist button with variants and sizes.
* Renders as <a> when href is provided, <button> otherwise. * Renders as <a> when href is provided, <button> otherwise.
*/ */
export function Button({ variant = 'primary', size = 'md', className, children, ...props }: Props) { export function Button({ variant = 'solid', size = 'md', className, children, ...props }: Props) {
const cls = cn(BASE, VARIANTS[variant], SIZES[size], className); const cls = cn(BASE, VARIANTS[variant], SIZES[size], className);
if (isAnchorProps(props)) { if (isAnchorProps(props)) {