@handled-ai/design-system 0.9.28 → 0.11.0
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.
- package/dist/components/account-contacts-popover.d.ts +22 -0
- package/dist/components/account-contacts-popover.js +180 -0
- package/dist/components/account-contacts-popover.js.map +1 -0
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/compliance-badge.d.ts +10 -0
- package/dist/components/compliance-badge.js +95 -0
- package/dist/components/compliance-badge.js.map +1 -0
- package/dist/components/contact-chip.d.ts +12 -0
- package/dist/components/contact-chip.js +98 -0
- package/dist/components/contact-chip.js.map +1 -0
- package/dist/components/draft-feedback-inline.d.ts +11 -0
- package/dist/components/draft-feedback-inline.js +153 -0
- package/dist/components/draft-feedback-inline.js.map +1 -0
- package/dist/components/empty-state.d.ts +11 -0
- package/dist/components/empty-state.js +46 -0
- package/dist/components/empty-state.js.map +1 -0
- package/dist/components/filter-chip.d.ts +9 -0
- package/dist/components/filter-chip.js +67 -0
- package/dist/components/filter-chip.js.map +1 -0
- package/dist/components/inline-banner.d.ts +10 -0
- package/dist/components/inline-banner.js +97 -0
- package/dist/components/inline-banner.js.map +1 -0
- package/dist/components/kbd-hint.d.ts +5 -0
- package/dist/components/kbd-hint.js +51 -0
- package/dist/components/kbd-hint.js.map +1 -0
- package/dist/components/rich-text-toolbar.d.ts +9 -0
- package/dist/components/rich-text-toolbar.js +103 -0
- package/dist/components/rich-text-toolbar.js.map +1 -0
- package/dist/components/step-timeline.d.ts +19 -0
- package/dist/components/step-timeline.js +134 -0
- package/dist/components/step-timeline.js.map +1 -0
- package/dist/components/sticky-action-bar.d.ts +10 -0
- package/dist/components/sticky-action-bar.js +56 -0
- package/dist/components/sticky-action-bar.js.map +1 -0
- package/dist/components/suggested-actions.js +2 -304
- package/dist/components/suggested-actions.js.map +1 -1
- package/dist/components/switch.d.ts +6 -0
- package/dist/components/switch.js +66 -0
- package/dist/components/switch.js.map +1 -0
- package/dist/components/variable-autocomplete.d.ts +21 -0
- package/dist/components/variable-autocomplete.js +171 -0
- package/dist/components/variable-autocomplete.js.map +1 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/components/__tests__/compliance-badge.test.tsx +88 -0
- package/src/components/__tests__/contact-chip.test.tsx +88 -0
- package/src/components/__tests__/empty-state.test.tsx +76 -0
- package/src/components/__tests__/filter-chip.test.tsx +73 -0
- package/src/components/__tests__/inline-banner.test.tsx +110 -0
- package/src/components/__tests__/kbd-hint.test.tsx +29 -0
- package/src/components/__tests__/rich-text-toolbar.test.tsx +92 -0
- package/src/components/__tests__/step-timeline.test.tsx +174 -0
- package/src/components/__tests__/sticky-action-bar.test.tsx +52 -0
- package/src/components/__tests__/switch.test.tsx +39 -0
- package/src/components/__tests__/variable-autocomplete.test.tsx +155 -0
- package/src/components/account-contacts-popover.tsx +192 -0
- package/src/components/compliance-badge.tsx +68 -0
- package/src/components/contact-chip.tsx +68 -0
- package/src/components/draft-feedback-inline.tsx +193 -0
- package/src/components/empty-state.tsx +37 -0
- package/src/components/filter-chip.tsx +37 -0
- package/src/components/inline-banner.tsx +69 -0
- package/src/components/kbd-hint.tsx +21 -0
- package/src/components/rich-text-toolbar.tsx +90 -0
- package/src/components/step-timeline.tsx +149 -0
- package/src/components/sticky-action-bar.tsx +36 -0
- package/src/components/suggested-actions.tsx +2 -363
- package/src/components/switch.tsx +29 -0
- package/src/components/variable-autocomplete.tsx +178 -0
- package/src/index.ts +16 -1
- package/src/styles/globals.css +60 -0
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
ThumbsUp,
|
|
19
19
|
ThumbsDown,
|
|
20
20
|
Check,
|
|
21
|
-
RefreshCw,
|
|
22
21
|
ArrowLeft,
|
|
23
22
|
Mail,
|
|
24
23
|
Phone,
|
|
@@ -31,21 +30,8 @@ import {
|
|
|
31
30
|
Globe,
|
|
32
31
|
} from "lucide-react"
|
|
33
32
|
import { Button } from "./button"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Brand Icons (image-based from registry)
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
function BrandIcon({ src, alt, className }: { src: string; alt: string; className?: string }) {
|
|
40
|
-
return (
|
|
41
|
-
<img
|
|
42
|
-
src={src}
|
|
43
|
-
alt={alt}
|
|
44
|
-
className={`${className ?? ""} object-contain`}
|
|
45
|
-
draggable={false}
|
|
46
|
-
/>
|
|
47
|
-
)
|
|
48
|
-
}
|
|
33
|
+
import { DraftFeedbackInline } from "./draft-feedback-inline"
|
|
34
|
+
import { AccountContactsPopover, BrandIcon } from "./account-contacts-popover"
|
|
49
35
|
|
|
50
36
|
export interface SuggestedActionsIconMap {
|
|
51
37
|
gmail?: string
|
|
@@ -248,186 +234,6 @@ function AiEditPanel({ onApply }: { onApply?: (pills: string[], description: str
|
|
|
248
234
|
)
|
|
249
235
|
}
|
|
250
236
|
|
|
251
|
-
// ---------------------------------------------------------------------------
|
|
252
|
-
// DraftFeedbackInline
|
|
253
|
-
// ---------------------------------------------------------------------------
|
|
254
|
-
|
|
255
|
-
const positivePills = ["Tone", "Personalization", "Length", "CTA", "Other"]
|
|
256
|
-
const negativePills = ["Too formal", "Too casual", "Too long", "Missing context", "Wrong angle", "Factual error", "Other"]
|
|
257
|
-
|
|
258
|
-
function DraftFeedbackInline({
|
|
259
|
-
onRegenerateRequest,
|
|
260
|
-
onSubmitFeedback,
|
|
261
|
-
onDiscardRequest,
|
|
262
|
-
}: {
|
|
263
|
-
onRegenerateRequest?: (pills: string[], detail: string) => void
|
|
264
|
-
onSubmitFeedback?: (type: "up" | "down", pills: string[], detail: string) => void
|
|
265
|
-
onDiscardRequest?: (pills: string[], detail: string) => void
|
|
266
|
-
}) {
|
|
267
|
-
const [thumbState, setThumbState] = React.useState<"up" | "down" | null>(null)
|
|
268
|
-
const [selectedPills, setSelectedPills] = React.useState<string[]>([])
|
|
269
|
-
const [detailText, setDetailText] = React.useState("")
|
|
270
|
-
const [noted, setNoted] = React.useState(false)
|
|
271
|
-
const [regenerated, setRegenerated] = React.useState(false)
|
|
272
|
-
|
|
273
|
-
const togglePill = React.useCallback((pill: string) => {
|
|
274
|
-
setSelectedPills((prev) => (prev.includes(pill) ? prev.filter((p) => p !== pill) : [...prev, pill]))
|
|
275
|
-
}, [])
|
|
276
|
-
|
|
277
|
-
const handleSubmit = React.useCallback(() => {
|
|
278
|
-
if (!thumbState) return
|
|
279
|
-
onSubmitFeedback?.(thumbState, selectedPills, detailText)
|
|
280
|
-
setNoted(true)
|
|
281
|
-
setTimeout(() => {
|
|
282
|
-
setThumbState(null)
|
|
283
|
-
setSelectedPills([])
|
|
284
|
-
setDetailText("")
|
|
285
|
-
setNoted(false)
|
|
286
|
-
}, 3000)
|
|
287
|
-
}, [thumbState, selectedPills, detailText, onSubmitFeedback])
|
|
288
|
-
|
|
289
|
-
const handleRegenerate = React.useCallback(() => {
|
|
290
|
-
if (!thumbState) return
|
|
291
|
-
onRegenerateRequest?.(selectedPills, detailText)
|
|
292
|
-
setRegenerated(true)
|
|
293
|
-
setTimeout(() => {
|
|
294
|
-
setThumbState(null)
|
|
295
|
-
setSelectedPills([])
|
|
296
|
-
setDetailText("")
|
|
297
|
-
setRegenerated(false)
|
|
298
|
-
}, 3000)
|
|
299
|
-
}, [thumbState, selectedPills, detailText, onRegenerateRequest])
|
|
300
|
-
|
|
301
|
-
const handleDiscard = React.useCallback(() => {
|
|
302
|
-
if (!thumbState) return
|
|
303
|
-
onDiscardRequest?.(selectedPills, detailText)
|
|
304
|
-
}, [thumbState, selectedPills, detailText, onDiscardRequest])
|
|
305
|
-
|
|
306
|
-
if (noted) {
|
|
307
|
-
return (
|
|
308
|
-
<div className="flex items-center gap-1.5 py-1 animate-in fade-in slide-in-from-top-1 duration-200">
|
|
309
|
-
<Check className="w-3.5 h-3.5 text-emerald-500" />
|
|
310
|
-
<span className="text-xs text-muted-foreground">Feedback recorded</span>
|
|
311
|
-
</div>
|
|
312
|
-
)
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (regenerated) {
|
|
316
|
-
return (
|
|
317
|
-
<div className="py-2 animate-in fade-in slide-in-from-top-1 duration-200">
|
|
318
|
-
<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">
|
|
319
|
-
<RefreshCw className="w-3 h-3 text-indigo-500 animate-spin" />
|
|
320
|
-
<span className="text-xs font-medium text-indigo-600 dark:text-indigo-400">Regenerating draft...</span>
|
|
321
|
-
</div>
|
|
322
|
-
</div>
|
|
323
|
-
)
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return (
|
|
327
|
-
<div className="space-y-0">
|
|
328
|
-
<div className="flex items-center justify-between">
|
|
329
|
-
<span className="text-sm text-foreground font-medium">How's this draft?</span>
|
|
330
|
-
<div className="flex gap-1">
|
|
331
|
-
<button
|
|
332
|
-
onClick={() => {
|
|
333
|
-
setThumbState(thumbState === "up" ? null : "up")
|
|
334
|
-
setSelectedPills([])
|
|
335
|
-
setDetailText("")
|
|
336
|
-
}}
|
|
337
|
-
className={`p-1.5 rounded transition-colors ${
|
|
338
|
-
thumbState === "up"
|
|
339
|
-
? "bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400"
|
|
340
|
-
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
|
341
|
-
}`}
|
|
342
|
-
>
|
|
343
|
-
<ThumbsUp className="w-4 h-4" fill={thumbState === "up" ? "currentColor" : "none"} />
|
|
344
|
-
</button>
|
|
345
|
-
<button
|
|
346
|
-
onClick={() => {
|
|
347
|
-
setThumbState(thumbState === "down" ? null : "down")
|
|
348
|
-
setSelectedPills([])
|
|
349
|
-
setDetailText("")
|
|
350
|
-
}}
|
|
351
|
-
className={`p-1.5 rounded transition-colors ${
|
|
352
|
-
thumbState === "down"
|
|
353
|
-
? "bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400"
|
|
354
|
-
: "hover:bg-muted text-muted-foreground hover:text-foreground"
|
|
355
|
-
}`}
|
|
356
|
-
>
|
|
357
|
-
<ThumbsDown className="w-4 h-4" fill={thumbState === "down" ? "currentColor" : "none"} />
|
|
358
|
-
</button>
|
|
359
|
-
</div>
|
|
360
|
-
</div>
|
|
361
|
-
|
|
362
|
-
{thumbState && (
|
|
363
|
-
<div className="pt-3 space-y-3 animate-in fade-in slide-in-from-top-2 duration-200">
|
|
364
|
-
<div>
|
|
365
|
-
<span className="text-xs text-muted-foreground mb-2 block font-medium">
|
|
366
|
-
{thumbState === "up" ? "What worked well?" : "What needs improvement?"}
|
|
367
|
-
</span>
|
|
368
|
-
<div className="flex flex-wrap gap-1.5">
|
|
369
|
-
{(thumbState === "up" ? positivePills : negativePills).map((pill) => (
|
|
370
|
-
<button
|
|
371
|
-
key={pill}
|
|
372
|
-
onClick={() => togglePill(pill)}
|
|
373
|
-
className={`px-2.5 py-1 rounded-full text-[11px] font-medium border transition-colors ${
|
|
374
|
-
selectedPills.includes(pill)
|
|
375
|
-
? thumbState === "up"
|
|
376
|
-
? "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-300 dark:border-emerald-800"
|
|
377
|
-
: "bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-800"
|
|
378
|
-
: "bg-background text-muted-foreground border-border hover:bg-muted/50 hover:text-foreground"
|
|
379
|
-
}`}
|
|
380
|
-
>
|
|
381
|
-
{pill}
|
|
382
|
-
</button>
|
|
383
|
-
))}
|
|
384
|
-
</div>
|
|
385
|
-
</div>
|
|
386
|
-
|
|
387
|
-
<textarea
|
|
388
|
-
value={detailText}
|
|
389
|
-
onChange={(e) => setDetailText(e.target.value)}
|
|
390
|
-
placeholder={thumbState === "up" ? "Add specific praise (optional)..." : "Provide specific instructions (optional)..."}
|
|
391
|
-
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]"
|
|
392
|
-
/>
|
|
393
|
-
|
|
394
|
-
<div className="flex items-center gap-2 pt-1">
|
|
395
|
-
{thumbState === "down" ? (
|
|
396
|
-
<>
|
|
397
|
-
<button
|
|
398
|
-
onClick={handleRegenerate}
|
|
399
|
-
disabled={selectedPills.length === 0 && detailText.length === 0}
|
|
400
|
-
className={`flex-1 py-1.5 rounded-md text-xs font-semibold transition-colors flex items-center justify-center gap-1.5 ${
|
|
401
|
-
selectedPills.length > 0 || detailText.length > 0
|
|
402
|
-
? "bg-foreground text-background hover:bg-foreground/90"
|
|
403
|
-
: "bg-muted text-muted-foreground cursor-not-allowed"
|
|
404
|
-
}`}
|
|
405
|
-
>
|
|
406
|
-
<RefreshCw className="w-3 h-3" />
|
|
407
|
-
Regenerate draft
|
|
408
|
-
</button>
|
|
409
|
-
<button
|
|
410
|
-
onClick={handleDiscard}
|
|
411
|
-
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"
|
|
412
|
-
>
|
|
413
|
-
Discard draft
|
|
414
|
-
</button>
|
|
415
|
-
</>
|
|
416
|
-
) : (
|
|
417
|
-
<button
|
|
418
|
-
onClick={handleSubmit}
|
|
419
|
-
className="flex-1 py-1.5 rounded-md text-xs font-semibold transition-colors bg-foreground text-background hover:bg-foreground/90 border-transparent"
|
|
420
|
-
>
|
|
421
|
-
Submit feedback
|
|
422
|
-
</button>
|
|
423
|
-
)}
|
|
424
|
-
</div>
|
|
425
|
-
</div>
|
|
426
|
-
)}
|
|
427
|
-
</div>
|
|
428
|
-
)
|
|
429
|
-
}
|
|
430
|
-
|
|
431
237
|
// ---------------------------------------------------------------------------
|
|
432
238
|
// EditorToolbar
|
|
433
239
|
// ---------------------------------------------------------------------------
|
|
@@ -696,173 +502,6 @@ function ContactCard({
|
|
|
696
502
|
)
|
|
697
503
|
}
|
|
698
504
|
|
|
699
|
-
// ---------------------------------------------------------------------------
|
|
700
|
-
// AccountContactsPopover
|
|
701
|
-
// ---------------------------------------------------------------------------
|
|
702
|
-
|
|
703
|
-
function AccountContactsPopover({
|
|
704
|
-
contacts,
|
|
705
|
-
onSelect,
|
|
706
|
-
onSelectTo,
|
|
707
|
-
onSelectCc,
|
|
708
|
-
onSelectBcc,
|
|
709
|
-
onViewAll,
|
|
710
|
-
onOpenRecentActivity,
|
|
711
|
-
trigger,
|
|
712
|
-
iconMap,
|
|
713
|
-
}: {
|
|
714
|
-
contacts: SuggestedContact[]
|
|
715
|
-
onSelect: (contact: SuggestedContact) => void
|
|
716
|
-
onSelectTo?: (contact: SuggestedContact) => void
|
|
717
|
-
onSelectCc?: (contact: SuggestedContact) => void
|
|
718
|
-
onSelectBcc?: (contact: SuggestedContact) => void
|
|
719
|
-
onViewAll?: () => void
|
|
720
|
-
onOpenRecentActivity?: () => void
|
|
721
|
-
trigger: React.ReactNode
|
|
722
|
-
iconMap?: SuggestedActionsIconMap
|
|
723
|
-
}) {
|
|
724
|
-
const [open, setOpen] = React.useState(false)
|
|
725
|
-
const triggerRef = React.useRef<HTMLDivElement>(null)
|
|
726
|
-
const [popoverStyle, setPopoverStyle] = React.useState<React.CSSProperties>({})
|
|
727
|
-
|
|
728
|
-
React.useEffect(() => {
|
|
729
|
-
if (open && triggerRef.current) {
|
|
730
|
-
const rect = triggerRef.current.getBoundingClientRect()
|
|
731
|
-
const popoverWidth = Math.min(448, window.innerWidth - 32)
|
|
732
|
-
let left = rect.right - popoverWidth
|
|
733
|
-
if (left < 16) left = 16
|
|
734
|
-
if (left + popoverWidth > window.innerWidth - 16) left = window.innerWidth - 16 - popoverWidth
|
|
735
|
-
setPopoverStyle({ position: "fixed", top: rect.bottom + 4, left })
|
|
736
|
-
}
|
|
737
|
-
}, [open])
|
|
738
|
-
|
|
739
|
-
return (
|
|
740
|
-
<div>
|
|
741
|
-
<div ref={triggerRef} onClick={() => setOpen(!open)}>{trigger}</div>
|
|
742
|
-
{open && (
|
|
743
|
-
<>
|
|
744
|
-
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
|
|
745
|
-
<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">
|
|
746
|
-
<div className="px-3 py-1.5 text-[11px] font-medium text-muted-foreground/60 uppercase tracking-wide">
|
|
747
|
-
Account Contacts
|
|
748
|
-
</div>
|
|
749
|
-
<div className="max-h-48 overflow-y-auto">
|
|
750
|
-
{contacts.map((c, i) => (
|
|
751
|
-
<div
|
|
752
|
-
key={i}
|
|
753
|
-
role="button"
|
|
754
|
-
onClick={() => { (onSelectTo ?? onSelect)(c); setOpen(false) }}
|
|
755
|
-
className="flex items-center gap-3 w-full px-3 py-2 text-left hover:bg-muted/50 transition-colors cursor-pointer"
|
|
756
|
-
>
|
|
757
|
-
<div className="w-7 h-7 rounded-full bg-muted flex items-center justify-center text-[10px] font-medium text-muted-foreground shrink-0">
|
|
758
|
-
{c.name.split(" ").map((n) => n[0]).join("")}
|
|
759
|
-
</div>
|
|
760
|
-
<div className="flex-1 min-w-0 overflow-hidden">
|
|
761
|
-
<div className="truncate text-sm font-medium text-foreground">{c.name}</div>
|
|
762
|
-
<div className="truncate text-xs text-muted-foreground leading-tight">
|
|
763
|
-
{c.role} · {c.email ?? c.emails?.[0] ?? c.phone ?? c.phones?.[0] ?? ""}
|
|
764
|
-
</div>
|
|
765
|
-
{c.lastActivity && (
|
|
766
|
-
<button
|
|
767
|
-
type="button"
|
|
768
|
-
onClick={(e) => {
|
|
769
|
-
e.stopPropagation()
|
|
770
|
-
onOpenRecentActivity?.()
|
|
771
|
-
setOpen(false)
|
|
772
|
-
}}
|
|
773
|
-
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"
|
|
774
|
-
>
|
|
775
|
-
<Clock className="w-3 h-3 shrink-0" />
|
|
776
|
-
<span className="shrink-0 font-medium">Last activity</span>
|
|
777
|
-
<span className="shrink-0 text-muted-foreground/60">·</span>
|
|
778
|
-
<span className="shrink-0">{c.lastActivity.date}</span>
|
|
779
|
-
<span className="shrink-0 text-muted-foreground/60">·</span>
|
|
780
|
-
<span className="truncate capitalize">{c.lastActivity.type}</span>
|
|
781
|
-
</button>
|
|
782
|
-
)}
|
|
783
|
-
</div>
|
|
784
|
-
<div className="ml-2 flex items-center gap-1.5 shrink-0">
|
|
785
|
-
{onSelectTo && (
|
|
786
|
-
<button
|
|
787
|
-
type="button"
|
|
788
|
-
onClick={(e) => {
|
|
789
|
-
e.stopPropagation()
|
|
790
|
-
onSelectTo(c)
|
|
791
|
-
setOpen(false)
|
|
792
|
-
}}
|
|
793
|
-
className="h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
|
794
|
-
>
|
|
795
|
-
To
|
|
796
|
-
</button>
|
|
797
|
-
)}
|
|
798
|
-
{onSelectCc && (
|
|
799
|
-
<button
|
|
800
|
-
type="button"
|
|
801
|
-
onClick={(e) => {
|
|
802
|
-
e.stopPropagation()
|
|
803
|
-
onSelectCc(c)
|
|
804
|
-
setOpen(false)
|
|
805
|
-
}}
|
|
806
|
-
className="h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
|
807
|
-
>
|
|
808
|
-
Cc
|
|
809
|
-
</button>
|
|
810
|
-
)}
|
|
811
|
-
{onSelectBcc && (
|
|
812
|
-
<button
|
|
813
|
-
type="button"
|
|
814
|
-
onClick={(e) => {
|
|
815
|
-
e.stopPropagation()
|
|
816
|
-
onSelectBcc(c)
|
|
817
|
-
setOpen(false)
|
|
818
|
-
}}
|
|
819
|
-
className="h-6 rounded border border-border bg-background px-1.5 text-[10px] text-muted-foreground hover:text-foreground hover:bg-muted/40"
|
|
820
|
-
>
|
|
821
|
-
Bcc
|
|
822
|
-
</button>
|
|
823
|
-
)}
|
|
824
|
-
<button
|
|
825
|
-
type="button"
|
|
826
|
-
onClick={(e) => {
|
|
827
|
-
e.stopPropagation()
|
|
828
|
-
if (c.salesforceUrl) {
|
|
829
|
-
window.open(c.salesforceUrl, "_blank", "noopener,noreferrer")
|
|
830
|
-
} else {
|
|
831
|
-
onViewAll?.()
|
|
832
|
-
}
|
|
833
|
-
}}
|
|
834
|
-
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"
|
|
835
|
-
aria-label={`Open ${c.name} in Salesforce`}
|
|
836
|
-
>
|
|
837
|
-
{iconMap?.salesforce ? (
|
|
838
|
-
<BrandIcon src={iconMap.salesforce} alt="Salesforce" className="w-3.5 h-3.5" />
|
|
839
|
-
) : (
|
|
840
|
-
<ExternalLink className="w-3.5 h-3.5 text-muted-foreground" />
|
|
841
|
-
)}
|
|
842
|
-
</button>
|
|
843
|
-
</div>
|
|
844
|
-
</div>
|
|
845
|
-
))}
|
|
846
|
-
</div>
|
|
847
|
-
{onViewAll && (
|
|
848
|
-
<>
|
|
849
|
-
<div className="h-px bg-border mx-3 my-1" />
|
|
850
|
-
<button
|
|
851
|
-
onClick={() => { onViewAll(); setOpen(false) }}
|
|
852
|
-
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"
|
|
853
|
-
>
|
|
854
|
-
<ExternalLink className="w-3 h-3" />
|
|
855
|
-
View all contacts
|
|
856
|
-
</button>
|
|
857
|
-
</>
|
|
858
|
-
)}
|
|
859
|
-
</div>
|
|
860
|
-
</>
|
|
861
|
-
)}
|
|
862
|
-
</div>
|
|
863
|
-
)
|
|
864
|
-
}
|
|
865
|
-
|
|
866
505
|
// ---------------------------------------------------------------------------
|
|
867
506
|
// EmailHeader (Notion Mail style)
|
|
868
507
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Switch as SwitchPrimitive } from "radix-ui"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
|
|
8
|
+
function Switch({
|
|
9
|
+
className,
|
|
10
|
+
...props
|
|
11
|
+
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
|
12
|
+
return (
|
|
13
|
+
<SwitchPrimitive.Root
|
|
14
|
+
data-slot="switch"
|
|
15
|
+
className={cn(
|
|
16
|
+
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-switch-background",
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
<SwitchPrimitive.Thumb
|
|
22
|
+
data-slot="switch-thumb"
|
|
23
|
+
className="pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
|
24
|
+
/>
|
|
25
|
+
</SwitchPrimitive.Root>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { Switch }
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
|
|
7
|
+
interface VariableDef {
|
|
8
|
+
name: string
|
|
9
|
+
description?: string
|
|
10
|
+
source?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface VariableGroup {
|
|
14
|
+
label: string
|
|
15
|
+
variables: VariableDef[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface VariableAutocompleteProps
|
|
19
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect"> {
|
|
20
|
+
groups: VariableGroup[]
|
|
21
|
+
filter?: string
|
|
22
|
+
onSelect: (variable: VariableDef) => void
|
|
23
|
+
onClose: () => void
|
|
24
|
+
maxResults?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function VariableAutocomplete({
|
|
28
|
+
groups,
|
|
29
|
+
filter = "",
|
|
30
|
+
onSelect,
|
|
31
|
+
onClose,
|
|
32
|
+
maxResults = 10,
|
|
33
|
+
className,
|
|
34
|
+
...rest
|
|
35
|
+
}: VariableAutocompleteProps) {
|
|
36
|
+
const containerRef = React.useRef<HTMLDivElement>(null)
|
|
37
|
+
const [activeIndex, setActiveIndex] = React.useState(-1)
|
|
38
|
+
|
|
39
|
+
// Build filtered results preserving group structure
|
|
40
|
+
const filteredGroups = React.useMemo(
|
|
41
|
+
() =>
|
|
42
|
+
groups
|
|
43
|
+
.map((group) => ({
|
|
44
|
+
...group,
|
|
45
|
+
variables: group.variables.filter((v) =>
|
|
46
|
+
v.name.toLowerCase().startsWith(filter.toLowerCase())
|
|
47
|
+
),
|
|
48
|
+
}))
|
|
49
|
+
.filter((group) => group.variables.length > 0),
|
|
50
|
+
[groups, filter]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// Flatten for indexing / keyboard navigation, respecting maxResults
|
|
54
|
+
const allFiltered = React.useMemo(() => {
|
|
55
|
+
const result: { group: VariableGroup; variable: VariableDef }[] = []
|
|
56
|
+
for (const group of filteredGroups) {
|
|
57
|
+
for (const variable of group.variables) {
|
|
58
|
+
if (result.length >= maxResults) break
|
|
59
|
+
result.push({ group, variable })
|
|
60
|
+
}
|
|
61
|
+
if (result.length >= maxResults) break
|
|
62
|
+
}
|
|
63
|
+
return result
|
|
64
|
+
}, [filteredGroups, maxResults])
|
|
65
|
+
|
|
66
|
+
// Reset active index when results change
|
|
67
|
+
React.useEffect(() => {
|
|
68
|
+
setActiveIndex(-1)
|
|
69
|
+
}, [filter])
|
|
70
|
+
|
|
71
|
+
React.useEffect(() => {
|
|
72
|
+
function handleMouseDown(e: MouseEvent) {
|
|
73
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
74
|
+
onClose()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function handleKeyDown(e: KeyboardEvent) {
|
|
78
|
+
if (e.key === "Escape") {
|
|
79
|
+
onClose()
|
|
80
|
+
}
|
|
81
|
+
if (e.key === "ArrowDown") {
|
|
82
|
+
e.preventDefault()
|
|
83
|
+
setActiveIndex((prev) => (prev < allFiltered.length - 1 ? prev + 1 : 0))
|
|
84
|
+
}
|
|
85
|
+
if (e.key === "ArrowUp") {
|
|
86
|
+
e.preventDefault()
|
|
87
|
+
setActiveIndex((prev) => (prev > 0 ? prev - 1 : allFiltered.length - 1))
|
|
88
|
+
}
|
|
89
|
+
if (e.key === "Enter" && activeIndex >= 0 && activeIndex < allFiltered.length) {
|
|
90
|
+
e.preventDefault()
|
|
91
|
+
onSelect(allFiltered[activeIndex].variable)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
document.addEventListener("mousedown", handleMouseDown)
|
|
95
|
+
document.addEventListener("keydown", handleKeyDown)
|
|
96
|
+
return () => {
|
|
97
|
+
document.removeEventListener("mousedown", handleMouseDown)
|
|
98
|
+
document.removeEventListener("keydown", handleKeyDown)
|
|
99
|
+
}
|
|
100
|
+
}, [onClose, onSelect, allFiltered, activeIndex])
|
|
101
|
+
|
|
102
|
+
if (allFiltered.length === 0) {
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Group the capped results back for rendering
|
|
107
|
+
const groupsToRender: { label: string; items: { variable: VariableDef; flatIndex: number }[] }[] = []
|
|
108
|
+
let flatIndex = 0
|
|
109
|
+
for (const group of filteredGroups) {
|
|
110
|
+
const items: { variable: VariableDef; flatIndex: number }[] = []
|
|
111
|
+
for (const variable of group.variables) {
|
|
112
|
+
if (flatIndex >= maxResults) break
|
|
113
|
+
items.push({ variable, flatIndex })
|
|
114
|
+
flatIndex++
|
|
115
|
+
}
|
|
116
|
+
if (items.length > 0) {
|
|
117
|
+
groupsToRender.push({ label: group.label, items })
|
|
118
|
+
}
|
|
119
|
+
if (flatIndex >= maxResults) break
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
ref={containerRef}
|
|
125
|
+
data-slot="variable-autocomplete"
|
|
126
|
+
className={cn(
|
|
127
|
+
"absolute z-50 bg-popover border border-border rounded-lg shadow-lg w-64 overflow-hidden",
|
|
128
|
+
className
|
|
129
|
+
)}
|
|
130
|
+
{...rest}
|
|
131
|
+
>
|
|
132
|
+
<div data-slot="variable-autocomplete-header" className="px-3 py-1.5 border-b border-border">
|
|
133
|
+
<span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
|
|
134
|
+
Insert variable
|
|
135
|
+
</span>
|
|
136
|
+
</div>
|
|
137
|
+
<div role="listbox" aria-label="Variables" className="overflow-y-auto" style={{ maxHeight: "312px" }}>
|
|
138
|
+
{groupsToRender.map((group) => (
|
|
139
|
+
<div key={group.label} role="group" aria-label={group.label}>
|
|
140
|
+
<div data-slot="variable-autocomplete-group-label" className="px-3 py-1 text-[10px] font-medium text-muted-foreground/60 uppercase tracking-wider">
|
|
141
|
+
{group.label}
|
|
142
|
+
</div>
|
|
143
|
+
{group.items.map(({ variable, flatIndex: idx }) => (
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
key={`${group.label}:${variable.name}`}
|
|
147
|
+
role="option"
|
|
148
|
+
aria-selected={idx === activeIndex}
|
|
149
|
+
data-slot="variable-autocomplete-item"
|
|
150
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
151
|
+
onClick={() => onSelect(variable)}
|
|
152
|
+
onMouseEnter={() => setActiveIndex(idx)}
|
|
153
|
+
className={cn(
|
|
154
|
+
"w-full text-left px-3 py-2 cursor-pointer transition-colors flex items-start justify-between gap-2",
|
|
155
|
+
idx === activeIndex ? "bg-muted/50" : "hover:bg-muted/30"
|
|
156
|
+
)}
|
|
157
|
+
>
|
|
158
|
+
<div className="min-w-0">
|
|
159
|
+
<div className="text-xs font-mono text-blue-600">{`{{${variable.name}}}`}</div>
|
|
160
|
+
{variable.description && (
|
|
161
|
+
<div className="text-[10px] text-muted-foreground">{variable.description}</div>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
{variable.source && (
|
|
165
|
+
<span className="text-[9px] px-1.5 py-0.5 rounded bg-muted/30 text-muted-foreground shrink-0 mt-0.5 capitalize">
|
|
166
|
+
{variable.source}
|
|
167
|
+
</span>
|
|
168
|
+
)}
|
|
169
|
+
</button>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export { VariableAutocomplete, type VariableAutocompleteProps, type VariableDef, type VariableGroup }
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,8 @@ export * from "./components/avatar"
|
|
|
19
19
|
export * from "./components/badge"
|
|
20
20
|
export * from "./components/button"
|
|
21
21
|
export * from "./components/card"
|
|
22
|
+
export * from "./components/compliance-badge"
|
|
23
|
+
export * from "./components/contact-chip"
|
|
22
24
|
export * from "./components/contact-list"
|
|
23
25
|
export * from "./components/dashboard-cards"
|
|
24
26
|
export * from "./components/data-table"
|
|
@@ -29,15 +31,19 @@ export * from "./components/data-table-toolbar"
|
|
|
29
31
|
export * from "./components/detail-view"
|
|
30
32
|
export * from "./components/dialog"
|
|
31
33
|
export * from "./components/dropdown-menu"
|
|
34
|
+
export * from "./components/empty-state"
|
|
32
35
|
export * from "./components/entity-panel"
|
|
36
|
+
export * from "./components/filter-chip"
|
|
33
37
|
export * from "./components/inbox-row"
|
|
34
38
|
export * from "./components/inbox-toolbar"
|
|
39
|
+
export * from "./components/inline-banner"
|
|
35
40
|
export * from "./components/input"
|
|
36
41
|
export * from "./components/insights-filter-bar"
|
|
37
42
|
export * from "./components/item-list"
|
|
38
43
|
export * from "./components/item-list-display"
|
|
39
44
|
export * from "./components/item-list-filter"
|
|
40
45
|
export * from "./components/item-list-toolbar"
|
|
46
|
+
export * from "./components/kbd-hint"
|
|
41
47
|
export * from "./components/label"
|
|
42
48
|
export * from "./components/message"
|
|
43
49
|
export * from "./components/metric-card"
|
|
@@ -54,6 +60,7 @@ export {
|
|
|
54
60
|
export * from "./components/quick-action-sidebar-nav"
|
|
55
61
|
export * from "./components/recommended-actions-section"
|
|
56
62
|
export * from "./components/report-card"
|
|
63
|
+
export * from "./components/rich-text-toolbar"
|
|
57
64
|
export * from "./components/score-analysis-modal"
|
|
58
65
|
export * from "./components/score-breakdown"
|
|
59
66
|
export * from "./components/score-feedback"
|
|
@@ -65,17 +72,25 @@ export * from "./components/sheet"
|
|
|
65
72
|
export * from "./components/sidebar"
|
|
66
73
|
export * from "./components/signal-feedback-inline"
|
|
67
74
|
export * from "./components/simple-data-table"
|
|
68
|
-
export * from "./components/virtualized-data-table"
|
|
69
75
|
export * from "./components/skeleton"
|
|
70
76
|
export * from "./components/status-badge"
|
|
77
|
+
export * from "./components/step-timeline"
|
|
78
|
+
export * from "./components/sticky-action-bar"
|
|
71
79
|
export * from "./components/styled-bar-list"
|
|
80
|
+
export { DraftFeedbackInline } from "./components/draft-feedback-inline"
|
|
81
|
+
export type { DraftFeedbackInlineProps } from "./components/draft-feedback-inline"
|
|
82
|
+
export { AccountContactsPopover, BrandIcon } from "./components/account-contacts-popover"
|
|
83
|
+
export type { AccountContactsPopoverProps } from "./components/account-contacts-popover"
|
|
72
84
|
export * from "./components/suggested-actions"
|
|
85
|
+
export * from "./components/switch"
|
|
73
86
|
export * from "./components/table"
|
|
74
87
|
export * from "./components/tabs"
|
|
75
88
|
export * from "./components/textarea"
|
|
76
89
|
export * from "./components/timeline-activity"
|
|
77
90
|
export * from "./components/tooltip"
|
|
91
|
+
export * from "./components/variable-autocomplete"
|
|
78
92
|
export * from "./components/view-mode-toggle"
|
|
93
|
+
export * from "./components/virtualized-data-table"
|
|
79
94
|
|
|
80
95
|
// Charts (re-exported for backward compatibility with root imports)
|
|
81
96
|
export * from "./charts/index"
|