@wealthx/shadcn 1.5.34 → 1.5.36

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wealthx/shadcn",
3
- "version": "1.5.34",
3
+ "version": "1.5.36",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,242 @@
1
+ import React from "react";
2
+ import ReactMarkdown from "react-markdown";
3
+ import rehypeRaw from "rehype-raw";
4
+ import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
5
+ import { Bot } from "lucide-react";
6
+ import { cn, getInitials } from "@/lib/utils";
7
+ import { Avatar, AvatarFallback } from "@/components/ui/avatar";
8
+ import { Separator } from "@/components/ui/separator";
9
+ import type { AiConvChannel, AiConvMessage, AiConvMessageRole } from "./types";
10
+
11
+ // Strips <style>, <script>, and <head> subtrees entirely so that CSS/JS text
12
+ // inside those tags is never surfaced as visible content when rendering
13
+ // full HTML email payloads.
14
+ const EMAIL_SANITIZE_SCHEMA = {
15
+ ...defaultSchema,
16
+ strip: ["script", "style", "head", ...(defaultSchema.strip ?? [])],
17
+ };
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Internal primitives
21
+ // ---------------------------------------------------------------------------
22
+
23
+ function BubbleAvatar({
24
+ role,
25
+ senderName,
26
+ }: {
27
+ role: AiConvMessageRole;
28
+ senderName?: string;
29
+ }) {
30
+ if (role === "bot") {
31
+ return (
32
+ <Avatar size="sm">
33
+ <AvatarFallback className="border border-border bg-muted">
34
+ <Bot className="size-3.5 text-muted-foreground" />
35
+ </AvatarFallback>
36
+ </Avatar>
37
+ );
38
+ }
39
+ if (role === "advisor") {
40
+ return (
41
+ <Avatar size="sm">
42
+ <AvatarFallback className="font-semibold">
43
+ {getInitials(senderName ?? "Advisor")}
44
+ </AvatarFallback>
45
+ </Avatar>
46
+ );
47
+ }
48
+ return (
49
+ <Avatar size="sm">
50
+ <AvatarFallback>{getInitials(senderName ?? "?")}</AvatarFallback>
51
+ </Avatar>
52
+ );
53
+ }
54
+
55
+ function SystemDivider({
56
+ content,
57
+ className,
58
+ }: {
59
+ content: string;
60
+ className?: string;
61
+ }) {
62
+ return (
63
+ <div className={cn("my-2 flex items-center gap-3 px-2", className)}>
64
+ <Separator className="flex-1" />
65
+ <span className="shrink-0 text-caption text-muted-foreground">
66
+ {content}
67
+ </span>
68
+ <Separator className="flex-1" />
69
+ </div>
70
+ );
71
+ }
72
+
73
+ function ChatMessageBubble({
74
+ message,
75
+ className,
76
+ }: {
77
+ message: AiConvMessage;
78
+ className?: string;
79
+ }) {
80
+ const { role, content, timestamp, senderName } = message;
81
+ const isAdvisor = role === "advisor";
82
+ const isBot = role === "bot";
83
+ const isVisitor = role === "visitor";
84
+
85
+ const displayName = isBot
86
+ ? "AI Assistant"
87
+ : isAdvisor
88
+ ? senderName ?? "Advisor"
89
+ : senderName ?? "Lead";
90
+
91
+ return (
92
+ <div
93
+ className={cn(
94
+ "flex gap-2.5",
95
+ isAdvisor ? "flex-row-reverse" : "flex-row",
96
+ className
97
+ )}
98
+ >
99
+ <BubbleAvatar role={role} senderName={senderName} />
100
+
101
+ <div
102
+ className={cn(
103
+ "flex max-w-[70%] flex-col gap-1",
104
+ isAdvisor && "items-end"
105
+ )}
106
+ >
107
+ <span className="text-caption text-muted-foreground">
108
+ {displayName}
109
+ </span>
110
+
111
+ <div
112
+ className={cn(
113
+ "break-words px-3 py-2 text-sm leading-relaxed [&_a]:underline [&_p]:m-0",
114
+ isBot &&
115
+ "border border-border bg-muted/60 text-foreground [&_a]:text-primary",
116
+ isVisitor &&
117
+ "border border-border bg-background text-foreground [&_a]:text-primary",
118
+ isAdvisor &&
119
+ "bg-primary text-primary-foreground [&_a]:text-primary-foreground"
120
+ )}
121
+ >
122
+ <ReactMarkdown
123
+ rehypePlugins={[rehypeRaw, [rehypeSanitize, defaultSchema]]}
124
+ >
125
+ {content}
126
+ </ReactMarkdown>
127
+ </div>
128
+
129
+ {timestamp && (
130
+ <span className="text-caption text-muted-foreground">
131
+ {timestamp}
132
+ </span>
133
+ )}
134
+ </div>
135
+ </div>
136
+ );
137
+ }
138
+
139
+ function EmailMessageBubble({
140
+ message,
141
+ className,
142
+ }: {
143
+ message: AiConvMessage;
144
+ className?: string;
145
+ }) {
146
+ const { role, content, timestamp, senderName, subject } = message;
147
+ const isAdvisor = role === "advisor";
148
+ const isBot = role === "bot";
149
+
150
+ const displayName = isBot
151
+ ? "AI Assistant"
152
+ : isAdvisor
153
+ ? senderName ?? "Advisor"
154
+ : senderName ?? "Lead";
155
+
156
+ return (
157
+ <div
158
+ className={cn(
159
+ "flex gap-2.5",
160
+ isAdvisor ? "flex-row-reverse" : "flex-row",
161
+ className
162
+ )}
163
+ >
164
+ <BubbleAvatar role={role} senderName={senderName} />
165
+
166
+ <div
167
+ className={cn(
168
+ "flex w-full max-w-[85%] flex-col gap-1",
169
+ isAdvisor && "items-end"
170
+ )}
171
+ >
172
+ <span className="text-caption text-muted-foreground">
173
+ {displayName}
174
+ </span>
175
+
176
+ <div
177
+ className={cn(
178
+ "w-full min-w-0 break-words whitespace-pre-line border border-border bg-background px-4 py-3 text-sm leading-relaxed [overflow-wrap:anywhere]",
179
+ "[&_p]:mb-2 [&_p:last-child]:mb-0",
180
+ "[&_ul]:mb-2 [&_ul]:list-disc [&_ul]:pl-4",
181
+ "[&_ol]:mb-2 [&_ol]:list-decimal [&_ol]:pl-4",
182
+ "[&_li]:mb-0.5",
183
+ "[&_h1]:mb-2 [&_h1]:text-sm [&_h1]:font-bold",
184
+ "[&_h2]:mb-1.5 [&_h2]:text-sm [&_h2]:font-semibold",
185
+ "[&_h3]:mb-1 [&_h3]:text-sm [&_h3]:font-medium",
186
+ "[&_strong]:font-semibold [&_em]:italic",
187
+ "[&_a]:text-primary [&_a]:underline",
188
+ "[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:italic [&_blockquote]:text-muted-foreground",
189
+ "[&_hr]:my-3 [&_hr]:border-border",
190
+ "[&_pre]:whitespace-pre-wrap [&_pre]:break-words [&_pre]:bg-transparent [&_pre]:p-0 [&_pre]:font-[inherit] [&_pre]:text-[length:inherit]",
191
+ "[&_code]:whitespace-pre-wrap [&_code]:break-words [&_code]:bg-transparent [&_code]:p-0 [&_code]:font-[inherit] [&_code]:text-[length:inherit]",
192
+ isAdvisor && "bg-muted/30"
193
+ )}
194
+ >
195
+ {subject && (
196
+ <div className="mb-2.5 border-b border-border pb-2">
197
+ <span className="text-xs font-medium text-muted-foreground">
198
+ Subject:
199
+ </span>{" "}
200
+ <span className="text-sm font-semibold text-foreground">
201
+ {subject}
202
+ </span>
203
+ </div>
204
+ )}
205
+ <ReactMarkdown
206
+ rehypePlugins={[rehypeRaw, [rehypeSanitize, EMAIL_SANITIZE_SCHEMA]]}
207
+ >
208
+ {content}
209
+ </ReactMarkdown>
210
+ </div>
211
+
212
+ {timestamp && (
213
+ <span className="text-caption text-muted-foreground">
214
+ {timestamp}
215
+ </span>
216
+ )}
217
+ </div>
218
+ </div>
219
+ );
220
+ }
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // ChatBubble — public API
224
+ // ---------------------------------------------------------------------------
225
+
226
+ export interface ChatBubbleProps {
227
+ message: AiConvMessage;
228
+ /** Channel the conversation is using — determines chat vs email rendering. */
229
+ channel?: AiConvChannel;
230
+ className?: string;
231
+ }
232
+
233
+ /** Routes to ChatMessageBubble, EmailMessageBubble, or SystemDivider. */
234
+ export function ChatBubble({ message, channel, className }: ChatBubbleProps) {
235
+ if (message.role === "system") {
236
+ return <SystemDivider content={message.content} className={className} />;
237
+ }
238
+ if (channel === "email") {
239
+ return <EmailMessageBubble message={message} className={className} />;
240
+ }
241
+ return <ChatMessageBubble message={message} className={className} />;
242
+ }
@@ -51,6 +51,7 @@ import { LeadInfoPanel } from "./lead-panel";
51
51
  // Re-export everything so consumers import from the one public path.
52
52
  export * from "./types";
53
53
  export * from "./list";
54
+ export * from "./bubble";
54
55
  export * from "./thread";
55
56
  export * from "./lead-panel";
56
57
 
@@ -186,7 +187,7 @@ export function ConversationsPage({
186
187
  className,
187
188
  }: ConversationsPageProps) {
188
189
  const [mobilePanel, setMobilePanel] = useState<"list" | "chat" | "lead">(
189
- "list",
190
+ "list"
190
191
  );
191
192
 
192
193
  const handleSelectConversation = (id: string) => {
@@ -221,7 +222,7 @@ export function ConversationsPage({
221
222
  onLoadMore={onLoadMore}
222
223
  className={cn(
223
224
  "shrink-0 md:w-[320px]",
224
- mobilePanel === "list" ? "flex w-full md:flex" : "hidden md:flex",
225
+ mobilePanel === "list" ? "flex w-full md:flex" : "hidden md:flex"
225
226
  )}
226
227
  />
227
228
 
@@ -258,14 +259,14 @@ export function ConversationsPage({
258
259
  "min-w-0 flex-1 border-r border-border",
259
260
  mobilePanel === "chat"
260
261
  ? "flex flex-col md:flex"
261
- : "hidden md:flex",
262
+ : "hidden md:flex"
262
263
  )}
263
264
  />
264
265
  ) : (
265
266
  <div
266
267
  className={cn(
267
268
  "min-w-0 flex-1 items-center justify-center border-r border-border bg-muted/10",
268
- mobilePanel === "chat" ? "flex md:flex" : "hidden md:flex",
269
+ mobilePanel === "chat" ? "flex md:flex" : "hidden md:flex"
269
270
  )}
270
271
  >
271
272
  <div className="flex flex-col items-center gap-2 text-muted-foreground">
@@ -284,7 +285,7 @@ export function ConversationsPage({
284
285
  ? "flex w-full shrink-0 flex-col"
285
286
  : "hidden",
286
287
  "md:block md:shrink-0 md:overflow-hidden md:transition-[width] md:duration-200 md:ease-in-out",
287
- showLeadPanel ? "md:w-[320px]" : "md:w-0",
288
+ showLeadPanel ? "md:w-[320px]" : "md:w-0"
288
289
  )}
289
290
  >
290
291
  <LeadInfoPanel
@@ -363,13 +364,13 @@ export function AiConvAssignAdvisorDialog({
363
364
  const [roleFilter, setRoleFilter] = useState("");
364
365
 
365
366
  const roles = Array.from(
366
- new Set(advisors.map((a) => a.role).filter(Boolean)),
367
+ new Set(advisors.map((a) => a.role).filter(Boolean))
367
368
  ) as string[];
368
369
 
369
370
  const filtered = advisors.filter(
370
371
  (a) =>
371
372
  a.name.toLowerCase().includes(search.toLowerCase()) &&
372
- (!roleFilter || a.role === roleFilter),
373
+ (!roleFilter || a.role === roleFilter)
373
374
  );
374
375
 
375
376
  const handleOpenChange = (v: boolean) => {
@@ -435,7 +436,7 @@ export function AiConvAssignAdvisorDialog({
435
436
  onClick={() => onValueChange(advisor.id)}
436
437
  className={cn(
437
438
  "flex w-full items-center gap-3 px-3 py-2.5 text-left text-sm transition-colors hover:bg-muted",
438
- value === advisor.id && "bg-muted font-medium",
439
+ value === advisor.id && "bg-muted font-medium"
439
440
  )}
440
441
  >
441
442
  <Avatar size="sm">
@@ -3,9 +3,6 @@ import { useEditor, EditorContent } from "@tiptap/react";
3
3
  import StarterKit from "@tiptap/starter-kit";
4
4
  import TiptapUnderline from "@tiptap/extension-underline";
5
5
  import TiptapLink from "@tiptap/extension-link";
6
- import ReactMarkdown from "react-markdown";
7
- import rehypeRaw from "rehype-raw";
8
- import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
9
6
  import {
10
7
  Archive,
11
8
  ArrowLeft,
@@ -25,8 +22,7 @@ import {
25
22
  Underline,
26
23
  UserCheck,
27
24
  } from "lucide-react";
28
- import { cn, getInitials } from "@/lib/utils";
29
- import { Avatar, AvatarFallback } from "@/components/ui/avatar";
25
+ import { cn } from "@/lib/utils";
30
26
  import { Button, buttonVariants } from "@/components/ui/button";
31
27
  import {
32
28
  DropdownMenu,
@@ -48,7 +44,6 @@ import type {
48
44
  AiConvChannel,
49
45
  AiConvContact,
50
46
  AiConvMessage,
51
- AiConvMessageRole,
52
47
  AiConvMode,
53
48
  AiConvStatus,
54
49
  } from "./types";
@@ -58,154 +53,7 @@ import {
58
53
  PANEL_HEADER_HEIGHT,
59
54
  } from "./helpers.tsx";
60
55
  import { ConversationStatusChip } from "./list";
61
-
62
- // ---------------------------------------------------------------------------
63
- // ChatBubble
64
- // ---------------------------------------------------------------------------
65
-
66
- export interface ChatBubbleProps {
67
- message: AiConvMessage;
68
- /** Channel the conversation is using — affects rendering style for email. */
69
- channel?: AiConvChannel;
70
- className?: string;
71
- }
72
-
73
- function BubbleAvatar({
74
- role,
75
- senderName,
76
- }: {
77
- role: AiConvMessageRole;
78
- senderName?: string;
79
- }) {
80
- if (role === "bot") {
81
- return (
82
- <Avatar size="sm">
83
- <AvatarFallback className="border border-border bg-muted">
84
- <Bot className="size-3.5 text-muted-foreground" />
85
- </AvatarFallback>
86
- </Avatar>
87
- );
88
- }
89
- if (role === "advisor") {
90
- return (
91
- <Avatar size="sm">
92
- <AvatarFallback className="font-semibold">
93
- {getInitials(senderName ?? "Advisor")}
94
- </AvatarFallback>
95
- </Avatar>
96
- );
97
- }
98
- return (
99
- <Avatar size="sm">
100
- <AvatarFallback>{getInitials(senderName ?? "?")}</AvatarFallback>
101
- </Avatar>
102
- );
103
- }
104
-
105
- export function ChatBubble({ message, channel, className }: ChatBubbleProps) {
106
- const { role, content, timestamp, senderName, subject } = message;
107
- const isEmail = channel === "email";
108
-
109
- if (role === "system") {
110
- return (
111
- <div className={cn("my-2 flex items-center gap-3 px-2", className)}>
112
- <Separator className="flex-1" />
113
- <span className="shrink-0 text-caption text-muted-foreground">
114
- {content}
115
- </span>
116
- <Separator className="flex-1" />
117
- </div>
118
- );
119
- }
120
-
121
- const isAdvisor = role === "advisor";
122
- const isBot = role === "bot";
123
- const isVisitor = role === "visitor";
124
-
125
- const displayName = isBot
126
- ? "AI Assistant"
127
- : isAdvisor
128
- ? (senderName ?? "Advisor")
129
- : (senderName ?? "Lead");
130
-
131
- return (
132
- <div
133
- className={cn(
134
- "flex gap-2.5",
135
- isAdvisor ? "flex-row-reverse" : "flex-row",
136
- className,
137
- )}
138
- >
139
- <BubbleAvatar role={role} senderName={senderName} />
140
-
141
- {/* Bubble + label */}
142
- <div
143
- className={cn(
144
- "flex flex-col gap-1",
145
- isEmail ? "max-w-[85%] w-full" : "max-w-[70%]",
146
- isAdvisor && "items-end",
147
- )}
148
- >
149
- <span className="text-caption text-muted-foreground">
150
- {displayName}
151
- </span>
152
-
153
- <div
154
- className={cn(
155
- "text-sm leading-relaxed break-words",
156
- !isEmail && "px-3 py-2 [&_p]:m-0 [&_a]:underline",
157
- isEmail && [
158
- "border border-border bg-background px-4 py-3 w-full",
159
- "[&_p]:mb-2 [&_p:last-child]:mb-0",
160
- "[&_ul]:mb-2 [&_ul]:list-disc [&_ul]:pl-4",
161
- "[&_ol]:mb-2 [&_ol]:list-decimal [&_ol]:pl-4",
162
- "[&_li]:mb-0.5",
163
- "[&_h1]:mb-2 [&_h1]:text-sm [&_h1]:font-bold",
164
- "[&_h2]:mb-1.5 [&_h2]:text-sm [&_h2]:font-semibold",
165
- "[&_h3]:mb-1 [&_h3]:text-sm [&_h3]:font-medium",
166
- "[&_strong]:font-semibold [&_em]:italic",
167
- "[&_a]:underline [&_a]:text-primary",
168
- "[&_blockquote]:border-l-2 [&_blockquote]:border-border [&_blockquote]:pl-3 [&_blockquote]:text-muted-foreground [&_blockquote]:italic",
169
- "[&_hr]:my-3 [&_hr]:border-border",
170
- ],
171
- !isEmail &&
172
- isBot &&
173
- "border border-border bg-muted/60 text-foreground [&_a]:text-primary",
174
- !isEmail &&
175
- isVisitor &&
176
- "border border-border bg-background text-foreground [&_a]:text-primary",
177
- !isEmail &&
178
- isAdvisor &&
179
- "bg-primary text-primary-foreground [&_a]:text-primary-foreground",
180
- isEmail && isAdvisor && "bg-muted/30",
181
- )}
182
- >
183
- {isEmail && subject && (
184
- <div className="mb-2.5 border-b border-border pb-2">
185
- <span className="text-xs font-medium text-muted-foreground">
186
- Subject:
187
- </span>{" "}
188
- <span className="text-sm font-semibold text-foreground">
189
- {subject}
190
- </span>
191
- </div>
192
- )}
193
- <ReactMarkdown
194
- rehypePlugins={[rehypeRaw, [rehypeSanitize, defaultSchema]]}
195
- >
196
- {content}
197
- </ReactMarkdown>
198
- </div>
199
-
200
- {timestamp && (
201
- <span className="text-caption text-muted-foreground">
202
- {timestamp}
203
- </span>
204
- )}
205
- </div>
206
- </div>
207
- );
208
- }
56
+ import { ChatBubble } from "./bubble";
209
57
 
210
58
  // ---------------------------------------------------------------------------
211
59
  // ChatComposer
@@ -266,7 +114,7 @@ function ComposerToolbarButton({
266
114
  "flex size-7 items-center justify-center transition-colors",
267
115
  pressed
268
116
  ? "bg-foreground text-background"
269
- : "text-muted-foreground hover:bg-muted hover:text-foreground",
117
+ : "text-muted-foreground hover:bg-muted hover:text-foreground"
270
118
  )}
271
119
  >
272
120
  <Icon className="size-3.5" />
@@ -308,7 +156,7 @@ function ComposerLinkPopover({
308
156
  "flex size-7 items-center justify-center transition-colors",
309
157
  editor?.isActive("link")
310
158
  ? "bg-foreground text-background"
311
- : "text-muted-foreground hover:bg-muted hover:text-foreground",
159
+ : "text-muted-foreground hover:bg-muted hover:text-foreground"
312
160
  )}
313
161
  >
314
162
  <Link2 className="size-3.5" />
@@ -369,16 +217,16 @@ export function ChatComposer({
369
217
  // Force chat when email isn't integrated so the panel never lands on a hidden tab.
370
218
  const initialChannelRef = React.useRef(channelProp);
371
219
  const [channel, setChannel] = React.useState<AiConvChannel>(
372
- isEmailIntegrated ? channelProp : "chat",
220
+ isEmailIntegrated ? channelProp : "chat"
373
221
  );
374
222
  const [emailTo, setEmailTo] = React.useState(contactEmail);
375
223
  const [emailCc, setEmailCc] = React.useState("");
376
224
  const [showCc, setShowCc] = React.useState(false);
377
225
  const [emailSubject, setEmailSubject] = React.useState(
378
- emailReplySubject ? `Re: ${emailReplySubject}` : "",
226
+ emailReplySubject ? `Re: ${emailReplySubject}` : ""
379
227
  );
380
228
  const [emailMode, setEmailMode] = React.useState<"reply" | "new">(
381
- emailReplySubject ? "reply" : "new",
229
+ emailReplySubject ? "reply" : "new"
382
230
  );
383
231
 
384
232
  const [, forceUpdate] = React.useReducer((x: number) => x + 1, 0);
@@ -424,7 +272,7 @@ export function ChatComposer({
424
272
  <div
425
273
  className={cn(
426
274
  "flex flex-col border-t border-border bg-background",
427
- className,
275
+ className
428
276
  )}
429
277
  >
430
278
  {isEmailIntegrated && (
@@ -468,7 +316,7 @@ export function ChatComposer({
468
316
  <div
469
317
  className={cn(
470
318
  "flex flex-col",
471
- channel !== "email" && "invisible pointer-events-none",
319
+ channel !== "email" && "invisible pointer-events-none"
472
320
  )}
473
321
  aria-hidden={channel !== "email"}
474
322
  >
@@ -750,7 +598,7 @@ export function ChatThread({
750
598
  <div
751
599
  className={cn(
752
600
  PANEL_HEADER_HEIGHT,
753
- "flex items-center gap-3 border-b border-border px-4",
601
+ "flex items-center gap-3 border-b border-border px-4"
754
602
  )}
755
603
  >
756
604
  {onBack && (
@@ -803,7 +651,7 @@ export function ChatThread({
803
651
  <DropdownMenuTrigger
804
652
  className={cn(
805
653
  buttonVariants({ variant: "ghost", size: "icon" }),
806
- "size-8",
654
+ "size-8"
807
655
  )}
808
656
  aria-label="More actions"
809
657
  >