@varialkit/badge 0.1.0

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,42 @@
1
+ # Badge
2
+
3
+ The Badge component displays short status or categorization labels. It supports variants for different contexts, optional icons, and an optional dismiss action.
4
+
5
+ ## How to Use
6
+
7
+ ```tsx
8
+ import { Badge } from "@solara/badge";
9
+
10
+ export function MyComponent() {
11
+ return <Badge variant="blue">New</Badge>;
12
+ }
13
+ ```
14
+
15
+ ## Best Practices
16
+
17
+ - **Keep it short**: Badges are most effective with 1-2 words.
18
+ - **Use variants consistently**: Use colors semantically (e.g., `red` for destructive actions, `green` for success).
19
+ - **Avoid stacking actions**: Only make badges clickable when they truly trigger a meaningful action.
20
+
21
+ ## Props
22
+
23
+ | Prop | Type | Default | Description |
24
+ | --- | --- | --- | --- |
25
+ | `children` | `ReactNode` | _Required_ | The content of the badge. |
26
+ | `variant` | `"default" | "outline" | "cool-gray" | "red" | "yellow" | "green" | "teal" | "blue" | "purple" | "pink" | "orange" | "navy" | "sand" | "aqua" | "olive" | "cool-gray-med" | "cool-gray-dark" | "red-med" | "red-dark" | "yellow-med" | "yellow-dark" | "green-med" | "green-dark" | "teal-med" | "teal-dark" | "blue-med" | "blue-dark" | "purple-med" | "purple-dark" | "pink-med" | "pink-dark" | "orange-med" | "orange-dark" | "navy-med" | "navy-dark" | "sand-med" | "sand-dark" | "aqua-med" | "aqua-dark" | "olive-med" | "olive-dark"` | `"default"` | Visual style of the badge. |
27
+ | `size` | `"xs" | "small" | "medium" | "large"` | `"medium"` | Size of the badge. Legacy aliases `"sm" | "md" | "lg"` are also supported. |
28
+ | `clickable` | `boolean` | `false` | Adds interactive affordance and click handling. |
29
+ | `dismissible` | `boolean` | `false` | Shows a dismiss button inside the badge. |
30
+ | `onClick` | `() => void` | | Callback when the badge is clicked. |
31
+ | `onDismiss` | `() => void` | | Callback when the dismiss button is clicked. |
32
+ | `icon` | `SolaraIconName` | | Optional icon name from `@solara/icons`, rendered with the shared `Icon` component. |
33
+ | `iconSize` | `SolaraIconSize` | | Optional icon size override. By default, icon size follows the badge size token. |
34
+ | `fullWidth` | `boolean` | `false` | Stretches the badge to fill its container. |
35
+ | `disabled` | `boolean` | `false` | Disables interaction and dims the badge. |
36
+ | `className` | `string` | | Custom class name for the badge. |
37
+ | `dismissButtonClassName` | `string` | | Custom class name for the dismiss button. |
38
+
39
+ ## Accessibility
40
+
41
+ - The badge renders as a `button`, so it is keyboard focusable by default.
42
+ - When `dismissible`, the dismiss control is a keyboard-accessible inner control with `role="button"` and `aria-label="Dismiss"` to avoid nested button markup.
package/examples.tsx ADDED
@@ -0,0 +1,300 @@
1
+ import React from "react";
2
+ import type { SolaraIconName } from "@solara/icons";
3
+ import { Badge } from "./src/Badge";
4
+ import type { BadgeProps } from "./src/Badge.types";
5
+
6
+ const badgeIconOptions: SolaraIconName[] = [
7
+ "checkmark_circle_16",
8
+ "communication_bell_16",
9
+ "plus_16",
10
+ "star_16",
11
+ "dot_spoke_six_16",
12
+ ];
13
+
14
+ export const stories = {
15
+ playground: {
16
+ title: "Playground",
17
+ description: "Tweak the props to explore the Badge API.",
18
+ render: (props: BadgeProps) => (
19
+ <Badge
20
+ {...props}
21
+ onDismiss={props.dismissible ? () => undefined : undefined}
22
+ >
23
+ {props.children ?? "Badge"}
24
+ </Badge>
25
+ ),
26
+ controls: [
27
+ { name: "children", type: "text", label: "Content" },
28
+ {
29
+ name: "variant",
30
+ type: "select",
31
+ options: [
32
+ "default",
33
+ "outline",
34
+ "cool-gray",
35
+ "red",
36
+ "yellow",
37
+ "green",
38
+ "teal",
39
+ "blue",
40
+ "purple",
41
+ "pink",
42
+ "orange",
43
+ "navy",
44
+ "sand",
45
+ "aqua",
46
+ "olive",
47
+ "cool-gray-med",
48
+ "cool-gray-dark",
49
+ "red-med",
50
+ "red-dark",
51
+ "yellow-med",
52
+ "yellow-dark",
53
+ "green-med",
54
+ "green-dark",
55
+ "teal-med",
56
+ "teal-dark",
57
+ "blue-med",
58
+ "blue-dark",
59
+ "purple-med",
60
+ "purple-dark",
61
+ "pink-med",
62
+ "pink-dark",
63
+ "orange-med",
64
+ "orange-dark",
65
+ "navy-med",
66
+ "navy-dark",
67
+ "sand-med",
68
+ "sand-dark",
69
+ "aqua-med",
70
+ "aqua-dark",
71
+ "olive-med",
72
+ "olive-dark",
73
+ ],
74
+ },
75
+ {
76
+ name: "size",
77
+ type: "select",
78
+ options: ["xs", "small", "medium", "large"],
79
+ },
80
+ { name: "clickable", type: "boolean" },
81
+ { name: "dismissible", type: "boolean" },
82
+ {
83
+ name: "icon",
84
+ type: "select",
85
+ options: badgeIconOptions,
86
+ label: "Icon name",
87
+ },
88
+ { name: "fullWidth", type: "boolean" },
89
+ { name: "disabled", type: "boolean" },
90
+ ],
91
+ initialProps: {
92
+ children: "Badge",
93
+ variant: "default",
94
+ size: "medium",
95
+ clickable: false,
96
+ dismissible: false,
97
+ fullWidth: false,
98
+ disabled: false,
99
+ },
100
+ },
101
+ variants: {
102
+ title: "Variants",
103
+ showProps: false,
104
+ render: () => (
105
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
106
+ <Badge variant="default">Default</Badge>
107
+ <Badge variant="cool-gray">Cool Gray</Badge>
108
+ <Badge variant="red">Red</Badge>
109
+ <Badge variant="yellow">Yellow</Badge>
110
+ <Badge variant="green">Green</Badge>
111
+ <Badge variant="teal">Teal</Badge>
112
+ <Badge variant="blue">Blue</Badge>
113
+ <Badge variant="purple">Purple</Badge>
114
+ <Badge variant="pink">Pink</Badge>
115
+ <Badge variant="orange">Orange</Badge>
116
+ <Badge variant="navy">Navy</Badge>
117
+ <Badge variant="sand">Sand</Badge>
118
+ <Badge variant="aqua">Aqua</Badge>
119
+ <Badge variant="olive">Olive</Badge>
120
+ <Badge variant="outline">Outline</Badge>
121
+ </div>
122
+ ),
123
+ code: `import { Badge } from "@solara/badge";
124
+
125
+ export function Example() {
126
+ return (
127
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
128
+ <Badge variant="default">Default</Badge>
129
+ <Badge variant="cool-gray">Cool Gray</Badge>
130
+ <Badge variant="red">Red</Badge>
131
+ <Badge variant="yellow">Yellow</Badge>
132
+ <Badge variant="green">Green</Badge>
133
+ <Badge variant="teal">Teal</Badge>
134
+ <Badge variant="blue">Blue</Badge>
135
+ <Badge variant="purple">Purple</Badge>
136
+ <Badge variant="pink">Pink</Badge>
137
+ <Badge variant="orange">Orange</Badge>
138
+ <Badge variant="navy">Navy</Badge>
139
+ <Badge variant="sand">Sand</Badge>
140
+ <Badge variant="aqua">Aqua</Badge>
141
+ <Badge variant="olive">Olive</Badge>
142
+ <Badge variant="outline">Outline</Badge>
143
+ </div>
144
+ );
145
+ }
146
+ `,
147
+ },
148
+ colorVariants: {
149
+ title: "Color Variants",
150
+ showProps: false,
151
+ render: () => (
152
+ <div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
153
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
154
+ <Badge variant="cool-gray">Cool Gray</Badge>
155
+ <Badge variant="cool-gray-med">Cool Gray Med</Badge>
156
+ <Badge variant="cool-gray-dark">Cool Gray Dark</Badge>
157
+ </div>
158
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
159
+ <Badge variant="red">Red</Badge>
160
+ <Badge variant="red-med">Red Med</Badge>
161
+ <Badge variant="red-dark">Red Dark</Badge>
162
+ </div>
163
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
164
+ <Badge variant="yellow">Yellow</Badge>
165
+ <Badge variant="yellow-med">Yellow Med</Badge>
166
+ <Badge variant="yellow-dark">Yellow Dark</Badge>
167
+ </div>
168
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
169
+ <Badge variant="green">Green</Badge>
170
+ <Badge variant="green-med">Green Med</Badge>
171
+ <Badge variant="green-dark">Green Dark</Badge>
172
+ </div>
173
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
174
+ <Badge variant="teal">Teal</Badge>
175
+ <Badge variant="teal-med">Teal Med</Badge>
176
+ <Badge variant="teal-dark">Teal Dark</Badge>
177
+ </div>
178
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
179
+ <Badge variant="blue">Blue</Badge>
180
+ <Badge variant="blue-med">Blue Med</Badge>
181
+ <Badge variant="blue-dark">Blue Dark</Badge>
182
+ </div>
183
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
184
+ <Badge variant="purple">Purple</Badge>
185
+ <Badge variant="purple-med">Purple Med</Badge>
186
+ <Badge variant="purple-dark">Purple Dark</Badge>
187
+ </div>
188
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
189
+ <Badge variant="pink">Pink</Badge>
190
+ <Badge variant="pink-med">Pink Med</Badge>
191
+ <Badge variant="pink-dark">Pink Dark</Badge>
192
+ </div>
193
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
194
+ <Badge variant="orange">Orange</Badge>
195
+ <Badge variant="orange-med">Orange Med</Badge>
196
+ <Badge variant="orange-dark">Orange Dark</Badge>
197
+ </div>
198
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
199
+ <Badge variant="navy">Navy</Badge>
200
+ <Badge variant="navy-med">Navy Med</Badge>
201
+ <Badge variant="navy-dark">Navy Dark</Badge>
202
+ </div>
203
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
204
+ <Badge variant="sand">Sand</Badge>
205
+ <Badge variant="sand-med">Sand Med</Badge>
206
+ <Badge variant="sand-dark">Sand Dark</Badge>
207
+ </div>
208
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
209
+ <Badge variant="aqua">Aqua</Badge>
210
+ <Badge variant="aqua-med">Aqua Med</Badge>
211
+ <Badge variant="aqua-dark">Aqua Dark</Badge>
212
+ </div>
213
+ <div style={{ display: "flex", gap: "1rem", flexWrap: "wrap" }}>
214
+ <Badge variant="olive">Olive</Badge>
215
+ <Badge variant="olive-med">Olive Med</Badge>
216
+ <Badge variant="olive-dark">Olive Dark</Badge>
217
+ </div>
218
+ </div>
219
+ ),
220
+ },
221
+ sizes: {
222
+ title: "Sizes",
223
+ showProps: false,
224
+ render: () => (
225
+ <div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
226
+ <Badge size="xs">Extra Small</Badge>
227
+ <Badge size="small">Small</Badge>
228
+ <Badge size="medium">Medium</Badge>
229
+ <Badge size="large">Large</Badge>
230
+ </div>
231
+ ),
232
+ code: `import { Badge } from "@solara/badge";
233
+
234
+ export function Example() {
235
+ return (
236
+ <div style={{ display: "flex", gap: "1rem", alignItems: "center" }}>
237
+ <Badge size="xs">Extra Small</Badge>
238
+ <Badge size="small">Small</Badge>
239
+ <Badge size="medium">Medium</Badge>
240
+ <Badge size="large">Large</Badge>
241
+ </div>
242
+ );
243
+ }
244
+ `,
245
+ },
246
+ dismissible: {
247
+ title: "Dismissible",
248
+ showProps: false,
249
+ render: () => (
250
+ <div style={{ display: "flex", gap: "1rem" }}>
251
+ <Badge dismissible onDismiss={() => undefined}>
252
+ Dismiss me
253
+ </Badge>
254
+ <Badge dismissible variant="blue" onDismiss={() => undefined}>
255
+ Blue
256
+ </Badge>
257
+ </div>
258
+ ),
259
+ code: `import { Badge } from "@solara/badge";
260
+
261
+ export function Example() {
262
+ return (
263
+ <div style={{ display: "flex", gap: "1rem" }}>
264
+ <Badge dismissible onDismiss={() => undefined}>
265
+ Dismiss me
266
+ </Badge>
267
+ <Badge dismissible variant="blue" onDismiss={() => undefined}>
268
+ Blue
269
+ </Badge>
270
+ </div>
271
+ );
272
+ }
273
+ `,
274
+ },
275
+ icon: {
276
+ title: "With Icon",
277
+ showProps: false,
278
+ render: () => (
279
+ <div style={{ display: "flex", gap: "1rem" }}>
280
+ <Badge icon="dot_spoke_six_16">Status</Badge>
281
+ <Badge icon="checkmark_circle_16" variant="blue">
282
+ Online
283
+ </Badge>
284
+ </div>
285
+ ),
286
+ code: `import { Badge } from "@solara/badge";
287
+
288
+ export function Example() {
289
+ return (
290
+ <div style={{ display: "flex", gap: "1rem" }}>
291
+ <Badge icon="dot_spoke_six_16">Status</Badge>
292
+ <Badge icon="checkmark_circle_16" variant="blue">
293
+ Online
294
+ </Badge>
295
+ </div>
296
+ );
297
+ }
298
+ `,
299
+ },
300
+ };
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@varialkit/badge",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./examples": "./examples.tsx"
10
+ },
11
+ "files": [
12
+ "src",
13
+ "docs.md",
14
+ "examples.tsx"
15
+ ],
16
+ "peerDependencies": {
17
+ "react": "^19.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/react": "19.0.10",
21
+ "react": "19.0.0"
22
+ }
23
+ }
package/src/Badge.scss ADDED
@@ -0,0 +1,435 @@
1
+ .solara-badge {
2
+ --badge-padding-y: var(--space-1);
3
+ --badge-padding-x: var(--space-3);
4
+ --badge-font-size: var(--font-size-caption-scaled);
5
+ --badge-line-height: var(--line-height-caption-scaled);
6
+ --badge-height: var(--space-7);
7
+ --badge-icon-size: 14px;
8
+ --badge-gap: var(--space-1);
9
+
10
+ // Base layout aligns with shared spacing + density tokens.
11
+ display: inline-flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ gap: calc(var(--badge-gap) * var(--spacing-multiplier));
15
+ padding: calc(var(--badge-padding-y) * var(--spacing-multiplier))
16
+ calc(var(--badge-padding-x) * var(--spacing-multiplier));
17
+ min-height: calc(var(--badge-height) * var(--spacing-multiplier));
18
+ border-radius: var(--radius-pill);
19
+ font-weight: 500;
20
+ font-size: var(--badge-font-size);
21
+ line-height: var(--badge-line-height);
22
+ font-family: var(--font-body);
23
+ border: 1px solid transparent;
24
+ cursor: default;
25
+ transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease,
26
+ transform 0.2s ease, box-shadow 0.2s ease;
27
+ white-space: nowrap;
28
+ user-select: none;
29
+ -webkit-font-smoothing: antialiased;
30
+ -moz-osx-font-smoothing: grayscale;
31
+
32
+ &:focus-visible {
33
+ outline: none;
34
+ box-shadow: 0 0 0 3px var(--color-focus-halo);
35
+ }
36
+
37
+ &[data-clickable="true"] {
38
+ cursor: pointer;
39
+ }
40
+
41
+ &[data-clickable="true"]:not([data-disabled="true"]):hover {
42
+ transform: translateY(-1px);
43
+ }
44
+
45
+ &[data-disabled="true"] {
46
+ opacity: 0.6;
47
+ cursor: not-allowed;
48
+ }
49
+
50
+ // Size tokens map to the legacy sm/md/lg scale.
51
+ &[data-size="small"] {
52
+ --badge-padding-y: var(--space-1);
53
+ --badge-padding-x: var(--space-2);
54
+ --badge-font-size: var(--font-size-footnote-scaled);
55
+ --badge-line-height: var(--line-height-footnote-scaled);
56
+ --badge-height: var(--space-6);
57
+ --badge-icon-size: 12px;
58
+ }
59
+
60
+ &[data-size="xs"] {
61
+ --badge-padding-y: var(--space-0);
62
+ --badge-padding-x: var(--space-3);
63
+ --badge-font-size: var(--font-size-footnote-scaled);
64
+ --badge-line-height: var(--line-height-footnote-scaled);
65
+ --badge-height: var(--space-5);
66
+ --badge-icon-size: 10px;
67
+ }
68
+
69
+ &[data-size="large"] {
70
+ --badge-padding-y: var(--space-2);
71
+ --badge-padding-x: var(--space-4);
72
+ --badge-font-size: var(--font-size-body-scaled);
73
+ --badge-line-height: var(--line-height-body-scaled);
74
+ --badge-height: var(--space-8);
75
+ --badge-icon-size: 16px;
76
+ }
77
+
78
+ &.solara-badge--full-width {
79
+ width: 100%;
80
+ }
81
+
82
+ &.solara-badge--default {
83
+ background-color: var(--color-content-cool-gray-light);
84
+ color: var(--color-content-cool-gray-dark);
85
+ border-color: var(--color-content-cool-gray-light);
86
+ }
87
+
88
+ &.solara-badge--cool-gray {
89
+ background-color: var(--color-content-cool-gray-light);
90
+ color: var(--color-content-cool-gray-dark);
91
+ border-color: var(--color-content-cool-gray-light);
92
+ }
93
+
94
+ &.solara-badge--red {
95
+ background-color: var(--color-content-red-light);
96
+ color: var(--color-content-red-dark);
97
+ border-color: var(--color-content-red-light);
98
+ }
99
+
100
+ &.solara-badge--yellow {
101
+ background-color: var(--color-content-yellow-light);
102
+ color: var(--color-content-yellow-dark);
103
+ border-color: var(--color-content-yellow-light);
104
+ }
105
+
106
+ &.solara-badge--green {
107
+ background-color: var(--color-content-green-light);
108
+ color: var(--color-content-green-dark);
109
+ border-color: var(--color-content-green-light);
110
+ }
111
+
112
+ &.solara-badge--teal {
113
+ background-color: var(--color-content-teal-light);
114
+ color: var(--color-content-teal-dark);
115
+ border-color: var(--color-content-teal-light);
116
+ }
117
+
118
+ &.solara-badge--blue {
119
+ background-color: var(--color-content-blue-light);
120
+ color: var(--color-content-blue-dark);
121
+ border-color: var(--color-content-blue-light);
122
+ }
123
+
124
+ &.solara-badge--purple {
125
+ background-color: var(--color-content-purple-light);
126
+ color: var(--color-content-purple-dark);
127
+ border-color: var(--color-content-purple-light);
128
+ }
129
+
130
+ &.solara-badge--pink {
131
+ background-color: var(--color-content-pink-light);
132
+ color: var(--color-content-pink-dark);
133
+ border-color: var(--color-content-pink-light);
134
+ }
135
+
136
+ &.solara-badge--orange {
137
+ background-color: var(--color-content-orange-light);
138
+ color: var(--color-content-orange-dark);
139
+ border-color: var(--color-content-orange-light);
140
+ }
141
+
142
+ &.solara-badge--navy {
143
+ background-color: var(--color-content-navy-light);
144
+ color: var(--color-content-navy-dark);
145
+ border-color: var(--color-content-navy-light);
146
+ }
147
+
148
+ &.solara-badge--sand {
149
+ background-color: var(--color-content-sand-light);
150
+ color: var(--color-content-sand-dark);
151
+ border-color: var(--color-content-sand-light);
152
+ }
153
+
154
+ &.solara-badge--aqua {
155
+ background-color: var(--color-content-aqua-light);
156
+ color: var(--color-content-aqua-dark);
157
+ border-color: var(--color-content-aqua-light);
158
+ }
159
+
160
+ &.solara-badge--olive {
161
+ background-color: var(--color-content-olive-light);
162
+ color: var(--color-content-olive-dark);
163
+ border-color: var(--color-content-olive-light);
164
+ }
165
+
166
+ &.solara-badge--cool-gray-med {
167
+ background-color: var(--color-content-cool-gray-med);
168
+ color: var(--color-text-inverse);
169
+ border-color: var(--color-content-cool-gray-med);
170
+ }
171
+
172
+ &.solara-badge--cool-gray-dark {
173
+ background-color: var(--color-content-cool-gray-dark);
174
+ color: var(--color-text-inverse);
175
+ border-color: var(--color-content-cool-gray-dark);
176
+ }
177
+
178
+ &.solara-badge--red-med {
179
+ background-color: var(--color-content-red-med);
180
+ color: var(--color-text-inverse);
181
+ border-color: var(--color-content-red-med);
182
+ }
183
+
184
+ &.solara-badge--red-dark {
185
+ background-color: var(--color-content-red-dark);
186
+ color: var(--color-text-inverse);
187
+ border-color: var(--color-content-red-dark);
188
+ }
189
+
190
+ &.solara-badge--yellow-med {
191
+ background-color: var(--color-content-yellow-med);
192
+ color: var(--color-text-inverse);
193
+ border-color: var(--color-content-yellow-med);
194
+ }
195
+
196
+ &.solara-badge--yellow-dark {
197
+ background-color: var(--color-content-yellow-dark);
198
+ color: var(--color-text-inverse);
199
+ border-color: var(--color-content-yellow-dark);
200
+ }
201
+
202
+ &.solara-badge--green-med {
203
+ background-color: var(--color-content-green-med);
204
+ color: var(--color-text-inverse);
205
+ border-color: var(--color-content-green-med);
206
+ }
207
+
208
+ &.solara-badge--green-dark {
209
+ background-color: var(--color-content-green-dark);
210
+ color: var(--color-text-inverse);
211
+ border-color: var(--color-content-green-dark);
212
+ }
213
+
214
+ &.solara-badge--teal-med {
215
+ background-color: var(--color-content-teal-med);
216
+ color: var(--color-text-inverse);
217
+ border-color: var(--color-content-teal-med);
218
+ }
219
+
220
+ &.solara-badge--teal-dark {
221
+ background-color: var(--color-content-teal-dark);
222
+ color: var(--color-text-inverse);
223
+ border-color: var(--color-content-teal-dark);
224
+ }
225
+
226
+ &.solara-badge--blue-med {
227
+ background-color: var(--color-content-blue-med);
228
+ color: var(--color-text-inverse);
229
+ border-color: var(--color-content-blue-med);
230
+ }
231
+
232
+ &.solara-badge--blue-dark {
233
+ background-color: var(--color-content-blue-dark);
234
+ color: var(--color-text-inverse);
235
+ border-color: var(--color-content-blue-dark);
236
+ }
237
+
238
+ &.solara-badge--purple-med {
239
+ background-color: var(--color-content-purple-med);
240
+ color: var(--color-text-inverse);
241
+ border-color: var(--color-content-purple-med);
242
+ }
243
+
244
+ &.solara-badge--purple-dark {
245
+ background-color: var(--color-content-purple-dark);
246
+ color: var(--color-text-inverse);
247
+ border-color: var(--color-content-purple-dark);
248
+ }
249
+
250
+ &.solara-badge--pink-med {
251
+ background-color: var(--color-content-pink-med);
252
+ color: var(--color-text-inverse);
253
+ border-color: var(--color-content-pink-med);
254
+ }
255
+
256
+ &.solara-badge--pink-dark {
257
+ background-color: var(--color-content-pink-dark);
258
+ color: var(--color-text-inverse);
259
+ border-color: var(--color-content-pink-dark);
260
+ }
261
+
262
+ &.solara-badge--orange-med {
263
+ background-color: var(--color-content-orange-med);
264
+ color: var(--color-text-inverse);
265
+ border-color: var(--color-content-orange-med);
266
+ }
267
+
268
+ &.solara-badge--orange-dark {
269
+ background-color: var(--color-content-orange-dark);
270
+ color: var(--color-text-inverse);
271
+ border-color: var(--color-content-orange-dark);
272
+ }
273
+
274
+ &.solara-badge--navy-med {
275
+ background-color: var(--color-content-navy-med);
276
+ color: var(--color-text-inverse);
277
+ border-color: var(--color-content-navy-med);
278
+ }
279
+
280
+ &.solara-badge--navy-dark {
281
+ background-color: var(--color-content-navy-dark);
282
+ color: var(--color-text-inverse);
283
+ border-color: var(--color-content-navy-dark);
284
+ }
285
+
286
+ &.solara-badge--sand-med {
287
+ background-color: var(--color-content-sand-med);
288
+ color: var(--color-text-inverse);
289
+ border-color: var(--color-content-sand-med);
290
+ }
291
+
292
+ &.solara-badge--sand-dark {
293
+ background-color: var(--color-content-sand-dark);
294
+ color: var(--color-text-inverse);
295
+ border-color: var(--color-content-sand-dark);
296
+ }
297
+
298
+ &.solara-badge--aqua-med {
299
+ background-color: var(--color-content-aqua-med);
300
+ color: var(--color-text-inverse);
301
+ border-color: var(--color-content-aqua-med);
302
+ }
303
+
304
+ &.solara-badge--aqua-dark {
305
+ background-color: var(--color-content-aqua-dark);
306
+ color: var(--color-text-inverse);
307
+ border-color: var(--color-content-aqua-dark);
308
+ }
309
+
310
+ &.solara-badge--olive-med {
311
+ background-color: var(--color-content-olive-med);
312
+ color: var(--color-text-inverse);
313
+ border-color: var(--color-content-olive-med);
314
+ }
315
+
316
+ &.solara-badge--olive-dark {
317
+ background-color: var(--color-content-olive-dark);
318
+ color: var(--color-text-inverse);
319
+ border-color: var(--color-content-olive-dark);
320
+ }
321
+
322
+ &.solara-badge--outline {
323
+ background-color: transparent;
324
+ color: var(--color-content-cool-gray-dark);
325
+ border-color: var(--color-content-cool-gray-med);
326
+ }
327
+
328
+ &.solara-badge--clickable:not([data-disabled="true"]):hover {
329
+ &.solara-badge--default,
330
+ &.solara-badge--cool-gray {
331
+ background-color: var(--color-content-cool-gray-med);
332
+ }
333
+
334
+ &.solara-badge--red {
335
+ background-color: var(--color-content-red-med);
336
+ }
337
+
338
+ &.solara-badge--yellow {
339
+ background-color: var(--color-content-yellow-med);
340
+ }
341
+
342
+ &.solara-badge--green {
343
+ background-color: var(--color-content-green-med);
344
+ }
345
+
346
+ &.solara-badge--teal {
347
+ background-color: var(--color-content-teal-med);
348
+ }
349
+
350
+ &.solara-badge--blue {
351
+ background-color: var(--color-content-blue-med);
352
+ }
353
+
354
+ &.solara-badge--purple {
355
+ background-color: var(--color-content-purple-med);
356
+ }
357
+
358
+ &.solara-badge--pink {
359
+ background-color: var(--color-content-pink-med);
360
+ }
361
+
362
+ &.solara-badge--orange {
363
+ background-color: var(--color-content-orange-med);
364
+ }
365
+
366
+ &.solara-badge--navy {
367
+ background-color: var(--color-content-navy-med);
368
+ }
369
+
370
+ &.solara-badge--sand {
371
+ background-color: var(--color-content-sand-med);
372
+ }
373
+
374
+ &.solara-badge--aqua {
375
+ background-color: var(--color-content-aqua-med);
376
+ }
377
+
378
+ &.solara-badge--olive {
379
+ background-color: var(--color-content-olive-med);
380
+ }
381
+
382
+ &.solara-badge--outline {
383
+ background-color: var(--color-content-cool-gray-light);
384
+ }
385
+ }
386
+ }
387
+
388
+ .solara-badge__icon {
389
+ display: inline-flex;
390
+ align-items: center;
391
+ justify-content: center;
392
+ width: var(--badge-icon-size);
393
+ height: var(--badge-icon-size);
394
+ flex-shrink: 0;
395
+ }
396
+
397
+ .solara-badge__content {
398
+ display: inline-flex;
399
+ align-items: center;
400
+ }
401
+
402
+ // Nested dismiss control mirrors legacy behavior for badges with actions.
403
+ .solara-badge__dismiss {
404
+ display: inline-flex;
405
+ align-items: center;
406
+ justify-content: center;
407
+ margin-left: calc(var(--badge-gap) * var(--spacing-multiplier));
408
+ margin-right: calc(-1 * var(--badge-gap) * var(--spacing-multiplier));
409
+ border-radius: var(--radius-pill);
410
+ padding: calc(var(--space-1) * var(--spacing-multiplier));
411
+ border: none;
412
+ background: transparent;
413
+ cursor: pointer;
414
+ color: currentColor;
415
+ opacity: 0.7;
416
+ transition: background-color 0.2s ease, opacity 0.2s ease, box-shadow 0.2s ease;
417
+
418
+ &:hover {
419
+ background-color: var(--color-surface-200);
420
+ opacity: 1;
421
+ }
422
+
423
+ &:focus-visible {
424
+ outline: none;
425
+ box-shadow: 0 0 0 2px currentColor;
426
+ opacity: 1;
427
+ }
428
+ }
429
+
430
+ .solara-badge__dismiss-icon {
431
+ display: block;
432
+ width: 0.75em;
433
+ height: 0.75em;
434
+ line-height: 1;
435
+ }
package/src/Badge.tsx ADDED
@@ -0,0 +1,130 @@
1
+ import { Icon } from "@solara/icons";
2
+ import React, { forwardRef } from "react";
3
+ import type { BadgeProps, BadgeSize } from "./Badge.types";
4
+ import "./Badge.scss";
5
+
6
+ const sizeAliasMap: Record<BadgeSize, "xs" | "small" | "medium" | "large"> = {
7
+ xs: "xs",
8
+ sm: "small",
9
+ md: "medium",
10
+ lg: "large",
11
+ small: "small",
12
+ medium: "medium",
13
+ large: "large",
14
+ };
15
+
16
+ const badgeIconSizeMap: Record<"xs" | "small" | "medium" | "large", 10 | 12 | 14 | 16> = {
17
+ xs: 10,
18
+ small: 12,
19
+ medium: 14,
20
+ large: 16,
21
+ };
22
+
23
+ /**
24
+ * A badge component that can display text, icons, and optional dismiss actions.
25
+ */
26
+ export const Badge = forwardRef<HTMLButtonElement, BadgeProps>(
27
+ (
28
+ {
29
+ children,
30
+ variant = "default",
31
+ size = "medium",
32
+ clickable = false,
33
+ dismissible = false,
34
+ onClick,
35
+ onDismiss,
36
+ icon,
37
+ iconSize,
38
+ fullWidth = false,
39
+ className,
40
+ dismissButtonClassName,
41
+ disabled,
42
+ type,
43
+ ...props
44
+ },
45
+ ref
46
+ ) => {
47
+ // Normalize legacy sizes to the current small/medium/large scale.
48
+ const resolvedSize = sizeAliasMap[size];
49
+
50
+ // Treat the badge as interactive when clickable or a click handler is provided.
51
+ const isInteractive = clickable || Boolean(onClick);
52
+ const resolvedIconSize = iconSize ?? badgeIconSizeMap[resolvedSize];
53
+
54
+ const classes = [
55
+ "solara-badge",
56
+ `solara-badge--${variant}`,
57
+ `solara-badge--${resolvedSize}`,
58
+ isInteractive ? "solara-badge--clickable" : null,
59
+ dismissible ? "solara-badge--dismissible" : null,
60
+ fullWidth ? "solara-badge--full-width" : null,
61
+ disabled ? "solara-badge--disabled" : null,
62
+ className,
63
+ ]
64
+ .filter(Boolean)
65
+ .join(" ");
66
+
67
+ const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
68
+ if (disabled) return;
69
+ if (onClick) {
70
+ event.stopPropagation();
71
+ onClick();
72
+ }
73
+ };
74
+
75
+ const handleDismiss = (event: React.MouseEvent<HTMLElement>) => {
76
+ event.stopPropagation();
77
+ if (onDismiss) onDismiss();
78
+ };
79
+
80
+ const handleDismissKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
81
+ if (event.key !== "Enter" && event.key !== " ") return;
82
+ event.preventDefault();
83
+ event.stopPropagation();
84
+ if (onDismiss) onDismiss();
85
+ };
86
+
87
+ return (
88
+ <button
89
+ ref={ref}
90
+ type={type ?? "button"}
91
+ className={classes}
92
+ data-size={resolvedSize}
93
+ data-variant={variant}
94
+ data-disabled={disabled ? "true" : undefined}
95
+ data-clickable={isInteractive ? "true" : undefined}
96
+ data-full-width={fullWidth ? "true" : undefined}
97
+ onClick={isInteractive ? handleClick : undefined}
98
+ aria-pressed={isInteractive ? "false" : undefined}
99
+ disabled={disabled}
100
+ {...props}>
101
+ {icon ? (
102
+ <span className="solara-badge__icon" aria-hidden="true">
103
+ <Icon name={icon} size={resolvedIconSize} />
104
+ </span>
105
+ ) : null}
106
+ <span className="solara-badge__content">{children}</span>
107
+ {dismissible ? (
108
+ // Use a non-button control to avoid nested <button> hydration errors.
109
+ <span
110
+ role="button"
111
+ tabIndex={0}
112
+ onClick={handleDismiss}
113
+ onKeyDown={handleDismissKeyDown}
114
+ className={[
115
+ "solara-badge__dismiss",
116
+ dismissButtonClassName,
117
+ ]
118
+ .filter(Boolean)
119
+ .join(" ")}
120
+ aria-label="Dismiss"
121
+ data-dismiss>
122
+ <Icon name="x_16" size={16} className="solara-badge__dismiss-icon" aria-hidden="true" />
123
+ </span>
124
+ ) : null}
125
+ </button>
126
+ );
127
+ }
128
+ );
129
+
130
+ Badge.displayName = "Badge";
@@ -0,0 +1,78 @@
1
+ import type React from "react";
2
+ import type { SolaraIconName, SolaraIconSize } from "@solara/icons";
3
+
4
+ export type BadgeVariant =
5
+ | "default"
6
+ | "outline"
7
+ | "cool-gray"
8
+ | "red"
9
+ | "yellow"
10
+ | "green"
11
+ | "teal"
12
+ | "blue"
13
+ | "purple"
14
+ | "pink"
15
+ | "orange"
16
+ | "navy"
17
+ | "sand"
18
+ | "aqua"
19
+ | "olive"
20
+ | "cool-gray-med"
21
+ | "cool-gray-dark"
22
+ | "red-med"
23
+ | "red-dark"
24
+ | "yellow-med"
25
+ | "yellow-dark"
26
+ | "green-med"
27
+ | "green-dark"
28
+ | "teal-med"
29
+ | "teal-dark"
30
+ | "blue-med"
31
+ | "blue-dark"
32
+ | "purple-med"
33
+ | "purple-dark"
34
+ | "pink-med"
35
+ | "pink-dark"
36
+ | "orange-med"
37
+ | "orange-dark"
38
+ | "navy-med"
39
+ | "navy-dark"
40
+ | "sand-med"
41
+ | "sand-dark"
42
+ | "aqua-med"
43
+ | "aqua-dark"
44
+ | "olive-med"
45
+ | "olive-dark";
46
+
47
+ // Support legacy size aliases (sm/md/lg) while standardizing on small/medium/large.
48
+ export type BadgeSize = "xs" | "sm" | "md" | "lg" | "small" | "medium" | "large";
49
+
50
+ export type BadgeProps = Omit<
51
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
52
+ "children" | "onClick"
53
+ > & {
54
+ /** The content of the badge. */
55
+ children: React.ReactNode;
56
+ /** Visual style of the badge. */
57
+ variant?: BadgeVariant;
58
+ /** Size of the badge (supports legacy aliases). */
59
+ size?: BadgeSize;
60
+ /** Whether the badge should show interactive affordance. */
61
+ clickable?: boolean;
62
+ /** Whether the badge should show a dismiss button. */
63
+ dismissible?: boolean;
64
+ /** Callback when the badge is clicked. */
65
+ onClick?: () => void;
66
+ /** Callback when the dismiss button is clicked. */
67
+ onDismiss?: () => void;
68
+ /** Optional Solara icon name rendered before the content. */
69
+ icon?: SolaraIconName;
70
+ /** Explicit icon size override. Defaults to the badge size token. */
71
+ iconSize?: SolaraIconSize;
72
+ /** Whether the badge stretches to full width. */
73
+ fullWidth?: boolean;
74
+ /** Custom class name for the badge. */
75
+ className?: string;
76
+ /** Custom class name for the dismiss button. */
77
+ dismissButtonClassName?: string;
78
+ };
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { Badge } from "./Badge";
2
+ export type { BadgeProps, BadgeSize, BadgeVariant } from "./Badge.types";