@handled-ai/design-system 0.10.0 → 0.11.1

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.
@@ -0,0 +1,192 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ Clock,
6
+ ExternalLink,
7
+ } from "lucide-react"
8
+ import type { SuggestedContact, SuggestedActionsIconMap } from "./suggested-actions"
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // BrandIcon
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export function BrandIcon({ src, alt, className }: { src: string; alt: string; className?: string }) {
15
+ return (
16
+ <img
17
+ src={src}
18
+ alt={alt}
19
+ className={`${className ?? ""} object-contain`}
20
+ draggable={false}
21
+ />
22
+ )
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // AccountContactsPopover
27
+ // ---------------------------------------------------------------------------
28
+
29
+ export interface AccountContactsPopoverProps {
30
+ contacts: SuggestedContact[]
31
+ onSelect: (contact: SuggestedContact) => void
32
+ onSelectTo?: (contact: SuggestedContact) => void
33
+ onSelectCc?: (contact: SuggestedContact) => void
34
+ onSelectBcc?: (contact: SuggestedContact) => void
35
+ onViewAll?: () => void
36
+ onOpenRecentActivity?: () => void
37
+ trigger: React.ReactNode
38
+ iconMap?: SuggestedActionsIconMap
39
+ }
40
+
41
+ export function AccountContactsPopover({
42
+ contacts,
43
+ onSelect,
44
+ onSelectTo,
45
+ onSelectCc,
46
+ onSelectBcc,
47
+ onViewAll,
48
+ onOpenRecentActivity,
49
+ trigger,
50
+ iconMap,
51
+ }: AccountContactsPopoverProps) {
52
+ const [open, setOpen] = React.useState(false)
53
+ const triggerRef = React.useRef<HTMLDivElement>(null)
54
+ const [popoverStyle, setPopoverStyle] = React.useState<React.CSSProperties>({})
55
+
56
+ React.useEffect(() => {
57
+ if (open && triggerRef.current) {
58
+ const rect = triggerRef.current.getBoundingClientRect()
59
+ const popoverWidth = Math.min(448, window.innerWidth - 32)
60
+ let left = rect.right - popoverWidth
61
+ if (left < 16) left = 16
62
+ if (left + popoverWidth > window.innerWidth - 16) left = window.innerWidth - 16 - popoverWidth
63
+ setPopoverStyle({ position: "fixed", top: rect.bottom + 4, left })
64
+ }
65
+ }, [open])
66
+
67
+ return (
68
+ <div>
69
+ <div ref={triggerRef} onClick={() => setOpen(!open)}>{trigger}</div>
70
+ {open && (
71
+ <>
72
+ <div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
73
+ <div style={popoverStyle} className="fixed bg-background border border-border rounded-lg shadow-xl z-50 w-[28rem] max-w-[calc(100vw-2rem)] py-2 animate-in fade-in slide-in-from-top-1 duration-150">
74
+ <div className="px-3 py-1.5 text-[11px] font-medium text-muted-foreground/60 uppercase tracking-wide">
75
+ Account Contacts
76
+ </div>
77
+ <div className="max-h-48 overflow-y-auto">
78
+ {contacts.map((c, i) => (
79
+ <div
80
+ key={i}
81
+ role="button"
82
+ onClick={() => { (onSelectTo ?? onSelect)(c); setOpen(false) }}
83
+ className="flex items-center gap-3 w-full px-3 py-2 text-left hover:bg-muted/50 transition-colors cursor-pointer"
84
+ >
85
+ <div className="w-7 h-7 rounded-full bg-muted flex items-center justify-center text-[10px] font-medium text-muted-foreground shrink-0">
86
+ {c.name.split(" ").map((n) => n[0]).join("")}
87
+ </div>
88
+ <div className="flex-1 min-w-0 overflow-hidden">
89
+ <div className="truncate text-sm font-medium text-foreground">{c.name}</div>
90
+ <div className="truncate text-xs text-muted-foreground leading-tight">
91
+ {c.role} · {c.email ?? c.emails?.[0] ?? c.phone ?? c.phones?.[0] ?? ""}
92
+ </div>
93
+ {c.lastActivity && (
94
+ <button
95
+ type="button"
96
+ onClick={(e) => {
97
+ e.stopPropagation()
98
+ onOpenRecentActivity?.()
99
+ setOpen(false)
100
+ }}
101
+ className="mt-1.5 flex max-w-full items-center gap-1.5 overflow-hidden rounded-md border border-border/70 bg-muted/30 px-2 py-1 text-[11px] text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
102
+ >
103
+ <Clock className="w-3 h-3 shrink-0" />
104
+ <span className="shrink-0 font-medium">Last activity</span>
105
+ <span className="shrink-0 text-muted-foreground/60">·</span>
106
+ <span className="shrink-0">{c.lastActivity.date}</span>
107
+ <span className="shrink-0 text-muted-foreground/60">·</span>
108
+ <span className="truncate capitalize">{c.lastActivity.type}</span>
109
+ </button>
110
+ )}
111
+ </div>
112
+ <div className="ml-2 flex items-center gap-1.5 shrink-0">
113
+ {onSelectTo && (
114
+ <button
115
+ type="button"
116
+ onClick={(e) => {
117
+ e.stopPropagation()
118
+ onSelectTo(c)
119
+ setOpen(false)
120
+ }}
121
+ className="h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40"
122
+ >
123
+ To
124
+ </button>
125
+ )}
126
+ {onSelectCc && (
127
+ <button
128
+ type="button"
129
+ onClick={(e) => {
130
+ e.stopPropagation()
131
+ onSelectCc(c)
132
+ setOpen(false)
133
+ }}
134
+ className="h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40"
135
+ >
136
+ Cc
137
+ </button>
138
+ )}
139
+ {onSelectBcc && (
140
+ <button
141
+ type="button"
142
+ onClick={(e) => {
143
+ e.stopPropagation()
144
+ onSelectBcc(c)
145
+ setOpen(false)
146
+ }}
147
+ className="h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40"
148
+ >
149
+ Bcc
150
+ </button>
151
+ )}
152
+ <button
153
+ type="button"
154
+ onClick={(e) => {
155
+ e.stopPropagation()
156
+ if (c.salesforceUrl) {
157
+ window.open(c.salesforceUrl, "_blank", "noopener,noreferrer")
158
+ } else {
159
+ onViewAll?.()
160
+ }
161
+ }}
162
+ className="h-7 w-7 inline-flex items-center justify-center rounded-md border border-border bg-background hover:bg-muted/40 transition-colors shrink-0"
163
+ aria-label={`Open ${c.name} in Salesforce`}
164
+ >
165
+ {iconMap?.salesforce ? (
166
+ <BrandIcon src={iconMap.salesforce} alt="Salesforce" className="w-3.5 h-3.5" />
167
+ ) : (
168
+ <ExternalLink className="w-3.5 h-3.5 text-muted-foreground" />
169
+ )}
170
+ </button>
171
+ </div>
172
+ </div>
173
+ ))}
174
+ </div>
175
+ {onViewAll && (
176
+ <>
177
+ <div className="h-px bg-border mx-3 my-1" />
178
+ <button
179
+ onClick={() => { onViewAll(); setOpen(false) }}
180
+ className="flex items-center gap-2 w-full px-3 py-2 text-left text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors"
181
+ >
182
+ <ExternalLink className="w-3 h-3" />
183
+ View all contacts
184
+ </button>
185
+ </>
186
+ )}
187
+ </div>
188
+ </>
189
+ )}
190
+ </div>
191
+ )
192
+ }
@@ -0,0 +1,193 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ ThumbsUp,
6
+ ThumbsDown,
7
+ Check,
8
+ RefreshCw,
9
+ } from "lucide-react"
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // DraftFeedbackInline
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const positivePills = ["Tone", "Personalization", "Length", "CTA", "Other"]
16
+ const negativePills = ["Too formal", "Too casual", "Too long", "Missing context", "Wrong angle", "Factual error", "Other"]
17
+
18
+ export interface DraftFeedbackInlineProps {
19
+ initialDirection?: 'up' | 'down' | null
20
+ onRegenerateRequest?: (pills: string[], detail: string) => void
21
+ onSubmitFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
22
+ onDiscardRequest?: (pills: string[], detail: string) => void
23
+ }
24
+
25
+ export function DraftFeedbackInline({
26
+ initialDirection,
27
+ onRegenerateRequest,
28
+ onSubmitFeedback,
29
+ onDiscardRequest,
30
+ }: DraftFeedbackInlineProps) {
31
+ const [thumbState, setThumbState] = React.useState<"up" | "down" | null>(initialDirection ?? null)
32
+ const [selectedPills, setSelectedPills] = React.useState<string[]>([])
33
+ const [detailText, setDetailText] = React.useState("")
34
+ const [noted, setNoted] = React.useState(false)
35
+ const [regenerated, setRegenerated] = React.useState(false)
36
+
37
+ const togglePill = React.useCallback((pill: string) => {
38
+ setSelectedPills((prev) => (prev.includes(pill) ? prev.filter((p) => p !== pill) : [...prev, pill]))
39
+ }, [])
40
+
41
+ const handleSubmit = React.useCallback(() => {
42
+ if (!thumbState) return
43
+ onSubmitFeedback?.(thumbState, selectedPills, detailText)
44
+ setNoted(true)
45
+ setTimeout(() => {
46
+ setThumbState(null)
47
+ setSelectedPills([])
48
+ setDetailText("")
49
+ setNoted(false)
50
+ }, 3000)
51
+ }, [thumbState, selectedPills, detailText, onSubmitFeedback])
52
+
53
+ const handleRegenerate = React.useCallback(() => {
54
+ if (!thumbState) return
55
+ onRegenerateRequest?.(selectedPills, detailText)
56
+ setRegenerated(true)
57
+ setTimeout(() => {
58
+ setThumbState(null)
59
+ setSelectedPills([])
60
+ setDetailText("")
61
+ setRegenerated(false)
62
+ }, 3000)
63
+ }, [thumbState, selectedPills, detailText, onRegenerateRequest])
64
+
65
+ const handleDiscard = React.useCallback(() => {
66
+ if (!thumbState) return
67
+ onDiscardRequest?.(selectedPills, detailText)
68
+ }, [thumbState, selectedPills, detailText, onDiscardRequest])
69
+
70
+ if (noted) {
71
+ return (
72
+ <div className="flex items-center gap-1.5 py-1 animate-in fade-in slide-in-from-top-1 duration-200">
73
+ <Check className="w-3.5 h-3.5 text-emerald-500" />
74
+ <span className="text-xs text-muted-foreground">Feedback recorded</span>
75
+ </div>
76
+ )
77
+ }
78
+
79
+ if (regenerated) {
80
+ return (
81
+ <div className="py-2 animate-in fade-in slide-in-from-top-1 duration-200">
82
+ <div className="flex items-center gap-2 px-3 py-2 rounded-md bg-indigo-50 dark:bg-indigo-950/30 border border-indigo-200 dark:border-indigo-800">
83
+ <RefreshCw className="w-3 h-3 text-indigo-500 animate-spin" />
84
+ <span className="text-xs font-medium text-indigo-600 dark:text-indigo-400">Regenerating draft...</span>
85
+ </div>
86
+ </div>
87
+ )
88
+ }
89
+
90
+ return (
91
+ <div className="space-y-0">
92
+ <div className="flex items-center justify-between">
93
+ <span className="text-sm text-foreground font-medium">How&apos;s this draft?</span>
94
+ <div className="flex gap-1">
95
+ <button
96
+ onClick={() => {
97
+ setThumbState(thumbState === "up" ? null : "up")
98
+ setSelectedPills([])
99
+ setDetailText("")
100
+ }}
101
+ className={`p-1.5 rounded transition-colors ${
102
+ thumbState === "up"
103
+ ? "bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400"
104
+ : "hover:bg-muted text-muted-foreground hover:text-foreground"
105
+ }`}
106
+ >
107
+ <ThumbsUp className="w-4 h-4" fill={thumbState === "up" ? "currentColor" : "none"} />
108
+ </button>
109
+ <button
110
+ onClick={() => {
111
+ setThumbState(thumbState === "down" ? null : "down")
112
+ setSelectedPills([])
113
+ setDetailText("")
114
+ }}
115
+ className={`p-1.5 rounded transition-colors ${
116
+ thumbState === "down"
117
+ ? "bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400"
118
+ : "hover:bg-muted text-muted-foreground hover:text-foreground"
119
+ }`}
120
+ >
121
+ <ThumbsDown className="w-4 h-4" fill={thumbState === "down" ? "currentColor" : "none"} />
122
+ </button>
123
+ </div>
124
+ </div>
125
+
126
+ {thumbState && (
127
+ <div className="pt-3 space-y-3 animate-in fade-in slide-in-from-top-2 duration-200">
128
+ <div>
129
+ <span className="text-xs text-muted-foreground mb-2 block font-medium">
130
+ {thumbState === "up" ? "What worked well?" : "What needs improvement?"}
131
+ </span>
132
+ <div className="flex flex-wrap gap-1.5">
133
+ {(thumbState === "up" ? positivePills : negativePills).map((pill) => (
134
+ <button
135
+ key={pill}
136
+ onClick={() => togglePill(pill)}
137
+ className={`px-2.5 py-1 rounded-full text-[11px] font-medium border transition-colors ${
138
+ selectedPills.includes(pill)
139
+ ? thumbState === "up"
140
+ ? "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-300 dark:border-emerald-800"
141
+ : "bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-800"
142
+ : "bg-background text-muted-foreground border-border hover:bg-muted/50 hover:text-foreground"
143
+ }`}
144
+ >
145
+ {pill}
146
+ </button>
147
+ ))}
148
+ </div>
149
+ </div>
150
+
151
+ <textarea
152
+ value={detailText}
153
+ onChange={(e) => setDetailText(e.target.value)}
154
+ placeholder={thumbState === "up" ? "Add specific praise (optional)..." : "Provide specific instructions (optional)..."}
155
+ className="w-full text-xs bg-background border border-border rounded-md px-2.5 py-2 text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-indigo-500/50 focus:border-indigo-500/50 resize-none min-h-[60px]"
156
+ />
157
+
158
+ <div className="flex items-center gap-2 pt-1">
159
+ {thumbState === "down" ? (
160
+ <>
161
+ <button
162
+ onClick={handleRegenerate}
163
+ disabled={selectedPills.length === 0 && detailText.length === 0}
164
+ className={`flex-1 py-1.5 rounded-md text-xs font-semibold transition-colors flex items-center justify-center gap-1.5 ${
165
+ selectedPills.length > 0 || detailText.length > 0
166
+ ? "bg-foreground text-background hover:bg-foreground/90"
167
+ : "bg-muted text-muted-foreground cursor-not-allowed"
168
+ }`}
169
+ >
170
+ <RefreshCw className="w-3 h-3" />
171
+ Regenerate draft
172
+ </button>
173
+ <button
174
+ onClick={handleDiscard}
175
+ className="flex-1 py-1.5 rounded-md text-xs font-medium transition-colors border bg-background text-foreground border-border hover:bg-muted/50 flex items-center justify-center gap-1.5"
176
+ >
177
+ Discard draft
178
+ </button>
179
+ </>
180
+ ) : (
181
+ <button
182
+ onClick={handleSubmit}
183
+ className="flex-1 py-1.5 rounded-md text-xs font-semibold transition-colors bg-foreground text-background hover:bg-foreground/90 border-transparent"
184
+ >
185
+ Submit feedback
186
+ </button>
187
+ )}
188
+ </div>
189
+ </div>
190
+ )}
191
+ </div>
192
+ )
193
+ }