@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
|
@@ -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'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
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/utils"
|
|
4
|
+
|
|
5
|
+
interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
icon?: React.ReactNode
|
|
7
|
+
title?: string
|
|
8
|
+
description: string
|
|
9
|
+
action?: React.ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function EmptyState({ icon, title, description, action, className, ...rest }: EmptyStateProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div data-slot="empty-state" className={cn("flex flex-col items-center justify-center py-24 gap-3", className)} {...rest}>
|
|
15
|
+
{icon && (
|
|
16
|
+
<div data-slot="empty-state-icon" className="text-muted-foreground/30 [&>svg]:w-12 [&>svg]:h-12">
|
|
17
|
+
{icon}
|
|
18
|
+
</div>
|
|
19
|
+
)}
|
|
20
|
+
{title && (
|
|
21
|
+
<p data-slot="empty-state-title" className="text-sm font-medium text-muted-foreground">
|
|
22
|
+
{title}
|
|
23
|
+
</p>
|
|
24
|
+
)}
|
|
25
|
+
<p data-slot="empty-state-description" className="text-xs text-muted-foreground">
|
|
26
|
+
{description}
|
|
27
|
+
</p>
|
|
28
|
+
{action && (
|
|
29
|
+
<div data-slot="empty-state-action" className="mt-3">
|
|
30
|
+
{action}
|
|
31
|
+
</div>
|
|
32
|
+
)}
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { EmptyState, type EmptyStateProps }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Filter, ChevronDown } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
|
|
8
|
+
interface FilterChipProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
9
|
+
icon?: React.ReactNode
|
|
10
|
+
active?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function FilterChip({ icon, children, onClick, active, className, ...rest }: FilterChipProps) {
|
|
14
|
+
return (
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
data-slot="filter-chip"
|
|
18
|
+
data-active={active ? "" : undefined}
|
|
19
|
+
aria-pressed={active}
|
|
20
|
+
onClick={onClick}
|
|
21
|
+
className={cn(
|
|
22
|
+
"text-[11px] px-2.5 py-1 rounded-md border border-border",
|
|
23
|
+
"text-muted-foreground hover:bg-muted/30 cursor-pointer",
|
|
24
|
+
"inline-flex items-center gap-1.5 transition-colors",
|
|
25
|
+
"data-[active]:border-foreground/20 data-[active]:text-foreground",
|
|
26
|
+
className
|
|
27
|
+
)}
|
|
28
|
+
{...rest}
|
|
29
|
+
>
|
|
30
|
+
{icon ?? <Filter size={10} />}
|
|
31
|
+
{children}
|
|
32
|
+
<ChevronDown size={10} />
|
|
33
|
+
</button>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { FilterChip, type FilterChipProps }
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Info, AlertTriangle, ShieldAlert, X } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
|
|
8
|
+
interface InlineBannerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
9
|
+
variant?: "info" | "warning" | "destructive"
|
|
10
|
+
icon?: React.ReactNode
|
|
11
|
+
onDismiss?: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const variantConfig = {
|
|
15
|
+
info: {
|
|
16
|
+
icon: Info,
|
|
17
|
+
classes: "bg-status-info-bg border-status-info-border text-status-info-fg",
|
|
18
|
+
},
|
|
19
|
+
warning: {
|
|
20
|
+
icon: AlertTriangle,
|
|
21
|
+
classes: "bg-status-warning-bg border-status-warning-border text-status-warning-fg",
|
|
22
|
+
},
|
|
23
|
+
destructive: {
|
|
24
|
+
icon: ShieldAlert,
|
|
25
|
+
classes: "bg-status-destructive-bg border-status-destructive-border text-status-destructive-fg",
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function InlineBanner({
|
|
30
|
+
variant = "warning",
|
|
31
|
+
icon,
|
|
32
|
+
children,
|
|
33
|
+
onDismiss,
|
|
34
|
+
className,
|
|
35
|
+
...rest
|
|
36
|
+
}: InlineBannerProps) {
|
|
37
|
+
const config = variantConfig[variant]
|
|
38
|
+
const DefaultIcon = config.icon
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div
|
|
42
|
+
data-slot="inline-banner"
|
|
43
|
+
data-variant={variant}
|
|
44
|
+
role={variant === "destructive" || variant === "warning" ? "alert" : "status"}
|
|
45
|
+
className={cn("rounded-lg border px-3 py-2 flex items-center gap-2", config.classes, className)}
|
|
46
|
+
{...rest}
|
|
47
|
+
>
|
|
48
|
+
<span data-slot="inline-banner-icon" className="shrink-0">
|
|
49
|
+
{icon ?? <DefaultIcon size={14} />}
|
|
50
|
+
</span>
|
|
51
|
+
<div data-slot="inline-banner-content" className="flex-1 min-w-0">
|
|
52
|
+
{children}
|
|
53
|
+
</div>
|
|
54
|
+
{onDismiss && (
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
aria-label="Dismiss"
|
|
58
|
+
data-slot="inline-banner-dismiss"
|
|
59
|
+
onClick={onDismiss}
|
|
60
|
+
className="shrink-0 p-0.5 rounded hover:bg-black/5 transition-colors"
|
|
61
|
+
>
|
|
62
|
+
<X size={14} className="opacity-60" />
|
|
63
|
+
</button>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { InlineBanner, type InlineBannerProps }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/utils"
|
|
4
|
+
|
|
5
|
+
function KbdHint({
|
|
6
|
+
className,
|
|
7
|
+
...props
|
|
8
|
+
}: React.ComponentProps<"kbd">) {
|
|
9
|
+
return (
|
|
10
|
+
<kbd
|
|
11
|
+
data-slot="kbd-hint"
|
|
12
|
+
className={cn(
|
|
13
|
+
"inline-block text-[10px] leading-none text-muted-foreground/40 font-sans",
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { KbdHint }
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import type { LucideIcon } from "lucide-react"
|
|
5
|
+
import { Undo2, Redo2, Bold, Italic, Underline, AlignLeft, List, Trash2, ChevronDown } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "../lib/utils"
|
|
8
|
+
|
|
9
|
+
type RichTextAction =
|
|
10
|
+
| "undo" | "redo"
|
|
11
|
+
| "font"
|
|
12
|
+
| "bold" | "italic" | "underline"
|
|
13
|
+
| "align" | "list"
|
|
14
|
+
| "delete"
|
|
15
|
+
|
|
16
|
+
interface RichTextToolbarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
17
|
+
onAction?: (action: RichTextAction) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function ToolbarButton({
|
|
21
|
+
action,
|
|
22
|
+
icon: Icon,
|
|
23
|
+
label,
|
|
24
|
+
extraClassName,
|
|
25
|
+
onAction,
|
|
26
|
+
}: {
|
|
27
|
+
action: RichTextAction
|
|
28
|
+
icon: LucideIcon
|
|
29
|
+
label: string
|
|
30
|
+
extraClassName?: string
|
|
31
|
+
onAction?: (action: RichTextAction) => void
|
|
32
|
+
}) {
|
|
33
|
+
return (
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
data-slot="rich-text-toolbar-button"
|
|
37
|
+
onClick={() => onAction?.(action)}
|
|
38
|
+
aria-label={label}
|
|
39
|
+
className={cn("p-1.5 rounded hover:bg-muted/50 cursor-pointer text-muted-foreground", extraClassName)}
|
|
40
|
+
>
|
|
41
|
+
<Icon size={14} />
|
|
42
|
+
</button>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function RichTextToolbar({ onAction, className, ...rest }: RichTextToolbarProps) {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
data-slot="rich-text-toolbar"
|
|
50
|
+
role="toolbar"
|
|
51
|
+
aria-label="Rich text formatting"
|
|
52
|
+
className={cn("px-3 py-1.5 flex items-center justify-between", className)}
|
|
53
|
+
{...rest}
|
|
54
|
+
>
|
|
55
|
+
<div className="flex items-center gap-0.5">
|
|
56
|
+
<ToolbarButton action="undo" icon={Undo2} label="Undo" onAction={onAction} />
|
|
57
|
+
<ToolbarButton action="redo" icon={Redo2} label="Redo" onAction={onAction} />
|
|
58
|
+
|
|
59
|
+
<div className="w-px h-4 bg-border mx-1" aria-hidden="true" />
|
|
60
|
+
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
data-slot="rich-text-toolbar-button"
|
|
64
|
+
onClick={() => onAction?.("font")}
|
|
65
|
+
aria-label="Font family"
|
|
66
|
+
aria-haspopup="true"
|
|
67
|
+
className="text-[11px] text-muted-foreground px-1.5 py-0.5 rounded hover:bg-muted/50 cursor-pointer flex items-center gap-1"
|
|
68
|
+
>
|
|
69
|
+
Sans Serif
|
|
70
|
+
<ChevronDown size={10} />
|
|
71
|
+
</button>
|
|
72
|
+
|
|
73
|
+
<div className="w-px h-4 bg-border mx-1" aria-hidden="true" />
|
|
74
|
+
|
|
75
|
+
<ToolbarButton action="bold" icon={Bold} label="Bold" onAction={onAction} />
|
|
76
|
+
<ToolbarButton action="italic" icon={Italic} label="Italic" onAction={onAction} />
|
|
77
|
+
<ToolbarButton action="underline" icon={Underline} label="Underline" onAction={onAction} />
|
|
78
|
+
|
|
79
|
+
<div className="w-px h-4 bg-border mx-1" aria-hidden="true" />
|
|
80
|
+
|
|
81
|
+
<ToolbarButton action="align" icon={AlignLeft} label="Align left" onAction={onAction} />
|
|
82
|
+
<ToolbarButton action="list" icon={List} label="List" onAction={onAction} />
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<ToolbarButton action="delete" icon={Trash2} label="Delete" extraClassName="hover:text-destructive" onAction={onAction} />
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { RichTextToolbar, type RichTextToolbarProps, type RichTextAction }
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import type { LucideIcon } from "lucide-react"
|
|
5
|
+
import { Mail, Phone, CheckSquare, Clock, Circle, ChevronUp, ChevronDown } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "../lib/utils"
|
|
8
|
+
|
|
9
|
+
type StepType = "email" | "call" | "task" | "wait" | (string & {})
|
|
10
|
+
|
|
11
|
+
interface TimelineStep {
|
|
12
|
+
id: string
|
|
13
|
+
type: StepType
|
|
14
|
+
label: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface StepTimelineProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
18
|
+
steps: TimelineStep[]
|
|
19
|
+
expandedStepId?: string
|
|
20
|
+
onStepClick?: (stepId: string) => void
|
|
21
|
+
onInsert?: (index: number) => void
|
|
22
|
+
renderStepBody?: (step: TimelineStep) => React.ReactNode
|
|
23
|
+
renderStepAccessory?: (step: TimelineStep) => React.ReactNode
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const stepTypeConfig: Record<string, { icon: LucideIcon; classes: string }> = {
|
|
27
|
+
email: { icon: Mail, classes: "bg-blue-500/10 text-blue-600" },
|
|
28
|
+
call: { icon: Phone, classes: "bg-emerald-500/10 text-emerald-600" },
|
|
29
|
+
task: { icon: CheckSquare, classes: "bg-purple-500/10 text-purple-600" },
|
|
30
|
+
wait: { icon: Clock, classes: "bg-muted text-muted-foreground" },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const defaultStepConfig: { icon: LucideIcon; classes: string } = { icon: Circle, classes: "bg-muted text-muted-foreground" }
|
|
34
|
+
|
|
35
|
+
function StepTimeline({
|
|
36
|
+
steps,
|
|
37
|
+
expandedStepId,
|
|
38
|
+
onStepClick,
|
|
39
|
+
onInsert,
|
|
40
|
+
renderStepBody,
|
|
41
|
+
renderStepAccessory,
|
|
42
|
+
className,
|
|
43
|
+
...rest
|
|
44
|
+
}: StepTimelineProps) {
|
|
45
|
+
return (
|
|
46
|
+
<div data-slot="step-timeline" className={cn("relative ml-5", className)} {...rest}>
|
|
47
|
+
<div className="absolute left-0 top-0 bottom-0 border-l-2 border-border" />
|
|
48
|
+
|
|
49
|
+
{steps.map((step, index) => {
|
|
50
|
+
const expanded = step.id === expandedStepId
|
|
51
|
+
const config = stepTypeConfig[step.type] ?? defaultStepConfig
|
|
52
|
+
const TypeIcon = config.icon
|
|
53
|
+
const isInteractive = !!(onStepClick || renderStepBody)
|
|
54
|
+
const bodyId = `step-timeline-body-${step.id}`
|
|
55
|
+
|
|
56
|
+
const headerContent = (
|
|
57
|
+
<>
|
|
58
|
+
<div className={cn("w-8 h-8 rounded-full flex items-center justify-center shrink-0", config.classes)}>
|
|
59
|
+
<TypeIcon size={16} />
|
|
60
|
+
</div>
|
|
61
|
+
<div className="flex flex-col min-w-0">
|
|
62
|
+
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider">
|
|
63
|
+
{step.type}
|
|
64
|
+
</span>
|
|
65
|
+
<span className="text-sm font-medium">{step.label}</span>
|
|
66
|
+
</div>
|
|
67
|
+
</>
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<React.Fragment key={step.id}>
|
|
72
|
+
{index > 0 && onInsert && (
|
|
73
|
+
<div className="relative pl-8 py-1">
|
|
74
|
+
<button
|
|
75
|
+
type="button"
|
|
76
|
+
data-slot="step-timeline-insert"
|
|
77
|
+
onClick={() => onInsert(index)}
|
|
78
|
+
className="border border-dashed border-border/50 rounded-lg py-2 text-center text-xs text-muted-foreground/50 hover:text-muted-foreground hover:border-border cursor-pointer transition-colors w-full"
|
|
79
|
+
>
|
|
80
|
+
+ Add Step
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
<div data-slot="step-timeline-step" className="relative pl-8 py-2">
|
|
86
|
+
<div className="absolute left-0 top-1/2 -translate-x-1/2 -translate-y-1/2 w-3 h-3 rounded-full border-2 border-border bg-background z-10" />
|
|
87
|
+
|
|
88
|
+
<div data-slot="step-timeline-card" className="border border-border rounded-lg overflow-hidden">
|
|
89
|
+
{isInteractive ? (
|
|
90
|
+
<div className="flex items-center w-full px-4 py-3 gap-3">
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
aria-expanded={expanded}
|
|
94
|
+
aria-controls={bodyId}
|
|
95
|
+
onClick={() => onStepClick?.(step.id)}
|
|
96
|
+
className="flex items-center gap-3 flex-1 min-w-0 cursor-pointer hover:opacity-80 transition-opacity text-left"
|
|
97
|
+
>
|
|
98
|
+
{headerContent}
|
|
99
|
+
</button>
|
|
100
|
+
<div className="ml-auto flex items-center gap-2 shrink-0">
|
|
101
|
+
{renderStepAccessory && (
|
|
102
|
+
<div className="shrink-0">
|
|
103
|
+
{renderStepAccessory(step)}
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
<div className="shrink-0 text-muted-foreground">
|
|
107
|
+
{expanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
) : (
|
|
112
|
+
<div className="w-full px-4 py-3 flex items-center gap-3">
|
|
113
|
+
{headerContent}
|
|
114
|
+
{renderStepAccessory && (
|
|
115
|
+
<div className="ml-auto shrink-0">
|
|
116
|
+
{renderStepAccessory(step)}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{expanded && renderStepBody && (
|
|
123
|
+
<div id={bodyId} data-slot="step-timeline-body">
|
|
124
|
+
{renderStepBody(step)}
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</React.Fragment>
|
|
130
|
+
)
|
|
131
|
+
})}
|
|
132
|
+
|
|
133
|
+
{onInsert && (
|
|
134
|
+
<div className="relative pl-8 py-1">
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
data-slot="step-timeline-insert"
|
|
138
|
+
onClick={() => onInsert(steps.length)}
|
|
139
|
+
className="border border-dashed border-border/50 rounded-lg py-2 text-center text-xs text-muted-foreground/50 hover:text-muted-foreground hover:border-border cursor-pointer transition-colors w-full"
|
|
140
|
+
>
|
|
141
|
+
+ Add Step
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { StepTimeline, type StepTimelineProps, type TimelineStep, type StepType }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/utils"
|
|
4
|
+
|
|
5
|
+
interface StickyActionBarProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
left?: React.ReactNode
|
|
7
|
+
right?: React.ReactNode
|
|
8
|
+
bordered?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function StickyActionBar({ left, right, bordered = true, className, ...rest }: StickyActionBarProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
data-slot="sticky-action-bar"
|
|
15
|
+
className={cn(
|
|
16
|
+
"flex items-center justify-between sticky bottom-0 bg-background",
|
|
17
|
+
bordered && "pt-3 border-t border-border/50",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...rest}
|
|
21
|
+
>
|
|
22
|
+
{left && (
|
|
23
|
+
<div data-slot="sticky-action-bar-left" className="flex items-center gap-1">
|
|
24
|
+
{left}
|
|
25
|
+
</div>
|
|
26
|
+
)}
|
|
27
|
+
{right && (
|
|
28
|
+
<div data-slot="sticky-action-bar-right" className="ml-auto flex items-center gap-2">
|
|
29
|
+
{right}
|
|
30
|
+
</div>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { StickyActionBar, type StickyActionBarProps }
|