@telegraph/truncate 0.0.8 → 0.0.10

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +719 -41
  3. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @telegraph/truncate
2
2
 
3
+ ## 0.0.10
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`c0244e3`](https://github.com/knocklabs/telegraph/commit/c0244e3f4b6232f633ba4d99bb0eb603909c87fa)]:
8
+ - @telegraph/tooltip@0.0.55
9
+
10
+ ## 0.0.9
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies []:
15
+ - @telegraph/typography@0.1.23
16
+ - @telegraph/tooltip@0.0.54
17
+
3
18
  ## 0.0.8
4
19
 
5
20
  ### Patch Changes
package/README.md CHANGED
@@ -1,85 +1,763 @@
1
+ # ✂️ Truncate
2
+
3
+ > Smart text truncation utilities with overflow detection, conditional tooltips, and responsive text handling for optimal user experience.
4
+
1
5
  ![Telegraph by Knock](https://github.com/knocklabs/telegraph/assets/29106675/9b5022e3-b02c-4582-ba57-3d6171e45e44)
2
6
 
3
7
  [![npm version](https://img.shields.io/npm/v/@telegraph/truncate.svg)](https://www.npmjs.com/package/@telegraph/truncate)
8
+ [![minzipped size](https://img.shields.io/bundlephobia/minzip/@telegraph/truncate)](https://bundlephobia.com/result?p=@telegraph/truncate)
9
+ [![license](https://img.shields.io/npm/l/@telegraph/truncate)](https://github.com/knocklabs/telegraph/blob/main/LICENSE)
4
10
 
5
- # @telegraph/truncate
11
+ ## Installation
6
12
 
7
- > Utility components for detecting and responding to content overflow, truncation, and visibility states in the UI.
13
+ ```bash
14
+ npm install @telegraph/truncate
15
+ ```
8
16
 
9
- ## Installation Instructions
17
+ > **Note**: This package has no stylesheets required.
10
18
 
11
- ```
12
- npm install @telegraph/truncate
19
+ ## Quick Start
20
+
21
+ ```tsx
22
+ import {
23
+ TooltipIfTruncated,
24
+ TruncatedText,
25
+ useTruncate,
26
+ } from "@telegraph/truncate";
27
+
28
+ export const TruncateExamples = () => (
29
+ <div>
30
+ {/* Simple truncated text with tooltip */}
31
+ <TruncatedText maxWidth="40">
32
+ This text will be truncated if it exceeds the container width
33
+ </TruncatedText>
34
+
35
+ {/* Custom content with conditional tooltip */}
36
+ <TooltipIfTruncated label="Full content shown in tooltip">
37
+ <span style={{ maxWidth: "200px", overflow: "hidden" }}>
38
+ This content might be truncated
39
+ </span>
40
+ </TooltipIfTruncated>
41
+
42
+ {/* Using the hook for custom behavior */}
43
+ <CustomTruncatedComponent />
44
+ </div>
45
+ );
46
+
47
+ const CustomTruncatedComponent = () => {
48
+ const ref = useRef<HTMLDivElement>(null);
49
+ const { truncated } = useTruncate({ tgphRef: ref });
50
+
51
+ return (
52
+ <div ref={ref} style={{ maxWidth: "150px", overflow: "hidden" }}>
53
+ Content status: {truncated ? "Truncated" : "Fits"}
54
+ </div>
55
+ );
56
+ };
13
57
  ```
14
58
 
15
- ## Components
59
+ ## API Reference
16
60
 
17
- ### `<TruncatedText/>`
61
+ ### `<TruncatedText>`
18
62
 
19
63
  A text component that automatically truncates content with an ellipsis and shows a tooltip when truncated.
20
64
 
65
+ | Prop | Type | Default | Description |
66
+ | -------------- | ---------------------------------- | ----------- | --------------------------------------------------------------- |
67
+ | `maxWidth` | `string` | `undefined` | Maximum width of the text container |
68
+ | `tooltipProps` | `Partial<TooltipIfTruncatedProps>` | `{}` | Props to pass to the underlying TooltipIfTruncated component |
69
+ | `...TextProps` | `TgphComponentProps<typeof Text>` | - | All props from [@telegraph/typography](../typography/README.md) |
70
+
71
+ ### `<TooltipIfTruncated>`
72
+
73
+ A component that conditionally shows a tooltip only when its content is truncated.
74
+
75
+ | Prop | Type | Default | Description |
76
+ | ----------------- | ------------------------------------ | ----------- | ----------------------------------------------------------------------------- |
77
+ | `label` | `string` | `undefined` | The text to show in the tooltip. If not provided, will use the content's text |
78
+ | `children` | `ReactNode` | - | **Required.** Content to monitor for truncation |
79
+ | `...TooltipProps` | `TgphComponentProps<typeof Tooltip>` | - | All props from [@telegraph/tooltip](../tooltip/README.md) |
80
+
81
+ ### `useTruncate`
82
+
83
+ A hook that detects whether an element's content is truncated.
84
+
85
+ #### Parameters
86
+
87
+ | Name | Type | Description |
88
+ | -------- | ------------------------------------------- | --------------------------------------------------------------------- |
89
+ | `params` | `{ tgphRef: React.RefObject<HTMLElement> }` | A ref to the element to check for truncation |
90
+ | `deps` | `React.DependencyList` | Optional dependencies to re-run the truncation check when they change |
91
+
92
+ #### Returns
93
+
94
+ | Name | Type | Description |
95
+ | ----------- | --------- | ------------------------------------------ |
96
+ | `truncated` | `boolean` | Whether the element's content is truncated |
97
+
98
+ ## Usage Patterns
99
+
100
+ ### Basic Text Truncation
101
+
21
102
  ```tsx
22
103
  import { TruncatedText } from "@telegraph/truncate";
23
104
 
24
- <TruncatedText maxWidth="40">
25
- This text will be truncated if it exceeds the container width
26
- </TruncatedText>;
105
+ export const UserName = ({ name }: { name: string }) => (
106
+ <TruncatedText maxWidth="40">{name}</TruncatedText>
107
+ );
27
108
  ```
28
109
 
29
- #### Props
110
+ ### Custom Container Width
30
111
 
31
- | Name | Type | Default | Description |
32
- | ------------ | ------------------------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------ |
33
- | tooltipProps | Partial<TgphComponentProps<typeof TooltipIfTruncated>> | `{}` | Props to pass to the underlying TooltipIfTruncated component |
34
- | ...TextProps | TgphComponentProps<typeof Text> | - | All props from [@telegraph/typography](https://github.com/knocklabs/telegraph/tree/main/packages/typography) |
112
+ ```tsx
113
+ import { TruncatedText } from "@telegraph/truncate";
35
114
 
36
- ### `<TooltipIfTruncated/>`
115
+ export const ResponsiveTruncation = () => (
116
+ <div>
117
+ <TruncatedText maxWidth="20">Short container</TruncatedText>
118
+ <TruncatedText maxWidth="60">Longer container</TruncatedText>
119
+ <TruncatedText maxWidth="100%">Full width container</TruncatedText>
120
+ </div>
121
+ );
122
+ ```
37
123
 
38
- A component that conditionally shows a tooltip only when its content is truncated.
124
+ ### Conditional Tooltip
39
125
 
40
126
  ```tsx
41
127
  import { TooltipIfTruncated } from "@telegraph/truncate";
42
128
 
43
- <TooltipIfTruncated>
44
- <span>This text will show a tooltip only when truncated</span>
45
- </TooltipIfTruncated>;
129
+ export const FileNameDisplay = ({ fileName }: { fileName: string }) => (
130
+ <TooltipIfTruncated label={fileName}>
131
+ <div
132
+ className="file-name"
133
+ style={{ maxWidth: "200px", overflow: "hidden" }}
134
+ >
135
+ {fileName}
136
+ </div>
137
+ </TooltipIfTruncated>
138
+ );
46
139
  ```
47
140
 
48
- #### Props
141
+ ### Custom Truncation Detection
49
142
 
50
- | Name | Type | Default | Description |
51
- | --------------- | ---------------------------------- | ------- | ------------------------------------------------------------------------------------------------------ |
52
- | label | string | - | The text to show in the tooltip. If not provided, will use the content's text |
53
- | ...TooltipProps | TgphComponentProps<typeof Tooltip> | - | All props from [@telegraph/tooltip](https://github.com/knocklabs/telegraph/tree/main/packages/tooltip) |
143
+ ```tsx
144
+ import { useTruncate } from "@telegraph/truncate";
145
+ import { useRef, useState } from "react";
54
146
 
55
- ## Hooks
147
+ export const DynamicContent = ({ content }: { content: string }) => {
148
+ const ref = useRef<HTMLDivElement>(null);
149
+ const [expanded, setExpanded] = useState(false);
150
+ const { truncated } = useTruncate({ tgphRef: ref }, [content, expanded]);
56
151
 
57
- ### `useTruncate`
152
+ return (
153
+ <div>
154
+ <div
155
+ ref={ref}
156
+ style={{
157
+ maxWidth: "300px",
158
+ overflow: "hidden",
159
+ whiteSpace: expanded ? "normal" : "nowrap",
160
+ }}
161
+ >
162
+ {content}
163
+ </div>
58
164
 
59
- A hook that detects whether an element's content is truncated.
165
+ {truncated && !expanded && (
166
+ <button onClick={() => setExpanded(true)}>Show more</button>
167
+ )}
168
+
169
+ {expanded && (
170
+ <button onClick={() => setExpanded(false)}>Show less</button>
171
+ )}
172
+ </div>
173
+ );
174
+ };
175
+ ```
176
+
177
+ ### Multi-line Truncation
178
+
179
+ ```tsx
180
+ import { TooltipIfTruncated } from "@telegraph/truncate";
181
+
182
+ export const ArticlePreview = ({
183
+ title,
184
+ description,
185
+ }: {
186
+ title: string;
187
+ description: string;
188
+ }) => (
189
+ <div className="article-preview">
190
+ <TooltipIfTruncated label={title}>
191
+ <h3
192
+ style={{
193
+ maxWidth: "100%",
194
+ overflow: "hidden",
195
+ whiteSpace: "nowrap",
196
+ textOverflow: "ellipsis",
197
+ }}
198
+ >
199
+ {title}
200
+ </h3>
201
+ </TooltipIfTruncated>
202
+
203
+ <TooltipIfTruncated label={description}>
204
+ <p
205
+ style={{
206
+ display: "-webkit-box",
207
+ WebkitLineClamp: 3,
208
+ WebkitBoxOrient: "vertical",
209
+ overflow: "hidden",
210
+ }}
211
+ >
212
+ {description}
213
+ </p>
214
+ </TooltipIfTruncated>
215
+ </div>
216
+ );
217
+ ```
218
+
219
+ ## Advanced Usage
220
+
221
+ ### Responsive Truncation
222
+
223
+ ```tsx
224
+ import { useMediaQuery } from "@telegraph/helpers";
225
+ import { TruncatedText } from "@telegraph/truncate";
226
+
227
+ export const ResponsiveTruncatedText = ({ children }: { children: string }) => {
228
+ const isMobile = useMediaQuery("(max-width: 768px)");
229
+ const isTablet = useMediaQuery("(max-width: 1024px)");
230
+
231
+ const maxWidth = isMobile ? "30" : isTablet ? "50" : "80";
232
+
233
+ return <TruncatedText maxWidth={maxWidth}>{children}</TruncatedText>;
234
+ };
235
+ ```
236
+
237
+ ### Dynamic Content Monitoring
60
238
 
61
239
  ```tsx
62
240
  import { useTruncate } from "@telegraph/truncate";
241
+ import { useEffect, useRef, useState } from "react";
63
242
 
64
- const MyComponent = () => {
65
- const ref = React.useRef<HTMLDivElement>(null);
66
- const { truncated } = useTruncate({ tgphRef: ref });
243
+ export const DynamicTruncationMonitor = ({ items }: { items: string[] }) => {
244
+ const ref = useRef<HTMLDivElement>(null);
245
+ const [visibleItems, setVisibleItems] = useState(items);
246
+ const { truncated } = useTruncate({ tgphRef: ref }, [visibleItems]);
247
+
248
+ useEffect(() => {
249
+ if (truncated && visibleItems.length > 1) {
250
+ // Remove items until content fits
251
+ setVisibleItems((prev) => prev.slice(0, -1));
252
+ }
253
+ }, [truncated, visibleItems.length]);
254
+
255
+ const hiddenCount = items.length - visibleItems.length;
67
256
 
68
257
  return (
69
- <div ref={ref}>{truncated ? "Content is truncated" : "Content fits"}</div>
258
+ <div
259
+ ref={ref}
260
+ style={{
261
+ maxWidth: "300px",
262
+ overflow: "hidden",
263
+ whiteSpace: "nowrap",
264
+ }}
265
+ >
266
+ {visibleItems.join(", ")}
267
+ {hiddenCount > 0 && ` +${hiddenCount} more`}
268
+ </div>
70
269
  );
71
270
  };
72
271
  ```
73
272
 
74
- #### Parameters
273
+ ### Table Cell Truncation
75
274
 
76
- | Name | Type | Description |
77
- | ------ | ------------------------------------------- | --------------------------------------------------------------------- |
78
- | params | `{ tgphRef: React.RefObject<HTMLElement> }` | A ref to the element to check for truncation |
79
- | deps | `React.DependencyList` | Optional dependencies to re-run the truncation check when they change |
275
+ ```tsx
276
+ import { TooltipIfTruncated } from "@telegraph/truncate";
80
277
 
81
- #### Returns
278
+ export const DataTable = ({
279
+ rows,
280
+ }: {
281
+ rows: Array<{ id: string; name: string; email: string }>;
282
+ }) => (
283
+ <table className="data-table">
284
+ <thead>
285
+ <tr>
286
+ <th>Name</th>
287
+ <th>Email</th>
288
+ </tr>
289
+ </thead>
290
+ <tbody>
291
+ {rows.map((row) => (
292
+ <tr key={row.id}>
293
+ <td style={{ maxWidth: "150px" }}>
294
+ <TooltipIfTruncated label={row.name}>
295
+ <div style={{ overflow: "hidden", textOverflow: "ellipsis" }}>
296
+ {row.name}
297
+ </div>
298
+ </TooltipIfTruncated>
299
+ </td>
300
+ <td style={{ maxWidth: "200px" }}>
301
+ <TooltipIfTruncated label={row.email}>
302
+ <div style={{ overflow: "hidden", textOverflow: "ellipsis" }}>
303
+ {row.email}
304
+ </div>
305
+ </TooltipIfTruncated>
306
+ </td>
307
+ </tr>
308
+ ))}
309
+ </tbody>
310
+ </table>
311
+ );
312
+ ```
313
+
314
+ ### Breadcrumb Truncation
315
+
316
+ ```tsx
317
+ import { TooltipIfTruncated, useTruncate } from "@telegraph/truncate";
318
+ import { useRef, useState } from "react";
319
+
320
+ export const SmartBreadcrumbs = ({
321
+ items,
322
+ }: {
323
+ items: Array<{ label: string; href: string }>;
324
+ }) => {
325
+ const ref = useRef<HTMLDivElement>(null);
326
+ const [showAll, setShowAll] = useState(false);
327
+ const { truncated } = useTruncate({ tgphRef: ref }, [items, showAll]);
328
+
329
+ const displayItems = showAll
330
+ ? items
331
+ : truncated && items.length > 3
332
+ ? [items[0], { label: "...", href: "#" }, ...items.slice(-2)]
333
+ : items;
334
+
335
+ return (
336
+ <nav className="breadcrumbs">
337
+ <div
338
+ ref={ref}
339
+ style={{
340
+ display: "flex",
341
+ overflow: "hidden",
342
+ maxWidth: "100%",
343
+ }}
344
+ >
345
+ {displayItems.map((item, index) => (
346
+ <span key={index} className="breadcrumb-item">
347
+ {item.label === "..." ? (
348
+ <button
349
+ onClick={() => setShowAll(true)}
350
+ className="breadcrumb-expand"
351
+ >
352
+ ...
353
+ </button>
354
+ ) : (
355
+ <TooltipIfTruncated label={item.label}>
356
+ <a
357
+ href={item.href}
358
+ style={{
359
+ maxWidth: "100px",
360
+ overflow: "hidden",
361
+ textOverflow: "ellipsis",
362
+ whiteSpace: "nowrap",
363
+ }}
364
+ >
365
+ {item.label}
366
+ </a>
367
+ </TooltipIfTruncated>
368
+ )}
369
+ {index < displayItems.length - 1 && (
370
+ <span className="separator"> / </span>
371
+ )}
372
+ </span>
373
+ ))}
374
+ </div>
375
+ </nav>
376
+ );
377
+ };
378
+ ```
379
+
380
+ ### Tag List with Overflow
381
+
382
+ ```tsx
383
+ import { TooltipIfTruncated, useTruncate } from "@telegraph/truncate";
384
+ import { useRef, useState } from "react";
385
+
386
+ export const TagList = ({ tags }: { tags: string[] }) => {
387
+ const ref = useRef<HTMLDivElement>(null);
388
+ const [showAll, setShowAll] = useState(false);
389
+ const { truncated } = useTruncate({ tgphRef: ref }, [tags, showAll]);
390
+
391
+ const displayTags = showAll ? tags : tags.slice(0, 3);
392
+ const hiddenCount = tags.length - displayTags.length;
393
+
394
+ return (
395
+ <div className="tag-list">
396
+ <div
397
+ ref={ref}
398
+ style={{
399
+ display: "flex",
400
+ gap: "8px",
401
+ flexWrap: showAll ? "wrap" : "nowrap",
402
+ overflow: "hidden",
403
+ }}
404
+ >
405
+ {displayTags.map((tag, index) => (
406
+ <TooltipIfTruncated key={index} label={tag}>
407
+ <span
408
+ className="tag"
409
+ style={{
410
+ maxWidth: "100px",
411
+ overflow: "hidden",
412
+ textOverflow: "ellipsis",
413
+ whiteSpace: "nowrap",
414
+ }}
415
+ >
416
+ {tag}
417
+ </span>
418
+ </TooltipIfTruncated>
419
+ ))}
420
+
421
+ {!showAll && hiddenCount > 0 && (
422
+ <button onClick={() => setShowAll(true)} className="tag-more">
423
+ +{hiddenCount}
424
+ </button>
425
+ )}
426
+ </div>
427
+ </div>
428
+ );
429
+ };
430
+ ```
431
+
432
+ ### Form Field Labels
433
+
434
+ ```tsx
435
+ import { Input } from "@telegraph/input";
436
+ import { TruncatedText } from "@telegraph/truncate";
437
+
438
+ export const FormField = ({
439
+ label,
440
+ id,
441
+ ...inputProps
442
+ }: {
443
+ label: string;
444
+ id: string;
445
+ } & React.InputHTMLAttributes<HTMLInputElement>) => (
446
+ <div className="form-field">
447
+ <label htmlFor={id}>
448
+ <TruncatedText maxWidth="100%">{label}</TruncatedText>
449
+ </label>
450
+ <Input id={id} {...inputProps} />
451
+ </div>
452
+ );
453
+ ```
454
+
455
+ ### Card Title Truncation
456
+
457
+ ```tsx
458
+ import { TooltipIfTruncated, TruncatedText } from "@telegraph/truncate";
459
+
460
+ export const ProductCard = ({
461
+ product,
462
+ }: {
463
+ product: {
464
+ name: string;
465
+ description: string;
466
+ price: string;
467
+ };
468
+ }) => (
469
+ <div className="product-card">
470
+ <TruncatedText
471
+ maxWidth="100%"
472
+ size="3"
473
+ weight="semibold"
474
+ tooltipProps={{ side: "top" }}
475
+ >
476
+ {product.name}
477
+ </TruncatedText>
478
+
479
+ <TooltipIfTruncated label={product.description}>
480
+ <p
481
+ className="product-description"
482
+ style={{
483
+ display: "-webkit-box",
484
+ WebkitLineClamp: 2,
485
+ WebkitBoxOrient: "vertical",
486
+ overflow: "hidden",
487
+ lineHeight: "1.4em",
488
+ maxHeight: "2.8em",
489
+ }}
490
+ >
491
+ {product.description}
492
+ </p>
493
+ </TooltipIfTruncated>
494
+
495
+ <div className="product-price">{product.price}</div>
496
+ </div>
497
+ );
498
+ ```
499
+
500
+ ## Accessibility
501
+
502
+ - ✅ **Screen Reader Support**: Full content available via tooltips when truncated
503
+ - ✅ **Keyboard Navigation**: Tooltips work with keyboard navigation
504
+ - ✅ **Focus Management**: Proper focus indicators for interactive elements
505
+ - ✅ **High Contrast**: Compatible with high contrast modes
506
+ - ✅ **Content Preservation**: No content is permanently hidden
507
+
508
+ ### Best Practices
509
+
510
+ 1. **Provide Full Content**: Always make complete content accessible via tooltips
511
+ 2. **Logical Truncation**: Truncate at word boundaries when possible
512
+ 3. **Clear Indicators**: Make it obvious when content is truncated
513
+ 4. **Responsive Design**: Adjust truncation based on available space
514
+ 5. **Keyboard Access**: Ensure truncated content is accessible via keyboard
515
+
516
+ ### ARIA Considerations
517
+
518
+ - Tooltips include proper ARIA attributes when content is truncated
519
+ - Screen readers can access full content through tooltip mechanisms
520
+ - Focus management works correctly with truncated interactive elements
521
+
522
+ ## Examples
523
+
524
+ ### Basic Example
525
+
526
+ ```tsx
527
+ import { TruncatedText } from "@telegraph/truncate";
528
+
529
+ export const UserProfile = ({
530
+ user,
531
+ }: {
532
+ user: { name: string; bio: string };
533
+ }) => (
534
+ <div className="user-profile">
535
+ <TruncatedText maxWidth="40" size="4" weight="semibold">
536
+ {user.name}
537
+ </TruncatedText>
538
+ <TruncatedText maxWidth="60" color="gray">
539
+ {user.bio}
540
+ </TruncatedText>
541
+ </div>
542
+ );
543
+ ```
544
+
545
+ ### Advanced Example
546
+
547
+ ```tsx
548
+ import { TooltipIfTruncated, useTruncate } from "@telegraph/truncate";
549
+ import { useRef, useState } from "react";
550
+
551
+ export const SmartProductList = ({
552
+ products,
553
+ }: {
554
+ products: Array<{
555
+ id: string;
556
+ name: string;
557
+ description: string;
558
+ tags: string[];
559
+ }>;
560
+ }) => {
561
+ return (
562
+ <div className="product-list">
563
+ {products.map((product) => (
564
+ <ProductListItem key={product.id} product={product} />
565
+ ))}
566
+ </div>
567
+ );
568
+ };
569
+
570
+ const ProductListItem = ({ product }: { product: any }) => {
571
+ const [expanded, setExpanded] = useState(false);
572
+ const descriptionRef = useRef<HTMLParagraphElement>(null);
573
+ const { truncated } = useTruncate({ tgphRef: descriptionRef }, [expanded]);
574
+
575
+ return (
576
+ <div className="product-item">
577
+ <TooltipIfTruncated label={product.name}>
578
+ <h3
579
+ className="product-name"
580
+ style={{
581
+ maxWidth: "250px",
582
+ overflow: "hidden",
583
+ textOverflow: "ellipsis",
584
+ whiteSpace: "nowrap",
585
+ }}
586
+ >
587
+ {product.name}
588
+ </h3>
589
+ </TooltipIfTruncated>
590
+
591
+ <div className="product-description-container">
592
+ <p
593
+ ref={descriptionRef}
594
+ className="product-description"
595
+ style={{
596
+ display: "-webkit-box",
597
+ WebkitLineClamp: expanded ? "none" : 2,
598
+ WebkitBoxOrient: "vertical",
599
+ overflow: "hidden",
600
+ lineHeight: "1.4em",
601
+ }}
602
+ >
603
+ {product.description}
604
+ </p>
605
+
606
+ {truncated && (
607
+ <button
608
+ onClick={() => setExpanded(!expanded)}
609
+ className="expand-button"
610
+ >
611
+ {expanded ? "Show less" : "Show more"}
612
+ </button>
613
+ )}
614
+ </div>
615
+
616
+ <div className="product-tags">
617
+ <SmartTagList tags={product.tags} maxVisible={3} />
618
+ </div>
619
+ </div>
620
+ );
621
+ };
622
+
623
+ const SmartTagList = ({
624
+ tags,
625
+ maxVisible = 3,
626
+ }: {
627
+ tags: string[];
628
+ maxVisible?: number;
629
+ }) => {
630
+ const [showAll, setShowAll] = useState(false);
631
+ const visibleTags = showAll ? tags : tags.slice(0, maxVisible);
632
+ const hiddenCount = tags.length - visibleTags.length;
633
+
634
+ return (
635
+ <div className="tag-list">
636
+ {visibleTags.map((tag, index) => (
637
+ <TooltipIfTruncated key={index} label={tag}>
638
+ <span
639
+ className="tag"
640
+ style={{
641
+ maxWidth: "80px",
642
+ overflow: "hidden",
643
+ textOverflow: "ellipsis",
644
+ whiteSpace: "nowrap",
645
+ }}
646
+ >
647
+ {tag}
648
+ </span>
649
+ </TooltipIfTruncated>
650
+ ))}
651
+
652
+ {!showAll && hiddenCount > 0 && (
653
+ <button onClick={() => setShowAll(true)} className="show-more-tags">
654
+ +{hiddenCount} more
655
+ </button>
656
+ )}
657
+ </div>
658
+ );
659
+ };
660
+ ```
661
+
662
+ ### Real-world Example
663
+
664
+ ```tsx
665
+ import {
666
+ TooltipIfTruncated,
667
+ TruncatedText,
668
+ useTruncate,
669
+ } from "@telegraph/truncate";
670
+ import { useRef, useState } from "react";
671
+
672
+ export const EmailInbox = ({
673
+ emails,
674
+ }: {
675
+ emails: Array<{
676
+ id: string;
677
+ from: string;
678
+ subject: string;
679
+ preview: string;
680
+ timestamp: string;
681
+ isRead: boolean;
682
+ }>;
683
+ }) => {
684
+ return (
685
+ <div className="email-inbox">
686
+ {emails.map((email) => (
687
+ <EmailRow key={email.id} email={email} />
688
+ ))}
689
+ </div>
690
+ );
691
+ };
692
+
693
+ const EmailRow = ({ email }: { email: any }) => {
694
+ const [isExpanded, setIsExpanded] = useState(false);
695
+ const previewRef = useRef<HTMLDivElement>(null);
696
+ const { truncated } = useTruncate({ tgphRef: previewRef }, [isExpanded]);
697
+
698
+ return (
699
+ <div
700
+ className={`email-row ${!email.isRead ? "unread" : ""}`}
701
+ onClick={() => setIsExpanded(!isExpanded)}
702
+ >
703
+ <div className="email-header">
704
+ <div className="email-from">
705
+ <TruncatedText
706
+ maxWidth="30"
707
+ weight={email.isRead ? "regular" : "semibold"}
708
+ >
709
+ {email.from}
710
+ </TruncatedText>
711
+ </div>
712
+
713
+ <div className="email-subject">
714
+ <TruncatedText
715
+ maxWidth="50"
716
+ weight={email.isRead ? "regular" : "medium"}
717
+ >
718
+ {email.subject}
719
+ </TruncatedText>
720
+ </div>
721
+
722
+ <div className="email-timestamp">{email.timestamp}</div>
723
+ </div>
724
+
725
+ <div className="email-preview-container">
726
+ <div
727
+ ref={previewRef}
728
+ className="email-preview"
729
+ style={{
730
+ display: "-webkit-box",
731
+ WebkitLineClamp: isExpanded ? "none" : 1,
732
+ WebkitBoxOrient: "vertical",
733
+ overflow: "hidden",
734
+ color: email.isRead ? "#666" : "#333",
735
+ }}
736
+ >
737
+ {email.preview}
738
+ </div>
739
+
740
+ {truncated && !isExpanded && (
741
+ <TooltipIfTruncated label={email.preview}>
742
+ <button className="expand-preview">...</button>
743
+ </TooltipIfTruncated>
744
+ )}
745
+ </div>
746
+ </div>
747
+ );
748
+ };
749
+ ```
750
+
751
+ ## References
752
+
753
+ - [Storybook Demo](https://storybook.telegraph.dev/?path=/docs/truncate)
754
+ - [Typography Component](../typography/README.md) - Used by TruncatedText
755
+ - [Tooltip Component](../tooltip/README.md) - Used for showing full content
756
+
757
+ ## Contributing
758
+
759
+ See our [Contributing Guide](../../CONTRIBUTING.md) for more details.
760
+
761
+ ## License
82
762
 
83
- | Name | Type | Description |
84
- | --------- | --------- | ------------------------------------------ |
85
- | truncated | `boolean` | Whether the element's content is truncated |
763
+ MIT License - see [LICENSE](../../LICENSE) for details.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telegraph/truncate",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Utility components for detecting and responding to content overflow, truncation, and visibility states in the UI.",
5
5
  "repository": "https://github.com/knocklabs/telegraph/tree/main/packages/truncate",
6
6
  "author": "@knocklabs",
@@ -33,13 +33,13 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@telegraph/helpers": "^0.0.13",
36
- "@telegraph/tooltip": "^0.0.53",
37
- "@telegraph/typography": "^0.1.22"
36
+ "@telegraph/tooltip": "^0.0.55",
37
+ "@telegraph/typography": "^0.1.23"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@knocklabs/eslint-config": "^0.0.4",
41
41
  "@knocklabs/typescript-config": "^0.0.2",
42
- "@telegraph/postcss-config": "^0.0.28",
42
+ "@telegraph/postcss-config": "^0.0.29",
43
43
  "@telegraph/prettier-config": "^0.0.7",
44
44
  "@telegraph/vite-config": "^0.0.15",
45
45
  "@types/react": "^18.3.18",