@handled-ai/design-system 0.18.48 → 0.18.50

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.
@@ -10,7 +10,7 @@ import { DataRow } from './components/data-table.js';
10
10
  import { MetricCardProps } from './components/metric-card.js';
11
11
  import { PipelineStage, PipelineStageMetrics, PipelineStageTiming } from './charts/pipeline-overview.js';
12
12
  import { TimelineEvent } from './components/timeline-activity.js';
13
- import { OpportunityDraft, ApprovalState } from './components/signal-feedback-inline.js';
13
+ import { ApprovalState } from './components/signal-feedback-inline.js';
14
14
  import { LucideIcon } from 'lucide-react';
15
15
 
16
16
  interface TimelineSystemEventsConfig {
@@ -93,6 +93,8 @@ interface SignalScoreData {
93
93
  confidence: number;
94
94
  urgencyLabel?: SignalScoreUrgencyLabel;
95
95
  urgencyExplanation?: string;
96
+ /** Controls whether the priority popover header shows the raw overall score number. @default "number" */
97
+ priorityScoreDisplay?: SignalPriorityScoreDisplay;
96
98
  explanationBuckets?: SignalScoreExplanationBucket[];
97
99
  onFactorFeedback?: (factorKey: string, type: "up" | "down" | null, detail?: string) => void;
98
100
  /** @deprecated The compact score UX no longer renders score-level thumbs by default. */
@@ -181,7 +183,7 @@ interface InboxViewConfig {
181
183
  }>;
182
184
  hideAccountsButton?: boolean;
183
185
  accountDetailsLabel?: string;
184
- onSignalApprove?: (item: QueueItem, draft?: OpportunityDraft) => void | Promise<boolean>;
186
+ onSignalApprove?: (item: QueueItem) => void | Promise<boolean>;
185
187
  getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined;
186
188
  signalLabels?: {
187
189
  approveButton?: string;
@@ -412,8 +414,11 @@ interface PriorityFactor {
412
414
  /** Evidence text (e.g. "$3.4M moved in 8h - current treasury balance $0.00"). */
413
415
  rationale: string;
414
416
  }
417
+ type SignalPriorityScoreDisplay = "label" | "number";
415
418
  interface SignalPriorityPopoverProps {
416
419
  score: number;
420
+ /** Controls whether the overall score number is shown in the popover header. @default "number" */
421
+ scoreDisplay?: SignalPriorityScoreDisplay;
417
422
  urgencyLabel?: SignalScoreUrgencyLabel;
418
423
  /** Synthesis sentence displayed in the popover head. */
419
424
  urgencyExplanation?: string;
@@ -435,6 +440,6 @@ interface SignalPriorityPopoverProps {
435
440
  /** Persisted priority-level feedback for the footer. */
436
441
  initialPriorityFeedback?: PersistedFeedbackData | null;
437
442
  }
438
- declare function SignalPriorityPopover({ score, urgencyLabel: providedLabel, urgencyExplanation, factors, metaText, feedbackChips, onFeedbackSubmit, className, initialFactorFeedback, onFactorFeedback, initialPriorityFeedback, }: SignalPriorityPopoverProps): React.JSX.Element;
443
+ declare function SignalPriorityPopover({ score, urgencyLabel: providedLabel, urgencyExplanation, factors, metaText, feedbackChips, onFeedbackSubmit, className, initialFactorFeedback, onFactorFeedback, initialPriorityFeedback, scoreDisplay, }: SignalPriorityPopoverProps): React.JSX.Element;
439
444
 
440
- export { type AccountFilterTab as A, type BriefStyleVariant as B, type EntityPanelConfig as E, type InboxDetailSections as I, type PriorityFactor as P, type QueueItem as Q, SignalPriorityPopover as S, type TimelineSystemEventsConfig as T, type WorkQueueViewConfig as W, type AccountsViewConfig as a, type AdminTab as b, type AdminViewConfig as c, type EntityPanelSection as d, type InboxSortOption as e, type InboxViewConfig as f, type InsightsCustomTab as g, type InsightsViewConfig as h, type PrototypeBrandConfig as i, type PrototypeConfig as j, type SignalPriorityPopoverProps as k, type SignalScoreData as l, type SignalScoreExplanationBucket as m, type SignalScoreExplanationSignal as n, type SignalScoreUrgencyLabel as o };
445
+ export { type AccountFilterTab as A, type BriefStyleVariant as B, type EntityPanelConfig as E, type InboxDetailSections as I, type PriorityFactor as P, type QueueItem as Q, SignalPriorityPopover as S, type TimelineSystemEventsConfig as T, type WorkQueueViewConfig as W, type AccountsViewConfig as a, type AdminTab as b, type AdminViewConfig as c, type EntityPanelSection as d, type InboxSortOption as e, type InboxViewConfig as f, type InsightsCustomTab as g, type InsightsViewConfig as h, type PrototypeBrandConfig as i, type PrototypeConfig as j, type SignalPriorityPopoverProps as k, type SignalScoreData as l, type SignalScoreExplanationBucket as m, type SignalScoreExplanationSignal as n, type SignalScoreUrgencyLabel as o, type SignalPriorityScoreDisplay as p };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@handled-ai/design-system",
3
- "version": "0.18.48",
3
+ "version": "0.18.50",
4
4
  "description": "Handled UI component library (shadcn-style, New York)",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@9.12.0",
@@ -149,86 +149,4 @@ describe("CasePanelSignalApprovalActions", () => {
149
149
  expect(screen.getByText(/this will approve this action for/i)).toBeInTheDocument()
150
150
  expect(screen.getByRole("button", { name: /confirm/i }).className).toContain("h-7")
151
151
  })
152
-
153
- it("lets callers collect edited opportunity preview fields before approval", async () => {
154
- const onApprove = vi.fn()
155
-
156
- renderApprovalActions({
157
- initialApprovalState: "confirming",
158
- onApprove,
159
- opportunityPreview: {
160
- name: "Churn Risk - Northwind Systems",
161
- accountName: "Northwind Systems",
162
- stage: "Prospecting",
163
- closeDate: "Jun 30, 2026",
164
- closeDateValue: "2026-06-30",
165
- amount: "$75,000",
166
- amountValue: 75000,
167
- description: "Initial description",
168
- churnType: "Churn Risk",
169
- churnTypeOptions: ["Churn Risk", { value: "Win Back", label: "Win-back" }],
170
- },
171
- })
172
-
173
- fireEvent.change(screen.getByLabelText("Close Date"), { target: { value: "2026-07-15" } })
174
- expect(screen.getByLabelText("Amount")).toHaveValue("$75,000")
175
- fireEvent.change(screen.getByLabelText("Amount"), { target: { value: "90000" } })
176
- fireEvent.change(screen.getByLabelText("Churn Type"), { target: { value: "Win Back" } })
177
- fireEvent.change(screen.getByLabelText("Description"), { target: { value: "Updated before create" } })
178
- fireEvent.click(screen.getByRole("button", { name: /confirm/i }))
179
-
180
- expect(onApprove).toHaveBeenCalledWith({
181
- closeDate: "2026-07-15",
182
- amount: "90000",
183
- churnType: "Win Back",
184
- description: "Updated before create",
185
- })
186
- })
187
-
188
- it("keeps legacy display-only opportunity previews confirmable", () => {
189
- const onApprove = vi.fn()
190
-
191
- renderApprovalActions({
192
- initialApprovalState: "confirming",
193
- onApprove,
194
- opportunityPreview: {
195
- name: "Churn Risk - Northwind Systems",
196
- accountName: "Northwind Systems",
197
- stage: "Prospecting",
198
- closeDate: "Jun 30, 2026",
199
- amount: "$75,000",
200
- },
201
- })
202
-
203
- expect(screen.getByText(/this will approve this action for/i)).toBeInTheDocument()
204
- expect(screen.queryByLabelText("Close Date")).not.toBeInTheDocument()
205
- expect(screen.getByRole("button", { name: /confirm/i })).not.toBeDisabled()
206
-
207
- fireEvent.click(screen.getByRole("button", { name: /confirm/i }))
208
- expect(onApprove).toHaveBeenCalledWith(undefined)
209
- })
210
-
211
- it("blocks confirmation until the close date is valid", () => {
212
- const onApprove = vi.fn()
213
-
214
- renderApprovalActions({
215
- initialApprovalState: "confirming",
216
- onApprove,
217
- opportunityPreview: {
218
- name: "Churn Risk - Northwind Systems",
219
- accountName: "Northwind Systems",
220
- stage: "Prospecting",
221
- closeDate: "Jun 30, 2026",
222
- closeDateValue: "2026-06-30",
223
- amount: "$75,000",
224
- },
225
- })
226
-
227
- fireEvent.change(screen.getByLabelText("Close Date"), { target: { value: "" } })
228
- expect(screen.getByRole("button", { name: /confirm/i })).toBeDisabled()
229
- expect(screen.getByText("Enter a valid close date.")).toBeInTheDocument()
230
-
231
- fireEvent.click(screen.getByRole("button", { name: /confirm/i }))
232
- expect(onApprove).not.toHaveBeenCalled()
233
- })
234
152
  })
@@ -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,36 @@ 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 calibrated priority copy", () => {
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 = weighted signals + calibration")
198
+ expect(content.textContent).not.toContain("Score = weighted sum")
199
+ })
200
+
201
+
202
+ it("renders the overall score number by default while preserving factor row scores", () => {
203
+ render(<SignalPriorityPopover {...defaultProps} />)
204
+ fireEvent.click(screen.getByTestId("priority-popover-trigger"))
205
+
206
+ expect(screen.getByTestId("priority-overall-score").textContent).toBe("79/100")
207
+ expect(screen.getByTestId("factor-row-test_severity").textContent).toContain("85/100")
208
+ expect(screen.getByTestId("factor-row-account_depth").textContent).toContain("30/100")
209
+ })
210
+
211
+ it("hides only the overall header score in label display mode", () => {
212
+ render(<SignalPriorityPopover {...defaultProps} scoreDisplay="label" />)
213
+ fireEvent.click(screen.getByTestId("priority-popover-trigger"))
214
+
215
+ const header = screen.getByTestId("priority-popover-header")
216
+ expect(screen.queryByTestId("priority-overall-score")).toBeNull()
217
+ expect(header.textContent).toContain("Why this is high priority")
218
+ expect(header.textContent).not.toContain("79/100")
219
+ expect(screen.getByTestId("factor-row-test_severity").textContent).toContain("85/100")
220
+ expect(screen.getByTestId("factor-row-account_depth").textContent).toContain("30/100")
198
221
  })
199
222
 
200
223
  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
  }
@@ -225,8 +205,8 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
225
205
  setApprovalState("pending")
226
206
  }, [])
227
207
 
228
- const approve = React.useCallback((draft?: OpportunityDraft) => {
229
- const result = onApprove?.(draft)
208
+ const approve = React.useCallback(() => {
209
+ const result = onApprove?.()
230
210
  // If the callback returns a Promise, show a loading state and wait for it.
231
211
  if (result && typeof (result as Promise<boolean>).then === "function") {
232
212
  setApprovalState("creating")
@@ -437,49 +417,6 @@ function SubmittedFeedback({
437
417
  )
438
418
  }
439
419
 
440
- function optionValue(option: string | OpportunityPreviewOption): string {
441
- return typeof option === "string" ? option : option.value
442
- }
443
-
444
- function optionLabel(option: string | OpportunityPreviewOption): string {
445
- return typeof option === "string" ? option : option.label
446
- }
447
-
448
- function formatAmountDraftValue(value: string | number | null | undefined): string {
449
- if (value == null || value === "") return ""
450
- if (typeof value === "number") {
451
- return new Intl.NumberFormat("en-US", {
452
- style: "currency",
453
- currency: "USD",
454
- maximumFractionDigits: 0,
455
- }).format(value)
456
- }
457
- return value
458
- }
459
-
460
- function buildOpportunityDraft(preview?: OpportunityPreview): OpportunityDraft {
461
- return {
462
- closeDate: preview?.closeDateValue ?? preview?.closeDate ?? "",
463
- amount: preview?.amountValue === undefined
464
- ? preview?.amount ?? ""
465
- : formatAmountDraftValue(preview.amountValue),
466
- description: preview?.description ?? "",
467
- churnType: preview?.churnType ?? "",
468
- }
469
- }
470
-
471
- function hasEditableOpportunityPreview(preview?: OpportunityPreview): boolean {
472
- return !!preview && isValidDateInput(preview.closeDateValue ?? preview.closeDate)
473
- }
474
-
475
- function isValidDateInput(value: string): boolean {
476
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
477
- if (!match) return false
478
- const parsed = new Date(`${value}T00:00:00Z`)
479
- if (Number.isNaN(parsed.getTime())) return false
480
- return parsed.toISOString().slice(0, 10) === value
481
- }
482
-
483
420
  function Actions() {
484
421
  const { approvalState, companyName, opportunityUrl, scheduledTime, labels, hideApproveButton, approveButtonIconUrl, opportunityPreview, requestingApproval, approve, submitApproveFeedback, skipApproveFeedback, dismiss, requestApproval, requestDismiss, cancel } =
485
422
  useSignalApproval()
@@ -489,16 +426,6 @@ function Actions() {
489
426
  const [detailText, setDetailText] = React.useState("")
490
427
  const [submittedFeedback, setSubmittedFeedback] = React.useState<{ reasons: string[]; detail: string; subReason?: string } | null>(null)
491
428
  const [isEditing, setIsEditing] = React.useState(false)
492
- const [opportunityDraft, setOpportunityDraft] = React.useState<OpportunityDraft>(() => buildOpportunityDraft(opportunityPreview))
493
-
494
- React.useEffect(() => {
495
- if (approvalState === "confirming") {
496
- setOpportunityDraft(buildOpportunityDraft(opportunityPreview))
497
- }
498
- }, [approvalState, opportunityPreview])
499
-
500
- const churnTypeOptions = opportunityPreview?.churnTypeOptions ?? []
501
- const hasChurnTypeOptions = churnTypeOptions.length > 0
502
429
 
503
430
  const topNode = dismissReasonTree.find((n) => n.label === selectedTopReason)
504
431
  const hasSubOptions = !!(topNode?.subOptions && topNode.subOptions.length > 0)
@@ -511,8 +438,6 @@ function Actions() {
511
438
  (!needsText || detailText.trim().length > 0)
512
439
 
513
440
  const canSubmitApprove = selectedReasons.length > 0 || detailText.trim().length > 0
514
- const isEditableOpportunityPreview = hasEditableOpportunityPreview(opportunityPreview)
515
- const canConfirmOpportunity = !isEditableOpportunityPreview || isValidDateInput(opportunityDraft.closeDate)
516
441
 
517
442
  const selectTopReason = (label: string) => {
518
443
  if (selectedTopReason === label) {
@@ -814,8 +739,8 @@ function Actions() {
814
739
  <p className="text-sm text-foreground">
815
740
  {labels.confirmPrompt} <strong>{companyName}</strong>. Confirm?
816
741
  </p>
817
- {opportunityPreview && !isEditableOpportunityPreview && (
818
- <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">
819
744
  {[
820
745
  { label: "Opportunity", value: opportunityPreview.name },
821
746
  { label: "Account", value: opportunityPreview.accountName },
@@ -823,105 +748,19 @@ function Actions() {
823
748
  { label: "Close Date", value: opportunityPreview.closeDate },
824
749
  { label: "Amount", value: opportunityPreview.amount },
825
750
  ].map(({ label, value }) => (
826
- <div key={label} className="flex items-center justify-between gap-3 text-xs">
827
- <span className="text-muted-foreground">{label}</span>
828
- <span className="text-right font-medium text-foreground">{value}</span>
829
- </div>
830
- ))}
831
- </div>
832
- )}
833
- {opportunityPreview && isEditableOpportunityPreview && (
834
- <div className="mt-3 space-y-3 border-t border-border/50 pt-3">
835
- {[
836
- { label: "Opportunity", value: opportunityPreview.name },
837
- { label: "Account", value: opportunityPreview.accountName },
838
- { label: "Stage", value: opportunityPreview.stage },
839
- ].map(({ label, value }) => (
840
- <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">
841
752
  <span className="text-muted-foreground">{label}</span>
842
- <span className="text-right font-medium text-foreground">{value}</span>
753
+ <span className="font-medium text-foreground">{value}</span>
843
754
  </div>
844
755
  ))}
845
-
846
- <div className="grid gap-2 sm:grid-cols-2">
847
- <label className="space-y-1 text-xs">
848
- <span className="font-medium text-muted-foreground">Close Date</span>
849
- <input
850
- type="date"
851
- value={opportunityDraft.closeDate}
852
- onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, closeDate: event.target.value }))}
853
- aria-invalid={!canConfirmOpportunity}
854
- 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"
855
- />
856
- {!canConfirmOpportunity && (
857
- <span className="text-[11px] text-red-600">Enter a valid close date.</span>
858
- )}
859
- </label>
860
-
861
- <label className="space-y-1 text-xs">
862
- <span className="font-medium text-muted-foreground">Amount</span>
863
- <input
864
- type="text"
865
- inputMode="decimal"
866
- value={opportunityDraft.amount}
867
- onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, amount: event.target.value }))}
868
- placeholder={opportunityPreview.amount}
869
- 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"
870
- />
871
- </label>
872
- </div>
873
-
874
- <label className="space-y-1 text-xs">
875
- <span className="font-medium text-muted-foreground">Churn Type</span>
876
- {hasChurnTypeOptions ? (
877
- <select
878
- value={opportunityDraft.churnType}
879
- onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, churnType: event.target.value }))}
880
- 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"
881
- >
882
- <option value="">No churn type</option>
883
- {churnTypeOptions.map((option) => (
884
- <option key={optionValue(option)} value={optionValue(option)}>
885
- {optionLabel(option)}
886
- </option>
887
- ))}
888
- </select>
889
- ) : (
890
- <input
891
- type="text"
892
- value={opportunityDraft.churnType}
893
- onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, churnType: event.target.value }))}
894
- placeholder="No churn type"
895
- 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"
896
- />
897
- )}
898
- </label>
899
-
900
- <label className="space-y-1 text-xs">
901
- <span className="font-medium text-muted-foreground">Description</span>
902
- <textarea
903
- value={opportunityDraft.description}
904
- onChange={(event) => setOpportunityDraft((draft) => ({ ...draft, description: event.target.value }))}
905
- rows={3}
906
- placeholder="Add a short description"
907
- 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"
908
- />
909
- </label>
910
756
  </div>
911
757
  )}
912
758
  </div>
913
759
  <div className="flex items-center gap-2">
914
760
  <button
915
761
  type="button"
916
- onClick={() => {
917
- if (!opportunityPreview) {
918
- approve()
919
- return
920
- }
921
- approve(isEditableOpportunityPreview ? opportunityDraft : undefined)
922
- }}
923
- disabled={!canConfirmOpportunity}
924
- 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"
925
764
  >
926
765
  <Check className="h-3 w-3" />
927
766
  Confirm
@@ -1022,4 +861,5 @@ export {
1022
861
  Gate as SignalApprovalGate,
1023
862
  }
1024
863
  export const SignalApproval = { Root, Actions, Gate }
1025
- 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,12 @@ 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
55
59
  urgencyLabel?: SignalScoreUrgencyLabel
56
60
  /** Synthesis sentence displayed in the popover head. */
57
61
  urgencyExplanation?: string
@@ -282,6 +286,7 @@ export function SignalPriorityPopover({
282
286
  initialFactorFeedback,
283
287
  onFactorFeedback,
284
288
  initialPriorityFeedback,
289
+ scoreDisplay = "number",
285
290
  }: SignalPriorityPopoverProps) {
286
291
  const urgencyLabel = providedLabel ?? getUrgencyLevel(score)
287
292
  const scoreRange = getUrgencyRange(urgencyLabel)
@@ -330,15 +335,20 @@ export function SignalPriorityPopover({
330
335
  data-testid="priority-popover-content"
331
336
  >
332
337
  {/* Head section */}
333
- <div className="p-4 pb-3">
338
+ <div className="p-4 pb-3" data-testid="priority-popover-header">
334
339
  <div className="flex items-start justify-between gap-3">
335
340
  <p className="text-sm font-semibold text-foreground">
336
341
  Why this is {urgencyLabel.toLowerCase()} priority
337
342
  </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>
343
+ {scoreDisplay === "number" && (
344
+ <span
345
+ className="text-2xl font-bold tabular-nums text-foreground"
346
+ data-testid="priority-overall-score"
347
+ >
348
+ {score}
349
+ <span className="text-sm font-normal text-muted-foreground">/100</span>
350
+ </span>
351
+ )}
342
352
  </div>
343
353
 
344
354
  {/* Band indicator */}
@@ -369,7 +379,7 @@ export function SignalPriorityPopover({
369
379
  </span>
370
380
  <span className="flex items-center gap-1 text-[10px] text-muted-foreground">
371
381
  <Info className="h-3 w-3" />
372
- Score = weighted sum
382
+ Priority = weighted signals + calibration
373
383
  </span>
374
384
  </div>
375
385
 
@@ -89,6 +89,38 @@ 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 through to the priority popover", () => {
94
+ render(
95
+ <DetailView
96
+ {...baseProps({
97
+ getSignalScore: () =>
98
+ makeSignalScore({
99
+ urgencyLabel: "High",
100
+ priorityScoreDisplay: "label",
101
+ priorityFactors: [
102
+ {
103
+ key: "treasury",
104
+ label: "Treasury activity",
105
+ icon: "wallet",
106
+ tone: "alert",
107
+ direction: "raises",
108
+ score: 85,
109
+ rationale: "Full liquidation detected.",
110
+ },
111
+ ],
112
+ }),
113
+ })}
114
+ />,
115
+ );
116
+
117
+ fireEvent.click(screen.getByRole("button", { name: /high priority/i }));
118
+
119
+ expect(screen.queryByTestId("priority-overall-score")).toBeNull();
120
+ expect(screen.getByTestId("priority-popover-header").textContent).not.toContain("82/100");
121
+ expect(screen.getByTestId("factor-row-treasury").textContent).toContain("85/100");
122
+ });
123
+
92
124
  it("keeps signal WHY chips collapsed by default and excludes factor-only buckets", () => {
93
125
  render(
94
126
  <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,8 @@ 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
112
114
  explanationBuckets?: SignalScoreExplanationBucket[]
113
115
  onFactorFeedback?: (factorKey: string, type: "up" | "down" | null, detail?: string) => void
114
116
  /** @deprecated The compact score UX no longer renders score-level thumbs by default. */
@@ -180,7 +182,7 @@ export interface InboxViewConfig {
180
182
  quickFilterTabs?: Array<{ id: string; label: string; matchValue?: string; count?: number }>
181
183
  hideAccountsButton?: boolean
182
184
  accountDetailsLabel?: string
183
- onSignalApprove?: (item: QueueItem, draft?: OpportunityDraft) => void | Promise<boolean>
185
+ onSignalApprove?: (item: QueueItem) => void | Promise<boolean>
184
186
  getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined
185
187
  signalLabels?: {
186
188
  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,7 @@ export function DetailView({
519
519
  score={signalData.score}
520
520
  urgencyLabel={signalData.urgencyLabel}
521
521
  urgencyExplanation={signalData.urgencyExplanation ?? signalData.signalBrief}
522
+ scoreDisplay={signalData.priorityScoreDisplay}
522
523
  factors={signalData.priorityFactors ?? []}
523
524
  metaText={undefined}
524
525
  feedbackChips={signalData.priorityFeedbackChips}