devnonla-ui 0.2.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 (50) 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 +694 -30
  27. package/src/colorpicker/color.ts +246 -0
  28. package/src/datepicker/DatePicker.tsx +3 -3
  29. package/src/desktop/DesktopHeader.tsx +15 -20
  30. package/src/desktop/DesktopIcon.tsx +1 -1
  31. package/src/desktop/DesktopWindow.tsx +54 -30
  32. package/src/form/Form.tsx +1 -1
  33. package/src/form/FormItem.tsx +2 -2
  34. package/src/form/index.ts +23 -24
  35. package/src/form/variants/FieldColor.tsx +16 -51
  36. package/src/index.ts +167 -196
  37. package/src/input/Input.tsx +14 -12
  38. package/src/input/SearchInput.tsx +5 -2
  39. package/src/lib/sizes.ts +1 -1
  40. package/src/modal/Modal.tsx +3 -0
  41. package/src/pagination/Pagination.tsx +528 -51
  42. package/src/scroll/OverlayScroll.tsx +58 -37
  43. package/src/select/Select.tsx +2 -2
  44. package/src/spin/Spin.tsx +14 -17
  45. package/src/splitter/Splitter.tsx +330 -0
  46. package/src/splitter/sizes.ts +67 -0
  47. package/src/styles.css +28 -3
  48. package/src/switch/Switch.tsx +1 -1
  49. package/src/table/Table.tsx +43 -18
  50. package/src/timepicker/TimePicker.tsx +2 -2
@@ -1,8 +1,8 @@
1
1
  import { type ReactNode, useState } from "react";
2
+ import { FluentIcon } from "../../icon/FluentIcon";
2
3
  import { cn } from "../../lib/cn";
3
4
  import { Shimmer } from "../../shimmer/Shimmer";
4
5
  import { formatToolName, hasMeaningfulInput, prettyJson } from "../common/utils";
5
- import { ChatSpinner } from "../message-ui/ChatSpinner";
6
6
  import { ToolUiTrailing } from "./ToolUiTrailing";
7
7
 
8
8
  export type ChatToolCallProps = {
@@ -11,7 +11,7 @@ export type ChatToolCallProps = {
11
11
  toolInput?: unknown;
12
12
  toolOutput?: unknown;
13
13
  toolError?: boolean | string;
14
- /** Show running spinner (pending execution). */
14
+ /** Pending execution label shimmers, no spinner. */
15
15
  running?: boolean;
16
16
  /** Optional icon node (left of label). */
17
17
  icon?: ReactNode;
@@ -28,44 +28,38 @@ export function ChatToolCall({ toolName = "Tool", label, toolInput, toolOutput,
28
28
  const [open, setOpen] = useState(defaultOpen);
29
29
  const displayLabel = label ?? formatToolName(toolName);
30
30
 
31
- const identityIcon = icon ?? (
32
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" className="shrink-0 text-muted-foreground" aria-hidden>
33
- <path d="M8 7h8M8 12h8M8 17h5" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" />
34
- <rect x="4" y="4" width="16" height="16" rx="3" stroke="currentColor" strokeWidth="1.75" />
35
- </svg>
36
- );
31
+ const identityIcon = icon ?? <FluentIcon name="board-24" size={13} className="shrink-0" />;
37
32
 
38
33
  const header = (
39
34
  <>
40
35
  {identityIcon}
41
36
  <Shimmer active={running} className={cn("min-w-0 flex-1 truncate text-left text-[14px] font-medium", !running && "text-muted-foreground", expandable && !running && "transition-colors group-hover:text-foreground")}>{displayLabel}</Shimmer>
42
- <ToolUiTrailing running={running} failed={hasError} chevron={expandable} chevronClassName={cn("group-hover:opacity-100", open && "opacity-100 rotate-90")} />
37
+ <ToolUiTrailing failed={hasError} chevron={expandable} chevronClassName={cn("group-hover:opacity-100", open && "opacity-100 rotate-90")} />
43
38
  </>
44
39
  );
45
40
 
46
41
  return (
47
42
  <div className={cn("nonla-chat-tool mt-1", className)}>
48
43
  <div className="px-4 pb-0.5">
49
- <div className={cn("mb-1 overflow-hidden rounded-xl border", hasError ? "border-destructive/35" : "border-border")}>
44
+ <div className={cn("mb-1 overflow-hidden rounded-md", open && "bg-card")}>
50
45
  {expandable ? (
51
- <button type="button" onClick={() => setOpen((v) => !v)} className={cn("group flex w-full cursor-pointer items-center gap-2 px-2.5 py-1.5 outline-none transition-colors hover:bg-muted border-0 bg-transparent", open && "bg-muted")}>
46
+ <button type="button" onClick={() => setOpen((v) => !v)} className="group flex w-full cursor-pointer items-center gap-2 border-0 bg-transparent px-2.5 py-1 outline-none">
52
47
  {header}
53
48
  </button>
54
49
  ) : (
55
- <div className="flex w-full items-center gap-2 px-2.5 py-1.5">{header}</div>
50
+ <div className="flex w-full items-center gap-2 bg-transparent px-2.5 py-1">{header}</div>
56
51
  )}
57
52
 
58
53
  {open && expandable ? (
59
- <div className="border-t border-border font-mono text-[14px]">
60
- {hasInput ? <pre className="m-0 max-h-27.5 overflow-y-auto bg-transparent px-3 py-2 break-all whitespace-pre-wrap font-normal leading-[1.65] text-muted-foreground">{prettyJson(toolInput)}</pre> : null}
54
+ <div className="font-mono text-[13px]">
55
+ {hasInput ? <pre className="m-0 max-h-27.5 overflow-y-auto bg-well px-3 py-1.5 break-all whitespace-pre-wrap font-normal leading-[1.65] text-muted-foreground">{prettyJson(toolInput)}</pre> : null}
61
56
  {running ? (
62
- <div className="flex items-center gap-2 border-t border-border bg-muted/40 px-3 py-2 text-muted-foreground">
63
- <ChatSpinner />
57
+ <div className="bg-well-strong px-3 py-1.5 text-muted-foreground">
64
58
  <Shimmer className="italic">Running…</Shimmer>
65
59
  </div>
66
60
  ) : null}
67
61
  {!isPending ? (
68
- <pre className={cn("m-0 max-h-75 overflow-y-auto px-3 py-2 break-all whitespace-pre-wrap font-normal leading-[1.65] text-muted-foreground", hasInput && "border-t border-border", hasError ? "bg-destructive/6" : "bg-muted/40")}>
62
+ <pre className={cn("m-0 max-h-75 overflow-y-auto px-3 py-1.5 break-all whitespace-pre-wrap font-normal leading-[1.65] text-muted-foreground", hasError ? "bg-destructive/6" : "nonla-chat-tool-result-ok")}>
69
63
  {hasOutput ? prettyJson(toolOutput) : typeof toolError === "string" ? toolError : "Tool execution failed"}
70
64
  </pre>
71
65
  ) : null}
@@ -1,6 +1,6 @@
1
1
  import { FluentIcon } from "../../icon/FluentIcon";
2
- import { parseJsonObject } from "../common/utils";
3
2
  import { Shimmer } from "../../shimmer/Shimmer";
3
+ import { parseJsonObject } from "../common/utils";
4
4
  import { ToolUiBadge } from "./ToolUiBadge";
5
5
  import { ToolUiTrailing } from "./ToolUiTrailing";
6
6
  import { isToolRunning, type ToolUIProps } from "./types";
@@ -42,7 +42,7 @@ export function GetCurrentTimeToolUI({ msg, assistantLabel = "Assistant", assist
42
42
  <div className="flex items-center gap-2 py-1">
43
43
  <FluentIcon name="clock-24" size={13} className="shrink-0 text-muted-foreground" />
44
44
  <Shimmer active={running} className="truncate text-[14px] font-medium tabular-nums text-muted-foreground">{hasError ? "Failed to get time" : running ? "Getting time…" : label}</Shimmer>
45
- <ToolUiTrailing running={running} failed={hasError} />
45
+ <ToolUiTrailing failed={hasError} />
46
46
  </div>
47
47
  </div>
48
48
  </div>
@@ -60,16 +60,16 @@ export function ReadSkillToolUI({ msg, assistantLabel = "Assistant", assistantCo
60
60
  {verb}
61
61
  {targetLabel ? <span className="font-normal text-tertiary-foreground"> {targetLabel}</span> : null}
62
62
  </Shimmer>
63
- <ToolUiTrailing running={running} failed={failed} chevron chevronClassName="group-hover/readskill:opacity-100 group-open/readskill:opacity-100 group-open/readskill:rotate-90" />
63
+ <ToolUiTrailing failed={failed} chevron chevronClassName="group-hover/readskill:opacity-100 group-open/readskill:opacity-100 group-open/readskill:rotate-90" />
64
64
  </summary>
65
65
  {body ? (
66
- <pre className={cn("m-0 mt-1.5 mb-1 max-h-40 overflow-y-auto rounded-lg border px-3 py-2 font-mono text-[14px] font-normal leading-[1.65] break-all whitespace-pre-wrap", failed ? "border-destructive/35 bg-destructive/6 text-destructive" : "border-(--popper-border) bg-(--nonla-elevated) text-muted-foreground")}>{body}</pre>
66
+ <pre className={cn("m-0 mt-1.5 mb-1 max-h-40 overflow-y-auto rounded-lg border px-3 py-2 font-mono text-[13px] font-normal leading-[1.65] break-all whitespace-pre-wrap", failed ? "border-destructive/35 bg-destructive/6 text-destructive" : "border-(--popper-border) bg-(--nonla-elevated) text-muted-foreground")}>{body}</pre>
67
67
  ) : null}
68
68
  {!running && !reference && available.length > 0 ? (
69
69
  <div className="mt-0.5 mb-1 flex flex-wrap items-center gap-1">
70
70
  <span className="text-[10px] font-medium tracking-wide text-quaternary-foreground uppercase">Refs</span>
71
71
  {available.map((name) => (
72
- <span key={name} className="inline-flex max-w-full items-center rounded-md border border-border-subtle bg-muted/40 px-1.5 py-0.5 font-mono text-[14px] leading-[1.4] text-tertiary-foreground">
72
+ <span key={name} className="inline-flex max-w-full items-center rounded-md border border-border-subtle bg-muted/40 px-1.5 py-0.5 font-mono text-[13px] leading-[1.4] text-tertiary-foreground">
73
73
  {skillName ? `${skillName} / ${name}` : name}
74
74
  </span>
75
75
  ))}
@@ -1,6 +1,6 @@
1
+ import { CodeBlock } from "../../codeblock/CodeBlock";
1
2
  import { FluentIcon } from "../../icon/FluentIcon";
2
3
  import { cn } from "../../lib/cn";
3
- import { CodeBlock } from "../../codeblock/CodeBlock";
4
4
  import { Shimmer } from "../../shimmer/Shimmer";
5
5
  import { parseBgTaskRef } from "../common/bgTasks";
6
6
  import { parseJsonObject, prettyJson } from "../common/utils";
@@ -87,7 +87,7 @@ export function RunJsToolUI({ msg, assistantLabel = "Assistant", assistantColor,
87
87
  <ToolUiBadge show={showAvatar} label={assistantLabel} color={assistantColor} />
88
88
  <details className="group/runjs px-4 pb-2" style={{ overflowAnchor: "none" }}>
89
89
  <summary className="flex cursor-pointer list-none items-center gap-2 py-0.5 text-[14px] leading-5.5 select-none [&::-webkit-details-marker]:hidden">
90
- <FluentIcon name="code-24" size={13} className="shrink-0 text-muted-foreground" />
90
+ <FluentIcon name="code-block-24" size={13} className="shrink-0 text-muted-foreground" />
91
91
  <Shimmer active={running} className="min-w-0 truncate font-medium text-muted-foreground">{verb}</Shimmer>
92
92
  <ToolUiTrailing running={running} failed={failed} chevron chevronClassName="group-hover/runjs:opacity-100 group-open/runjs:opacity-100 group-open/runjs:rotate-90" />
93
93
  </summary>
@@ -1,4 +1,4 @@
1
- import { FluentIcon } from "../../icon/FluentIcon";
1
+ import { ChevronRight, CircleX } from "lucide-react";
2
2
  import { cn } from "../../lib/cn";
3
3
  import { ChatSpinner } from "../message-ui/ChatSpinner";
4
4
 
@@ -14,11 +14,7 @@ export function ToolUiTrailing({
14
14
  chevronClassName?: string;
15
15
  }) {
16
16
  if (running) return <ChatSpinner />;
17
- if (failed) return <FluentIcon name="error-circle-24" size={13} className="text-destructive" />;
17
+ if (failed) return <CircleX size={13} className="text-destructive" aria-hidden />;
18
18
  if (!chevron) return null;
19
- return (
20
- <svg className={cn("h-3 w-3 shrink-0 opacity-0 transition-[opacity,transform] duration-150", chevronClassName)} fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden>
21
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
22
- </svg>
23
- );
19
+ return <ChevronRight size={12} className={cn("shrink-0 opacity-0 transition-[opacity,transform] duration-150", chevronClassName)} aria-hidden />;
24
20
  }
@@ -32,23 +32,32 @@ export function WebFetchToolUI({ msg, assistantLabel = "Assistant", assistantCol
32
32
  const running = isToolRunning(msg, generating);
33
33
  const url = output?.url || inputUrl(msg.toolInput) || "—";
34
34
  const body = failed ? (output?.error ?? (typeof msg.toolError === "string" ? msg.toolError : null) ?? "Tool execution failed") : output?.text?.trim() || "";
35
- const verb = running || failed ? "Fetch page" : "Fetched page";
35
+ const expandable = Boolean(body);
36
+ const verb = running ? "Fetching page" : failed ? "Fetch page" : "Fetched page";
37
+
38
+ const header = (
39
+ <>
40
+ <FluentIcon name="globe-24" size={13} className="shrink-0 text-muted-foreground" />
41
+ <Shimmer active={running} className="min-w-0 truncate font-medium text-muted-foreground">
42
+ {verb} <span className="font-normal text-tertiary-foreground">{url}</span>
43
+ </Shimmer>
44
+ <ToolUiTrailing failed={failed} chevron={expandable} chevronClassName="group-hover/webfetch:opacity-100 group-open/webfetch:opacity-100 group-open/webfetch:rotate-90" />
45
+ </>
46
+ );
36
47
 
37
48
  return (
38
49
  <div className="mt-1 animate-fadeIn">
39
50
  <ToolUiBadge show={showAvatar} label={assistantLabel} color={assistantColor} />
40
- <details className="group/webfetch px-4 pb-2" style={{ overflowAnchor: "none" }}>
41
- <summary className="flex cursor-pointer list-none items-center gap-2 py-0.5 text-[14px] leading-5.5 select-none [&::-webkit-details-marker]:hidden">
42
- <FluentIcon name="globe-24" size={13} className="shrink-0 text-muted-foreground" />
43
- <Shimmer active={running} className="min-w-0 truncate font-medium text-muted-foreground">
44
- {verb} <span className="font-normal text-tertiary-foreground">{url}</span>
45
- </Shimmer>
46
- <ToolUiTrailing running={running} failed={failed} chevron chevronClassName="group-hover/webfetch:opacity-100 group-open/webfetch:opacity-100 group-open/webfetch:rotate-90" />
47
- </summary>
48
- {body ? (
49
- <pre className={cn("m-0 mt-1.5 mb-1 max-h-40 overflow-y-auto rounded-lg border px-3 py-2 font-mono text-[14px] font-normal leading-[1.65] break-all whitespace-pre-wrap", failed ? "border-destructive/35 bg-destructive/6 text-destructive" : "border-(--popper-border) bg-(--nonla-elevated) text-muted-foreground")}>{body}</pre>
50
- ) : null}
51
- </details>
51
+ {expandable ? (
52
+ <details className="group/webfetch px-4 pb-2" style={{ overflowAnchor: "none" }}>
53
+ <summary className="flex cursor-pointer list-none items-center gap-2 py-0.5 text-[14px] leading-5.5 select-none [&::-webkit-details-marker]:hidden">{header}</summary>
54
+ <pre className={cn("m-0 mt-1.5 mb-1 max-h-40 overflow-y-auto rounded-lg border px-3 py-2 font-mono text-[13px] font-normal leading-[1.65] break-all whitespace-pre-wrap", failed ? "border-destructive/35 bg-destructive/6 text-destructive" : "border-(--popper-border) bg-(--nonla-elevated) text-muted-foreground")}>{body}</pre>
55
+ </details>
56
+ ) : (
57
+ <div className="px-4 pb-2">
58
+ <div className="flex items-center gap-2 py-0.5 text-[14px] leading-5.5">{header}</div>
59
+ </div>
60
+ )}
52
61
  </div>
53
62
  );
54
63
  }
@@ -1,5 +1,5 @@
1
1
  import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
2
- import { type ComponentPropsWithoutRef, type ReactNode, forwardRef } from "react";
2
+ import { type ComponentPropsWithoutRef, forwardRef, type ReactNode } from "react";
3
3
  import { cn } from "../lib/cn";
4
4
 
5
5
  export type CheckboxProps = Omit<ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>, "onCheckedChange" | "checked" | "onChange"> & {
@@ -1,6 +1,9 @@
1
- import { type ComponentProps, type ReactNode, createContext, useContext, useMemo } from "react";
1
+ import { FileText } from "lucide-react";
2
+ import { type ComponentProps, createContext, type ReactNode, useContext, useMemo } from "react";
2
3
  import { ButtonCopy } from "../button/ButtonCopy";
3
4
  import { cn } from "../lib/cn";
5
+ import type { ControlSize } from "../lib/sizes";
6
+ import { OverlayScroll } from "../scroll/OverlayScroll";
4
7
  import { highlightCode, languageLabel, wrapHljsLines } from "./highlight";
5
8
 
6
9
  type CodeBlockContextValue = {
@@ -22,16 +25,9 @@ export type CodeBlockProps = ComponentProps<"div"> & {
22
25
  wordWrap?: boolean;
23
26
  };
24
27
 
25
- function CodeFileIcon({ size = 14 }: { size?: number }) {
26
- return (
27
- <svg width={size} height={size} viewBox="0 0 256 256" aria-hidden>
28
- <path fill="none" stroke="currentColor" strokeWidth="25" strokeLinecap="round" d="M208 128l-80 80M192 40 40 192" />
29
- </svg>
30
- );
31
- }
32
-
33
- export type CodeBlockCopyButtonProps = ComponentProps<"button"> & {
28
+ export type CodeBlockCopyButtonProps = Omit<ComponentProps<"button">, "size"> & {
34
29
  content?: string;
30
+ size?: ControlSize;
35
31
  };
36
32
 
37
33
  export function CodeBlockCopyButton({ content, className, ...props }: CodeBlockCopyButtonProps) {
@@ -54,7 +50,7 @@ export function CodeBlock({ code, language, title, lineNumbers = false, wordWrap
54
50
  <div className="flex min-w-0 items-center gap-2">
55
51
  {label ? (
56
52
  <>
57
- <CodeFileIcon />
53
+ <FileText size={14} className="shrink-0" aria-hidden />
58
54
  <span className="truncate font-medium">{label}</span>
59
55
  </>
60
56
  ) : null}
@@ -62,14 +58,16 @@ export function CodeBlock({ code, language, title, lineNumbers = false, wordWrap
62
58
  <CodeBlockCopyButton />
63
59
  </div>
64
60
 
65
- <div className={cn("nonla-codeblock-body nonla-codeblock-well relative min-w-0 overflow-y-auto bg-card font-mono text-[13px] leading-5", children ? "max-h-96" : "mx-0.5 mb-0.5 max-h-96 rounded-lg")}>
66
- <div className={cn("w-full", !wordWrap && "overflow-x-auto")}>
67
- <pre className={cn("nonla-codeblock-pre m-0 whitespace-pre break-normal", lineNumbers && "nonla-codeblock-lines", wordWrap && "whitespace-pre-wrap wrap-break-word")}>
68
- {/* biome-ignore lint/security/noDangerouslySetInnerHtml: highlight.js emits escaped HTML */}
69
- <code className={cn("block px-3 py-2.5", !wordWrap && "w-max min-w-full")} dangerouslySetInnerHTML={{ __html: html }} />
70
- </pre>
71
- </div>
72
- </div>
61
+ <OverlayScroll
62
+ autoHeight
63
+ className={cn("nonla-codeblock-body nonla-codeblock-well min-w-0 bg-card font-mono text-[13px] leading-5 max-h-96", !children && "mx-0.5 mb-0.5 rounded-lg")}
64
+ innerClassName={wordWrap ? undefined : "overflow-x-auto"}
65
+ >
66
+ <pre className={cn("nonla-codeblock-pre m-0 whitespace-pre break-normal", lineNumbers && "nonla-codeblock-lines", wordWrap && "whitespace-pre-wrap wrap-break-word")}>
67
+ {/* biome-ignore lint/security/noDangerouslySetInnerHtml: highlight.js emits escaped HTML */}
68
+ <code className={cn("block px-3 py-2.5", !wordWrap && "w-max min-w-full")} dangerouslySetInnerHTML={{ __html: html }} />
69
+ </pre>
70
+ </OverlayScroll>
73
71
  {children}
74
72
  </div>
75
73
  </CodeBlockContext.Provider>