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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Badge
3
- description: Colorful status indicator and label component with multiple variants and optional removable functionality.
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
- A versatile badge component for displaying status, categories, tags, and labels. Features 12 color variants, three sizes, and optional remove functionality for tag-like behavior.
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" variant="green" size="S" />
31
+ return <Badge label="Active" color="green" />
33
32
  }
34
33
  ```
35
34
 
36
- ## Examples
37
-
38
- ### All Variants
35
+ Defaults: `badgeStyle="subtle"`, `color="gray"`, `size="S"`, `showIcon=true`.
39
36
 
40
- Badge offers 12 distinct color variants for different use cases.
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
- ### All Sizes
39
+ ### Styles
76
40
 
77
- Three size options to match different contexts.
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 BadgeSizes() {
44
+ export function BadgeStyles() {
81
45
  return (
82
- <div className="flex items-center gap-4">
83
- <div className="flex flex-col gap-2 items-center">
84
- <Badge label="Extra Small" variant="blue" size="XS" />
85
- <span className="text-xs">XS (18px)</span>
86
- </div>
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-col gap-2 items-center">
94
- <Badge label="Medium" variant="blue" size="M" />
95
- <span className="text-xs">M (26px)</span>
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
- ### Status Indicators
65
+ ### Colors
103
66
 
104
- Use badges to show item status.
67
+ Ten colors. Color drives the background only — foreground is uniform per `badgeStyle`.
105
68
 
106
69
  ```tsx
107
- export function StatusBadges() {
108
- const statuses = [
109
- { status: 'active', label: 'Active', variant: 'green' },
110
- { status: 'pending', label: 'Pending', variant: 'yellow' },
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="space-y-4">
117
- {statuses.map(({ status, label, variant }) => (
118
- <div key={status} className="flex items-center justify-between p-4 border rounded-lg">
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
- ### Removable Tags
134
-
135
- Interactive badges that can be removed.
86
+ ### Sizes
136
87
 
137
88
  ```tsx
138
- export function RemovableTags() {
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
- <h3 className="font-semibold mb-3">Selected Technologies</h3>
153
- <div className="flex flex-wrap gap-2">
154
- {tags.map(tag => (
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
- ### With Custom Icons
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
- Replace default dot with custom icons.
106
+ ### Hide the dot
172
107
 
173
- ```tsx
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
- export function UserRoleBadges() {
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
- ### Product Categories
114
+ ### Custom icon
248
115
 
249
- Categorize items with color-coded badges.
116
+ Replace the default dot via `badgeIcon`.
250
117
 
251
118
  ```tsx
252
- export function ProductCategories() {
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="grid grid-cols-1 md:grid-cols-2 gap-4">
262
- {products.map(product => (
263
- <div key={product.name} className="border rounded-lg p-4">
264
- <div className="flex items-start justify-between mb-2">
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
- ### Priority Levels
130
+ ### Closable (chips / tags)
280
131
 
281
- Visual priority indicators.
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
- export function PriorityBadges() {
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
- ### Notification Counts
307
-
308
- Combine with count indicators.
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
- <nav className="flex gap-4">
314
- <button className="relative px-4 py-2 rounded hover:bg-background-presentation-global-secondary">
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
- label="New"
338
- variant="cocktailGreen"
339
- size="XS"
340
- className="absolute -top-1 -right-1"
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
- </button>
343
- </nav>
155
+ ))}
156
+ </div>
344
157
  )
345
158
  }
346
159
  ```
347
160
 
348
- ### Highlight Badge
161
+ ### Status indicators
349
162
 
350
- Special highlight variant without dot indicator.
163
+ Pair semantic colors with status states.
351
164
 
352
165
  ```tsx
353
- export function HighlightBadge() {
354
- return (
355
- <div className="space-y-4">
356
- <div className="flex items-center gap-2">
357
- <span>Regular badges:</span>
358
- <Badge label="Green" variant="green" size="S" />
359
- <Badge label="Blue" variant="blue" size="S" />
360
- </div>
361
-
362
- <div className="flex items-center gap-2">
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
- ### Badge Props
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
- ### Base Styles
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
- - **Border**: 1px solid border matching variant
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
- ### Customization
198
+ ## Styling
425
199
 
426
- ```tsx
427
- // Custom background
428
- <Badge label="Custom" className="!bg-purple-500 !border-purple-600" />
200
+ ### Foreground rules
429
201
 
430
- // No cursor
431
- <Badge label="Static" className="cursor-default" />
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
- // Full width
434
- <Badge label="Wide" className="w-full justify-center" />
435
- ```
205
+ ### Backgrounds (CSS variables)
436
206
 
437
- ## TypeScript Types
207
+ Each color resolves to a pair of tokens:
438
208
 
439
- ```typescript
440
- import { VariantProps } from 'class-variance-authority'
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
- ## Common Patterns
468
-
469
- ### Dynamic Status
214
+ ### Override via className
470
215
 
471
216
  ```tsx
472
- function StatusBadge({ status }) {
473
- const variantMap = {
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
- ### Badge List
221
+ ## TypeScript Types
490
222
 
491
- ```tsx
492
- function BadgeList({ items, onRemove }) {
493
- return (
494
- <div className="flex flex-wrap gap-2">
495
- {items.map(item => (
496
- <Badge
497
- key={item.id}
498
- label={item.label}
499
- variant={item.variant}
500
- isSelected={!!onRemove}
501
- onUnselect={() => onRemove?.(item.id)}
502
- />
503
- ))}
504
- </div>
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
- ```typescript
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 correctly', () => {
517
- render(<Badge label="Test Badge" />)
518
- expect(screen.getByText('Test Badge')).toBeInTheDocument()
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 remove button when isSelected', () => {
527
- render(<Badge label="Test" isSelected />)
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('calls onUnselect when remove clicked', () => {
532
- const handleUnselect = jest.fn()
533
- render(<Badge label="Test" isSelected onUnselect={handleUnselect} />)
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(handleUnselect).toHaveBeenCalledTimes(1)
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('handles keyboard interaction on remove button', () => {
551
- const handleUnselect = jest.fn()
552
- render(<Badge label="Test" isSelected onUnselect={handleUnselect} />)
553
-
554
- const removeBtn = screen.getByRole('button', { name: 'Remove badge' })
555
- fireEvent.keyDown(removeBtn, { key: 'Enter' })
556
- expect(handleUnselect).toHaveBeenCalledTimes(1)
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
- - **ARIA Labels**: Remove button has proper aria-label
564
- - **Keyboard Support**: Enter and Space keys trigger removal
565
- - **Focus Management**: Remove button is keyboard focusable
566
- - **Screen Reader**: Badge content is announced
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. **Consistent Variants**: Use the same variant for the same meaning across your app
616
- 2. **Size Context**: Match badge size to surrounding content
617
- 3. **Accessibility**: Always provide meaningful labels
618
- 4. **Color Semantics**: Follow color conventions (green=success, red=error)
619
- 5. **Icon Usage**: Use icons sparingly for special emphasis
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.