@handled-ai/design-system 0.18.22 → 0.18.23

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 (70) hide show
  1. package/dist/components/case-panel-activity-timeline.d.ts +100 -0
  2. package/dist/components/case-panel-activity-timeline.js +270 -0
  3. package/dist/components/case-panel-activity-timeline.js.map +1 -0
  4. package/dist/components/case-panel-detail.d.ts +60 -0
  5. package/dist/components/case-panel-detail.js +129 -0
  6. package/dist/components/case-panel-detail.js.map +1 -0
  7. package/dist/components/case-panel-email-composer.d.ts +61 -0
  8. package/dist/components/case-panel-email-composer.js +304 -0
  9. package/dist/components/case-panel-email-composer.js.map +1 -0
  10. package/dist/components/case-panel-why.d.ts +35 -0
  11. package/dist/components/case-panel-why.js +149 -0
  12. package/dist/components/case-panel-why.js.map +1 -0
  13. package/dist/components/contextual-quick-action-launcher.d.ts +7 -3
  14. package/dist/components/contextual-quick-action-launcher.js +99 -27
  15. package/dist/components/contextual-quick-action-launcher.js.map +1 -1
  16. package/dist/components/data-table.js +0 -1
  17. package/dist/components/data-table.js.map +1 -1
  18. package/dist/components/pill.d.ts +1 -1
  19. package/dist/components/score-analysis-modal.d.ts +2 -8
  20. package/dist/components/score-analysis-modal.js +6 -19
  21. package/dist/components/score-analysis-modal.js.map +1 -1
  22. package/dist/components/score-breakdown.d.ts +1 -3
  23. package/dist/components/score-breakdown.js +6 -5
  24. package/dist/components/score-breakdown.js.map +1 -1
  25. package/dist/components/score-ring.d.ts +3 -6
  26. package/dist/components/score-ring.js +14 -11
  27. package/dist/components/score-ring.js.map +1 -1
  28. package/dist/components/score-why-chips.d.ts +2 -3
  29. package/dist/components/score-why-chips.js +21 -10
  30. package/dist/components/score-why-chips.js.map +1 -1
  31. package/dist/components/signal-priority-popover.d.ts +0 -1
  32. package/dist/components/signal-priority-popover.js +20 -20
  33. package/dist/components/signal-priority-popover.js.map +1 -1
  34. package/dist/index.d.ts +7 -4
  35. package/dist/index.js +4 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/prototype/index.d.ts +0 -1
  38. package/dist/prototype/prototype-accounts-view.d.ts +0 -1
  39. package/dist/prototype/prototype-admin-view.d.ts +0 -1
  40. package/dist/prototype/prototype-config.d.ts +0 -1
  41. package/dist/prototype/prototype-inbox-view.d.ts +0 -1
  42. package/dist/prototype/prototype-insights-view.d.ts +0 -1
  43. package/dist/prototype/prototype-shell.d.ts +0 -1
  44. package/package.json +1 -1
  45. package/src/components/__tests__/case-panel-activity-timeline.test.tsx +152 -0
  46. package/src/components/__tests__/case-panel-detail.test.tsx +138 -0
  47. package/src/components/__tests__/case-panel-email-composer.test.tsx +171 -0
  48. package/src/components/__tests__/case-panel-why.test.tsx +152 -0
  49. package/src/components/__tests__/contextual-quick-action-launcher.test.tsx +87 -0
  50. package/src/components/__tests__/signal-priority-popover.test.tsx +5 -7
  51. package/src/components/case-panel-activity-timeline.tsx +414 -0
  52. package/src/components/case-panel-detail.tsx +228 -0
  53. package/src/components/case-panel-email-composer.tsx +341 -0
  54. package/src/components/case-panel-why.tsx +214 -0
  55. package/src/components/contextual-quick-action-launcher.tsx +92 -15
  56. package/src/components/data-table.tsx +0 -1
  57. package/src/components/score-analysis-modal.tsx +5 -22
  58. package/src/components/score-breakdown.tsx +6 -7
  59. package/src/components/score-ring.tsx +13 -11
  60. package/src/components/score-why-chips.tsx +23 -12
  61. package/src/components/signal-priority-popover.tsx +21 -21
  62. package/src/index.ts +4 -1
  63. package/dist/components/score-semantics.d.ts +0 -27
  64. package/dist/components/score-semantics.js +0 -173
  65. package/dist/components/score-semantics.js.map +0 -1
  66. package/src/components/__tests__/score-analysis-modal.test.tsx +0 -55
  67. package/src/components/__tests__/score-breakdown-intent.test.tsx +0 -47
  68. package/src/components/__tests__/score-ring.test.tsx +0 -43
  69. package/src/components/__tests__/score-semantics.test.ts +0 -107
  70. package/src/components/score-semantics.ts +0 -187
@@ -0,0 +1,341 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../lib/utils"
6
+ import { BRAND_ICONS } from "../lib/icons"
7
+
8
+ export type CasePanelEmailComposerRowState = "default" | "confirmed" | "unconfirmed"
9
+
10
+ export interface CasePanelEmailComposerRowProps extends React.HTMLAttributes<HTMLDivElement> {
11
+ label: React.ReactNode
12
+ children?: React.ReactNode
13
+ value?: React.ReactNode
14
+ placeholder?: React.ReactNode
15
+ actions?: React.ReactNode
16
+ state?: CasePanelEmailComposerRowState
17
+ }
18
+
19
+ export function CasePanelEmailComposerRow({
20
+ label,
21
+ children,
22
+ value,
23
+ placeholder,
24
+ actions,
25
+ state = "default",
26
+ className,
27
+ ...props
28
+ }: CasePanelEmailComposerRowProps) {
29
+ const content = children ?? value ?? placeholder
30
+ const rowClassName =
31
+ state === "unconfirmed"
32
+ ? "flex min-h-11 items-stretch border-b border-amber-200/80 bg-amber-50/75 text-sm dark:border-amber-900/50 dark:bg-amber-950/20"
33
+ : "flex min-h-11 items-stretch border-b border-border/70 bg-background text-sm"
34
+ const labelClassName =
35
+ state === "unconfirmed"
36
+ ? "flex w-[60px] shrink-0 items-center border-r border-amber-200/80 px-3 text-[11px] font-semibold uppercase tracking-wide text-amber-700 dark:border-amber-900/50 dark:text-amber-300"
37
+ : "flex w-[60px] shrink-0 items-center border-r border-border/70 px-3 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground"
38
+ const contentClassName =
39
+ state === "unconfirmed"
40
+ ? "flex min-w-0 flex-1 items-center gap-2 px-3 py-2 text-amber-950 dark:text-amber-100"
41
+ : "flex min-w-0 flex-1 items-center gap-2 px-3 py-2 text-foreground"
42
+ const placeholderClassName =
43
+ state === "unconfirmed"
44
+ ? "min-w-0 truncate text-amber-700/80 dark:text-amber-200/80"
45
+ : "min-w-0 truncate text-muted-foreground"
46
+
47
+ return (
48
+ <div
49
+ data-slot="case-panel-email-composer-row"
50
+ data-state={state}
51
+ className={cn(rowClassName, className)}
52
+ {...props}
53
+ >
54
+ <div data-slot="case-panel-email-composer-row-label" className={labelClassName}>
55
+ {label}
56
+ </div>
57
+ <div data-slot="case-panel-email-composer-row-content" className={contentClassName}>
58
+ {content ? (
59
+ <div className="min-w-0 flex-1 truncate">{content}</div>
60
+ ) : (
61
+ <div className={placeholderClassName}>Add {label}</div>
62
+ )}
63
+ {actions ? <div className="flex shrink-0 items-center gap-1.5">{actions}</div> : null}
64
+ </div>
65
+ </div>
66
+ )
67
+ }
68
+
69
+ export interface CasePanelEmailComposerChipProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
70
+ children: React.ReactNode
71
+ }
72
+
73
+ export function CasePanelEmailComposerChip({
74
+ children,
75
+ className,
76
+ type = "button",
77
+ ...props
78
+ }: CasePanelEmailComposerChipProps) {
79
+ return (
80
+ <button
81
+ data-slot="case-panel-email-composer-chip"
82
+ type={type}
83
+ className={cn(
84
+ "inline-flex h-6 items-center justify-center rounded-full border border-border bg-background px-2.5 text-[11px] font-medium text-muted-foreground shadow-xs transition-colors hover:bg-muted/60 hover:text-foreground disabled:pointer-events-none disabled:opacity-50",
85
+ className,
86
+ )}
87
+ {...props}
88
+ >
89
+ {children}
90
+ </button>
91
+ )
92
+ }
93
+
94
+ export interface CasePanelEmailComposerSignatureRowProps extends React.HTMLAttributes<HTMLDivElement> {
95
+ children?: React.ReactNode
96
+ label?: React.ReactNode
97
+ checked?: boolean
98
+ disabled?: boolean
99
+ }
100
+
101
+ export function CasePanelEmailComposerSignatureRow({
102
+ children,
103
+ label = "Include signature",
104
+ checked = true,
105
+ disabled,
106
+ className,
107
+ ...props
108
+ }: CasePanelEmailComposerSignatureRowProps) {
109
+ return (
110
+ <div
111
+ data-slot="case-panel-email-composer-signature-row"
112
+ className={cn(
113
+ "flex items-center justify-between border-t border-border/70 bg-muted/20 px-4 py-2.5 text-xs text-muted-foreground",
114
+ className,
115
+ )}
116
+ {...props}
117
+ >
118
+ {children ?? (
119
+ <div className="flex items-center gap-2">
120
+ <span
121
+ aria-hidden="true"
122
+ data-checked={checked ? "true" : "false"}
123
+ className={
124
+ checked
125
+ ? "flex size-4 items-center justify-center rounded border border-foreground bg-foreground text-[10px] leading-none text-background"
126
+ : "flex size-4 items-center justify-center rounded border border-border bg-background text-[10px] leading-none text-background"
127
+ }
128
+ >
129
+ {checked ? "✓" : ""}
130
+ </span>
131
+ <span className={disabled ? "text-muted-foreground/60" : "text-muted-foreground"}>{label}</span>
132
+ </div>
133
+ )}
134
+ </div>
135
+ )
136
+ }
137
+
138
+ export interface CasePanelEmailComposerSlotHelpers {
139
+ Row: typeof CasePanelEmailComposerRow
140
+ Chip: typeof CasePanelEmailComposerChip
141
+ SignatureRow: typeof CasePanelEmailComposerSignatureRow
142
+ }
143
+
144
+ export interface CasePanelEmailComposerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onKeyDown" | "title"> {
145
+ title?: React.ReactNode
146
+ providerLabel?: React.ReactNode
147
+ recipientRows?: React.ReactNode | ((helpers: CasePanelEmailComposerSlotHelpers) => React.ReactNode)
148
+ from?: React.ReactNode
149
+ to?: React.ReactNode
150
+ cc?: React.ReactNode
151
+ bcc?: React.ReactNode
152
+ subject?: React.ReactNode
153
+ toState?: CasePanelEmailComposerRowState
154
+ draftEditor?: React.ReactNode
155
+ toolbar?: React.ReactNode
156
+ sendBarActions?: React.ReactNode
157
+ signatureControl?: React.ReactNode
158
+ disabledReason?: React.ReactNode
159
+ disabled?: boolean
160
+ sendDisabled?: boolean
161
+ sendLabel?: React.ReactNode
162
+ onSendIntent?: () => void
163
+ onContactsIntent?: () => void
164
+ onAccountDetailsIntent?: () => void
165
+ onAddCcIntent?: () => void
166
+ onAddBccIntent?: () => void
167
+ contactsLabel?: React.ReactNode
168
+ accountDetailsLabel?: React.ReactNode
169
+ addCcLabel?: React.ReactNode
170
+ addBccLabel?: React.ReactNode
171
+ showRecipientActionChips?: boolean
172
+ onComposerKeyDown?: React.KeyboardEventHandler<HTMLDivElement>
173
+ }
174
+
175
+ const slotHelpers: CasePanelEmailComposerSlotHelpers = {
176
+ Row: CasePanelEmailComposerRow,
177
+ Chip: CasePanelEmailComposerChip,
178
+ SignatureRow: CasePanelEmailComposerSignatureRow,
179
+ }
180
+
181
+ export function CasePanelEmailComposer({
182
+ title = "Draft",
183
+ providerLabel = "Gmail",
184
+ recipientRows,
185
+ from,
186
+ to,
187
+ cc,
188
+ bcc,
189
+ subject,
190
+ toState = "default",
191
+ draftEditor,
192
+ toolbar,
193
+ sendBarActions,
194
+ signatureControl,
195
+ disabledReason,
196
+ disabled = false,
197
+ sendDisabled = false,
198
+ sendLabel = "Send",
199
+ onSendIntent,
200
+ onContactsIntent,
201
+ onAccountDetailsIntent,
202
+ onAddCcIntent,
203
+ onAddBccIntent,
204
+ contactsLabel = "Contacts",
205
+ accountDetailsLabel = "Account details",
206
+ addCcLabel = "Add Cc",
207
+ addBccLabel = "Add Bcc",
208
+ showRecipientActionChips = true,
209
+ onComposerKeyDown,
210
+ className,
211
+ ...props
212
+ }: CasePanelEmailComposerProps) {
213
+ const disabledReasonId = React.useId()
214
+ const sendIsDisabled = disabled || sendDisabled || Boolean(disabledReason)
215
+
216
+ const handleSendIntent = React.useCallback(() => {
217
+ if (sendIsDisabled) return
218
+ onSendIntent?.()
219
+ }, [onSendIntent, sendIsDisabled])
220
+
221
+ const handleKeyDown = React.useCallback(
222
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
223
+ onComposerKeyDown?.(event)
224
+ if (event.defaultPrevented) return
225
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
226
+ event.preventDefault()
227
+ handleSendIntent()
228
+ }
229
+ },
230
+ [handleSendIntent, onComposerKeyDown],
231
+ )
232
+
233
+ const renderedRecipientRows =
234
+ typeof recipientRows === "function"
235
+ ? recipientRows(slotHelpers)
236
+ : recipientRows ?? (
237
+ <>
238
+ <CasePanelEmailComposerRow label="From" value={from} placeholder="Sender" />
239
+ <CasePanelEmailComposerRow label="To" value={to} placeholder="Recipient" state={toState} />
240
+ {cc ? <CasePanelEmailComposerRow label="Cc" value={cc} /> : null}
241
+ {bcc ? <CasePanelEmailComposerRow label="Bcc" value={bcc} /> : null}
242
+ <CasePanelEmailComposerRow label="Subject" value={subject} placeholder="Subject" />
243
+ </>
244
+ )
245
+
246
+ const showActionChips =
247
+ showRecipientActionChips &&
248
+ (onContactsIntent || onAccountDetailsIntent || onAddCcIntent || onAddBccIntent)
249
+
250
+ return (
251
+ <div
252
+ data-slot="case-panel-email-composer"
253
+ aria-disabled={disabled ? "true" : undefined}
254
+ className={cn(
255
+ "overflow-hidden rounded-xl border border-border bg-background shadow-sm",
256
+ className,
257
+ )}
258
+ onKeyDown={handleKeyDown}
259
+ {...props}
260
+ >
261
+ <div data-slot="case-panel-email-composer-header" className="flex items-center justify-between border-b border-border/70 bg-muted/20 px-4 py-3">
262
+ <div className="flex min-w-0 items-center gap-2">
263
+ <img
264
+ src={BRAND_ICONS.gmail.icon}
265
+ alt="Gmail"
266
+ className="size-4 shrink-0 object-contain"
267
+ draggable={false}
268
+ />
269
+ <div className="min-w-0 truncate text-sm font-semibold text-foreground">{title}</div>
270
+ </div>
271
+ <div className="shrink-0 text-[11px] font-medium text-muted-foreground">{providerLabel}</div>
272
+ </div>
273
+
274
+ <div data-slot="case-panel-email-composer-card" className="bg-background">
275
+ <div data-slot="case-panel-email-composer-recipient-rows">{renderedRecipientRows}</div>
276
+
277
+ {showActionChips ? (
278
+ <div data-slot="case-panel-email-composer-chip-row" className="flex flex-wrap items-center gap-1.5 border-b border-border/70 bg-muted/10 px-3 py-2">
279
+ {onContactsIntent ? (
280
+ <CasePanelEmailComposerChip onClick={onContactsIntent} disabled={disabled}>
281
+ {contactsLabel}
282
+ </CasePanelEmailComposerChip>
283
+ ) : null}
284
+ {onAccountDetailsIntent ? (
285
+ <CasePanelEmailComposerChip onClick={onAccountDetailsIntent} disabled={disabled}>
286
+ {accountDetailsLabel}
287
+ </CasePanelEmailComposerChip>
288
+ ) : null}
289
+ {onAddCcIntent ? (
290
+ <CasePanelEmailComposerChip onClick={onAddCcIntent} disabled={disabled}>
291
+ {addCcLabel}
292
+ </CasePanelEmailComposerChip>
293
+ ) : null}
294
+ {onAddBccIntent ? (
295
+ <CasePanelEmailComposerChip onClick={onAddBccIntent} disabled={disabled}>
296
+ {addBccLabel}
297
+ </CasePanelEmailComposerChip>
298
+ ) : null}
299
+ </div>
300
+ ) : null}
301
+
302
+ <div data-slot="case-panel-email-composer-editor" className="min-h-40 px-4 py-4 text-sm text-foreground">
303
+ {draftEditor ?? <div className="text-muted-foreground">Draft editor slot</div>}
304
+ </div>
305
+
306
+ {signatureControl ? (
307
+ <div data-slot="case-panel-email-composer-signature-control">{signatureControl}</div>
308
+ ) : (
309
+ <CasePanelEmailComposerSignatureRow disabled={disabled} />
310
+ )}
311
+
312
+ <div data-slot="case-panel-email-composer-toolbar" className="flex min-h-11 items-center gap-2 border-t border-border/70 bg-muted/10 px-3 py-2">
313
+ {toolbar ?? <div className="text-xs font-medium text-muted-foreground">Toolbar</div>}
314
+ </div>
315
+
316
+ <div data-slot="case-panel-email-composer-send-bar" className="flex items-center justify-between gap-3 border-t border-border/70 bg-background px-3 py-3">
317
+ <div className="min-w-0 flex-1">
318
+ {disabledReason ? (
319
+ <div id={disabledReasonId} data-slot="case-panel-email-composer-disabled-reason" className="truncate text-xs text-muted-foreground">
320
+ {disabledReason}
321
+ </div>
322
+ ) : null}
323
+ </div>
324
+ <div className="flex shrink-0 items-center gap-2">
325
+ {sendBarActions}
326
+ <button
327
+ type="button"
328
+ data-slot="case-panel-email-composer-send-button"
329
+ aria-describedby={disabledReason ? disabledReasonId : undefined}
330
+ disabled={sendIsDisabled}
331
+ onClick={handleSendIntent}
332
+ className="inline-flex h-8 items-center justify-center rounded-md bg-foreground px-3 text-xs font-semibold text-background shadow-xs transition-colors hover:bg-foreground/90 disabled:pointer-events-none disabled:bg-muted disabled:text-muted-foreground"
333
+ >
334
+ {sendLabel}
335
+ </button>
336
+ </div>
337
+ </div>
338
+ </div>
339
+ </div>
340
+ )
341
+ }
@@ -0,0 +1,214 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { CirclePlus, Loader2, ThumbsDown } from "lucide-react"
5
+ import { cn } from "../lib/utils"
6
+ import { SignalApprovalActions, useSignalApproval } from "./signal-feedback-inline"
7
+
8
+ export type CasePanelWhyTone = "neutral" | "alert" | "warn" | "info" | "success"
9
+
10
+ export interface CasePanelWhyRow {
11
+ id: string
12
+ label: React.ReactNode
13
+ value?: React.ReactNode
14
+ meta?: React.ReactNode
15
+ description?: React.ReactNode
16
+ }
17
+
18
+ export interface CasePanelWhyItem {
19
+ id: string
20
+ label: React.ReactNode
21
+ /** Count rendered in the summary pill as ×N when provided. */
22
+ count?: number
23
+ summary?: React.ReactNode
24
+ details?: React.ReactNode
25
+ rows?: CasePanelWhyRow[]
26
+ icon?: React.ReactNode
27
+ tone?: CasePanelWhyTone
28
+ }
29
+
30
+ export interface CasePanelWhyProps {
31
+ items: CasePanelWhyItem[]
32
+ label?: React.ReactNode
33
+ defaultExpandedId?: string | null
34
+ onExpandedChange?: (itemId: string | null) => void
35
+ className?: string
36
+ }
37
+
38
+ const toneDotClasses: Record<CasePanelWhyTone, string> = {
39
+ neutral: "bg-muted-foreground/50",
40
+ alert: "bg-red-500",
41
+ warn: "bg-amber-500",
42
+ info: "bg-blue-500",
43
+ success: "bg-emerald-500",
44
+ }
45
+
46
+ function makeDomId(...parts: Array<string | undefined>): string {
47
+ return parts
48
+ .filter((part): part is string => Boolean(part))
49
+ .join("-")
50
+ .replace(/[^A-Za-z0-9_-]+/g, "-")
51
+ }
52
+
53
+ function DefaultWhyIcon({ tone = "neutral" }: { tone?: CasePanelWhyTone }) {
54
+ return <span aria-hidden="true" className={cn("h-1.5 w-1.5 rounded-full", toneDotClasses[tone])} />
55
+ }
56
+
57
+ export function CasePanelWhy({
58
+ items,
59
+ label = "Why",
60
+ defaultExpandedId = null,
61
+ onExpandedChange,
62
+ className,
63
+ }: CasePanelWhyProps) {
64
+ const [expandedId, setExpandedId] = React.useState<string | null>(defaultExpandedId)
65
+ const reactId = React.useId()
66
+ const idPrefix = makeDomId("case-panel-why", reactId)
67
+
68
+ const toggleExpanded = React.useCallback(
69
+ (itemId: string) => {
70
+ setExpandedId((current) => {
71
+ const next = current === itemId ? null : itemId
72
+ onExpandedChange?.(next)
73
+ return next
74
+ })
75
+ },
76
+ [onExpandedChange],
77
+ )
78
+
79
+ if (items.length === 0) return null
80
+
81
+ return (
82
+ <section className={cn("space-y-2", className)} aria-label={typeof label === "string" ? label : "Case panel why"}>
83
+ {label ? (
84
+ <div className="text-[11px] font-semibold uppercase tracking-[0.07em] text-muted-foreground">
85
+ {label}
86
+ </div>
87
+ ) : null}
88
+
89
+ <div className="space-y-2">
90
+ {items.map((item) => {
91
+ const isExpanded = expandedId === item.id
92
+ const panelId = `${idPrefix}-panel-${makeDomId(item.id)}`
93
+ const buttonId = `${idPrefix}-button-${makeDomId(item.id)}`
94
+
95
+ return (
96
+ <div key={item.id} className="space-y-2">
97
+ <button
98
+ id={buttonId}
99
+ type="button"
100
+ aria-expanded={isExpanded}
101
+ aria-controls={panelId}
102
+ onClick={() => toggleExpanded(item.id)}
103
+ onKeyDown={(event) => {
104
+ if (event.key === "Enter" || event.key === " ") {
105
+ event.preventDefault()
106
+ toggleExpanded(item.id)
107
+ }
108
+ }}
109
+ className={cn(
110
+ "inline-flex min-h-8 max-w-full items-center gap-2 rounded-full border border-border bg-background px-3 py-1.5 text-left text-xs font-semibold text-foreground shadow-[0_1px_1.5px_rgba(0,0,0,0.03)] transition-colors hover:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
111
+ isExpanded ? "bg-muted/30" : "bg-background",
112
+ )}
113
+ >
114
+ <span className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-muted">
115
+ {item.icon ?? <DefaultWhyIcon tone={item.tone} />}
116
+ </span>
117
+ <span className="min-w-0 truncate">{item.label}</span>
118
+ {typeof item.count === "number" ? (
119
+ <span className="shrink-0 rounded-full bg-muted px-1.5 py-0 text-[10px] font-bold tabular-nums text-muted-foreground">
120
+ ×{item.count}
121
+ </span>
122
+ ) : null}
123
+ </button>
124
+
125
+ {isExpanded ? (
126
+ <div
127
+ id={panelId}
128
+ role="region"
129
+ aria-labelledby={buttonId}
130
+ className="mt-2 rounded-xl border border-border bg-background p-3 shadow-[0_1px_2px_rgba(0,0,0,0.03)]"
131
+ >
132
+ {item.summary ? <div className="text-sm leading-6 text-foreground">{item.summary}</div> : null}
133
+ {item.details ? <div className="mt-2 text-xs leading-5 text-muted-foreground">{item.details}</div> : null}
134
+ {item.rows && item.rows.length > 0 ? (
135
+ <ul className="mt-3 divide-y divide-border/50" aria-label="Why details">
136
+ {item.rows.map((row) => (
137
+ <li key={row.id} className="py-2 first:pt-0 last:pb-0">
138
+ <div className="flex items-start justify-between gap-3">
139
+ <div className="min-w-0">
140
+ <div className="text-sm font-medium leading-5 text-foreground">{row.label}</div>
141
+ {row.description ? (
142
+ <div className="mt-1 text-xs leading-5 text-muted-foreground">{row.description}</div>
143
+ ) : null}
144
+ {row.meta ? <div className="mt-1 text-[11px] leading-4 text-muted-foreground/80">{row.meta}</div> : null}
145
+ </div>
146
+ {row.value ? (
147
+ <div className="shrink-0 text-right text-xs font-semibold tabular-nums text-foreground">
148
+ {row.value}
149
+ </div>
150
+ ) : null}
151
+ </div>
152
+ </li>
153
+ ))}
154
+ </ul>
155
+ ) : null}
156
+ </div>
157
+ ) : null}
158
+ </div>
159
+ )
160
+ })}
161
+ </div>
162
+ </section>
163
+ )
164
+ }
165
+
166
+ export interface CasePanelSignalApprovalActionsProps {
167
+ className?: string
168
+ }
169
+
170
+ export function CasePanelSignalApprovalActions({ className }: CasePanelSignalApprovalActionsProps) {
171
+ const {
172
+ approvalState,
173
+ labels,
174
+ hideApproveButton,
175
+ approveButtonIconUrl,
176
+ requestingApproval,
177
+ requestApproval,
178
+ requestDismiss,
179
+ } = useSignalApproval()
180
+
181
+ if (approvalState !== "pending") {
182
+ return <SignalApprovalActions />
183
+ }
184
+
185
+ return (
186
+ <div className={cn("flex flex-wrap items-center gap-2", className)}>
187
+ {!hideApproveButton ? (
188
+ <button
189
+ type="button"
190
+ onClick={requestApproval}
191
+ disabled={requestingApproval}
192
+ className="inline-flex h-8 items-center gap-1.5 rounded-md border border-foreground bg-foreground px-3 text-xs font-semibold text-background shadow-none transition-colors hover:bg-foreground/90 disabled:cursor-not-allowed disabled:opacity-50"
193
+ >
194
+ {requestingApproval ? (
195
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
196
+ ) : approveButtonIconUrl ? (
197
+ <img src={approveButtonIconUrl} alt="" className="h-3.5 w-3.5 object-contain" draggable={false} />
198
+ ) : (
199
+ <CirclePlus className="h-3.5 w-3.5" />
200
+ )}
201
+ {labels.approveButton}
202
+ </button>
203
+ ) : null}
204
+ <button
205
+ type="button"
206
+ onClick={requestDismiss}
207
+ className="inline-flex h-8 items-center gap-1.5 rounded-md border border-border bg-background px-3 text-xs font-medium text-muted-foreground shadow-none transition-colors hover:bg-muted hover:text-foreground"
208
+ >
209
+ <ThumbsDown className="h-3.5 w-3.5" />
210
+ {labels.dismissButton}
211
+ </button>
212
+ </div>
213
+ )
214
+ }