@cntyclub/ui-react 0.1.0 → 0.2.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/chunk-2HQPCV2L.js +895 -0
- package/dist/chunk-2HQPCV2L.js.map +1 -0
- package/dist/form.d.ts +1 -1
- package/dist/form.js +10 -4861
- package/dist/form.js.map +1 -1
- package/dist/index.d.ts +97 -3
- package/dist/index.js +8976 -80744
- package/dist/index.js.map +1 -1
- package/dist/{input-CZvh825j.d.ts → input-JCZs61MO.d.ts} +1 -1
- package/package.json +3 -1
- package/src/components/ui/chat.tsx +271 -0
- package/src/components/ui/data-table-paged.tsx +163 -0
- package/src/components/ui/fab.tsx +45 -0
- package/src/components/ui/markdown.tsx +107 -0
- package/src/index.ts +4 -0
- package/dist/chunk-HDGMSYQS.js +0 -26461
- package/dist/chunk-HDGMSYQS.js.map +0 -1
- package/dist/chunk-PR4QN5HX.js +0 -39
- package/dist/chunk-PR4QN5HX.js.map +0 -1
- package/dist/qr-code-styling-3Y6LZH6V.js +0 -1123
- package/dist/qr-code-styling-3Y6LZH6V.js.map +0 -1
|
@@ -21,4 +21,4 @@ type InputProps = Omit<Input$1.Props & React.RefAttributes<HTMLInputElement>, "s
|
|
|
21
21
|
};
|
|
22
22
|
declare function Input({ className, size, unstyled, nativeInput, ...props }: InputProps): React.JSX.Element;
|
|
23
23
|
|
|
24
|
-
export { Button as B, Input as I, type
|
|
24
|
+
export { Button as B, Input as I, type ButtonProps as a, type InputProps as b, buttonVariants as c };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cntyclub/ui-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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": [
|
|
@@ -48,8 +48,10 @@
|
|
|
48
48
|
"qr-code-styling": "^1.9.2",
|
|
49
49
|
"react-day-picker": "^9.14.0",
|
|
50
50
|
"react-dropzone": "^14.4.0",
|
|
51
|
+
"react-markdown": "^9.1.0",
|
|
51
52
|
"react-resizable-panels": "^3.0.6",
|
|
52
53
|
"recharts": "^3.7.0",
|
|
54
|
+
"remark-gfm": "^4.0.1",
|
|
53
55
|
"spin-delay": "^2.0.1",
|
|
54
56
|
"tailwind-merge": "^3.4.0",
|
|
55
57
|
"tw-animate-css": "^1.4.0",
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import { ArrowUpIcon } from "lucide-react";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../lib/utils/css";
|
|
8
|
+
import { Button } from "./button";
|
|
9
|
+
|
|
10
|
+
/**
|
|
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.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function Chat({ className, ...props }: React.ComponentProps<"div">) {
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className={cn("flex min-h-0 flex-1 flex-col", className)}
|
|
21
|
+
data-slot="chat"
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function ChatMessages({
|
|
28
|
+
autoScroll = true,
|
|
29
|
+
className,
|
|
30
|
+
children,
|
|
31
|
+
...props
|
|
32
|
+
}: React.ComponentProps<"div"> & {
|
|
33
|
+
/** Keep the viewport pinned to the bottom while new messages stream in. */
|
|
34
|
+
autoScroll?: boolean;
|
|
35
|
+
}) {
|
|
36
|
+
const viewportRef = React.useRef<HTMLDivElement>(null);
|
|
37
|
+
const pinnedRef = React.useRef(true);
|
|
38
|
+
|
|
39
|
+
const handleScroll = React.useCallback(() => {
|
|
40
|
+
const viewport = viewportRef.current;
|
|
41
|
+
if (!viewport) return;
|
|
42
|
+
const distanceFromBottom =
|
|
43
|
+
viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
|
|
44
|
+
pinnedRef.current = distanceFromBottom < 48;
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: children is the scroll trigger
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
const viewport = viewportRef.current;
|
|
50
|
+
if (!viewport || !autoScroll || !pinnedRef.current) return;
|
|
51
|
+
viewport.scrollTop = viewport.scrollHeight;
|
|
52
|
+
}, [children, autoScroll]);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
className={cn(
|
|
57
|
+
"flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto scroll-smooth p-4",
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
60
|
+
data-slot="chat-messages"
|
|
61
|
+
onScroll={handleScroll}
|
|
62
|
+
ref={viewportRef}
|
|
63
|
+
{...props}
|
|
64
|
+
>
|
|
65
|
+
{children}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
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",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export interface ChatMessageProps
|
|
83
|
+
extends React.ComponentProps<"div">,
|
|
84
|
+
VariantProps<typeof chatMessageVariants> {}
|
|
85
|
+
|
|
86
|
+
function ChatMessage({ className, from, ...props }: ChatMessageProps) {
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
className={cn(chatMessageVariants({ className, from }))}
|
|
90
|
+
data-from={from ?? "assistant"}
|
|
91
|
+
data-slot="chat-message"
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const chatMessageBubbleVariants = cva(
|
|
98
|
+
"max-w-[85%] rounded-2xl px-3.5 py-2 text-sm leading-relaxed break-words",
|
|
99
|
+
{
|
|
100
|
+
defaultVariants: {
|
|
101
|
+
from: "assistant",
|
|
102
|
+
},
|
|
103
|
+
variants: {
|
|
104
|
+
from: {
|
|
105
|
+
assistant: "rounded-bl-md bg-muted text-foreground",
|
|
106
|
+
user: "rounded-br-md bg-primary text-primary-foreground",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
export interface ChatMessageBubbleProps
|
|
113
|
+
extends React.ComponentProps<"div">,
|
|
114
|
+
VariantProps<typeof chatMessageBubbleVariants> {}
|
|
115
|
+
|
|
116
|
+
function ChatMessageBubble({ className, from, ...props }: ChatMessageBubbleProps) {
|
|
117
|
+
return (
|
|
118
|
+
<div
|
|
119
|
+
className={cn(chatMessageBubbleVariants({ className, from }))}
|
|
120
|
+
data-from={from ?? "assistant"}
|
|
121
|
+
data-slot="chat-message-bubble"
|
|
122
|
+
{...props}
|
|
123
|
+
/>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Framed card for tool activity inside a conversation: running/finished tool
|
|
129
|
+
* calls, results, and write-action approval prompts. Compose with `Button`s
|
|
130
|
+
* in the `actions` area for approve/decline flows.
|
|
131
|
+
*/
|
|
132
|
+
function ChatToolCard({ className, ...props }: React.ComponentProps<"div">) {
|
|
133
|
+
return (
|
|
134
|
+
<div
|
|
135
|
+
className={cn(
|
|
136
|
+
"w-full max-w-[95%] rounded-xl border border-border bg-card p-3 text-card-foreground text-sm shadow-xs",
|
|
137
|
+
className,
|
|
138
|
+
)}
|
|
139
|
+
data-slot="chat-tool-card"
|
|
140
|
+
{...props}
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function ChatToolCardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
146
|
+
return (
|
|
147
|
+
<div
|
|
148
|
+
className={cn("flex items-center gap-2 font-medium", className)}
|
|
149
|
+
data-slot="chat-tool-card-header"
|
|
150
|
+
{...props}
|
|
151
|
+
/>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function ChatToolCardActions({ className, ...props }: React.ComponentProps<"div">) {
|
|
156
|
+
return (
|
|
157
|
+
<div
|
|
158
|
+
className={cn("mt-3 flex items-center justify-end gap-2", className)}
|
|
159
|
+
data-slot="chat-tool-card-actions"
|
|
160
|
+
{...props}
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function ChatTypingIndicator({ className, ...props }: React.ComponentProps<"div">) {
|
|
166
|
+
return (
|
|
167
|
+
<div
|
|
168
|
+
aria-label="Assistant is typing"
|
|
169
|
+
className={cn(
|
|
170
|
+
"flex w-fit items-center gap-1 rounded-2xl rounded-bl-md bg-muted px-3.5 py-2.5",
|
|
171
|
+
className,
|
|
172
|
+
)}
|
|
173
|
+
data-slot="chat-typing-indicator"
|
|
174
|
+
role="status"
|
|
175
|
+
{...props}
|
|
176
|
+
>
|
|
177
|
+
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/60 [animation-delay:0ms]" />
|
|
178
|
+
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/60 [animation-delay:150ms]" />
|
|
179
|
+
<span className="size-1.5 animate-bounce rounded-full bg-muted-foreground/60 [animation-delay:300ms]" />
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface ChatInputProps
|
|
185
|
+
extends Omit<React.ComponentProps<"textarea">, "onSubmit" | "value" | "onChange"> {
|
|
186
|
+
value: string;
|
|
187
|
+
onValueChange: (value: string) => void;
|
|
188
|
+
/** Called with the trimmed message when the user submits (Enter or button). */
|
|
189
|
+
onSubmit: (value: string) => void;
|
|
190
|
+
/** Disables both the textarea and the send button (e.g. while sending). */
|
|
191
|
+
disabled?: boolean;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function ChatInput({
|
|
195
|
+
className,
|
|
196
|
+
disabled,
|
|
197
|
+
onSubmit,
|
|
198
|
+
onValueChange,
|
|
199
|
+
placeholder = "Ask anything…",
|
|
200
|
+
value,
|
|
201
|
+
...props
|
|
202
|
+
}: ChatInputProps) {
|
|
203
|
+
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
|
204
|
+
|
|
205
|
+
const submit = React.useCallback(() => {
|
|
206
|
+
const trimmed = value.trim();
|
|
207
|
+
if (!trimmed || disabled) return;
|
|
208
|
+
onSubmit(trimmed);
|
|
209
|
+
}, [disabled, onSubmit, value]);
|
|
210
|
+
|
|
211
|
+
// Auto-grow up to ~6 lines.
|
|
212
|
+
React.useEffect(() => {
|
|
213
|
+
const textarea = textareaRef.current;
|
|
214
|
+
if (!textarea) return;
|
|
215
|
+
textarea.style.height = "auto";
|
|
216
|
+
textarea.style.height = `${Math.min(textarea.scrollHeight, 144)}px`;
|
|
217
|
+
}, [value]);
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<form
|
|
221
|
+
className={cn(
|
|
222
|
+
"flex items-end gap-2 border-border border-t bg-background p-3",
|
|
223
|
+
className,
|
|
224
|
+
)}
|
|
225
|
+
data-slot="chat-input"
|
|
226
|
+
onSubmit={(event) => {
|
|
227
|
+
event.preventDefault();
|
|
228
|
+
submit();
|
|
229
|
+
}}
|
|
230
|
+
>
|
|
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"
|
|
252
|
+
>
|
|
253
|
+
<ArrowUpIcon />
|
|
254
|
+
</Button>
|
|
255
|
+
</form>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export {
|
|
260
|
+
Chat,
|
|
261
|
+
ChatInput,
|
|
262
|
+
ChatMessage,
|
|
263
|
+
ChatMessageBubble,
|
|
264
|
+
chatMessageBubbleVariants,
|
|
265
|
+
chatMessageVariants,
|
|
266
|
+
ChatMessages,
|
|
267
|
+
ChatToolCard,
|
|
268
|
+
ChatToolCardActions,
|
|
269
|
+
ChatToolCardHeader,
|
|
270
|
+
ChatTypingIndicator,
|
|
271
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { SearchIcon } from "lucide-react";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils/css";
|
|
7
|
+
import { Input } from "./input";
|
|
8
|
+
import { PaginationControls } from "./pagination-controls";
|
|
9
|
+
import {
|
|
10
|
+
Table,
|
|
11
|
+
TableBody,
|
|
12
|
+
TableCell,
|
|
13
|
+
TableHead,
|
|
14
|
+
TableHeader,
|
|
15
|
+
TableRow,
|
|
16
|
+
} from "./table";
|
|
17
|
+
|
|
18
|
+
export interface DataTablePagedColumn {
|
|
19
|
+
key: string;
|
|
20
|
+
label?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DataTablePagedProps {
|
|
24
|
+
/** Columns to render. Pass `label` to override the header text. */
|
|
25
|
+
columns: DataTablePagedColumn[];
|
|
26
|
+
/** Row objects; values are rendered with `String(...)` unless a ReactNode. */
|
|
27
|
+
rows: Record<string, unknown>[];
|
|
28
|
+
pageSize?: number;
|
|
29
|
+
/** Show the client-side search box (filters across all columns). */
|
|
30
|
+
searchable?: boolean;
|
|
31
|
+
searchPlaceholder?: string;
|
|
32
|
+
emptyLabel?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Total row count when `rows` is only one page of a larger server-side set —
|
|
35
|
+
* shown as "n of total" in the footer.
|
|
36
|
+
*/
|
|
37
|
+
totalCount?: number;
|
|
38
|
+
className?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatCell(value: unknown): React.ReactNode {
|
|
42
|
+
if (value === null || value === undefined || value === "") return "—";
|
|
43
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
44
|
+
if (React.isValidElement(value)) return value;
|
|
45
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
46
|
+
return String(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Client-side paginated (and optionally searchable) data table. Designed for
|
|
51
|
+
* result sets a chat agent or API returns: pass plain row objects and column
|
|
52
|
+
* keys, get a themed table with pagination that never renders huge lists.
|
|
53
|
+
*/
|
|
54
|
+
function DataTablePaged({
|
|
55
|
+
className,
|
|
56
|
+
columns,
|
|
57
|
+
emptyLabel = "No results",
|
|
58
|
+
pageSize = 10,
|
|
59
|
+
rows,
|
|
60
|
+
searchable = true,
|
|
61
|
+
searchPlaceholder = "Filter results…",
|
|
62
|
+
totalCount,
|
|
63
|
+
}: DataTablePagedProps) {
|
|
64
|
+
const [page, setPage] = React.useState(1);
|
|
65
|
+
const [query, setQuery] = React.useState("");
|
|
66
|
+
|
|
67
|
+
const filteredRows = React.useMemo(() => {
|
|
68
|
+
const trimmed = query.trim().toLowerCase();
|
|
69
|
+
if (!trimmed) return rows;
|
|
70
|
+
return rows.filter((row) =>
|
|
71
|
+
columns.some((column) =>
|
|
72
|
+
String(row[column.key] ?? "")
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.includes(trimmed),
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
}, [columns, query, rows]);
|
|
78
|
+
|
|
79
|
+
const pageCount = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
|
80
|
+
const safePage = Math.min(page, pageCount);
|
|
81
|
+
const pageRows = filteredRows.slice((safePage - 1) * pageSize, safePage * pageSize);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className={cn("flex flex-col gap-2", className)} data-slot="data-table-paged">
|
|
85
|
+
{searchable && rows.length > pageSize && (
|
|
86
|
+
<div className="relative">
|
|
87
|
+
<SearchIcon className="-translate-y-1/2 absolute top-1/2 left-2.5 size-4 text-muted-foreground" />
|
|
88
|
+
<Input
|
|
89
|
+
className="h-8 pl-8 text-sm"
|
|
90
|
+
onChange={(event) => {
|
|
91
|
+
setQuery(event.target.value);
|
|
92
|
+
setPage(1);
|
|
93
|
+
}}
|
|
94
|
+
placeholder={searchPlaceholder}
|
|
95
|
+
value={query}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
<div className="overflow-x-auto rounded-lg border border-border">
|
|
101
|
+
<Table>
|
|
102
|
+
<TableHeader>
|
|
103
|
+
<TableRow>
|
|
104
|
+
{columns.map((column) => (
|
|
105
|
+
<TableHead className="whitespace-nowrap" key={column.key}>
|
|
106
|
+
{column.label ?? column.key.replaceAll("_", " ")}
|
|
107
|
+
</TableHead>
|
|
108
|
+
))}
|
|
109
|
+
</TableRow>
|
|
110
|
+
</TableHeader>
|
|
111
|
+
<TableBody>
|
|
112
|
+
{pageRows.length === 0 ? (
|
|
113
|
+
<TableRow>
|
|
114
|
+
<TableCell
|
|
115
|
+
className="py-6 text-center text-muted-foreground"
|
|
116
|
+
colSpan={columns.length}
|
|
117
|
+
>
|
|
118
|
+
{emptyLabel}
|
|
119
|
+
</TableCell>
|
|
120
|
+
</TableRow>
|
|
121
|
+
) : (
|
|
122
|
+
pageRows.map((row, rowIndex) => (
|
|
123
|
+
<TableRow key={`${safePage}-${rowIndex}`}>
|
|
124
|
+
{columns.map((column) => (
|
|
125
|
+
<TableCell className="max-w-56 truncate" key={column.key}>
|
|
126
|
+
{formatCell(row[column.key])}
|
|
127
|
+
</TableCell>
|
|
128
|
+
))}
|
|
129
|
+
</TableRow>
|
|
130
|
+
))
|
|
131
|
+
)}
|
|
132
|
+
</TableBody>
|
|
133
|
+
</Table>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
137
|
+
<span className="text-muted-foreground text-xs">
|
|
138
|
+
{filteredRows.length}
|
|
139
|
+
{typeof totalCount === "number" && totalCount > rows.length
|
|
140
|
+
? ` of ${totalCount}`
|
|
141
|
+
: ""}{" "}
|
|
142
|
+
rows
|
|
143
|
+
</span>
|
|
144
|
+
<PaginationControls
|
|
145
|
+
page={safePage}
|
|
146
|
+
pageCount={pageCount}
|
|
147
|
+
renderPageLink={(nextPage) => (
|
|
148
|
+
// biome-ignore lint/a11y/useValidAnchor: client-side pagination
|
|
149
|
+
<a
|
|
150
|
+
href="#"
|
|
151
|
+
onClick={(event) => {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
setPage(nextPage);
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
)}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export { DataTablePaged };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
import type * as React from "react";
|
|
5
|
+
|
|
6
|
+
import { cn } from "../../lib/utils/css";
|
|
7
|
+
import { Button, type ButtonProps } from "./button";
|
|
8
|
+
|
|
9
|
+
const fabVariants = cva(
|
|
10
|
+
"fixed z-50 rounded-full shadow-lg transition-transform duration-150 hover:scale-105 active:scale-95 before:rounded-full",
|
|
11
|
+
{
|
|
12
|
+
defaultVariants: {
|
|
13
|
+
position: "bottom-right",
|
|
14
|
+
},
|
|
15
|
+
variants: {
|
|
16
|
+
position: {
|
|
17
|
+
"bottom-right": "right-4 bottom-4 sm:right-6 sm:bottom-6",
|
|
18
|
+
"bottom-left": "bottom-4 left-4 sm:bottom-6 sm:left-6",
|
|
19
|
+
"top-right": "top-4 right-4 sm:top-6 sm:right-6",
|
|
20
|
+
"top-left": "top-4 left-4 sm:top-6 sm:left-6",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export interface FabProps extends ButtonProps {
|
|
27
|
+
position?: VariantProps<typeof fabVariants>["position"];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Floating action button — a fixed, round icon button used to summon overlays
|
|
32
|
+
* such as chat widgets. Composes `Button`, so all button variants/sizes apply.
|
|
33
|
+
*/
|
|
34
|
+
function Fab({ className, position, size = "icon-xl", ...props }: FabProps) {
|
|
35
|
+
return (
|
|
36
|
+
<Button
|
|
37
|
+
className={cn(fabVariants({ className, position }))}
|
|
38
|
+
data-slot="fab"
|
|
39
|
+
size={size}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { Fab, fabVariants };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type * as React from "react";
|
|
4
|
+
import ReactMarkdown from "react-markdown";
|
|
5
|
+
import remarkGfm from "remark-gfm";
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../lib/utils/css";
|
|
8
|
+
|
|
9
|
+
export interface MarkdownProps {
|
|
10
|
+
/** The markdown source to render. */
|
|
11
|
+
children: string;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Markdown renderer for assistant/chat content (GitHub-flavored markdown).
|
|
17
|
+
* Raw HTML in the source is NOT rendered — output is limited to markdown
|
|
18
|
+
* elements, which keeps untrusted model output safe to display.
|
|
19
|
+
*/
|
|
20
|
+
function Markdown({ children, className }: MarkdownProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className={cn(
|
|
24
|
+
"space-y-2 break-words text-foreground text-sm leading-relaxed",
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
data-slot="markdown"
|
|
28
|
+
>
|
|
29
|
+
<ReactMarkdown
|
|
30
|
+
components={{
|
|
31
|
+
a: ({ node: _node, ...props }) => (
|
|
32
|
+
<a
|
|
33
|
+
className="font-medium text-primary underline underline-offset-2 hover:opacity-80"
|
|
34
|
+
rel="noreferrer noopener"
|
|
35
|
+
target="_blank"
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
),
|
|
39
|
+
blockquote: ({ node: _node, ...props }) => (
|
|
40
|
+
<blockquote
|
|
41
|
+
className="border-border border-l-2 pl-3 text-muted-foreground italic"
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
),
|
|
45
|
+
code: ({ node: _node, className: codeClassName, ...props }) => {
|
|
46
|
+
const isBlock = /language-/.test(codeClassName ?? "");
|
|
47
|
+
return (
|
|
48
|
+
<code
|
|
49
|
+
className={cn(
|
|
50
|
+
"rounded bg-muted font-mono text-[0.85em]",
|
|
51
|
+
isBlock ? "block overflow-x-auto p-3" : "px-1 py-0.5",
|
|
52
|
+
codeClassName,
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
h1: ({ node: _node, ...props }) => (
|
|
59
|
+
<h1 className="font-semibold text-base" {...props} />
|
|
60
|
+
),
|
|
61
|
+
h2: ({ node: _node, ...props }) => (
|
|
62
|
+
<h2 className="font-semibold text-base" {...props} />
|
|
63
|
+
),
|
|
64
|
+
h3: ({ node: _node, ...props }) => (
|
|
65
|
+
<h3 className="font-semibold text-sm" {...props} />
|
|
66
|
+
),
|
|
67
|
+
hr: ({ node: _node, ...props }) => (
|
|
68
|
+
<hr className="border-border" {...props} />
|
|
69
|
+
),
|
|
70
|
+
li: ({ node: _node, ...props }) => <li className="my-0.5" {...props} />,
|
|
71
|
+
ol: ({ node: _node, ...props }) => (
|
|
72
|
+
<ol className="list-decimal space-y-1 pl-5" {...props} />
|
|
73
|
+
),
|
|
74
|
+
pre: ({ node: _node, ...props }) => (
|
|
75
|
+
<pre
|
|
76
|
+
className="overflow-x-auto rounded-lg bg-muted p-0 text-[0.85em]"
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
),
|
|
80
|
+
table: ({ node: _node, ...props }) => (
|
|
81
|
+
<div className="overflow-x-auto rounded-lg border border-border">
|
|
82
|
+
<table className="w-full text-left text-sm" {...props} />
|
|
83
|
+
</div>
|
|
84
|
+
),
|
|
85
|
+
td: ({ node: _node, ...props }) => (
|
|
86
|
+
<td className="border-border border-t px-3 py-1.5 align-top" {...props} />
|
|
87
|
+
),
|
|
88
|
+
th: ({ node: _node, ...props }) => (
|
|
89
|
+
<th
|
|
90
|
+
className="bg-muted px-3 py-1.5 font-medium text-muted-foreground"
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
),
|
|
94
|
+
ul: ({ node: _node, ...props }) => (
|
|
95
|
+
<ul className="list-disc space-y-1 pl-5" {...props} />
|
|
96
|
+
),
|
|
97
|
+
}}
|
|
98
|
+
remarkPlugins={[remarkGfm]}
|
|
99
|
+
skipHtml
|
|
100
|
+
>
|
|
101
|
+
{children}
|
|
102
|
+
</ReactMarkdown>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { Markdown };
|
package/src/index.ts
CHANGED
|
@@ -19,16 +19,19 @@ export * from "./components/ui/calendar";
|
|
|
19
19
|
export * from "./components/ui/card";
|
|
20
20
|
export * from "./components/ui/carousel";
|
|
21
21
|
export * from "./components/ui/chart";
|
|
22
|
+
export * from "./components/ui/chat";
|
|
22
23
|
export * from "./components/ui/checkbox";
|
|
23
24
|
export * from "./components/ui/checkbox-group";
|
|
24
25
|
export * from "./components/ui/collapsible";
|
|
25
26
|
export * from "./components/ui/combobox";
|
|
26
27
|
export * from "./components/ui/command";
|
|
27
28
|
export * from "./components/ui/credit-card";
|
|
29
|
+
export * from "./components/ui/data-table-paged";
|
|
28
30
|
export * from "./components/ui/drawer";
|
|
29
31
|
export * from "./components/ui/context-menu";
|
|
30
32
|
export * from "./components/ui/dialog";
|
|
31
33
|
export * from "./components/ui/empty";
|
|
34
|
+
export * from "./components/ui/fab";
|
|
32
35
|
export * from "./components/ui/featured-icon";
|
|
33
36
|
export * from "./components/ui/field";
|
|
34
37
|
export * from "./components/ui/fieldset";
|
|
@@ -43,6 +46,7 @@ export * from "./components/ui/input-otp";
|
|
|
43
46
|
export * from "./components/ui/item";
|
|
44
47
|
export * from "./components/ui/kbd";
|
|
45
48
|
export * from "./components/ui/label";
|
|
49
|
+
export * from "./components/ui/markdown";
|
|
46
50
|
export * from "./components/ui/menu";
|
|
47
51
|
export * from "./components/ui/menubar";
|
|
48
52
|
export * from "./components/ui/meter";
|