@handled-ai/design-system 0.18.49 → 0.18.51

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 (33) hide show
  1. package/dist/components/badge.d.ts +1 -1
  2. package/dist/components/button.d.ts +1 -1
  3. package/dist/components/pill.d.ts +1 -1
  4. package/dist/components/score-why-chips.d.ts +1 -1
  5. package/dist/components/signal-feedback-inline.d.ts +12 -28
  6. package/dist/components/signal-feedback-inline.js +10 -146
  7. package/dist/components/signal-feedback-inline.js.map +1 -1
  8. package/dist/components/signal-priority-popover.d.ts +1 -1
  9. package/dist/components/signal-priority-popover.js +16 -7
  10. package/dist/components/signal-priority-popover.js.map +1 -1
  11. package/dist/components/tabs.d.ts +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.js.map +1 -1
  14. package/dist/prototype/index.d.ts +1 -1
  15. package/dist/prototype/prototype-accounts-view.d.ts +1 -1
  16. package/dist/prototype/prototype-admin-view.d.ts +1 -1
  17. package/dist/prototype/prototype-config.d.ts +1 -1
  18. package/dist/prototype/prototype-inbox-view.d.ts +3 -3
  19. package/dist/prototype/prototype-inbox-view.js +3 -1
  20. package/dist/prototype/prototype-inbox-view.js.map +1 -1
  21. package/dist/prototype/prototype-insights-view.d.ts +1 -1
  22. package/dist/prototype/prototype-shell.d.ts +1 -1
  23. package/dist/{signal-priority-popover-QJngMAj7.d.ts → signal-priority-popover-DTedstRL.d.ts} +13 -4
  24. package/package.json +1 -1
  25. package/src/components/__tests__/case-panel-why.test.tsx +0 -126
  26. package/src/components/__tests__/signal-priority-popover.test.tsx +41 -4
  27. package/src/components/signal-feedback-inline.tsx +20 -181
  28. package/src/components/signal-priority-popover.tsx +19 -6
  29. package/src/index.ts +1 -1
  30. package/src/prototype/__tests__/detail-view-score-why.test.tsx +34 -0
  31. package/src/prototype/prototype-config.ts +7 -3
  32. package/src/prototype/prototype-inbox-view.tsx +5 -3
  33. package/src/prototype/__tests__/detail-view-opportunity-preview.test.tsx +0 -90
@@ -119,8 +119,8 @@ describe("SignalPriorityPopover", () => {
119
119
 
120
120
  // Check head section
121
121
  expect(content.textContent).toContain("Why this is high priority")
122
- expect(content.textContent).toContain("79")
123
- expect(content.textContent).toContain("/100")
122
+ expect(screen.getByTestId("priority-overall-score").textContent).toContain("79")
123
+ expect(screen.getByTestId("priority-overall-score").textContent).toContain("/100")
124
124
  expect(content.textContent).toContain("High range")
125
125
  expect(content.textContent).toContain("60-79")
126
126
  })
@@ -188,13 +188,50 @@ describe("SignalPriorityPopover", () => {
188
188
  expect(row.textContent).not.toContain("Raises0/100")
189
189
  })
190
190
 
191
- it("renders Contributing factors section label", () => {
191
+ it("renders Contributing factors section label and shared-consumer-safe default formula label", () => {
192
192
  render(<SignalPriorityPopover {...defaultProps} />)
193
193
  fireEvent.click(screen.getByTestId("priority-popover-trigger"))
194
194
 
195
195
  const content = screen.getByTestId("priority-popover-content")
196
196
  expect(content.textContent).toContain("Contributing factors")
197
- expect(content.textContent).toContain("Score = weighted sum")
197
+ expect(content.textContent).toContain("Priority factors")
198
+ expect(content.textContent).not.toContain("Score = weighted sum")
199
+ expect(content.textContent).not.toContain("Priority = weighted signals + calibration")
200
+ })
201
+
202
+ it("renders a custom formula label when provided", () => {
203
+ render(
204
+ <SignalPriorityPopover
205
+ {...defaultProps}
206
+ formulaLabel="Priority = weighted signals + calibration"
207
+ />,
208
+ )
209
+ fireEvent.click(screen.getByTestId("priority-popover-trigger"))
210
+
211
+ const content = screen.getByTestId("priority-popover-content")
212
+ expect(content.textContent).toContain("Priority = weighted signals + calibration")
213
+ })
214
+
215
+
216
+ it("renders the overall score number by default while preserving factor row scores", () => {
217
+ render(<SignalPriorityPopover {...defaultProps} />)
218
+ fireEvent.click(screen.getByTestId("priority-popover-trigger"))
219
+
220
+ expect(screen.getByTestId("priority-overall-score").textContent).toBe("79/100")
221
+ expect(screen.getByTestId("factor-row-test_severity").textContent).toContain("85/100")
222
+ expect(screen.getByTestId("factor-row-account_depth").textContent).toContain("30/100")
223
+ })
224
+
225
+ it("hides only the overall header score in label display mode", () => {
226
+ render(<SignalPriorityPopover {...defaultProps} scoreDisplay="label" />)
227
+ fireEvent.click(screen.getByTestId("priority-popover-trigger"))
228
+
229
+ const header = screen.getByTestId("priority-popover-header")
230
+ expect(screen.queryByTestId("priority-overall-score")).toBeNull()
231
+ expect(header.textContent).toContain("Why this is high priority")
232
+ expect(header.textContent).not.toContain("79/100")
233
+ expect(screen.getByTestId("factor-row-test_severity").textContent).toContain("85/100")
234
+ expect(screen.getByTestId("factor-row-account_depth").textContent).toContain("30/100")
198
235
  })
199
236
 
200
237
  it("renders score track bars with correct width percentage", () => {
@@ -73,32 +73,6 @@ const approveReasons = [
73
73
 
74
74
  type ApprovalState = "pending" | "confirming" | "creating" | "approving-feedback" | "dismissing" | "approved" | "dismissed" | "auto-approved"
75
75
 
76
- interface OpportunityPreviewOption {
77
- value: string
78
- label: string
79
- }
80
-
81
- interface OpportunityPreview {
82
- name: string
83
- stage: string
84
- closeDate: string
85
- closeDateValue?: string
86
- amount: string
87
- /** Raw draft input value. Numeric values render as currency in the editable field. */
88
- amountValue?: string | number | null
89
- accountName: string
90
- description?: string | null
91
- churnType?: string | null
92
- churnTypeOptions?: Array<string | OpportunityPreviewOption>
93
- }
94
-
95
- interface OpportunityDraft {
96
- closeDate: string
97
- amount: string
98
- description: string
99
- churnType: string
100
- }
101
-
102
76
  interface SignalApprovalLabels {
103
77
  approveButton?: string
104
78
  dismissButton?: string
@@ -132,9 +106,9 @@ interface SignalApprovalContextValue {
132
106
  labels: Required<SignalApprovalLabels>
133
107
  hideApproveButton?: boolean
134
108
  approveButtonIconUrl?: string
135
- opportunityPreview?: OpportunityPreview
109
+ opportunityPreview?: RootProps['opportunityPreview']
136
110
  requestingApproval: boolean
137
- approve: (draft?: OpportunityDraft) => void
111
+ approve: () => void
138
112
  submitApproveFeedback: (reasons: string[], detail: string) => void
139
113
  skipApproveFeedback: () => void
140
114
  dismiss: (reasons: string[], detail: string, subReason?: string) => void
@@ -163,7 +137,13 @@ interface RootProps {
163
137
  /** Optional icon URL for the approve button. Renders an img instead of CirclePlus when provided. */
164
138
  approveButtonIconUrl?: string
165
139
  /** Optional structured preview data shown in the confirmation dialog. */
166
- opportunityPreview?: OpportunityPreview
140
+ opportunityPreview?: {
141
+ name: string
142
+ stage: string
143
+ closeDate: string
144
+ amount: string
145
+ accountName: string
146
+ }
167
147
  /**
168
148
  * Async callback fired when the user clicks the approve button, BEFORE
169
149
  * transitioning to the "confirming" state. While the promise is pending,
@@ -180,7 +160,7 @@ interface RootProps {
180
160
  * "creating" loading state while the promise is pending. On `true` it
181
161
  * transitions to the feedback step; on `false` it reverts to "pending".
182
162
  */
183
- onApprove?: (draft?: OpportunityDraft) => void | Promise<boolean>
163
+ onApprove?: () => void | Promise<boolean>
184
164
  onApproveFeedback?: (reasons: string[], detail: string) => void
185
165
  onDismiss?: (reasons: string[], detail: string, subReason?: string) => void
186
166
  }
@@ -194,7 +174,6 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
194
174
  // an async onApprove promise is still in flight).
195
175
  const mountedRef = React.useRef(true)
196
176
  React.useEffect(() => {
197
- mountedRef.current = true
198
177
  return () => { mountedRef.current = false }
199
178
  }, [])
200
179
 
@@ -226,8 +205,8 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
226
205
  setApprovalState("pending")
227
206
  }, [])
228
207
 
229
- const approve = React.useCallback((draft?: OpportunityDraft) => {
230
- const result = onApprove?.(draft)
208
+ const approve = React.useCallback(() => {
209
+ const result = onApprove?.()
231
210
  // If the callback returns a Promise, show a loading state and wait for it.
232
211
  if (result && typeof (result as Promise<boolean>).then === "function") {
233
212
  setApprovalState("creating")
@@ -438,49 +417,6 @@ function SubmittedFeedback({
438
417
  )
439
418
  }
440
419
 
441
- function optionValue(option: string | OpportunityPreviewOption): string {
442
- return typeof option === "string" ? option : option.value
443
- }
444
-
445
- function optionLabel(option: string | OpportunityPreviewOption): string {
446
- return typeof option === "string" ? option : option.label
447
- }
448
-
449
- function formatAmountDraftValue(value: string | number | null | undefined): string {
450
- if (value == null || value === "") return ""
451
- if (typeof value === "number") {
452
- return new Intl.NumberFormat("en-US", {
453
- style: "currency",
454
- currency: "USD",
455
- maximumFractionDigits: 0,
456
- }).format(value)
457
- }
458
- return value
459
- }
460
-
461
- function buildOpportunityDraft(preview?: OpportunityPreview): OpportunityDraft {
462
- return {
463
- closeDate: preview?.closeDateValue ?? preview?.closeDate ?? "",
464
- amount: preview?.amountValue === undefined
465
- ? preview?.amount ?? ""
466
- : formatAmountDraftValue(preview.amountValue),
467
- description: preview?.description ?? "",
468
- churnType: preview?.churnType ?? "",
469
- }
470
- }
471
-
472
- function hasEditableOpportunityPreview(preview?: OpportunityPreview): boolean {
473
- return !!preview && isValidDateInput(preview.closeDateValue ?? preview.closeDate)
474
- }
475
-
476
- function isValidDateInput(value: string): boolean {
477
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
478
- if (!match) return false
479
- const parsed = new Date(`${value}T00:00:00Z`)
480
- if (Number.isNaN(parsed.getTime())) return false
481
- return parsed.toISOString().slice(0, 10) === value
482
- }
483
-
484
420
  function Actions() {
485
421
  const { approvalState, companyName, opportunityUrl, scheduledTime, labels, hideApproveButton, approveButtonIconUrl, opportunityPreview, requestingApproval, approve, submitApproveFeedback, skipApproveFeedback, dismiss, requestApproval, requestDismiss, cancel } =
486
422
  useSignalApproval()
@@ -490,16 +426,6 @@ function Actions() {
490
426
  const [detailText, setDetailText] = React.useState("")
491
427
  const [submittedFeedback, setSubmittedFeedback] = React.useState<{ reasons: string[]; detail: string; subReason?: string } | null>(null)
492
428
  const [isEditing, setIsEditing] = React.useState(false)
493
- const [opportunityDraft, setOpportunityDraft] = React.useState<OpportunityDraft>(() => buildOpportunityDraft(opportunityPreview))
494
-
495
- React.useEffect(() => {
496
- if (approvalState === "confirming") {
497
- setOpportunityDraft(buildOpportunityDraft(opportunityPreview))
498
- }
499
- }, [approvalState, opportunityPreview])
500
-
501
- const churnTypeOptions = opportunityPreview?.churnTypeOptions ?? []
502
- const hasChurnTypeOptions = churnTypeOptions.length > 0
503
429
 
504
430
  const topNode = dismissReasonTree.find((n) => n.label === selectedTopReason)
505
431
  const hasSubOptions = !!(topNode?.subOptions && topNode.subOptions.length > 0)
@@ -512,8 +438,6 @@ function Actions() {
512
438
  (!needsText || detailText.trim().length > 0)
513
439
 
514
440
  const canSubmitApprove = selectedReasons.length > 0 || detailText.trim().length > 0
515
- const isEditableOpportunityPreview = hasEditableOpportunityPreview(opportunityPreview)
516
- const canConfirmOpportunity = !isEditableOpportunityPreview || isValidDateInput(opportunityDraft.closeDate)
517
441
 
518
442
  const selectTopReason = (label: string) => {
519
443
  if (selectedTopReason === label) {
@@ -815,8 +739,8 @@ function Actions() {
815
739
  <p className="text-sm text-foreground">
816
740
  {labels.confirmPrompt} <strong>{companyName}</strong>. Confirm?
817
741
  </p>
818
- {opportunityPreview && !isEditableOpportunityPreview && (
819
- <div className="mt-3 space-y-2 border-t border-border/50 pt-3">
742
+ {opportunityPreview && (
743
+ <div className="mt-3 space-y-1.5 border-t border-border/50 pt-3">
820
744
  {[
821
745
  { label: "Opportunity", value: opportunityPreview.name },
822
746
  { label: "Account", value: opportunityPreview.accountName },
@@ -824,105 +748,19 @@ function Actions() {
824
748
  { label: "Close Date", value: opportunityPreview.closeDate },
825
749
  { label: "Amount", value: opportunityPreview.amount },
826
750
  ].map(({ label, value }) => (
827
- <div key={label} className="flex items-center justify-between gap-3 text-xs">
828
- <span className="text-muted-foreground">{label}</span>
829
- <span className="text-right font-medium text-foreground">{value}</span>
830
- </div>
831
- ))}
832
- </div>
833
- )}
834
- {opportunityPreview && isEditableOpportunityPreview && (
835
- <div className="mt-3 space-y-3 border-t border-border/50 pt-3">
836
- {[
837
- { label: "Opportunity", value: opportunityPreview.name },
838
- { label: "Account", value: opportunityPreview.accountName },
839
- { label: "Stage", value: opportunityPreview.stage },
840
- ].map(({ label, value }) => (
841
- <div key={label} className="flex items-center justify-between gap-3 text-xs">
751
+ <div key={label} className="flex items-center justify-between text-xs">
842
752
  <span className="text-muted-foreground">{label}</span>
843
- <span className="text-right font-medium text-foreground">{value}</span>
753
+ <span className="font-medium text-foreground">{value}</span>
844
754
  </div>
845
755
  ))}
846
-
847
- <div className="grid gap-2 sm:grid-cols-2">
848
- <label className="space-y-1 text-xs">
849
- <span className="font-medium text-muted-foreground">Close Date</span>
850
- <input
851
- type="date"
852
- value={opportunityDraft.closeDate}
853
- onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, closeDate: event.target.value }))}
854
- aria-invalid={!canConfirmOpportunity}
855
- className="h-8 w-full rounded-md border border-border bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
856
- />
857
- {!canConfirmOpportunity && (
858
- <span className="text-[11px] text-red-600">Enter a valid close date.</span>
859
- )}
860
- </label>
861
-
862
- <label className="space-y-1 text-xs">
863
- <span className="font-medium text-muted-foreground">Amount</span>
864
- <input
865
- type="text"
866
- inputMode="decimal"
867
- value={opportunityDraft.amount}
868
- onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, amount: event.target.value }))}
869
- placeholder={opportunityPreview.amount}
870
- className="h-8 w-full rounded-md border border-border bg-background px-2 text-xs text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-ring"
871
- />
872
- </label>
873
- </div>
874
-
875
- <label className="space-y-1 text-xs">
876
- <span className="font-medium text-muted-foreground">Churn Type</span>
877
- {hasChurnTypeOptions ? (
878
- <select
879
- value={opportunityDraft.churnType}
880
- onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, churnType: event.target.value }))}
881
- className="h-8 w-full rounded-md border border-border bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
882
- >
883
- <option value="">No churn type</option>
884
- {churnTypeOptions.map((option) => (
885
- <option key={optionValue(option)} value={optionValue(option)}>
886
- {optionLabel(option)}
887
- </option>
888
- ))}
889
- </select>
890
- ) : (
891
- <input
892
- type="text"
893
- value={opportunityDraft.churnType}
894
- onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, churnType: event.target.value }))}
895
- placeholder="No churn type"
896
- className="h-8 w-full rounded-md border border-border bg-background px-2 text-xs text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-ring"
897
- />
898
- )}
899
- </label>
900
-
901
- <label className="space-y-1 text-xs">
902
- <span className="font-medium text-muted-foreground">Description</span>
903
- <textarea
904
- value={opportunityDraft.description}
905
- onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, description: event.target.value }))}
906
- rows={3}
907
- placeholder="Add a short description"
908
- className="w-full resize-none rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-ring"
909
- />
910
- </label>
911
756
  </div>
912
757
  )}
913
758
  </div>
914
759
  <div className="flex items-center gap-2">
915
760
  <button
916
761
  type="button"
917
- onClick={() => {
918
- if (!opportunityPreview) {
919
- approve()
920
- return
921
- }
922
- approve(isEditableOpportunityPreview ? opportunityDraft : undefined)
923
- }}
924
- disabled={!canConfirmOpportunity}
925
- className="inline-flex h-7 items-center gap-1.5 rounded-md bg-foreground px-3 text-xs font-semibold text-background transition-colors hover:bg-foreground/90 disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground"
762
+ onClick={approve}
763
+ className="inline-flex h-7 items-center gap-1.5 rounded-md bg-foreground px-3 text-xs font-semibold text-background transition-colors hover:bg-foreground/90"
926
764
  >
927
765
  <Check className="h-3 w-3" />
928
766
  Confirm
@@ -1023,4 +861,5 @@ export {
1023
861
  Gate as SignalApprovalGate,
1024
862
  }
1025
863
  export const SignalApproval = { Root, Actions, Gate }
1026
- export type { ApprovalState, OpportunityPreview, OpportunityDraft, SignalApprovalLabels, SignalApprovalContextValue, RootProps as SignalApprovalRootProps }
864
+ export type OpportunityPreview = NonNullable<RootProps['opportunityPreview']>
865
+ export type { ApprovalState, SignalApprovalLabels, SignalApprovalContextValue, RootProps as SignalApprovalRootProps }
@@ -50,8 +50,14 @@ export interface PriorityFactor {
50
50
  rationale: string
51
51
  }
52
52
 
53
+ export type SignalPriorityScoreDisplay = "label" | "number"
54
+
53
55
  export interface SignalPriorityPopoverProps {
54
56
  score: number
57
+ /** Controls whether the overall score number is shown in the popover header. @default "number" */
58
+ scoreDisplay?: SignalPriorityScoreDisplay
59
+ /** Short formula/context label shown beside the contributing factors heading. @default "Priority factors" */
60
+ formulaLabel?: string
55
61
  urgencyLabel?: SignalScoreUrgencyLabel
56
62
  /** Synthesis sentence displayed in the popover head. */
57
63
  urgencyExplanation?: string
@@ -282,6 +288,8 @@ export function SignalPriorityPopover({
282
288
  initialFactorFeedback,
283
289
  onFactorFeedback,
284
290
  initialPriorityFeedback,
291
+ scoreDisplay = "number",
292
+ formulaLabel = "Priority factors",
285
293
  }: SignalPriorityPopoverProps) {
286
294
  const urgencyLabel = providedLabel ?? getUrgencyLevel(score)
287
295
  const scoreRange = getUrgencyRange(urgencyLabel)
@@ -330,15 +338,20 @@ export function SignalPriorityPopover({
330
338
  data-testid="priority-popover-content"
331
339
  >
332
340
  {/* Head section */}
333
- <div className="p-4 pb-3">
341
+ <div className="p-4 pb-3" data-testid="priority-popover-header">
334
342
  <div className="flex items-start justify-between gap-3">
335
343
  <p className="text-sm font-semibold text-foreground">
336
344
  Why this is {urgencyLabel.toLowerCase()} priority
337
345
  </p>
338
- <span className="text-2xl font-bold tabular-nums text-foreground">
339
- {score}
340
- <span className="text-sm font-normal text-muted-foreground">/100</span>
341
- </span>
346
+ {scoreDisplay === "number" && (
347
+ <span
348
+ className="text-2xl font-bold tabular-nums text-foreground"
349
+ data-testid="priority-overall-score"
350
+ >
351
+ {score}
352
+ <span className="text-sm font-normal text-muted-foreground">/100</span>
353
+ </span>
354
+ )}
342
355
  </div>
343
356
 
344
357
  {/* Band indicator */}
@@ -369,7 +382,7 @@ export function SignalPriorityPopover({
369
382
  </span>
370
383
  <span className="flex items-center gap-1 text-[10px] text-muted-foreground">
371
384
  <Info className="h-3 w-3" />
372
- Score = weighted sum
385
+ {formulaLabel}
373
386
  </span>
374
387
  </div>
375
388
 
package/src/index.ts CHANGED
@@ -50,7 +50,7 @@ export * from "./components/related-record-action-card"
50
50
  export { FeedbackFooter, FeedbackChipGroup, FeedbackInput, FeedbackActions, InlineFeedbackControl } from "./components/feedback-primitives"
51
51
  export type { FeedbackFooterProps, FeedbackChipTree, FeedbackChipGroupProps, FeedbackInputProps, FeedbackActionsProps, FeedbackSubmitData, PersistedFeedbackData, InlineFeedbackControlProps } from "./components/feedback-primitives"
52
52
  export { SignalPriorityPopover } from "./components/signal-priority-popover"
53
- export type { SignalPriorityPopoverProps, PriorityFactor } from "./components/signal-priority-popover"
53
+ export type { SignalPriorityPopoverProps, SignalPriorityScoreDisplay, PriorityFactor } from "./components/signal-priority-popover"
54
54
  export * from "./components/filter-chip"
55
55
  export * from "./components/inbox-row"
56
56
  export * from "./components/inbox-toolbar"
@@ -89,6 +89,40 @@ describe("DetailView corrected compact score WHY UX", () => {
89
89
  expect(screen.queryByText("How's this score?")).toBeNull();
90
90
  });
91
91
 
92
+
93
+ it("passes priorityScoreDisplay and priorityFormulaLabel through to the priority popover", () => {
94
+ render(
95
+ <DetailView
96
+ {...baseProps({
97
+ getSignalScore: () =>
98
+ makeSignalScore({
99
+ urgencyLabel: "High",
100
+ priorityScoreDisplay: "label",
101
+ priorityFormulaLabel: "Priority = weighted signals + calibration",
102
+ priorityFactors: [
103
+ {
104
+ key: "treasury",
105
+ label: "Treasury activity",
106
+ icon: "wallet",
107
+ tone: "alert",
108
+ direction: "raises",
109
+ score: 85,
110
+ rationale: "Full liquidation detected.",
111
+ },
112
+ ],
113
+ }),
114
+ })}
115
+ />,
116
+ );
117
+
118
+ fireEvent.click(screen.getByRole("button", { name: /high priority/i }));
119
+
120
+ expect(screen.queryByTestId("priority-overall-score")).toBeNull();
121
+ expect(screen.getByTestId("priority-popover-header").textContent).not.toContain("82/100");
122
+ expect(screen.getByText("Priority = weighted signals + calibration")).toBeInTheDocument();
123
+ expect(screen.getByTestId("factor-row-treasury").textContent).toContain("85/100");
124
+ });
125
+
92
126
  it("keeps signal WHY chips collapsed by default and excludes factor-only buckets", () => {
93
127
  render(
94
128
  <DetailView
@@ -13,9 +13,9 @@ import type {
13
13
  PipelineStageTiming,
14
14
  } from "../charts/pipeline-overview"
15
15
  import type { TimelineEvent } from "../components/timeline-activity"
16
- import type { ApprovalState, OpportunityDraft } from "../components/signal-feedback-inline"
16
+ import type { ApprovalState } from "../components/signal-feedback-inline"
17
17
  import type { LucideIcon } from "lucide-react"
18
- import type { PriorityFactor } from "../components/signal-priority-popover"
18
+ import type { PriorityFactor, SignalPriorityScoreDisplay } from "../components/signal-priority-popover"
19
19
  import type { FeedbackChipTree, FeedbackSubmitData, PersistedFeedbackData } from "../components/feedback-primitives"
20
20
 
21
21
  // ---------------------------------------------------------------------------
@@ -109,6 +109,10 @@ export interface SignalScoreData {
109
109
  confidence: number
110
110
  urgencyLabel?: SignalScoreUrgencyLabel
111
111
  urgencyExplanation?: string
112
+ /** Controls whether the priority popover header shows the raw overall score number. @default "number" */
113
+ priorityScoreDisplay?: SignalPriorityScoreDisplay
114
+ /** Formula/context label shown in the priority popover factor section. @default "Priority factors" */
115
+ priorityFormulaLabel?: string
112
116
  explanationBuckets?: SignalScoreExplanationBucket[]
113
117
  onFactorFeedback?: (factorKey: string, type: "up" | "down" | null, detail?: string) => void
114
118
  /** @deprecated The compact score UX no longer renders score-level thumbs by default. */
@@ -180,7 +184,7 @@ export interface InboxViewConfig {
180
184
  quickFilterTabs?: Array<{ id: string; label: string; matchValue?: string; count?: number }>
181
185
  hideAccountsButton?: boolean
182
186
  accountDetailsLabel?: string
183
- onSignalApprove?: (item: QueueItem, draft?: OpportunityDraft) => void | Promise<boolean>
187
+ onSignalApprove?: (item: QueueItem) => void | Promise<boolean>
184
188
  getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined
185
189
  signalLabels?: {
186
190
  approveButton?: string
@@ -38,7 +38,7 @@ import {
38
38
  type InboxFilterCategory,
39
39
  } from "../components/inbox-toolbar"
40
40
  import { GroupedListView, type GroupedListGroup } from "../components/item-list"
41
- import { SignalApproval, type ApprovalState, type OpportunityDraft, type OpportunityPreview } from "../components/signal-feedback-inline"
41
+ import { SignalApproval, type ApprovalState, type OpportunityPreview } from "../components/signal-feedback-inline"
42
42
  import { ScoreWhyChips } from "../components/score-why-chips"
43
43
  import { SignalPriorityPopover } from "../components/signal-priority-popover"
44
44
  import { type SourceDef } from "../components/detail-view"
@@ -149,7 +149,7 @@ export interface DetailViewProps {
149
149
  onSuggestedActionFeedback?: (actionId: number | string, feedback: string, actionTitle?: string) => void
150
150
  /** @deprecated The compact score UX no longer renders score-level thumbs by default. */
151
151
  onScoreFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
152
- onSignalApprove?: (item: QueueItem, draft?: OpportunityDraft) => void | Promise<boolean>
152
+ onSignalApprove?: (item: QueueItem) => void | Promise<boolean>
153
153
  getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined
154
154
  signalLabels?: InboxViewConfig["signalLabels"]
155
155
  hideApproveButton?: boolean
@@ -475,7 +475,7 @@ export function DetailView({
475
475
  opportunityPreview={opportunityPreview}
476
476
  onRequestApproval={onRequestApproval}
477
477
  initialApprovalState={getSignalApprovalState?.(item)}
478
- onApprove={(draft) => onSignalApprove?.(item, draft)}
478
+ onApprove={() => onSignalApprove?.(item)}
479
479
  onApproveFeedback={(reasons, detail) => {
480
480
  signalData.onApproveFeedback?.(reasons, detail)
481
481
  console.log("Approval feedback:", { taskId: item.id, company: item.company, reasons, detail })
@@ -519,6 +519,8 @@ export function DetailView({
519
519
  score={signalData.score}
520
520
  urgencyLabel={signalData.urgencyLabel}
521
521
  urgencyExplanation={signalData.urgencyExplanation ?? signalData.signalBrief}
522
+ scoreDisplay={signalData.priorityScoreDisplay}
523
+ formulaLabel={signalData.priorityFormulaLabel}
522
524
  factors={signalData.priorityFactors ?? []}
523
525
  metaText={undefined}
524
526
  feedbackChips={signalData.priorityFeedbackChips}
@@ -1,90 +0,0 @@
1
- import React from "react"
2
- import { describe, expect, it } from "vitest"
3
- import { fireEvent, render, screen } from "@testing-library/react"
4
-
5
- import { DetailView, type DetailViewProps } from "../prototype-inbox-view"
6
- import type { QueueItem, SignalScoreData } from "../prototype-config"
7
-
8
- const baseItem: QueueItem = {
9
- id: "case-1",
10
- title: "WIT-825 Opportunity Preview Fixture",
11
- details: "Some details",
12
- statusColor: "green",
13
- time: "2h ago",
14
- company: "WIT-825 Fixture Account",
15
- tag1: "churn_risk",
16
- }
17
-
18
- function makeSignalScore(overrides: Partial<SignalScoreData> = {}): SignalScoreData {
19
- return {
20
- score: 82,
21
- factors: [],
22
- whyNow: "Strong signals detected.",
23
- evidence: ["Evidence line 1"],
24
- confidence: 80,
25
- signalBrief: "Signals indicate a potential opportunity.",
26
- ...overrides,
27
- }
28
- }
29
-
30
- function baseProps(overrides: Partial<DetailViewProps> = {}): DetailViewProps {
31
- return {
32
- item: baseItem,
33
- sections: { signalBrief: true, suggestedActions: false, timeline: false },
34
- getSignalScore: () => makeSignalScore(),
35
- buildSuggestedActions: () => [],
36
- buildSourceItems: () => [],
37
- accountContacts: [],
38
- emailSignature: "",
39
- iconMap: {},
40
- signalLabels: {
41
- approveButton: "Create Opportunity",
42
- dismissButton: "Not Helpful",
43
- },
44
- ...overrides,
45
- }
46
- }
47
-
48
- describe("DetailView opportunity approval preview", () => {
49
- it("keeps the approval flow mounted and shows editable preview fields after async request approval in StrictMode", async () => {
50
- function DetailViewPreviewHarness() {
51
- const [opportunityPreview, setOpportunityPreview] = React.useState<DetailViewProps["opportunityPreview"]>()
52
-
53
- return (
54
- <DetailView
55
- {...baseProps({
56
- opportunityPreview,
57
- onRequestApproval: async () => {
58
- setOpportunityPreview({
59
- name: "Churn Risk - WIT-825 Fixture Account",
60
- accountName: "WIT-825 Fixture Account",
61
- stage: "Prospecting",
62
- closeDate: "Jun 30, 2026",
63
- closeDateValue: "2026-06-30",
64
- amount: "$75,000",
65
- amountValue: 75000,
66
- description: "Initial description",
67
- churnType: "Churn Risk",
68
- churnTypeOptions: ["Churn Risk", "Win Back"],
69
- })
70
- },
71
- })}
72
- />
73
- )
74
- }
75
-
76
- render(
77
- <React.StrictMode>
78
- <DetailViewPreviewHarness />
79
- </React.StrictMode>,
80
- )
81
-
82
- fireEvent.click(screen.getByRole("button", { name: /create opportunity/i }))
83
-
84
- expect((await screen.findByLabelText("Close Date") as HTMLInputElement).value).toBe("2026-06-30")
85
- expect((screen.getByLabelText("Amount") as HTMLInputElement).value).toBe("$75,000")
86
- expect((screen.getByLabelText("Churn Type") as HTMLSelectElement).value).toBe("Churn Risk")
87
- expect((screen.getByLabelText("Description") as HTMLTextAreaElement).value).toBe("Initial description")
88
- expect((screen.getByRole("button", { name: /confirm/i }) as HTMLButtonElement).disabled).toBe(false)
89
- })
90
- })