Skip to content

Commit 58fa6cf

Browse files
committed
feat: refactor Button component for improved clarity and semantic naming conventions
1 parent 178c86e commit 58fa6cf

File tree

4 files changed

+139
-32
lines changed

4 files changed

+139
-32
lines changed

apps/docs/stories/button.stories.tsx

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,19 @@ const meta: Meta<typeof Button> = {
1818
argTypes: {
1919
variant: {
2020
control: "select",
21-
options: ["primary", "secondary", "outline", "ghost", "destructive"],
21+
options: [
22+
"default",
23+
"destructive",
24+
"outline",
25+
"secondary",
26+
"ghost",
27+
"link",
28+
],
2229
description: "The visual style variant of the button",
2330
},
2431
size: {
2532
control: "select",
26-
options: ["sm", "md", "lg", "icon"],
33+
options: ["default", "sm", "lg", "icon", "icon-sm", "icon-lg"],
2734
description: "The size of the button",
2835
},
2936
asChild: {
@@ -41,13 +48,13 @@ export default meta;
4148
type Story = StoryObj<typeof Button>;
4249

4350
/**
44-
* Default Primary Button
51+
* Default Button
4552
*/
46-
export const Primary: Story = {
53+
export const Default: Story = {
4754
args: {
48-
children: "Primary Button",
49-
variant: "primary",
50-
size: "md",
55+
children: "Button",
56+
variant: "default",
57+
size: "default",
5158
},
5259
};
5360

@@ -111,6 +118,18 @@ export const Large: Story = {
111118
},
112119
};
113120

121+
/**
122+
* Link Variant
123+
*
124+
* Renders as a text link with underline on hover
125+
*/
126+
export const Link: Story = {
127+
args: {
128+
children: "Link Button",
129+
variant: "link",
130+
},
131+
};
132+
114133
/**
115134
* Icon Button (Square)
116135
*/
@@ -121,6 +140,26 @@ export const Icon: Story = {
121140
},
122141
};
123142

143+
/**
144+
* Small Icon Button
145+
*/
146+
export const IconSmall: Story = {
147+
args: {
148+
children: "×",
149+
size: "icon-sm",
150+
},
151+
};
152+
153+
/**
154+
* Large Icon Button
155+
*/
156+
export const IconLarge: Story = {
157+
args: {
158+
children: "☰",
159+
size: "icon-lg",
160+
},
161+
};
162+
124163
/**
125164
* Disabled State
126165
*/
@@ -158,11 +197,12 @@ export const AsLink: Story = {
158197
export const AllVariants: Story = {
159198
render: () => (
160199
<div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
161-
<Button variant="primary">Primary</Button>
162-
<Button variant="secondary">Secondary</Button>
200+
<Button variant="default">Default</Button>
201+
<Button variant="destructive">Destructive</Button>
163202
<Button variant="outline">Outline</Button>
203+
<Button variant="secondary">Secondary</Button>
164204
<Button variant="ghost">Ghost</Button>
165-
<Button variant="destructive">Destructive</Button>
205+
<Button variant="link">Link</Button>
166206
</div>
167207
),
168208
};
@@ -174,11 +214,32 @@ export const AllVariants: Story = {
174214
*/
175215
export const AllSizes: Story = {
176216
render: () => (
177-
<div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
217+
<div
218+
style={{
219+
display: "flex",
220+
gap: "1rem",
221+
alignItems: "center",
222+
flexWrap: "wrap",
223+
}}
224+
>
178225
<Button size="sm">Small</Button>
179-
<Button size="md">Medium</Button>
226+
<Button size="default">Default</Button>
180227
<Button size="lg">Large</Button>
228+
</div>
229+
),
230+
};
231+
232+
/**
233+
* All Icon Sizes Showcase
234+
*
235+
* Shows all icon button sizes side by side
236+
*/
237+
export const AllIconSizes: Story = {
238+
render: () => (
239+
<div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
240+
<Button size="icon-sm">×</Button>
181241
<Button size="icon"></Button>
242+
<Button size="icon-lg"></Button>
182243
</div>
183244
),
184245
};

packages/react/src/components/button/button.spec.tsx

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,12 @@ describe("Button", () => {
3535

3636
it("renders all variant options", () => {
3737
const variants = [
38-
"primary",
39-
"secondary",
38+
"default",
39+
"destructive",
4040
"outline",
41+
"secondary",
4142
"ghost",
42-
"destructive",
43+
"link",
4344
] as const;
4445

4546
variants.forEach((variant) => {
@@ -51,7 +52,14 @@ describe("Button", () => {
5152
});
5253

5354
it("renders all size options", () => {
54-
const sizes = ["sm", "md", "lg", "icon"] as const;
55+
const sizes = [
56+
"default",
57+
"sm",
58+
"lg",
59+
"icon",
60+
"icon-sm",
61+
"icon-lg",
62+
] as const;
5563

5664
sizes.forEach((size) => {
5765
const { container } = render(<Button size={size}>Button</Button>);
@@ -162,6 +170,27 @@ describe("Button", () => {
162170
});
163171
});
164172

173+
describe("Link Variant", () => {
174+
it("renders link variant with underline styles", () => {
175+
const { container } = render(<Button variant="link">Link Button</Button>);
176+
const button = container.firstChild as HTMLElement;
177+
expect(button).toHaveClass("text-primary-500");
178+
expect(button).toHaveClass("underline-offset-4");
179+
});
180+
181+
it("link variant can be used with asChild for actual links", () => {
182+
render(
183+
<Button asChild variant="link">
184+
<a href="/docs">Read Docs</a>
185+
</Button>,
186+
);
187+
188+
const link = screen.getByRole("link", { name: /read docs/i });
189+
expect(link).toBeInTheDocument();
190+
expect(link).toHaveAttribute("href", "/docs");
191+
});
192+
});
193+
165194
describe("Polymorphism (asChild)", () => {
166195
it("renders as a link when asChild is true", () => {
167196
render(
@@ -177,7 +206,7 @@ describe("Button", () => {
177206

178207
it("applies button styles to child element", () => {
179208
const { container } = render(
180-
<Button asChild variant="primary">
209+
<Button asChild variant="default">
181210
<a href="/home">Link</a>
182211
</Button>,
183212
);

packages/react/src/components/button/button.tsx

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,45 +46,62 @@ const buttonVariants = cva(
4646
*
4747
* These use our design tokens from \@prism/tokens
4848
* mapped through Tailwind's \@theme directive
49+
*
50+
* Educational Note:
51+
* - 'default' is the primary action style (instead of 'primary')
52+
* - 'link' renders as text with underline, useful for in-text actions
53+
* - shadcn/ui uses this naming convention for better semantic clarity
4954
*/
5055
variant: {
51-
primary: [
56+
default: [
5257
"bg-primary-500 text-white",
5358
"hover:bg-primary-600",
5459
"focus-visible:ring-primary-500",
5560
].join(" "),
56-
secondary: [
57-
"bg-neutral-200 text-neutral-900",
58-
"hover:bg-neutral-300",
59-
"focus-visible:ring-neutral-500",
61+
destructive: [
62+
"bg-error-main text-white",
63+
"hover:bg-error-dark",
64+
"focus-visible:ring-error-main",
6065
].join(" "),
6166
outline: [
6267
"border-2 border-neutral-300 bg-transparent",
6368
"hover:bg-neutral-100",
6469
"focus-visible:ring-neutral-500",
6570
].join(" "),
71+
secondary: [
72+
"bg-neutral-200 text-neutral-900",
73+
"hover:bg-neutral-300",
74+
"focus-visible:ring-neutral-500",
75+
].join(" "),
6676
ghost: [
6777
"bg-transparent",
6878
"hover:bg-neutral-100",
6979
"focus-visible:ring-neutral-500",
7080
].join(" "),
71-
destructive: [
72-
"bg-error-main text-white",
73-
"hover:bg-error-dark",
74-
"focus-visible:ring-error-main",
81+
link: [
82+
"text-primary-500 underline-offset-4",
83+
"hover:underline",
84+
"focus-visible:ring-0", // Links don't need ring focus, underline is enough
7585
].join(" "),
7686
},
7787

7888
/**
7989
* Size variants
8090
*
8191
* Use spacing tokens from \@prism/tokens
92+
*
93+
* Educational Note:
94+
* - Icon buttons are square (equal width/height)
95+
* - Icon sizes (sm, md, lg) match their text button counterparts
96+
* - This creates visual consistency across your UI
8297
*/
8398
size: {
99+
default: "h-10 px-4 text-base",
84100
sm: "h-8 px-3 text-sm",
85-
md: "h-10 px-4 text-base",
86101
lg: "h-12 px-6 text-lg",
87-
icon: "h-10 w-10", // Square button for icons
102+
icon: "h-10 w-10", // Default icon size
103+
"icon-sm": "h-8 w-8", // Small icon button
104+
"icon-lg": "h-12 w-12", // Large icon button
88105
},
89106
},
90107

@@ -94,8 +111,8 @@ const buttonVariants = cva(
94111
* Applied when no variant props are specified
95112
*/
96113
defaultVariants: {
97-
variant: "primary",
98-
size: "md",
114+
variant: "default",
115+
size: "default",
99116
},
100117
},
101118
);

packages/vitest-config/react.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
import { defineConfig, mergeConfig } from "vitest/config";
1414
import react from "@vitejs/plugin-react";
1515

16-
import baseConfig from "./base";
16+
import baseConfig from "@prism/vitest-config/base";
1717

1818
export default mergeConfig(
1919
baseConfig,
2020
defineConfig({
2121
plugins: [react()],
2222

2323
test: {
24-
// Use happy-dom for React component testing
24+
// Use happy-dom for React component testing/
2525
// Faster and lighter than jsdom
2626
environment: "happy-dom",
2727

0 commit comments

Comments
 (0)