@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/CHANGELOG.md +19 -0
- package/README.md +711 -81
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.mjs +1618 -1606
- package/dist/esm/index.mjs.map +1 -1
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
# 🏷️ Tag
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Flexible tag component with optional interactive features like removal and copying, supporting multiple variants and colors.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/@telegraph/tag)
|
|
8
|
+
[](https://bundlephobia.com/result?p=@telegraph/tag)
|
|
9
|
+
[](https://github.com/knocklabs/telegraph/blob/main/LICENSE)
|
|
8
10
|
|
|
9
|
-
## Installation
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
+
For detailed props of each component, see the [Complete Component Reference](#complete-component-reference) section below.
|
|
48
119
|
|
|
49
|
-
|
|
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
|
-
###
|
|
122
|
+
### Basic Tags
|
|
59
123
|
|
|
60
|
-
|
|
124
|
+
```tsx
|
|
125
|
+
import { Tag } from "@telegraph/tag";
|
|
61
126
|
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
483
|
+
## Accessibility
|
|
97
484
|
|
|
98
|
-
|
|
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
|
-
|
|
491
|
+
### Best Practices
|
|
101
492
|
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
506
|
+
## Complete Component Reference
|
|
111
507
|
|
|
112
|
-
|
|
508
|
+
### `<Tag.Root>`
|
|
113
509
|
|
|
114
|
-
|
|
510
|
+
The container component that provides context for all child components.
|
|
115
511
|
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
529
|
+
### `<Tag.Icon>`
|
|
127
530
|
|
|
128
|
-
|
|
531
|
+
Icon component with contextual styling.
|
|
129
532
|
|
|
130
|
-
|
|
533
|
+
| Prop | Type | Default | Description |
|
|
534
|
+
| ------ | ------------ | ------- | -------------------------------------- |
|
|
535
|
+
| `icon` | `LucideIcon` | - | **Required.** Icon component to render |
|
|
536
|
+
| `alt` | `string` | - | **Required.** Alternative text |
|
|
131
537
|
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
556
|
+
## Examples
|
|
143
557
|
|
|
144
|
-
|
|
558
|
+
### Basic Example
|
|
145
559
|
|
|
146
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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.
|