@handled-ai/design-system 0.18.45 → 0.18.47

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.
@@ -28,10 +28,13 @@ export interface FilterOption {
28
28
  value: string
29
29
  }
30
30
 
31
- interface DataTableFilterCategoryBase {
31
+ export interface DataTableFilterCategory {
32
32
  id: string
33
33
  label: string
34
34
  icon: React.ComponentType<{ className?: string }>
35
+ options: (string | FilterOption)[]
36
+ /** Filter behavior. Defaults to "multi" (checkbox multi-select). */
37
+ type?: "multi" | "single" | "boolean"
35
38
  /**
36
39
  * Submenu search behavior. Defaults to the DataTableFilter
37
40
  * optionSearchThreshold prop. Use true to always show search or false to
@@ -40,25 +43,6 @@ interface DataTableFilterCategoryBase {
40
43
  searchable?: boolean | { threshold?: number }
41
44
  }
42
45
 
43
- export interface DataTableOptionFilterCategory extends DataTableFilterCategoryBase {
44
- options: (string | FilterOption)[]
45
- /** Filter behavior. Defaults to "multi" (checkbox multi-select). */
46
- type?: "multi" | "single" | "boolean"
47
- }
48
-
49
- export interface DataTableTextFilterCategory extends DataTableFilterCategoryBase {
50
- /** Free-text filter behavior. Renders a top-level submenu with a text input. */
51
- type: "text"
52
- /** Placeholder shown in the text filter input. */
53
- valuePlaceholder?: string
54
- /** Not used for text filters; optional for backwards-compatible category shapes. */
55
- options?: (string | FilterOption)[]
56
- }
57
-
58
- export type DataTableFilterCategory =
59
- | DataTableOptionFilterCategory
60
- | DataTableTextFilterCategory
61
-
62
46
  function getOptionValue(option: string | FilterOption): string {
63
47
  return typeof option === "string" ? option : option.value
64
48
  }
@@ -66,111 +50,6 @@ function getOptionLabel(option: string | FilterOption): string {
66
50
  return typeof option === "string" ? option : option.label
67
51
  }
68
52
 
69
- function isTextFilterCategory(
70
- category: DataTableFilterCategory
71
- ): category is DataTableTextFilterCategory {
72
- return category.type === "text"
73
- }
74
-
75
- function TextFilterSubmenu({
76
- category,
77
- value,
78
- onValueChange,
79
- }: {
80
- category: DataTableTextFilterCategory
81
- value: string
82
- onValueChange?: (categoryId: string, value: string) => void
83
- }) {
84
- const [draftValue, setDraftValue] = React.useState(value)
85
-
86
- React.useEffect(() => {
87
- setDraftValue(value)
88
- }, [value])
89
-
90
- const active = value.trim().length > 0
91
- const applyValue = React.useCallback(() => {
92
- onValueChange?.(category.id, draftValue.trim())
93
- }, [category.id, draftValue, onValueChange])
94
-
95
- return (
96
- <DropdownMenuSub
97
- onOpenChange={(open) => {
98
- if (!open) {
99
- setDraftValue(value)
100
- }
101
- }}
102
- >
103
- <DropdownMenuSubTrigger
104
- className={cn(
105
- "cursor-pointer py-1.5 text-xs",
106
- active && "text-brand-purple"
107
- )}
108
- >
109
- <category.icon
110
- className={cn(
111
- "mr-2 h-3.5 w-3.5 text-muted-foreground",
112
- active && "text-brand-purple"
113
- )}
114
- />
115
- {category.label}
116
- {active ? <Check className="ml-auto h-4 w-4" /> : null}
117
- </DropdownMenuSubTrigger>
118
- <DropdownMenuSubContent className="w-64 p-2">
119
- <div className="space-y-2">
120
- <input
121
- aria-label={category.label}
122
- className="h-8 w-full rounded-md bg-muted/50 px-2 py-1 text-xs outline-none transition-colors placeholder:text-muted-foreground/70 focus:bg-muted"
123
- placeholder={
124
- category.valuePlaceholder ??
125
- `Enter ${category.label.toLowerCase()}...`
126
- }
127
- value={draftValue}
128
- onChange={(event) => setDraftValue(event.target.value)}
129
- onClick={(event) => event.stopPropagation()}
130
- onKeyDown={(event) => {
131
- event.stopPropagation()
132
- if (event.key === "Enter") {
133
- event.preventDefault()
134
- applyValue()
135
- }
136
- }}
137
- />
138
- <div className="flex items-center justify-end gap-2">
139
- {active ? (
140
- <Button
141
- type="button"
142
- variant="ghost"
143
- size="sm"
144
- className="h-7 px-2 text-xs"
145
- onClick={(event) => {
146
- event.preventDefault()
147
- event.stopPropagation()
148
- setDraftValue("")
149
- onValueChange?.(category.id, "")
150
- }}
151
- >
152
- Clear
153
- </Button>
154
- ) : null}
155
- <Button
156
- type="button"
157
- size="sm"
158
- className="h-7 px-2 text-xs"
159
- onClick={(event) => {
160
- event.preventDefault()
161
- event.stopPropagation()
162
- applyValue()
163
- }}
164
- >
165
- Apply
166
- </Button>
167
- </div>
168
- </div>
169
- </DropdownMenuSubContent>
170
- </DropdownMenuSub>
171
- )
172
- }
173
-
174
53
  export interface DataTableFilterProps {
175
54
  categories: DataTableFilterCategory[]
176
55
  selectedFilters: Record<string, string[]>
@@ -192,10 +71,6 @@ export interface DataTableFilterProps {
192
71
  onConditionFiltersChange?: (conditions: ConditionFilterValue[]) => void
193
72
  /** Dropdown entry label for the condition-builder panel. Default: "Add filter". */
194
73
  conditionBuilderLabel?: string
195
- /** Active free-text filters keyed by category id. */
196
- textFilters?: Record<string, string>
197
- /** Callback when a free-text filter value is applied or cleared. */
198
- onTextFilterChange?: (categoryId: string, value: string) => void
199
74
  }
200
75
 
201
76
  export function DataTableFilter({
@@ -211,8 +86,6 @@ export function DataTableFilter({
211
86
  conditionFilters = [],
212
87
  onConditionFiltersChange,
213
88
  conditionBuilderLabel = "Add filter",
214
- textFilters = {},
215
- onTextFilterChange,
216
89
  }: DataTableFilterProps) {
217
90
  const [query, setQuery] = React.useState("")
218
91
  const [subQueries, setSubQueries] = React.useState<Record<string, string>>({})
@@ -230,10 +103,6 @@ export function DataTableFilter({
230
103
  return true
231
104
  }
232
105
 
233
- if (isTextFilterCategory(category)) {
234
- return false
235
- }
236
-
237
106
  return category.options.some((option) =>
238
107
  getOptionLabel(option).toLowerCase().includes(normalized)
239
108
  )
@@ -254,16 +123,8 @@ export function DataTableFilter({
254
123
  0
255
124
  )
256
125
 
257
- const textCount = categories.reduce((count, category) => {
258
- if (!isTextFilterCategory(category)) {
259
- return count
260
- }
261
-
262
- return textFilters[category.id]?.trim() ? count + 1 : count
263
- }, 0)
264
-
265
- return userCount + conditionFilters.length + textCount
266
- }, [categories, selectedFilters, conditionFilters.length, textFilters])
126
+ return userCount + conditionFilters.length
127
+ }, [selectedFilters, conditionFilters.length])
267
128
 
268
129
  /** Collect all preset chips to render */
269
130
  const presetChips = React.useMemo(() => {
@@ -274,9 +135,9 @@ export function DataTableFilter({
274
135
  for (const [categoryId, values] of Object.entries(presetFilters)) {
275
136
  const category = categories.find((c) => c.id === categoryId)
276
137
  for (const value of values) {
277
- const option = category && !isTextFilterCategory(category)
278
- ? category.options.find((opt) => getOptionValue(opt) === value)
279
- : undefined
138
+ const option = category?.options.find(
139
+ (opt) => getOptionValue(opt) === value
140
+ )
280
141
  const label = option ? getOptionLabel(option) : value
281
142
  const active = selectedFilters[categoryId]?.includes(value) ?? false
282
143
  chips.push({ categoryId, value, label, active })
@@ -347,18 +208,6 @@ export function DataTableFilter({
347
208
  )
348
209
  }
349
210
 
350
- /* ── Free-text submenu ───────────────────────────────── */
351
- if (isTextFilterCategory(category)) {
352
- return (
353
- <TextFilterSubmenu
354
- key={category.id}
355
- category={category}
356
- value={textFilters[category.id] ?? ""}
357
- onValueChange={onTextFilterChange}
358
- />
359
- )
360
- }
361
-
362
211
  /* ── Sub-menu (single / multi) ──────────────────────── */
363
212
  const subQuery = (subQueries[category.id] ?? "").trim().toLowerCase()
364
213
  const filteredOptions = subQuery
@@ -73,6 +73,32 @@ 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
+
76
102
  interface SignalApprovalLabels {
77
103
  approveButton?: string
78
104
  dismissButton?: string
@@ -106,9 +132,9 @@ interface SignalApprovalContextValue {
106
132
  labels: Required<SignalApprovalLabels>
107
133
  hideApproveButton?: boolean
108
134
  approveButtonIconUrl?: string
109
- opportunityPreview?: RootProps['opportunityPreview']
135
+ opportunityPreview?: OpportunityPreview
110
136
  requestingApproval: boolean
111
- approve: () => void
137
+ approve: (draft?: OpportunityDraft) => void
112
138
  submitApproveFeedback: (reasons: string[], detail: string) => void
113
139
  skipApproveFeedback: () => void
114
140
  dismiss: (reasons: string[], detail: string, subReason?: string) => void
@@ -137,13 +163,7 @@ interface RootProps {
137
163
  /** Optional icon URL for the approve button. Renders an img instead of CirclePlus when provided. */
138
164
  approveButtonIconUrl?: string
139
165
  /** Optional structured preview data shown in the confirmation dialog. */
140
- opportunityPreview?: {
141
- name: string
142
- stage: string
143
- closeDate: string
144
- amount: string
145
- accountName: string
146
- }
166
+ opportunityPreview?: OpportunityPreview
147
167
  /**
148
168
  * Async callback fired when the user clicks the approve button, BEFORE
149
169
  * transitioning to the "confirming" state. While the promise is pending,
@@ -160,7 +180,7 @@ interface RootProps {
160
180
  * "creating" loading state while the promise is pending. On `true` it
161
181
  * transitions to the feedback step; on `false` it reverts to "pending".
162
182
  */
163
- onApprove?: () => void | Promise<boolean>
183
+ onApprove?: (draft?: OpportunityDraft) => void | Promise<boolean>
164
184
  onApproveFeedback?: (reasons: string[], detail: string) => void
165
185
  onDismiss?: (reasons: string[], detail: string, subReason?: string) => void
166
186
  }
@@ -205,8 +225,8 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
205
225
  setApprovalState("pending")
206
226
  }, [])
207
227
 
208
- const approve = React.useCallback(() => {
209
- const result = onApprove?.()
228
+ const approve = React.useCallback((draft?: OpportunityDraft) => {
229
+ const result = onApprove?.(draft)
210
230
  // If the callback returns a Promise, show a loading state and wait for it.
211
231
  if (result && typeof (result as Promise<boolean>).then === "function") {
212
232
  setApprovalState("creating")
@@ -417,6 +437,49 @@ function SubmittedFeedback({
417
437
  )
418
438
  }
419
439
 
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
+
420
483
  function Actions() {
421
484
  const { approvalState, companyName, opportunityUrl, scheduledTime, labels, hideApproveButton, approveButtonIconUrl, opportunityPreview, requestingApproval, approve, submitApproveFeedback, skipApproveFeedback, dismiss, requestApproval, requestDismiss, cancel } =
422
485
  useSignalApproval()
@@ -426,6 +489,16 @@ function Actions() {
426
489
  const [detailText, setDetailText] = React.useState("")
427
490
  const [submittedFeedback, setSubmittedFeedback] = React.useState<{ reasons: string[]; detail: string; subReason?: string } | null>(null)
428
491
  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
429
502
 
430
503
  const topNode = dismissReasonTree.find((n) => n.label === selectedTopReason)
431
504
  const hasSubOptions = !!(topNode?.subOptions && topNode.subOptions.length > 0)
@@ -438,6 +511,8 @@ function Actions() {
438
511
  (!needsText || detailText.trim().length > 0)
439
512
 
440
513
  const canSubmitApprove = selectedReasons.length > 0 || detailText.trim().length > 0
514
+ const isEditableOpportunityPreview = hasEditableOpportunityPreview(opportunityPreview)
515
+ const canConfirmOpportunity = !isEditableOpportunityPreview || isValidDateInput(opportunityDraft.closeDate)
441
516
 
442
517
  const selectTopReason = (label: string) => {
443
518
  if (selectedTopReason === label) {
@@ -739,8 +814,8 @@ function Actions() {
739
814
  <p className="text-sm text-foreground">
740
815
  {labels.confirmPrompt} <strong>{companyName}</strong>. Confirm?
741
816
  </p>
742
- {opportunityPreview && (
743
- <div className="mt-3 space-y-1.5 border-t border-border/50 pt-3">
817
+ {opportunityPreview && !isEditableOpportunityPreview && (
818
+ <div className="mt-3 space-y-2 border-t border-border/50 pt-3">
744
819
  {[
745
820
  { label: "Opportunity", value: opportunityPreview.name },
746
821
  { label: "Account", value: opportunityPreview.accountName },
@@ -748,19 +823,99 @@ function Actions() {
748
823
  { label: "Close Date", value: opportunityPreview.closeDate },
749
824
  { label: "Amount", value: opportunityPreview.amount },
750
825
  ].map(({ label, value }) => (
751
- <div key={label} className="flex items-center justify-between text-xs">
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">
752
841
  <span className="text-muted-foreground">{label}</span>
753
- <span className="font-medium text-foreground">{value}</span>
842
+ <span className="text-right font-medium text-foreground">{value}</span>
754
843
  </div>
755
844
  ))}
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>
756
910
  </div>
757
911
  )}
758
912
  </div>
759
913
  <div className="flex items-center gap-2">
760
914
  <button
761
915
  type="button"
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"
916
+ onClick={() => approve(isEditableOpportunityPreview ? opportunityDraft : undefined)}
917
+ disabled={!canConfirmOpportunity}
918
+ 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"
764
919
  >
765
920
  <Check className="h-3 w-3" />
766
921
  Confirm
@@ -861,5 +1016,4 @@ export {
861
1016
  Gate as SignalApprovalGate,
862
1017
  }
863
1018
  export const SignalApproval = { Root, Actions, Gate }
864
- export type OpportunityPreview = NonNullable<RootProps['opportunityPreview']>
865
- export type { ApprovalState, SignalApprovalLabels, SignalApprovalContextValue, RootProps as SignalApprovalRootProps }
1019
+ export type { ApprovalState, OpportunityPreview, OpportunityDraft, SignalApprovalLabels, SignalApprovalContextValue, RootProps as SignalApprovalRootProps }
@@ -13,7 +13,7 @@ import type {
13
13
  PipelineStageTiming,
14
14
  } from "../charts/pipeline-overview"
15
15
  import type { TimelineEvent } from "../components/timeline-activity"
16
- import type { ApprovalState } from "../components/signal-feedback-inline"
16
+ import type { ApprovalState, OpportunityDraft } from "../components/signal-feedback-inline"
17
17
  import type { LucideIcon } from "lucide-react"
18
18
  import type { PriorityFactor } from "../components/signal-priority-popover"
19
19
  import type { FeedbackChipTree, FeedbackSubmitData, PersistedFeedbackData } from "../components/feedback-primitives"
@@ -180,7 +180,7 @@ export interface InboxViewConfig {
180
180
  quickFilterTabs?: Array<{ id: string; label: string; matchValue?: string; count?: number }>
181
181
  hideAccountsButton?: boolean
182
182
  accountDetailsLabel?: string
183
- onSignalApprove?: (item: QueueItem) => void | Promise<boolean>
183
+ onSignalApprove?: (item: QueueItem, draft?: OpportunityDraft) => void | Promise<boolean>
184
184
  getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined
185
185
  signalLabels?: {
186
186
  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 OpportunityPreview } from "../components/signal-feedback-inline"
41
+ import { SignalApproval, type ApprovalState, type OpportunityDraft, 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) => void | Promise<boolean>
152
+ onSignalApprove?: (item: QueueItem, draft?: OpportunityDraft) => 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={() => onSignalApprove?.(item)}
478
+ onApprove={(draft) => onSignalApprove?.(item, draft)}
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 })