devnonla-ui 0.3.0 → 0.4.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.
Files changed (47) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -1
  3. package/src/app/App.tsx +4 -4
  4. package/src/button/Button.tsx +1 -1
  5. package/src/button/ButtonCopy.tsx +11 -21
  6. package/src/calendar/Calendar.tsx +1 -1
  7. package/src/chat/AgentPanel.tsx +3 -3
  8. package/src/chat/index.ts +35 -51
  9. package/src/chat/message-ui/ChatError.tsx +2 -4
  10. package/src/chat/message-ui/ChatInput.tsx +5 -9
  11. package/src/chat/message-ui/ChatThinking.tsx +2 -3
  12. package/src/chat/message-ui/ChatUserMessage.tsx +4 -5
  13. package/src/chat/message-ui/ChatWelcome.tsx +3 -8
  14. package/src/chat/message-ui/MermaidBlock.tsx +7 -47
  15. package/src/chat/tool-ui/BackgroundTaskToolUI.tsx +8 -11
  16. package/src/chat/tool-ui/BackgroundTasksBar.tsx +13 -14
  17. package/src/chat/tool-ui/CallAgentToolUI.tsx +10 -4
  18. package/src/chat/tool-ui/ChatToolCall.tsx +11 -17
  19. package/src/chat/tool-ui/GetCurrentTimeToolUI.tsx +2 -2
  20. package/src/chat/tool-ui/ReadSkillToolUI.tsx +3 -3
  21. package/src/chat/tool-ui/RunJsToolUI.tsx +2 -2
  22. package/src/chat/tool-ui/ToolUiTrailing.tsx +3 -7
  23. package/src/chat/tool-ui/WebFetchToolUI.tsx +22 -13
  24. package/src/checkbox/Checkbox.tsx +1 -1
  25. package/src/codeblock/CodeBlock.tsx +17 -19
  26. package/src/colorpicker/ColorPicker.tsx +7 -7
  27. package/src/datepicker/DatePicker.tsx +3 -3
  28. package/src/desktop/DesktopHeader.tsx +15 -20
  29. package/src/desktop/DesktopIcon.tsx +1 -1
  30. package/src/desktop/DesktopWindow.tsx +54 -30
  31. package/src/form/Form.tsx +1 -1
  32. package/src/form/FormItem.tsx +2 -2
  33. package/src/form/index.ts +23 -24
  34. package/src/index.ts +167 -199
  35. package/src/input/Input.tsx +14 -12
  36. package/src/input/SearchInput.tsx +5 -2
  37. package/src/lib/sizes.ts +1 -1
  38. package/src/modal/Modal.tsx +3 -0
  39. package/src/pagination/Pagination.tsx +528 -51
  40. package/src/scroll/OverlayScroll.tsx +58 -37
  41. package/src/select/Select.tsx +2 -2
  42. package/src/spin/Spin.tsx +14 -17
  43. package/src/splitter/Splitter.tsx +3 -3
  44. package/src/styles.css +13 -3
  45. package/src/switch/Switch.tsx +1 -1
  46. package/src/table/Table.tsx +43 -18
  47. package/src/timepicker/TimePicker.tsx +2 -2
package/README.md CHANGED
@@ -108,14 +108,14 @@ import { DesktopStage, DesktopHeader, MeadowDesktop, DesktopWindow, MeadowShell,
108
108
 
109
109
  <DesktopStage>
110
110
  <MeadowDesktop>{icons}</MeadowDesktop>
111
- <DesktopHeader logo={logo} leading={nav} trailing={apps} profile={avatar} />
112
- <DesktopWindow title="Tools" expanded={false} onClose={close} onToggleExpand={toggle}>
111
+ <DesktopHeader left={nav} right={apps} />
112
+ <DesktopWindow title="Tools" left={nav} right={actions} expanded={false} onClose={close} onToggleExpand={toggle}>
113
113
  {children}
114
114
  </DesktopWindow>
115
115
  </DesktopStage>
116
116
  ```
117
117
 
118
- `MeadowShell` is the login/setup backdrop. `Menu` is the glass picker (trigger + items + hover action) used by AgentsMenu. Wallpaper defaults to the bundled meadow; pass `src` on `MeadowWallpaper` to swap.
118
+ `MeadowShell` is the login/setup backdrop. `DesktopWindow` chrome is inline: traffic lights, `left`, a flex middle (title — double-click to expand), then `right`. A child can fill the same slots with `WindowHeader`. `Menu` is the glass picker (trigger + items + hover action) used by AgentsMenu. Wallpaper defaults to the bundled meadow; pass `src` on `MeadowWallpaper` to swap.
119
119
 
120
120
  # Chat
121
121
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devnonla-ui",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "NonlaUI — React controls for Nonla Agents",
6
6
  "author": "devlangla <devlangla0x01@gmail.com>",
@@ -49,6 +49,7 @@
49
49
  "clsx": "^2.1.1",
50
50
  "date-fns": "^4.4.0",
51
51
  "highlight.js": "^11.12.0",
52
+ "lucide-react": "^1.45.0",
52
53
  "mermaid": "^11.14.0",
53
54
  "react-day-picker": "^10.0.1",
54
55
  "react-markdown": "^10.1.0",
package/src/app/App.tsx CHANGED
@@ -1,10 +1,10 @@
1
- import { type ReactNode, useLayoutEffect, useMemo, useState } from "react";
2
1
  import * as TooltipPrimitive from "@radix-ui/react-tooltip";
3
- import { message, MessageHolder } from "../message/message";
2
+ import { type ReactNode, useLayoutEffect, useMemo, useState } from "react";
3
+ import { type ControlSize, ControlSizeContext, normalizeSize } from "../lib/sizes";
4
+ import { MessageHolder, message } from "../message/message";
4
5
  import { ConfirmHolder, Modal } from "../modal/Modal";
5
- import { ControlSizeContext, normalizeSize, type ControlSize } from "../lib/sizes";
6
6
  import { applyNonlaTheme, type NonlaThemeConfig } from "../theme";
7
- import { AppContext, useAppConfig, usePopupContainer, useToken, type NonlaAppConfig } from "./context";
7
+ import { AppContext, type NonlaAppConfig, useAppConfig, usePopupContainer, useToken } from "./context";
8
8
 
9
9
  export type { NonlaAppConfig };
10
10
  export { useAppConfig, usePopupContainer, useToken };
@@ -1,4 +1,4 @@
1
- import { type AnchorHTMLAttributes, type ButtonHTMLAttributes, type CSSProperties, type ForwardRefExoticComponent, type MouseEvent, type ReactNode, type RefAttributes, createContext, forwardRef, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
1
+ import { type AnchorHTMLAttributes, type ButtonHTMLAttributes, type CSSProperties, createContext, type ForwardRefExoticComponent, forwardRef, type MouseEvent, type ReactNode, type RefAttributes, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import { cn } from "../lib/cn";
3
3
  import type { ControlSize } from "../lib/sizes";
4
4
  import { controlHeightVar, controlRadiusVar, getSizeTokens, useControlSize } from "../lib/sizes";
@@ -1,32 +1,21 @@
1
+ import { Check, Copy } from "lucide-react";
1
2
  import { type ComponentProps, useEffect, useState } from "react";
2
3
  import { cn } from "../lib/cn";
4
+ import { type ControlSize, getSizeTokens, useControlSize } from "../lib/sizes";
3
5
 
4
- export type ButtonCopyProps = Omit<ComponentProps<"button">, "children"> & {
6
+ export type ButtonCopyProps = Omit<ComponentProps<"button">, "children" | "size"> & {
5
7
  text?: string;
6
8
  getText?: () => string;
7
9
  /** Idle tooltip / aria-label. Copied state uses "Copied". */
8
10
  label?: string;
11
+ size?: ControlSize;
9
12
  };
10
13
 
11
- function CopyIcon({ size = 14 }: { size?: number }) {
12
- return (
13
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
14
- <rect x="8" y="8" width="14" height="14" rx="2" stroke="currentColor" strokeWidth="2" />
15
- <path d="M4 16V4c0-1.1.9-2 2-2h10" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
16
- </svg>
17
- );
18
- }
19
-
20
- function CheckIcon({ size = 14 }: { size?: number }) {
21
- return (
22
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
23
- <path d="M5 13l4 4L19 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
24
- </svg>
25
- );
26
- }
27
-
28
- export function ButtonCopy({ text, getText, label = "Copy", className, onClick, ...props }: ButtonCopyProps) {
14
+ export function ButtonCopy({ text, getText, label = "Copy", className, onClick, size, ...props }: ButtonCopyProps) {
29
15
  const [copied, setCopied] = useState(false);
16
+ const tok = getSizeTokens(useControlSize(size));
17
+ const icon = tok.icon;
18
+ const box = icon + 14;
30
19
 
31
20
  useEffect(() => {
32
21
  if (!copied) return;
@@ -40,7 +29,8 @@ export function ButtonCopy({ text, getText, label = "Copy", className, onClick,
40
29
  {...props}
41
30
  title={copied ? "Copied" : label}
42
31
  aria-label={copied ? "Copied" : label}
43
- className={cn("inline-flex size-7 cursor-pointer shrink-0 items-center justify-center text-muted-foreground transition-colors duration-200 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", className)}
32
+ className={cn("inline-flex cursor-pointer shrink-0 items-center justify-center text-muted-foreground transition-colors duration-200 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", className)}
33
+ style={{ width: box, height: box }}
44
34
  onClick={async (e) => {
45
35
  onClick?.(e);
46
36
  if (e.defaultPrevented) return;
@@ -54,7 +44,7 @@ export function ButtonCopy({ text, getText, label = "Copy", className, onClick,
54
44
  }
55
45
  }}
56
46
  >
57
- {copied ? <CheckIcon /> : <CopyIcon />}
47
+ {copied ? <Check size={icon} aria-hidden /> : <Copy size={icon} aria-hidden />}
58
48
  </button>
59
49
  );
60
50
  }
@@ -1,5 +1,5 @@
1
1
  import { type CSSProperties, useEffect, useRef } from "react";
2
- import { DayPicker, getDefaultClassNames, type DayButtonProps, type DayPickerProps } from "react-day-picker";
2
+ import { type DayButtonProps, DayPicker, type DayPickerProps, getDefaultClassNames } from "react-day-picker";
3
3
  import { cn } from "../lib/cn";
4
4
 
5
5
  export type CalendarProps = DayPickerProps;
@@ -1,8 +1,9 @@
1
1
  import { type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
2
2
  import { cn } from "../lib/cn";
3
3
  import { OverlayScroll, type OverlayScrollVisibility } from "../scroll/OverlayScroll";
4
- import type { AgentMessage, AgentPanelEndpoint, AgentToolAction, AgentToolHook } from "./common/types";
4
+ import { Shimmer } from "../shimmer/Shimmer";
5
5
  import { parseBgTaskRef } from "./common/bgTasks";
6
+ import type { AgentMessage, AgentPanelEndpoint, AgentToolAction, AgentToolHook } from "./common/types";
6
7
  import { useAgentStream } from "./common/useAgentStream";
7
8
  import { formatToolName } from "./common/utils";
8
9
  import { ChatAgentMessage } from "./message-ui/ChatAgentMessage";
@@ -14,8 +15,7 @@ import { ChatUserMessage } from "./message-ui/ChatUserMessage";
14
15
  import { ChatWelcome } from "./message-ui/ChatWelcome";
15
16
  import { BackgroundTaskToolUI } from "./tool-ui/BackgroundTaskToolUI";
16
17
  import { ChatToolCall } from "./tool-ui/ChatToolCall";
17
- import { resolveToolUI, type AgentToolUI } from "./tool-ui/registry";
18
- import { Shimmer } from "../shimmer/Shimmer";
18
+ import { type AgentToolUI, resolveToolUI } from "./tool-ui/registry";
19
19
 
20
20
  export type AgentPanelProps = {
21
21
  /** POST URL, or a function that returns an SSE `Response`. */
package/src/chat/index.ts CHANGED
@@ -1,58 +1,42 @@
1
- export { ChatThinking } from "./message-ui/ChatThinking";
2
- export type { ChatThinkingProps } from "./message-ui/ChatThinking";
3
-
4
- export { ChatUserMessage } from "./message-ui/ChatUserMessage";
5
- export type { ChatUserMessageProps } from "./message-ui/ChatUserMessage";
6
-
7
- export { ChatAgentMessage } from "./message-ui/ChatAgentMessage";
1
+ export type { AgentPanelProps } from "./AgentPanel";
2
+ export { AgentPanel } from "./AgentPanel";
3
+ export type { ChatBgTask, ChatBgTaskStatus } from "./common/bgTasks";
4
+ export { formatBgElapsed, parseBgTaskRef } from "./common/bgTasks";
5
+ export type { AgentSseCallbacks, AgentSseEvent, ParseSseResult } from "./common/sse";
6
+ export { normalizeSseEvent, parseSseStream } from "./common/sse";
7
+ export type { AgentHistoryMessage, AgentMessage, AgentMessageRole, AgentPanelEndpoint, AgentStreamRequest, AgentToolAction, AgentToolCallEvent, AgentToolHook, AgentToolNameMatch, AgentToolResultEvent } from "./common/types";
8
+ export type { UseAgentStreamOptions } from "./common/useAgentStream";
9
+ export { buildAgentHistory, useAgentStream } from "./common/useAgentStream";
10
+ export { formatToolName, isCallAgentToolName, matchesToolHook, matchesToolName, parseCallAgentToolTargetId, prettyJson } from "./common/utils";
8
11
  export type { ChatAgentMessageProps } from "./message-ui/ChatAgentMessage";
9
-
10
- export { ChatError } from "./message-ui/ChatError";
12
+ export { ChatAgentMessage } from "./message-ui/ChatAgentMessage";
11
13
  export type { ChatErrorProps } from "./message-ui/ChatError";
12
-
13
- export { ChatToolCall } from "./tool-ui/ChatToolCall";
14
- export type { ChatToolCallProps } from "./tool-ui/ChatToolCall";
15
-
16
- export { resolveToolUI, builtinToolUis, matchesToolUIName } from "./tool-ui/registry";
17
- export type { AgentToolUI, AgentToolUIName } from "./tool-ui/registry";
18
- export type { ToolUIProps, ChatToolMessage } from "./tool-ui/types";
19
- export { isToolRunning } from "./tool-ui/types";
14
+ export { ChatError } from "./message-ui/ChatError";
15
+ export type { ChatInputProps } from "./message-ui/ChatInput";
16
+ export { ChatInput } from "./message-ui/ChatInput";
17
+ export type { ChatMarkdownProps, ChatMarkdownStreamState } from "./message-ui/ChatMarkdown";
18
+ export { ChatMarkdown, chatBodyClass, chatMarkdownClass, chatMarkdownComponents, createChatMarkdownComponents } from "./message-ui/ChatMarkdown";
19
+ export { ChatSpinner } from "./message-ui/ChatSpinner";
20
+ export type { ChatThinkingProps } from "./message-ui/ChatThinking";
21
+ export { ChatThinking } from "./message-ui/ChatThinking";
22
+ export type { ChatUserMessageProps } from "./message-ui/ChatUserMessage";
23
+ export { ChatUserMessage } from "./message-ui/ChatUserMessage";
24
+ export type { ChatWelcomeProps } from "./message-ui/ChatWelcome";
25
+ export { ChatWelcome } from "./message-ui/ChatWelcome";
26
+ export type { MermaidBlockProps } from "./message-ui/MermaidBlock";
27
+ export { MermaidBlock } from "./message-ui/MermaidBlock";
28
+ export type { BackgroundTasksBarProps } from "./tool-ui/BackgroundTasksBar";
29
+ export { BackgroundTasksBar } from "./tool-ui/BackgroundTasksBar";
30
+ export { BackgroundTaskToolUI } from "./tool-ui/BackgroundTaskToolUI";
20
31
  export { CallAgentToolUI } from "./tool-ui/CallAgentToolUI";
21
- export { WebFetchToolUI } from "./tool-ui/WebFetchToolUI";
32
+ export type { ChatToolCallProps } from "./tool-ui/ChatToolCall";
33
+ export { ChatToolCall } from "./tool-ui/ChatToolCall";
22
34
  export { GetCurrentTimeToolUI } from "./tool-ui/GetCurrentTimeToolUI";
23
35
  export { ReadSkillToolUI } from "./tool-ui/ReadSkillToolUI";
24
36
  export { RunJsToolUI } from "./tool-ui/RunJsToolUI";
25
- export { BackgroundTaskToolUI } from "./tool-ui/BackgroundTaskToolUI";
26
- export { BackgroundTasksBar } from "./tool-ui/BackgroundTasksBar";
37
+ export type { AgentToolUI, AgentToolUIName } from "./tool-ui/registry";
38
+ export { builtinToolUis, matchesToolUIName, resolveToolUI } from "./tool-ui/registry";
27
39
  export { ToolUiTrailing } from "./tool-ui/ToolUiTrailing";
28
- export type { BackgroundTasksBarProps } from "./tool-ui/BackgroundTasksBar";
29
-
30
- export { ChatInput } from "./message-ui/ChatInput";
31
- export type { ChatInputProps } from "./message-ui/ChatInput";
32
-
33
- export { ChatWelcome } from "./message-ui/ChatWelcome";
34
- export type { ChatWelcomeProps } from "./message-ui/ChatWelcome";
35
-
36
- export { ChatSpinner } from "./message-ui/ChatSpinner";
37
-
38
- export { ChatMarkdown, createChatMarkdownComponents, chatMarkdownComponents, chatMarkdownClass, chatBodyClass } from "./message-ui/ChatMarkdown";
39
- export type { ChatMarkdownProps, ChatMarkdownStreamState } from "./message-ui/ChatMarkdown";
40
-
41
- export { MermaidBlock } from "./message-ui/MermaidBlock";
42
- export type { MermaidBlockProps } from "./message-ui/MermaidBlock";
43
-
44
- export { AgentPanel } from "./AgentPanel";
45
- export type { AgentPanelProps } from "./AgentPanel";
46
-
47
- export { useAgentStream, buildAgentHistory } from "./common/useAgentStream";
48
- export type { UseAgentStreamOptions } from "./common/useAgentStream";
49
-
50
- export { parseSseStream, normalizeSseEvent } from "./common/sse";
51
- export type { AgentSseEvent, AgentSseCallbacks, ParseSseResult } from "./common/sse";
52
-
53
- export type { AgentMessage, AgentMessageRole, AgentHistoryMessage, AgentStreamRequest, AgentPanelEndpoint, AgentToolAction, AgentToolHook, AgentToolCallEvent, AgentToolResultEvent, AgentToolNameMatch } from "./common/types";
54
-
55
- export { formatToolName, prettyJson, isCallAgentToolName, parseCallAgentToolTargetId, matchesToolName, matchesToolHook } from "./common/utils";
56
-
57
- export { parseBgTaskRef, formatBgElapsed } from "./common/bgTasks";
58
- export type { ChatBgTask, ChatBgTaskStatus } from "./common/bgTasks";
40
+ export type { ChatToolMessage, ToolUIProps } from "./tool-ui/types";
41
+ export { isToolRunning } from "./tool-ui/types";
42
+ export { WebFetchToolUI } from "./tool-ui/WebFetchToolUI";
@@ -1,3 +1,4 @@
1
+ import { CircleX } from "lucide-react";
1
2
  import type { ReactNode } from "react";
2
3
  import { cn } from "../../lib/cn";
3
4
 
@@ -13,10 +14,7 @@ export function ChatError({ children, className }: ChatErrorProps) {
13
14
  role="alert"
14
15
  className="flex items-center gap-2 rounded-lg border border-destructive/40 bg-[color-mix(in_oklab,var(--destructive)_14%,var(--nonla-surface))] px-3 py-2 text-(length:--chat-body-size) leading-(--chat-body-leading) text-destructive"
15
16
  >
16
- <svg className="size-4 shrink-0" viewBox="0 0 16 16" fill="none" aria-hidden>
17
- <circle cx="8" cy="8" r="5.75" stroke="currentColor" strokeWidth="1.4" />
18
- <path d="M6 6l4 4M10 6l-4 4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
19
- </svg>
17
+ <CircleX className="size-4 shrink-0" aria-hidden />
20
18
  <div className="min-w-0">{children}</div>
21
19
  </div>
22
20
  </div>
@@ -1,5 +1,5 @@
1
+ import { ArrowUp, Eraser, Square } from "lucide-react";
1
2
  import { type KeyboardEvent as ReactKeyboardEvent, type ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react";
2
- import { FluentIcon } from "../../icon/FluentIcon";
3
3
  import { cn } from "../../lib/cn";
4
4
 
5
5
  export type ChatInputProps = {
@@ -128,8 +128,8 @@ export function ChatInput({ generating = false, placeholder = "Message…", disa
128
128
 
129
129
  <div className="flex items-center gap-1.5 pb-2 px-2">
130
130
  {onClear ? (
131
- <button type="button" onClick={onClear} title="New chat" aria-label="New chat" className="inline-flex size-6 shrink-0 items-center justify-center rounded-lg border-0 bg-transparent cursor-pointer hover:bg-ink-hover">
132
- <FluentIcon name="chat-add-24" size={16} />
131
+ <button type="button" onClick={onClear} title="Clear chat" aria-label="Clear chat" className="inline-flex size-6 shrink-0 items-center justify-center rounded-lg border-0 bg-transparent cursor-pointer hover:bg-ink-hover">
132
+ <Eraser size={16} aria-hidden />
133
133
  </button>
134
134
  ) : null}
135
135
  {toolbar}
@@ -137,9 +137,7 @@ export function ChatInput({ generating = false, placeholder = "Message…", disa
137
137
 
138
138
  {generating ? (
139
139
  <button type="button" onClick={onCancel} title="Stop" className="w-6 h-6 rounded-full bg-brand text-(--nonla-solid-fg) flex items-center justify-center shrink-0 cursor-pointer hover:bg-brand/90 active:scale-95 transition-all duration-100 border-0">
140
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden>
141
- <rect x="1.5" y="1.5" width="9" height="9" rx="2" fill="currentColor" />
142
- </svg>
140
+ <Square size={12} fill="currentColor" strokeWidth={0} aria-hidden />
143
141
  <span className="sr-only">Stop</span>
144
142
  </button>
145
143
  ) : (
@@ -150,9 +148,7 @@ export function ChatInput({ generating = false, placeholder = "Message…", disa
150
148
  title="Send (Enter)"
151
149
  className={cn("w-6 h-6 rounded-full flex items-center justify-center shrink-0 transition-all duration-100 border-0", canSend ? "bg-foreground text-background cursor-pointer" : "bg-border text-muted-foreground cursor-not-allowed opacity-50")}
152
150
  >
153
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden>
154
- <path d="M6 9.5V2.5M6 2.5L3 5.5M6 2.5L9 5.5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
155
- </svg>
151
+ <ArrowUp size={14} strokeWidth={2.5} aria-hidden />
156
152
  <span className="sr-only">Send</span>
157
153
  </button>
158
154
  )}
@@ -1,3 +1,4 @@
1
+ import { ChevronRight } from "lucide-react";
1
2
  import { useEffect, useLayoutEffect, useRef, useState } from "react";
2
3
  import { cn } from "../../lib/cn";
3
4
  import { Shimmer } from "../../shimmer/Shimmer";
@@ -49,9 +50,7 @@ export function ChatThinking({ thinking, duration = 0, streaming = false, classN
49
50
  >
50
51
  <summary className="cursor-pointer select-none text-sm font-medium flex items-center gap-1 py-0.5 list-none [&::-webkit-details-marker]:hidden">
51
52
  <Shimmer active={streaming} className="text-tertiary-foreground">{label}</Shimmer>
52
- <svg className="w-3 h-3 shrink-0 opacity-0 text-tertiary-foreground transition-[opacity,transform] duration-150 group-hover/thinking:opacity-100 group-open/thinking:opacity-100 group-open/thinking:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden>
53
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
54
- </svg>
53
+ <ChevronRight className="size-3 shrink-0 opacity-0 text-tertiary-foreground transition-[opacity,transform] duration-150 group-hover/thinking:opacity-100 group-open/thinking:opacity-100 group-open/thinking:rotate-90" aria-hidden />
55
54
  </summary>
56
55
 
57
56
  <div ref={bodyRef} className="pt-2 max-h-40 min-w-0 overflow-y-auto overflow-x-hidden mb-2 text-[14px] leading-normal">
@@ -1,3 +1,4 @@
1
+ import { ChevronDown } from "lucide-react";
1
2
  import { useEffect, useRef, useState } from "react";
2
3
  import { cn } from "../../lib/cn";
3
4
 
@@ -28,8 +29,8 @@ export function ChatUserMessage({ content, className }: ChatUserMessageProps) {
28
29
  return (
29
30
  <div
30
31
  className={cn(
31
- "nonla-chat-user relative isolate mt-6 mb-3 mx-4 overflow-hidden rounded-xl px-3 py-2.5",
32
- "border border-brand/40 bg-[color-mix(in_oklab,var(--brand)_14%,var(--glass))] backdrop-blur-xl backdrop-saturate-150",
32
+ "nonla-chat-user relative isolate mt-6 mb-3 mx-4 overflow-hidden rounded-xl px-3 py-1.5",
33
+ "border border-solid border-border bg-[color-mix(in_oklab,var(--nonla-ink)_7%,transparent)] backdrop-blur-xl",
33
34
  className,
34
35
  )}
35
36
  >
@@ -54,9 +55,7 @@ export function ChatUserMessage({ content, className }: ChatUserMessageProps) {
54
55
  }}
55
56
  className="flex w-full cursor-pointer items-center justify-center border-0 bg-transparent pt-1 text-muted-foreground hover:text-foreground"
56
57
  >
57
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden className={cn("transition-transform duration-200", isExpanded && "rotate-180")}>
58
- <path d="M2.5 4.5 6 8l3.5-3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
59
- </svg>
58
+ <ChevronDown size={12} className={cn("transition-transform duration-200", isExpanded && "rotate-180")} aria-hidden />
60
59
  </button>
61
60
  ) : null}
62
61
  </div>
@@ -1,4 +1,5 @@
1
1
  import type { ReactNode } from "react";
2
+ import { Button } from "../../button/Button";
2
3
  import { cn } from "../../lib/cn";
3
4
 
4
5
  export type ChatWelcomeProps = {
@@ -53,15 +54,9 @@ export function ChatWelcome({ name, description, avatar, starters = [...DEFAULT_
53
54
  {starters.length > 0 && onStarter ? (
54
55
  <div className="mt-8 flex flex-wrap items-center justify-center gap-2 max-w-130">
55
56
  {starters.map((text) => (
56
- <button
57
- key={text}
58
- type="button"
59
- disabled={disabled}
60
- onClick={() => onStarter(text)}
61
- className="px-3 py-1.5 rounded-lg border border-border bg-transparent text-[12px] font-medium text-tertiary-foreground cursor-pointer transition-colors duration-150 font-[inherit] hover:border-border hover:bg-muted/60 hover:text-foreground disabled:opacity-40 disabled:cursor-not-allowed"
62
- >
57
+ <Button key={text} type="default" disabled={disabled} onClick={() => onStarter(text)}>
63
58
  {text}
64
- </button>
59
+ </Button>
65
60
  ))}
66
61
  </div>
67
62
  ) : null}
@@ -1,3 +1,4 @@
1
+ import { Download, Maximize2, Minimize2, Moon, Sun } from "lucide-react";
1
2
  import { useCallback, useEffect, useId, useRef, useState } from "react";
2
3
  import { CodeBlock } from "../../codeblock/CodeBlock";
3
4
  import { cn } from "../../lib/cn";
@@ -12,47 +13,6 @@ type MermaidTheme = "light" | "dark";
12
13
 
13
14
  const btnClass = "flex items-center gap-1 px-2 py-1 rounded-lg border border-border text-muted-foreground text-xs cursor-pointer transition-colors hover:text-primary hover:border-primary/30 bg-transparent";
14
15
 
15
- function IconSun({ size = 14 }: { size?: number }) {
16
- return (
17
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
18
- <circle cx="12" cy="12" r="4" stroke="currentColor" strokeWidth="1.75" />
19
- <path d="M12 3v2M12 19v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M3 12h2M19 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" />
20
- </svg>
21
- );
22
- }
23
-
24
- function IconMoon({ size = 14 }: { size?: number }) {
25
- return (
26
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
27
- <path d="M21 14.5A8.5 8.5 0 1 1 12 3a7 7 0 0 0 9 11.5z" stroke="currentColor" strokeWidth="1.75" strokeLinejoin="round" />
28
- </svg>
29
- );
30
- }
31
-
32
- function IconDownload({ size = 14 }: { size?: number }) {
33
- return (
34
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
35
- <path d="M12 4v12M7 12l5 5 5-5M5 20h14" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
36
- </svg>
37
- );
38
- }
39
-
40
- function IconFullScreen({ size = 14 }: { size?: number }) {
41
- return (
42
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
43
- <path d="M8 4H4v4M16 4h4v4M8 20H4v-4M16 20h4v-4" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
44
- </svg>
45
- );
46
- }
47
-
48
- function IconQuitFullScreen({ size = 16 }: { size?: number }) {
49
- return (
50
- <svg width={size} height={size} viewBox="0 0 24 24" fill="none" aria-hidden>
51
- <path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
52
- </svg>
53
- );
54
- }
55
-
56
16
  export function MermaidBlock({ children, className }: MermaidBlockProps) {
57
17
  const id = useId().replace(/:/g, "_");
58
18
  const containerRef = useRef<HTMLDivElement>(null);
@@ -216,13 +176,13 @@ export function MermaidBlock({ children, className }: MermaidBlockProps) {
216
176
  <div className={cn("my-3 last:mb-0 group relative rounded-xl border border-border overflow-hidden", surfaceClass, className)}>
217
177
  <div className="absolute top-2 right-2 z-10 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
218
178
  <button type="button" onClick={toggleTheme} className={cn(btnClass, surfaceClass)} title={isDark ? "Light theme" : "Dark theme"}>
219
- {isDark ? <IconSun /> : <IconMoon />}
179
+ {isDark ? <Sun size={14} aria-hidden /> : <Moon size={14} aria-hidden />}
220
180
  </button>
221
181
  <button type="button" onClick={downloadSvg} className={cn(btnClass, surfaceClass)} title="Download SVG" disabled={!svgContent}>
222
- <IconDownload />
182
+ <Download size={14} aria-hidden />
223
183
  </button>
224
184
  <button type="button" onClick={openFullscreen} className={cn(btnClass, surfaceClass)} title="Fullscreen">
225
- <IconFullScreen />
185
+ <Maximize2 size={14} aria-hidden />
226
186
  </button>
227
187
  </div>
228
188
  <div ref={containerRef} className="flex justify-center p-6 overflow-x-auto [&_svg]:max-w-full" />
@@ -237,13 +197,13 @@ export function MermaidBlock({ children, className }: MermaidBlockProps) {
237
197
  >
238
198
  <div className="fixed top-5 right-5 z-50 flex items-center gap-1.5">
239
199
  <button type="button" onClick={toggleTheme} className={cn(btnClass, surfaceClass, "px-3 py-2 text-sm shadow-md")} title={isDark ? "Light theme" : "Dark theme"}>
240
- {isDark ? <IconSun size={16} /> : <IconMoon size={16} />}
200
+ {isDark ? <Sun size={16} aria-hidden /> : <Moon size={16} aria-hidden />}
241
201
  </button>
242
202
  <button type="button" onClick={downloadSvg} className={cn(btnClass, surfaceClass, "px-3 py-2 text-sm shadow-md")} title="Download SVG" disabled={!svgContent}>
243
- <IconDownload size={16} />
203
+ <Download size={16} aria-hidden />
244
204
  </button>
245
205
  <button type="button" onClick={closeFullscreen} className={cn(btnClass, surfaceClass, "px-3 py-2 text-sm shadow-md")} title="Exit fullscreen">
246
- <IconQuitFullScreen />
206
+ <Minimize2 size={16} aria-hidden />
247
207
  <span>Exit</span>
248
208
  </button>
249
209
  </div>
@@ -1,8 +1,8 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import { Shimmer } from "../../shimmer/Shimmer";
3
+ import { Spin } from "../../spin/Spin";
3
4
  import { formatBgElapsed, parseBgTaskRef } from "../common/bgTasks";
4
5
  import { formatToolName, timestampMs } from "../common/utils";
5
- import { ChatSpinner } from "../message-ui/ChatSpinner";
6
6
  import { ToolUiBadge } from "./ToolUiBadge";
7
7
  import type { ToolUIProps } from "./types";
8
8
 
@@ -20,16 +20,13 @@ export function BackgroundTaskToolUI({ msg, assistantLabel = "Assistant", assist
20
20
  <div className="mt-1 animate-fadeIn">
21
21
  <ToolUiBadge show={showAvatar} label={assistantLabel} color={assistantColor} />
22
22
  <div className="px-4 pb-1">
23
- <div className="flex gap-0">
24
- <div className="w-0.5 shrink-0 self-stretch rounded-full bg-brand-700" />
25
- <div className="min-w-0 flex-1 pl-2">
26
- <div className="flex items-center gap-2 py-1">
27
- <ChatSpinner />
28
- <Shimmer className="min-w-0 flex-1 truncate text-[14px] font-medium text-foreground">{label}</Shimmer>
29
- <span className="shrink-0 rounded px-1 py-px text-[9px] font-semibold uppercase tracking-wider text-brand-700">Background</span>
30
- <span className="shrink-0 text-[14px] tabular-nums text-muted-foreground">{formatBgElapsed(timestampMs(msg.timestamp), now)}</span>
31
- </div>
32
- </div>
23
+ <div className="flex items-center gap-2 py-1">
24
+ <Spin variant="agent" size="small" className="shrink-0" />
25
+ <Shimmer className="min-w-0 truncate text-[14px] font-medium text-muted-foreground">
26
+ {label}
27
+ <span className="font-normal text-tertiary-foreground"> in background</span>
28
+ </Shimmer>
29
+ <span className="shrink-0 text-[13px] tabular-nums text-tertiary-foreground">{formatBgElapsed(timestampMs(msg.timestamp), now)}</span>
33
30
  </div>
34
31
  </div>
35
32
  </div>
@@ -1,12 +1,12 @@
1
+ import { ChevronDown, Square } from "lucide-react";
1
2
  import { type ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react";
2
- import { FluentIcon } from "../../icon/FluentIcon";
3
3
  import { cn } from "../../lib/cn";
4
4
  import { Modal } from "../../modal/Modal";
5
5
  import { Popconfirm } from "../../popconfirm/Popconfirm";
6
+ import { Spin } from "../../spin/Spin";
6
7
  import type { ChatBgTask } from "../common/bgTasks";
7
8
  import { formatBgElapsed } from "../common/bgTasks";
8
9
  import { formatToolName } from "../common/utils";
9
- import { ChatSpinner } from "../message-ui/ChatSpinner";
10
10
 
11
11
  function TaskLogsModal({
12
12
  task,
@@ -27,7 +27,7 @@ function TaskLogsModal({
27
27
 
28
28
  return (
29
29
  <Modal open={open} onCancel={onClose} footer={null} title={task ? formatToolName(task.toolName) : "Logs"} width={560}>
30
- <pre ref={preRef} className="m-0 max-h-[50vh] overflow-auto rounded-md border border-border-subtle bg-muted px-3 py-2 font-mono text-[14px] leading-relaxed whitespace-pre-wrap break-all text-foreground">
30
+ <pre ref={preRef} className="m-0 max-h-[50vh] overflow-auto rounded-md border border-border-subtle bg-muted px-3 py-2 font-mono text-[13px] leading-relaxed whitespace-pre-wrap break-all text-foreground">
31
31
  {logs || "Waiting for output…"}
32
32
  </pre>
33
33
  </Modal>
@@ -48,16 +48,15 @@ function TaskRow({
48
48
  onLogs: () => void;
49
49
  }) {
50
50
  return (
51
- <div className="flex items-center gap-2 px-2.5 py-1">
52
- <ChatSpinner className="size-2.5" />
53
- <div className="min-w-0 flex-1 truncate text-[14px] text-foreground">{formatToolName(task.toolName)}</div>
54
- <span className="shrink-0 text-[14px] tabular-nums text-muted-foreground">{formatBgElapsed(task.startedAt, now)}</span>
55
- <button type="button" onClick={onLogs} title="Logs" aria-label={`Logs ${formatToolName(task.toolName)}`} className="inline-flex size-5 shrink-0 items-center justify-center cursor-pointer rounded text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground">
56
- <FluentIcon name="document-text-24" size={12} />
51
+ <div className="flex items-center gap-1 px-1.5 py-0.5">
52
+ <button type="button" onClick={onLogs} title="Logs" aria-label={`Logs ${formatToolName(task.toolName)}`} className="flex min-w-0 flex-1 cursor-pointer items-center gap-2 rounded-md px-1 py-[3px] text-left transition-colors hover:bg-secondary">
53
+ <Spin variant="agent" size="small" className="shrink-0" />
54
+ <span className="min-w-0 flex-1 truncate text-[13px] font-medium text-foreground">{formatToolName(task.toolName)}</span>
55
+ <span className="shrink-0 text-[12px] tabular-nums text-tertiary-foreground">{formatBgElapsed(task.startedAt, now)}</span>
57
56
  </button>
58
57
  <Popconfirm title={`Stop ${formatToolName(task.toolName)}?`} okText="Stop" okType="danger" onConfirm={onCancel} getPopupContainer={() => document.body}>
59
- <button type="button" disabled={cancelling} title="Stop" aria-label={`Stop ${formatToolName(task.toolName)}`} className="inline-flex h-5 shrink-0 items-center rounded px-1.5 text-[10px] cursor-pointer text-muted-foreground transition-colors hover:bg-secondary hover:text-destructive disabled:opacity-40">
60
- Stop
58
+ <button type="button" disabled={cancelling} title="Stop" aria-label={`Stop ${formatToolName(task.toolName)}`} className="inline-flex size-5 shrink-0 items-center justify-center rounded cursor-pointer text-destructive transition-colors hover:bg-destructive/10 disabled:opacity-40">
59
+ <Square size={10} fill="currentColor" strokeWidth={0} aria-hidden />
61
60
  </button>
62
61
  </Popconfirm>
63
62
  </div>
@@ -118,10 +117,10 @@ export function BackgroundTasksBar({ children, tasks = [], cancellingIds, onCanc
118
117
  {children}
119
118
 
120
119
  {tasks.length > 0 ? (
121
- <div className="absolute right-5 bottom-[calc(100%)] left-5 z-20 overflow-hidden rounded-t-lg border-x border-t border-border">
120
+ <div className="absolute inset-x-6 bottom-[calc(100%)] z-20 overflow-hidden rounded-t-lg border-x border-t border-border">
122
121
  <div className={cn("flex h-7.5 items-center gap-1 px-1", open && "border-b border-border-subtle")}>
123
- <button type="button" onClick={() => setOpen((v) => !v)} className="flex min-w-0 flex-1 font-medium items-center gap-1 rounded-md px-1.5 py-1 text-[14px] text-muted-foreground/90 transition-colors hover:text-foreground" aria-expanded={open} aria-label={label}>
124
- {open ? <FluentIcon name="arrow-square-down-24" size={12} /> : <FluentIcon name="arrow-square-down-24" size={12} className="-rotate-90" />}
122
+ <button type="button" onClick={() => setOpen((v) => !v)} className="flex min-w-0 flex-1 cursor-pointer font-medium items-center gap-1 rounded-md px-1.5 py-1 text-[13px] text-muted-foreground/90 transition-colors hover:text-foreground" aria-expanded={open} aria-label={label}>
123
+ <ChevronDown size={13} className={cn("shrink-0 transition-transform duration-150", !open && "-rotate-90")} aria-hidden />
125
124
  <span className="truncate">{label}</span>
126
125
  </button>
127
126
  </div>
@@ -2,6 +2,7 @@ import { type ReactNode, useEffect, useRef, useState } from "react";
2
2
  import { FluentIcon } from "../../icon/FluentIcon";
3
3
  import { cn } from "../../lib/cn";
4
4
  import { Shimmer } from "../../shimmer/Shimmer";
5
+ import { Spin } from "../../spin/Spin";
5
6
  import { formatBgElapsed, parseBgTaskRef } from "../common/bgTasks";
6
7
  import { parseJsonObject, prettyJson, timestampMs } from "../common/utils";
7
8
  import { ChatMarkdown } from "../message-ui/ChatMarkdown";
@@ -111,6 +112,7 @@ export function CallAgentToolUI({ msg, assistantLabel = "Assistant", assistantCo
111
112
  const hasRequest = requestMessage.length > 0;
112
113
  const composing = !hasOutput && !hasError && !hasRequest;
113
114
  const awaitingReply = bgRunning || (!hasOutput && !hasError && hasRequest);
115
+ const calling = composing || awaitingReply;
114
116
  const showCallee = awaitingReply || failed || parsed != null;
115
117
  const calleeName = msg.toolLabel?.replace(/^Call\s+/i, "") ?? "Agent";
116
118
  const statusLabel = failed && !awaitingReply ? (parsed?.error ?? "Call failed") : null;
@@ -121,11 +123,15 @@ export function CallAgentToolUI({ msg, assistantLabel = "Assistant", assistantCo
121
123
  <div className="px-4 py-1">
122
124
  <div className={cn("overflow-hidden rounded-xl border bg-card", failed ? "border-destructive/35" : "border-border")}>
123
125
  <div className="flex items-center gap-2 border-b border-border-subtle px-3 py-2">
124
- <FluentIcon name="chat-24" size={13} className="shrink-0 text-muted-foreground" />
125
- <Shimmer active={composing || awaitingReply} className="min-w-0 flex-1 truncate text-[14px] font-medium text-foreground">Calling {calleeName}</Shimmer>
126
+ {calling ? (
127
+ <Spin variant="agent" size="small" className="shrink-0" />
128
+ ) : (
129
+ <FluentIcon name="chat-multiple-24" size={13} className="shrink-0 text-muted-foreground" />
130
+ )}
131
+ <Shimmer active={calling} className="min-w-0 flex-1 truncate text-[14px] font-medium text-foreground">Calling {calleeName}</Shimmer>
126
132
  {bgRunning ? <span className="text-[14px] tabular-nums text-muted-foreground">{formatBgElapsed(timestampMs(msg.timestamp), now)}</span> : null}
127
133
  {statusLabel ? <span className="max-w-40 truncate text-[13px] italic text-destructive">{statusLabel}</span> : null}
128
- <ToolUiTrailing running={composing || awaitingReply} failed={failed && !awaitingReply} />
134
+ <ToolUiTrailing failed={failed && !awaitingReply} />
129
135
  </div>
130
136
 
131
137
  {composing || hasRequest || showCallee ? (
@@ -154,7 +160,7 @@ export function CallAgentToolUI({ msg, assistantLabel = "Assistant", assistantCo
154
160
  </ExpandableBody>
155
161
  ) : (
156
162
  <ExpandableBody>
157
- <pre className="m-0 font-mono text-[14px] leading-normal break-all whitespace-pre-wrap text-muted-foreground">{prettyJson(msg.toolOutput)}</pre>
163
+ <pre className="m-0 font-mono text-[13px] leading-normal break-all whitespace-pre-wrap text-muted-foreground">{prettyJson(msg.toolOutput)}</pre>
158
164
  </ExpandableBody>
159
165
  )}
160
166
  </div>