@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/dist/index.d.ts +47 -4
- package/dist/index.js +226 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ui/chat.tsx +266 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cntyclub/ui-react",
|
|
3
|
-
"version": "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,
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
<
|
|
232
|
-
className=
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
<
|
|
254
|
-
|
|
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
|
};
|