@handled-ai/design-system 0.18.36 → 0.18.37

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 (46) hide show
  1. package/dist/charts/chart.d.ts +1 -1
  2. package/dist/components/badge.d.ts +1 -1
  3. package/dist/components/button.d.ts +1 -1
  4. package/dist/components/draft-feedback-inline.d.ts +1 -1
  5. package/dist/components/draft-feedback-inline.js +10 -10
  6. package/dist/components/draft-feedback-inline.js.map +1 -1
  7. package/dist/components/email-composer-row.d.ts +11 -0
  8. package/dist/components/email-composer-row.js +82 -0
  9. package/dist/components/email-composer-row.js.map +1 -0
  10. package/dist/components/email-preview-card.d.ts +17 -0
  11. package/dist/components/email-preview-card.js +71 -0
  12. package/dist/components/email-preview-card.js.map +1 -0
  13. package/dist/components/email-recipient-field.d.ts +26 -0
  14. package/dist/components/email-recipient-field.js +400 -0
  15. package/dist/components/email-recipient-field.js.map +1 -0
  16. package/dist/components/email-send-bar.d.ts +22 -0
  17. package/dist/components/email-send-bar.js +66 -0
  18. package/dist/components/email-send-bar.js.map +1 -0
  19. package/dist/components/entity-panel.d.ts +1 -15
  20. package/dist/components/entity-panel.js +1 -74
  21. package/dist/components/entity-panel.js.map +1 -1
  22. package/dist/components/pill.d.ts +1 -1
  23. package/dist/components/score-feedback.js +6 -6
  24. package/dist/components/score-feedback.js.map +1 -1
  25. package/dist/components/suggested-actions.js +5 -17
  26. package/dist/components/suggested-actions.js.map +1 -1
  27. package/dist/components/tabs.d.ts +1 -1
  28. package/dist/index.d.ts +5 -1
  29. package/dist/index.js +4 -0
  30. package/dist/index.js.map +1 -1
  31. package/package.json +5 -1
  32. package/src/components/__tests__/email-composer-row.test.tsx +51 -0
  33. package/src/components/__tests__/email-preview-card.test.tsx +62 -0
  34. package/src/components/__tests__/email-recipient-field.test.tsx +256 -0
  35. package/src/components/__tests__/email-send-bar.test.tsx +80 -0
  36. package/src/components/draft-feedback-inline.tsx +13 -13
  37. package/src/components/email-composer-row.tsx +47 -0
  38. package/src/components/email-preview-card.tsx +94 -0
  39. package/src/components/email-recipient-field.tsx +456 -0
  40. package/src/components/email-send-bar.tsx +95 -0
  41. package/src/components/entity-panel.tsx +0 -117
  42. package/src/components/score-feedback.tsx +7 -7
  43. package/src/components/suggested-actions.tsx +5 -19
  44. package/src/index.ts +4 -0
  45. package/src/components/__tests__/draft-feedback-inline.test.tsx +0 -72
  46. package/src/components/__tests__/suggested-actions-feedback-header.test.tsx +0 -86
@@ -0,0 +1,94 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Eye } from "lucide-react"
5
+
6
+ import { cn } from "../lib/utils"
7
+
8
+ export interface EmailPreviewCardProps {
9
+ from: { name: string; email: string }
10
+ to?: string
11
+ subject?: string
12
+ htmlBody?: string
13
+ textBody?: string
14
+ signatureHtml?: string | null
15
+ className?: string
16
+ }
17
+
18
+ function getInitials(name: string): string {
19
+ return name
20
+ .split(" ")
21
+ .map((part) => part[0])
22
+ .filter(Boolean)
23
+ .slice(0, 2)
24
+ .join("")
25
+ .toUpperCase()
26
+ }
27
+
28
+ function escapeHtml(text: string): string {
29
+ return text
30
+ .replace(/&/g, "&")
31
+ .replace(/</g, "&lt;")
32
+ .replace(/>/g, "&gt;")
33
+ .replace(/"/g, "&quot;")
34
+ .replace(/'/g, "&#39;")
35
+ }
36
+
37
+ export function EmailPreviewCard({
38
+ from,
39
+ to,
40
+ subject,
41
+ htmlBody,
42
+ textBody,
43
+ signatureHtml,
44
+ className,
45
+ }: EmailPreviewCardProps) {
46
+ const recipientLabel = to ? `${to}'s` : "the recipient's"
47
+ const bodyHtml = htmlBody ?? (textBody ? escapeHtml(textBody) : "")
48
+
49
+ return (
50
+ <div className={cn("p-4 bg-muted/30 min-h-full", className)}>
51
+ <div className="flex items-start gap-2 mb-3 py-2.5 px-3 border rounded-lg bg-background text-[11.5px] text-muted-foreground">
52
+ <Eye className="size-4 shrink-0 mt-0.5" />
53
+ <span>
54
+ This is how your email lands in {recipientLabel} inbox. Nothing has
55
+ been sent yet.
56
+ </span>
57
+ </div>
58
+
59
+ <div className="bg-background border rounded-xl shadow-sm overflow-hidden">
60
+ <div className="px-[18px] pt-4 pb-3 text-base font-semibold border-b border-border/50">
61
+ {subject || "(no subject)"}
62
+ </div>
63
+
64
+ <div className="flex items-center gap-3 px-[18px] pt-3">
65
+ <div className="flex size-9 shrink-0 items-center justify-center rounded-full bg-foreground text-background text-xs font-semibold">
66
+ {getInitials(from.name)}
67
+ </div>
68
+ <div className="min-w-0 flex-1">
69
+ <div className="text-sm">
70
+ <span className="font-semibold text-foreground">{from.name}</span>{" "}
71
+ <span className="text-muted-foreground">&lt;{from.email}&gt;</span>
72
+ </div>
73
+ <div className="text-xs text-muted-foreground">
74
+ {to ? `to ${to}` : "to no recipient yet"}
75
+ </div>
76
+ </div>
77
+ <div className="text-xs text-muted-foreground shrink-0">just now</div>
78
+ </div>
79
+
80
+ <div
81
+ className="px-[18px] py-2 ml-[47px] text-[13.5px] leading-relaxed whitespace-pre-wrap"
82
+ dangerouslySetInnerHTML={{ __html: bodyHtml }}
83
+ />
84
+
85
+ {signatureHtml ? (
86
+ <div
87
+ className="ml-[47px] px-[18px] pt-3 mt-3 border-t border-border/50 pb-4 text-xs leading-relaxed text-muted-foreground"
88
+ dangerouslySetInnerHTML={{ __html: signatureHtml }}
89
+ />
90
+ ) : null}
91
+ </div>
92
+ </div>
93
+ )
94
+ }
@@ -0,0 +1,456 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { createPortal } from "react-dom"
5
+ import {
6
+ Check,
7
+ ChevronDown,
8
+ CornerDownLeft,
9
+ Plus,
10
+ Search,
11
+ Users,
12
+ X,
13
+ } from "lucide-react"
14
+
15
+ import { cn } from "../lib/utils"
16
+ import type { SuggestedContact } from "./suggested-actions"
17
+
18
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
19
+
20
+ function isValidEmail(value: string): boolean {
21
+ return EMAIL_REGEX.test(value.trim())
22
+ }
23
+
24
+ function contactEmail(contact: SuggestedContact): string | undefined {
25
+ return contact.email ?? contact.emails?.[0]
26
+ }
27
+
28
+ function getInitials(name: string, fallback: string): string {
29
+ const source = name?.trim() || fallback
30
+ return source
31
+ .split(/[\s@.]+/)
32
+ .map((part) => part[0])
33
+ .filter(Boolean)
34
+ .slice(0, 2)
35
+ .join("")
36
+ .toUpperCase()
37
+ }
38
+
39
+ export interface RecipientChip {
40
+ id: string
41
+ email: string
42
+ name: string
43
+ confirmed: boolean
44
+ }
45
+
46
+ export interface EmailRecipientFieldProps {
47
+ label: string
48
+ recipients: RecipientChip[]
49
+ onRecipientsChange: (recipients: RecipientChip[]) => void
50
+ amber?: boolean
51
+ contacts?: SuggestedContact[]
52
+ showPicker?: boolean
53
+ showCcBcc?: boolean
54
+ ccBccOpen?: boolean
55
+ onCcBccToggle?: () => void
56
+ addedEmails?: Set<string>
57
+ placeholder?: string
58
+ contactToRecipient?: (contact: SuggestedContact) => RecipientChip
59
+ }
60
+
61
+ function RecipientChipPill({
62
+ recipient,
63
+ onConfirm,
64
+ onRemove,
65
+ }: {
66
+ recipient: RecipientChip
67
+ onConfirm: () => void
68
+ onRemove: () => void
69
+ }) {
70
+ const display = recipient.name || recipient.email
71
+
72
+ if (!recipient.confirmed) {
73
+ return (
74
+ <span className="inline-flex items-center gap-1 h-6 px-2 rounded-md text-xs border border-amber-300 bg-amber-50 text-amber-900">
75
+ <span className="truncate max-w-[180px]">{display}</span>
76
+ <button
77
+ type="button"
78
+ onClick={onConfirm}
79
+ className="text-[10.5px] font-semibold px-[7px] py-0.5 rounded bg-amber-300/50 hover:bg-amber-300/85"
80
+ >
81
+ Confirm
82
+ </button>
83
+ <button
84
+ type="button"
85
+ aria-label={`Remove ${display}`}
86
+ onClick={onRemove}
87
+ className="inline-flex items-center justify-center size-[17px] rounded text-amber-700/80 hover:bg-amber-300/40"
88
+ >
89
+ <X className="size-3" />
90
+ </button>
91
+ </span>
92
+ )
93
+ }
94
+
95
+ return (
96
+ <span className="inline-flex items-center gap-1 h-6 px-2 rounded-md text-xs border border-border bg-muted/50 text-foreground">
97
+ <span className="inline-flex items-center justify-center size-[17px] rounded bg-emerald-50 text-emerald-700">
98
+ <Check className="size-3" />
99
+ </span>
100
+ <span className="truncate max-w-[180px]">{display}</span>
101
+ <button
102
+ type="button"
103
+ aria-label={`Remove ${display}`}
104
+ onClick={onRemove}
105
+ className="inline-flex items-center justify-center size-[17px] rounded text-muted-foreground hover:bg-muted"
106
+ >
107
+ <X className="size-3" />
108
+ </button>
109
+ </span>
110
+ )
111
+ }
112
+
113
+ function ContactPickerPopover({
114
+ triggerRef,
115
+ contacts,
116
+ addedEmails,
117
+ onSelect,
118
+ onAddEmail,
119
+ onClose,
120
+ }: {
121
+ triggerRef: React.RefObject<HTMLElement | null>
122
+ contacts: SuggestedContact[]
123
+ addedEmails: Set<string>
124
+ onSelect: (contact: SuggestedContact) => void
125
+ onAddEmail: (email: string) => void
126
+ onClose: () => void
127
+ }) {
128
+ const containerRef = React.useRef<HTMLDivElement>(null)
129
+ const searchRef = React.useRef<HTMLInputElement>(null)
130
+ const [query, setQuery] = React.useState("")
131
+ const [style, setStyle] = React.useState<React.CSSProperties>({
132
+ position: "fixed",
133
+ top: -9999,
134
+ left: -9999,
135
+ })
136
+
137
+ React.useEffect(() => {
138
+ const trigger = triggerRef.current
139
+ if (!trigger) return
140
+ const rect = trigger.getBoundingClientRect()
141
+ const width = Math.min(448, window.innerWidth - 32)
142
+ let left = rect.left
143
+ if (left + width > window.innerWidth - 16) {
144
+ left = window.innerWidth - 16 - width
145
+ }
146
+ if (left < 16) left = 16
147
+ const popoverHeight = 280
148
+ const spaceBelow = window.innerHeight - rect.bottom - 4
149
+ const spaceAbove = rect.top - 4
150
+ const placeAbove = spaceBelow < popoverHeight && spaceAbove > spaceBelow
151
+ let top = placeAbove ? rect.top - popoverHeight - 4 : rect.bottom + 4
152
+ if (top < 16) top = 16
153
+ setStyle({ position: "fixed", top, left, width })
154
+ }, [triggerRef])
155
+
156
+ React.useEffect(() => {
157
+ searchRef.current?.focus()
158
+ const trigger = triggerRef.current
159
+ return () => {
160
+ if (trigger && typeof trigger.focus === "function") {
161
+ trigger.focus()
162
+ }
163
+ }
164
+ }, [triggerRef])
165
+
166
+ React.useEffect(() => {
167
+ function handleMouseDown(event: MouseEvent) {
168
+ if (
169
+ containerRef.current &&
170
+ !containerRef.current.contains(event.target as Node) &&
171
+ !triggerRef.current?.contains(event.target as Node)
172
+ ) {
173
+ onClose()
174
+ }
175
+ }
176
+ function handleKeyDown(event: KeyboardEvent) {
177
+ if (event.key === "Escape") {
178
+ event.stopPropagation()
179
+ onClose()
180
+ }
181
+ }
182
+ document.addEventListener("mousedown", handleMouseDown)
183
+ document.addEventListener("keydown", handleKeyDown)
184
+ return () => {
185
+ document.removeEventListener("mousedown", handleMouseDown)
186
+ document.removeEventListener("keydown", handleKeyDown)
187
+ }
188
+ }, [onClose, triggerRef])
189
+
190
+ const normalizedQuery = query.trim().toLowerCase()
191
+ const filtered = normalizedQuery
192
+ ? contacts.filter((contact) => {
193
+ const email = contactEmail(contact) ?? ""
194
+ return (
195
+ contact.name.toLowerCase().includes(normalizedQuery) ||
196
+ contact.role.toLowerCase().includes(normalizedQuery) ||
197
+ email.toLowerCase().includes(normalizedQuery)
198
+ )
199
+ })
200
+ : contacts
201
+
202
+ const queryIsEmail = isValidEmail(query)
203
+
204
+ function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
205
+ if (event.key === "Enter" && queryIsEmail) {
206
+ event.preventDefault()
207
+ onAddEmail(query.trim())
208
+ setQuery("")
209
+ }
210
+ }
211
+
212
+ return createPortal(
213
+ <div
214
+ ref={containerRef}
215
+ style={style}
216
+ className="bg-background border rounded-lg shadow-xl z-50 pointer-events-auto"
217
+ >
218
+ <div className="flex items-center gap-2 px-3 py-2.5 border-b border-border/50">
219
+ <Search className="size-4 text-muted-foreground shrink-0" />
220
+ <input
221
+ ref={searchRef}
222
+ autoFocus
223
+ value={query}
224
+ onChange={(event) => setQuery(event.target.value)}
225
+ onKeyDown={handleKeyDown}
226
+ className="flex-1 text-[13px] bg-transparent outline-none"
227
+ placeholder="Search contacts..."
228
+ />
229
+ </div>
230
+
231
+ <div role="listbox" className="max-h-[208px] overflow-y-auto p-1">
232
+ {filtered.length === 0 ? (
233
+ <div className="px-3 py-4 text-center text-[13px] text-muted-foreground">
234
+ <div>No contact matches &lsquo;{query}&rsquo;.</div>
235
+ {queryIsEmail ? (
236
+ <div className="mt-1">Press Enter to add {query}.</div>
237
+ ) : null}
238
+ </div>
239
+ ) : (
240
+ filtered.map((contact, index) => {
241
+ const email = contactEmail(contact)
242
+ const noEmail = !email || !isValidEmail(email)
243
+ const alreadyAdded = email
244
+ ? addedEmails.has(email.toLowerCase())
245
+ : false
246
+ const disabled = noEmail || alreadyAdded
247
+
248
+ return (
249
+ <div
250
+ key={`${contact.name}-${email ?? index}`}
251
+ role="option"
252
+ aria-selected={false}
253
+ aria-disabled={disabled}
254
+ onClick={() => {
255
+ if (!disabled) onSelect(contact)
256
+ }}
257
+ className={cn(
258
+ "flex items-center gap-2.5 px-2 py-1.5 rounded-md cursor-pointer hover:bg-muted/60",
259
+ disabled && "opacity-45 pointer-events-none",
260
+ )}
261
+ >
262
+ <div className="flex size-7 shrink-0 items-center justify-center rounded-[7px] bg-muted text-[11px] font-medium text-muted-foreground">
263
+ {getInitials(contact.name, email ?? "?")}
264
+ </div>
265
+ <div className="min-w-0 flex-1">
266
+ <div className="flex items-center gap-1.5">
267
+ <span className="truncate text-[13px] font-medium text-foreground">
268
+ {contact.name}
269
+ </span>
270
+ <span className="truncate text-[11px] text-muted-foreground">
271
+ {contact.role}
272
+ </span>
273
+ </div>
274
+ {email ? (
275
+ <div className="truncate text-[11px] text-muted-foreground">
276
+ {email}
277
+ </div>
278
+ ) : null}
279
+ </div>
280
+ {alreadyAdded ? (
281
+ <span className="shrink-0 text-[10.5px] font-medium text-muted-foreground">
282
+ Added
283
+ </span>
284
+ ) : noEmail ? (
285
+ <span className="shrink-0 text-[10.5px] font-medium text-muted-foreground">
286
+ No email
287
+ </span>
288
+ ) : null}
289
+ </div>
290
+ )
291
+ })
292
+ )}
293
+ </div>
294
+
295
+ <div className="flex items-center gap-1.5 px-3 py-2 border-t border-border/50 text-[11px] text-muted-foreground">
296
+ <CornerDownLeft className="size-3 shrink-0" />
297
+ <span>Type an address and press Enter to add someone not listed.</span>
298
+ </div>
299
+ </div>,
300
+ document.body,
301
+ )
302
+ }
303
+
304
+ export function EmailRecipientField({
305
+ label,
306
+ recipients,
307
+ onRecipientsChange,
308
+ amber = false,
309
+ contacts = [],
310
+ showPicker = false,
311
+ showCcBcc = false,
312
+ ccBccOpen = false,
313
+ onCcBccToggle,
314
+ addedEmails,
315
+ placeholder,
316
+ contactToRecipient,
317
+ }: EmailRecipientFieldProps) {
318
+ const [value, setValue] = React.useState("")
319
+ const [pickerOpen, setPickerOpen] = React.useState(false)
320
+ const contactsTriggerRef = React.useRef<HTMLButtonElement>(null)
321
+
322
+ const hasUnconfirmed = recipients.some((r) => !r.confirmed)
323
+ const state: "default" | "amber" =
324
+ amber && hasUnconfirmed ? "amber" : "default"
325
+ const amberRow = state === "amber"
326
+
327
+ const added = addedEmails ?? new Set<string>()
328
+
329
+ const resolvedPlaceholder =
330
+ placeholder ?? (recipients.length > 0 ? "Add another..." : "Add email...")
331
+
332
+ function addEmail(email: string) {
333
+ const trimmed = email.trim()
334
+ if (!isValidEmail(trimmed)) return
335
+ if (added.has(trimmed.toLowerCase())) return
336
+ onRecipientsChange([
337
+ ...recipients,
338
+ { id: trimmed, email: trimmed, name: "", confirmed: false },
339
+ ])
340
+ setValue("")
341
+ }
342
+
343
+ function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
344
+ if ((event.key === "Enter" || event.key === ",") && isValidEmail(value)) {
345
+ event.preventDefault()
346
+ addEmail(value)
347
+ return
348
+ }
349
+ if (event.key === "Backspace" && value === "" && recipients.length > 0) {
350
+ event.preventDefault()
351
+ onRecipientsChange(recipients.slice(0, -1))
352
+ }
353
+ }
354
+
355
+ function confirmRecipient(id: string) {
356
+ onRecipientsChange(
357
+ recipients.map((r) => (r.id === id ? { ...r, confirmed: true } : r)),
358
+ )
359
+ }
360
+
361
+ function removeRecipient(id: string) {
362
+ onRecipientsChange(recipients.filter((r) => r.id !== id))
363
+ }
364
+
365
+ function selectContact(contact: SuggestedContact) {
366
+ const recipient =
367
+ contactToRecipient?.(contact) ??
368
+ ({
369
+ id: contactEmail(contact) ?? contact.name,
370
+ email: contactEmail(contact) ?? "",
371
+ name: contact.name,
372
+ confirmed: true,
373
+ } satisfies RecipientChip)
374
+ onRecipientsChange([...recipients, recipient])
375
+ setPickerOpen(false)
376
+ }
377
+
378
+ return (
379
+ <div
380
+ className={cn(
381
+ "grid grid-cols-[60px_1fr] gap-2 px-[18px] py-[9px] border-b border-border/70 items-start text-sm",
382
+ amberRow && "bg-amber-50/35 border-amber-200/80",
383
+ )}
384
+ >
385
+ <div
386
+ className={cn(
387
+ "text-[11px] font-semibold uppercase tracking-wide text-muted-foreground pt-[7px]",
388
+ amberRow && "text-amber-700",
389
+ )}
390
+ >
391
+ {label}
392
+ </div>
393
+
394
+ <div className="min-w-0">
395
+ <div className="flex flex-wrap gap-1.5 items-center">
396
+ {recipients.map((recipient) => (
397
+ <RecipientChipPill
398
+ key={recipient.id}
399
+ recipient={recipient}
400
+ onConfirm={() => confirmRecipient(recipient.id)}
401
+ onRemove={() => removeRecipient(recipient.id)}
402
+ />
403
+ ))}
404
+ <input
405
+ value={value}
406
+ onChange={(event) => setValue(event.target.value)}
407
+ onKeyDown={handleKeyDown}
408
+ placeholder={resolvedPlaceholder}
409
+ className="min-w-[130px] flex-1 h-6 text-[13px] bg-transparent border-0 outline-none placeholder:text-muted-foreground"
410
+ />
411
+ </div>
412
+
413
+ {showPicker || showCcBcc ? (
414
+ <div className="flex gap-1.5 mt-2">
415
+ {showPicker ? (
416
+ <button
417
+ ref={contactsTriggerRef}
418
+ type="button"
419
+ onClick={() => setPickerOpen((open) => !open)}
420
+ className="inline-flex items-center gap-1 h-6 px-2.5 rounded-md border border-border bg-background text-[11px] font-medium text-muted-foreground hover:bg-muted/60 hover:text-foreground transition-colors duration-[120ms]"
421
+ >
422
+ <Users className="size-3" />
423
+ Contacts
424
+ <ChevronDown className="size-3" />
425
+ </button>
426
+ ) : null}
427
+ {showCcBcc ? (
428
+ <button
429
+ type="button"
430
+ onClick={onCcBccToggle}
431
+ className="inline-flex items-center gap-1 h-6 px-2.5 rounded-md border border-border bg-background text-[11px] font-medium text-muted-foreground hover:bg-muted/60 hover:text-foreground transition-colors duration-[120ms]"
432
+ >
433
+ <Plus className="size-3" />
434
+ {ccBccOpen ? "Hide Cc/Bcc" : "Add Cc/Bcc"}
435
+ </button>
436
+ ) : null}
437
+ </div>
438
+ ) : null}
439
+
440
+ {pickerOpen ? (
441
+ <ContactPickerPopover
442
+ triggerRef={contactsTriggerRef}
443
+ contacts={contacts}
444
+ addedEmails={added}
445
+ onSelect={selectContact}
446
+ onAddEmail={(email) => {
447
+ addEmail(email)
448
+ setPickerOpen(false)
449
+ }}
450
+ onClose={() => setPickerOpen(false)}
451
+ />
452
+ ) : null}
453
+ </div>
454
+ </div>
455
+ )
456
+ }
@@ -0,0 +1,95 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { cn } from "../lib/utils"
6
+
7
+ export interface EmailSendBarAction {
8
+ key: string
9
+ label: string
10
+ icon?: React.ReactNode
11
+ onClick?: () => void
12
+ disabled?: boolean
13
+ }
14
+
15
+ export interface EmailSendBarProps {
16
+ primaryActions?: EmailSendBarAction[]
17
+ secondaryActions?: EmailSendBarAction[]
18
+ sendLabel?: string
19
+ sendIcon?: React.ReactNode
20
+ sendDisabled?: boolean
21
+ onSend?: () => void
22
+ sendHint?: React.ReactNode
23
+ className?: string
24
+ }
25
+
26
+ function GhostAction({ action }: { action: EmailSendBarAction }) {
27
+ return (
28
+ <button
29
+ type="button"
30
+ onClick={action.onClick}
31
+ disabled={action.disabled}
32
+ className="group inline-flex items-center gap-[7px] h-[30px] px-[11px] rounded-[7px] border border-transparent text-xs font-medium text-muted-foreground transition-all duration-[120ms] hover:bg-background hover:text-foreground hover:border-border hover:shadow-sm disabled:opacity-50 disabled:pointer-events-none"
33
+ >
34
+ {action.icon ? (
35
+ <span className="text-muted-foreground/60 group-hover:text-muted-foreground">
36
+ {action.icon}
37
+ </span>
38
+ ) : null}
39
+ {action.label}
40
+ </button>
41
+ )
42
+ }
43
+
44
+ export function EmailSendBar({
45
+ primaryActions = [],
46
+ secondaryActions = [],
47
+ sendLabel = "Send",
48
+ sendIcon,
49
+ sendDisabled = false,
50
+ onSend,
51
+ sendHint,
52
+ className,
53
+ }: EmailSendBarProps) {
54
+ const hasPrimary = primaryActions.length > 0
55
+ const hasSecondary = secondaryActions.length > 0
56
+
57
+ return (
58
+ <div
59
+ className={cn(
60
+ "flex items-center gap-1.5 px-3.5 py-2.5 border-t bg-muted/20",
61
+ className,
62
+ )}
63
+ >
64
+ {primaryActions.map((action) => (
65
+ <GhostAction key={action.key} action={action} />
66
+ ))}
67
+
68
+ {hasPrimary && hasSecondary ? (
69
+ <div className="w-px h-[18px] bg-border mx-1" />
70
+ ) : null}
71
+
72
+ {secondaryActions.map((action) => (
73
+ <GhostAction key={action.key} action={action} />
74
+ ))}
75
+
76
+ <div className="flex-1" />
77
+
78
+ {sendHint ? (
79
+ <span className="text-[11px] text-muted-foreground mr-1">
80
+ {sendHint}
81
+ </span>
82
+ ) : null}
83
+
84
+ <button
85
+ type="button"
86
+ onClick={onSend}
87
+ disabled={sendDisabled}
88
+ className="inline-flex items-center gap-2 h-8 px-3.5 rounded-md bg-foreground text-background text-xs font-semibold shadow-sm hover:bg-foreground/90 disabled:bg-muted disabled:text-muted-foreground disabled:shadow-none"
89
+ >
90
+ {sendIcon}
91
+ {sendLabel}
92
+ </button>
93
+ </div>
94
+ )
95
+ }