@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.
@@ -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="inline-flex max-w-full items-center gap-1 truncate font-medium text-foreground underline-offset-4 hover:text-primary hover:underline"
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 data-slot="linked-entity-cell-name" className="block truncate font-medium text-foreground">
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-1">
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(header.column.columnDef.header, header.getContext())}
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
@@ -5,6 +5,7 @@
5
5
 
6
6
  // Utilities
7
7
  export { cn } from "./lib/utils"
8
+ export { getEntityColor } from "./lib/entity-color"
8
9
  export { BRAND_ICONS, BRAND_GRAPHICS } from "./lib/icons"
9
10
  export { displayName, getInitials, shortName, type ProfileLike } from "./lib/user-display"
10
11
 
@@ -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
+ }