@handled-ai/design-system 0.17.2 → 0.18.1

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 (58) hide show
  1. package/dist/charts/empty-chart-state.d.ts +11 -0
  2. package/dist/charts/empty-chart-state.js +70 -0
  3. package/dist/charts/empty-chart-state.js.map +1 -0
  4. package/dist/charts/index.d.ts +1 -0
  5. package/dist/charts/index.js +1 -0
  6. package/dist/charts/index.js.map +1 -1
  7. package/dist/charts/pipeline-overview.d.ts +2 -1
  8. package/dist/charts/pipeline-overview.js +29 -1
  9. package/dist/charts/pipeline-overview.js.map +1 -1
  10. package/dist/components/actor-byline.d.ts +3 -0
  11. package/dist/components/actor-byline.js +5 -0
  12. package/dist/components/actor-byline.js.map +1 -0
  13. package/dist/components/days-open-cell.d.ts +16 -0
  14. package/dist/components/days-open-cell.js +73 -0
  15. package/dist/components/days-open-cell.js.map +1 -0
  16. package/dist/components/detail-drawer.d.ts +16 -0
  17. package/dist/components/detail-drawer.js +45 -0
  18. package/dist/components/detail-drawer.js.map +1 -0
  19. package/dist/components/insights-filter-bar.d.ts +2 -1
  20. package/dist/components/insights-filter-bar.js +13 -5
  21. package/dist/components/insights-filter-bar.js.map +1 -1
  22. package/dist/components/linked-entity-cell.d.ts +14 -0
  23. package/dist/components/linked-entity-cell.js +96 -0
  24. package/dist/components/linked-entity-cell.js.map +1 -0
  25. package/dist/components/metric-card.d.ts +14 -1
  26. package/dist/components/metric-card.js +86 -0
  27. package/dist/components/metric-card.js.map +1 -1
  28. package/dist/components/performance-metrics-table.d.ts +2 -1
  29. package/dist/components/performance-metrics-table.js +78 -46
  30. package/dist/components/performance-metrics-table.js.map +1 -1
  31. package/dist/components/pill.d.ts +26 -0
  32. package/dist/components/pill.js +77 -0
  33. package/dist/components/pill.js.map +1 -0
  34. package/dist/components/quick-segment.d.ts +13 -0
  35. package/dist/components/quick-segment.js +96 -0
  36. package/dist/components/quick-segment.js.map +1 -0
  37. package/dist/index.d.ts +7 -1
  38. package/dist/index.js +5 -0
  39. package/dist/index.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/charts/__tests__/insights-charts.test.tsx +62 -0
  42. package/src/charts/empty-chart-state.tsx +44 -0
  43. package/src/charts/index.ts +1 -0
  44. package/src/charts/pipeline-overview.tsx +38 -1
  45. package/src/components/__tests__/insights-primitives.test.tsx +117 -0
  46. package/src/components/__tests__/performance-metrics-table.test.tsx +54 -0
  47. package/src/components/__tests__/user-display.test.tsx +75 -0
  48. package/src/components/actor-byline.tsx +1 -0
  49. package/src/components/days-open-cell.tsx +50 -0
  50. package/src/components/detail-drawer.tsx +60 -0
  51. package/src/components/insights-filter-bar.tsx +13 -4
  52. package/src/components/linked-entity-cell.tsx +74 -0
  53. package/src/components/metric-card.tsx +82 -0
  54. package/src/components/performance-metrics-table.tsx +99 -63
  55. package/src/components/pill.tsx +67 -0
  56. package/src/components/quick-segment.tsx +68 -0
  57. package/src/index.ts +5 -0
  58. package/src/lib/__tests__/user-display.test.ts +53 -11
@@ -52,6 +52,7 @@ interface PerformanceMetricsTableProps {
52
52
  title?: string
53
53
  entityColumnLabel?: string
54
54
  primaryMetricColumnLabel?: string
55
+ primaryMetricDisplayMode?: "progress" | "value"
55
56
  rateColumnLabel?: string
56
57
  metricOneColumnLabel?: string
57
58
  metricTwoColumnLabel?: string
@@ -179,9 +180,48 @@ function getProgressStatus(value: number, target: number) {
179
180
  }
180
181
  }
181
182
 
183
+ function PrimaryMetricCell({
184
+ row,
185
+ displayMode,
186
+ }: {
187
+ row: PerformanceMetricsTableRow
188
+ displayMode: "progress" | "value"
189
+ }) {
190
+ if (displayMode === "value") {
191
+ return (
192
+ <div className="text-sm font-bold text-foreground">
193
+ {row.primaryValue}
194
+ </div>
195
+ )
196
+ }
197
+
198
+ const percentage = (row.primaryValue / row.primaryTarget) * 100
199
+ const progress = getProgressStatus(row.primaryValue, row.primaryTarget)
200
+
201
+ return (
202
+ <div className="flex items-center gap-2">
203
+ <span className="shrink-0">{progress.icon}</span>
204
+ <div className="w-full max-w-[180px]">
205
+ <div className="mb-1 text-sm font-bold text-foreground">
206
+ {row.primaryValue}/{row.primaryTarget}
207
+ </div>
208
+ <div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
209
+ <div
210
+ className={cn("h-full rounded-full", progress.color)}
211
+ style={{ width: `${Math.min(100, percentage)}%` }}
212
+ />
213
+ </div>
214
+ <div className={cn("mt-1 text-xs font-medium", progress.textColor)}>
215
+ {Math.round(percentage)}%
216
+ </div>
217
+ </div>
218
+ </div>
219
+ )
220
+ }
221
+
182
222
  function sortRows(
183
223
  rows: PerformanceMetricsTableRow[],
184
- sortId: PerformanceMetricsTableSortOption["id"]
224
+ sortId: PerformanceMetricsTableSortOption["id"],
185
225
  ) {
186
226
  const copy = [...rows]
187
227
  switch (sortId) {
@@ -201,23 +241,28 @@ export function PerformanceMetricsTable({
201
241
  title = "Performance Table",
202
242
  entityColumnLabel = "Entity",
203
243
  primaryMetricColumnLabel = "Primary Goal",
244
+ primaryMetricDisplayMode = "progress",
204
245
  rateColumnLabel = "Rate",
205
246
  metricOneColumnLabel = "Metric One",
206
247
  metricTwoColumnLabel = "Metric Two",
207
248
  metricThreeColumnLabel = "Metric Three",
208
249
  metricFourColumnLabel = "Metric Four",
209
250
  viewOptions = ["By Entity"],
210
- roleOptions = ["All", "Senior Coordinator", "Coordinator", "Junior Coordinator"],
251
+ roleOptions = [
252
+ "All",
253
+ "Senior Coordinator",
254
+ "Coordinator",
255
+ "Junior Coordinator",
256
+ ],
211
257
  sortOptions = DEFAULT_SORT_OPTIONS,
212
258
  rows = DEFAULT_ROWS,
213
259
  pageSize = 6,
214
260
  searchPlaceholder = "Search rows...",
215
261
  }: PerformanceMetricsTableProps) {
216
262
  const [view, setView] = React.useState(viewOptions[0] ?? "By Entity")
217
- const [sortId, setSortId] =
218
- React.useState<PerformanceMetricsTableSortOption["id"]>(
219
- sortOptions[0]?.id ?? "primary-desc"
220
- )
263
+ const [sortId, setSortId] = React.useState<
264
+ PerformanceMetricsTableSortOption["id"]
265
+ >(sortOptions[0]?.id ?? "primary-desc")
221
266
  const [roleFilter, setRoleFilter] = React.useState(roleOptions[0] ?? "All")
222
267
  const [search, setSearch] = React.useState("")
223
268
  const [page, setPage] = React.useState(1)
@@ -237,7 +282,7 @@ export function PerformanceMetricsTable({
237
282
 
238
283
  const sortedRows = React.useMemo(
239
284
  () => sortRows(filteredRows, sortId),
240
- [filteredRows, sortId]
285
+ [filteredRows, sortId],
241
286
  )
242
287
 
243
288
  const pageCount = Math.max(1, Math.ceil(sortedRows.length / pageSize))
@@ -285,7 +330,10 @@ export function PerformanceMetricsTable({
285
330
  </DropdownMenuTrigger>
286
331
  <DropdownMenuContent align="end" className="w-56">
287
332
  {sortOptions.map((option) => (
288
- <DropdownMenuItem key={option.id} onClick={() => setSortId(option.id)}>
333
+ <DropdownMenuItem
334
+ key={option.id}
335
+ onClick={() => setSortId(option.id)}
336
+ >
289
337
  {option.label}
290
338
  </DropdownMenuItem>
291
339
  ))}
@@ -301,7 +349,10 @@ export function PerformanceMetricsTable({
301
349
  </DropdownMenuTrigger>
302
350
  <DropdownMenuContent align="end">
303
351
  {roleOptions.map((option) => (
304
- <DropdownMenuItem key={option} onClick={() => setRoleFilter(option)}>
352
+ <DropdownMenuItem
353
+ key={option}
354
+ onClick={() => setRoleFilter(option)}
355
+ >
305
356
  {option}
306
357
  </DropdownMenuItem>
307
358
  ))}
@@ -346,61 +397,45 @@ export function PerformanceMetricsTable({
346
397
  </TableRow>
347
398
  </TableHeader>
348
399
  <TableBody>
349
- {paginatedRows.map((row) => {
350
- const percentage = (row.primaryValue / row.primaryTarget) * 100
351
- const progress = getProgressStatus(row.primaryValue, row.primaryTarget)
400
+ {paginatedRows.map((row) => (
401
+ <TableRow key={row.id} className="hover:bg-muted/30">
402
+ <TableCell className="px-4 py-3">
403
+ <div className="flex items-center gap-3">
404
+ <Avatar className="h-8 w-8 border border-border">
405
+ <AvatarFallback className="bg-emerald-100 text-[11px] font-medium text-emerald-700">
406
+ {row.avatarFallback}
407
+ </AvatarFallback>
408
+ </Avatar>
409
+ <span className="text-sm font-medium text-foreground">
410
+ {row.label}
411
+ </span>
412
+ </div>
413
+ </TableCell>
352
414
 
353
- return (
354
- <TableRow key={row.id} className="hover:bg-muted/30">
355
- <TableCell className="px-4 py-3">
356
- <div className="flex items-center gap-3">
357
- <Avatar className="h-8 w-8 border border-border">
358
- <AvatarFallback className="bg-emerald-100 text-[11px] font-medium text-emerald-700">
359
- {row.avatarFallback}
360
- </AvatarFallback>
361
- </Avatar>
362
- <span className="text-sm font-medium text-foreground">{row.label}</span>
363
- </div>
364
- </TableCell>
415
+ <TableCell className="px-4 py-3">
416
+ <PrimaryMetricCell
417
+ row={row}
418
+ displayMode={primaryMetricDisplayMode}
419
+ />
420
+ </TableCell>
365
421
 
366
- <TableCell className="px-4 py-3">
367
- <div className="flex items-center gap-2">
368
- <span className="shrink-0">{progress.icon}</span>
369
- <div className="w-full max-w-[180px]">
370
- <div className="mb-1 text-sm font-bold text-foreground">
371
- {row.primaryValue}/{row.primaryTarget}
372
- </div>
373
- <div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
374
- <div
375
- className={cn("h-full rounded-full", progress.color)}
376
- style={{ width: `${Math.min(100, percentage)}%` }}
377
- />
378
- </div>
379
- <div className={cn("mt-1 text-xs font-medium", progress.textColor)}>
380
- {Math.round(percentage)}%
381
- </div>
382
- </div>
383
- </div>
384
- </TableCell>
385
-
386
- <TableCell className="px-4 py-3 text-right text-sm font-semibold text-emerald-600">
387
- {row.ratePercent}%
388
- </TableCell>
389
- <TableCell className="px-4 py-3 text-right text-sm font-medium text-foreground">
390
- {row.metricOne}
391
- </TableCell>
392
- <TableCell className="px-4 py-3 text-right text-sm text-muted-foreground">
393
- {row.metricTwo}
394
- </TableCell>
395
- <TableCell className="px-4 py-3 text-right text-sm font-medium text-foreground">
396
- {row.metricThree}
397
- </TableCell>
398
- <TableCell className="px-4 py-3 text-right text-sm font-medium text-foreground">
399
- {row.metricFour}
400
- </TableCell>
401
- </TableRow>
402
- )
403
- })}
422
+ <TableCell className="px-4 py-3 text-right text-sm font-semibold text-emerald-600">
423
+ {row.ratePercent}%
424
+ </TableCell>
425
+ <TableCell className="px-4 py-3 text-right text-sm font-medium text-foreground">
426
+ {row.metricOne}
427
+ </TableCell>
428
+ <TableCell className="px-4 py-3 text-right text-sm text-muted-foreground">
429
+ {row.metricTwo}
430
+ </TableCell>
431
+ <TableCell className="px-4 py-3 text-right text-sm font-medium text-foreground">
432
+ {row.metricThree}
433
+ </TableCell>
434
+ <TableCell className="px-4 py-3 text-right text-sm font-medium text-foreground">
435
+ {row.metricFour}
436
+ </TableCell>
437
+ </TableRow>
438
+ ))}
404
439
  </TableBody>
405
440
  </Table>
406
441
  </div>
@@ -410,7 +445,8 @@ export function PerformanceMetricsTable({
410
445
  <div className="flex items-center justify-between border-t border-border bg-muted/20 px-4 py-3">
411
446
  <span className="text-xs text-muted-foreground">
412
447
  Showing {sortedRows.length === 0 ? 0 : start + 1} to{" "}
413
- {Math.min(start + pageSize, sortedRows.length)} of {sortedRows.length} rows
448
+ {Math.min(start + pageSize, sortedRows.length)} of {sortedRows.length}{" "}
449
+ rows
414
450
  </span>
415
451
  <div className="flex items-center gap-2">
416
452
  <Button
@@ -0,0 +1,67 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "../lib/utils"
5
+
6
+ /**
7
+ * Insights-friendly pill convenience wrappers.
8
+ *
9
+ * Pill and StatusPill are small, rounded wrappers for dense Insights surfaces
10
+ * such as tables, KPI strips, and filter summaries. They intentionally wrap the
11
+ * existing Badge/StatusBadge visual language and are not a replacement for all
12
+ * Badge usage across the design system.
13
+ */
14
+ export type PillStatus = "success" | "warning" | "error" | "neutral" | "info"
15
+
16
+ const pillVariants = cva(
17
+ "inline-flex w-fit shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors [&>svg]:size-3",
18
+ {
19
+ variants: {
20
+ variant: {
21
+ default: "border-transparent bg-primary text-primary-foreground",
22
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
23
+ destructive: "border-transparent bg-destructive text-white dark:bg-destructive/60",
24
+ outline: "border-border bg-background text-foreground",
25
+ ghost: "border-transparent bg-transparent text-muted-foreground",
26
+ success: "border-transparent bg-green-100 text-green-950 dark:bg-green-950 dark:text-green-100",
27
+ warning: "border-transparent bg-yellow-100 text-yellow-950 dark:bg-yellow-950 dark:text-yellow-100",
28
+ error: "border-transparent bg-red-100 text-red-950 dark:bg-red-950 dark:text-red-100",
29
+ neutral: "border-transparent bg-muted text-foreground",
30
+ info: "border-transparent bg-blue-100 text-blue-950 dark:bg-blue-950 dark:text-blue-100",
31
+ },
32
+ },
33
+ defaultVariants: {
34
+ variant: "neutral",
35
+ },
36
+ }
37
+ )
38
+
39
+ export interface PillProps
40
+ extends React.ComponentProps<"span">,
41
+ VariantProps<typeof pillVariants> {}
42
+
43
+ export function Pill({ className, variant = "neutral", ...props }: PillProps) {
44
+ return (
45
+ <span
46
+ data-slot="pill"
47
+ data-variant={variant}
48
+ className={cn(pillVariants({ variant }), className)}
49
+ {...props}
50
+ />
51
+ )
52
+ }
53
+
54
+ export interface StatusPillProps extends Omit<PillProps, "variant"> {
55
+ status: React.ReactNode
56
+ intent?: PillStatus
57
+ }
58
+
59
+ export function StatusPill({ status, intent = "neutral", children, ...props }: StatusPillProps) {
60
+ return (
61
+ <Pill data-slot="status-pill" variant={intent} {...props}>
62
+ {children ?? status}
63
+ </Pill>
64
+ )
65
+ }
66
+
67
+ export { pillVariants }
@@ -0,0 +1,68 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../lib/utils"
6
+
7
+ export interface QuickSegmentProps
8
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect" | "value"> {
9
+ label: React.ReactNode
10
+ value: string
11
+ selected?: boolean
12
+ count?: number | string
13
+ description?: React.ReactNode
14
+ onSelect?: (value: string) => void
15
+ }
16
+
17
+ export function QuickSegment({
18
+ label,
19
+ value,
20
+ selected = false,
21
+ count,
22
+ description,
23
+ onSelect,
24
+ className,
25
+ type = "button",
26
+ ...props
27
+ }: QuickSegmentProps) {
28
+ return (
29
+ <button
30
+ data-slot="quick-segment"
31
+ data-selected={selected ? "true" : "false"}
32
+ type={type}
33
+ aria-pressed={selected}
34
+ className={cn(
35
+ "inline-flex min-h-8 items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-medium transition-colors",
36
+ selected
37
+ ? "border-primary bg-primary text-primary-foreground shadow-sm"
38
+ : "border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground",
39
+ className
40
+ )}
41
+ onClick={(event) => {
42
+ props.onClick?.(event)
43
+ if (!event.defaultPrevented) onSelect?.(value)
44
+ }}
45
+ {...props}
46
+ >
47
+ <span data-slot="quick-segment-label">{label}</span>
48
+ {count !== undefined ? (
49
+ <span
50
+ data-slot="quick-segment-count"
51
+ className={cn(
52
+ "rounded-full px-1.5 py-0.5 text-[11px] leading-none",
53
+ selected
54
+ ? "bg-primary-foreground/20 text-primary-foreground"
55
+ : "bg-muted text-muted-foreground"
56
+ )}
57
+ >
58
+ {count}
59
+ </span>
60
+ ) : null}
61
+ {description ? (
62
+ <span data-slot="quick-segment-description" className="sr-only">
63
+ {description}
64
+ </span>
65
+ ) : null}
66
+ </button>
67
+ )
68
+ }
package/src/index.ts CHANGED
@@ -33,6 +33,7 @@ export * from "./components/data-table-filter"
33
33
  export * from "./components/data-table-quick-views"
34
34
  export * from "./components/data-table-toolbar"
35
35
  export * from "./components/detail-view"
36
+ export * from "./components/detail-drawer"
36
37
  export * from "./components/dialog"
37
38
  export * from "./components/dropdown-menu"
38
39
  export * from "./components/empty-state"
@@ -47,6 +48,8 @@ export * from "./components/inbox-toolbar"
47
48
  export * from "./components/inline-banner"
48
49
  export * from "./components/input"
49
50
  export * from "./components/insights-filter-bar"
51
+ export * from "./components/days-open-cell"
52
+ export * from "./components/linked-entity-cell"
50
53
  export * from "./components/item-list"
51
54
  export * from "./components/item-list-display"
52
55
  export * from "./components/item-list-filter"
@@ -56,9 +59,11 @@ export * from "./components/label"
56
59
  export * from "./components/message"
57
60
  export * from "./components/metric-card"
58
61
  export * from "./components/performance-metrics-table"
62
+ export * from "./components/pill"
59
63
  export * from "./components/preview-list"
60
64
  export * from "./components/progress"
61
65
  export * from "./components/quick-action-chat-area"
66
+ export * from "./components/quick-segment"
62
67
  export {
63
68
  QuickActionModal,
64
69
  type QuickActionPriority,
@@ -3,41 +3,83 @@ import { describe, expect, it } from "vitest"
3
3
  import { displayName, getInitials, shortName } from "../user-display"
4
4
 
5
5
  describe("displayName", () => {
6
- it("prefers first and last name", () => {
6
+ it("returns first_name + last_name when both are present", () => {
7
7
  expect(
8
- displayName({ first_name: "Sarah", last_name: "Mitchell", name: "S Mitchell", email: "sarah@example.com" }),
8
+ displayName({ first_name: "Sarah", last_name: "Mitchell", name: "S Mitchell", email: "sarah@example.com" })
9
9
  ).toBe("Sarah Mitchell")
10
10
  })
11
11
 
12
- it("falls back through first name, name, email local part, then unknown", () => {
13
- expect(displayName({ first_name: "Sarah", last_name: null, name: null, email: "sarah@example.com" })).toBe("Sarah")
14
- expect(displayName({ first_name: null, last_name: null, name: "Sarah Mitchell", email: "sarah@example.com" })).toBe("Sarah Mitchell")
12
+ it("returns first_name alone when last_name is missing", () => {
13
+ expect(displayName({ first_name: "Sarah", last_name: null, name: null, email: "sarah@example.com" })).toBe(
14
+ "Sarah"
15
+ )
16
+ })
17
+
18
+ it("falls back to name when first_name is missing", () => {
19
+ expect(displayName({ first_name: null, last_name: null, name: "Sarah Mitchell", email: "sarah@example.com" })).toBe(
20
+ "Sarah Mitchell"
21
+ )
22
+ })
23
+
24
+ it("falls back to email local-part when name fields are missing or empty", () => {
15
25
  expect(displayName({ first_name: "", last_name: "", name: "", email: "sarah@example.com" })).toBe("sarah")
26
+ })
27
+
28
+ it("falls back to Unknown user when profile fields are absent", () => {
16
29
  expect(displayName({})).toBe("Unknown user")
17
30
  expect(displayName()).toBe("Unknown user")
18
31
  })
32
+
33
+ it("ignores last_name if first_name is missing", () => {
34
+ expect(displayName({ first_name: null, last_name: "Mitchell", name: "Old Name", email: "sarah@example.com" })).toBe(
35
+ "Old Name"
36
+ )
37
+ })
19
38
  })
20
39
 
21
40
  describe("getInitials", () => {
22
- it("uses first and last initials", () => {
41
+ it("returns uppercased initials from first_name and last_name", () => {
23
42
  expect(getInitials({ first_name: "joe", last_name: "kim", email: "joe@example.com" })).toBe("JK")
24
43
  })
25
44
 
26
- it("falls back through name, email local part, then question mark", () => {
27
- expect(getInitials({ first_name: null, last_name: null, name: "Sarah Mitchell", email: "sarah@example.com" })).toBe("SM")
45
+ it("splits name into two parts for initials when first/last are not set", () => {
46
+ expect(getInitials({ first_name: null, last_name: null, name: "Sarah Mitchell", email: "sarah@example.com" })).toBe(
47
+ "SM"
48
+ )
49
+ })
50
+
51
+ it("uses first two characters of a single-word name", () => {
28
52
  expect(getInitials({ first_name: null, last_name: null, name: "Sarah", email: "sarah@example.com" })).toBe("SA")
53
+ })
54
+
55
+ it("falls back to first two email local characters when no name fields are set", () => {
29
56
  expect(getInitials({ first_name: null, last_name: null, name: null, email: "zz@example.com" })).toBe("ZZ")
57
+ })
58
+
59
+ it("returns question mark when no initials can be derived", () => {
30
60
  expect(getInitials({})).toBe("?")
31
61
  expect(getInitials()).toBe("?")
32
62
  })
63
+
64
+ it("handles multi-word name taking the first two initials", () => {
65
+ expect(getInitials({ name: "Mary Jane Watson", email: "mj@example.com" })).toBe("MJ")
66
+ })
33
67
  })
34
68
 
35
69
  describe("shortName", () => {
36
- it("returns compact first name and last initial", () => {
70
+ it("returns 'First L.' when first and last are present", () => {
37
71
  expect(shortName({ first_name: "Sarah", last_name: "Mitchell", email: "sarah@example.com" })).toBe("Sarah M.")
38
72
  })
39
73
 
40
- it("falls back to displayName when last name is unavailable", () => {
41
- expect(shortName({ name: "Sarah Mitchell", email: "sarah@example.com" })).toBe("Sarah Mitchell")
74
+ it("falls back to displayName when last_name is unavailable", () => {
75
+ expect(shortName({ first_name: "Sarah", last_name: null, name: null, email: "sarah@example.com" })).toBe("Sarah")
76
+ })
77
+
78
+ it("falls back to email local-part when all names are missing", () => {
79
+ expect(shortName({ first_name: null, last_name: null, name: null, email: "sarah@example.com" })).toBe("sarah")
80
+ })
81
+
82
+ it("falls back to Unknown user when no profile fields are present", () => {
83
+ expect(shortName({})).toBe("Unknown user")
42
84
  })
43
85
  })