@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.
@@ -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: "h-4 w-4" }) }))] }));
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" | "full" | "compact" | null | undefined;
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 timeout = this.options.timeout || 30000;
220
+ const timeoutMs = this.options.timeout ?? 10 * 60 * 1000;
221
221
  const controller = new AbortController();
222
- const timeoutId = setTimeout(() => controller.abort(), timeout);
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 ${timeout}ms`);
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.18",
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.15",
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",
@@ -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 {