@telegraph/tag 0.0.94 → 0.0.96

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/README.md CHANGED
@@ -1,14 +1,16 @@
1
- ![Telegraph by Knock](https://github.com/knocklabs/telegraph/assets/29106675/9b5022e3-b02c-4582-ba57-3d6171e45e44)
1
+ # 🏷️ Tag
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/@telegraph/tag.svg)](https://www.npmjs.com/package/@telegraph/tag)
3
+ > Flexible tag component with optional interactive features like removal and copying, supporting multiple variants and colors.
4
4
 
5
- # @telegraph/tag
5
+ ![Telegraph by Knock](https://github.com/knocklabs/telegraph/assets/29106675/9b5022e3-b02c-4582-ba57-3d6171e45e44)
6
6
 
7
- > A tag component with optional interactive button
7
+ [![npm version](https://img.shields.io/npm/v/@telegraph/tag.svg)](https://www.npmjs.com/package/@telegraph/tag)
8
+ [![minzipped size](https://img.shields.io/bundlephobia/minzip/@telegraph/tag)](https://bundlephobia.com/result?p=@telegraph/tag)
9
+ [![license](https://img.shields.io/npm/l/@telegraph/tag)](https://github.com/knocklabs/telegraph/blob/main/LICENSE)
8
10
 
9
- ## Installation Instructions
11
+ ## Installation
10
12
 
11
- ```
13
+ ```bash
12
14
  npm install @telegraph/tag
13
15
  ```
14
16
 
@@ -18,140 +20,768 @@ Pick one:
18
20
 
19
21
  Via CSS (preferred):
20
22
 
21
- ```
22
- @import "@telegraph/tag"
23
+ ```css
24
+ @import "@telegraph/tag";
23
25
  ```
24
26
 
25
27
  Via Javascript:
26
28
 
27
- ```
28
- import "@telegraph/tag/default.css"
29
+ ```tsx
30
+ import "@telegraph/tag/default.css";
29
31
  ```
30
32
 
31
33
  > Then, include `className="tgph"` on the farthest parent element wrapping the telegraph components
32
34
 
33
- ## Usage
35
+ ## Quick Start
36
+
37
+ ```tsx
38
+ import { Tag } from "@telegraph/tag";
39
+ import { AlertCircle, User } from "lucide-react";
40
+
41
+ export const TagExamples = () => (
42
+ <div>
43
+ {/* Basic tag */}
44
+ <Tag>New Feature</Tag>
45
+
46
+ {/* Tag with icon */}
47
+ <Tag icon={{ icon: User, alt: "User" }}>John Doe</Tag>
48
+
49
+ {/* Interactive tag with remove */}
50
+ <Tag onRemove={() => console.log("removed")}>Removable Tag</Tag>
51
+
52
+ {/* Tag with copy functionality */}
53
+ <Tag onCopy={() => console.log("copied")} textToCopy="api-key-12345">
54
+ API Key
55
+ </Tag>
56
+
57
+ {/* Different variants and colors */}
58
+ <Tag variant="solid" color="blue">
59
+ Status: Active
60
+ </Tag>
61
+ <Tag variant="soft" color="red" icon={{ icon: AlertCircle, alt: "Error" }}>
62
+ Error
63
+ </Tag>
64
+ </div>
65
+ );
66
+ ```
34
67
 
35
- ### Basic Usage
68
+ ## API Reference
69
+
70
+ ### `<Tag>`
71
+
72
+ The main tag component with built-in interactive features.
73
+
74
+ | Prop | Type | Default | Description |
75
+ | ------------ | ------------------- | ---------------- | ----------------------------------- |
76
+ | `size` | `"0" \| "1" \| "2"` | `"1"` | Size of the tag |
77
+ | `color` | `TagColor` | `"default"` | Color scheme of the tag |
78
+ | `variant` | `"soft" \| "solid"` | `"soft"` | Visual style variant |
79
+ | `icon` | `IconProps` | `undefined` | Icon to display at the start |
80
+ | `onRemove` | `() => void` | `undefined` | Makes tag removable with X button |
81
+ | `onCopy` | `() => void` | `undefined` | Adds copy button functionality |
82
+ | `textToCopy` | `string` | `undefined` | Text to copy (defaults to children) |
83
+ | `textProps` | `TextProps` | `{ maxW: "40" }` | Props passed to the text component |
84
+
85
+ #### TagColor Type
86
+
87
+ ```tsx
88
+ type TagColor =
89
+ | "default"
90
+ | "gray"
91
+ | "red"
92
+ | "accent"
93
+ | "blue"
94
+ | "green"
95
+ | "yellow"
96
+ | "purple";
97
+ ```
36
98
 
37
- Shorthand tag component that adheres to the telegraph design system
99
+ #### IconProps Type
38
100
 
101
+ ```tsx
102
+ type IconProps = {
103
+ icon: LucideIcon;
104
+ alt: string;
105
+ };
39
106
  ```
40
- import { Tag } from "@telegraph/tag"
41
107
 
42
- ...
108
+ ### Composition Components
43
109
 
44
- <Tag>Tag</Tag>
45
- ```
110
+ For custom layouts, use the individual components:
111
+
112
+ - **`<Tag.Root>`** - Container component that provides context
113
+ - **`<Tag.Text>`** - Text content with overflow handling
114
+ - **`<Tag.Icon>`** - Icon component with contextual styling
115
+ - **`<Tag.Button>`** - Remove button
116
+ - **`<Tag.CopyButton>`** - Copy button with animation
46
117
 
47
- #### Props
118
+ For detailed props of each component, see the [Complete Component Reference](#complete-component-reference) section below.
48
119
 
49
- | Name | Type | Default | Options |
50
- | -------- | ---------------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------- |
51
- | size | string | "1" | "1" "2" |
52
- | color | string | "default" | "default", "gray", "red", "accent", "blue", "green", "yellow", "purple" |
53
- | variant | string | "soft" | "soft", "solid" |
54
- | icon | [Icon Props](https://github.com/knocklabs/telegraph/tree/main/packages/icon#props) | `undefined` | |
55
- | onRemove | () => {} | `undefined` | |
56
- | onCopy | () => {} | `undefined` | |
120
+ ## Usage Patterns
57
121
 
58
- ### Advanced Usage
122
+ ### Basic Tags
59
123
 
60
- Individual parts of the tag component that can be composed in configurations different from the default telegraph design system styles. This can be used to create modifications to one-off tag components without the need to modify the tag exported from this package.
124
+ ```tsx
125
+ import { Tag } from "@telegraph/tag";
61
126
 
62
- #### `<Tag.Root/>`
127
+ export const StatusTags = () => (
128
+ <div>
129
+ <Tag>Draft</Tag>
130
+ <Tag color="blue">Published</Tag>
131
+ <Tag color="yellow">Pending Review</Tag>
132
+ <Tag color="green">Approved</Tag>
133
+ <Tag color="red">Rejected</Tag>
134
+ </div>
135
+ );
136
+ ```
63
137
 
64
- Wraps the Tag children components and relays props to them.
138
+ ### Different Sizes
65
139
 
140
+ ```tsx
141
+ <div>
142
+ <Tag size="0">Small Tag</Tag>
143
+ <Tag size="1">Medium Tag</Tag>
144
+ <Tag size="2">Large Tag</Tag>
145
+ </div>
66
146
  ```
67
- import { Tag } from '@telegraph/tag'
68
147
 
69
- ...
148
+ ### Solid Variant
149
+
150
+ ```tsx
151
+ <div>
152
+ <Tag variant="solid" color="blue">
153
+ Blue Solid
154
+ </Tag>
155
+ <Tag variant="solid" color="green">
156
+ Green Solid
157
+ </Tag>
158
+ <Tag variant="solid" color="red">
159
+ Red Solid
160
+ </Tag>
161
+ </div>
162
+ ```
70
163
 
71
- <Tag.Root></Tag.Root>
164
+ ### With Icons
165
+
166
+ ```tsx
167
+ import { Tag } from "@telegraph/tag";
168
+ import { Calendar, MapPin, Star, User } from "lucide-react";
169
+
170
+ export const IconTags = () => (
171
+ <div>
172
+ <Tag icon={{ icon: User, alt: "User" }}>John Doe</Tag>
173
+ <Tag icon={{ icon: Calendar, alt: "Date" }} color="blue">
174
+ Due: Dec 25
175
+ </Tag>
176
+ <Tag icon={{ icon: MapPin, alt: "Location" }} color="green">
177
+ San Francisco
178
+ </Tag>
179
+ <Tag icon={{ icon: Star, alt: "Rating" }} color="yellow">
180
+ 4.8 Rating
181
+ </Tag>
182
+ </div>
183
+ );
72
184
  ```
73
185
 
74
- #### Props
186
+ ### Interactive Tags
187
+
188
+ ```tsx
189
+ import { Tag } from "@telegraph/tag";
190
+ import { useState } from "react";
191
+
192
+ export const InteractiveTags = () => {
193
+ const [tags, setTags] = useState(["React", "TypeScript", "Next.js"]);
194
+
195
+ const removeTag = (tagToRemove: string) => {
196
+ setTags(tags.filter((tag) => tag !== tagToRemove));
197
+ };
198
+
199
+ return (
200
+ <div>
201
+ {tags.map((tag) => (
202
+ <Tag key={tag} onRemove={() => removeTag(tag)} color="blue">
203
+ {tag}
204
+ </Tag>
205
+ ))}
206
+ </div>
207
+ );
208
+ };
209
+ ```
75
210
 
76
- | Name | Type | Default | Options |
77
- | ------- | ------ | --------- | ----------------------------------------------------------------------- |
78
- | size | string | "1" | "1" "2" |
79
- | color | string | "default" | "default", "gray", "red", "accent", "blue", "green", "yellow", "purple" |
80
- | variant | string | "soft" | "soft", "solid" |
211
+ ### Copy Functionality
212
+
213
+ ```tsx
214
+ import { Tag } from "@telegraph/tag";
215
+
216
+ export const CopyableTags = () => (
217
+ <div>
218
+ <Tag
219
+ onCopy={() => navigator.clipboard.writeText("api-key-12345")}
220
+ textToCopy="api-key-12345"
221
+ color="accent"
222
+ >
223
+ API Key
224
+ </Tag>
225
+
226
+ <Tag
227
+ onCopy={() => navigator.clipboard.writeText("user-id-67890")}
228
+ textToCopy="user-id-67890"
229
+ color="blue"
230
+ >
231
+ User ID
232
+ </Tag>
233
+ </div>
234
+ );
235
+ ```
81
236
 
82
- #### `<Tag.Text/>`
237
+ ## Advanced Usage
238
+
239
+ ### Custom Composition
240
+
241
+ ```tsx
242
+ import { Tag } from "@telegraph/tag";
243
+ import { Crown, X } from "lucide-react";
244
+
245
+ export const CustomTag = ({ onRemove, isPremium }) => (
246
+ <Tag.Root color={isPremium ? "yellow" : "gray"} variant="soft" size="2">
247
+ {isPremium && <Tag.Icon icon={Crown} alt="Premium user" />}
248
+ <Tag.Text>Premium User</Tag.Text>
249
+ {onRemove && (
250
+ <Tag.Button
251
+ onClick={onRemove}
252
+ icon={{ icon: X, alt: "Remove premium status" }}
253
+ />
254
+ )}
255
+ </Tag.Root>
256
+ );
257
+ ```
83
258
 
84
- A component built on top of the Text component from [@telegraph/typography](https://github.com/knocklabs/telegraph/tree/main/packages/typography) with translated props to adhere to the telegraph design system
259
+ ### Dynamic Tag Management
260
+
261
+ ```tsx
262
+ import { Tag } from "@telegraph/tag";
263
+ import { Plus } from "lucide-react";
264
+ import { useState } from "react";
265
+
266
+ export const TagManager = () => {
267
+ const [tags, setTags] = useState([
268
+ { id: 1, label: "Frontend", color: "blue" },
269
+ { id: 2, label: "Backend", color: "green" },
270
+ { id: 3, label: "Design", color: "purple" },
271
+ ]);
272
+ const [newTag, setNewTag] = useState("");
273
+
274
+ const addTag = () => {
275
+ if (newTag.trim()) {
276
+ setTags([
277
+ ...tags,
278
+ {
279
+ id: Date.now(),
280
+ label: newTag,
281
+ color: "default",
282
+ },
283
+ ]);
284
+ setNewTag("");
285
+ }
286
+ };
287
+
288
+ const removeTag = (id: number) => {
289
+ setTags(tags.filter((tag) => tag.id !== id));
290
+ };
291
+
292
+ return (
293
+ <div>
294
+ <div className="tag-list">
295
+ {tags.map((tag) => (
296
+ <Tag
297
+ key={tag.id}
298
+ color={tag.color}
299
+ onRemove={() => removeTag(tag.id)}
300
+ >
301
+ {tag.label}
302
+ </Tag>
303
+ ))}
304
+ </div>
305
+
306
+ <div className="add-tag">
307
+ <input
308
+ value={newTag}
309
+ onChange={(e) => setNewTag(e.target.value)}
310
+ placeholder="Add new tag..."
311
+ onKeyPress={(e) => e.key === "Enter" && addTag()}
312
+ />
313
+ <button onClick={addTag}>
314
+ <Plus size={16} /> Add Tag
315
+ </button>
316
+ </div>
317
+ </div>
318
+ );
319
+ };
320
+ ```
85
321
 
322
+ ### Tag Filtering
323
+
324
+ ```tsx
325
+ import { Tag } from "@telegraph/tag";
326
+ import { useState } from "react";
327
+
328
+ export const TagFilter = ({ items, onFilter }) => {
329
+ const [selectedTags, setSelectedTags] = useState([]);
330
+
331
+ const availableTags = [...new Set(items.flatMap((item) => item.tags))];
332
+
333
+ const toggleTag = (tag: string) => {
334
+ const newSelectedTags = selectedTags.includes(tag)
335
+ ? selectedTags.filter((t) => t !== tag)
336
+ : [...selectedTags, tag];
337
+
338
+ setSelectedTags(newSelectedTags);
339
+ onFilter(newSelectedTags);
340
+ };
341
+
342
+ return (
343
+ <div>
344
+ <h3>Filter by tags:</h3>
345
+ <div className="tag-filters">
346
+ {availableTags.map((tag) => (
347
+ <Tag
348
+ key={tag}
349
+ variant={selectedTags.includes(tag) ? "solid" : "soft"}
350
+ color="blue"
351
+ onClick={() => toggleTag(tag)}
352
+ style={{ cursor: "pointer" }}
353
+ >
354
+ {tag}
355
+ </Tag>
356
+ ))}
357
+ </div>
358
+
359
+ {selectedTags.length > 0 && (
360
+ <div className="active-filters">
361
+ <span>Active filters: </span>
362
+ {selectedTags.map((tag) => (
363
+ <Tag
364
+ key={tag}
365
+ color="accent"
366
+ onRemove={() => toggleTag(tag)}
367
+ size="0"
368
+ >
369
+ {tag}
370
+ </Tag>
371
+ ))}
372
+ </div>
373
+ )}
374
+ </div>
375
+ );
376
+ };
86
377
  ```
87
- import { Tag } from '@telegraph/tag'
88
378
 
89
- ...
379
+ ### Polymorphic Usage
380
+
381
+ ```tsx
382
+ import { Tag } from "@telegraph/tag";
383
+ import { Link } from "next/link";
384
+
385
+ export const TagLinks = () => (
386
+ <div>
387
+ {/* Tag as a link */}
388
+ <Tag.Root as={Link} href="/category/frontend" color="blue">
389
+ <Tag.Text>Frontend</Tag.Text>
390
+ </Tag.Root>
391
+
392
+ {/* Tag as a button */}
393
+ <Tag.Root as="button" onClick={() => console.log("clicked")} color="green">
394
+ <Tag.Text>Clickable Tag</Tag.Text>
395
+ </Tag.Root>
396
+ </div>
397
+ );
398
+ ```
90
399
 
91
- <Tag.Text>Text</Tag.Text>
400
+ ### Form Integration
401
+
402
+ ```tsx
403
+ import { Tag } from "@telegraph/tag";
404
+ import { useState } from "react";
405
+
406
+ export const TagInput = ({
407
+ value = [],
408
+ onChange,
409
+ placeholder = "Add tags...",
410
+ }) => {
411
+ const [inputValue, setInputValue] = useState("");
412
+
413
+ const addTag = () => {
414
+ const trimmed = inputValue.trim();
415
+ if (trimmed && !value.includes(trimmed)) {
416
+ onChange([...value, trimmed]);
417
+ setInputValue("");
418
+ }
419
+ };
420
+
421
+ const removeTag = (tagToRemove: string) => {
422
+ onChange(value.filter((tag) => tag !== tagToRemove));
423
+ };
424
+
425
+ const handleKeyDown = (e: React.KeyboardEvent) => {
426
+ if (e.key === "Enter") {
427
+ e.preventDefault();
428
+ addTag();
429
+ } else if (e.key === "Backspace" && !inputValue && value.length > 0) {
430
+ removeTag(value[value.length - 1]);
431
+ }
432
+ };
433
+
434
+ return (
435
+ <div className="tag-input">
436
+ <div className="tag-list">
437
+ {value.map((tag) => (
438
+ <Tag key={tag} onRemove={() => removeTag(tag)} color="blue" size="0">
439
+ {tag}
440
+ </Tag>
441
+ ))}
442
+ <input
443
+ value={inputValue}
444
+ onChange={(e) => setInputValue(e.target.value)}
445
+ onKeyDown={handleKeyDown}
446
+ onBlur={addTag}
447
+ placeholder={placeholder}
448
+ className="tag-input-field"
449
+ />
450
+ </div>
451
+ </div>
452
+ );
453
+ };
92
454
  ```
93
455
 
94
- ##### Props
456
+ ### Animated Tag Groups
457
+
458
+ ```tsx
459
+ import { Tag } from "@telegraph/tag";
460
+ import { AnimatePresence, motion } from "motion/react";
461
+
462
+ export const AnimatedTags = ({ tags, onRemove }) => (
463
+ <div className="animated-tags">
464
+ <AnimatePresence>
465
+ {tags.map((tag) => (
466
+ <motion.div
467
+ key={tag.id}
468
+ initial={{ opacity: 0, scale: 0.8 }}
469
+ animate={{ opacity: 1, scale: 1 }}
470
+ exit={{ opacity: 0, scale: 0.8 }}
471
+ transition={{ duration: 0.2 }}
472
+ >
473
+ <Tag color={tag.color} onRemove={() => onRemove(tag.id)}>
474
+ {tag.label}
475
+ </Tag>
476
+ </motion.div>
477
+ ))}
478
+ </AnimatePresence>
479
+ </div>
480
+ );
481
+ ```
95
482
 
96
- > See [text props](https://github.com/knocklabs/telegraph/tree/main/packages/typography)
483
+ ## Accessibility
97
484
 
98
- #### `<Tag.Button/>`
485
+ - ✅ **Keyboard Navigation**: Full keyboard support for interactive elements
486
+ - ✅ **Screen Reader Support**: Proper ARIA labels and descriptions
487
+ - ✅ **Focus Management**: Clear focus indicators for buttons
488
+ - ✅ **High Contrast**: Compatible with high contrast modes
489
+ - ✅ **Icon Alt Text**: Required alt text for all icons
99
490
 
100
- A component build on top of the Button component from [@telegraph/button](https://github.com/knocklabs/telegraph/tree/main/packages/button)
491
+ ### Best Practices
101
492
 
102
- ```
103
- import { Tag } from '@telegraph/tag'
493
+ 1. **Provide Icon Alt Text**: Always include meaningful alt text for icons
494
+ 2. **Meaningful Labels**: Use descriptive text that explains the tag's purpose
495
+ 3. **Action Feedback**: Copy and remove actions provide immediate feedback
496
+ 4. **Keyboard Accessible**: All interactive elements are keyboard accessible
497
+ 5. **Color Independence**: Don't rely solely on color to convey meaning
104
498
 
105
- ...
499
+ ### ARIA Attributes
106
500
 
107
- <Tag.Button/>
108
- ```
501
+ - Icons include proper `alt` attributes or `aria-hidden="true"`
502
+ - Copy button includes tooltip with descriptive text
503
+ - Remove button includes clear action description
504
+ - Motion respects `prefers-reduced-motion` preference
109
505
 
110
- #### Props
506
+ ## Complete Component Reference
111
507
 
112
- > See [button props](https://github.com/knocklabs/telegraph/tree/main/packages/icon#props)
508
+ ### `<Tag.Root>`
113
509
 
114
- #### `<Tag.Text/>`
510
+ The container component that provides context for all child components.
115
511
 
116
- A component built on top of the Text component from [@telegraph/typography](https://github.com/knocklabs/telegraph/tree/main/packages/typography) with translated props to adhere to the telegraph design system
512
+ | Prop | Type | Default | Description |
513
+ | --------- | ------------------- | ----------- | ------------------------ |
514
+ | `size` | `"0" \| "1" \| "2"` | `"1"` | Size of the tag |
515
+ | `color` | `TagColor` | `"default"` | Color scheme |
516
+ | `variant` | `"soft" \| "solid"` | `"soft"` | Visual style |
517
+ | `as` | `TgphElement` | `"span"` | Polymorphic element type |
117
518
 
118
- ```
119
- import { Tag } from '@telegraph/tag'
519
+ ### `<Tag.Text>`
120
520
 
121
- ...
521
+ Text content component with overflow handling.
122
522
 
123
- <Tag.Text>Text</Tag.Text>
124
- ```
523
+ | Prop | Type | Default | Description |
524
+ | ---------- | ------------- | ---------- | -------------------------------- |
525
+ | `maxW` | `string` | `"40"` | Maximum width (in design tokens) |
526
+ | `overflow` | `string` | `"hidden"` | CSS overflow behavior |
527
+ | `as` | `TgphElement` | `"span"` | Polymorphic element type |
125
528
 
126
- ##### Props
529
+ ### `<Tag.Icon>`
127
530
 
128
- > See [text props](https://github.com/knocklabs/telegraph/tree/main/packages/typography)
531
+ Icon component with contextual styling.
129
532
 
130
- #### `<Tag.Icon/>`
533
+ | Prop | Type | Default | Description |
534
+ | ------ | ------------ | ------- | -------------------------------------- |
535
+ | `icon` | `LucideIcon` | - | **Required.** Icon component to render |
536
+ | `alt` | `string` | - | **Required.** Alternative text |
131
537
 
132
- A component build on top of the Icon component from [@telegraph/icon](https://github.com/knocklabs/telegraph/tree/main/packages/icon)
538
+ ### `<Tag.Button>`
133
539
 
134
- ```
135
- import { Tag } from '@telegraph/tag'
540
+ Remove button component.
136
541
 
137
- ...
542
+ | Prop | Type | Default | Description |
543
+ | --------- | ------------ | --------------------------- | ------------------------- |
544
+ | `onClick` | `() => void` | - | Click handler for removal |
545
+ | `icon` | `IconProps` | `{ icon: X, alt: "close" }` | Button icon configuration |
138
546
 
139
- <Tag.Icon icon={addSharp} alt="create"/>
140
- ```
547
+ ### `<Tag.CopyButton>`
548
+
549
+ Copy functionality button with animation.
550
+
551
+ | Prop | Type | Default | Description |
552
+ | ------------ | ------------ | ------- | ------------------------- |
553
+ | `onClick` | `() => void` | - | Additional click handler |
554
+ | `textToCopy` | `string` | - | Text to copy to clipboard |
141
555
 
142
- #### Props
556
+ ## Examples
143
557
 
144
- > See [icon props](https://github.com/knocklabs/telegraph/tree/main/packages/icon)
558
+ ### Basic Example
145
559
 
146
- #### An example of composing an advanced Tag
560
+ ```tsx
561
+ import { Tag } from "@telegraph/tag";
147
562
 
563
+ export const ProductTags = () => (
564
+ <div className="product-tags">
565
+ <Tag color="green">In Stock</Tag>
566
+ <Tag color="blue">Free Shipping</Tag>
567
+ <Tag color="yellow">Limited Time</Tag>
568
+ <Tag color="purple">Premium</Tag>
569
+ </div>
570
+ );
148
571
  ```
149
- import { Tag } from "@telegraph/tag"
150
- import { Add, X } from "lucide-react"
151
572
 
152
- <Tag.Root color="blue" variant="solid" size="2">
153
- <Tag.Icon icon={Add} alt="Create"/>
154
- <Tag.Text>Text</Tag.Text>
155
- <Tag.Button icon={{ icon: X, alt: "remove"}} onClick={() => {}}/>
156
- </Tag.Root>
573
+ ### Advanced Example
574
+
575
+ ```tsx
576
+ import { Tag } from "@telegraph/tag";
577
+ import { Calendar, Copy, Star, User } from "lucide-react";
578
+ import { useState } from "react";
579
+
580
+ export const AdvancedTagExample = () => {
581
+ const [skills, setSkills] = useState([
582
+ { id: 1, name: "React", level: "expert", color: "blue" },
583
+ { id: 2, name: "TypeScript", level: "advanced", color: "blue" },
584
+ { id: 3, name: "Node.js", level: "intermediate", color: "green" },
585
+ ]);
586
+
587
+ const removeSkill = (id: number) => {
588
+ setSkills(skills.filter((skill) => skill.id !== id));
589
+ };
590
+
591
+ const copySkill = (skillName: string) => {
592
+ navigator.clipboard.writeText(skillName);
593
+ console.log(`Copied ${skillName} to clipboard`);
594
+ };
595
+
596
+ return (
597
+ <div className="skill-tags">
598
+ <h3>Technical Skills</h3>
599
+
600
+ <div className="tags-grid">
601
+ {skills.map((skill) => (
602
+ <Tag
603
+ key={skill.id}
604
+ color={skill.color}
605
+ variant={skill.level === "expert" ? "solid" : "soft"}
606
+ size="1"
607
+ icon={{
608
+ icon: skill.level === "expert" ? Star : User,
609
+ alt: skill.level,
610
+ }}
611
+ onRemove={() => removeSkill(skill.id)}
612
+ onCopy={() => copySkill(skill.name)}
613
+ textToCopy={skill.name}
614
+ >
615
+ {skill.name}
616
+ </Tag>
617
+ ))}
618
+ </div>
619
+
620
+ <div className="meta-tags">
621
+ <Tag icon={{ icon: Calendar, alt: "Updated" }} color="gray" size="0">
622
+ Updated Today
623
+ </Tag>
624
+
625
+ <Tag
626
+ icon={{ icon: Copy, alt: "Copy all" }}
627
+ color="accent"
628
+ size="0"
629
+ onCopy={() => {
630
+ const allSkills = skills.map((s) => s.name).join(", ");
631
+ navigator.clipboard.writeText(allSkills);
632
+ }}
633
+ textToCopy={skills.map((s) => s.name).join(", ")}
634
+ >
635
+ Copy All Skills
636
+ </Tag>
637
+ </div>
638
+ </div>
639
+ );
640
+ };
157
641
  ```
642
+
643
+ ### Real-world Example
644
+
645
+ ```tsx
646
+ import { Tag } from "@telegraph/tag";
647
+ import { Filter, Plus, X } from "lucide-react";
648
+ import { useEffect, useState } from "react";
649
+
650
+ export const TaskManagerTags = () => {
651
+ const [tasks, setTasks] = useState([
652
+ {
653
+ id: 1,
654
+ title: "Design Homepage",
655
+ tags: ["design", "frontend", "urgent"],
656
+ },
657
+ {
658
+ id: 2,
659
+ title: "API Integration",
660
+ tags: ["backend", "api"],
661
+ },
662
+ {
663
+ id: 3,
664
+ title: "User Testing",
665
+ tags: ["research", "ux"],
666
+ },
667
+ ]);
668
+
669
+ const [activeFilters, setActiveFilters] = useState([]);
670
+ const [allTags, setAllTags] = useState([]);
671
+
672
+ useEffect(() => {
673
+ const uniqueTags = [...new Set(tasks.flatMap((task) => task.tags))];
674
+ setAllTags(uniqueTags);
675
+ }, [tasks]);
676
+
677
+ const toggleFilter = (tag: string) => {
678
+ setActiveFilters((prev) =>
679
+ prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag],
680
+ );
681
+ };
682
+
683
+ const clearFilters = () => {
684
+ setActiveFilters([]);
685
+ };
686
+
687
+ const filteredTasks = tasks.filter(
688
+ (task) =>
689
+ activeFilters.length === 0 ||
690
+ activeFilters.some((filter) => task.tags.includes(filter)),
691
+ );
692
+
693
+ const getTagColor = (tag: string) => {
694
+ const colorMap = {
695
+ urgent: "red",
696
+ design: "purple",
697
+ frontend: "blue",
698
+ backend: "green",
699
+ api: "yellow",
700
+ research: "accent",
701
+ ux: "purple",
702
+ };
703
+ return colorMap[tag] || "default";
704
+ };
705
+
706
+ return (
707
+ <div className="task-manager">
708
+ <div className="filter-section">
709
+ <h3>
710
+ <Filter size={20} />
711
+ Filter by Tags
712
+ </h3>
713
+
714
+ <div className="available-filters">
715
+ {allTags.map((tag) => (
716
+ <Tag
717
+ key={tag}
718
+ color={getTagColor(tag)}
719
+ variant={activeFilters.includes(tag) ? "solid" : "soft"}
720
+ onClick={() => toggleFilter(tag)}
721
+ style={{ cursor: "pointer" }}
722
+ >
723
+ {tag}
724
+ </Tag>
725
+ ))}
726
+ </div>
727
+
728
+ {activeFilters.length > 0 && (
729
+ <div className="active-filters">
730
+ <span>Active filters:</span>
731
+ {activeFilters.map((filter) => (
732
+ <Tag
733
+ key={filter}
734
+ color={getTagColor(filter)}
735
+ onRemove={() => toggleFilter(filter)}
736
+ size="0"
737
+ >
738
+ {filter}
739
+ </Tag>
740
+ ))}
741
+ <button onClick={clearFilters} className="clear-all">
742
+ Clear All
743
+ </button>
744
+ </div>
745
+ )}
746
+ </div>
747
+
748
+ <div className="tasks-section">
749
+ <h3>Tasks ({filteredTasks.length})</h3>
750
+
751
+ {filteredTasks.map((task) => (
752
+ <div key={task.id} className="task-card">
753
+ <h4>{task.title}</h4>
754
+ <div className="task-tags">
755
+ {task.tags.map((tag) => (
756
+ <Tag
757
+ key={tag}
758
+ color={getTagColor(tag)}
759
+ size="0"
760
+ onClick={() => toggleFilter(tag)}
761
+ style={{ cursor: "pointer" }}
762
+ >
763
+ {tag}
764
+ </Tag>
765
+ ))}
766
+ </div>
767
+ </div>
768
+ ))}
769
+ </div>
770
+ </div>
771
+ );
772
+ };
773
+ ```
774
+
775
+ ## References
776
+
777
+ - [Storybook Demo](https://storybook.telegraph.dev/?path=/docs/tag)
778
+ - [Icon Component](../icon/README.md) - Used for tag icons
779
+ - [Button Component](../button/README.md) - Used for interactive buttons
780
+
781
+ ## Contributing
782
+
783
+ See our [Contributing Guide](../../CONTRIBUTING.md) for more details.
784
+
785
+ ## License
786
+
787
+ MIT License - see [LICENSE](../../LICENSE) for details.