@handled-ai/design-system 0.9.24 → 0.9.25

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.
@@ -71,7 +71,7 @@ const approveReasons = [
71
71
  "Actionable",
72
72
  ]
73
73
 
74
- type ApprovalState = "pending" | "confirming" | "approving-feedback" | "dismissing" | "approved" | "dismissed" | "auto-approved"
74
+ type ApprovalState = "pending" | "confirming" | "creating" | "approving-feedback" | "dismissing" | "approved" | "dismissed" | "auto-approved"
75
75
 
76
76
  interface SignalApprovalLabels {
77
77
  approveButton?: string
@@ -82,6 +82,8 @@ interface SignalApprovalLabels {
82
82
  confirmPrompt?: string
83
83
  dismissPrompt?: string
84
84
  feedbackPrompt?: string
85
+ /** Label shown while the approve action is in progress (e.g. "Creating Opportunity..."). */
86
+ creatingStatus?: string
85
87
  }
86
88
 
87
89
  const DEFAULT_LABELS: Required<SignalApprovalLabels> = {
@@ -93,6 +95,7 @@ const DEFAULT_LABELS: Required<SignalApprovalLabels> = {
93
95
  confirmPrompt: "This will approve this action for",
94
96
  dismissPrompt: "What\u2019s the issue with this action?",
95
97
  feedbackPrompt: "Quick feedback \u2014 what made this action useful?",
98
+ creatingStatus: "Creating\u2026",
96
99
  }
97
100
 
98
101
  interface SignalApprovalContextValue {
@@ -128,7 +131,16 @@ interface RootProps {
128
131
  labels?: SignalApprovalLabels
129
132
  /** When true, the approve/create-opportunity button is hidden but the dismiss button remains. */
130
133
  hideApproveButton?: boolean
131
- onApprove?: () => void
134
+ /**
135
+ * Called when the user confirms the approval action.
136
+ *
137
+ * - If the callback returns `void` (or `undefined`), the component transitions
138
+ * directly to the feedback step (backward-compatible behavior).
139
+ * - If the callback returns a `Promise<boolean>`, the component shows a
140
+ * "creating" loading state while the promise is pending. On `true` it
141
+ * transitions to the feedback step; on `false` it reverts to "pending".
142
+ */
143
+ onApprove?: () => void | Promise<boolean>
132
144
  onApproveFeedback?: (reasons: string[], detail: string) => void
133
145
  onDismiss?: (reasons: string[], detail: string, subReason?: string) => void
134
146
  }
@@ -137,6 +149,13 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
137
149
  const labels = React.useMemo(() => ({ ...DEFAULT_LABELS, ...labelOverrides }), [labelOverrides])
138
150
  const [approvalState, setApprovalState] = React.useState<ApprovalState>(initialApprovalState ?? "pending")
139
151
 
152
+ // Guard against state updates after unmount (e.g. user navigates away while
153
+ // an async onApprove promise is still in flight).
154
+ const mountedRef = React.useRef(true)
155
+ React.useEffect(() => {
156
+ return () => { mountedRef.current = false }
157
+ }, [])
158
+
140
159
  const requestApproval = React.useCallback(() => {
141
160
  setApprovalState("confirming")
142
161
  }, [])
@@ -150,8 +169,23 @@ function Root({ children, companyName, opportunityUrl, scheduledTime, initialApp
150
169
  }, [])
151
170
 
152
171
  const approve = React.useCallback(() => {
153
- setApprovalState("approving-feedback")
154
- onApprove?.()
172
+ const result = onApprove?.()
173
+ // If the callback returns a Promise, show a loading state and wait for it.
174
+ if (result && typeof (result as Promise<boolean>).then === "function") {
175
+ setApprovalState("creating")
176
+ ;(result as Promise<boolean>).then((success) => {
177
+ if (mountedRef.current) {
178
+ setApprovalState(success ? "approving-feedback" : "pending")
179
+ }
180
+ }).catch(() => {
181
+ if (mountedRef.current) {
182
+ setApprovalState("pending")
183
+ }
184
+ })
185
+ } else {
186
+ // Synchronous / void — transition immediately (backward-compatible).
187
+ setApprovalState("approving-feedback")
188
+ }
155
189
  }, [onApprove])
156
190
 
157
191
  const submitApproveFeedback = React.useCallback(
@@ -439,6 +473,18 @@ function Actions() {
439
473
  setDetailText("")
440
474
  }
441
475
 
476
+ if (approvalState === "creating") {
477
+ return (
478
+ <div className="flex items-center gap-2 text-xs font-medium text-muted-foreground">
479
+ <svg className="h-3.5 w-3.5 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
480
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
481
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
482
+ </svg>
483
+ <span>{labels.creatingStatus}</span>
484
+ </div>
485
+ )
486
+ }
487
+
442
488
  if (approvalState === "approved") {
443
489
  if (isEditing) {
444
490
  return (
@@ -728,7 +774,7 @@ function Gate({ children }: { children: React.ReactNode }) {
728
774
  const { approvalState, hideApproveButton } = useSignalApproval()
729
775
  // When the approve button is hidden, don't lock content behind approval
730
776
  const isLocked = !hideApproveButton &&
731
- (approvalState === "pending" || approvalState === "confirming" || approvalState === "dismissing")
777
+ (approvalState === "pending" || approvalState === "confirming" || approvalState === "creating" || approvalState === "dismissing")
732
778
 
733
779
  return (
734
780
  <div className="relative">
@@ -80,7 +80,7 @@ export interface InboxViewConfig {
80
80
  quickFilterTabs?: Array<{ id: string; label: string; matchValue?: string; count?: number }>
81
81
  hideAccountsButton?: boolean
82
82
  accountDetailsLabel?: string
83
- onSignalApprove?: (item: QueueItem) => void
83
+ onSignalApprove?: (item: QueueItem) => void | Promise<boolean>
84
84
  getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined
85
85
  signalLabels?: {
86
86
  approveButton?: string
@@ -89,6 +89,7 @@ export interface InboxViewConfig {
89
89
  dismissedStatus?: string
90
90
  opportunityCreated?: string
91
91
  confirmPrompt?: string
92
+ creatingStatus?: string
92
93
  }
93
94
  /** When true, the approve/create-opportunity button is hidden but the dismiss button remains. */
94
95
  hideApproveButton?: boolean
@@ -102,7 +102,7 @@ export interface DetailViewProps {
102
102
  onOpenEntityPanel?: () => void
103
103
  onOpenRecentActivity?: () => void
104
104
  onSuggestedActionFeedback?: (actionId: number | string, feedback: string, actionTitle?: string) => void
105
- onSignalApprove?: (item: QueueItem) => void
105
+ onSignalApprove?: (item: QueueItem) => void | Promise<boolean>
106
106
  getSignalApprovalState?: (item: QueueItem) => ApprovalState | undefined
107
107
  signalLabels?: InboxViewConfig["signalLabels"]
108
108
  hideApproveButton?: boolean