@handled-ai/design-system 0.18.35 → 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.
- package/dist/charts/chart.d.ts +1 -1
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/draft-feedback-inline.d.ts +1 -1
- package/dist/components/draft-feedback-inline.js +10 -10
- package/dist/components/draft-feedback-inline.js.map +1 -1
- package/dist/components/email-composer-row.d.ts +11 -0
- package/dist/components/email-composer-row.js +82 -0
- package/dist/components/email-composer-row.js.map +1 -0
- package/dist/components/email-preview-card.d.ts +17 -0
- package/dist/components/email-preview-card.js +71 -0
- package/dist/components/email-preview-card.js.map +1 -0
- package/dist/components/email-recipient-field.d.ts +26 -0
- package/dist/components/email-recipient-field.js +400 -0
- package/dist/components/email-recipient-field.js.map +1 -0
- package/dist/components/email-send-bar.d.ts +22 -0
- package/dist/components/email-send-bar.js +66 -0
- package/dist/components/email-send-bar.js.map +1 -0
- package/dist/components/pill.d.ts +1 -1
- package/dist/components/score-feedback.js +6 -6
- package/dist/components/score-feedback.js.map +1 -1
- package/dist/components/suggested-actions.js +5 -17
- package/dist/components/suggested-actions.js.map +1 -1
- package/dist/components/tabs.d.ts +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/src/components/__tests__/email-composer-row.test.tsx +51 -0
- package/src/components/__tests__/email-preview-card.test.tsx +62 -0
- package/src/components/__tests__/email-recipient-field.test.tsx +256 -0
- package/src/components/__tests__/email-send-bar.test.tsx +80 -0
- package/src/components/draft-feedback-inline.tsx +13 -13
- package/src/components/email-composer-row.tsx +47 -0
- package/src/components/email-preview-card.tsx +94 -0
- package/src/components/email-recipient-field.tsx +456 -0
- package/src/components/email-send-bar.tsx +95 -0
- package/src/components/score-feedback.tsx +7 -7
- package/src/components/suggested-actions.tsx +5 -19
- package/src/index.ts +4 -0
- package/src/components/__tests__/draft-feedback-inline.test.tsx +0 -72
- package/src/components/__tests__/suggested-actions-feedback-header.test.tsx +0 -86
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
"use client";
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __defProps = Object.defineProperties;
|
|
6
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
7
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
10
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
11
|
+
var __spreadValues = (a, b) => {
|
|
12
|
+
for (var prop in b || (b = {}))
|
|
13
|
+
if (__hasOwnProp.call(b, prop))
|
|
14
|
+
__defNormalProp(a, prop, b[prop]);
|
|
15
|
+
if (__getOwnPropSymbols)
|
|
16
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
17
|
+
if (__propIsEnum.call(b, prop))
|
|
18
|
+
__defNormalProp(a, prop, b[prop]);
|
|
19
|
+
}
|
|
20
|
+
return a;
|
|
21
|
+
};
|
|
22
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
23
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
24
|
+
import * as React from "react";
|
|
25
|
+
import { createPortal } from "react-dom";
|
|
26
|
+
import {
|
|
27
|
+
Check,
|
|
28
|
+
ChevronDown,
|
|
29
|
+
CornerDownLeft,
|
|
30
|
+
Plus,
|
|
31
|
+
Search,
|
|
32
|
+
Users,
|
|
33
|
+
X
|
|
34
|
+
} from "lucide-react";
|
|
35
|
+
import { cn } from "../lib/utils.js";
|
|
36
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
37
|
+
function isValidEmail(value) {
|
|
38
|
+
return EMAIL_REGEX.test(value.trim());
|
|
39
|
+
}
|
|
40
|
+
function contactEmail(contact) {
|
|
41
|
+
var _a, _b;
|
|
42
|
+
return (_b = contact.email) != null ? _b : (_a = contact.emails) == null ? void 0 : _a[0];
|
|
43
|
+
}
|
|
44
|
+
function getInitials(name, fallback) {
|
|
45
|
+
const source = (name == null ? void 0 : name.trim()) || fallback;
|
|
46
|
+
return source.split(/[\s@.]+/).map((part) => part[0]).filter(Boolean).slice(0, 2).join("").toUpperCase();
|
|
47
|
+
}
|
|
48
|
+
function RecipientChipPill({
|
|
49
|
+
recipient,
|
|
50
|
+
onConfirm,
|
|
51
|
+
onRemove
|
|
52
|
+
}) {
|
|
53
|
+
const display = recipient.name || recipient.email;
|
|
54
|
+
if (!recipient.confirmed) {
|
|
55
|
+
return /* @__PURE__ */ jsxs("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", children: [
|
|
56
|
+
/* @__PURE__ */ jsx("span", { className: "truncate max-w-[180px]", children: display }),
|
|
57
|
+
/* @__PURE__ */ jsx(
|
|
58
|
+
"button",
|
|
59
|
+
{
|
|
60
|
+
type: "button",
|
|
61
|
+
onClick: onConfirm,
|
|
62
|
+
className: "text-[10.5px] font-semibold px-[7px] py-0.5 rounded bg-amber-300/50 hover:bg-amber-300/85",
|
|
63
|
+
children: "Confirm"
|
|
64
|
+
}
|
|
65
|
+
),
|
|
66
|
+
/* @__PURE__ */ jsx(
|
|
67
|
+
"button",
|
|
68
|
+
{
|
|
69
|
+
type: "button",
|
|
70
|
+
"aria-label": `Remove ${display}`,
|
|
71
|
+
onClick: onRemove,
|
|
72
|
+
className: "inline-flex items-center justify-center size-[17px] rounded text-amber-700/80 hover:bg-amber-300/40",
|
|
73
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3" })
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
] });
|
|
77
|
+
}
|
|
78
|
+
return /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 h-6 px-2 rounded-md text-xs border border-border bg-muted/50 text-foreground", children: [
|
|
79
|
+
/* @__PURE__ */ jsx("span", { className: "inline-flex items-center justify-center size-[17px] rounded bg-emerald-50 text-emerald-700", children: /* @__PURE__ */ jsx(Check, { className: "size-3" }) }),
|
|
80
|
+
/* @__PURE__ */ jsx("span", { className: "truncate max-w-[180px]", children: display }),
|
|
81
|
+
/* @__PURE__ */ jsx(
|
|
82
|
+
"button",
|
|
83
|
+
{
|
|
84
|
+
type: "button",
|
|
85
|
+
"aria-label": `Remove ${display}`,
|
|
86
|
+
onClick: onRemove,
|
|
87
|
+
className: "inline-flex items-center justify-center size-[17px] rounded text-muted-foreground hover:bg-muted",
|
|
88
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-3" })
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
] });
|
|
92
|
+
}
|
|
93
|
+
function ContactPickerPopover({
|
|
94
|
+
triggerRef,
|
|
95
|
+
contacts,
|
|
96
|
+
addedEmails,
|
|
97
|
+
onSelect,
|
|
98
|
+
onAddEmail,
|
|
99
|
+
onClose
|
|
100
|
+
}) {
|
|
101
|
+
const containerRef = React.useRef(null);
|
|
102
|
+
const searchRef = React.useRef(null);
|
|
103
|
+
const [query, setQuery] = React.useState("");
|
|
104
|
+
const [style, setStyle] = React.useState({
|
|
105
|
+
position: "fixed",
|
|
106
|
+
top: -9999,
|
|
107
|
+
left: -9999
|
|
108
|
+
});
|
|
109
|
+
React.useEffect(() => {
|
|
110
|
+
const trigger = triggerRef.current;
|
|
111
|
+
if (!trigger) return;
|
|
112
|
+
const rect = trigger.getBoundingClientRect();
|
|
113
|
+
const width = Math.min(448, window.innerWidth - 32);
|
|
114
|
+
let left = rect.left;
|
|
115
|
+
if (left + width > window.innerWidth - 16) {
|
|
116
|
+
left = window.innerWidth - 16 - width;
|
|
117
|
+
}
|
|
118
|
+
if (left < 16) left = 16;
|
|
119
|
+
const popoverHeight = 280;
|
|
120
|
+
const spaceBelow = window.innerHeight - rect.bottom - 4;
|
|
121
|
+
const spaceAbove = rect.top - 4;
|
|
122
|
+
const placeAbove = spaceBelow < popoverHeight && spaceAbove > spaceBelow;
|
|
123
|
+
let top = placeAbove ? rect.top - popoverHeight - 4 : rect.bottom + 4;
|
|
124
|
+
if (top < 16) top = 16;
|
|
125
|
+
setStyle({ position: "fixed", top, left, width });
|
|
126
|
+
}, [triggerRef]);
|
|
127
|
+
React.useEffect(() => {
|
|
128
|
+
var _a;
|
|
129
|
+
(_a = searchRef.current) == null ? void 0 : _a.focus();
|
|
130
|
+
const trigger = triggerRef.current;
|
|
131
|
+
return () => {
|
|
132
|
+
if (trigger && typeof trigger.focus === "function") {
|
|
133
|
+
trigger.focus();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}, [triggerRef]);
|
|
137
|
+
React.useEffect(() => {
|
|
138
|
+
function handleMouseDown(event) {
|
|
139
|
+
var _a;
|
|
140
|
+
if (containerRef.current && !containerRef.current.contains(event.target) && !((_a = triggerRef.current) == null ? void 0 : _a.contains(event.target))) {
|
|
141
|
+
onClose();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function handleKeyDown2(event) {
|
|
145
|
+
if (event.key === "Escape") {
|
|
146
|
+
event.stopPropagation();
|
|
147
|
+
onClose();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
document.addEventListener("mousedown", handleMouseDown);
|
|
151
|
+
document.addEventListener("keydown", handleKeyDown2);
|
|
152
|
+
return () => {
|
|
153
|
+
document.removeEventListener("mousedown", handleMouseDown);
|
|
154
|
+
document.removeEventListener("keydown", handleKeyDown2);
|
|
155
|
+
};
|
|
156
|
+
}, [onClose, triggerRef]);
|
|
157
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
158
|
+
const filtered = normalizedQuery ? contacts.filter((contact) => {
|
|
159
|
+
var _a;
|
|
160
|
+
const email = (_a = contactEmail(contact)) != null ? _a : "";
|
|
161
|
+
return contact.name.toLowerCase().includes(normalizedQuery) || contact.role.toLowerCase().includes(normalizedQuery) || email.toLowerCase().includes(normalizedQuery);
|
|
162
|
+
}) : contacts;
|
|
163
|
+
const queryIsEmail = isValidEmail(query);
|
|
164
|
+
function handleKeyDown(event) {
|
|
165
|
+
if (event.key === "Enter" && queryIsEmail) {
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
onAddEmail(query.trim());
|
|
168
|
+
setQuery("");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return createPortal(
|
|
172
|
+
/* @__PURE__ */ jsxs(
|
|
173
|
+
"div",
|
|
174
|
+
{
|
|
175
|
+
ref: containerRef,
|
|
176
|
+
style,
|
|
177
|
+
className: "bg-background border rounded-lg shadow-xl z-50 pointer-events-auto",
|
|
178
|
+
children: [
|
|
179
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2.5 border-b border-border/50", children: [
|
|
180
|
+
/* @__PURE__ */ jsx(Search, { className: "size-4 text-muted-foreground shrink-0" }),
|
|
181
|
+
/* @__PURE__ */ jsx(
|
|
182
|
+
"input",
|
|
183
|
+
{
|
|
184
|
+
ref: searchRef,
|
|
185
|
+
autoFocus: true,
|
|
186
|
+
value: query,
|
|
187
|
+
onChange: (event) => setQuery(event.target.value),
|
|
188
|
+
onKeyDown: handleKeyDown,
|
|
189
|
+
className: "flex-1 text-[13px] bg-transparent outline-none",
|
|
190
|
+
placeholder: "Search contacts..."
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
] }),
|
|
194
|
+
/* @__PURE__ */ jsx("div", { role: "listbox", className: "max-h-[208px] overflow-y-auto p-1", children: filtered.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "px-3 py-4 text-center text-[13px] text-muted-foreground", children: [
|
|
195
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
196
|
+
"No contact matches \u2018",
|
|
197
|
+
query,
|
|
198
|
+
"\u2019."
|
|
199
|
+
] }),
|
|
200
|
+
queryIsEmail ? /* @__PURE__ */ jsxs("div", { className: "mt-1", children: [
|
|
201
|
+
"Press Enter to add ",
|
|
202
|
+
query,
|
|
203
|
+
"."
|
|
204
|
+
] }) : null
|
|
205
|
+
] }) : filtered.map((contact, index) => {
|
|
206
|
+
const email = contactEmail(contact);
|
|
207
|
+
const noEmail = !email || !isValidEmail(email);
|
|
208
|
+
const alreadyAdded = email ? addedEmails.has(email.toLowerCase()) : false;
|
|
209
|
+
const disabled = noEmail || alreadyAdded;
|
|
210
|
+
return /* @__PURE__ */ jsxs(
|
|
211
|
+
"div",
|
|
212
|
+
{
|
|
213
|
+
role: "option",
|
|
214
|
+
"aria-selected": false,
|
|
215
|
+
"aria-disabled": disabled,
|
|
216
|
+
onClick: () => {
|
|
217
|
+
if (!disabled) onSelect(contact);
|
|
218
|
+
},
|
|
219
|
+
className: cn(
|
|
220
|
+
"flex items-center gap-2.5 px-2 py-1.5 rounded-md cursor-pointer hover:bg-muted/60",
|
|
221
|
+
disabled && "opacity-45 pointer-events-none"
|
|
222
|
+
),
|
|
223
|
+
children: [
|
|
224
|
+
/* @__PURE__ */ jsx("div", { className: "flex size-7 shrink-0 items-center justify-center rounded-[7px] bg-muted text-[11px] font-medium text-muted-foreground", children: getInitials(contact.name, email != null ? email : "?") }),
|
|
225
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
226
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
227
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-[13px] font-medium text-foreground", children: contact.name }),
|
|
228
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-[11px] text-muted-foreground", children: contact.role })
|
|
229
|
+
] }),
|
|
230
|
+
email ? /* @__PURE__ */ jsx("div", { className: "truncate text-[11px] text-muted-foreground", children: email }) : null
|
|
231
|
+
] }),
|
|
232
|
+
alreadyAdded ? /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[10.5px] font-medium text-muted-foreground", children: "Added" }) : noEmail ? /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[10.5px] font-medium text-muted-foreground", children: "No email" }) : null
|
|
233
|
+
]
|
|
234
|
+
},
|
|
235
|
+
`${contact.name}-${email != null ? email : index}`
|
|
236
|
+
);
|
|
237
|
+
}) }),
|
|
238
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-3 py-2 border-t border-border/50 text-[11px] text-muted-foreground", children: [
|
|
239
|
+
/* @__PURE__ */ jsx(CornerDownLeft, { className: "size-3 shrink-0" }),
|
|
240
|
+
/* @__PURE__ */ jsx("span", { children: "Type an address and press Enter to add someone not listed." })
|
|
241
|
+
] })
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
),
|
|
245
|
+
document.body
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
function EmailRecipientField({
|
|
249
|
+
label,
|
|
250
|
+
recipients,
|
|
251
|
+
onRecipientsChange,
|
|
252
|
+
amber = false,
|
|
253
|
+
contacts = [],
|
|
254
|
+
showPicker = false,
|
|
255
|
+
showCcBcc = false,
|
|
256
|
+
ccBccOpen = false,
|
|
257
|
+
onCcBccToggle,
|
|
258
|
+
addedEmails,
|
|
259
|
+
placeholder,
|
|
260
|
+
contactToRecipient
|
|
261
|
+
}) {
|
|
262
|
+
const [value, setValue] = React.useState("");
|
|
263
|
+
const [pickerOpen, setPickerOpen] = React.useState(false);
|
|
264
|
+
const contactsTriggerRef = React.useRef(null);
|
|
265
|
+
const hasUnconfirmed = recipients.some((r) => !r.confirmed);
|
|
266
|
+
const state = amber && hasUnconfirmed ? "amber" : "default";
|
|
267
|
+
const amberRow = state === "amber";
|
|
268
|
+
const added = addedEmails != null ? addedEmails : /* @__PURE__ */ new Set();
|
|
269
|
+
const resolvedPlaceholder = placeholder != null ? placeholder : recipients.length > 0 ? "Add another..." : "Add email...";
|
|
270
|
+
function addEmail(email) {
|
|
271
|
+
const trimmed = email.trim();
|
|
272
|
+
if (!isValidEmail(trimmed)) return;
|
|
273
|
+
if (added.has(trimmed.toLowerCase())) return;
|
|
274
|
+
onRecipientsChange([
|
|
275
|
+
...recipients,
|
|
276
|
+
{ id: trimmed, email: trimmed, name: "", confirmed: false }
|
|
277
|
+
]);
|
|
278
|
+
setValue("");
|
|
279
|
+
}
|
|
280
|
+
function handleKeyDown(event) {
|
|
281
|
+
if ((event.key === "Enter" || event.key === ",") && isValidEmail(value)) {
|
|
282
|
+
event.preventDefault();
|
|
283
|
+
addEmail(value);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (event.key === "Backspace" && value === "" && recipients.length > 0) {
|
|
287
|
+
event.preventDefault();
|
|
288
|
+
onRecipientsChange(recipients.slice(0, -1));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function confirmRecipient(id) {
|
|
292
|
+
onRecipientsChange(
|
|
293
|
+
recipients.map((r) => r.id === id ? __spreadProps(__spreadValues({}, r), { confirmed: true }) : r)
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
function removeRecipient(id) {
|
|
297
|
+
onRecipientsChange(recipients.filter((r) => r.id !== id));
|
|
298
|
+
}
|
|
299
|
+
function selectContact(contact) {
|
|
300
|
+
var _a, _b, _c;
|
|
301
|
+
const recipient = (_c = contactToRecipient == null ? void 0 : contactToRecipient(contact)) != null ? _c : {
|
|
302
|
+
id: (_a = contactEmail(contact)) != null ? _a : contact.name,
|
|
303
|
+
email: (_b = contactEmail(contact)) != null ? _b : "",
|
|
304
|
+
name: contact.name,
|
|
305
|
+
confirmed: true
|
|
306
|
+
};
|
|
307
|
+
onRecipientsChange([...recipients, recipient]);
|
|
308
|
+
setPickerOpen(false);
|
|
309
|
+
}
|
|
310
|
+
return /* @__PURE__ */ jsxs(
|
|
311
|
+
"div",
|
|
312
|
+
{
|
|
313
|
+
className: cn(
|
|
314
|
+
"grid grid-cols-[60px_1fr] gap-2 px-[18px] py-[9px] border-b border-border/70 items-start text-sm",
|
|
315
|
+
amberRow && "bg-amber-50/35 border-amber-200/80"
|
|
316
|
+
),
|
|
317
|
+
children: [
|
|
318
|
+
/* @__PURE__ */ jsx(
|
|
319
|
+
"div",
|
|
320
|
+
{
|
|
321
|
+
className: cn(
|
|
322
|
+
"text-[11px] font-semibold uppercase tracking-wide text-muted-foreground pt-[7px]",
|
|
323
|
+
amberRow && "text-amber-700"
|
|
324
|
+
),
|
|
325
|
+
children: label
|
|
326
|
+
}
|
|
327
|
+
),
|
|
328
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
|
|
329
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-1.5 items-center", children: [
|
|
330
|
+
recipients.map((recipient) => /* @__PURE__ */ jsx(
|
|
331
|
+
RecipientChipPill,
|
|
332
|
+
{
|
|
333
|
+
recipient,
|
|
334
|
+
onConfirm: () => confirmRecipient(recipient.id),
|
|
335
|
+
onRemove: () => removeRecipient(recipient.id)
|
|
336
|
+
},
|
|
337
|
+
recipient.id
|
|
338
|
+
)),
|
|
339
|
+
/* @__PURE__ */ jsx(
|
|
340
|
+
"input",
|
|
341
|
+
{
|
|
342
|
+
value,
|
|
343
|
+
onChange: (event) => setValue(event.target.value),
|
|
344
|
+
onKeyDown: handleKeyDown,
|
|
345
|
+
placeholder: resolvedPlaceholder,
|
|
346
|
+
className: "min-w-[130px] flex-1 h-6 text-[13px] bg-transparent border-0 outline-none placeholder:text-muted-foreground"
|
|
347
|
+
}
|
|
348
|
+
)
|
|
349
|
+
] }),
|
|
350
|
+
showPicker || showCcBcc ? /* @__PURE__ */ jsxs("div", { className: "flex gap-1.5 mt-2", children: [
|
|
351
|
+
showPicker ? /* @__PURE__ */ jsxs(
|
|
352
|
+
"button",
|
|
353
|
+
{
|
|
354
|
+
ref: contactsTriggerRef,
|
|
355
|
+
type: "button",
|
|
356
|
+
onClick: () => setPickerOpen((open) => !open),
|
|
357
|
+
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]",
|
|
358
|
+
children: [
|
|
359
|
+
/* @__PURE__ */ jsx(Users, { className: "size-3" }),
|
|
360
|
+
"Contacts",
|
|
361
|
+
/* @__PURE__ */ jsx(ChevronDown, { className: "size-3" })
|
|
362
|
+
]
|
|
363
|
+
}
|
|
364
|
+
) : null,
|
|
365
|
+
showCcBcc ? /* @__PURE__ */ jsxs(
|
|
366
|
+
"button",
|
|
367
|
+
{
|
|
368
|
+
type: "button",
|
|
369
|
+
onClick: onCcBccToggle,
|
|
370
|
+
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]",
|
|
371
|
+
children: [
|
|
372
|
+
/* @__PURE__ */ jsx(Plus, { className: "size-3" }),
|
|
373
|
+
ccBccOpen ? "Hide Cc/Bcc" : "Add Cc/Bcc"
|
|
374
|
+
]
|
|
375
|
+
}
|
|
376
|
+
) : null
|
|
377
|
+
] }) : null,
|
|
378
|
+
pickerOpen ? /* @__PURE__ */ jsx(
|
|
379
|
+
ContactPickerPopover,
|
|
380
|
+
{
|
|
381
|
+
triggerRef: contactsTriggerRef,
|
|
382
|
+
contacts,
|
|
383
|
+
addedEmails: added,
|
|
384
|
+
onSelect: selectContact,
|
|
385
|
+
onAddEmail: (email) => {
|
|
386
|
+
addEmail(email);
|
|
387
|
+
setPickerOpen(false);
|
|
388
|
+
},
|
|
389
|
+
onClose: () => setPickerOpen(false)
|
|
390
|
+
}
|
|
391
|
+
) : null
|
|
392
|
+
] })
|
|
393
|
+
]
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
export {
|
|
398
|
+
EmailRecipientField
|
|
399
|
+
};
|
|
400
|
+
//# sourceMappingURL=email-recipient-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/email-recipient-field.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\nimport { createPortal } from \"react-dom\"\nimport {\n Check,\n ChevronDown,\n CornerDownLeft,\n Plus,\n Search,\n Users,\n X,\n} from \"lucide-react\"\n\nimport { cn } from \"../lib/utils\"\nimport type { SuggestedContact } from \"./suggested-actions\"\n\nconst EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nfunction isValidEmail(value: string): boolean {\n return EMAIL_REGEX.test(value.trim())\n}\n\nfunction contactEmail(contact: SuggestedContact): string | undefined {\n return contact.email ?? contact.emails?.[0]\n}\n\nfunction getInitials(name: string, fallback: string): string {\n const source = name?.trim() || fallback\n return source\n .split(/[\\s@.]+/)\n .map((part) => part[0])\n .filter(Boolean)\n .slice(0, 2)\n .join(\"\")\n .toUpperCase()\n}\n\nexport interface RecipientChip {\n id: string\n email: string\n name: string\n confirmed: boolean\n}\n\nexport interface EmailRecipientFieldProps {\n label: string\n recipients: RecipientChip[]\n onRecipientsChange: (recipients: RecipientChip[]) => void\n amber?: boolean\n contacts?: SuggestedContact[]\n showPicker?: boolean\n showCcBcc?: boolean\n ccBccOpen?: boolean\n onCcBccToggle?: () => void\n addedEmails?: Set<string>\n placeholder?: string\n contactToRecipient?: (contact: SuggestedContact) => RecipientChip\n}\n\nfunction RecipientChipPill({\n recipient,\n onConfirm,\n onRemove,\n}: {\n recipient: RecipientChip\n onConfirm: () => void\n onRemove: () => void\n}) {\n const display = recipient.name || recipient.email\n\n if (!recipient.confirmed) {\n return (\n <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\">\n <span className=\"truncate max-w-[180px]\">{display}</span>\n <button\n type=\"button\"\n onClick={onConfirm}\n className=\"text-[10.5px] font-semibold px-[7px] py-0.5 rounded bg-amber-300/50 hover:bg-amber-300/85\"\n >\n Confirm\n </button>\n <button\n type=\"button\"\n aria-label={`Remove ${display}`}\n onClick={onRemove}\n className=\"inline-flex items-center justify-center size-[17px] rounded text-amber-700/80 hover:bg-amber-300/40\"\n >\n <X className=\"size-3\" />\n </button>\n </span>\n )\n }\n\n return (\n <span className=\"inline-flex items-center gap-1 h-6 px-2 rounded-md text-xs border border-border bg-muted/50 text-foreground\">\n <span className=\"inline-flex items-center justify-center size-[17px] rounded bg-emerald-50 text-emerald-700\">\n <Check className=\"size-3\" />\n </span>\n <span className=\"truncate max-w-[180px]\">{display}</span>\n <button\n type=\"button\"\n aria-label={`Remove ${display}`}\n onClick={onRemove}\n className=\"inline-flex items-center justify-center size-[17px] rounded text-muted-foreground hover:bg-muted\"\n >\n <X className=\"size-3\" />\n </button>\n </span>\n )\n}\n\nfunction ContactPickerPopover({\n triggerRef,\n contacts,\n addedEmails,\n onSelect,\n onAddEmail,\n onClose,\n}: {\n triggerRef: React.RefObject<HTMLElement | null>\n contacts: SuggestedContact[]\n addedEmails: Set<string>\n onSelect: (contact: SuggestedContact) => void\n onAddEmail: (email: string) => void\n onClose: () => void\n}) {\n const containerRef = React.useRef<HTMLDivElement>(null)\n const searchRef = React.useRef<HTMLInputElement>(null)\n const [query, setQuery] = React.useState(\"\")\n const [style, setStyle] = React.useState<React.CSSProperties>({\n position: \"fixed\",\n top: -9999,\n left: -9999,\n })\n\n React.useEffect(() => {\n const trigger = triggerRef.current\n if (!trigger) return\n const rect = trigger.getBoundingClientRect()\n const width = Math.min(448, window.innerWidth - 32)\n let left = rect.left\n if (left + width > window.innerWidth - 16) {\n left = window.innerWidth - 16 - width\n }\n if (left < 16) left = 16\n const popoverHeight = 280\n const spaceBelow = window.innerHeight - rect.bottom - 4\n const spaceAbove = rect.top - 4\n const placeAbove = spaceBelow < popoverHeight && spaceAbove > spaceBelow\n let top = placeAbove ? rect.top - popoverHeight - 4 : rect.bottom + 4\n if (top < 16) top = 16\n setStyle({ position: \"fixed\", top, left, width })\n }, [triggerRef])\n\n React.useEffect(() => {\n searchRef.current?.focus()\n const trigger = triggerRef.current\n return () => {\n if (trigger && typeof trigger.focus === \"function\") {\n trigger.focus()\n }\n }\n }, [triggerRef])\n\n React.useEffect(() => {\n function handleMouseDown(event: MouseEvent) {\n if (\n containerRef.current &&\n !containerRef.current.contains(event.target as Node) &&\n !triggerRef.current?.contains(event.target as Node)\n ) {\n onClose()\n }\n }\n function handleKeyDown(event: KeyboardEvent) {\n if (event.key === \"Escape\") {\n event.stopPropagation()\n onClose()\n }\n }\n document.addEventListener(\"mousedown\", handleMouseDown)\n document.addEventListener(\"keydown\", handleKeyDown)\n return () => {\n document.removeEventListener(\"mousedown\", handleMouseDown)\n document.removeEventListener(\"keydown\", handleKeyDown)\n }\n }, [onClose, triggerRef])\n\n const normalizedQuery = query.trim().toLowerCase()\n const filtered = normalizedQuery\n ? contacts.filter((contact) => {\n const email = contactEmail(contact) ?? \"\"\n return (\n contact.name.toLowerCase().includes(normalizedQuery) ||\n contact.role.toLowerCase().includes(normalizedQuery) ||\n email.toLowerCase().includes(normalizedQuery)\n )\n })\n : contacts\n\n const queryIsEmail = isValidEmail(query)\n\n function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {\n if (event.key === \"Enter\" && queryIsEmail) {\n event.preventDefault()\n onAddEmail(query.trim())\n setQuery(\"\")\n }\n }\n\n return createPortal(\n <div\n ref={containerRef}\n style={style}\n className=\"bg-background border rounded-lg shadow-xl z-50 pointer-events-auto\"\n >\n <div className=\"flex items-center gap-2 px-3 py-2.5 border-b border-border/50\">\n <Search className=\"size-4 text-muted-foreground shrink-0\" />\n <input\n ref={searchRef}\n autoFocus\n value={query}\n onChange={(event) => setQuery(event.target.value)}\n onKeyDown={handleKeyDown}\n className=\"flex-1 text-[13px] bg-transparent outline-none\"\n placeholder=\"Search contacts...\"\n />\n </div>\n\n <div role=\"listbox\" className=\"max-h-[208px] overflow-y-auto p-1\">\n {filtered.length === 0 ? (\n <div className=\"px-3 py-4 text-center text-[13px] text-muted-foreground\">\n <div>No contact matches ‘{query}’.</div>\n {queryIsEmail ? (\n <div className=\"mt-1\">Press Enter to add {query}.</div>\n ) : null}\n </div>\n ) : (\n filtered.map((contact, index) => {\n const email = contactEmail(contact)\n const noEmail = !email || !isValidEmail(email)\n const alreadyAdded = email\n ? addedEmails.has(email.toLowerCase())\n : false\n const disabled = noEmail || alreadyAdded\n\n return (\n <div\n key={`${contact.name}-${email ?? index}`}\n role=\"option\"\n aria-selected={false}\n aria-disabled={disabled}\n onClick={() => {\n if (!disabled) onSelect(contact)\n }}\n className={cn(\n \"flex items-center gap-2.5 px-2 py-1.5 rounded-md cursor-pointer hover:bg-muted/60\",\n disabled && \"opacity-45 pointer-events-none\",\n )}\n >\n <div className=\"flex size-7 shrink-0 items-center justify-center rounded-[7px] bg-muted text-[11px] font-medium text-muted-foreground\">\n {getInitials(contact.name, email ?? \"?\")}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1.5\">\n <span className=\"truncate text-[13px] font-medium text-foreground\">\n {contact.name}\n </span>\n <span className=\"truncate text-[11px] text-muted-foreground\">\n {contact.role}\n </span>\n </div>\n {email ? (\n <div className=\"truncate text-[11px] text-muted-foreground\">\n {email}\n </div>\n ) : null}\n </div>\n {alreadyAdded ? (\n <span className=\"shrink-0 text-[10.5px] font-medium text-muted-foreground\">\n Added\n </span>\n ) : noEmail ? (\n <span className=\"shrink-0 text-[10.5px] font-medium text-muted-foreground\">\n No email\n </span>\n ) : null}\n </div>\n )\n })\n )}\n </div>\n\n <div className=\"flex items-center gap-1.5 px-3 py-2 border-t border-border/50 text-[11px] text-muted-foreground\">\n <CornerDownLeft className=\"size-3 shrink-0\" />\n <span>Type an address and press Enter to add someone not listed.</span>\n </div>\n </div>,\n document.body,\n )\n}\n\nexport function EmailRecipientField({\n label,\n recipients,\n onRecipientsChange,\n amber = false,\n contacts = [],\n showPicker = false,\n showCcBcc = false,\n ccBccOpen = false,\n onCcBccToggle,\n addedEmails,\n placeholder,\n contactToRecipient,\n}: EmailRecipientFieldProps) {\n const [value, setValue] = React.useState(\"\")\n const [pickerOpen, setPickerOpen] = React.useState(false)\n const contactsTriggerRef = React.useRef<HTMLButtonElement>(null)\n\n const hasUnconfirmed = recipients.some((r) => !r.confirmed)\n const state: \"default\" | \"amber\" =\n amber && hasUnconfirmed ? \"amber\" : \"default\"\n const amberRow = state === \"amber\"\n\n const added = addedEmails ?? new Set<string>()\n\n const resolvedPlaceholder =\n placeholder ?? (recipients.length > 0 ? \"Add another...\" : \"Add email...\")\n\n function addEmail(email: string) {\n const trimmed = email.trim()\n if (!isValidEmail(trimmed)) return\n if (added.has(trimmed.toLowerCase())) return\n onRecipientsChange([\n ...recipients,\n { id: trimmed, email: trimmed, name: \"\", confirmed: false },\n ])\n setValue(\"\")\n }\n\n function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {\n if ((event.key === \"Enter\" || event.key === \",\") && isValidEmail(value)) {\n event.preventDefault()\n addEmail(value)\n return\n }\n if (event.key === \"Backspace\" && value === \"\" && recipients.length > 0) {\n event.preventDefault()\n onRecipientsChange(recipients.slice(0, -1))\n }\n }\n\n function confirmRecipient(id: string) {\n onRecipientsChange(\n recipients.map((r) => (r.id === id ? { ...r, confirmed: true } : r)),\n )\n }\n\n function removeRecipient(id: string) {\n onRecipientsChange(recipients.filter((r) => r.id !== id))\n }\n\n function selectContact(contact: SuggestedContact) {\n const recipient =\n contactToRecipient?.(contact) ??\n ({\n id: contactEmail(contact) ?? contact.name,\n email: contactEmail(contact) ?? \"\",\n name: contact.name,\n confirmed: true,\n } satisfies RecipientChip)\n onRecipientsChange([...recipients, recipient])\n setPickerOpen(false)\n }\n\n return (\n <div\n className={cn(\n \"grid grid-cols-[60px_1fr] gap-2 px-[18px] py-[9px] border-b border-border/70 items-start text-sm\",\n amberRow && \"bg-amber-50/35 border-amber-200/80\",\n )}\n >\n <div\n className={cn(\n \"text-[11px] font-semibold uppercase tracking-wide text-muted-foreground pt-[7px]\",\n amberRow && \"text-amber-700\",\n )}\n >\n {label}\n </div>\n\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap gap-1.5 items-center\">\n {recipients.map((recipient) => (\n <RecipientChipPill\n key={recipient.id}\n recipient={recipient}\n onConfirm={() => confirmRecipient(recipient.id)}\n onRemove={() => removeRecipient(recipient.id)}\n />\n ))}\n <input\n value={value}\n onChange={(event) => setValue(event.target.value)}\n onKeyDown={handleKeyDown}\n placeholder={resolvedPlaceholder}\n className=\"min-w-[130px] flex-1 h-6 text-[13px] bg-transparent border-0 outline-none placeholder:text-muted-foreground\"\n />\n </div>\n\n {showPicker || showCcBcc ? (\n <div className=\"flex gap-1.5 mt-2\">\n {showPicker ? (\n <button\n ref={contactsTriggerRef}\n type=\"button\"\n onClick={() => setPickerOpen((open) => !open)}\n 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]\"\n >\n <Users className=\"size-3\" />\n Contacts\n <ChevronDown className=\"size-3\" />\n </button>\n ) : null}\n {showCcBcc ? (\n <button\n type=\"button\"\n onClick={onCcBccToggle}\n 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]\"\n >\n <Plus className=\"size-3\" />\n {ccBccOpen ? \"Hide Cc/Bcc\" : \"Add Cc/Bcc\"}\n </button>\n ) : null}\n </div>\n ) : null}\n\n {pickerOpen ? (\n <ContactPickerPopover\n triggerRef={contactsTriggerRef}\n contacts={contacts}\n addedEmails={added}\n onSelect={selectContact}\n onAddEmail={(email) => {\n addEmail(email)\n setPickerOpen(false)\n }}\n onClose={() => setPickerOpen(false)}\n />\n ) : null}\n </div>\n </div>\n )\n}\n"],"mappings":"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","names":["handleKeyDown"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
interface EmailSendBarAction {
|
|
4
|
+
key: string;
|
|
5
|
+
label: string;
|
|
6
|
+
icon?: React.ReactNode;
|
|
7
|
+
onClick?: () => void;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface EmailSendBarProps {
|
|
11
|
+
primaryActions?: EmailSendBarAction[];
|
|
12
|
+
secondaryActions?: EmailSendBarAction[];
|
|
13
|
+
sendLabel?: string;
|
|
14
|
+
sendIcon?: React.ReactNode;
|
|
15
|
+
sendDisabled?: boolean;
|
|
16
|
+
onSend?: () => void;
|
|
17
|
+
sendHint?: React.ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
declare function EmailSendBar({ primaryActions, secondaryActions, sendLabel, sendIcon, sendDisabled, onSend, sendHint, className, }: EmailSendBarProps): React.JSX.Element;
|
|
21
|
+
|
|
22
|
+
export { EmailSendBar, type EmailSendBarAction, type EmailSendBarProps };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
"use client";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { cn } from "../lib/utils.js";
|
|
6
|
+
function GhostAction({ action }) {
|
|
7
|
+
return /* @__PURE__ */ jsxs(
|
|
8
|
+
"button",
|
|
9
|
+
{
|
|
10
|
+
type: "button",
|
|
11
|
+
onClick: action.onClick,
|
|
12
|
+
disabled: action.disabled,
|
|
13
|
+
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",
|
|
14
|
+
children: [
|
|
15
|
+
action.icon ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60 group-hover:text-muted-foreground", children: action.icon }) : null,
|
|
16
|
+
action.label
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
function EmailSendBar({
|
|
22
|
+
primaryActions = [],
|
|
23
|
+
secondaryActions = [],
|
|
24
|
+
sendLabel = "Send",
|
|
25
|
+
sendIcon,
|
|
26
|
+
sendDisabled = false,
|
|
27
|
+
onSend,
|
|
28
|
+
sendHint,
|
|
29
|
+
className
|
|
30
|
+
}) {
|
|
31
|
+
const hasPrimary = primaryActions.length > 0;
|
|
32
|
+
const hasSecondary = secondaryActions.length > 0;
|
|
33
|
+
return /* @__PURE__ */ jsxs(
|
|
34
|
+
"div",
|
|
35
|
+
{
|
|
36
|
+
className: cn(
|
|
37
|
+
"flex items-center gap-1.5 px-3.5 py-2.5 border-t bg-muted/20",
|
|
38
|
+
className
|
|
39
|
+
),
|
|
40
|
+
children: [
|
|
41
|
+
primaryActions.map((action) => /* @__PURE__ */ jsx(GhostAction, { action }, action.key)),
|
|
42
|
+
hasPrimary && hasSecondary ? /* @__PURE__ */ jsx("div", { className: "w-px h-[18px] bg-border mx-1" }) : null,
|
|
43
|
+
secondaryActions.map((action) => /* @__PURE__ */ jsx(GhostAction, { action }, action.key)),
|
|
44
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
45
|
+
sendHint ? /* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground mr-1", children: sendHint }) : null,
|
|
46
|
+
/* @__PURE__ */ jsxs(
|
|
47
|
+
"button",
|
|
48
|
+
{
|
|
49
|
+
type: "button",
|
|
50
|
+
onClick: onSend,
|
|
51
|
+
disabled: sendDisabled,
|
|
52
|
+
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",
|
|
53
|
+
children: [
|
|
54
|
+
sendIcon,
|
|
55
|
+
sendLabel
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
EmailSendBar
|
|
65
|
+
};
|
|
66
|
+
//# sourceMappingURL=email-send-bar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/email-send-bar.tsx"],"sourcesContent":["\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"../lib/utils\"\n\nexport interface EmailSendBarAction {\n key: string\n label: string\n icon?: React.ReactNode\n onClick?: () => void\n disabled?: boolean\n}\n\nexport interface EmailSendBarProps {\n primaryActions?: EmailSendBarAction[]\n secondaryActions?: EmailSendBarAction[]\n sendLabel?: string\n sendIcon?: React.ReactNode\n sendDisabled?: boolean\n onSend?: () => void\n sendHint?: React.ReactNode\n className?: string\n}\n\nfunction GhostAction({ action }: { action: EmailSendBarAction }) {\n return (\n <button\n type=\"button\"\n onClick={action.onClick}\n disabled={action.disabled}\n 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\"\n >\n {action.icon ? (\n <span className=\"text-muted-foreground/60 group-hover:text-muted-foreground\">\n {action.icon}\n </span>\n ) : null}\n {action.label}\n </button>\n )\n}\n\nexport function EmailSendBar({\n primaryActions = [],\n secondaryActions = [],\n sendLabel = \"Send\",\n sendIcon,\n sendDisabled = false,\n onSend,\n sendHint,\n className,\n}: EmailSendBarProps) {\n const hasPrimary = primaryActions.length > 0\n const hasSecondary = secondaryActions.length > 0\n\n return (\n <div\n className={cn(\n \"flex items-center gap-1.5 px-3.5 py-2.5 border-t bg-muted/20\",\n className,\n )}\n >\n {primaryActions.map((action) => (\n <GhostAction key={action.key} action={action} />\n ))}\n\n {hasPrimary && hasSecondary ? (\n <div className=\"w-px h-[18px] bg-border mx-1\" />\n ) : null}\n\n {secondaryActions.map((action) => (\n <GhostAction key={action.key} action={action} />\n ))}\n\n <div className=\"flex-1\" />\n\n {sendHint ? (\n <span className=\"text-[11px] text-muted-foreground mr-1\">\n {sendHint}\n </span>\n ) : null}\n\n <button\n type=\"button\"\n onClick={onSend}\n disabled={sendDisabled}\n 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\"\n >\n {sendIcon}\n {sendLabel}\n </button>\n </div>\n )\n}\n"],"mappings":";AA2BI,SAOI,KAPJ;AAvBJ,SAAS,UAAU;AAqBnB,SAAS,YAAY,EAAE,OAAO,GAAmC;AAC/D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS,OAAO;AAAA,MAChB,UAAU,OAAO;AAAA,MACjB,WAAU;AAAA,MAET;AAAA,eAAO,OACN,oBAAC,UAAK,WAAU,8DACb,iBAAO,MACV,IACE;AAAA,QACH,OAAO;AAAA;AAAA;AAAA,EACV;AAEJ;AAEO,SAAS,aAAa;AAAA,EAC3B,iBAAiB,CAAC;AAAA,EAClB,mBAAmB,CAAC;AAAA,EACpB,YAAY;AAAA,EACZ;AAAA,EACA,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AACF,GAAsB;AACpB,QAAM,aAAa,eAAe,SAAS;AAC3C,QAAM,eAAe,iBAAiB,SAAS;AAE/C,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEC;AAAA,uBAAe,IAAI,CAAC,WACnB,oBAAC,eAA6B,UAAZ,OAAO,GAAqB,CAC/C;AAAA,QAEA,cAAc,eACb,oBAAC,SAAI,WAAU,gCAA+B,IAC5C;AAAA,QAEH,iBAAiB,IAAI,CAAC,WACrB,oBAAC,eAA6B,UAAZ,OAAO,GAAqB,CAC/C;AAAA,QAED,oBAAC,SAAI,WAAU,UAAS;AAAA,QAEvB,WACC,oBAAC,UAAK,WAAU,0CACb,oBACH,IACE;AAAA,QAEJ;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAS;AAAA,YACT,UAAU;AAAA,YACV,WAAU;AAAA,YAET;AAAA;AAAA,cACA;AAAA;AAAA;AAAA,QACH;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
|
|
@@ -12,7 +12,7 @@ import { VariantProps } from 'class-variance-authority';
|
|
|
12
12
|
*/
|
|
13
13
|
type PillStatus = "success" | "warning" | "error" | "neutral" | "info";
|
|
14
14
|
declare const pillVariants: (props?: ({
|
|
15
|
-
variant?: "default" | "secondary" | "destructive" | "outline" | "ghost" | "
|
|
15
|
+
variant?: "error" | "default" | "secondary" | "destructive" | "outline" | "ghost" | "neutral" | "info" | "success" | "warning" | null | undefined;
|
|
16
16
|
} & class_variance_authority_types.ClassProp) | undefined) => string;
|
|
17
17
|
interface PillProps extends React.ComponentProps<"span">, VariantProps<typeof pillVariants> {
|
|
18
18
|
}
|
|
@@ -102,7 +102,7 @@ function Trigger({ className }) {
|
|
|
102
102
|
className
|
|
103
103
|
),
|
|
104
104
|
children: [
|
|
105
|
-
/* @__PURE__ */ jsx(Check, { className: "w-3 h-3 text-
|
|
105
|
+
/* @__PURE__ */ jsx(Check, { className: "w-3 h-3 text-emerald-500" }),
|
|
106
106
|
/* @__PURE__ */ jsx("span", { className: "text-[11px] text-muted-foreground", children: label })
|
|
107
107
|
]
|
|
108
108
|
}
|
|
@@ -116,9 +116,9 @@ function Trigger({ className }) {
|
|
|
116
116
|
onClick: () => handleThumbClick("up"),
|
|
117
117
|
className: cn(
|
|
118
118
|
"p-1.5 rounded transition-colors",
|
|
119
|
-
thumbState === "up" ? "bg-
|
|
119
|
+
thumbState === "up" ? "bg-emerald-100 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400" : "hover:bg-muted text-muted-foreground hover:text-foreground"
|
|
120
120
|
),
|
|
121
|
-
children: /* @__PURE__ */ jsx(ThumbsUp, { className: "w-3.5 h-3.5" })
|
|
121
|
+
children: /* @__PURE__ */ jsx(ThumbsUp, { className: "w-3.5 h-3.5", fill: thumbState === "up" ? "currentColor" : "none" })
|
|
122
122
|
}
|
|
123
123
|
),
|
|
124
124
|
/* @__PURE__ */ jsx(
|
|
@@ -128,9 +128,9 @@ function Trigger({ className }) {
|
|
|
128
128
|
onClick: () => handleThumbClick("down"),
|
|
129
129
|
className: cn(
|
|
130
130
|
"p-1.5 rounded transition-colors",
|
|
131
|
-
thumbState === "down" ? "bg-
|
|
131
|
+
thumbState === "down" ? "bg-red-100 text-red-600 dark:bg-red-900/30 dark:text-red-400" : "hover:bg-muted text-muted-foreground hover:text-foreground"
|
|
132
132
|
),
|
|
133
|
-
children: /* @__PURE__ */ jsx(ThumbsDown, { className: "w-3.5 h-3.5" })
|
|
133
|
+
children: /* @__PURE__ */ jsx(ThumbsDown, { className: "w-3.5 h-3.5", fill: thumbState === "down" ? "currentColor" : "none" })
|
|
134
134
|
}
|
|
135
135
|
)
|
|
136
136
|
] });
|
|
@@ -158,7 +158,7 @@ function Panel({ className }) {
|
|
|
158
158
|
onClick: () => togglePill(pill),
|
|
159
159
|
className: cn(
|
|
160
160
|
"px-2.5 py-1 rounded-full text-[11px] font-medium border transition-colors",
|
|
161
|
-
selectedPills.includes(pill) ? thumbState === "up" ? "bg-
|
|
161
|
+
selectedPills.includes(pill) ? thumbState === "up" ? "bg-emerald-100 text-emerald-700 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-300 dark:border-emerald-800" : "bg-red-100 text-red-700 border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-800" : "bg-background text-muted-foreground border-border hover:bg-muted/50 hover:text-foreground"
|
|
162
162
|
),
|
|
163
163
|
children: pill
|
|
164
164
|
},
|