@handled-ai/design-system 0.16.2 → 0.17.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.
@@ -38,9 +38,8 @@ import {
38
38
  } from "../components/inbox-toolbar"
39
39
  import { GroupedListView, type GroupedListGroup } from "../components/item-list"
40
40
  import { SignalApproval, type ApprovalState, type OpportunityPreview } from "../components/signal-feedback-inline"
41
- import { ScoreFeedback } from "../components/score-feedback"
42
- import { ScoreBreakdown } from "../components/score-breakdown"
43
- import { Citation, type SourceDef } from "../components/detail-view"
41
+ import { ScoreWhyChips, SignalPriorityChip, SignalPriorityPanel } from "../components/score-why-chips"
42
+ import { type SourceDef } from "../components/detail-view"
44
43
  import {
45
44
  SuggestedActions,
46
45
  type SuggestedAction,
@@ -124,6 +123,8 @@ export interface DetailViewProps {
124
123
  onOpenEntityPanel?: () => void
125
124
  onOpenRecentActivity?: () => void
126
125
  onSuggestedActionFeedback?: (actionId: number | string, feedback: string, actionTitle?: string) => void
126
+ /** @deprecated The compact score UX no longer renders score-level thumbs by default. */
127
+ onScoreFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
127
128
  onSignalApprove?: (item: QueueItem) => void | Promise<boolean>
128
129
  getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined
129
130
  signalLabels?: InboxViewConfig["signalLabels"]
@@ -136,9 +137,13 @@ export interface DetailViewProps {
136
137
  /** Render content between the signal score section and the activity timeline. */
137
138
  renderAfterScore?: (item: QueueItem) => React.ReactNode
138
139
  lastActivityTime?: string
140
+ /** Render extra content inline with the detail title. */
141
+ renderTitleExtra?: (item: QueueItem) => React.ReactNode
142
+ /** Render supporting content below the detail title. */
143
+ renderTitleSubtext?: (item: QueueItem) => React.ReactNode
139
144
  /** Render extra metadata chips (e.g. assignee) inside the chips row below the title. */
140
145
  renderMetadataExtra?: (item: QueueItem) => React.ReactNode
141
- onScoreFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
146
+ onOpenSignalBucket?: (args: { item: QueueItem; bucketKey: string; signalId: string }) => void
142
147
  approveButtonIconUrl?: string
143
148
  opportunityPreview?: OpportunityPreview
144
149
  onRequestApproval?: () => Promise<void>
@@ -151,7 +156,7 @@ export function DetailView({
151
156
  sections,
152
157
  getSignalScore,
153
158
  buildSuggestedActions,
154
- buildSourceItems,
159
+ buildSourceItems: _buildSourceItems,
155
160
  getTimelineEvents,
156
161
  accountContacts,
157
162
  emailSignature,
@@ -159,6 +164,7 @@ export function DetailView({
159
164
  onOpenEntityPanel,
160
165
  onOpenRecentActivity,
161
166
  onSuggestedActionFeedback: _onSuggestedActionFeedback,
167
+ onScoreFeedback: _onScoreFeedback,
162
168
  onSignalApprove,
163
169
  getSignalApprovalState,
164
170
  signalLabels,
@@ -169,33 +175,35 @@ export function DetailView({
169
175
  renderBeforeScore,
170
176
  renderAfterScore,
171
177
  lastActivityTime,
178
+ renderTitleExtra,
179
+ renderTitleSubtext,
172
180
  renderMetadataExtra,
173
- onScoreFeedback,
181
+ onOpenSignalBucket,
174
182
  approveButtonIconUrl,
175
183
  opportunityPreview,
176
184
  onRequestApproval,
177
185
  attentionCount,
178
186
  }: DetailViewProps) {
179
- const [evidenceExpanded, setEvidenceExpanded] = React.useState(false)
180
187
  const [showTimeline, setShowTimeline] = React.useState(false)
181
188
  const [extraActions, setExtraActions] = React.useState<SuggestedAction[]>([])
189
+ const [priorityOpen, setPriorityOpen] = React.useState(false)
190
+ const priorityPanelId = React.useId()
182
191
 
183
192
  React.useEffect(() => {
184
193
  setShowTimeline(false)
185
- setEvidenceExpanded(false)
186
194
  setExtraActions([])
195
+ setPriorityOpen(false)
187
196
  }, [item.id])
188
197
 
189
198
  const signalData = React.useMemo(
190
199
  () => getSignalScore(item.company, item),
191
- [getSignalScore, item.company, item],
200
+ [getSignalScore, item],
192
201
  )
193
202
 
194
203
  const suggestedActions = React.useMemo(
195
204
  () => [...buildSuggestedActions(item), ...extraActions],
196
205
  [buildSuggestedActions, item, extraActions],
197
206
  )
198
- const sourceItems = React.useMemo(() => buildSourceItems(item), [buildSourceItems, item])
199
207
  const timelineEvents = React.useMemo(
200
208
  () => getTimelineEvents?.(item) ?? [],
201
209
  [getTimelineEvents, item],
@@ -249,21 +257,22 @@ export function DetailView({
249
257
  <span className="text-xs text-muted-foreground">{item.company}</span>
250
258
  </div>
251
259
 
252
- <h1 className="mb-3 text-2xl font-bold tracking-tight text-foreground">{item.title}</h1>
260
+ <div className="mb-3 flex flex-wrap items-start gap-x-3 gap-y-2">
261
+ <div className="min-w-0 flex-1">
262
+ <h1 className="text-2xl font-bold tracking-tight text-foreground">{item.title}</h1>
263
+ {renderTitleSubtext?.(item)}
264
+ </div>
265
+ {renderTitleExtra?.(item)}
266
+ </div>
253
267
 
254
268
  <div className="mb-6 flex flex-wrap items-center gap-2">
255
- {(item.statusColor === "red" || item.statusColor === "orange") && (
256
- <div className={`inline-flex items-center gap-1 rounded-md px-2.5 py-1 text-xs font-semibold ${
257
- item.statusColor === "red"
258
- ? "bg-red-50 text-red-700"
259
- : "bg-orange-50 text-orange-700"
260
- }`}>
261
- <span className="text-[10px] font-bold">!</span> {item.tag1.charAt(0).toUpperCase() + item.tag1.slice(1)}
262
- </div>
263
- )}
264
- <div className="inline-flex items-center gap-1 rounded-md bg-muted px-2.5 py-1 text-xs font-medium text-muted-foreground">
265
- {item.tag1}
266
- </div>
269
+ <SignalPriorityChip
270
+ score={signalData.score}
271
+ urgencyLabel={signalData.urgencyLabel}
272
+ isOpen={priorityOpen}
273
+ controlsId={priorityPanelId}
274
+ onClick={() => setPriorityOpen((prev) => !prev)}
275
+ />
267
276
  {signalData.timeChipLabel && (
268
277
  <Badge variant="outline" title={signalData.timeChipDetail ?? undefined}>
269
278
  {signalData.timeChipLabel}
@@ -283,37 +292,10 @@ export function DetailView({
283
292
  {renderMetadataExtra?.(item)}
284
293
  </div>
285
294
 
295
+ {priorityOpen && <SignalPriorityPanel id={priorityPanelId} signalData={signalData} className="mb-6" />}
296
+
286
297
  {/* Signal Brief */}
287
298
  {sections.signalBrief && (() => {
288
- const pct = signalData.score
289
- const scoreColor = pct >= 70 ? "text-emerald-600" : pct >= 40 ? "text-amber-600" : "text-red-600"
290
- const barColor = pct >= 70 ? "bg-emerald-500" : pct >= 40 ? "bg-amber-500" : "bg-red-500"
291
- const scoreLabel = pct >= 70 ? "HIGH" : pct >= 40 ? "MEDIUM" : "LOW"
292
-
293
- const evidenceWithCitations: React.ReactNode[] =
294
- sourceItems.length >= 4
295
- ? [
296
- <>
297
- There are <span className="font-medium text-foreground">3 unusual signals</span> including a large balance
298
- outflow and reduced login frequency
299
- <Citation number={1} source={sourceItems[0]} />
300
- <Citation number={2} source={sourceItems[1]} />
301
- <Citation number={3} source={sourceItems[2]} />
302
- </>,
303
- <>
304
- Scott mentioned in <span className="font-medium text-foreground">#treasury-questions</span> that they are actively
305
- looking for treasury management options
306
- <Citation number={4} source={sourceItems[2]} />
307
- </>,
308
- <>
309
- You have a recent email thread regarding optimization options that hasn&apos;t been replied to
310
- <Citation number={5} source={sourceItems[3]} />
311
- </>,
312
- ]
313
- : signalData.evidence.map((ev, i) => (
314
- <span key={i}>{ev}</span>
315
- ))
316
-
317
299
  const briefHeading = signalBriefCopy?.heading ?? "Signal brief"
318
300
  const introOpt = signalBriefCopy?.intro
319
301
  const briefIntro =
@@ -355,59 +337,14 @@ export function DetailView({
355
337
  {/* Before-score content slot (e.g. "Signals on Case" chips) */}
356
338
  {renderBeforeScore?.(item)}
357
339
 
358
- <ScoreFeedback.Root
359
- onSubmitFeedback={(type, pills, detail) => (signalData.onScoreFeedback ?? onScoreFeedback)?.(type, pills, detail)}
360
- initialFeedback={signalData.initialScoreFeedback}
361
- >
362
- <div className="mb-5 rounded-md border border-border bg-muted/20 p-3">
363
- <div className="flex items-center justify-between mb-1.5">
364
- <span className="text-[10px] font-bold text-muted-foreground uppercase tracking-wider">Signal Score</span>
365
- <div className="flex items-center gap-2">
366
- <span className="text-sm font-bold text-foreground">{signalData.score}/100</span>
367
- <span className={`text-[10px] font-bold uppercase ${scoreColor}`}>{scoreLabel}</span>
368
- <ScoreFeedback.Trigger />
369
- </div>
370
- </div>
371
- <ScoreFeedback.Panel />
372
- <div className="h-1.5 bg-muted rounded-full overflow-hidden mb-2">
373
- <div
374
- className={`h-full rounded-full transition-all duration-500 ${barColor}`}
375
- style={{ width: `${signalData.score}%` }}
376
- />
377
- </div>
378
- <button
379
- type="button"
380
- onClick={() => setEvidenceExpanded((prev) => !prev)}
381
- className="flex items-center gap-1 text-[11px] font-medium text-muted-foreground hover:text-foreground transition-colors"
382
- >
383
- <ChevronDown className={`h-3 w-3 transition-transform duration-200 ${evidenceExpanded ? "rotate-180" : ""}`} />
384
- View more
385
- </button>
386
-
387
- {evidenceExpanded && (
388
- <div className="mt-3 space-y-3">
389
- <ul className="space-y-2">
390
- {evidenceWithCitations.map((ev, index) => (
391
- <li key={index} className="flex items-start gap-2 text-sm">
392
- <div className="w-1.5 h-1.5 bg-primary rounded-full mt-2 flex-shrink-0" />
393
- <span className="text-muted-foreground leading-relaxed">{ev}</span>
394
- </li>
395
- ))}
396
- </ul>
397
- <ScoreBreakdown
398
- factors={signalData.factors}
399
- onFactorFeedback={signalData.onFactorFeedback ?? ((key, type, detail) =>
400
- console.log("Signal factor feedback:", { company: item.company, factor: key, type, detail })
401
- )}
402
- initialFeedback={signalData.initialFactorFeedback}
403
- />
404
- <SignalApproval.Actions />
405
- </div>
406
- )}
340
+ <ScoreWhyChips
341
+ item={item}
342
+ signalData={signalData}
343
+ onOpenSignalBucket={onOpenSignalBucket}
344
+ />
345
+ <div className="mt-4">
346
+ <SignalApproval.Actions />
407
347
  </div>
408
- </ScoreFeedback.Root>
409
-
410
- {!evidenceExpanded && <SignalApproval.Actions />}
411
348
  </div>
412
349
  )
413
350
  })()}
@@ -495,6 +432,7 @@ export function PrototypeInboxView({
495
432
  hideHoverActions,
496
433
  onSuggestedActionFeedback,
497
434
  onScoreFeedback,
435
+ onOpenSignalBucket,
498
436
  headerActions,
499
437
  onOpenEntityPanel,
500
438
  onOpenRecentActivity,
@@ -514,6 +452,8 @@ export function PrototypeInboxView({
514
452
  renderBeforeScore,
515
453
  renderAfterScore,
516
454
  lastActivityTime,
455
+ renderTitleExtra,
456
+ renderTitleSubtext,
517
457
  sortOptions,
518
458
  activeSortId,
519
459
  onSortChange,
@@ -741,6 +681,7 @@ export function PrototypeInboxView({
741
681
  onOpenEntityPanel,
742
682
  onOpenRecentActivity,
743
683
  onSuggestedActionFeedback,
684
+ onScoreFeedback,
744
685
  onSignalApprove,
745
686
  getSignalApprovalState,
746
687
  signalLabels,
@@ -751,7 +692,9 @@ export function PrototypeInboxView({
751
692
  renderBeforeScore,
752
693
  renderAfterScore,
753
694
  lastActivityTime,
754
- onScoreFeedback,
695
+ renderTitleExtra,
696
+ renderTitleSubtext,
697
+ onOpenSignalBucket,
755
698
  }
756
699
 
757
700
  return (