@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 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
+ }
@@ -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
@@ -0,0 +1,2 @@
1
+ export { Button } from "./Button";
2
+ export type { ButtonIcon, ButtonProps, ButtonRadius, ButtonSize, ButtonVariant } from "./Button.types";