@flamingo-stack/openframe-frontend-core 0.0.202 → 0.0.203

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 (105) hide show
  1. package/dist/{chunk-OII2IERE.cjs → chunk-25LVV26X.cjs} +4 -4
  2. package/dist/chunk-25LVV26X.cjs.map +1 -0
  3. package/dist/{chunk-JIKTMXTZ.cjs → chunk-3YH2M76N.cjs} +896 -740
  4. package/dist/chunk-3YH2M76N.cjs.map +1 -0
  5. package/dist/{chunk-55HF462A.js → chunk-CPXLQ57U.js} +6 -7
  6. package/dist/chunk-CPXLQ57U.js.map +1 -0
  7. package/dist/{chunk-IDULPYOU.js → chunk-E6Q6UGDK.js} +1897 -1741
  8. package/dist/chunk-E6Q6UGDK.js.map +1 -0
  9. package/dist/{chunk-3B43AHYE.cjs → chunk-RMB5DVED.cjs} +6 -7
  10. package/dist/chunk-RMB5DVED.cjs.map +1 -0
  11. package/dist/{chunk-4ML3NA2L.js → chunk-XGL5FKIK.js} +4 -4
  12. package/dist/chunk-XGL5FKIK.js.map +1 -0
  13. package/dist/components/chat/chat-ticket-item.d.ts.map +1 -1
  14. package/dist/components/features/index.cjs +4 -4
  15. package/dist/components/features/index.js +3 -3
  16. package/dist/components/features/select-button.d.ts.map +1 -1
  17. package/dist/components/index.cjs +6 -4
  18. package/dist/components/index.cjs.map +1 -1
  19. package/dist/components/index.js +5 -3
  20. package/dist/components/navigation/index.cjs +4 -4
  21. package/dist/components/navigation/index.js +3 -3
  22. package/dist/components/navigation/navigation-sidebar.d.ts.map +1 -1
  23. package/dist/components/resizable.d.ts +1 -1
  24. package/dist/components/ui/button/split-button.d.ts.map +1 -1
  25. package/dist/components/ui/data-table/data-table-row.d.ts +16 -4
  26. package/dist/components/ui/data-table/data-table-row.d.ts.map +1 -1
  27. package/dist/components/ui/file-manager/index.cjs +52 -52
  28. package/dist/components/ui/file-manager/index.cjs.map +1 -1
  29. package/dist/components/ui/file-manager/index.js +3 -3
  30. package/dist/components/ui/file-manager/index.js.map +1 -1
  31. package/dist/components/ui/floating-tooltip.d.ts +3 -1
  32. package/dist/components/ui/floating-tooltip.d.ts.map +1 -1
  33. package/dist/components/ui/index.cjs +6 -4
  34. package/dist/components/ui/index.cjs.map +1 -1
  35. package/dist/components/ui/index.d.ts +1 -0
  36. package/dist/components/ui/index.d.ts.map +1 -1
  37. package/dist/components/ui/index.js +5 -3
  38. package/dist/components/ui/input-trigger.d.ts.map +1 -1
  39. package/dist/components/ui/radio-group.d.ts.map +1 -1
  40. package/dist/components/ui/ticket-info-section.d.ts.map +1 -1
  41. package/dist/components/ui/ticket-note-card.d.ts.map +1 -1
  42. package/dist/components/ui/truncate-text.d.ts +33 -0
  43. package/dist/components/ui/truncate-text.d.ts.map +1 -0
  44. package/dist/components/user-summary-stub.d.ts.map +1 -1
  45. package/dist/hooks/index.cjs +2 -2
  46. package/dist/hooks/index.js +1 -1
  47. package/dist/index.cjs +6 -4
  48. package/dist/index.cjs.map +1 -1
  49. package/dist/index.js +5 -3
  50. package/package.json +1 -1
  51. package/src/components/chat/chat-container.tsx +2 -2
  52. package/src/components/chat/chat-ticket-item.tsx +2 -3
  53. package/src/components/features/board/ticket-card.tsx +2 -2
  54. package/src/components/features/filters-dropdown.tsx +1 -1
  55. package/src/components/features/notifications/notification-tile.tsx +2 -2
  56. package/src/components/features/policy-configuration-panel.tsx +1 -1
  57. package/src/components/features/push-button-selector.tsx +1 -1
  58. package/src/components/features/select-button.tsx +2 -3
  59. package/src/components/features/video-bites-display.tsx +1 -1
  60. package/src/components/features/waitlist-form.tsx +1 -1
  61. package/src/components/filter-chip.tsx +1 -1
  62. package/src/components/layout/title-block.tsx +2 -2
  63. package/src/components/navigation/header-organization-filter.tsx +1 -1
  64. package/src/components/navigation/navigation-sidebar.tsx +107 -54
  65. package/src/components/platform/ScriptInfoSection.tsx +1 -1
  66. package/src/components/shared/onboarding/onboarding-step-card.tsx +2 -2
  67. package/src/components/shared/product-release/product-release-card.tsx +6 -6
  68. package/src/components/shared/product-release/release-detail-page.tsx +1 -1
  69. package/src/components/ui/assignee-dropdown.tsx +3 -3
  70. package/src/components/ui/autocomplete.tsx +2 -2
  71. package/src/components/ui/button/split-button.tsx +3 -5
  72. package/src/components/ui/checkbox-block.tsx +1 -1
  73. package/src/components/ui/data-table/data-table-row.tsx +82 -48
  74. package/src/components/ui/device-card-compact.tsx +2 -2
  75. package/src/components/ui/device-card.tsx +2 -2
  76. package/src/components/ui/entity-image.tsx +1 -1
  77. package/src/components/ui/field-wrapper.tsx +1 -1
  78. package/src/components/ui/file-manager/file-manager-table-row.tsx +2 -2
  79. package/src/components/ui/file-upload.tsx +2 -2
  80. package/src/components/ui/filter-list.tsx +1 -1
  81. package/src/components/ui/floating-tooltip.tsx +9 -5
  82. package/src/components/ui/hidden-tags-popup.tsx +1 -1
  83. package/src/components/ui/index.ts +1 -0
  84. package/src/components/ui/info-card.tsx +2 -2
  85. package/src/components/ui/input-trigger.tsx +1 -2
  86. package/src/components/ui/organization-card.tsx +3 -3
  87. package/src/components/ui/radio-group.tsx +2 -3
  88. package/src/components/ui/search-input.tsx +2 -2
  89. package/src/components/ui/service-card.tsx +3 -3
  90. package/src/components/ui/tag.tsx +1 -1
  91. package/src/components/ui/tags-manager.tsx +2 -2
  92. package/src/components/ui/ticket-attachments-list.tsx +1 -1
  93. package/src/components/ui/ticket-info-section.tsx +2 -3
  94. package/src/components/ui/ticket-note-card.tsx +4 -1
  95. package/src/components/ui/toaster.tsx +3 -3
  96. package/src/components/ui/truncate-text.tsx +116 -0
  97. package/src/components/user-summary-stub.tsx +32 -26
  98. package/src/components/vendor-display-button.tsx +1 -1
  99. package/src/stories/SplitButton.stories.tsx +7 -1
  100. package/dist/chunk-3B43AHYE.cjs.map +0 -1
  101. package/dist/chunk-4ML3NA2L.js.map +0 -1
  102. package/dist/chunk-55HF462A.js.map +0 -1
  103. package/dist/chunk-IDULPYOU.js.map +0 -1
  104. package/dist/chunk-JIKTMXTZ.cjs.map +0 -1
  105. package/dist/chunk-OII2IERE.cjs.map +0 -1
@@ -56,7 +56,7 @@ export function FilterListItem({
56
56
  )}
57
57
  >
58
58
  <div className="flex min-w-0 flex-1 flex-col">
59
- <p className="truncate text-h4 text-ods-text-primary">
59
+ <p className="truncate text-h4 text-ods-text-primary" title={title}>
60
60
  {title}
61
61
  </p>
62
62
 
@@ -23,6 +23,8 @@ interface FloatingTooltipProps {
23
23
  side?: "top" | "right" | "bottom" | "left"
24
24
  className?: string
25
25
  delayDuration?: number
26
+ /** Disable the tooltip without unmounting the trigger wrapper. */
27
+ disabled?: boolean
26
28
  }
27
29
 
28
30
  // Parse colored text markup like [YELLOW]text[/YELLOW] into JSX
@@ -75,12 +77,13 @@ function parseColoredText(text: string): React.ReactNode {
75
77
  return parts.length > 0 ? <>{parts}</> : text
76
78
  }
77
79
 
78
- export function FloatingTooltip({
79
- content,
80
- children,
81
- side = "right",
80
+ export function FloatingTooltip({
81
+ content,
82
+ children,
83
+ side = "right",
82
84
  className,
83
- delayDuration = 0
85
+ delayDuration = 0,
86
+ disabled = false,
84
87
  }: FloatingTooltipProps) {
85
88
  const [isOpen, setIsOpen] = React.useState(false)
86
89
  const arrowRef = React.useRef<HTMLDivElement>(null)
@@ -104,6 +107,7 @@ export function FloatingTooltip({
104
107
 
105
108
  const hover = useHover(context, {
106
109
  move: false,
110
+ enabled: !disabled,
107
111
  delay: { open: delayDuration, close: 0 },
108
112
  handleClose: safePolygon(),
109
113
  })
@@ -40,7 +40,7 @@ export const HiddenTagsPopup = forwardRef(function HiddenTagsPopup(
40
40
  "border-b border-ods-border last:border-b-0",
41
41
  )}
42
42
  >
43
- <span className="text-h5 truncate uppercase text-ods-text-primary">
43
+ <span className="text-h5 truncate uppercase text-ods-text-primary" title={typeof item.label === 'string' ? item.label : undefined}>
44
44
  {item.label}
45
45
  </span>
46
46
  {!disabled && onRemove && (
@@ -76,6 +76,7 @@ export * from './entity-image'
76
76
  export * from './feature-card'
77
77
  export * from './feature-list'
78
78
  export { FloatingTooltip } from './floating-tooltip'
79
+ export { TruncateText, type TruncateTextProps } from './truncate-text'
79
80
  export * from './highlight-card'
80
81
  export * from './icons-block'
81
82
  export * from './filter-modal'
@@ -39,7 +39,7 @@ export function InfoCard({ data, className = '' }: InfoCardProps) {
39
39
  >
40
40
  {data.title && (
41
41
  <div className="flex items-center gap-[var(--spacing-system-xsf)] self-stretch h-6">
42
- <span className="text-h4 text-ods-text-primary truncate">
42
+ <span className="text-h4 text-ods-text-primary truncate" title={data.title}>
43
43
  {data.title}
44
44
  </span>
45
45
  {data.icon}
@@ -48,7 +48,7 @@ export function InfoCard({ data, className = '' }: InfoCardProps) {
48
48
 
49
49
  {/* Subtitle */}
50
50
  {data.subtitle && (
51
- <div className="text-h4 text-ods-text-secondary truncate self-stretch">
51
+ <div className="text-h4 text-ods-text-secondary truncate self-stretch" title={data.subtitle}>
52
52
  {data.subtitle}
53
53
  </div>
54
54
  )}
@@ -66,8 +66,7 @@ export const InputTrigger = React.forwardRef<HTMLButtonElement, InputTriggerProp
66
66
  className={cn(
67
67
  "flex-1 min-w-0 text-left truncate",
68
68
  isPlaceholder && "text-ods-text-secondary",
69
- )}
70
- >
69
+ )} title={isPlaceholder ? placeholder : (typeof selectedLabel === 'string' ? selectedLabel : undefined)}>
71
70
  {isPlaceholder ? placeholder : selectedLabel}
72
71
  </span>
73
72
  {endIcon && (
@@ -104,10 +104,10 @@ export function OrganizationCard({
104
104
  />
105
105
 
106
106
  <div className="flex-1 flex flex-col justify-center py-2 min-w-0">
107
- <h3 className="font-['DM_Sans'] font-bold text-lg leading-[1.33] tracking-[-0.02em] text-ods-text-primary transition-colors truncate">
107
+ <h3 className="font-['DM_Sans'] font-bold text-lg leading-[1.33] tracking-[-0.02em] text-ods-text-primary transition-colors truncate" title={organization.name}>
108
108
  {organization.name}
109
109
  </h3>
110
- <p className="font-['DM_Sans'] font-medium text-sm leading-[1.43] text-ods-text-secondary truncate">
110
+ <p className="font-['DM_Sans'] font-medium text-sm leading-[1.43] text-ods-text-secondary truncate" title={organization.industry || organization.tier || organization.websiteUrl || "Organization"}>
111
111
  {organization.industry || organization.tier || organization.websiteUrl || "Organization"}
112
112
  </p>
113
113
  </div>
@@ -116,7 +116,7 @@ export function OrganizationCard({
116
116
  {/* Description */}
117
117
  {organization.description && (
118
118
  <div className="w-full h-12 overflow-hidden">
119
- <p className="font-['DM_Sans'] font-medium text-lg leading-[1.33] text-ods-text-primary line-clamp-2">
119
+ <p className="font-['DM_Sans'] font-medium text-lg leading-[1.33] text-ods-text-primary line-clamp-2" title={organization.description}>
120
120
  {organization.description}
121
121
  </p>
122
122
  </div>
@@ -145,8 +145,7 @@ const RadioGroupBlock = React.forwardRef<
145
145
  className={cn(
146
146
  "font-[family-name:var(--font-h4-family)] font-[number:var(--font-h4-weight)] text-[length:var(--font-size-h4-body)] leading-[24px]",
147
147
  "text-ods-text-primary select-none truncate"
148
- )}
149
- >
148
+ )} title={typeof option.label === 'string' ? option.label : undefined}>
150
149
  {option.label}
151
150
  </span>
152
151
  {option.description && (
@@ -168,7 +167,7 @@ const RadioGroupBlock = React.forwardRef<
168
167
  })}
169
168
  </RadioGroupPrimitive.Root>
170
169
  {error && (
171
- <p className="absolute bottom-0 left-0 right-0 translate-y-full text-h6 truncate text-ods-error">
170
+ <p className="absolute bottom-0 left-0 right-0 translate-y-full text-h6 truncate text-ods-error" title={error}>
172
171
  {error}
173
172
  </p>
174
173
  )}
@@ -325,11 +325,11 @@ export function SearchInput({
325
325
  <div className={cn(
326
326
  "text-sm font-medium leading-5 truncate",
327
327
  isHighlighted ? "text-ods-accent" : "text-ods-text-primary"
328
- )}>
328
+ )} title={result.title}>
329
329
  {result.title}
330
330
  </div>
331
331
  {result.description && (
332
- <div className="text-xs leading-4 text-ods-text-secondary truncate mt-0.5">
332
+ <div className="text-xs leading-4 text-ods-text-secondary truncate mt-0.5" title={result.description}>
333
333
  {result.description}
334
334
  </div>
335
335
  )}
@@ -61,9 +61,9 @@ export function ServiceCard({ title, subtitle, icon, tag, rows, className }: Ser
61
61
  {resolvedIcon}
62
62
  </div>
63
63
  <div className="min-w-0">
64
- <div className="text-xl font-semibold text-ods-text-primary truncate">{title}</div>
64
+ <div className="text-xl font-semibold text-ods-text-primary truncate" title={title}>{title}</div>
65
65
  {subtitle && (
66
- <div className="text-sm text-ods-text-secondary truncate">{subtitle}</div>
66
+ <div className="text-sm text-ods-text-secondary truncate" title={subtitle}>{subtitle}</div>
67
67
  )}
68
68
  </div>
69
69
  </div>
@@ -104,7 +104,7 @@ function ServiceCardRowItem({ row }: { row: ServiceCardRow }) {
104
104
  <div className="w-20 md:w-24 shrink-0 text-sm font-medium text-ods-text-primary">{row.label}</div>
105
105
  )}
106
106
  <div className={cn('flex-1 h-12 rounded-md border border-ods-border bg-ods-bg px-3 md:px-4 flex items-center justify-between min-w-0', row.monospace ? 'font-mono' : '')}>
107
- <div className="truncate text-ods-text-primary min-w-0">{displayValue}</div>
107
+ <div className="truncate text-ods-text-primary min-w-0" title={typeof row.value === 'string' ? row.value : undefined}>{displayValue}</div>
108
108
  <div className="flex items-center gap-2 pl-3 flex-shrink-0">
109
109
  {actions.reveal && (
110
110
  <button
@@ -92,7 +92,7 @@ function Tag({
92
92
  {icon}
93
93
  </span>
94
94
  )}
95
- <span className={cn("truncate", labelClassName)}>{label}</span>
95
+ <span className={cn("truncate", labelClassName)} title={typeof label === 'string' ? label : undefined}>{label}</span>
96
96
  {onClose && (
97
97
  <button
98
98
  type="button"
@@ -329,7 +329,7 @@ export function TagsManager({
329
329
  }}
330
330
  >
331
331
  <div className="flex items-center justify-between w-full">
332
- <span className="truncate">{tag.name}</span>
332
+ <span className="truncate" title={tag.name}>{tag.name}</span>
333
333
  <div className="flex items-center gap-1 shrink-0">
334
334
  {isSelected && (
335
335
  <CheckIcon
@@ -401,7 +401,7 @@ export function TagsManager({
401
401
  size={16}
402
402
  className="text-ods-accent shrink-0"
403
403
  />
404
- <span className="text-ods-accent truncate">
404
+ <span className="text-ods-accent truncate" title={`Create "${search.trim()}"`}>
405
405
  Create &ldquo;{search.trim()}&rdquo;
406
406
  </span>
407
407
  </div>
@@ -44,7 +44,7 @@ export function TicketAttachmentsList({ attachments, className }: TicketAttachme
44
44
  </div>
45
45
  )}
46
46
  <div className="flex-1 min-w-0 overflow-hidden">
47
- <p className="text-h4 text-ods-text-primary truncate">{attachment.fileName}</p>
47
+ <p className="text-h4 text-ods-text-primary truncate" title={attachment.fileName}>{attachment.fileName}</p>
48
48
  <p className="text-h6 text-ods-text-secondary">{attachment.fileSize}</p>
49
49
  </div>
50
50
  {attachment.onDownload && (
@@ -89,12 +89,11 @@ function InfoCell({ value, label, icon, onClick }: {
89
89
  <button
90
90
  type="button"
91
91
  onClick={onClick}
92
- className="text-h4 text-ods-text-primary truncate hover:text-ods-accent transition-colors cursor-pointer text-left"
93
- >
92
+ className="text-h4 text-ods-text-primary truncate hover:text-ods-accent transition-colors cursor-pointer text-left" title={value}>
94
93
  {value}
95
94
  </button>
96
95
  ) : (
97
- <span className="text-h4 text-ods-text-primary truncate">{value}</span>
96
+ <span className="text-h4 text-ods-text-primary truncate" title={value}>{value}</span>
98
97
  )}
99
98
  </div>
100
99
  <span className="text-h6 text-ods-text-secondary truncate">{label}</span>
@@ -96,7 +96,10 @@ export function TicketNoteCard({ note, onEdit, onDelete, className }: TicketNote
96
96
  ) : (
97
97
  <>
98
98
  <p className="text-h4 text-ods-text-primary">{note.text}</p>
99
- <p className="text-h6 text-ods-text-secondary truncate">
99
+ <p
100
+ className="text-h6 text-ods-text-secondary truncate"
101
+ title={`${note.authorName} • ${note.createdAt}`}
102
+ >
100
103
  {note.authorName} &bull; {note.createdAt}
101
104
  </p>
102
105
  </>
@@ -61,10 +61,10 @@ function ToastHeader({
61
61
 
62
62
  <div className="flex min-w-0 flex-1 flex-col justify-center font-['DM_Sans'] font-medium">
63
63
  {title ? (
64
- <p className="truncate pr-5 text-[18px] leading-6 text-ods-text-primary">{title}</p>
64
+ <p className="truncate pr-5 text-[18px] leading-6 text-ods-text-primary" title={typeof title === 'string' ? title : undefined}>{title}</p>
65
65
  ) : null}
66
66
  {description ? (
67
- <p className="text-[14px] leading-5 text-ods-text-secondary line-clamp-3">{description}</p>
67
+ <p className="text-[14px] leading-5 text-ods-text-secondary line-clamp-3" title={typeof description === 'string' ? description : undefined}>{description}</p>
68
68
  ) : null}
69
69
  </div>
70
70
 
@@ -206,7 +206,7 @@ export function CommandApprovalToast({
206
206
  >
207
207
  <div className="overflow-hidden">
208
208
  <div className="flex h-11 w-full items-center gap-2 border-b border-ods-border bg-ods-card px-3 py-2">
209
- <p className="min-w-0 flex-1 truncate font-['DM_Sans'] text-[14px] font-medium leading-5 text-ods-text-primary">
209
+ <p className="min-w-0 flex-1 truncate font-['DM_Sans'] text-[14px] font-medium leading-5 text-ods-text-primary" title={command}>
210
210
  {command}
211
211
  </p>
212
212
  {toolType ? <ToolIcon toolType={toolType} size={16} /> : null}
@@ -0,0 +1,116 @@
1
+ 'use client'
2
+
3
+ import React, { type ReactNode, useEffect, useRef, useState } from 'react'
4
+ import { cn } from '../../utils/cn'
5
+ import { FloatingTooltip } from './floating-tooltip'
6
+
7
+ /** ODS typography variants. Maps to the `.text-h1`…`.text-h6` utilities. */
8
+ export type TruncateTextVariant = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
9
+
10
+ /** ODS text tone. Maps to the `text-ods-text-*` colour utilities. */
11
+ export type TruncateTextTone = 'primary' | 'secondary'
12
+
13
+ export interface TruncateTextProps {
14
+ children: string
15
+ /** Tooltip content; defaults to `children`. */
16
+ tooltip?: ReactNode
17
+ /** Extra classes merged after the variant/tone defaults. */
18
+ className?: string
19
+ side?: 'top' | 'right' | 'bottom' | 'left'
20
+ /** Max visible lines. `1` uses `truncate` (single-line ellipsis); higher values use `line-clamp-N`. */
21
+ lines?: 1 | 2 | 3 | 4 | 5 | 6
22
+ /** ODS typography token. Default: `'h4'` (body). */
23
+ variant?: TruncateTextVariant
24
+ /** ODS text tone. Default: `'primary'`. */
25
+ tone?: TruncateTextTone
26
+ /** Force the monospace (heading) font family — preserves the variant's size while swapping family. */
27
+ mono?: boolean
28
+ }
29
+
30
+ const VARIANT_CLASS: Record<TruncateTextVariant, string> = {
31
+ h1: 'text-h1',
32
+ h2: 'text-h2',
33
+ h3: 'text-h3',
34
+ h4: 'text-h4',
35
+ h5: 'text-h5',
36
+ h6: 'text-h6',
37
+ }
38
+
39
+ const TONE_CLASS: Record<TruncateTextTone, string> = {
40
+ primary: 'text-ods-text-primary',
41
+ secondary: 'text-ods-text-secondary',
42
+ }
43
+
44
+ const LINE_CLAMP_CLASS: Record<2 | 3 | 4 | 5 | 6, string> = {
45
+ 2: 'line-clamp-2',
46
+ 3: 'line-clamp-3',
47
+ 4: 'line-clamp-4',
48
+ 5: 'line-clamp-5',
49
+ 6: 'line-clamp-6',
50
+ }
51
+
52
+ /**
53
+ * Truncated text bound to the ODS typography system. Shows a `FloatingTooltip`
54
+ * with the full value when (and only when) the content overflows.
55
+ *
56
+ * ```tsx
57
+ * <TruncateText>{name}</TruncateText> // h4 / primary
58
+ * <TruncateText variant="h6" tone="secondary">{email}</TruncateText> // caption
59
+ * <TruncateText lines={3}>{description}</TruncateText> // 3-line clamp
60
+ * ```
61
+ */
62
+ export function TruncateText({
63
+ children,
64
+ tooltip,
65
+ className,
66
+ side = 'top',
67
+ lines = 1,
68
+ variant = 'h4',
69
+ tone = 'primary',
70
+ mono = false,
71
+ }: TruncateTextProps) {
72
+ const ref = useRef<HTMLSpanElement>(null)
73
+ const [isTruncated, setIsTruncated] = useState(false)
74
+ const isMultiLine = lines > 1
75
+
76
+ useEffect(() => {
77
+ const el = ref.current
78
+ if (!el) return
79
+ const check = () => {
80
+ const overflows = isMultiLine
81
+ ? el.scrollHeight > el.clientHeight + 1
82
+ : el.scrollWidth > el.clientWidth + 1
83
+ setIsTruncated(overflows)
84
+ }
85
+ check()
86
+ const ro = new ResizeObserver(check)
87
+ ro.observe(el)
88
+ return () => ro.disconnect()
89
+ }, [children, isMultiLine])
90
+
91
+ const clampClass = isMultiLine
92
+ ? LINE_CLAMP_CLASS[lines as Exclude<typeof lines, 1>]
93
+ : 'truncate block'
94
+
95
+ return (
96
+ <FloatingTooltip
97
+ content={tooltip ?? children}
98
+ side={side}
99
+ disabled={!isTruncated}
100
+ className="max-w-xs whitespace-pre-line [overflow-wrap:anywhere]"
101
+ >
102
+ <span
103
+ ref={ref}
104
+ className={cn(
105
+ VARIANT_CLASS[variant],
106
+ TONE_CLASS[tone],
107
+ mono && '[font-family:var(--font-family-heading)]',
108
+ clampClass,
109
+ className,
110
+ )}
111
+ >
112
+ {children}
113
+ </span>
114
+ </FloatingTooltip>
115
+ )
116
+ }
@@ -102,13 +102,16 @@ export function UserSummary({
102
102
  )}
103
103
  </div>
104
104
  <div className="min-w-0 flex-1">
105
- <p className="text-h4 text-ods-text-primary truncate">
105
+ <p
106
+ className="text-h4 text-ods-text-primary truncate"
107
+ title={mspPreview?.name ? `${name} • ${mspPreview.name}` : name}
108
+ >
106
109
  {name}
107
110
  {mspPreview?.name && (
108
111
  <span className="text-ods-text-secondary"> • {mspPreview.name}</span>
109
112
  )}
110
113
  </p>
111
- <p className="text-h6 text-ods-text-secondary truncate">
114
+ <p className="text-h6 text-ods-text-secondary truncate" title={subtitle && subtitle.trim().length > 0 ? subtitle : (email && email.trim().length > 0 ? email : '\u00A0')}>
112
115
  {subtitle && subtitle.trim().length > 0 ? subtitle : (email && email.trim().length > 0 ? email : '\u00A0')}
113
116
  </p>
114
117
  </div>
@@ -154,34 +157,37 @@ export function UserSummary({
154
157
  <div className="flex-1 grid grid-cols-[1fr_auto] gap-4">
155
158
  {/* LEFT : text stack */}
156
159
  <div className="min-h-[6rem] flex flex-col justify-center space-y-3 truncate">
157
- <p className="text-h2 text-ods-text-primary leading-none truncate">
160
+ <p className="text-h2 text-ods-text-primary leading-none truncate" title={name}>
158
161
  {name}
159
162
  </p>
160
- <p className="text-h4 text-ods-text-secondary break-all truncate">
163
+ <p className="text-h4 text-ods-text-secondary break-all truncate" title={(subtitle && subtitle.trim().length > 0) ? subtitle : (email && email.trim().length > 0 ? email : '\u00A0')}>
161
164
  {(subtitle && subtitle.trim().length > 0) ? subtitle : (email && email.trim().length > 0 ? email : '\u00A0')}
162
165
  </p>
163
- {mspPreview && (
164
- <p className="text-h6 text-ods-text-primary truncate">
165
- {/* Build string with separators */}
166
- {[
167
- mspPreview.name ?? '—',
168
- typeof mspPreview.seatCount === 'number'
169
- ? `${formatNumber(mspPreview.seatCount)} Seats`
170
- : null,
171
- typeof mspPreview.technicianCount === 'number'
172
- ? `${formatNumber(mspPreview.technicianCount)} Technicians`
173
- : null,
174
- typeof mspPreview.annualRevenue === 'number'
175
- ? `$${formatNumber(mspPreview.annualRevenue)}`
176
- : null,
177
- ]
178
- .filter(Boolean)
179
- .flatMap((txt, idx) => (idx === 0 ? [txt] : [' • ', txt]))
180
- .map((seg, idx) => (
181
- <span key={idx} className={seg === ' ' ? 'text-ods-text-secondary' : ''}>{seg}</span>
182
- ))}
183
- </p>
184
- )}
166
+ {mspPreview && (() => {
167
+ const mspSegments = [
168
+ mspPreview.name ?? '—',
169
+ typeof mspPreview.seatCount === 'number'
170
+ ? `${formatNumber(mspPreview.seatCount)} Seats`
171
+ : null,
172
+ typeof mspPreview.technicianCount === 'number'
173
+ ? `${formatNumber(mspPreview.technicianCount)} Technicians`
174
+ : null,
175
+ typeof mspPreview.annualRevenue === 'number'
176
+ ? `$${formatNumber(mspPreview.annualRevenue)}`
177
+ : null,
178
+ ].filter(Boolean) as string[];
179
+ const mspTitle = mspSegments.join(' • ');
180
+ return (
181
+ <p className="text-h6 text-ods-text-primary truncate" title={mspTitle}>
182
+ {/* Build string with separators */}
183
+ {mspSegments
184
+ .flatMap((txt, idx) => (idx === 0 ? [txt] : [' ', txt]))
185
+ .map((seg, idx) => (
186
+ <span key={idx} className={seg === ' • ' ? 'text-ods-text-secondary' : ''}>{seg}</span>
187
+ ))}
188
+ </p>
189
+ );
190
+ })()}
185
191
  </div>
186
192
 
187
193
  {/* RIGHT (desktop) */}
@@ -79,7 +79,7 @@ export function VendorDisplayButton({ vendor, onClick, variant = 'default', exte
79
79
  </span>
80
80
  </div>
81
81
  )}
82
- <span className="text-h4 text-ods-text-primary truncate min-w-0">
82
+ <span className="text-h4 text-ods-text-primary truncate min-w-0" title={vendor.title}>
83
83
  {vendor.title}
84
84
  </span>
85
85
  </button>
@@ -1,5 +1,11 @@
1
1
  import type { Meta, StoryObj } from '@storybook/nextjs-vite'
2
- import { ChevronDown, ExternalLink, MoreVertical, Save, Trash2 } from 'lucide-react'
2
+ import {
3
+ Chevron01DownIcon as ChevronDown,
4
+ Ellipsis02Icon as MoreVertical,
5
+ ExternalLinkIcon as ExternalLink,
6
+ FloppyDiscIcon as Save,
7
+ TrashIcon as Trash2,
8
+ } from '../components/icons-v2-generated'
3
9
  import React from 'react'
4
10
  import { SplitButton } from '../components/ui/button'
5
11