devnonla-ui 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +22 -10
  2. package/package.json +2 -2
  3. package/src/alert/Alert.tsx +1 -1
  4. package/src/app/App.tsx +38 -18
  5. package/src/app/context.ts +32 -0
  6. package/src/button/Button.tsx +2 -2
  7. package/src/calendar/Calendar.tsx +4 -4
  8. package/src/chat/AgentPanel.tsx +15 -6
  9. package/src/chat/common/types.ts +27 -2
  10. package/src/chat/common/useAgentStream.ts +78 -12
  11. package/src/chat/common/utils.ts +13 -0
  12. package/src/chat/index.ts +5 -3
  13. package/src/chat/message-ui/ChatAgentMessage.tsx +1 -1
  14. package/src/chat/message-ui/ChatInput.tsx +1 -1
  15. package/src/chat/message-ui/ChatMarkdown.tsx +6 -6
  16. package/src/chat/message-ui/ChatMarkdownTable.tsx +1 -1
  17. package/src/chat/message-ui/ChatThinking.tsx +4 -3
  18. package/src/chat/message-ui/ChatUserMessage.tsx +0 -8
  19. package/src/chat/message-ui/MermaidBlock.tsx +11 -1
  20. package/src/chat/tool-ui/BackgroundTaskToolUI.tsx +2 -1
  21. package/src/chat/tool-ui/CallAgentToolUI.tsx +3 -2
  22. package/src/chat/tool-ui/ChatToolCall.tsx +3 -2
  23. package/src/chat/tool-ui/GetCurrentTimeToolUI.tsx +2 -1
  24. package/src/chat/tool-ui/ReadSkillToolUI.tsx +3 -2
  25. package/src/chat/tool-ui/RunJsToolUI.tsx +3 -2
  26. package/src/chat/tool-ui/WebFetchToolUI.tsx +3 -2
  27. package/src/chat/tool-ui/registry.ts +24 -12
  28. package/src/checkbox/Checkbox.tsx +5 -4
  29. package/src/codeblock/CodeBlock.tsx +1 -0
  30. package/src/datepicker/DatePicker.tsx +15 -9
  31. package/src/desktop/DesktopHeader.tsx +1 -1
  32. package/src/desktop/DesktopIcon.tsx +2 -4
  33. package/src/desktop/DesktopWindow.tsx +19 -5
  34. package/src/desktop/MeadowDesktop.tsx +2 -2
  35. package/src/drawer/Drawer.tsx +8 -6
  36. package/src/dropdown/ContextMenu.tsx +3 -1
  37. package/src/dropdown/Dropdown.tsx +8 -6
  38. package/src/form/Form.tsx +2 -2
  39. package/src/form/FormItem.tsx +2 -2
  40. package/src/form/partials/FormItemHelps.tsx +2 -2
  41. package/src/form/variants/FieldColor.tsx +1 -1
  42. package/src/form/variants/FieldObject.tsx +2 -2
  43. package/src/form/variants/FieldRepeaterItem.tsx +4 -4
  44. package/src/form-layout/FormLayout.tsx +6 -6
  45. package/src/index.ts +20 -6
  46. package/src/input/Input.tsx +11 -10
  47. package/src/lib/sizes.ts +13 -3
  48. package/src/lib/surface.ts +2 -1
  49. package/src/menu/Menu.tsx +2 -2
  50. package/src/message/message.tsx +35 -2
  51. package/src/modal/Modal.tsx +49 -3
  52. package/src/pagination/Pagination.tsx +6 -5
  53. package/src/popconfirm/Popconfirm.tsx +1 -1
  54. package/src/popover/Popover.tsx +6 -8
  55. package/src/scroll/OverlayScroll.tsx +5 -3
  56. package/src/segmented/Segmented.tsx +6 -5
  57. package/src/select/Select.tsx +9 -6
  58. package/src/shimmer/Shimmer.tsx +17 -0
  59. package/src/skeleton/Skeleton.tsx +3 -3
  60. package/src/spin/Spin.tsx +8 -5
  61. package/src/styles.css +81 -27
  62. package/src/switch/Switch.tsx +6 -5
  63. package/src/table/Table.tsx +6 -7
  64. package/src/tag/Tag.tsx +2 -2
  65. package/src/theme.ts +23 -3
  66. package/src/timepicker/TimePicker.tsx +13 -11
  67. package/src/tooltip/Tooltip.tsx +6 -7
@@ -24,11 +24,11 @@ export const chatMarkdownClass = cn(
24
24
  "[&_strong]:font-semibold [&_em]:italic",
25
25
  "[&_blockquote]:m-0 [&_blockquote]:mb-(--chat-p-mb) [&_blockquote]:border-0 [&_blockquote]:p-0 [&_blockquote]:not-italic [&_blockquote]:text-inherit [&_blockquote:last-child]:mb-0",
26
26
  "[&_ul]:mt-2 [&_ul]:mb-(--chat-p-mb)",
27
- "[&_ul:not(.contains-task-list)]:list-disc [&_ul:not(.contains-task-list)]:pl-[26px]",
28
- "[&_ul.contains-task-list]:list-none [&_ul.contains-task-list]:pl-[26px]",
27
+ "[&_ul:not(.contains-task-list)]:list-disc [&_ul:not(.contains-task-list)]:pl-6.5",
28
+ "[&_ul.contains-task-list]:list-none [&_ul.contains-task-list]:pl-6.5",
29
29
  "[&_li.task-list-item]:relative",
30
- "[&_li.task-list-item>.nonla-md-task-check]:absolute [&_li.task-list-item>.nonla-md-task-check]:top-0 [&_li.task-list-item>.nonla-md-task-check]:left-[-22px]",
31
- "[&_ol]:mt-2 [&_ol]:mb-(--chat-p-mb) [&_ol]:list-decimal [&_ol]:pl-[26px]",
30
+ "[&_li.task-list-item>.nonla-md-task-check]:absolute [&_li.task-list-item>.nonla-md-task-check]:top-0 [&_li.task-list-item>.nonla-md-task-check]:-left-5.5",
31
+ "[&_ol]:mt-2 [&_ol]:mb-(--chat-p-mb) [&_ol]:list-decimal [&_ol]:pl-6.5",
32
32
  "[&_li]:my-1.5 [&_li]:leading-(--chat-body-leading)",
33
33
  "[&_a]:text-link [&_a]:no-underline [&_a]:hover:underline [&_a]:hover:underline-offset-[3px]",
34
34
  );
@@ -78,14 +78,14 @@ export function createChatMarkdownComponents(getState?: () => ChatMarkdownStream
78
78
  th({ children }) {
79
79
  return (
80
80
  <th>
81
- <div className="inline-block max-w-[300px] wrap-break-word">{children}</div>
81
+ <div className="inline-block max-w-75 wrap-break-word">{children}</div>
82
82
  </th>
83
83
  );
84
84
  },
85
85
  td({ children }) {
86
86
  return (
87
87
  <td>
88
- <div className="inline-block max-w-[300px] wrap-break-word">{children}</div>
88
+ <div className="inline-block max-w-75 wrap-break-word">{children}</div>
89
89
  </td>
90
90
  );
91
91
  },
@@ -35,7 +35,7 @@ export function ChatMarkdownTable({ children }: { children: ReactNode }) {
35
35
  <div className="overflow-x-auto">
36
36
  <table
37
37
  ref={tableRef}
38
- className="w-max min-w-full wrap-normal border-separate border-spacing-0 text-[14px] [&_th]:bg-foreground/[0.04] [&_th]:text-foreground [&_th]:font-semibold [&_th]:px-2.5 [&_th]:py-1.5 [&_th]:text-left [&_th]:align-top [&_th]:border-r [&_th]:border-b [&_th]:border-border [&_th:last-child]:border-r-0 [&_td]:px-2.5 [&_td]:py-1.5 [&_td]:align-top [&_td]:text-foreground [&_td]:bg-card [&_td]:border-r [&_td]:border-b [&_td]:border-border [&_td:last-child]:border-r-0 [&_tbody_tr:last-child_td]:border-b-0"
38
+ className="w-max min-w-full wrap-normal border-separate border-spacing-0 text-[14px] [&_th]:bg-foreground/4 [&_th]:text-foreground [&_th]:font-semibold [&_th]:px-2.5 [&_th]:py-1.5 [&_th]:text-left [&_th]:align-top [&_th]:border-r [&_th]:border-b [&_th]:border-border [&_th:last-child]:border-r-0 [&_td]:px-2.5 [&_td]:py-1.5 [&_td]:align-top [&_td]:text-foreground [&_td]:bg-card [&_td]:border-r [&_td]:border-b [&_td]:border-border [&_td:last-child]:border-r-0 [&_tbody_tr:last-child_td]:border-b-0"
39
39
  >
40
40
  {children}
41
41
  </table>
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useLayoutEffect, useRef, useState } from "react";
2
2
  import { cn } from "../../lib/cn";
3
+ import { Shimmer } from "../../shimmer/Shimmer";
3
4
 
4
5
  export type ChatThinkingProps = {
5
6
  thinking: string;
@@ -46,9 +47,9 @@ export function ChatThinking({ thinking, duration = 0, streaming = false, classN
46
47
  if (el) el.scrollTop = el.scrollHeight;
47
48
  }}
48
49
  >
49
- <summary className="cursor-pointer select-none text-sm font-medium text-tertiary-foreground flex items-center gap-1 py-0.5 list-none [&::-webkit-details-marker]:hidden">
50
- <span className={streaming ? "nonla-chat-shimmer" : undefined}>{label}</span>
51
- <svg className="w-3 h-3 shrink-0 opacity-0 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>
50
+ <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
+ <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>
52
53
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
53
54
  </svg>
54
55
  </summary>
@@ -25,21 +25,13 @@ export function ChatUserMessage({ content, className }: ChatUserMessageProps) {
25
25
 
26
26
  const collapsed = isOverflow && !isExpanded;
27
27
 
28
- const toggle = () => {
29
- const sel = window.getSelection();
30
- if (sel?.toString()) return;
31
- setIsExpanded((v) => !v);
32
- };
33
-
34
28
  return (
35
29
  <div
36
30
  className={cn(
37
31
  "nonla-chat-user relative isolate mt-6 mb-3 mx-4 overflow-hidden rounded-xl px-3 py-2.5",
38
32
  "border border-brand/40 bg-[color-mix(in_oklab,var(--brand)_14%,var(--glass))] backdrop-blur-xl backdrop-saturate-150",
39
- isOverflow && "cursor-pointer",
40
33
  className,
41
34
  )}
42
- onClick={isOverflow ? toggle : undefined}
43
35
  >
44
36
  <div
45
37
  ref={contentRef}
@@ -257,7 +257,17 @@ export function MermaidBlock({ children, className }: MermaidBlockProps) {
257
257
  ) : null}
258
258
  </div>
259
259
 
260
- <div onWheel={handleWheel} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseLeave} onDoubleClick={resetView} className={cn("flex-1 overflow-hidden w-full h-full select-none", isDragging ? "cursor-grabbing" : "cursor-grab")}>
260
+ <div
261
+ role="application"
262
+ aria-label="Pan and zoom diagram"
263
+ onWheel={handleWheel}
264
+ onMouseDown={handleMouseDown}
265
+ onMouseMove={handleMouseMove}
266
+ onMouseUp={handleMouseUp}
267
+ onMouseLeave={handleMouseLeave}
268
+ onDoubleClick={resetView}
269
+ className={cn("flex-1 overflow-hidden w-full h-full select-none", isDragging ? "cursor-grabbing" : "cursor-grab")}
270
+ >
261
271
  <div ref={fullscreenRef} className="flex items-center justify-center w-full h-full origin-center [&_svg]:max-w-none [&_svg]:max-h-none pointer-events-none" style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})` }} />
262
272
  </div>
263
273
  </dialog>
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState } from "react";
2
+ import { Shimmer } from "../../shimmer/Shimmer";
2
3
  import { formatBgElapsed, parseBgTaskRef } from "../common/bgTasks";
3
4
  import { formatToolName, timestampMs } from "../common/utils";
4
5
  import { ChatSpinner } from "../message-ui/ChatSpinner";
@@ -24,7 +25,7 @@ export function BackgroundTaskToolUI({ msg, assistantLabel = "Assistant", assist
24
25
  <div className="min-w-0 flex-1 pl-2">
25
26
  <div className="flex items-center gap-2 py-1">
26
27
  <ChatSpinner />
27
- <span className="min-w-0 flex-1 truncate text-[14px] font-medium text-foreground">{label}</span>
28
+ <Shimmer className="min-w-0 flex-1 truncate text-[14px] font-medium text-foreground">{label}</Shimmer>
28
29
  <span className="shrink-0 rounded px-1 py-px text-[9px] font-semibold uppercase tracking-wider text-brand-700">Background</span>
29
30
  <span className="shrink-0 text-[14px] tabular-nums text-muted-foreground">{formatBgElapsed(timestampMs(msg.timestamp), now)}</span>
30
31
  </div>
@@ -1,6 +1,7 @@
1
1
  import { type ReactNode, useEffect, useRef, useState } from "react";
2
2
  import { FluentIcon } from "../../icon/FluentIcon";
3
3
  import { cn } from "../../lib/cn";
4
+ import { Shimmer } from "../../shimmer/Shimmer";
4
5
  import { formatBgElapsed, parseBgTaskRef } from "../common/bgTasks";
5
6
  import { parseJsonObject, prettyJson, timestampMs } from "../common/utils";
6
7
  import { ChatMarkdown } from "../message-ui/ChatMarkdown";
@@ -30,7 +31,7 @@ function parseCallAgentOutput(raw: unknown): CallAgentParsed | null {
30
31
 
31
32
  function TypingDots() {
32
33
  return (
33
- <div className="flex items-center gap-1 px-0.5 py-0.5" aria-label="Typing">
34
+ <div className="flex items-center gap-1 px-0.5 py-0.5" role="status" aria-label="Typing">
34
35
  {[0, 1, 2].map((i) => (
35
36
  <span key={i} className="size-1.5 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: `${i * 150}ms` }} />
36
37
  ))}
@@ -121,7 +122,7 @@ export function CallAgentToolUI({ msg, assistantLabel = "Assistant", assistantCo
121
122
  <div className={cn("overflow-hidden rounded-xl border bg-card", failed ? "border-destructive/35" : "border-border")}>
122
123
  <div className="flex items-center gap-2 border-b border-border-subtle px-3 py-2">
123
124
  <FluentIcon name="chat-24" size={13} className="shrink-0 text-muted-foreground" />
124
- <span className="min-w-0 flex-1 truncate text-[14px] font-medium text-foreground">Calling {calleeName}</span>
125
+ <Shimmer active={composing || awaitingReply} className="min-w-0 flex-1 truncate text-[14px] font-medium text-foreground">Calling {calleeName}</Shimmer>
125
126
  {bgRunning ? <span className="text-[14px] tabular-nums text-muted-foreground">{formatBgElapsed(timestampMs(msg.timestamp), now)}</span> : null}
126
127
  {statusLabel ? <span className="max-w-40 truncate text-[13px] italic text-destructive">{statusLabel}</span> : null}
127
128
  <ToolUiTrailing running={composing || awaitingReply} failed={failed && !awaitingReply} />
@@ -1,5 +1,6 @@
1
1
  import { type ReactNode, useState } from "react";
2
2
  import { cn } from "../../lib/cn";
3
+ import { Shimmer } from "../../shimmer/Shimmer";
3
4
  import { formatToolName, hasMeaningfulInput, prettyJson } from "../common/utils";
4
5
  import { ChatSpinner } from "../message-ui/ChatSpinner";
5
6
  import { ToolUiTrailing } from "./ToolUiTrailing";
@@ -37,7 +38,7 @@ export function ChatToolCall({ toolName = "Tool", label, toolInput, toolOutput,
37
38
  const header = (
38
39
  <>
39
40
  {identityIcon}
40
- <span className={cn("min-w-0 flex-1 truncate text-left text-[14px] font-medium", running ? "nonla-chat-shimmer" : "text-muted-foreground", expandable && !running && "transition-colors group-hover:text-foreground")}>{displayLabel}</span>
41
+ <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>
41
42
  <ToolUiTrailing running={running} failed={hasError} chevron={expandable} chevronClassName={cn("group-hover:opacity-100", open && "opacity-100 rotate-90")} />
42
43
  </>
43
44
  );
@@ -60,7 +61,7 @@ export function ChatToolCall({ toolName = "Tool", label, toolInput, toolOutput,
60
61
  {running ? (
61
62
  <div className="flex items-center gap-2 border-t border-border bg-muted/40 px-3 py-2 text-muted-foreground">
62
63
  <ChatSpinner />
63
- <span className="italic">Running…</span>
64
+ <Shimmer className="italic">Running…</Shimmer>
64
65
  </div>
65
66
  ) : null}
66
67
  {!isPending ? (
@@ -1,5 +1,6 @@
1
1
  import { FluentIcon } from "../../icon/FluentIcon";
2
2
  import { parseJsonObject } from "../common/utils";
3
+ import { Shimmer } from "../../shimmer/Shimmer";
3
4
  import { ToolUiBadge } from "./ToolUiBadge";
4
5
  import { ToolUiTrailing } from "./ToolUiTrailing";
5
6
  import { isToolRunning, type ToolUIProps } from "./types";
@@ -40,7 +41,7 @@ export function GetCurrentTimeToolUI({ msg, assistantLabel = "Assistant", assist
40
41
  <div className="px-4 pb-1">
41
42
  <div className="flex items-center gap-2 py-1">
42
43
  <FluentIcon name="clock-24" size={13} className="shrink-0 text-muted-foreground" />
43
- <span className="truncate text-[14px] font-medium tabular-nums text-muted-foreground">{hasError ? "Failed to get time" : running ? "Getting time…" : label}</span>
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>
44
45
  <ToolUiTrailing running={running} failed={hasError} />
45
46
  </div>
46
47
  </div>
@@ -1,5 +1,6 @@
1
1
  import { FluentIcon } from "../../icon/FluentIcon";
2
2
  import { cn } from "../../lib/cn";
3
+ import { Shimmer } from "../../shimmer/Shimmer";
3
4
  import { parseJsonObject } from "../common/utils";
4
5
  import { ToolUiBadge } from "./ToolUiBadge";
5
6
  import { ToolUiTrailing } from "./ToolUiTrailing";
@@ -55,10 +56,10 @@ export function ReadSkillToolUI({ msg, assistantLabel = "Assistant", assistantCo
55
56
  <details className="group/readskill px-4 pb-2" style={{ overflowAnchor: "none" }}>
56
57
  <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">
57
58
  <FluentIcon name={reference ? "document-text-24" : "book-24"} size={13} className="shrink-0 text-muted-foreground" />
58
- <span className={cn("min-w-0 truncate font-medium text-muted-foreground", running && "nonla-chat-shimmer")}>
59
+ <Shimmer active={running} className="min-w-0 truncate font-medium text-muted-foreground">
59
60
  {verb}
60
61
  {targetLabel ? <span className="font-normal text-tertiary-foreground"> {targetLabel}</span> : null}
61
- </span>
62
+ </Shimmer>
62
63
  <ToolUiTrailing running={running} failed={failed} chevron chevronClassName="group-hover/readskill:opacity-100 group-open/readskill:opacity-100 group-open/readskill:rotate-90" />
63
64
  </summary>
64
65
  {body ? (
@@ -1,6 +1,7 @@
1
1
  import { FluentIcon } from "../../icon/FluentIcon";
2
2
  import { cn } from "../../lib/cn";
3
3
  import { CodeBlock } from "../../codeblock/CodeBlock";
4
+ import { Shimmer } from "../../shimmer/Shimmer";
4
5
  import { parseBgTaskRef } from "../common/bgTasks";
5
6
  import { parseJsonObject, prettyJson } from "../common/utils";
6
7
  import { ChatSpinner } from "../message-ui/ChatSpinner";
@@ -46,7 +47,7 @@ function SandboxOutput({
46
47
  return (
47
48
  <div className="flex items-center gap-2 border-t border-border px-3 py-2 text-[13px] text-muted-foreground">
48
49
  <ChatSpinner />
49
- <span>Running…</span>
50
+ <Shimmer>Running…</Shimmer>
50
51
  </div>
51
52
  );
52
53
  }
@@ -87,7 +88,7 @@ export function RunJsToolUI({ msg, assistantLabel = "Assistant", assistantColor,
87
88
  <details className="group/runjs px-4 pb-2" style={{ overflowAnchor: "none" }}>
88
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">
89
90
  <FluentIcon name="code-24" size={13} className="shrink-0 text-muted-foreground" />
90
- <span className={cn("min-w-0 truncate font-medium text-muted-foreground", running && "nonla-chat-shimmer")}>{verb}</span>
91
+ <Shimmer active={running} className="min-w-0 truncate font-medium text-muted-foreground">{verb}</Shimmer>
91
92
  <ToolUiTrailing running={running} failed={failed} chevron chevronClassName="group-hover/runjs:opacity-100 group-open/runjs:opacity-100 group-open/runjs:rotate-90" />
92
93
  </summary>
93
94
 
@@ -1,5 +1,6 @@
1
1
  import { FluentIcon } from "../../icon/FluentIcon";
2
2
  import { cn } from "../../lib/cn";
3
+ import { Shimmer } from "../../shimmer/Shimmer";
3
4
  import { parseJsonObject } from "../common/utils";
4
5
  import { ToolUiBadge } from "./ToolUiBadge";
5
6
  import { ToolUiTrailing } from "./ToolUiTrailing";
@@ -39,9 +40,9 @@ export function WebFetchToolUI({ msg, assistantLabel = "Assistant", assistantCol
39
40
  <details className="group/webfetch px-4 pb-2" style={{ overflowAnchor: "none" }}>
40
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">
41
42
  <FluentIcon name="globe-24" size={13} className="shrink-0 text-muted-foreground" />
42
- <span className={cn("min-w-0 truncate font-medium text-muted-foreground", running && "nonla-chat-shimmer")}>
43
+ <Shimmer active={running} className="min-w-0 truncate font-medium text-muted-foreground">
43
44
  {verb} <span className="font-normal text-tertiary-foreground">{url}</span>
44
- </span>
45
+ </Shimmer>
45
46
  <ToolUiTrailing running={running} failed={failed} chevron chevronClassName="group-hover/webfetch:opacity-100 group-open/webfetch:opacity-100 group-open/webfetch:rotate-90" />
46
47
  </summary>
47
48
  {body ? (
@@ -1,5 +1,6 @@
1
1
  import type { ComponentType } from "react";
2
- import { isCallAgentToolName } from "../common/utils";
2
+ import type { AgentToolNameMatch } from "../common/types";
3
+ import { isCallAgentToolName, matchesToolName } from "../common/utils";
3
4
  import { CallAgentToolUI } from "./CallAgentToolUI";
4
5
  import { GetCurrentTimeToolUI } from "./GetCurrentTimeToolUI";
5
6
  import { ReadSkillToolUI } from "./ReadSkillToolUI";
@@ -7,23 +8,34 @@ import { RunJsToolUI } from "./RunJsToolUI";
7
8
  import type { ToolUIProps } from "./types";
8
9
  import { WebFetchToolUI } from "./WebFetchToolUI";
9
10
 
10
- type ToolUIEntry = {
11
- match: (toolName: string) => boolean;
11
+ /** Exact name, aliases, or a predicate (e.g. `call_agent__*`). */
12
+ export type AgentToolUIName = AgentToolNameMatch;
13
+
14
+ export type AgentToolUI = {
15
+ name: AgentToolUIName;
12
16
  component: ComponentType<ToolUIProps>;
13
17
  };
14
18
 
15
- const TOOL_UIS: ToolUIEntry[] = [
16
- { match: isCallAgentToolName, component: CallAgentToolUI },
17
- { match: (n) => n === "web_fetch" || n === "fetch_url" || n === "browser", component: WebFetchToolUI },
18
- { match: (n) => n === "get_current_time", component: GetCurrentTimeToolUI },
19
- { match: (n) => n === "read_skill", component: ReadSkillToolUI },
20
- { match: (n) => n === "run_js", component: RunJsToolUI },
19
+ export function matchesToolUIName(name: AgentToolUIName, toolName: string): boolean {
20
+ return matchesToolName(name, toolName);
21
+ }
22
+
23
+ export const builtinToolUis: AgentToolUI[] = [
24
+ { name: isCallAgentToolName, component: CallAgentToolUI },
25
+ { name: ["web_fetch", "fetch_url", "browser"], component: WebFetchToolUI },
26
+ { name: "get_current_time", component: GetCurrentTimeToolUI },
27
+ { name: "read_skill", component: ReadSkillToolUI },
28
+ { name: "run_js", component: RunJsToolUI },
21
29
  ];
22
30
 
23
- export function resolveToolUI(toolName: string | null | undefined): ComponentType<ToolUIProps> | null {
31
+ /** App `toolUis` win on the same name; builtins are the fallback. */
32
+ export function resolveToolUI(toolName: string | null | undefined, extras?: readonly AgentToolUI[] | null): ComponentType<ToolUIProps> | null {
24
33
  if (!toolName) return null;
25
- for (const entry of TOOL_UIS) {
26
- if (entry.match(toolName)) return entry.component;
34
+ const lists = extras?.length ? [extras, builtinToolUis] : [builtinToolUis];
35
+ for (const list of lists) {
36
+ for (const entry of list) {
37
+ if (matchesToolUIName(entry.name, toolName)) return entry.component;
38
+ }
27
39
  }
28
40
  return null;
29
41
  }
@@ -5,7 +5,7 @@ import { cn } from "../lib/cn";
5
5
  export type CheckboxProps = Omit<ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>, "onCheckedChange" | "checked" | "onChange"> & {
6
6
  checked?: boolean | "indeterminate";
7
7
  defaultChecked?: boolean;
8
- /** antd-style — receives next checked boolean (not a DOM event). */
8
+ /** Receives next checked boolean (not a DOM event). */
9
9
  onChange?: (checked: boolean) => void;
10
10
  children?: ReactNode;
11
11
  indeterminate?: boolean;
@@ -25,6 +25,7 @@ export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(function Ch
25
25
  ) {
26
26
  const resolved = indeterminate ? "indeterminate" : checked;
27
27
  return (
28
+ // biome-ignore lint/a11y/noLabelWithoutControl: wraps Radix checkbox button, not a native input
28
29
  <label className={cn("inline-flex items-center gap-2 text-sm text-foreground cursor-pointer", disabled && "opacity-45 cursor-not-allowed")}>
29
30
  <CheckboxPrimitive.Root
30
31
  ref={ref}
@@ -33,9 +34,9 @@ export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(function Ch
33
34
  disabled={disabled}
34
35
  onCheckedChange={(v) => onChange?.(v === true)}
35
36
  className={cn(
36
- "size-4 shrink-0 rounded-[4px] border border-input bg-[var(--control-bg)] transition-colors duration-150 ease-[cubic-bezier(0.16,1,0.3,1)]",
37
- "focus-visible:outline-none data-[state=checked]:bg-brand data-[state=checked]:border-brand data-[state=checked]:text-[var(--nonla-solid-fg)]",
38
- "data-[state=indeterminate]:bg-brand data-[state=indeterminate]:border-brand data-[state=indeterminate]:text-[var(--nonla-solid-fg)]",
37
+ "size-4 shrink-0 rounded-sm border border-input bg-(--control-bg) transition-colors duration-150 ease-[cubic-bezier(0.16,1,0.3,1)]",
38
+ "focus-visible:outline-none data-[state=checked]:bg-brand data-[state=checked]:border-brand data-[state=checked]:text-(--nonla-solid-fg)",
39
+ "data-[state=indeterminate]:bg-brand data-[state=indeterminate]:border-brand data-[state=indeterminate]:text-(--nonla-solid-fg)",
39
40
  className,
40
41
  )}
41
42
  {...rest}
@@ -65,6 +65,7 @@ export function CodeBlock({ code, language, title, lineNumbers = false, wordWrap
65
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
66
  <div className={cn("w-full", !wordWrap && "overflow-x-auto")}>
67
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 */}
68
69
  <code className={cn("block px-3 py-2.5", !wordWrap && "w-max min-w-full")} dangerouslySetInnerHTML={{ __html: html }} />
69
70
  </pre>
70
71
  </div>
@@ -16,8 +16,9 @@ import {
16
16
  import type { DateRange } from "react-day-picker";
17
17
  import { Calendar } from "../calendar/Calendar";
18
18
  import { Button } from "../button/Button";
19
+ import { usePopupContainer } from "../app/context";
19
20
  import { cn } from "../lib/cn";
20
- import { type ControlSize, controlFieldFocusBorder, controlFieldStyle, controlFieldSurface, controlFieldTransition, controlStatusClass, getSizeTokens } from "../lib/sizes";
21
+ import { type ControlSize, controlFieldFocusBorder, controlFieldStyle, controlFieldSurface, controlFieldTransition, controlStatusClass, getSizeTokens, useControlSize } from "../lib/sizes";
21
22
  import { glassOverlayClass } from "../lib/surface";
22
23
 
23
24
  export type DatePickerProps = {
@@ -29,7 +30,7 @@ export type DatePickerProps = {
29
30
  allowClear?: boolean;
30
31
  format?: string;
31
32
  showTime?: boolean;
32
- /** Keep the panel open until OK (antd). */
33
+ /** Keep the panel open until OK. */
33
34
  needConfirm?: boolean;
34
35
  placeholder?: string;
35
36
  disabled?: boolean;
@@ -127,6 +128,7 @@ const TriggerChrome = forwardRef<
127
128
  children: ReactNode;
128
129
  } & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children" | "style">
129
130
  >(function TriggerChrome({ size, status, disabled, className, style, icon, clear, children, ...rest }, ref) {
131
+ const resolvedSize = useControlSize(size);
130
132
  return (
131
133
  <button
132
134
  ref={ref}
@@ -141,7 +143,7 @@ const TriggerChrome = forwardRef<
141
143
  controlStatusClass(status),
142
144
  className,
143
145
  )}
144
- style={{ ...controlFieldStyle(size), ...style }}
146
+ style={{ ...controlFieldStyle(resolvedSize), ...style }}
145
147
  {...rest}
146
148
  >
147
149
  {icon}
@@ -185,7 +187,9 @@ const DatePickerRoot = forwardRef<HTMLButtonElement, DatePickerProps>(function D
185
187
  };
186
188
  const [month, setMonth] = useState<Date>(() => selected ?? new Date());
187
189
  const [draft, setDraft] = useState<Date | undefined>(selected);
188
- const tok = getSizeTokens(size);
190
+ const resolvedSize = useControlSize(size);
191
+ const tok = getSizeTokens(resolvedSize);
192
+ const portal = usePopupContainer()?.();
189
193
  const confirm = Boolean(needConfirm);
190
194
 
191
195
  useEffect(() => {
@@ -253,7 +257,7 @@ const DatePickerRoot = forwardRef<HTMLButtonElement, DatePickerProps>(function D
253
257
  <PopoverPrimitive.Trigger asChild>
254
258
  <TriggerChrome
255
259
  ref={ref}
256
- size={size}
260
+ size={resolvedSize}
257
261
  status={status}
258
262
  disabled={disabled}
259
263
  className={className}
@@ -277,7 +281,7 @@ const DatePickerRoot = forwardRef<HTMLButtonElement, DatePickerProps>(function D
277
281
  <span className={cn(!selected && "text-quaternary-foreground")}>{selected ? label : placeholder}</span>
278
282
  </TriggerChrome>
279
283
  </PopoverPrimitive.Trigger>
280
- <PopoverPrimitive.Portal>
284
+ <PopoverPrimitive.Portal container={portal}>
281
285
  <PopoverPrimitive.Content
282
286
  align="start"
283
287
  sideOffset={6}
@@ -351,7 +355,9 @@ const RangePicker = forwardRef<HTMLButtonElement, RangePickerProps>(function Ran
351
355
  const selected = controlled ? parseRange(value) : inner;
352
356
  const [open, setOpen] = useState(false);
353
357
  const [month, setMonth] = useState<Date>(() => selected?.from ?? new Date());
354
- const tok = getSizeTokens(size);
358
+ const resolvedSize = useControlSize(size);
359
+ const tok = getSizeTokens(resolvedSize);
360
+ const portal = usePopupContainer()?.();
355
361
 
356
362
  useEffect(() => {
357
363
  if (selected?.from) setMonth(selected.from);
@@ -390,7 +396,7 @@ const RangePicker = forwardRef<HTMLButtonElement, RangePickerProps>(function Ran
390
396
  <PopoverPrimitive.Trigger asChild>
391
397
  <TriggerChrome
392
398
  ref={ref}
393
- size={size}
399
+ size={resolvedSize}
394
400
  status={status}
395
401
  disabled={disabled}
396
402
  className={className}
@@ -418,7 +424,7 @@ const RangePicker = forwardRef<HTMLButtonElement, RangePickerProps>(function Ran
418
424
  </span>
419
425
  </TriggerChrome>
420
426
  </PopoverPrimitive.Trigger>
421
- <PopoverPrimitive.Portal>
427
+ <PopoverPrimitive.Portal container={portal}>
422
428
  <PopoverPrimitive.Content
423
429
  align="start"
424
430
  sideOffset={6}
@@ -78,7 +78,7 @@ export function DesktopHeader({
78
78
  profile?: ReactNode;
79
79
  }) {
80
80
  return (
81
- <header className="fixed inset-x-0 top-0 z-40 flex h-desktop-bar items-center justify-between gap-3 border-0 bg-glass-bar px-3 backdrop-blur-lg">
81
+ <header className="nonla-header-layer fixed inset-x-0 top-0 flex h-desktop-bar items-center justify-between gap-3 border-0 bg-glass-bar px-3 backdrop-blur-lg">
82
82
  <div className="flex min-w-0 items-center">
83
83
  {logo}
84
84
  {leading}
@@ -6,9 +6,7 @@ const iconButtonClass =
6
6
 
7
7
  const labelClass = "text-xs leading-[18px] font-semibold text-center line-clamp-2 w-full";
8
8
 
9
- function plateClass(active: boolean) {
10
- return `w-11 h-11 flex items-center justify-center filter-[drop-shadow(0_1px_2px_rgba(0,0,0,0.45))] ${active ? "ring-2 ring-brand rounded-[10px]" : ""}`;
11
- }
9
+ const plateClass = "nonla-window-glass nonla-desktop-icon-plate w-11 h-11 flex items-center justify-center";
12
10
 
13
11
  export const DesktopIcon = forwardRef<
14
12
  HTMLButtonElement,
@@ -24,7 +22,7 @@ export const DesktopIcon = forwardRef<
24
22
  return (
25
23
  <button ref={ref} type="button" title={label} aria-label={label} aria-current={active ? "true" : undefined} dir="ltr" onClick={onClick} className={iconButtonClass}>
26
24
  {media ?? (
27
- <span className={plateClass(active)}>
25
+ <span className={plateClass}>
28
26
  <FluentIcon name={icon ?? "sparkle-24"} size={32} />
29
27
  </span>
30
28
  )}
@@ -1,6 +1,7 @@
1
1
  import { type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode, createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import { cn } from "../lib/cn";
4
+ import { OverlayScroll, type OverlayScrollVisibility } from "../scroll/OverlayScroll";
4
5
  import { glassSurfaceClass } from "../lib/surface";
5
6
 
6
7
  type Phase = "open" | "leaving";
@@ -211,12 +212,17 @@ export function DesktopWindow({
211
212
  expanded,
212
213
  onClose,
213
214
  onToggleExpand,
215
+ scroll = true,
216
+ scrollbar = "hover",
214
217
  children,
215
218
  }: {
216
219
  title?: ReactNode;
217
220
  expanded: boolean;
218
221
  onClose: () => void;
219
222
  onToggleExpand: () => void;
223
+ /** Overlay auto-hide scrollbar on the body. Pass `false` when children manage their own scroll (split panes, AgentPanel). */
224
+ scroll?: boolean;
225
+ scrollbar?: OverlayScrollVisibility;
220
226
  children: ReactNode;
221
227
  }) {
222
228
  const overlayRef = useRef<HTMLDivElement>(null);
@@ -444,7 +450,7 @@ export function DesktopWindow({
444
450
 
445
451
  return (
446
452
  <WindowHeaderSlotContext.Provider value={headerChrome}>
447
- <div ref={overlayRef} className="pointer-events-none absolute inset-x-0 bottom-0 top-desktop-bar z-30">
453
+ <div ref={overlayRef} className="nonla-window-layer pointer-events-none absolute inset-x-0 bottom-0 top-desktop-bar">
448
454
  <section
449
455
  ref={frameRef}
450
456
  aria-label={typeof title === "string" && title ? title : "Window"}
@@ -464,16 +470,16 @@ export function DesktopWindow({
464
470
  opacity: visible ? 1 : 0,
465
471
  transform: visible ? "scale(1)" : "scale(0.18)",
466
472
  transition: [geom, fade].filter(Boolean).join(", ") || undefined,
467
- ["--glass" as string]: "color-mix(in srgb, white 72%, transparent)",
468
473
  }}
469
474
  data-expanded={expanded || undefined}
470
- className={cn("absolute flex flex-col rounded-xl pointer-events-auto transform-gpu", glassSurfaceClass, leaving && "pointer-events-none")}
475
+ className={cn("absolute flex flex-col rounded-xl pointer-events-auto transform-gpu", glassSurfaceClass, "nonla-window-glass", leaving && "pointer-events-none")}
471
476
  >
472
477
  <div className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl">
478
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: title bar is a pointer drag surface */}
473
479
  <header
474
480
  onPointerDown={startDrag}
475
481
  onDoubleClick={onTitleBarDoubleClick}
476
- className="flex h-8 shrink-0 cursor-default items-center gap-4 border-0 border-b border-solid border-glass-border px-3 select-none touch-none"
482
+ className="flex h-8 shrink-0 cursor-default items-center gap-4 border-0 border-b border-solid border-ink-line px-3 select-none touch-none"
477
483
  >
478
484
  <div className="group/traffic flex shrink-0 items-center gap-2">
479
485
  <TrafficLight label="Close window" tone="close" onClick={requestClose} shortcut="Esc">
@@ -489,7 +495,15 @@ export function DesktopWindow({
489
495
  </div>
490
496
  </header>
491
497
 
492
- <div className="min-h-0 flex-1 overflow-hidden">{children}</div>
498
+ <div className="min-h-0 flex-1 overflow-hidden">
499
+ {scroll ? (
500
+ <OverlayScroll visibility={scrollbar} className="h-full">
501
+ {children}
502
+ </OverlayScroll>
503
+ ) : (
504
+ children
505
+ )}
506
+ </div>
493
507
  </div>
494
508
 
495
509
  {!expanded && !leaving
@@ -5,8 +5,8 @@ export function MeadowDesktop({ children, src }: { children?: ReactNode; src?: s
5
5
  return (
6
6
  <div className="absolute inset-0 bg-meadow">
7
7
  <MeadowWallpaper src={src} />
8
- <div className="absolute inset-x-0 bottom-0 top-desktop-bar z-20">
9
- <nav aria-label="Desktop" className="absolute inset-0 overflow-auto px-3 pt-8 pb-3">
8
+ <div className="nonla-desktop-layer absolute inset-x-0 bottom-0 top-desktop-bar">
9
+ <nav aria-label="Desktop" className="absolute inset-0 overflow-auto pl-2 pr-3 pt-2 pb-3">
10
10
  <div className="flex h-full flex-col flex-wrap content-start gap-x-2 gap-y-3">{children}</div>
11
11
  </nav>
12
12
  </div>
@@ -1,5 +1,6 @@
1
1
  import * as Dialog from "@radix-ui/react-dialog";
2
2
  import { type AnimationEvent, type CSSProperties, type ReactNode, useEffect, useRef, useState } from "react";
3
+ import { usePopupContainer } from "../app/context";
3
4
  import { cn } from "../lib/cn";
4
5
  import { glassSurfaceClass } from "../lib/surface";
5
6
 
@@ -17,7 +18,7 @@ export type DrawerProps = {
17
18
  extra?: ReactNode;
18
19
  children?: ReactNode;
19
20
  width?: number | string;
20
- /** antd: width for left/right, height for top/bottom. */
21
+ /** Width for left/right, height for top/bottom. */
21
22
  size?: number | string;
22
23
  placement?: DrawerPlacement;
23
24
  destroyOnClose?: boolean;
@@ -66,6 +67,7 @@ export function Drawer({
66
67
  }, [isOpen, shouldDestroy]);
67
68
 
68
69
  const showBody = !shouldDestroy || present;
70
+ const portal = usePopupContainer()?.();
69
71
 
70
72
  const onContentAnimationEnd = (e: AnimationEvent<HTMLDivElement>) => {
71
73
  if (e.target !== e.currentTarget) return;
@@ -80,17 +82,17 @@ export function Drawer({
80
82
  if (!next) onClose?.();
81
83
  }}
82
84
  >
83
- <Dialog.Portal>
84
- <Dialog.Overlay className="nonla-overlay z-[9980]" />
85
+ <Dialog.Portal container={portal}>
86
+ <Dialog.Overlay className="nonla-overlay" />
85
87
  <Dialog.Content
86
88
  data-side={placement}
87
89
  className={cn(
88
- "nonla-drawer-panel fixed z-[9981] flex flex-col outline-none",
90
+ "nonla-drawer-panel fixed flex flex-col outline-none",
89
91
  glassSurfaceClass,
90
92
  placement === "right" && "top-0 right-0 bottom-0 max-w-[100vw]",
91
93
  placement === "left" && "top-0 left-0 bottom-0 max-w-[100vw]",
92
- placement === "bottom" && "right-0 bottom-0 left-0 max-h-[100vh]",
93
- placement === "top" && "top-0 right-0 left-0 max-h-[100vh]",
94
+ placement === "bottom" && "right-0 bottom-0 left-0 max-h-screen",
95
+ placement === "top" && "top-0 right-0 left-0 max-h-screen",
94
96
  className,
95
97
  )}
96
98
  style={{