@wealthx/shadcn 1.5.37 → 1.5.38

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.
Files changed (34) hide show
  1. package/.turbo/turbo-build.log +99 -93
  2. package/CHANGELOG.md +6 -0
  3. package/dist/{chunk-734FOOJC.mjs → chunk-B5PSUONN.mjs} +25 -58
  4. package/dist/chunk-EFHPSKVF.mjs +192 -0
  5. package/dist/{chunk-NB3ZL36B.mjs → chunk-MZI77ZMX.mjs} +17 -2
  6. package/dist/chunk-R7M657QL.mjs +587 -0
  7. package/dist/{chunk-DIH2NZZ3.mjs → chunk-RRROLESJ.mjs} +33 -23
  8. package/dist/components/ui/ai-assistant-drawer.js +269 -121
  9. package/dist/components/ui/ai-assistant-drawer.mjs +2 -1
  10. package/dist/components/ui/ai-conversations/index.js +474 -286
  11. package/dist/components/ui/ai-conversations/index.mjs +2 -1
  12. package/dist/components/ui/chat-input-area.js +429 -0
  13. package/dist/components/ui/chat-input-area.mjs +11 -0
  14. package/dist/components/ui/page-top-bar.js +182 -5
  15. package/dist/components/ui/page-top-bar.mjs +3 -1
  16. package/dist/components/ui/support-agent/index.js +1131 -0
  17. package/dist/components/ui/support-agent/index.mjs +27 -0
  18. package/dist/index.js +4760 -4027
  19. package/dist/index.mjs +54 -36
  20. package/dist/styles.css +1 -1
  21. package/package.json +11 -1
  22. package/src/components/index.tsx +24 -0
  23. package/src/components/ui/ai-assistant-drawer.tsx +24 -51
  24. package/src/components/ui/ai-conversations/index.tsx +16 -8
  25. package/src/components/ui/ai-conversations/thread.tsx +38 -27
  26. package/src/components/ui/chat-input-area.tsx +244 -0
  27. package/src/components/ui/page-top-bar.tsx +31 -5
  28. package/src/components/ui/support-agent/index.tsx +25 -0
  29. package/src/components/ui/support-agent/support-agent-fab.tsx +116 -0
  30. package/src/components/ui/support-agent/support-agent-panel.tsx +498 -0
  31. package/src/components/ui/support-agent/support-agent-primitives.tsx +354 -0
  32. package/src/styles/globals.css +1 -0
  33. package/src/styles/styles-css.ts +1 -1
  34. package/tsup.config.ts +2 -0
@@ -0,0 +1,244 @@
1
+ import * as React from "react";
2
+ import { ImagePlus, Paperclip, Send } from "lucide-react";
3
+ import { cn } from "@/lib/utils";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Textarea } from "@/components/ui/textarea";
6
+
7
+ /**
8
+ * ChatInputArea — WealthX Design System
9
+ *
10
+ * General-purpose chat input used across any feature that involves
11
+ * a conversation or messaging interface (Policy AI, Support Agent,
12
+ * AI Conversations, Website Chat Widget, etc.).
13
+ *
14
+ * Features:
15
+ * - Textarea with auto-resize up to `maxHeight`
16
+ * - Enter to send / Shift+Enter for new line
17
+ * - Optional file attachment (Paperclip) — shown only when `onAttachFile` is provided
18
+ * - Optional image upload (ImagePlus) — shown only when `onAttachImage` is provided
19
+ * - Focus ring on outer container via `focus-within`
20
+ * - Fully disabled during streaming / loading states
21
+ *
22
+ * @example
23
+ * <ChatInputArea
24
+ * value={inputValue}
25
+ * onChange={setInputValue}
26
+ * onSend={handleSend}
27
+ * onAttachFile={handleFiles}
28
+ * onAttachImage={handleImages}
29
+ * placeholder="Ask anything…"
30
+ * />
31
+ */
32
+ export interface ChatInputAreaProps {
33
+ /** Controlled text value. */
34
+ value: string;
35
+ /** Called on every keystroke. */
36
+ onChange: (value: string) => void;
37
+ /**
38
+ * Called when the user submits (Enter key or Send button click).
39
+ * Receives the trimmed text. Not called when value is empty or when disabled.
40
+ */
41
+ onSend: (value: string) => void;
42
+ /**
43
+ * When provided, a Paperclip button appears and this callback is fired
44
+ * with the selected FileList. Hidden when omitted.
45
+ */
46
+ onAttachFile?: (files: FileList) => void;
47
+ /**
48
+ * When provided, an ImagePlus button appears and this callback is fired
49
+ * with the selected image FileList. Hidden when omitted.
50
+ */
51
+ onAttachImage?: (files: FileList) => void;
52
+ /** Disables all controls — use while streaming / waiting for a response. */
53
+ disabled?: boolean;
54
+ /** Textarea placeholder text. */
55
+ placeholder?: string;
56
+ /**
57
+ * Hint text rendered below the input box.
58
+ * Pass `false` to hide it entirely.
59
+ * Defaults to "Enter to send · Shift+Enter for new line".
60
+ */
61
+ hint?: string | false;
62
+ /**
63
+ * Maximum textarea height in pixels before scrolling kicks in.
64
+ * @default 160
65
+ */
66
+ maxHeight?: number;
67
+ /** Focus the textarea on mount. */
68
+ autoFocus?: boolean;
69
+ className?: string;
70
+ }
71
+
72
+ const DEFAULT_HINT = "Enter to send · Shift+Enter for new line";
73
+
74
+ export function ChatInputArea({
75
+ value,
76
+ onChange,
77
+ onSend,
78
+ onAttachFile,
79
+ onAttachImage,
80
+ disabled = false,
81
+ placeholder = "Type your message…",
82
+ hint = DEFAULT_HINT,
83
+ maxHeight = 160,
84
+ autoFocus = false,
85
+ className,
86
+ }: ChatInputAreaProps) {
87
+ const textareaRef = React.useRef<HTMLTextAreaElement>(null);
88
+ const fileInputRef = React.useRef<HTMLInputElement>(null);
89
+ const imageInputRef = React.useRef<HTMLInputElement>(null);
90
+
91
+ // Focus on mount when autoFocus is requested
92
+ React.useEffect(() => {
93
+ if (autoFocus) {
94
+ setTimeout(() => textareaRef.current?.focus(), 50);
95
+ }
96
+ }, [autoFocus]);
97
+
98
+ const handleSend = React.useCallback(() => {
99
+ const text = value.trim();
100
+ if (!text || disabled) return;
101
+ onSend(text);
102
+ // Reset textarea height after clearing (caller is responsible for clearing value)
103
+ if (textareaRef.current) {
104
+ textareaRef.current.style.height = "auto";
105
+ }
106
+ }, [value, disabled, onSend]);
107
+
108
+ const handleKeyDown = React.useCallback(
109
+ (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
110
+ if (e.key === "Enter" && !e.shiftKey) {
111
+ e.preventDefault();
112
+ handleSend();
113
+ }
114
+ },
115
+ [handleSend],
116
+ );
117
+
118
+ const handleTextareaChange = React.useCallback(
119
+ (e: React.ChangeEvent<HTMLTextAreaElement>) => {
120
+ onChange(e.target.value);
121
+ // Auto-resize
122
+ const el = e.target;
123
+ el.style.height = "auto";
124
+ el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`;
125
+ },
126
+ [onChange, maxHeight],
127
+ );
128
+
129
+ const handleFileChange = React.useCallback(
130
+ (e: React.ChangeEvent<HTMLInputElement>) => {
131
+ if (e.target.files?.length) {
132
+ onAttachFile?.(e.target.files);
133
+ e.target.value = "";
134
+ }
135
+ },
136
+ [onAttachFile],
137
+ );
138
+
139
+ const handleImageChange = React.useCallback(
140
+ (e: React.ChangeEvent<HTMLInputElement>) => {
141
+ if (e.target.files?.length) {
142
+ onAttachImage?.(e.target.files);
143
+ e.target.value = "";
144
+ }
145
+ },
146
+ [onAttachImage],
147
+ );
148
+
149
+ const showFileButton = typeof onAttachFile === "function";
150
+ const showImageButton = typeof onAttachImage === "function";
151
+
152
+ return (
153
+ <div
154
+ data-slot="chat-input-area"
155
+ className={cn("flex flex-col gap-1.5", className)}
156
+ >
157
+ {/* Unified input box — textarea + action bar share one border */}
158
+ <div className="border border-border bg-background flex flex-col focus-within:ring-1 focus-within:ring-ring">
159
+ <Textarea
160
+ ref={textareaRef}
161
+ value={value}
162
+ onChange={handleTextareaChange}
163
+ onKeyDown={handleKeyDown}
164
+ placeholder={placeholder}
165
+ disabled={disabled}
166
+ rows={3}
167
+ className="resize-none text-sm border-0 shadow-none focus-visible:ring-0 px-3 pt-3 pb-1 min-h-[72px]"
168
+ aria-label={placeholder}
169
+ />
170
+
171
+ {/* Action bar — attachment buttons on the left, Send on the right */}
172
+ <div className="flex items-center justify-between px-2 pb-2">
173
+ {/* Left: attachment buttons */}
174
+ <div className="flex items-center gap-0.5">
175
+ {showFileButton && (
176
+ <>
177
+ <Button
178
+ variant="ghost"
179
+ size="icon-sm"
180
+ type="button"
181
+ title="Attach file"
182
+ aria-label="Attach file"
183
+ disabled={disabled}
184
+ onClick={() => fileInputRef.current?.click()}
185
+ >
186
+ <Paperclip className="size-3.5" aria-hidden="true" />
187
+ </Button>
188
+ <input
189
+ ref={fileInputRef}
190
+ type="file"
191
+ multiple
192
+ className="sr-only"
193
+ tabIndex={-1}
194
+ onChange={handleFileChange}
195
+ />
196
+ </>
197
+ )}
198
+
199
+ {showImageButton && (
200
+ <>
201
+ <Button
202
+ variant="ghost"
203
+ size="icon-sm"
204
+ type="button"
205
+ title="Upload image"
206
+ aria-label="Upload image"
207
+ disabled={disabled}
208
+ onClick={() => imageInputRef.current?.click()}
209
+ >
210
+ <ImagePlus className="size-3.5" aria-hidden="true" />
211
+ </Button>
212
+ <input
213
+ ref={imageInputRef}
214
+ type="file"
215
+ multiple
216
+ accept="image/*"
217
+ className="sr-only"
218
+ tabIndex={-1}
219
+ onChange={handleImageChange}
220
+ />
221
+ </>
222
+ )}
223
+ </div>
224
+
225
+ {/* Right: Send */}
226
+ <Button
227
+ size="icon-sm"
228
+ type="button"
229
+ aria-label="Send message"
230
+ disabled={!value.trim() || disabled}
231
+ onClick={handleSend}
232
+ >
233
+ <Send className="size-3.5" aria-hidden="true" />
234
+ </Button>
235
+ </div>
236
+ </div>
237
+
238
+ {/* Hint text */}
239
+ {hint !== false && (
240
+ <p className="text-xs text-muted-foreground">{hint}</p>
241
+ )}
242
+ </div>
243
+ );
244
+ }
@@ -1,4 +1,6 @@
1
1
  import * as React from "react";
2
+ import { Bot } from "lucide-react";
3
+ import { Button } from "@/components/ui/button";
2
4
  import { cn } from "@/lib/utils";
3
5
 
4
6
  /**
@@ -10,6 +12,12 @@ import { cn } from "@/lib/utils";
10
12
  *
11
13
  * Used by: Loan CRM, Contact, AI Builder, and other data-heavy pages.
12
14
  * For an in-page title block with a description, use PageHeader instead.
15
+ *
16
+ * Layout:
17
+ * [Title] [page-specific actions…] [Ask Support]
18
+ *
19
+ * "Ask Support" is always pinned to the far right when onAskSupport is
20
+ * provided — it is never passed through the actions slot.
13
21
  */
14
22
 
15
23
  // ---------------------------------------------------------------------------
@@ -19,8 +27,13 @@ import { cn } from "@/lib/utils";
19
27
  export interface PageTopBarProps {
20
28
  /** Page title shown on the left */
21
29
  title: string;
22
- /** Optional action elements rendered to the right */
30
+ /** Page-specific action elements (buttons, dropdowns, etc.) */
23
31
  actions?: React.ReactNode;
32
+ /**
33
+ * When provided, renders the "Ask Support" button pinned to the far right,
34
+ * after all page-specific actions. The handler should open the SupportAgentPanel.
35
+ */
36
+ onAskSupport?: () => void;
24
37
  className?: string;
25
38
  }
26
39
 
@@ -28,21 +41,34 @@ export interface PageTopBarProps {
28
41
  // Component
29
42
  // ---------------------------------------------------------------------------
30
43
 
31
- export function PageTopBar({ title, actions, className }: PageTopBarProps) {
44
+ export function PageTopBar({
45
+ title,
46
+ actions,
47
+ onAskSupport,
48
+ className,
49
+ }: PageTopBarProps) {
32
50
  return (
33
51
  <div
34
52
  data-slot="page-top-bar"
35
53
  className={cn(
36
54
  "flex shrink-0 items-center justify-between gap-4",
37
55
  "border-b border-border bg-background px-6 py-3",
38
- className,
56
+ className
39
57
  )}
40
58
  >
41
59
  {/* Title */}
42
60
  <h1 className="text-lg font-semibold leading-tight">{title}</h1>
43
61
 
44
- {/* Optional actions */}
45
- {actions && <div className="flex items-center gap-2">{actions}</div>}
62
+ {/* Right side: page actions + Ask Support (always last) */}
63
+ <div className="flex items-center gap-2">
64
+ {actions}
65
+ {onAskSupport && (
66
+ <Button variant="outline" size="sm" onClick={onAskSupport}>
67
+ <Bot className="size-3.5" aria-hidden="true" />
68
+ Ask Support
69
+ </Button>
70
+ )}
71
+ </div>
46
72
  </div>
47
73
  );
48
74
  }
@@ -0,0 +1,25 @@
1
+ export {
2
+ SupportContextChip,
3
+ SupportSuggestedQuestion,
4
+ SupportStepGuideCard,
5
+ SupportArticleCard,
6
+ } from "./support-agent-primitives";
7
+ export type {
8
+ SupportAgentContext,
9
+ SupportAgentStep,
10
+ SupportAgentRichContent,
11
+ SupportContextChipProps,
12
+ SupportSuggestedQuestionProps,
13
+ SupportStepGuideCardProps,
14
+ SupportArticleCardProps,
15
+ } from "./support-agent-primitives";
16
+
17
+ export { SupportAgentFAB } from "./support-agent-fab";
18
+ export type { SupportAgentFABProps } from "./support-agent-fab";
19
+
20
+ export { SupportAgentPanel } from "./support-agent-panel";
21
+ export type {
22
+ SupportAgentMessage,
23
+ SupportAgentRecentConversation,
24
+ SupportAgentPanelProps,
25
+ } from "./support-agent-panel";
@@ -0,0 +1,116 @@
1
+ import * as React from "react";
2
+ import { HelpCircle, X } from "lucide-react";
3
+ import { cn } from "@/lib/utils";
4
+ import { Button } from "@/components/ui/button";
5
+
6
+ /**
7
+ * SupportAgentFAB — WealthX DS
8
+ *
9
+ * Floating action button that opens/closes the Support Agent panel.
10
+ * Positioned fixed at bottom-right (or bottom-left) of the viewport.
11
+ *
12
+ * Features:
13
+ * - Nudge badge: red dot shown when the agent has a proactive suggestion
14
+ * - Nudge message: small dismissible tooltip bubble above the button
15
+ * - Toggles between HelpCircle (closed) and X (open) icon
16
+ */
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Types
20
+ // ---------------------------------------------------------------------------
21
+
22
+ export interface SupportAgentFABProps {
23
+ /** Whether the support panel is currently open. */
24
+ isOpen?: boolean;
25
+ /** Called when the FAB is clicked. */
26
+ onClick: () => void;
27
+ /** Show a red nudge dot on the button. */
28
+ hasNudge?: boolean;
29
+ /** Short message shown in a bubble above the FAB when hasNudge is true. */
30
+ nudgeMessage?: string;
31
+ /** Called when the user dismisses the nudge bubble. */
32
+ onDismissNudge?: () => void;
33
+ /** Position of the FAB on the screen. */
34
+ position?: "bottom-right" | "bottom-left";
35
+ className?: string;
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // SupportAgentFAB
40
+ // ---------------------------------------------------------------------------
41
+
42
+ export function SupportAgentFAB({
43
+ isOpen = false,
44
+ onClick,
45
+ hasNudge = false,
46
+ nudgeMessage,
47
+ onDismissNudge,
48
+ position = "bottom-right",
49
+ className,
50
+ }: SupportAgentFABProps) {
51
+ const showBubble = hasNudge && !!nudgeMessage && !isOpen;
52
+
53
+ return (
54
+ <div
55
+ className={cn(
56
+ "fixed bottom-6 z-50 flex flex-col items-end gap-2",
57
+ position === "bottom-right" ? "right-6" : "left-6",
58
+ className
59
+ )}
60
+ data-slot="support-agent-fab"
61
+ >
62
+ {/* Nudge message bubble */}
63
+ {showBubble && (
64
+ <div className="relative flex max-w-[220px] items-start gap-2 border border-border bg-background px-3 py-2 shadow-md">
65
+ <p className="flex-1 text-xs text-foreground leading-relaxed">
66
+ {nudgeMessage}
67
+ </p>
68
+ {onDismissNudge && (
69
+ <button
70
+ type="button"
71
+ onClick={onDismissNudge}
72
+ className="mt-0.5 shrink-0 text-muted-foreground hover:text-foreground"
73
+ aria-label="Dismiss"
74
+ >
75
+ <X className="size-3" />
76
+ </button>
77
+ )}
78
+ {/* Bubble tail pointing down */}
79
+ <span
80
+ className="absolute -bottom-[5px] right-4 size-2.5 rotate-45 border-b border-r border-border bg-background"
81
+ aria-hidden="true"
82
+ />
83
+ </div>
84
+ )}
85
+
86
+ {/* FAB button */}
87
+ <div className="relative">
88
+ <Button
89
+ type="button"
90
+ size="icon"
91
+ onClick={onClick}
92
+ className="size-12 rounded-full shadow-lg"
93
+ aria-label={
94
+ isOpen ? "Close support assistant" : "Open support assistant"
95
+ }
96
+ data-state={isOpen ? "open" : "closed"}
97
+ >
98
+ {isOpen ? (
99
+ <X className="size-5" aria-hidden="true" />
100
+ ) : (
101
+ <HelpCircle className="size-5" aria-hidden="true" />
102
+ )}
103
+ </Button>
104
+
105
+ {/* Nudge dot */}
106
+ {hasNudge && !isOpen && (
107
+ <span
108
+ className="absolute -right-0.5 -top-0.5 size-3 rounded-full border-2 border-background bg-destructive"
109
+ aria-label="New suggestion"
110
+ role="status"
111
+ />
112
+ )}
113
+ </div>
114
+ </div>
115
+ );
116
+ }