@townco/ui 0.1.18 → 0.1.19
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/gui/components/Button.js +1 -1
- package/dist/gui/components/ChatEmptyState.d.ts +18 -0
- package/dist/gui/components/ChatEmptyState.js +22 -0
- package/dist/gui/components/ChatInput.js +29 -1
- package/dist/gui/components/ChatLayout.js +2 -2
- package/dist/gui/components/Message.d.ts +1 -1
- package/dist/gui/components/index.d.ts +1 -0
- package/dist/gui/components/index.js +1 -0
- package/dist/sdk/transports/http.js +3 -3
- package/package.json +2 -2
- package/src/styles/global.css +64 -0
|
@@ -3,7 +3,7 @@ import { Slot } from "@radix-ui/react-slot";
|
|
|
3
3
|
import { cva } from "class-variance-authority";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
|
-
const buttonVariants = cva("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", {
|
|
6
|
+
const buttonVariants = cva("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer", {
|
|
7
7
|
variants: {
|
|
8
8
|
variant: {
|
|
9
9
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface ChatEmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
/** Agent name/title */
|
|
4
|
+
title: string;
|
|
5
|
+
/** Agent description */
|
|
6
|
+
description: string;
|
|
7
|
+
/** Optional guide link URL */
|
|
8
|
+
guideUrl?: string;
|
|
9
|
+
/** Optional guide link text */
|
|
10
|
+
guideText?: string;
|
|
11
|
+
/** Suggested prompts */
|
|
12
|
+
suggestedPrompts?: string[];
|
|
13
|
+
/** Callback when a prompt is clicked */
|
|
14
|
+
onPromptClick?: (prompt: string) => void;
|
|
15
|
+
/** Callback when guide is clicked */
|
|
16
|
+
onGuideClick?: () => void;
|
|
17
|
+
}
|
|
18
|
+
export declare const ChatEmptyState: React.ForwardRefExoticComponent<ChatEmptyStateProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronRight } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/utils.js";
|
|
5
|
+
export const ChatEmptyState = React.forwardRef(({ title, description, guideUrl, guideText = "Guide", suggestedPrompts = [], onPromptClick, onGuideClick, className, ...props }, ref) => {
|
|
6
|
+
const handlePromptClick = (prompt) => {
|
|
7
|
+
onPromptClick?.(prompt);
|
|
8
|
+
};
|
|
9
|
+
const handleGuideClick = () => {
|
|
10
|
+
if (guideUrl) {
|
|
11
|
+
window.open(guideUrl, "_blank", "noopener,noreferrer");
|
|
12
|
+
}
|
|
13
|
+
onGuideClick?.();
|
|
14
|
+
};
|
|
15
|
+
// Group prompts into rows of 2
|
|
16
|
+
const promptRows = [];
|
|
17
|
+
for (let i = 0; i < suggestedPrompts.length; i += 2) {
|
|
18
|
+
promptRows.push(suggestedPrompts.slice(i, i + 2));
|
|
19
|
+
}
|
|
20
|
+
return (_jsxs("div", { ref: ref, className: cn("flex flex-col items-start gap-6", className), ...props, children: [_jsx("h3", { className: "text-heading-3 text-text-primary hidden lg:block", children: title }), _jsx("p", { className: "text-subheading text-text-secondary max-w-prose", children: description }), (guideUrl || onGuideClick) && (_jsxs("button", { type: "button", onClick: handleGuideClick, className: "flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-accent transition-colors", children: [_jsx("span", { className: "text-sm font-medium leading-normal text-text-primary", children: guideText }), _jsx(ChevronRight, { className: "size-4 text-text-primary" })] })), suggestedPrompts.length > 0 && (_jsxs("div", { className: "flex flex-col gap-3 w-full max-w-prompt-container", children: [_jsx("p", { className: "text-label text-text-tertiary", children: "Suggested Prompts" }), _jsx("div", { className: "flex flex-col gap-2.5", children: promptRows.map((row) => (_jsx("div", { className: "flex gap-2.5 items-center", children: row.map((prompt) => (_jsx("button", { type: "button", onClick: () => handlePromptClick(prompt), className: "flex-1 flex items-start gap-2 p-3 bg-secondary hover:bg-secondary/80 rounded-2xl transition-colors min-w-0", children: _jsx("span", { className: "text-base font-normal leading-normal text-text-tertiary truncate", children: prompt }) }, prompt))) }, row.join("-")))) })] }))] }));
|
|
21
|
+
});
|
|
22
|
+
ChatEmptyState.displayName = "ChatEmptyState";
|
|
@@ -48,6 +48,34 @@ const ChatInputRoot = React.forwardRef(({ client, value: valueProp, onChange: on
|
|
|
48
48
|
}, 0);
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
|
+
// Handle clicks on the composer to focus the input field
|
|
52
|
+
const handleFormClick = (e) => {
|
|
53
|
+
// Don't focus if clicking on interactive elements (buttons, inputs, etc.)
|
|
54
|
+
const target = e.target;
|
|
55
|
+
const isInteractive = target.tagName === "BUTTON" ||
|
|
56
|
+
target.tagName === "INPUT" ||
|
|
57
|
+
target.tagName === "TEXTAREA" ||
|
|
58
|
+
target.closest("button");
|
|
59
|
+
if (!isInteractive && textareaRef.current) {
|
|
60
|
+
textareaRef.current.focus();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
// Handle keyboard events for accessibility
|
|
64
|
+
const handleFormKeyDown = (e) => {
|
|
65
|
+
// Focus input when Space is pressed on the form (but not on interactive elements)
|
|
66
|
+
// Enter is handled by form submission, so we don't intercept it
|
|
67
|
+
const target = e.target;
|
|
68
|
+
const isInteractive = target.tagName === "BUTTON" ||
|
|
69
|
+
target.tagName === "INPUT" ||
|
|
70
|
+
target.tagName === "TEXTAREA" ||
|
|
71
|
+
target.closest("button");
|
|
72
|
+
if (!isInteractive && e.key === " ") {
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
if (textareaRef.current) {
|
|
75
|
+
textareaRef.current.focus();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
51
79
|
// Expose textarea ref to children via context
|
|
52
80
|
React.useEffect(() => {
|
|
53
81
|
const textarea = document.querySelector('textarea[name="chat-input"]');
|
|
@@ -79,7 +107,7 @@ const ChatInputRoot = React.forwardRef(({ client, value: valueProp, onChange: on
|
|
|
79
107
|
setMenuItemCount,
|
|
80
108
|
triggerMenuSelect,
|
|
81
109
|
triggerCounter,
|
|
82
|
-
}, children: _jsx("form", { ref: ref, onSubmit: handleSubmit, className: cn("relative w-full divide-y rounded-xl border bg-background shadow-md", className), ...props, children: children }) }));
|
|
110
|
+
}, children: _jsx("form", { ref: ref, onSubmit: handleSubmit, onClick: handleFormClick, onKeyDown: handleFormKeyDown, className: cn("relative w-full divide-y rounded-xl border bg-background shadow-md", className), ...props, children: children }) }));
|
|
83
111
|
});
|
|
84
112
|
ChatInputRoot.displayName = "ChatInput.Root";
|
|
85
113
|
const ChatInputField = React.forwardRef(({ asChild = false, className, onKeyDown, children, ...props }, ref) => {
|
|
@@ -72,11 +72,11 @@ const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChan
|
|
|
72
72
|
React.useEffect(() => {
|
|
73
73
|
checkScrollPosition();
|
|
74
74
|
}, [checkScrollPosition]);
|
|
75
|
-
return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsx("div", { ref: scrollContainerRef, className: cn("h-full overflow-y-auto", className), onScroll: handleScroll, ...props, children: children }), showScrollButton && (_jsx("button", { type: "button", onClick: scrollToBottom, className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 z-10", "flex items-center justify-center p-2 rounded-full", "bg-card border border-border shadow-lg", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-all duration-200 ease-in-out", "animate-in fade-in slide-in-from-bottom-2"), "aria-label": "Scroll to bottom", children: _jsx(ArrowDown, { className: "
|
|
75
|
+
return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsx("div", { ref: scrollContainerRef, className: cn("h-full overflow-y-auto", className), onScroll: handleScroll, ...props, children: _jsx("div", { className: "mx-auto max-w-chat min-h-full flex flex-col", children: children }) }), showScrollButton && (_jsx("button", { type: "button", onClick: scrollToBottom, className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 z-10", "flex items-center justify-center p-2 rounded-full", "bg-card border border-border shadow-lg", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-all duration-200 ease-in-out", "animate-in fade-in slide-in-from-bottom-2"), "aria-label": "Scroll to bottom", children: _jsx(ArrowDown, { className: "size-4" }) }))] }));
|
|
76
76
|
});
|
|
77
77
|
ChatLayoutMessages.displayName = "ChatLayout.Messages";
|
|
78
78
|
const ChatLayoutFooter = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
79
|
-
return (_jsx("div", { ref: ref, className: cn("bg-linear-to-t from-background to-transparent px-4 pb-4", className), ...props, children: children }));
|
|
79
|
+
return (_jsx("div", { ref: ref, className: cn("bg-linear-to-t from-background to-transparent px-4 pb-4", className), ...props, children: _jsx("div", { className: "mx-auto max-w-chat", children: children }) }));
|
|
80
80
|
});
|
|
81
81
|
ChatLayoutFooter.displayName = "ChatLayout.Footer";
|
|
82
82
|
const ChatLayoutSidebar = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
@@ -7,7 +7,7 @@ import type { DisplayMessage } from "./MessageList.js";
|
|
|
7
7
|
*/
|
|
8
8
|
declare const messageVariants: (props?: ({
|
|
9
9
|
role?: "user" | "assistant" | "system" | null | undefined;
|
|
10
|
-
layout?: "default" | "
|
|
10
|
+
layout?: "default" | "compact" | "full" | null | undefined;
|
|
11
11
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
12
12
|
export interface MessageProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof messageVariants> {
|
|
13
13
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { toast } from "sonner";
|
|
2
2
|
export { Button, type ButtonProps, buttonVariants } from "./Button.js";
|
|
3
3
|
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "./Card.js";
|
|
4
|
+
export { ChatEmptyState, type ChatEmptyStateProps } from "./ChatEmptyState.js";
|
|
4
5
|
export type { ConnectionStatus } from "./ChatHeader.js";
|
|
5
6
|
export * as ChatHeader from "./ChatHeader.js";
|
|
6
7
|
export { Actions as ChatInputActions, Attachment as ChatInputAttachment, type ChatInputActionsProps, type ChatInputAttachmentProps, type ChatInputCommandMenuProps, type ChatInputFieldProps, type ChatInputRootProps, type ChatInputSubmitProps, type ChatInputToolbarProps, type ChatInputVoiceInputProps, CommandMenu as ChatInputCommandMenu, type CommandMenuItem, Field as ChatInputField, Root as ChatInputRoot, Submit as ChatInputSubmit, Toolbar as ChatInputToolbar, VoiceInput as ChatInputVoiceInput, } from "./ChatInput.js";
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export { toast } from "sonner";
|
|
3
3
|
export { Button, buttonVariants } from "./Button.js";
|
|
4
4
|
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "./Card.js";
|
|
5
|
+
export { ChatEmptyState } from "./ChatEmptyState.js";
|
|
5
6
|
export * as ChatHeader from "./ChatHeader.js";
|
|
6
7
|
// Chat components - composable primitives
|
|
7
8
|
export { Actions as ChatInputActions, Attachment as ChatInputAttachment, CommandMenu as ChatInputCommandMenu, Field as ChatInputField, Root as ChatInputRoot, Submit as ChatInputSubmit, Toolbar as ChatInputToolbar, VoiceInput as ChatInputVoiceInput, } from "./ChatInput.js";
|
|
@@ -217,9 +217,9 @@ export class HttpTransport {
|
|
|
217
217
|
"Content-Type": "application/json",
|
|
218
218
|
...this.options.headers,
|
|
219
219
|
};
|
|
220
|
-
const
|
|
220
|
+
const timeoutMs = this.options.timeout ?? 10 * 60 * 1000;
|
|
221
221
|
const controller = new AbortController();
|
|
222
|
-
const timeoutId = setTimeout(() => controller.abort(),
|
|
222
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
223
223
|
try {
|
|
224
224
|
const response = await fetch(`${this.options.baseUrl}/rpc`, {
|
|
225
225
|
method: "POST",
|
|
@@ -242,7 +242,7 @@ export class HttpTransport {
|
|
|
242
242
|
catch (error) {
|
|
243
243
|
clearTimeout(timeoutId);
|
|
244
244
|
if (error instanceof Error && error.name === "AbortError") {
|
|
245
|
-
throw new Error(`Request timeout after ${
|
|
245
|
+
throw new Error(`Request timeout after ${timeoutMs}ms`);
|
|
246
246
|
}
|
|
247
247
|
throw error;
|
|
248
248
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@tailwindcss/postcss": "^4.1.17",
|
|
64
|
-
"@townco/tsconfig": "0.1.
|
|
64
|
+
"@townco/tsconfig": "0.1.16",
|
|
65
65
|
"@types/node": "^24.10.0",
|
|
66
66
|
"@types/react": "^19.2.2",
|
|
67
67
|
"ink": "^6.4.0",
|
package/src/styles/global.css
CHANGED
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
--input: 214.3 31.8% 91.4%;
|
|
24
24
|
--ring: 222.2 84% 4.9%;
|
|
25
25
|
--radius: 0.5rem;
|
|
26
|
+
|
|
27
|
+
/* Text colors from design system */
|
|
28
|
+
--text-primary: 0 0% 9.02%; /* #171717 */
|
|
29
|
+
--text-secondary: 0 0% 32.16%; /* #525252 */
|
|
30
|
+
--text-tertiary: 0 0% 45.1%; /* #737373 */
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
.dark {
|
|
@@ -45,9 +50,15 @@
|
|
|
45
50
|
--border: 217.2 32.6% 17.5%;
|
|
46
51
|
--input: 217.2 32.6% 17.5%;
|
|
47
52
|
--ring: 212.7 26.8% 83.9%;
|
|
53
|
+
|
|
54
|
+
/* Text colors from design system (dark mode) */
|
|
55
|
+
--text-primary: 0 0% 98%; /* Light text for dark mode */
|
|
56
|
+
--text-secondary: 0 0% 70%; /* Muted light text */
|
|
57
|
+
--text-tertiary: 0 0% 55%; /* More muted text */
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
@theme {
|
|
61
|
+
/* Colors */
|
|
51
62
|
--color-background: hsl(var(--background));
|
|
52
63
|
--color-foreground: hsl(var(--foreground));
|
|
53
64
|
--color-card: hsl(var(--card));
|
|
@@ -67,8 +78,34 @@
|
|
|
67
78
|
--color-border: hsl(var(--border));
|
|
68
79
|
--color-input: hsl(var(--input));
|
|
69
80
|
--color-ring: hsl(var(--ring));
|
|
81
|
+
|
|
82
|
+
/* Text colors from design system */
|
|
83
|
+
--color-text-primary: hsl(var(--text-primary));
|
|
84
|
+
--color-text-secondary: hsl(var(--text-secondary));
|
|
85
|
+
--color-text-tertiary: hsl(var(--text-tertiary));
|
|
86
|
+
|
|
87
|
+
/* Shadows */
|
|
70
88
|
--shadow-sm: 0 8px 24px -16px rgba(0, 0, 0, 0.04), 0 4px 16px 0 rgba(0, 0, 0, 0.04);
|
|
71
89
|
--shadow-md: 0 8px 24px -16px rgba(0, 0, 0, 0.04), 0 4px 16px 0 rgba(0, 0, 0, 0.04);
|
|
90
|
+
|
|
91
|
+
/* Layout widths - max-width utilities */
|
|
92
|
+
--max-width-chat: 720px;
|
|
93
|
+
--max-width-prose: 477px;
|
|
94
|
+
--max-width-prompt-container: 393px;
|
|
95
|
+
|
|
96
|
+
/* Typography - Letter spacing */
|
|
97
|
+
--letter-spacing-tight: -0.48px;
|
|
98
|
+
--letter-spacing-uppercase: 1.12px;
|
|
99
|
+
|
|
100
|
+
/* Typography - Font sizes with line heights */
|
|
101
|
+
--font-size-heading-3: 1.5rem; /* 24px */
|
|
102
|
+
--line-height-heading-3: 1.2; /* 120% */
|
|
103
|
+
|
|
104
|
+
--font-size-subheading: 1.25rem; /* 20px */
|
|
105
|
+
--line-height-subheading: 1.5; /* 150% */
|
|
106
|
+
|
|
107
|
+
--font-size-label: 0.875rem; /* 14px */
|
|
108
|
+
--line-height-label: 1.5; /* 150% */
|
|
72
109
|
}
|
|
73
110
|
|
|
74
111
|
@layer base {
|
|
@@ -79,6 +116,33 @@
|
|
|
79
116
|
background-color: hsl(var(--background));
|
|
80
117
|
color: hsl(var(--foreground));
|
|
81
118
|
}
|
|
119
|
+
button {
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@layer utilities {
|
|
125
|
+
/* Typography utilities following design system */
|
|
126
|
+
.text-heading-3 {
|
|
127
|
+
font-size: var(--font-size-heading-3);
|
|
128
|
+
line-height: var(--line-height-heading-3);
|
|
129
|
+
font-weight: 600;
|
|
130
|
+
letter-spacing: var(--letter-spacing-tight);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.text-subheading {
|
|
134
|
+
font-size: var(--font-size-subheading);
|
|
135
|
+
line-height: var(--line-height-subheading);
|
|
136
|
+
font-weight: 400;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.text-label {
|
|
140
|
+
font-size: var(--font-size-label);
|
|
141
|
+
line-height: var(--line-height-label);
|
|
142
|
+
font-weight: 500;
|
|
143
|
+
letter-spacing: var(--letter-spacing-uppercase);
|
|
144
|
+
text-transform: uppercase;
|
|
145
|
+
}
|
|
82
146
|
}
|
|
83
147
|
|
|
84
148
|
@keyframes spin {
|