torch-glare 2.1.0 → 2.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/apps/lib/components/Badge.tsx +34 -137
- package/apps/lib/components/BadgeField.tsx +2 -2
- package/apps/lib/components/Charts-dev.tsx +365 -0
- package/apps/lib/components/Command-dev.tsx +151 -0
- package/apps/lib/components/IosDatePicker-dev.tsx +341 -0
- package/dist/bin/index.js +0 -0
- package/docs/components/badge-field.md +21 -21
- package/docs/components/badge.md +156 -483
- package/docs/reference/components.md +8 -7
- package/docs/reference/types.md +34 -26
- package/docs/tutorials/theming-basics.md +30 -27
- package/package.json +6 -6
package/docs/components/badge.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Badge
|
|
3
|
-
description:
|
|
3
|
+
description: Status indicator and label component with solid/subtle styles, ten color options, and an optional close button.
|
|
4
4
|
component: true
|
|
5
5
|
group: Data Display
|
|
6
6
|
keywords: [badge, tag, label, status, chip, pill, indicator]
|
|
@@ -8,7 +8,7 @@ keywords: [badge, tag, label, status, chip, pill, indicator]
|
|
|
8
8
|
|
|
9
9
|
# Badge
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Compact label for status, categories, and tags. Two visual styles (`solid`, `subtle`), ten colors, three sizes, and an optional close button for chip-style use.
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
@@ -19,8 +19,7 @@ npx torch-cli add badge
|
|
|
19
19
|
## Imports
|
|
20
20
|
|
|
21
21
|
```typescript
|
|
22
|
-
import { Badge } from '@/components/Badge'
|
|
23
|
-
import { badgeBase } from '@/components/Badge'
|
|
22
|
+
import { Badge, badgeStyles } from '@/components/Badge'
|
|
24
23
|
```
|
|
25
24
|
|
|
26
25
|
## Basic Usage
|
|
@@ -29,594 +28,268 @@ import { badgeBase } from '@/components/Badge'
|
|
|
29
28
|
import { Badge } from '@/components/Badge'
|
|
30
29
|
|
|
31
30
|
export function BasicBadge() {
|
|
32
|
-
return <Badge label="Active"
|
|
31
|
+
return <Badge label="Active" color="green" />
|
|
33
32
|
}
|
|
34
33
|
```
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
### All Variants
|
|
35
|
+
Defaults: `badgeStyle="subtle"`, `color="gray"`, `size="S"`, `showIcon=true`.
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
```tsx
|
|
43
|
-
export function BadgeVariants() {
|
|
44
|
-
const variants = [
|
|
45
|
-
{ variant: 'highlight', label: 'Highlight', description: 'Neutral gray highlight' },
|
|
46
|
-
{ variant: 'green', label: 'Success', description: 'Positive states' },
|
|
47
|
-
{ variant: 'greenLight', label: 'Active', description: 'Light green' },
|
|
48
|
-
{ variant: 'cocktailGreen', label: 'New', description: 'Bright green' },
|
|
49
|
-
{ variant: 'yellow', label: 'Warning', description: 'Caution states' },
|
|
50
|
-
{ variant: 'redOrange', label: 'Alert', description: 'Attention needed' },
|
|
51
|
-
{ variant: 'redLight', label: 'Error', description: 'Error states' },
|
|
52
|
-
{ variant: 'rose', label: 'Critical', description: 'Critical issues' },
|
|
53
|
-
{ variant: 'purple', label: 'Feature', description: 'Special features' },
|
|
54
|
-
{ variant: 'bluePurple', label: 'Beta', description: 'Beta features' },
|
|
55
|
-
{ variant: 'blue', label: 'Info', description: 'Informational' },
|
|
56
|
-
{ variant: 'navy', label: 'Default', description: 'Standard' },
|
|
57
|
-
{ variant: 'gray', label: 'Inactive', description: 'Disabled/inactive' },
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
|
62
|
-
{variants.map(({ variant, label, description }) => (
|
|
63
|
-
<div key={variant} className="flex flex-col gap-2">
|
|
64
|
-
<Badge label={label} variant={variant as any} />
|
|
65
|
-
<span className="text-xs text-content-presentation-global-secondary">
|
|
66
|
-
{description}
|
|
67
|
-
</span>
|
|
68
|
-
</div>
|
|
69
|
-
))}
|
|
70
|
-
</div>
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
```
|
|
37
|
+
## Examples
|
|
74
38
|
|
|
75
|
-
###
|
|
39
|
+
### Styles
|
|
76
40
|
|
|
77
|
-
|
|
41
|
+
`subtle` (default) tints the background and renders text/icons in a single neutral foreground color (`--Content-Presentation-Global-subtle`) blended with `mix-blend-mode: luminosity`. `solid` fills the badge and uses a light primary foreground.
|
|
78
42
|
|
|
79
43
|
```tsx
|
|
80
|
-
export function
|
|
44
|
+
export function BadgeStyles() {
|
|
81
45
|
return (
|
|
82
|
-
<div className="flex
|
|
83
|
-
<div className="flex flex-
|
|
84
|
-
<Badge
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<div className="flex flex-col gap-2 items-center">
|
|
89
|
-
<Badge label="Small" variant="blue" size="S" />
|
|
90
|
-
<span className="text-xs">S (22px)</span>
|
|
46
|
+
<div className="flex flex-col gap-3">
|
|
47
|
+
<div className="flex flex-wrap gap-2">
|
|
48
|
+
<Badge badgeStyle="subtle" color="gray" label="Subtle" />
|
|
49
|
+
<Badge badgeStyle="subtle" color="blue" label="Subtle" />
|
|
50
|
+
<Badge badgeStyle="subtle" color="green" label="Subtle" />
|
|
51
|
+
<Badge badgeStyle="subtle" color="red" label="Subtle" />
|
|
91
52
|
</div>
|
|
92
53
|
|
|
93
|
-
<div className="flex flex-
|
|
94
|
-
<Badge
|
|
95
|
-
<
|
|
54
|
+
<div className="flex flex-wrap gap-2">
|
|
55
|
+
<Badge badgeStyle="solid" color="gray" label="Solid" />
|
|
56
|
+
<Badge badgeStyle="solid" color="blue" label="Solid" />
|
|
57
|
+
<Badge badgeStyle="solid" color="green" label="Solid" />
|
|
58
|
+
<Badge badgeStyle="solid" color="red" label="Solid" />
|
|
96
59
|
</div>
|
|
97
60
|
</div>
|
|
98
61
|
)
|
|
99
62
|
}
|
|
100
63
|
```
|
|
101
64
|
|
|
102
|
-
###
|
|
65
|
+
### Colors
|
|
103
66
|
|
|
104
|
-
|
|
67
|
+
Ten colors. Color drives the background only — foreground is uniform per `badgeStyle`.
|
|
105
68
|
|
|
106
69
|
```tsx
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
{ status: 'inactive', label: 'Inactive', variant: 'gray' },
|
|
112
|
-
{ status: 'error', label: 'Error', variant: 'redLight' },
|
|
113
|
-
]
|
|
70
|
+
const COLORS = [
|
|
71
|
+
'gray', 'slate', 'red', 'orange', 'yellow',
|
|
72
|
+
'green', 'ocean', 'blue', 'purple', 'rose',
|
|
73
|
+
] as const
|
|
114
74
|
|
|
75
|
+
export function BadgeColors() {
|
|
115
76
|
return (
|
|
116
|
-
<div className="
|
|
117
|
-
{
|
|
118
|
-
<
|
|
119
|
-
<div>
|
|
120
|
-
<h4 className="font-semibold">Server {status}</h4>
|
|
121
|
-
<p className="text-sm text-content-presentation-global-secondary">
|
|
122
|
-
Last updated 5 minutes ago
|
|
123
|
-
</p>
|
|
124
|
-
</div>
|
|
125
|
-
<Badge label={label} variant={variant as any} />
|
|
126
|
-
</div>
|
|
77
|
+
<div className="flex flex-wrap gap-2">
|
|
78
|
+
{COLORS.map(color => (
|
|
79
|
+
<Badge key={color} color={color} label={color} />
|
|
127
80
|
))}
|
|
128
81
|
</div>
|
|
129
82
|
)
|
|
130
83
|
}
|
|
131
84
|
```
|
|
132
85
|
|
|
133
|
-
###
|
|
134
|
-
|
|
135
|
-
Interactive badges that can be removed.
|
|
86
|
+
### Sizes
|
|
136
87
|
|
|
137
88
|
```tsx
|
|
138
|
-
export function
|
|
139
|
-
const [tags, setTags] = useState([
|
|
140
|
-
{ id: 1, label: 'React', variant: 'blue' },
|
|
141
|
-
{ id: 2, label: 'TypeScript', variant: 'bluePurple' },
|
|
142
|
-
{ id: 3, label: 'Next.js', variant: 'navy' },
|
|
143
|
-
{ id: 4, label: 'Tailwind', variant: 'cocktailGreen' },
|
|
144
|
-
])
|
|
145
|
-
|
|
146
|
-
const removeTag = (id: number) => {
|
|
147
|
-
setTags(tags.filter(tag => tag.id !== id))
|
|
148
|
-
}
|
|
149
|
-
|
|
89
|
+
export function BadgeSizes() {
|
|
150
90
|
return (
|
|
151
|
-
<div>
|
|
152
|
-
<
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
<Badge
|
|
156
|
-
key={tag.id}
|
|
157
|
-
label={tag.label}
|
|
158
|
-
variant={tag.variant as any}
|
|
159
|
-
isSelected
|
|
160
|
-
onUnselect={() => removeTag(tag.id)}
|
|
161
|
-
/>
|
|
162
|
-
))}
|
|
163
|
-
</div>
|
|
91
|
+
<div className="flex items-center gap-3">
|
|
92
|
+
<Badge size="XS" color="blue" label="XS" />
|
|
93
|
+
<Badge size="S" color="blue" label="S" />
|
|
94
|
+
<Badge size="M" color="blue" label="M" />
|
|
164
95
|
</div>
|
|
165
96
|
)
|
|
166
97
|
}
|
|
167
98
|
```
|
|
168
99
|
|
|
169
|
-
|
|
100
|
+
| Size | Height | Default icon | Typography |
|
|
101
|
+
|------|--------|--------------|------------|
|
|
102
|
+
| XS | 18px | 12px | body-small-medium |
|
|
103
|
+
| S | 22px | 12px | body-small-medium |
|
|
104
|
+
| M | 26px | 16px | body-medium-medium |
|
|
170
105
|
|
|
171
|
-
|
|
106
|
+
### Hide the dot
|
|
172
107
|
|
|
173
|
-
|
|
174
|
-
export function BadgesWithIcons() {
|
|
175
|
-
return (
|
|
176
|
-
<div className="flex flex-wrap gap-3">
|
|
177
|
-
<Badge
|
|
178
|
-
label="Verified"
|
|
179
|
-
variant="green"
|
|
180
|
-
badgeIcon={<i className="ri-check-line text-sm"></i>}
|
|
181
|
-
/>
|
|
182
|
-
|
|
183
|
-
<Badge
|
|
184
|
-
label="Premium"
|
|
185
|
-
variant="purple"
|
|
186
|
-
badgeIcon={<i className="ri-vip-crown-line text-sm"></i>}
|
|
187
|
-
/>
|
|
188
|
-
|
|
189
|
-
<Badge
|
|
190
|
-
label="Locked"
|
|
191
|
-
variant="gray"
|
|
192
|
-
badgeIcon={<i className="ri-lock-line text-sm"></i>}
|
|
193
|
-
/>
|
|
194
|
-
|
|
195
|
-
<Badge
|
|
196
|
-
label="New"
|
|
197
|
-
variant="cocktailGreen"
|
|
198
|
-
badgeIcon={<i className="ri-star-fill text-sm"></i>}
|
|
199
|
-
/>
|
|
200
|
-
|
|
201
|
-
<Badge
|
|
202
|
-
label="Alert"
|
|
203
|
-
variant="redOrange"
|
|
204
|
-
badgeIcon={<i className="ri-alert-line text-sm"></i>}
|
|
205
|
-
/>
|
|
206
|
-
</div>
|
|
207
|
-
)
|
|
208
|
-
}
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
### User Roles
|
|
212
|
-
|
|
213
|
-
Badge system for user permissions.
|
|
108
|
+
Set `showIcon={false}` for a label-only badge.
|
|
214
109
|
|
|
215
110
|
```tsx
|
|
216
|
-
|
|
217
|
-
const users = [
|
|
218
|
-
{ name: 'John Doe', email: 'john@example.com', role: 'Admin', variant: 'purple' },
|
|
219
|
-
{ name: 'Jane Smith', email: 'jane@example.com', role: 'Editor', variant: 'blue' },
|
|
220
|
-
{ name: 'Bob Johnson', email: 'bob@example.com', role: 'Viewer', variant: 'gray' },
|
|
221
|
-
{ name: 'Alice Brown', email: 'alice@example.com', role: 'Owner', variant: 'green' },
|
|
222
|
-
]
|
|
223
|
-
|
|
224
|
-
return (
|
|
225
|
-
<div className="space-y-2">
|
|
226
|
-
{users.map(user => (
|
|
227
|
-
<div key={user.email} className="flex items-center justify-between p-3 border rounded-lg">
|
|
228
|
-
<div className="flex items-center gap-3">
|
|
229
|
-
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center text-white font-bold">
|
|
230
|
-
{user.name.split(' ').map(n => n[0]).join('')}
|
|
231
|
-
</div>
|
|
232
|
-
<div>
|
|
233
|
-
<div className="font-medium">{user.name}</div>
|
|
234
|
-
<div className="text-sm text-content-presentation-global-secondary">
|
|
235
|
-
{user.email}
|
|
236
|
-
</div>
|
|
237
|
-
</div>
|
|
238
|
-
</div>
|
|
239
|
-
<Badge label={user.role} variant={user.variant as any} size="S" />
|
|
240
|
-
</div>
|
|
241
|
-
))}
|
|
242
|
-
</div>
|
|
243
|
-
)
|
|
244
|
-
}
|
|
111
|
+
<Badge color="orange" showIcon={false} label="No icon" />
|
|
245
112
|
```
|
|
246
113
|
|
|
247
|
-
###
|
|
114
|
+
### Custom icon
|
|
248
115
|
|
|
249
|
-
|
|
116
|
+
Replace the default dot via `badgeIcon`.
|
|
250
117
|
|
|
251
118
|
```tsx
|
|
252
|
-
export function
|
|
253
|
-
const products = [
|
|
254
|
-
{ name: 'MacBook Pro', category: 'Electronics', variant: 'blue', price: '$2,499' },
|
|
255
|
-
{ name: 'Office Chair', category: 'Furniture', variant: 'navy', price: '$299' },
|
|
256
|
-
{ name: 'Coffee Maker', category: 'Appliances', variant: 'greenLight', price: '$149' },
|
|
257
|
-
{ name: 'Desk Lamp', category: 'Lighting', variant: 'yellow', price: '$79' },
|
|
258
|
-
]
|
|
259
|
-
|
|
119
|
+
export function BadgesWithIcons() {
|
|
260
120
|
return (
|
|
261
|
-
<div className="
|
|
262
|
-
{
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
<h4 className="font-semibold">{product.name}</h4>
|
|
266
|
-
<Badge label={product.category} variant={product.variant as any} size="XS" />
|
|
267
|
-
</div>
|
|
268
|
-
<div className="text-xl font-bold text-blue-600">{product.price}</div>
|
|
269
|
-
<button className="mt-3 w-full py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
|
|
270
|
-
Add to Cart
|
|
271
|
-
</button>
|
|
272
|
-
</div>
|
|
273
|
-
))}
|
|
121
|
+
<div className="flex flex-wrap gap-2">
|
|
122
|
+
<Badge color="green" badgeIcon={<i className="ri-check-line" />} label="Verified" />
|
|
123
|
+
<Badge color="purple" badgeIcon={<i className="ri-vip-crown-line" />} label="Premium" />
|
|
124
|
+
<Badge color="gray" badgeIcon={<i className="ri-lock-line" />} label="Locked" />
|
|
274
125
|
</div>
|
|
275
126
|
)
|
|
276
127
|
}
|
|
277
128
|
```
|
|
278
129
|
|
|
279
|
-
###
|
|
130
|
+
### Closable (chips / tags)
|
|
280
131
|
|
|
281
|
-
|
|
132
|
+
Set `isClosable` and pass `onClose`. The close button uses an inline 12×12 SVG (14×14 on size `S`, 16×16 on size `M`), gets a `4px`-radius hover background (`--Background-Presentation-Action-Secondary`), and inherits the same `mix-blend-luminosity` treatment as the rest of subtle foreground.
|
|
282
133
|
|
|
283
134
|
```tsx
|
|
284
|
-
|
|
285
|
-
const tasks = [
|
|
286
|
-
{ id: 1, task: 'Fix critical bug', priority: 'Critical', variant: 'rose' },
|
|
287
|
-
{ id: 2, task: 'Update documentation', priority: 'High', variant: 'redOrange' },
|
|
288
|
-
{ id: 3, task: 'Review PR', priority: 'Medium', variant: 'yellow' },
|
|
289
|
-
{ id: 4, task: 'Refactor code', priority: 'Low', variant: 'gray' },
|
|
290
|
-
]
|
|
291
|
-
|
|
292
|
-
return (
|
|
293
|
-
<div className="space-y-2">
|
|
294
|
-
{tasks.map(task => (
|
|
295
|
-
<div key={task.id} className="flex items-center gap-3 p-3 border rounded-lg">
|
|
296
|
-
<input type="checkbox" className="w-4 h-4" />
|
|
297
|
-
<span className="flex-1">{task.task}</span>
|
|
298
|
-
<Badge label={task.priority} variant={task.variant as any} size="S" />
|
|
299
|
-
</div>
|
|
300
|
-
))}
|
|
301
|
-
</div>
|
|
302
|
-
)
|
|
303
|
-
}
|
|
304
|
-
```
|
|
135
|
+
import { useState } from 'react'
|
|
305
136
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
137
|
+
export function RemovableTags() {
|
|
138
|
+
const [tags, setTags] = useState([
|
|
139
|
+
{ id: 1, label: 'React', color: 'blue' },
|
|
140
|
+
{ id: 2, label: 'TypeScript', color: 'purple' },
|
|
141
|
+
{ id: 3, label: 'Next.js', color: 'slate' },
|
|
142
|
+
{ id: 4, label: 'Tailwind', color: 'ocean' },
|
|
143
|
+
] as const)
|
|
309
144
|
|
|
310
|
-
```tsx
|
|
311
|
-
export function NotificationBadges() {
|
|
312
145
|
return (
|
|
313
|
-
<
|
|
314
|
-
|
|
315
|
-
Messages
|
|
316
|
-
<Badge
|
|
317
|
-
label="3"
|
|
318
|
-
variant="redLight"
|
|
319
|
-
size="XS"
|
|
320
|
-
className="absolute -top-1 -right-1"
|
|
321
|
-
/>
|
|
322
|
-
</button>
|
|
323
|
-
|
|
324
|
-
<button className="relative px-4 py-2 rounded hover:bg-background-presentation-global-secondary">
|
|
325
|
-
Notifications
|
|
326
|
-
<Badge
|
|
327
|
-
label="12"
|
|
328
|
-
variant="blue"
|
|
329
|
-
size="XS"
|
|
330
|
-
className="absolute -top-1 -right-1"
|
|
331
|
-
/>
|
|
332
|
-
</button>
|
|
333
|
-
|
|
334
|
-
<button className="relative px-4 py-2 rounded hover:bg-background-presentation-global-secondary">
|
|
335
|
-
Updates
|
|
146
|
+
<div className="flex flex-wrap gap-2">
|
|
147
|
+
{tags.map(tag => (
|
|
336
148
|
<Badge
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
149
|
+
key={tag.id}
|
|
150
|
+
label={tag.label}
|
|
151
|
+
color={tag.color}
|
|
152
|
+
isClosable
|
|
153
|
+
onClose={() => setTags(prev => prev.filter(t => t.id !== tag.id))}
|
|
341
154
|
/>
|
|
342
|
-
|
|
343
|
-
</
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
344
157
|
)
|
|
345
158
|
}
|
|
346
159
|
```
|
|
347
160
|
|
|
348
|
-
###
|
|
161
|
+
### Status indicators
|
|
349
162
|
|
|
350
|
-
|
|
163
|
+
Pair semantic colors with status states.
|
|
351
164
|
|
|
352
165
|
```tsx
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
<span>Highlight (no dot):</span>
|
|
364
|
-
<Badge label="Neutral" variant="highlight" size="S" />
|
|
365
|
-
<Badge label="Label" variant="highlight" size="S" />
|
|
366
|
-
</div>
|
|
367
|
-
</div>
|
|
368
|
-
)
|
|
166
|
+
const STATUS = {
|
|
167
|
+
active: { label: 'Active', color: 'green' },
|
|
168
|
+
pending: { label: 'Pending', color: 'yellow' },
|
|
169
|
+
inactive: { label: 'Inactive', color: 'gray' },
|
|
170
|
+
error: { label: 'Error', color: 'red' },
|
|
171
|
+
} as const
|
|
172
|
+
|
|
173
|
+
export function StatusBadges({ status }: { status: keyof typeof STATUS }) {
|
|
174
|
+
const { label, color } = STATUS[status]
|
|
175
|
+
return <Badge label={label} color={color} />
|
|
369
176
|
}
|
|
370
177
|
```
|
|
371
178
|
|
|
372
179
|
## API Reference
|
|
373
180
|
|
|
374
|
-
###
|
|
375
|
-
|
|
376
|
-
| Prop | Type | Default | Description |
|
|
377
|
-
|------|------|---------|-------------|
|
|
378
|
-
| label | `string` | - | Badge text content |
|
|
379
|
-
| variant | `BadgeVariant` | `'green'` | Color variant |
|
|
380
|
-
| size | `'XS' \| 'S' \| 'M'` | `'S'` | Badge size |
|
|
381
|
-
| badgeIcon | `ReactNode` | - | Custom icon (replaces dot) |
|
|
382
|
-
| isSelected | `boolean` | `false` | Shows remove button |
|
|
383
|
-
| onUnselect | `() => void` | - | Remove handler (requires isSelected) |
|
|
384
|
-
| theme | `Themes` | - | Theme override |
|
|
385
|
-
| className | `string` | - | Additional CSS classes |
|
|
386
|
-
|
|
387
|
-
### Variant Options
|
|
388
|
-
|
|
389
|
-
| Variant | Use Case | Color |
|
|
390
|
-
|---------|----------|-------|
|
|
391
|
-
| `highlight` | Neutral labels | Gray (no dot) |
|
|
392
|
-
| `green` | Success, active | Green |
|
|
393
|
-
| `greenLight` | Light success | Light green |
|
|
394
|
-
| `cocktailGreen` | New, special | Bright green |
|
|
395
|
-
| `yellow` | Warning, pending | Yellow |
|
|
396
|
-
| `redOrange` | Alerts | Red-orange |
|
|
397
|
-
| `redLight` | Errors | Light red |
|
|
398
|
-
| `rose` | Critical | Rose |
|
|
399
|
-
| `purple` | Premium, features | Purple |
|
|
400
|
-
| `bluePurple` | Beta | Blue-purple |
|
|
401
|
-
| `blue` | Info | Blue |
|
|
402
|
-
| `navy` | Default | Navy |
|
|
403
|
-
| `gray` | Inactive, disabled | Gray |
|
|
404
|
-
|
|
405
|
-
### Size Specifications
|
|
406
|
-
|
|
407
|
-
| Size | Height | Icon Size | Typography |
|
|
408
|
-
|------|--------|-----------|------------|
|
|
409
|
-
| XS | 18px | 12px | body-small-medium |
|
|
410
|
-
| S | 22px | 12px | body-small-medium |
|
|
411
|
-
| M | 26px | 16px | body-medium-medium |
|
|
412
|
-
|
|
413
|
-
## Styling
|
|
181
|
+
### Props
|
|
414
182
|
|
|
415
|
-
|
|
183
|
+
| Prop | Type | Default | Description |
|
|
184
|
+
| ------------- | ----------------------------------------------------------------------------------------------------- | ------------ | -------------------------------------------- |
|
|
185
|
+
| `label` | `string` | — | Badge text content. |
|
|
186
|
+
| `badgeStyle` | `'subtle' \| 'solid'` | `'subtle'` | Visual style. |
|
|
187
|
+
| `color` | `'gray' \| 'slate' \| 'red' \| 'orange' \| 'yellow' \| 'green' \| 'ocean' \| 'blue' \| 'purple' \| 'rose'` | `'gray'` | Background color. |
|
|
188
|
+
| `size` | `'XS' \| 'S' \| 'M'` | `'S'` | Badge size. |
|
|
189
|
+
| `showIcon` | `boolean` | `true` | Show the leading dot. Ignored when `badgeIcon` is set. |
|
|
190
|
+
| `badgeIcon` | `ReactNode` | — | Custom leading icon (replaces the default dot). |
|
|
191
|
+
| `isClosable` | `boolean` | `false` | Render the trailing close button. |
|
|
192
|
+
| `onClose` | `() => void` | — | Close handler. Called on click and on Enter/Space. |
|
|
193
|
+
| `theme` | `Themes` | — | Theme override (`'dark' \| 'light' \| 'default'`). |
|
|
194
|
+
| `className` | `string` | — | Extra classes merged onto the root `<span>`. |
|
|
416
195
|
|
|
417
|
-
|
|
418
|
-
- **Border Radius**: 6px
|
|
419
|
-
- **Padding**: 6px horizontal, 3px for text
|
|
420
|
-
- **Cursor**: Pointer by default
|
|
421
|
-
- **Transition**: 300ms ease-in-out
|
|
422
|
-
- **Width**: Fit content
|
|
196
|
+
The Badge root is a `<span>`. Standard `HTMLAttributes<HTMLSpanElement>` (minus `color`, which is overloaded as the variant prop) are forwarded.
|
|
423
197
|
|
|
424
|
-
|
|
198
|
+
## Styling
|
|
425
199
|
|
|
426
|
-
|
|
427
|
-
// Custom background
|
|
428
|
-
<Badge label="Custom" className="!bg-purple-500 !border-purple-600" />
|
|
200
|
+
### Foreground rules
|
|
429
201
|
|
|
430
|
-
|
|
431
|
-
|
|
202
|
+
- **Subtle**: text and icons use `text-content-presentation-global-subtle` (`#494949`). The label `<div>`, any inner `<i>`, and the close `<button>` all carry `mix-blend-mode: luminosity`, so the foreground harmonizes with whichever color background sits behind it.
|
|
203
|
+
- **Solid**: text and icons use `text-content-presentation-global-primary-light`. No blend mode.
|
|
432
204
|
|
|
433
|
-
|
|
434
|
-
<Badge label="Wide" className="w-full justify-center" />
|
|
435
|
-
```
|
|
205
|
+
### Backgrounds (CSS variables)
|
|
436
206
|
|
|
437
|
-
|
|
207
|
+
Each color resolves to a pair of tokens:
|
|
438
208
|
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
type BadgeVariant =
|
|
443
|
-
| 'highlight'
|
|
444
|
-
| 'green'
|
|
445
|
-
| 'greenLight'
|
|
446
|
-
| 'cocktailGreen'
|
|
447
|
-
| 'yellow'
|
|
448
|
-
| 'redOrange'
|
|
449
|
-
| 'redLight'
|
|
450
|
-
| 'rose'
|
|
451
|
-
| 'purple'
|
|
452
|
-
| 'bluePurple'
|
|
453
|
-
| 'blue'
|
|
454
|
-
| 'navy'
|
|
455
|
-
| 'gray'
|
|
456
|
-
|
|
457
|
-
interface BadgeProps extends HTMLAttributes<HTMLButtonElement>, VariantProps<typeof badgeBase> {
|
|
458
|
-
label?: string
|
|
459
|
-
onUnselect?: () => void
|
|
460
|
-
isSelected?: boolean
|
|
461
|
-
badgeIcon?: ReactNode
|
|
462
|
-
className?: string
|
|
463
|
-
theme?: Themes
|
|
464
|
-
}
|
|
209
|
+
```
|
|
210
|
+
--background-presentation-badge-{color}-subtle
|
|
211
|
+
--background-presentation-badge-{color}-solid
|
|
465
212
|
```
|
|
466
213
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
### Dynamic Status
|
|
214
|
+
### Override via className
|
|
470
215
|
|
|
471
216
|
```tsx
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
active: 'green',
|
|
475
|
-
pending: 'yellow',
|
|
476
|
-
error: 'redLight',
|
|
477
|
-
inactive: 'gray',
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
return (
|
|
481
|
-
<Badge
|
|
482
|
-
label={status.charAt(0).toUpperCase() + status.slice(1)}
|
|
483
|
-
variant={variantMap[status]}
|
|
484
|
-
/>
|
|
485
|
-
)
|
|
486
|
-
}
|
|
217
|
+
<Badge label="Custom" className="!bg-purple-500 !text-white" />
|
|
218
|
+
<Badge label="Wide" className="w-full justify-center" />
|
|
487
219
|
```
|
|
488
220
|
|
|
489
|
-
|
|
221
|
+
## TypeScript Types
|
|
490
222
|
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
223
|
+
```typescript
|
|
224
|
+
import type { VariantProps } from 'class-variance-authority'
|
|
225
|
+
import type { badgeStyles } from '@/components/Badge'
|
|
226
|
+
|
|
227
|
+
type BadgeVariants = VariantProps<typeof badgeStyles>
|
|
228
|
+
// {
|
|
229
|
+
// badgeStyle?: 'subtle' | 'solid'
|
|
230
|
+
// color?: 'gray' | 'slate' | 'red' | 'orange' | 'yellow' | 'green' | 'ocean' | 'blue' | 'purple' | 'rose'
|
|
231
|
+
// size?: 'XS' | 'S' | 'M'
|
|
232
|
+
// }
|
|
233
|
+
|
|
234
|
+
interface BadgeProps
|
|
235
|
+
extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'color'>,
|
|
236
|
+
BadgeVariants {
|
|
237
|
+
label?: string
|
|
238
|
+
badgeIcon?: React.ReactNode
|
|
239
|
+
showIcon?: boolean
|
|
240
|
+
isClosable?: boolean
|
|
241
|
+
onClose?: () => void
|
|
242
|
+
theme?: Themes
|
|
243
|
+
className?: string
|
|
506
244
|
}
|
|
507
245
|
```
|
|
508
246
|
|
|
509
247
|
## Testing
|
|
510
248
|
|
|
511
|
-
```
|
|
249
|
+
```tsx
|
|
512
250
|
import { render, screen, fireEvent } from '@testing-library/react'
|
|
513
251
|
import { Badge } from '@/components/Badge'
|
|
514
252
|
|
|
515
253
|
describe('Badge', () => {
|
|
516
|
-
it('renders label
|
|
517
|
-
render(<Badge label="
|
|
518
|
-
expect(screen.getByText('
|
|
519
|
-
})
|
|
520
|
-
|
|
521
|
-
it('applies correct variant class', () => {
|
|
522
|
-
const { container } = render(<Badge label="Test" variant="blue" />)
|
|
523
|
-
expect(container.firstChild).toHaveClass('border-border-presentation-badge-blue')
|
|
254
|
+
it('renders the label', () => {
|
|
255
|
+
render(<Badge label="Active" />)
|
|
256
|
+
expect(screen.getByText('Active')).toBeInTheDocument()
|
|
524
257
|
})
|
|
525
258
|
|
|
526
|
-
it('shows
|
|
527
|
-
render(<Badge label="
|
|
259
|
+
it('shows the close button when closable', () => {
|
|
260
|
+
render(<Badge label="Tag" isClosable onClose={() => {}} />)
|
|
528
261
|
expect(screen.getByRole('button', { name: 'Remove badge' })).toBeInTheDocument()
|
|
529
262
|
})
|
|
530
263
|
|
|
531
|
-
it('
|
|
532
|
-
const
|
|
533
|
-
render(<Badge label="
|
|
534
|
-
|
|
264
|
+
it('fires onClose on click', () => {
|
|
265
|
+
const onClose = jest.fn()
|
|
266
|
+
render(<Badge label="Tag" isClosable onClose={onClose} />)
|
|
535
267
|
fireEvent.click(screen.getByRole('button', { name: 'Remove badge' }))
|
|
536
|
-
expect(
|
|
537
|
-
})
|
|
538
|
-
|
|
539
|
-
it('renders custom icon', () => {
|
|
540
|
-
render(
|
|
541
|
-
<Badge
|
|
542
|
-
label="Test"
|
|
543
|
-
badgeIcon={<i className="ri-star-fill"></i>}
|
|
544
|
-
/>
|
|
545
|
-
)
|
|
546
|
-
|
|
547
|
-
expect(document.querySelector('.ri-star-fill')).toBeInTheDocument()
|
|
268
|
+
expect(onClose).toHaveBeenCalledTimes(1)
|
|
548
269
|
})
|
|
549
270
|
|
|
550
|
-
it('
|
|
551
|
-
const
|
|
552
|
-
render(<Badge label="
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
fireEvent.keyDown(
|
|
556
|
-
expect(
|
|
271
|
+
it('fires onClose on Enter and Space', () => {
|
|
272
|
+
const onClose = jest.fn()
|
|
273
|
+
render(<Badge label="Tag" isClosable onClose={onClose} />)
|
|
274
|
+
const btn = screen.getByRole('button', { name: 'Remove badge' })
|
|
275
|
+
fireEvent.keyDown(btn, { key: 'Enter' })
|
|
276
|
+
fireEvent.keyDown(btn, { key: ' ' })
|
|
277
|
+
expect(onClose).toHaveBeenCalledTimes(2)
|
|
557
278
|
})
|
|
558
279
|
})
|
|
559
280
|
```
|
|
560
281
|
|
|
561
282
|
## Accessibility
|
|
562
283
|
|
|
563
|
-
-
|
|
564
|
-
-
|
|
565
|
-
-
|
|
566
|
-
-
|
|
567
|
-
- **Color Independence**: Not relying solely on color (includes text)
|
|
568
|
-
- **Contrast**: All variants meet WCAG AA standards
|
|
569
|
-
|
|
570
|
-
## Performance
|
|
571
|
-
|
|
572
|
-
- **Lightweight**: < 1 KB per badge instance
|
|
573
|
-
- **CSS-Only Colors**: No JavaScript color calculations
|
|
574
|
-
- **Optimized Rendering**: Minimal DOM nodes
|
|
575
|
-
- **Bundle Size**: ~2 KB gzipped (including variants)
|
|
576
|
-
|
|
577
|
-
### Performance Tips
|
|
578
|
-
|
|
579
|
-
```tsx
|
|
580
|
-
// Memoize badge lists
|
|
581
|
-
const MemoizedBadge = React.memo(Badge)
|
|
582
|
-
|
|
583
|
-
// Use keys for lists
|
|
584
|
-
{tags.map(tag => (
|
|
585
|
-
<MemoizedBadge key={tag.id} {...tag} />
|
|
586
|
-
))}
|
|
587
|
-
```
|
|
588
|
-
|
|
589
|
-
## Migration Guide
|
|
590
|
-
|
|
591
|
-
### From Custom Spans
|
|
592
|
-
|
|
593
|
-
```tsx
|
|
594
|
-
// Before: Custom badge
|
|
595
|
-
<span className="px-2 py-1 bg-green-100 text-green-800 rounded text-sm">
|
|
596
|
-
Active
|
|
597
|
-
</span>
|
|
598
|
-
|
|
599
|
-
// After: Badge
|
|
600
|
-
<Badge label="Active" variant="green" size="S" />
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
### From Other Libraries
|
|
604
|
-
|
|
605
|
-
```tsx
|
|
606
|
-
// Before: Material-UI Chip
|
|
607
|
-
<Chip label="Active" color="success" size="small" onDelete={handleDelete} />
|
|
608
|
-
|
|
609
|
-
// After: Badge
|
|
610
|
-
<Badge label="Active" variant="green" size="S" isSelected onUnselect={handleDelete} />
|
|
611
|
-
```
|
|
284
|
+
- The close button has `aria-label="Remove badge"` and is keyboard-focusable.
|
|
285
|
+
- Enter and Space trigger `onClose`.
|
|
286
|
+
- The close-icon SVG is `aria-hidden`; the accessible name comes from the button label.
|
|
287
|
+
- Color is never the sole signal — always pair `color` with a `label` or icon.
|
|
612
288
|
|
|
613
289
|
## Best Practices
|
|
614
290
|
|
|
615
|
-
1.
|
|
616
|
-
2.
|
|
617
|
-
3.
|
|
618
|
-
4.
|
|
619
|
-
5.
|
|
620
|
-
6. **Removable UX**: Only make badges removable when it makes sense
|
|
621
|
-
7. **Whitespace**: Don't overcrowd badges - allow breathing room
|
|
622
|
-
8. **Limit Variants**: Stick to 3-5 variants in a single view for clarity
|
|
291
|
+
1. Use the same `color` for the same meaning across the app (e.g. `green` = success).
|
|
292
|
+
2. Match `size` to surrounding text — `XS` for inline metadata, `M` for standalone status.
|
|
293
|
+
3. Prefer `subtle` in dense UIs; reserve `solid` for emphasis.
|
|
294
|
+
4. Only set `isClosable` when removal is a real user action — don't fake it for visual flair.
|
|
295
|
+
5. Provide a meaningful `label`; never rely on color alone.
|