@handled-ai/design-system 0.20.33 → 0.21.0
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/dist/components/data-table.js +1 -15
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/linked-entity-cell.d.ts +16 -1
- package/dist/components/linked-entity-cell.js +33 -3
- package/dist/components/linked-entity-cell.js.map +1 -1
- package/dist/components/owner-chips.js +1 -1
- package/dist/components/owner-chips.js.map +1 -1
- package/dist/components/virtualized-data-table.js +7 -11
- package/dist/components/virtualized-data-table.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/entity-color.d.ts +3 -0
- package/dist/lib/entity-color.js +19 -0
- package/dist/lib/entity-color.js.map +1 -0
- package/package.json +1 -1
- package/src/components/__tests__/linked-entity-cell.test.tsx +143 -0
- package/src/components/__tests__/owner-chips.test.tsx +0 -10
- package/src/components/__tests__/virtualized-data-table.test.tsx +124 -0
- package/src/components/data-table.tsx +1 -17
- package/src/components/linked-entity-cell.tsx +44 -2
- package/src/components/owner-chips.tsx +1 -1
- package/src/components/virtualized-data-table.tsx +15 -14
- package/src/index.ts +1 -0
- package/src/lib/entity-color.ts +22 -0
|
@@ -4,6 +4,7 @@ import * as React from "react"
|
|
|
4
4
|
import { ExternalLink } from "lucide-react"
|
|
5
5
|
|
|
6
6
|
import { cn } from "../lib/utils"
|
|
7
|
+
import { getEntityColor } from "../lib/entity-color"
|
|
7
8
|
|
|
8
9
|
export interface LinkedEntityCellProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
9
10
|
name: React.ReactNode
|
|
@@ -13,6 +14,21 @@ export interface LinkedEntityCellProps extends React.HTMLAttributes<HTMLDivEleme
|
|
|
13
14
|
icon?: React.ReactNode
|
|
14
15
|
external?: boolean
|
|
15
16
|
onNavigate?: () => void
|
|
17
|
+
/**
|
|
18
|
+
* When provided, renders a small colored rounded initial badge before the
|
|
19
|
+
* name. The full string is used as the color seed; the displayed letter is
|
|
20
|
+
* the first character. Omit to render no badge.
|
|
21
|
+
*/
|
|
22
|
+
avatarLabel?: string
|
|
23
|
+
/**
|
|
24
|
+
* Optional node rendered after the name (a trailing slot), e.g. a separate
|
|
25
|
+
* action link. Does not affect the name's own `href`.
|
|
26
|
+
*/
|
|
27
|
+
trailingAction?: React.ReactNode
|
|
28
|
+
/**
|
|
29
|
+
* Escape-hatch class merged onto the name span/link. Defaults to `text-sm`.
|
|
30
|
+
*/
|
|
31
|
+
nameClassName?: string
|
|
16
32
|
}
|
|
17
33
|
|
|
18
34
|
export function LinkedEntityCell({
|
|
@@ -23,6 +39,9 @@ export function LinkedEntityCell({
|
|
|
23
39
|
icon,
|
|
24
40
|
external = false,
|
|
25
41
|
onNavigate,
|
|
42
|
+
avatarLabel,
|
|
43
|
+
trailingAction,
|
|
44
|
+
nameClassName,
|
|
26
45
|
className,
|
|
27
46
|
...props
|
|
28
47
|
}: LinkedEntityCellProps) {
|
|
@@ -39,6 +58,18 @@ export function LinkedEntityCell({
|
|
|
39
58
|
className={cn("flex min-w-0 items-center gap-2", className)}
|
|
40
59
|
{...props}
|
|
41
60
|
>
|
|
61
|
+
{avatarLabel ? (
|
|
62
|
+
<span
|
|
63
|
+
data-slot="linked-entity-cell-avatar"
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
className={cn(
|
|
66
|
+
"flex h-6 w-6 shrink-0 items-center justify-center rounded text-[10px] font-bold",
|
|
67
|
+
getEntityColor(avatarLabel),
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
{avatarLabel.slice(0, 1)}
|
|
71
|
+
</span>
|
|
72
|
+
) : null}
|
|
42
73
|
{icon ? (
|
|
43
74
|
<span data-slot="linked-entity-cell-icon" className="shrink-0 text-muted-foreground">
|
|
44
75
|
{icon}
|
|
@@ -52,12 +83,18 @@ export function LinkedEntityCell({
|
|
|
52
83
|
target={external ? "_blank" : undefined}
|
|
53
84
|
rel={external ? "noreferrer" : undefined}
|
|
54
85
|
onClick={onNavigate}
|
|
55
|
-
className=
|
|
86
|
+
className={cn(
|
|
87
|
+
"inline-flex max-w-full items-center gap-1 truncate text-sm font-medium text-foreground underline-offset-4 hover:text-primary hover:underline",
|
|
88
|
+
nameClassName,
|
|
89
|
+
)}
|
|
56
90
|
>
|
|
57
91
|
{content}
|
|
58
92
|
</a>
|
|
59
93
|
) : (
|
|
60
|
-
<span
|
|
94
|
+
<span
|
|
95
|
+
data-slot="linked-entity-cell-name"
|
|
96
|
+
className={cn("block truncate text-sm font-medium text-foreground", nameClassName)}
|
|
97
|
+
>
|
|
61
98
|
{name}
|
|
62
99
|
</span>
|
|
63
100
|
)}
|
|
@@ -69,6 +106,11 @@ export function LinkedEntityCell({
|
|
|
69
106
|
</div>
|
|
70
107
|
) : null}
|
|
71
108
|
</div>
|
|
109
|
+
{trailingAction ? (
|
|
110
|
+
<span data-slot="linked-entity-cell-trailing" className="shrink-0">
|
|
111
|
+
{trailingAction}
|
|
112
|
+
</span>
|
|
113
|
+
) : null}
|
|
72
114
|
</div>
|
|
73
115
|
)
|
|
74
116
|
}
|
|
@@ -73,7 +73,7 @@ function SalesforceMark({ size = 13 }: { size?: number }) {
|
|
|
73
73
|
|
|
74
74
|
function OwnerAvatar({ person, size = "sm" }: { person: OwnerPerson; size?: "sm" | "default" }) {
|
|
75
75
|
return (
|
|
76
|
-
<Avatar size={size} className="ring-background ring-
|
|
76
|
+
<Avatar size={size} className="ring-background ring-2">
|
|
77
77
|
{person.avatarUrl ? <AvatarImage src={person.avatarUrl} alt={person.name} /> : null}
|
|
78
78
|
<AvatarFallback className="bg-muted text-muted-foreground text-[10px] font-medium uppercase">
|
|
79
79
|
{getInitials({ name: person.name, email: person.email })}
|
|
@@ -270,6 +270,13 @@ export function VirtualizedDataTable<TData>({
|
|
|
270
270
|
onColumnSort!(sortKey!, newDir)
|
|
271
271
|
} : undefined
|
|
272
272
|
|
|
273
|
+
const headerDef = header.column.columnDef.header
|
|
274
|
+
// When the header is a plain string, expose it as a native title
|
|
275
|
+
// tooltip so truncated headers remain readable. Non-string
|
|
276
|
+
// ReactNode headers render no title.
|
|
277
|
+
const headerTitle =
|
|
278
|
+
typeof headerDef === "string" ? headerDef : undefined
|
|
279
|
+
|
|
273
280
|
return (
|
|
274
281
|
<div
|
|
275
282
|
key={header.id}
|
|
@@ -290,25 +297,22 @@ export function VirtualizedDataTable<TData>({
|
|
|
290
297
|
{canServerSort ? (
|
|
291
298
|
<button
|
|
292
299
|
type="button"
|
|
293
|
-
className="flex min-w-0 flex-1 items-center gap-1 hover:text-foreground transition-colors"
|
|
300
|
+
className="flex min-w-0 flex-1 items-center gap-1 text-xs leading-4 hover:text-foreground transition-colors"
|
|
294
301
|
onClick={handleHeaderClick}
|
|
295
302
|
>
|
|
296
|
-
<span className="min-w-0 truncate">
|
|
297
|
-
{flexRender(
|
|
303
|
+
<span className="min-w-0 truncate text-xs leading-4" title={headerTitle}>
|
|
304
|
+
{flexRender(headerDef, header.getContext())}
|
|
298
305
|
</span>
|
|
299
306
|
{sortIcon}
|
|
300
307
|
</button>
|
|
301
308
|
) : header.column.getCanSort() ? (
|
|
302
309
|
<button
|
|
303
310
|
type="button"
|
|
304
|
-
className="flex min-w-0 flex-1 items-center gap-1 hover:text-foreground transition-colors"
|
|
311
|
+
className="flex min-w-0 flex-1 items-center gap-1 text-xs leading-4 hover:text-foreground transition-colors"
|
|
305
312
|
onClick={header.column.getToggleSortingHandler()}
|
|
306
313
|
>
|
|
307
|
-
<span className="min-w-0 truncate">
|
|
308
|
-
{flexRender(
|
|
309
|
-
header.column.columnDef.header,
|
|
310
|
-
header.getContext(),
|
|
311
|
-
)}
|
|
314
|
+
<span className="min-w-0 truncate text-xs leading-4" title={headerTitle}>
|
|
315
|
+
{flexRender(headerDef, header.getContext())}
|
|
312
316
|
</span>
|
|
313
317
|
{header.column.getIsSorted() === "asc" ? (
|
|
314
318
|
<ArrowUp className="w-3 h-3 shrink-0" />
|
|
@@ -319,11 +323,8 @@ export function VirtualizedDataTable<TData>({
|
|
|
319
323
|
)}
|
|
320
324
|
</button>
|
|
321
325
|
) : (
|
|
322
|
-
<span className="min-w-0 flex-1 truncate">
|
|
323
|
-
{flexRender(
|
|
324
|
-
header.column.columnDef.header,
|
|
325
|
-
header.getContext(),
|
|
326
|
-
)}
|
|
326
|
+
<span className="min-w-0 flex-1 truncate text-xs leading-4" title={headerTitle}>
|
|
327
|
+
{flexRender(headerDef, header.getContext())}
|
|
327
328
|
</span>
|
|
328
329
|
)}
|
|
329
330
|
{(canServerSort || header.column.getCanSort() || header.column.getCanHide()) && (
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministically maps an entity name to a muted Tailwind color class pair
|
|
3
|
+
* (background + text) used for entity avatar/initial badges. The same input
|
|
4
|
+
* always yields the same color so a given entity keeps a stable color across
|
|
5
|
+
* the app.
|
|
6
|
+
*/
|
|
7
|
+
const COLORS = [
|
|
8
|
+
"bg-muted text-muted-foreground",
|
|
9
|
+
"bg-gray-100 text-gray-600",
|
|
10
|
+
"bg-zinc-100 text-zinc-600",
|
|
11
|
+
"bg-blue-50 text-blue-600",
|
|
12
|
+
"bg-indigo-50 text-indigo-600",
|
|
13
|
+
"bg-violet-50 text-violet-600",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
export function getEntityColor(name: string) {
|
|
17
|
+
let hash = 0
|
|
18
|
+
for (let i = 0; i < name.length; i += 1) {
|
|
19
|
+
hash = name.charCodeAt(i) + ((hash << 5) - hash)
|
|
20
|
+
}
|
|
21
|
+
return COLORS[Math.abs(hash) % COLORS.length]
|
|
22
|
+
}
|