@varialkit/button 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs.md +173 -0
- package/examples.tsx +264 -0
- package/package.json +27 -0
- package/src/Button.scss +200 -0
- package/src/Button.tsx +64 -0
- package/src/Button.types.ts +19 -0
- package/src/index.ts +2 -0
package/docs.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Button
|
|
2
|
+
|
|
3
|
+
The Button component is a fundamental interactive element that triggers user actions. It supports several visual variants, size settings for spacing, and is designed to be highly versatile.
|
|
4
|
+
|
|
5
|
+
## How to Use
|
|
6
|
+
|
|
7
|
+
To use the Button, import it from the `@solara/button` package:
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { Button } from "@solara/button";
|
|
11
|
+
|
|
12
|
+
export function MyComponent() {
|
|
13
|
+
return <Button label="Click Me" />;
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Best Practices
|
|
18
|
+
|
|
19
|
+
- **Clarity is Key**: The button's label should clearly describe the action it performs.
|
|
20
|
+
- **Consistent Usage**: Use variants consistently across your application to create a predictable user experience.
|
|
21
|
+
- **Limit Primary Buttons**: Use the `primary` variant for the most important action on a page. Avoid using multiple primary buttons in the same view.
|
|
22
|
+
- **Destructive Actions**: Use the `destructive` prop for actions that are difficult or impossible to undo, such as deleting data.
|
|
23
|
+
|
|
24
|
+
## Props
|
|
25
|
+
|
|
26
|
+
The Button component accepts the following props:
|
|
27
|
+
|
|
28
|
+
| Prop | Type | Default | Description |
|
|
29
|
+
| ------------- | -------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
30
|
+
| `label` | `string` | _Required_* | The text content displayed inside the button. Required unless `iconOnly` is set. |
|
|
31
|
+
| `variant` | `"primary" | "default" | "tertiary" | "ghost" | "accent"` | `"default"` | The visual style of the button. See [Variants](#variants) for more details. |
|
|
32
|
+
| `size` | `"small" | "medium" | "large"` | `"medium"` | Controls the button's internal padding and font size. See [Size](#size) for more details. |
|
|
33
|
+
| `radius` | `"default" | "none" | "full"` | `"default"` | Controls the button's border radius. `"default"` is a standard radius, `"none"` is a sharp corner, and `"full"` is a pill shape. |
|
|
34
|
+
| `destructive` | `boolean` | `false` | When `true`, applies a destructive style to the button, indicating a potentially dangerous action. See [Destructive](#destructive). |
|
|
35
|
+
| `disabled` | `boolean` | `false` | When `true`, the button is not interactive and has a disabled style. |
|
|
36
|
+
| `iconLeft` | `SolaraIconName | IconProps` | `undefined` | Renders a leading icon before the label. |
|
|
37
|
+
| `iconRight` | `SolaraIconName | IconProps` | `undefined` | Renders a trailing icon after the label. |
|
|
38
|
+
| `iconOnly` | `SolaraIconName | IconProps` | `undefined` | Renders an icon-only button. Provide `aria-label` (or `label`) for accessibility. |
|
|
39
|
+
|
|
40
|
+
_Required unless `iconOnly` is set._
|
|
41
|
+
|
|
42
|
+
The Button also accepts standard `button` props such as `className`, `style`, `type`, `onClick`, and `aria-*`.
|
|
43
|
+
|
|
44
|
+
## Variants
|
|
45
|
+
|
|
46
|
+
The `variant` prop allows you to control the button's visual appearance.
|
|
47
|
+
|
|
48
|
+
### Primary
|
|
49
|
+
|
|
50
|
+
Primary buttons are used for the main call-to-action on a page. They should be used sparingly to draw attention to the most important action.
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<Button label="Submit" variant="primary" />
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Default
|
|
57
|
+
|
|
58
|
+
Default buttons are used for secondary actions. They have a visible border and are less prominent than primary buttons.
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
<Button label="Cancel" variant="default" />
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Tertiary
|
|
65
|
+
|
|
66
|
+
Tertiary buttons are used for less important actions. They have a transparent background and a border.
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
<Button label="Learn More" variant="tertiary" />
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Ghost
|
|
73
|
+
|
|
74
|
+
Ghost buttons are used for the least prominent actions. They have a transparent background and no border, making them suitable for clean, minimalist layouts.
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
<Button label="Dismiss" variant="ghost" />
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Accent
|
|
81
|
+
|
|
82
|
+
Accent buttons use the primary accent color to draw attention to important actions without being as strong as the primary button.
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
<Button label="New Feature" variant="accent" />
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Size
|
|
89
|
+
|
|
90
|
+
The `size` prop adjusts the button's padding and font size to fit different layout requirements.
|
|
91
|
+
|
|
92
|
+
### Small
|
|
93
|
+
|
|
94
|
+
Small buttons have reduced padding and a smaller font size, making them suitable for compact interfaces.
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
<Button label="Compact" size="small" />
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Medium
|
|
101
|
+
|
|
102
|
+
Medium buttons have standard padding and font size. This is the default setting.
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
<Button label="Standard" size="medium" />
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Large
|
|
109
|
+
|
|
110
|
+
Large buttons have increased padding and a larger font size, making them more prominent and easier to click on touch devices.
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
<Button label="Spacious" size="large" />
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Radius And Density Behavior
|
|
117
|
+
|
|
118
|
+
Button radius is token-driven and density-aware:
|
|
119
|
+
|
|
120
|
+
- default radius uses the scaled radius token path (currently based on `--radius-2` with `--radius-multiplier`)
|
|
121
|
+
- global density radius tuning in the docs controls updates button rounding in real time
|
|
122
|
+
- `radius="none"` always forces square corners
|
|
123
|
+
- `radius="full"` always forces pill corners
|
|
124
|
+
|
|
125
|
+
This means radius density tuning affects default buttons, while explicit shape modes intentionally override it.
|
|
126
|
+
|
|
127
|
+
## Destructive
|
|
128
|
+
|
|
129
|
+
The `destructive` prop can be combined with any variant to indicate a potentially dangerous action, such as deleting data. When `true`, the button will be styled with a red background and white text.
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
<Button label="Delete" destructive />
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Destructive Ghost
|
|
136
|
+
|
|
137
|
+
The `destructive` prop can also be combined with the `ghost` variant for a less intrusive but still clear destructive action.
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
<Button label="Delete" variant="ghost" destructive />
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Disabled State
|
|
144
|
+
|
|
145
|
+
When a button is disabled, it cannot be clicked and has a visually distinct style. This is useful for preventing actions that are not currently available.
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
<Button label="Saving..." disabled />
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Accessibility
|
|
152
|
+
|
|
153
|
+
The `Button` component is designed with accessibility in mind.
|
|
154
|
+
|
|
155
|
+
- **Keyboard Navigation**: The button is focusable and can be activated using the `Enter` or `Space` key.
|
|
156
|
+
- **Screen Readers**: The button's label is read by screen readers. If you are using an icon-only button, be sure to provide an `aria-label` (or `label`) for screen reader users.
|
|
157
|
+
|
|
158
|
+
## Icons
|
|
159
|
+
|
|
160
|
+
The Button component can render icons on the left or right of the label, or as an icon-only control.
|
|
161
|
+
Icons inherit the button text color by default, and button styles force icon strokes/fills to match `currentColor`.
|
|
162
|
+
You can pass full `IconProps` to adjust size or stroke width; color is always aligned to the button text.
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
<Button label="Back" iconLeft="arrow_chevron_left_16" />
|
|
166
|
+
<Button label="Next" iconRight="arrow_chevron_right_16" />
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Icon-Only
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
<Button iconOnly="arrow_line_up_16" aria-label="Upload" />
|
|
173
|
+
```
|
package/examples.tsx
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
import { Button } from "./src/Button";
|
|
4
|
+
import type { ButtonRadius, ButtonSize, ButtonVariant } from "./src/Button.types";
|
|
5
|
+
import type { SolaraIconName } from "@solara/icons";
|
|
6
|
+
import { iconNames } from "@solara/icons";
|
|
7
|
+
|
|
8
|
+
type StoryControlOption = {
|
|
9
|
+
label: string;
|
|
10
|
+
value: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type StoryControl = {
|
|
14
|
+
name: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
type: "select" | "text" | "boolean" | "number";
|
|
17
|
+
options?: Array<string | StoryControlOption>;
|
|
18
|
+
min?: number;
|
|
19
|
+
max?: number;
|
|
20
|
+
step?: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type StoryDefinition = {
|
|
24
|
+
title: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
render: (props: Record<string, unknown>) => ReactElement;
|
|
27
|
+
controls?: StoryControl[];
|
|
28
|
+
initialProps?: Record<string, unknown>;
|
|
29
|
+
showProps?: boolean;
|
|
30
|
+
applyPropsToPreview?: boolean;
|
|
31
|
+
code?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const stories: Record<string, StoryDefinition> = {
|
|
35
|
+
playground: {
|
|
36
|
+
title: "Playground",
|
|
37
|
+
description: "Tweak the props to explore the Button API.",
|
|
38
|
+
render: (props) => {
|
|
39
|
+
const iconLeft = (props.iconLeft as SolaraIconName) || undefined;
|
|
40
|
+
const iconRight = (props.iconRight as SolaraIconName) || undefined;
|
|
41
|
+
const iconOnly = (props.iconOnly as SolaraIconName) || undefined;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Button
|
|
45
|
+
label={(props.label as string) ?? "Primary"}
|
|
46
|
+
variant={props.variant as ButtonVariant}
|
|
47
|
+
size={props.size as ButtonSize}
|
|
48
|
+
radius={props.radius as ButtonRadius}
|
|
49
|
+
destructive={props.destructive as boolean}
|
|
50
|
+
disabled={props.disabled as boolean}
|
|
51
|
+
iconLeft={iconLeft}
|
|
52
|
+
iconRight={iconRight}
|
|
53
|
+
iconOnly={iconOnly}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
controls: [
|
|
58
|
+
{ name: "label", label: "Label", type: "text" },
|
|
59
|
+
{
|
|
60
|
+
name: "variant",
|
|
61
|
+
label: "Variant",
|
|
62
|
+
type: "select",
|
|
63
|
+
options: ["default", "primary", "tertiary", "ghost", "accent"]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "size",
|
|
67
|
+
label: "Size",
|
|
68
|
+
type: "select",
|
|
69
|
+
options: ["small", "medium", "large"]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "radius",
|
|
73
|
+
label: "Radius",
|
|
74
|
+
type: "select",
|
|
75
|
+
options: ["default", "none", "full"]
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "destructive",
|
|
79
|
+
label: "Destructive",
|
|
80
|
+
type: "boolean"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "disabled",
|
|
84
|
+
label: "Disabled",
|
|
85
|
+
type: "boolean"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "iconLeft",
|
|
89
|
+
label: "Icon Left",
|
|
90
|
+
type: "select",
|
|
91
|
+
options: ["", ...iconNames]
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "iconRight",
|
|
95
|
+
label: "Icon Right",
|
|
96
|
+
type: "select",
|
|
97
|
+
options: ["", ...iconNames]
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "iconOnly",
|
|
101
|
+
label: "Icon Only",
|
|
102
|
+
type: "select",
|
|
103
|
+
options: ["", ...iconNames]
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
initialProps: {
|
|
107
|
+
label: "Primary",
|
|
108
|
+
variant: "primary",
|
|
109
|
+
size: "medium",
|
|
110
|
+
radius: "default",
|
|
111
|
+
destructive: false,
|
|
112
|
+
disabled: false,
|
|
113
|
+
iconLeft: "",
|
|
114
|
+
iconRight: "",
|
|
115
|
+
iconOnly: ""
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
overview: {
|
|
119
|
+
title: "Overview",
|
|
120
|
+
showProps: true,
|
|
121
|
+
render: () => (
|
|
122
|
+
<div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
|
|
123
|
+
<Button label="Primary" variant="primary" />
|
|
124
|
+
<Button label="Default" variant="default" />
|
|
125
|
+
</div>
|
|
126
|
+
),
|
|
127
|
+
code: `import { Button } from "@solara/button";
|
|
128
|
+
|
|
129
|
+
export function Example() {
|
|
130
|
+
return (
|
|
131
|
+
<div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
|
|
132
|
+
<Button label="Primary" variant="primary" />
|
|
133
|
+
<Button label="Default" variant="default" />
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
`
|
|
138
|
+
},
|
|
139
|
+
variants: {
|
|
140
|
+
title: "Variants",
|
|
141
|
+
showProps: false,
|
|
142
|
+
render: () => (
|
|
143
|
+
<div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
|
|
144
|
+
<Button label="Primary" variant="primary" />
|
|
145
|
+
<Button label="Default" variant="default" />
|
|
146
|
+
<Button label="Tertiary" variant="tertiary" />
|
|
147
|
+
<Button label="Ghost" variant="ghost" />
|
|
148
|
+
</div>
|
|
149
|
+
),
|
|
150
|
+
code: `import { Button } from "@solara/button";
|
|
151
|
+
|
|
152
|
+
export function Example() {
|
|
153
|
+
return (
|
|
154
|
+
<div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
|
|
155
|
+
<Button label="Primary" variant="primary" />
|
|
156
|
+
<Button label="Default" variant="default" />
|
|
157
|
+
<Button label="Tertiary" variant="tertiary" />
|
|
158
|
+
<Button label="Ghost" variant="ghost" />
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
`
|
|
163
|
+
},
|
|
164
|
+
icons: {
|
|
165
|
+
title: "Icons",
|
|
166
|
+
showProps: false,
|
|
167
|
+
render: () => (
|
|
168
|
+
<div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
|
|
169
|
+
<Button label="Back" iconLeft="arrow_chevron_left_16" />
|
|
170
|
+
<Button label="Next" iconRight="arrow_chevron_right_16" variant="primary" />
|
|
171
|
+
<Button label="Swap" iconLeft="arrow_swap_16" iconRight="arrow_swap_16" variant="tertiary" />
|
|
172
|
+
</div>
|
|
173
|
+
),
|
|
174
|
+
code: `import { Button } from "@solara/button";
|
|
175
|
+
|
|
176
|
+
export function Example() {
|
|
177
|
+
return (
|
|
178
|
+
<div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
|
|
179
|
+
<Button label="Back" iconLeft="arrow_chevron_left_16" />
|
|
180
|
+
<Button label="Next" iconRight="arrow_chevron_right_16" variant="primary" />
|
|
181
|
+
<Button label="Swap" iconLeft="arrow_swap_16" iconRight="arrow_swap_16" variant="tertiary" />
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
`
|
|
186
|
+
},
|
|
187
|
+
iconOnly: {
|
|
188
|
+
title: "Icon Only",
|
|
189
|
+
showProps: false,
|
|
190
|
+
render: () => (
|
|
191
|
+
<div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
|
|
192
|
+
<Button iconOnly="arrow_line_up_16" aria-label="Upload" />
|
|
193
|
+
<Button iconOnly="arrow_line_down_16" aria-label="Download" variant="primary" />
|
|
194
|
+
<Button iconOnly="arrow_line_rotate_right_16" aria-label="Refresh" variant="ghost" />
|
|
195
|
+
</div>
|
|
196
|
+
),
|
|
197
|
+
code: `import { Button } from "@solara/button";
|
|
198
|
+
|
|
199
|
+
export function Example() {
|
|
200
|
+
return (
|
|
201
|
+
<div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
|
|
202
|
+
<Button iconOnly="arrow_line_up_16" aria-label="Upload" />
|
|
203
|
+
<Button iconOnly="arrow_line_down_16" aria-label="Download" variant="primary" />
|
|
204
|
+
<Button iconOnly="arrow_line_rotate_right_16" aria-label="Refresh" variant="ghost" />
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
`
|
|
209
|
+
},
|
|
210
|
+
destructive: {
|
|
211
|
+
title: "Destructive",
|
|
212
|
+
showProps: false,
|
|
213
|
+
render: () => (
|
|
214
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
215
|
+
<div style={{ display: "flex", gap: "1rem" }}>
|
|
216
|
+
<Button label="Delete" variant="primary" destructive />
|
|
217
|
+
<Button label="Delete" destructive />
|
|
218
|
+
</div>
|
|
219
|
+
<div style={{ display: "flex", gap: "1rem" }}>
|
|
220
|
+
<Button label="Delete" variant="tertiary" destructive />
|
|
221
|
+
<Button label="Delete" variant="ghost" destructive />
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
),
|
|
225
|
+
code: `import { Button } from "@solara/button";
|
|
226
|
+
|
|
227
|
+
export function Example() {
|
|
228
|
+
return (
|
|
229
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
230
|
+
<div style={{ display: "flex", gap: "1rem" }}>
|
|
231
|
+
<Button label="Delete" variant="primary" destructive />
|
|
232
|
+
<Button label="Delete" destructive />
|
|
233
|
+
</div>
|
|
234
|
+
<div style={{ display: "flex", gap: "1rem" }}>
|
|
235
|
+
<Button label="Delete" variant="tertiary" destructive />
|
|
236
|
+
<Button label="Delete" variant="ghost" destructive />
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
`
|
|
242
|
+
},
|
|
243
|
+
disabled: {
|
|
244
|
+
title: "Disabled",
|
|
245
|
+
showProps: false,
|
|
246
|
+
render: () => (
|
|
247
|
+
<div style={{ display: "flex", gap: "1rem" }}>
|
|
248
|
+
<Button label="Disabled" disabled />
|
|
249
|
+
<Button label="Disabled" variant="primary" disabled />
|
|
250
|
+
</div>
|
|
251
|
+
),
|
|
252
|
+
code: `import { Button } from "@solara/button";
|
|
253
|
+
|
|
254
|
+
export function Example() {
|
|
255
|
+
return (
|
|
256
|
+
<div style={{ display: "flex", gap: "1rem" }}>
|
|
257
|
+
<Button label="Disabled" disabled />
|
|
258
|
+
<Button label="Disabled" variant="primary" disabled />
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
`
|
|
263
|
+
}
|
|
264
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@varialkit/button",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./examples": "./examples/index.tsx"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@varialkit/icons": "0.1.1"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"docs.md",
|
|
17
|
+
"examples",
|
|
18
|
+
"examples.tsx"
|
|
19
|
+
],
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"react": "^19.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/react": "19.0.10",
|
|
25
|
+
"react": "19.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/Button.scss
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
|
|
2
|
+
.sol-button {
|
|
3
|
+
border: 1px solid transparent;
|
|
4
|
+
// Use a slightly larger base radius so density radius tuning is visibly apparent on buttons.
|
|
5
|
+
--button-radius: var(--radius-2-scaled, calc(var(--radius-2) * var(--radius-multiplier, 1)));
|
|
6
|
+
border-radius: var(--button-radius);
|
|
7
|
+
cursor: pointer;
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, opacity 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease;
|
|
12
|
+
--button-padding-y: var(--space-2);
|
|
13
|
+
--button-padding-x: var(--space-3);
|
|
14
|
+
--button-font-weight: var(--font-weight-body, 500);
|
|
15
|
+
--button-font-size: var(--font-size-body-scaled);
|
|
16
|
+
--button-line-height: var(--line-height-caption-scaled);
|
|
17
|
+
--button-text-decoration: var(--text-decoration-body, none);
|
|
18
|
+
--button-active-text-decoration: var(--text-decoration-body, none);
|
|
19
|
+
--button-icon-gap: var(--space-2);
|
|
20
|
+
padding: calc(var(--button-padding-y) * var(--spacing-multiplier))
|
|
21
|
+
calc(var(--button-padding-x) * var(--spacing-multiplier));
|
|
22
|
+
font-size: var(--button-font-size);
|
|
23
|
+
line-height: var(--button-line-height);
|
|
24
|
+
font-weight: var(--button-font-weight);
|
|
25
|
+
text-decoration: var(--button-text-decoration);
|
|
26
|
+
gap: calc(var(--button-icon-gap) * var(--spacing-multiplier));
|
|
27
|
+
|
|
28
|
+
&:focus-visible {
|
|
29
|
+
outline: none;
|
|
30
|
+
box-shadow: 0 0 0 3px var(--color-focus-halo);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
&:disabled {
|
|
34
|
+
opacity: 0.5;
|
|
35
|
+
cursor: not-allowed;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&[data-radius="none"] {
|
|
39
|
+
border-radius: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&[data-radius="full"] {
|
|
43
|
+
border-radius: 9999px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
&[data-size="small"] {
|
|
47
|
+
--button-padding-y: var(--space-1);
|
|
48
|
+
--button-padding-x: var(--space-2);
|
|
49
|
+
--button-font-weight: var(--font-weight-footnote, 500);
|
|
50
|
+
--button-font-size: var(--font-size-footnote-scaled);
|
|
51
|
+
--button-line-height: var(--line-height-footnote-scaled);
|
|
52
|
+
--button-text-decoration: var(--text-decoration-caption, none);
|
|
53
|
+
--button-active-text-decoration: var(--text-decoration-caption, none);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&[data-size="large"] {
|
|
57
|
+
--button-padding-y: var(--space-3);
|
|
58
|
+
--button-padding-x: var(--space-4);
|
|
59
|
+
--button-font-weight: var(--font-weight-subhead, 500);
|
|
60
|
+
--button-font-size: var(--font-size-body-scaled);
|
|
61
|
+
--button-line-height: var(--line-height-body-scaled);
|
|
62
|
+
--button-text-decoration: var(--text-decoration-subhead, none);
|
|
63
|
+
--button-active-text-decoration: var(--text-decoration-subhead, none);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
&[data-icon-only="true"] {
|
|
67
|
+
--button-padding-x: var(--button-padding-y);
|
|
68
|
+
--button-icon-gap: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
&.primary {
|
|
72
|
+
background-color: var(--color-primary);
|
|
73
|
+
color: var(--color-on-primary);
|
|
74
|
+
|
|
75
|
+
&:not(:disabled):hover {
|
|
76
|
+
filter: brightness(0.9);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
&:not(:disabled):active {
|
|
80
|
+
filter: brightness(0.8);
|
|
81
|
+
text-decoration: var(--button-active-text-decoration);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&[data-destructive="true"] {
|
|
85
|
+
background-color: var(--color-destructive);
|
|
86
|
+
color: var(--color-on-destructive);
|
|
87
|
+
border-color: transparent;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
&.default {
|
|
92
|
+
background-color: var(--color-surface-300);
|
|
93
|
+
color: var(--color-on-surface);
|
|
94
|
+
border: 1px solid var(--color-surface-400);
|
|
95
|
+
|
|
96
|
+
&:not(:disabled):hover {
|
|
97
|
+
background-color: var(--color-surface-400);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
&:not(:disabled):active {
|
|
101
|
+
background-color: var(--color-surface-500);
|
|
102
|
+
text-decoration: var(--button-active-text-decoration);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&[data-destructive="true"] {
|
|
106
|
+
background-color: var(--color-destructive);
|
|
107
|
+
color: var(--color-on-destructive);
|
|
108
|
+
border-color: transparent;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
&.tertiary {
|
|
113
|
+
background-color: transparent;
|
|
114
|
+
color: var(--color-on-surface);
|
|
115
|
+
border: 1px solid var(--color-surface-400);
|
|
116
|
+
|
|
117
|
+
&:not(:disabled):hover {
|
|
118
|
+
background-color: var(--color-surface-200);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
&:not(:disabled):active {
|
|
122
|
+
background-color: var(--color-surface-300);
|
|
123
|
+
text-decoration: var(--button-active-text-decoration);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
&[data-destructive="true"] {
|
|
127
|
+
color: var(--color-destructive);
|
|
128
|
+
border-color: var(--color-destructive);
|
|
129
|
+
background-color: transparent;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
&.ghost {
|
|
134
|
+
background-color: transparent;
|
|
135
|
+
color: var(--color-on-surface);
|
|
136
|
+
|
|
137
|
+
&:not(:disabled):hover {
|
|
138
|
+
background-color: var(--color-surface-200);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
&:not(:disabled):active {
|
|
142
|
+
background-color: var(--color-surface-300);
|
|
143
|
+
text-decoration: var(--button-active-text-decoration);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
&[data-destructive="true"] {
|
|
147
|
+
color: var(--color-destructive);
|
|
148
|
+
background-color: transparent;
|
|
149
|
+
border-color: transparent;
|
|
150
|
+
|
|
151
|
+
&:not(:disabled):hover {
|
|
152
|
+
background-color: var(--color-surface-200);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
&.accent {
|
|
158
|
+
background-color: var(--color-accent-primary);
|
|
159
|
+
color: var(--color-text-inverse);
|
|
160
|
+
|
|
161
|
+
&:not(:disabled):hover {
|
|
162
|
+
filter: brightness(0.9);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
&:not(:disabled):active {
|
|
166
|
+
filter: brightness(0.8);
|
|
167
|
+
text-decoration: var(--button-active-text-decoration);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
&[data-destructive="true"] {
|
|
171
|
+
background-color: var(--color-destructive);
|
|
172
|
+
color: var(--color-on-destructive);
|
|
173
|
+
border-color: transparent;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.sol-button__icon {
|
|
179
|
+
display: inline-flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
justify-content: center;
|
|
182
|
+
line-height: 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.sol-button .solara-icon {
|
|
186
|
+
color: currentColor;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.sol-button .solara-icon [stroke]:not([stroke="none"]) {
|
|
190
|
+
stroke: currentColor;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.sol-button .solara-icon [fill]:not([fill="none"]) {
|
|
194
|
+
fill: currentColor;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.sol-button__label {
|
|
198
|
+
display: inline-flex;
|
|
199
|
+
align-items: center;
|
|
200
|
+
}
|
package/src/Button.tsx
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import "./Button.scss";
|
|
2
|
+
import { Icon } from "@solara/icons";
|
|
3
|
+
import type { IconProps } from "@solara/icons";
|
|
4
|
+
import type { ButtonIcon, ButtonProps } from "./Button.types";
|
|
5
|
+
|
|
6
|
+
const normalizeIconProps = (icon: ButtonIcon): IconProps =>
|
|
7
|
+
typeof icon === "string" ? { name: icon } : icon;
|
|
8
|
+
|
|
9
|
+
const resolveIconProps = (icon: ButtonIcon): IconProps => {
|
|
10
|
+
const iconProps = normalizeIconProps(icon);
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
...iconProps,
|
|
14
|
+
style: {
|
|
15
|
+
...iconProps.style,
|
|
16
|
+
// Force button icon color to follow the button's text color.
|
|
17
|
+
color: "currentColor"
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const renderIcon = (icon: ButtonIcon, position: "left" | "right" | "only") => (
|
|
23
|
+
<span className={`sol-button__icon sol-button__icon--${position}`}>
|
|
24
|
+
<Icon {...resolveIconProps(icon)} />
|
|
25
|
+
</span>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export function Button({
|
|
29
|
+
label,
|
|
30
|
+
variant = "default",
|
|
31
|
+
size = "medium",
|
|
32
|
+
radius = "default",
|
|
33
|
+
destructive = false,
|
|
34
|
+
iconLeft,
|
|
35
|
+
iconRight,
|
|
36
|
+
iconOnly,
|
|
37
|
+
className,
|
|
38
|
+
disabled,
|
|
39
|
+
type,
|
|
40
|
+
"aria-label": ariaLabel,
|
|
41
|
+
...props
|
|
42
|
+
}: ButtonProps) {
|
|
43
|
+
const isIconOnly = Boolean(iconOnly);
|
|
44
|
+
// Default aria-label to the label when an icon-only button is used.
|
|
45
|
+
const resolvedAriaLabel = ariaLabel ?? (isIconOnly ? label : undefined);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<button
|
|
49
|
+
type={type ?? "button"}
|
|
50
|
+
data-size={size}
|
|
51
|
+
data-radius={radius}
|
|
52
|
+
data-destructive={destructive}
|
|
53
|
+
data-icon-only={isIconOnly ? "true" : undefined}
|
|
54
|
+
disabled={disabled}
|
|
55
|
+
className={["sol-button", variant, className].filter(Boolean).join(" ")}
|
|
56
|
+
aria-label={resolvedAriaLabel}
|
|
57
|
+
{...props}>
|
|
58
|
+
{isIconOnly && iconOnly ? renderIcon(iconOnly, "only") : null}
|
|
59
|
+
{!isIconOnly && iconLeft ? renderIcon(iconLeft, "left") : null}
|
|
60
|
+
{!isIconOnly && label ? <span className="sol-button__label">{label}</span> : null}
|
|
61
|
+
{!isIconOnly && iconRight ? renderIcon(iconRight, "right") : null}
|
|
62
|
+
</button>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type React from "react";
|
|
2
|
+
import type { IconProps } from "@solara/icons";
|
|
3
|
+
|
|
4
|
+
export type ButtonVariant = "primary" | "default" | "tertiary" | "ghost" | "accent";
|
|
5
|
+
export type ButtonSize = "small" | "medium" | "large";
|
|
6
|
+
export type ButtonRadius = "default" | "none" | "full";
|
|
7
|
+
|
|
8
|
+
export type ButtonIcon = IconProps | IconProps["name"];
|
|
9
|
+
|
|
10
|
+
export type ButtonProps = Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> & {
|
|
11
|
+
label?: string;
|
|
12
|
+
variant?: ButtonVariant;
|
|
13
|
+
size?: ButtonSize;
|
|
14
|
+
radius?: ButtonRadius;
|
|
15
|
+
destructive?: boolean;
|
|
16
|
+
iconLeft?: ButtonIcon;
|
|
17
|
+
iconRight?: ButtonIcon;
|
|
18
|
+
iconOnly?: ButtonIcon;
|
|
19
|
+
};
|
package/src/index.ts
ADDED