@cntyclub/ui-react 0.2.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cntyclub/ui-react",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "React component library for the Country Club UI Kit — Base UI primitives styled with the Country Club design system (Tailwind CSS v4)",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { cva, type VariantProps } from "class-variance-authority";
4
- import { ArrowUpIcon } from "lucide-react";
4
+ import { ArrowRightIcon, ArrowUpIcon, SparklesIcon } from "lucide-react";
5
5
  import * as React from "react";
6
6
 
7
7
  import { cn } from "../../lib/utils/css";
@@ -9,9 +9,10 @@ import { Button } from "./button";
9
9
 
10
10
  /**
11
11
  * Chat primitives for AI-assistant experiences: a column layout, an
12
- * auto-scrolling message viewport, role-styled message bubbles, a tool
13
- * activity/approval card frame, a typing indicator, and a submit-on-Enter
14
- * input. Purely presentational — bring your own state.
12
+ * auto-scrolling message viewport, role-styled message bubbles, header chrome,
13
+ * an empty-state hero with suggestion prompts, tool activity chips/cards,
14
+ * a typing indicator, and a submit-on-Enter composer. Purely presentational —
15
+ * bring your own state.
15
16
  */
16
17
 
17
18
  function Chat({ className, ...props }: React.ComponentProps<"div">) {
@@ -24,6 +25,63 @@ function Chat({ className, ...props }: React.ComponentProps<"div">) {
24
25
  );
25
26
  }
26
27
 
28
+ /**
29
+ * Header chrome for a chat panel: avatar + title on the left, icon actions on
30
+ * the right. Compose `ChatHeaderAvatar`, `ChatHeaderTitle` and
31
+ * `ChatHeaderActions` inside.
32
+ */
33
+ function ChatHeader({ className, ...props }: React.ComponentProps<"div">) {
34
+ return (
35
+ <div
36
+ className={cn(
37
+ "flex shrink-0 items-center gap-1 border-border/72 border-b bg-background/95 px-3 py-2.5 backdrop-blur supports-backdrop-filter:bg-background/80",
38
+ className,
39
+ )}
40
+ data-slot="chat-header"
41
+ {...props}
42
+ />
43
+ );
44
+ }
45
+
46
+ function ChatHeaderAvatar({
47
+ className,
48
+ children,
49
+ ...props
50
+ }: React.ComponentProps<"span">) {
51
+ return (
52
+ <span
53
+ className={cn(
54
+ "relative flex size-8 shrink-0 items-center justify-center rounded-full bg-linear-to-br from-primary to-primary/78 text-primary-foreground shadow-primary/24 shadow-xs after:absolute after:right-0 after:bottom-0 after:size-2 after:rounded-full after:bg-success after:ring-2 after:ring-background [&_svg:not([class*='size-'])]:size-4",
55
+ className,
56
+ )}
57
+ data-slot="chat-header-avatar"
58
+ {...props}
59
+ >
60
+ {children ?? <SparklesIcon />}
61
+ </span>
62
+ );
63
+ }
64
+
65
+ function ChatHeaderTitle({ className, ...props }: React.ComponentProps<"div">) {
66
+ return (
67
+ <div
68
+ className={cn("min-w-0 flex-1 leading-tight", className)}
69
+ data-slot="chat-header-title"
70
+ {...props}
71
+ />
72
+ );
73
+ }
74
+
75
+ function ChatHeaderActions({ className, ...props }: React.ComponentProps<"div">) {
76
+ return (
77
+ <div
78
+ className={cn("flex shrink-0 items-center gap-0.5", className)}
79
+ data-slot="chat-header-actions"
80
+ {...props}
81
+ />
82
+ );
83
+ }
84
+
27
85
  function ChatMessages({
28
86
  autoScroll = true,
29
87
  className,
@@ -67,17 +125,133 @@ function ChatMessages({
67
125
  );
68
126
  }
69
127
 
70
- const chatMessageVariants = cva("flex w-full flex-col gap-1", {
71
- defaultVariants: {
72
- from: "assistant",
73
- },
74
- variants: {
75
- from: {
76
- assistant: "items-start",
77
- user: "items-end",
128
+ /**
129
+ * Centered hero shown when the conversation is empty. Compose
130
+ * `ChatEmptyStateIcon`, `ChatEmptyStateTitle`, `ChatEmptyStateDescription`
131
+ * and `ChatSuggestions` inside.
132
+ */
133
+ function ChatEmptyState({ className, ...props }: React.ComponentProps<"div">) {
134
+ return (
135
+ <div
136
+ className={cn(
137
+ "flex flex-1 flex-col items-center justify-center gap-3 p-6 text-center",
138
+ className,
139
+ )}
140
+ data-slot="chat-empty-state"
141
+ {...props}
142
+ />
143
+ );
144
+ }
145
+
146
+ function ChatEmptyStateIcon({
147
+ className,
148
+ children,
149
+ ...props
150
+ }: React.ComponentProps<"span">) {
151
+ return (
152
+ <span
153
+ className={cn(
154
+ "fade-in-0 zoom-in-90 relative flex size-13 shrink-0 animate-in items-center justify-center rounded-2xl bg-linear-to-br from-primary to-primary/78 text-primary-foreground shadow-lg shadow-primary/24 duration-500 before:absolute before:-inset-2.5 before:-z-10 before:rounded-3xl before:bg-primary/8 before:blur-lg [&_svg:not([class*='size-'])]:size-6",
155
+ className,
156
+ )}
157
+ data-slot="chat-empty-state-icon"
158
+ {...props}
159
+ >
160
+ {children ?? <SparklesIcon />}
161
+ </span>
162
+ );
163
+ }
164
+
165
+ function ChatEmptyStateTitle({ className, ...props }: React.ComponentProps<"p">) {
166
+ return (
167
+ <p
168
+ className={cn("mt-1 font-semibold text-base text-foreground", className)}
169
+ data-slot="chat-empty-state-title"
170
+ {...props}
171
+ />
172
+ );
173
+ }
174
+
175
+ function ChatEmptyStateDescription({
176
+ className,
177
+ ...props
178
+ }: React.ComponentProps<"p">) {
179
+ return (
180
+ <p
181
+ className={cn(
182
+ "max-w-72 text-muted-foreground text-sm leading-relaxed",
183
+ className,
184
+ )}
185
+ data-slot="chat-empty-state-description"
186
+ {...props}
187
+ />
188
+ );
189
+ }
190
+
191
+ /** Vertical stack of `ChatSuggestion` prompts. */
192
+ function ChatSuggestions({ className, ...props }: React.ComponentProps<"div">) {
193
+ return (
194
+ <div
195
+ className={cn("mt-2 flex w-full max-w-sm flex-col gap-2", className)}
196
+ data-slot="chat-suggestions"
197
+ {...props}
198
+ />
199
+ );
200
+ }
201
+
202
+ export interface ChatSuggestionProps extends React.ComponentProps<"button"> {
203
+ /** Leading icon; defaults to a sparkle. */
204
+ icon?: React.ReactNode;
205
+ /** Position in the list — staggers the entrance animation. */
206
+ index?: number;
207
+ }
208
+
209
+ /**
210
+ * One tappable suggested prompt: icon chip + label, with an arrow that slides
211
+ * in on hover and a staggered fade-in entrance (via `index`).
212
+ */
213
+ function ChatSuggestion({
214
+ className,
215
+ children,
216
+ icon,
217
+ index = 0,
218
+ style,
219
+ ...props
220
+ }: ChatSuggestionProps) {
221
+ return (
222
+ <button
223
+ className={cn(
224
+ "group fade-in-0 slide-in-from-bottom-2 flex w-full animate-in cursor-pointer items-center gap-2.5 fill-mode-both rounded-xl border border-border/72 bg-card px-3 py-2.5 text-left font-medium text-card-foreground text-sm shadow-xs outline-none transition-[border-color,background-color,box-shadow,transform] duration-200 hover:border-primary/24 hover:bg-accent/56 hover:shadow-sm focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background active:scale-[0.985] disabled:pointer-events-none disabled:opacity-64",
225
+ className,
226
+ )}
227
+ data-slot="chat-suggestion"
228
+ style={{ animationDelay: `${index * 80}ms`, ...style }}
229
+ type="button"
230
+ {...props}
231
+ >
232
+ <span className="flex size-6.5 shrink-0 items-center justify-center rounded-lg bg-primary/8 text-primary transition-colors duration-200 group-hover:bg-primary group-hover:text-primary-foreground [&_svg:not([class*='size-'])]:size-3.5">
233
+ {icon ?? <SparklesIcon />}
234
+ </span>
235
+ <span className="min-w-0 flex-1">{children}</span>
236
+ <ArrowRightIcon className="-translate-x-1 size-3.5 shrink-0 text-muted-foreground opacity-0 transition-[opacity,translate] duration-200 group-hover:translate-x-0 group-hover:opacity-100" />
237
+ </button>
238
+ );
239
+ }
240
+
241
+ const chatMessageVariants = cva(
242
+ "fade-in-0 slide-in-from-bottom-1 flex w-full animate-in flex-col gap-1 duration-200",
243
+ {
244
+ defaultVariants: {
245
+ from: "assistant",
246
+ },
247
+ variants: {
248
+ from: {
249
+ assistant: "items-start",
250
+ user: "items-end",
251
+ },
78
252
  },
79
253
  },
80
- });
254
+ );
81
255
 
82
256
  export interface ChatMessageProps
83
257
  extends React.ComponentProps<"div">,
@@ -103,7 +277,7 @@ const chatMessageBubbleVariants = cva(
103
277
  variants: {
104
278
  from: {
105
279
  assistant: "rounded-bl-md bg-muted text-foreground",
106
- user: "rounded-br-md bg-primary text-primary-foreground",
280
+ user: "rounded-br-md bg-primary text-primary-foreground shadow-primary/16 shadow-xs",
107
281
  },
108
282
  },
109
283
  },
@@ -124,6 +298,35 @@ function ChatMessageBubble({ className, from, ...props }: ChatMessageBubbleProps
124
298
  );
125
299
  }
126
300
 
301
+ /**
302
+ * Small inline pill for tool activity ("Using X…", "X finished"). Pass the
303
+ * status icon (wrench, spinner, check) as `icon`.
304
+ */
305
+ function ChatToolChip({
306
+ className,
307
+ children,
308
+ icon,
309
+ ...props
310
+ }: React.ComponentProps<"div"> & { icon?: React.ReactNode }) {
311
+ return (
312
+ <div
313
+ className={cn(
314
+ "fade-in-0 flex w-fit max-w-full animate-in items-center gap-1.5 rounded-full border border-border/72 bg-muted/48 py-1 pr-3 pl-1.5 text-muted-foreground text-xs duration-200",
315
+ className,
316
+ )}
317
+ data-slot="chat-tool-chip"
318
+ {...props}
319
+ >
320
+ {icon ? (
321
+ <span className="flex size-4.5 shrink-0 items-center justify-center rounded-full bg-background text-foreground/72 shadow-xs [&_svg:not([class*='size-'])]:size-2.5">
322
+ {icon}
323
+ </span>
324
+ ) : null}
325
+ <span className="min-w-0 truncate">{children}</span>
326
+ </div>
327
+ );
328
+ }
329
+
127
330
  /**
128
331
  * Framed card for tool activity inside a conversation: running/finished tool
129
332
  * calls, results, and write-action approval prompts. Compose with `Button`s
@@ -133,7 +336,7 @@ function ChatToolCard({ className, ...props }: React.ComponentProps<"div">) {
133
336
  return (
134
337
  <div
135
338
  className={cn(
136
- "w-full max-w-[95%] rounded-xl border border-border bg-card p-3 text-card-foreground text-sm shadow-xs",
339
+ "fade-in-0 slide-in-from-bottom-1 w-full max-w-[95%] animate-in rounded-xl border border-border bg-card p-3 text-card-foreground text-sm shadow-xs duration-200",
137
340
  className,
138
341
  )}
139
342
  data-slot="chat-tool-card"
@@ -167,7 +370,7 @@ function ChatTypingIndicator({ className, ...props }: React.ComponentProps<"div"
167
370
  <div
168
371
  aria-label="Assistant is typing"
169
372
  className={cn(
170
- "flex w-fit items-center gap-1 rounded-2xl rounded-bl-md bg-muted px-3.5 py-2.5",
373
+ "fade-in-0 flex w-fit animate-in items-center gap-1 rounded-2xl rounded-bl-md bg-muted px-3.5 py-2.5 duration-200",
171
374
  className,
172
375
  )}
173
376
  data-slot="chat-typing-indicator"
@@ -191,6 +394,10 @@ export interface ChatInputProps
191
394
  disabled?: boolean;
192
395
  }
193
396
 
397
+ /**
398
+ * Composer: a rounded card wrapping an auto-growing textarea and a round send
399
+ * button. Enter submits, Shift+Enter inserts a newline.
400
+ */
194
401
  function ChatInput({
195
402
  className,
196
403
  disabled,
@@ -218,54 +425,70 @@ function ChatInput({
218
425
 
219
426
  return (
220
427
  <form
221
- className={cn(
222
- "flex items-end gap-2 border-border border-t bg-background p-3",
223
- className,
224
- )}
428
+ className={cn("shrink-0 bg-background p-3 pt-1.5", className)}
225
429
  data-slot="chat-input"
226
430
  onSubmit={(event) => {
227
431
  event.preventDefault();
228
432
  submit();
229
433
  }}
230
434
  >
231
- <textarea
232
- className="max-h-36 min-h-9 flex-1 resize-none rounded-lg border border-input bg-transparent px-3 py-2 text-sm outline-none placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-64"
233
- disabled={disabled}
234
- onChange={(event) => onValueChange(event.target.value)}
235
- onKeyDown={(event) => {
236
- if (event.key === "Enter" && !event.shiftKey) {
237
- event.preventDefault();
238
- submit();
239
- }
240
- }}
241
- placeholder={placeholder}
242
- ref={textareaRef}
243
- rows={1}
244
- value={value}
245
- {...props}
246
- />
247
- <Button
248
- aria-label="Send message"
249
- disabled={disabled || !value.trim()}
250
- size="icon"
251
- type="submit"
435
+ <div
436
+ className={cn(
437
+ "flex items-end gap-1.5 rounded-2xl border border-input bg-popover p-1.5 pl-3.5 shadow-xs transition-[border-color,box-shadow] duration-150 focus-within:border-ring/64 focus-within:ring-2 focus-within:ring-ring/24 dark:bg-input/32",
438
+ disabled && "opacity-64",
439
+ )}
252
440
  >
253
- <ArrowUpIcon />
254
- </Button>
441
+ <textarea
442
+ className="max-h-36 min-h-8 flex-1 resize-none self-center bg-transparent py-1.5 text-sm outline-none placeholder:text-muted-foreground"
443
+ disabled={disabled}
444
+ onChange={(event) => onValueChange(event.target.value)}
445
+ onKeyDown={(event) => {
446
+ if (event.key === "Enter" && !event.shiftKey) {
447
+ event.preventDefault();
448
+ submit();
449
+ }
450
+ }}
451
+ placeholder={placeholder}
452
+ ref={textareaRef}
453
+ rows={1}
454
+ value={value}
455
+ {...props}
456
+ />
457
+ <Button
458
+ aria-label="Send message"
459
+ className="rounded-full before:rounded-full"
460
+ disabled={disabled || !value.trim()}
461
+ size="icon-sm"
462
+ type="submit"
463
+ >
464
+ <ArrowUpIcon />
465
+ </Button>
466
+ </div>
255
467
  </form>
256
468
  );
257
469
  }
258
470
 
259
471
  export {
260
472
  Chat,
473
+ ChatEmptyState,
474
+ ChatEmptyStateDescription,
475
+ ChatEmptyStateIcon,
476
+ ChatEmptyStateTitle,
477
+ ChatHeader,
478
+ ChatHeaderActions,
479
+ ChatHeaderAvatar,
480
+ ChatHeaderTitle,
261
481
  ChatInput,
262
482
  ChatMessage,
263
483
  ChatMessageBubble,
264
484
  chatMessageBubbleVariants,
265
485
  chatMessageVariants,
266
486
  ChatMessages,
487
+ ChatSuggestion,
488
+ ChatSuggestions,
267
489
  ChatToolCard,
268
490
  ChatToolCardActions,
269
491
  ChatToolCardHeader,
492
+ ChatToolChip,
270
493
  ChatTypingIndicator,
271
494
  };