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.
- package/README.md +3 -3
- package/package.json +2 -1
- package/src/app/App.tsx +4 -4
- package/src/button/Button.tsx +1 -1
- package/src/button/ButtonCopy.tsx +11 -21
- package/src/calendar/Calendar.tsx +1 -1
- package/src/chat/AgentPanel.tsx +3 -3
- package/src/chat/index.ts +35 -51
- package/src/chat/message-ui/ChatError.tsx +2 -4
- package/src/chat/message-ui/ChatInput.tsx +5 -9
- package/src/chat/message-ui/ChatThinking.tsx +2 -3
- package/src/chat/message-ui/ChatUserMessage.tsx +4 -5
- package/src/chat/message-ui/ChatWelcome.tsx +3 -8
- package/src/chat/message-ui/MermaidBlock.tsx +7 -47
- package/src/chat/tool-ui/BackgroundTaskToolUI.tsx +8 -11
- package/src/chat/tool-ui/BackgroundTasksBar.tsx +13 -14
- package/src/chat/tool-ui/CallAgentToolUI.tsx +10 -4
- package/src/chat/tool-ui/ChatToolCall.tsx +11 -17
- package/src/chat/tool-ui/GetCurrentTimeToolUI.tsx +2 -2
- package/src/chat/tool-ui/ReadSkillToolUI.tsx +3 -3
- package/src/chat/tool-ui/RunJsToolUI.tsx +2 -2
- package/src/chat/tool-ui/ToolUiTrailing.tsx +3 -7
- package/src/chat/tool-ui/WebFetchToolUI.tsx +22 -13
- package/src/checkbox/Checkbox.tsx +1 -1
- package/src/codeblock/CodeBlock.tsx +17 -19
- package/src/colorpicker/ColorPicker.tsx +7 -7
- package/src/datepicker/DatePicker.tsx +3 -3
- package/src/desktop/DesktopHeader.tsx +15 -20
- package/src/desktop/DesktopIcon.tsx +1 -1
- package/src/desktop/DesktopWindow.tsx +54 -30
- package/src/form/Form.tsx +1 -1
- package/src/form/FormItem.tsx +2 -2
- package/src/form/index.ts +23 -24
- package/src/index.ts +167 -199
- package/src/input/Input.tsx +14 -12
- package/src/input/SearchInput.tsx +5 -2
- package/src/lib/sizes.ts +1 -1
- package/src/modal/Modal.tsx +3 -0
- package/src/pagination/Pagination.tsx +528 -51
- package/src/scroll/OverlayScroll.tsx +58 -37
- package/src/select/Select.tsx +2 -2
- package/src/spin/Spin.tsx +14 -17
- package/src/splitter/Splitter.tsx +3 -3
- package/src/styles.css +13 -3
- package/src/switch/Switch.tsx +1 -1
- package/src/table/Table.tsx +43 -18
- 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
|
-
/**
|
|
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
|
|
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-
|
|
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=
|
|
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
|
|
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="
|
|
60
|
-
{hasInput ? <pre className="m-0 max-h-27.5 overflow-y-auto bg-
|
|
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="
|
|
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-
|
|
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
|
|
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
|
|
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-[
|
|
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-[
|
|
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 {
|
|
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 <
|
|
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
|
|
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
|
-
|
|
41
|
-
<
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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>
|
|
@@ -2,10 +2,10 @@ import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
|
2
2
|
import {
|
|
3
3
|
type CSSProperties,
|
|
4
4
|
type FC,
|
|
5
|
+
isValidElement,
|
|
5
6
|
type KeyboardEvent,
|
|
6
|
-
type PointerEvent as ReactPointerEvent,
|
|
7
7
|
type ReactNode,
|
|
8
|
-
|
|
8
|
+
type PointerEvent as ReactPointerEvent,
|
|
9
9
|
useEffect,
|
|
10
10
|
useRef,
|
|
11
11
|
useState,
|
|
@@ -27,17 +27,17 @@ import {
|
|
|
27
27
|
import { glassOverlayClass } from "../lib/surface";
|
|
28
28
|
import {
|
|
29
29
|
Color,
|
|
30
|
-
DEFAULT_COLOR,
|
|
31
|
-
clamp,
|
|
32
|
-
hsbaToRgba,
|
|
33
|
-
parseColor,
|
|
34
30
|
type ColorFormat,
|
|
35
31
|
type ColorType,
|
|
32
|
+
clamp,
|
|
33
|
+
DEFAULT_COLOR,
|
|
36
34
|
type HsbaColor,
|
|
35
|
+
hsbaToRgba,
|
|
36
|
+
parseColor,
|
|
37
37
|
} from "./color";
|
|
38
38
|
|
|
39
|
-
export { Color, DEFAULT_COLOR, parseColor };
|
|
40
39
|
export type { ColorFormat, ColorType, HsbaColor };
|
|
40
|
+
export { Color, DEFAULT_COLOR, parseColor };
|
|
41
41
|
|
|
42
42
|
export type PresetColorType = {
|
|
43
43
|
label: ReactNode;
|
|
@@ -4,19 +4,19 @@ import {
|
|
|
4
4
|
type ButtonHTMLAttributes,
|
|
5
5
|
type CSSProperties,
|
|
6
6
|
type ForwardRefExoticComponent,
|
|
7
|
+
forwardRef,
|
|
7
8
|
type MouseEvent,
|
|
8
9
|
type ReactElement,
|
|
9
10
|
type ReactNode,
|
|
10
11
|
type RefAttributes,
|
|
11
|
-
forwardRef,
|
|
12
12
|
useEffect,
|
|
13
13
|
useMemo,
|
|
14
14
|
useState,
|
|
15
15
|
} from "react";
|
|
16
16
|
import type { DateRange } from "react-day-picker";
|
|
17
|
-
import { Calendar } from "../calendar/Calendar";
|
|
18
|
-
import { Button } from "../button/Button";
|
|
19
17
|
import { usePopupContainer } from "../app/context";
|
|
18
|
+
import { Button } from "../button/Button";
|
|
19
|
+
import { Calendar } from "../calendar/Calendar";
|
|
20
20
|
import { cn } from "../lib/cn";
|
|
21
21
|
import { type ControlSize, controlFieldFocusBorder, controlFieldStyle, controlFieldSurface, controlFieldTransition, controlStatusClass, getSizeTokens, useControlSize } from "../lib/sizes";
|
|
22
22
|
import { glassOverlayClass } from "../lib/surface";
|
|
@@ -66,27 +66,22 @@ export function DesktopBarDivider({ className }: { className?: string }) {
|
|
|
66
66
|
return <span className={cn("mx-2 h-4 w-px bg-ink-line", className)} aria-hidden />;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
export
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
leading?: ReactNode;
|
|
77
|
-
trailing?: ReactNode;
|
|
78
|
-
profile?: ReactNode;
|
|
79
|
-
}) {
|
|
69
|
+
export type DesktopHeaderProps = {
|
|
70
|
+
left?: ReactNode;
|
|
71
|
+
right?: ReactNode;
|
|
72
|
+
className?: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export function DesktopHeader({ left, right, className }: DesktopHeaderProps) {
|
|
80
76
|
return (
|
|
81
|
-
<header
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
</div>
|
|
77
|
+
<header
|
|
78
|
+
className={cn(
|
|
79
|
+
"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",
|
|
80
|
+
className,
|
|
81
|
+
)}
|
|
82
|
+
>
|
|
83
|
+
<div className="flex min-w-0 items-center">{left}</div>
|
|
84
|
+
<div className="flex min-w-0 shrink-0 items-center">{right}</div>
|
|
90
85
|
</header>
|
|
91
86
|
);
|
|
92
87
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent,
|
|
1
|
+
import { createContext, type MouseEvent as ReactMouseEvent, type ReactNode, type PointerEvent as ReactPointerEvent, 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";
|
|
5
4
|
import { glassSurfaceClass } from "../lib/surface";
|
|
5
|
+
import { OverlayScroll, type OverlayScrollVisibility } from "../scroll/OverlayScroll";
|
|
6
6
|
|
|
7
7
|
type Phase = "open" | "leaving";
|
|
8
8
|
type Size = { w: number; h: number };
|
|
@@ -37,6 +37,10 @@ function isEditableTarget(target: EventTarget | null) {
|
|
|
37
37
|
return Boolean(target.closest(".monaco-editor"));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function hasOpenDialog() {
|
|
41
|
+
return Boolean(document.querySelector('[role="dialog"][data-state="open"], .nonla-modal-content'));
|
|
42
|
+
}
|
|
43
|
+
|
|
40
44
|
function originFromActiveIcon(overlay: HTMLElement, frame: HTMLElement) {
|
|
41
45
|
const source = document.querySelector('[aria-current="true"]');
|
|
42
46
|
if (!(source instanceof HTMLElement)) return "50% 50%";
|
|
@@ -190,33 +194,33 @@ function TrafficLight({
|
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
type WindowHeaderSlot = {
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
left: HTMLElement | null;
|
|
198
|
+
right: HTMLElement | null;
|
|
195
199
|
};
|
|
196
200
|
|
|
197
201
|
const WindowHeaderSlotContext = createContext<WindowHeaderSlot | null>(null);
|
|
198
202
|
|
|
199
|
-
export
|
|
203
|
+
export type WindowHeaderProps = {
|
|
204
|
+
left?: ReactNode;
|
|
205
|
+
right?: ReactNode;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/** Injects into the window title bar from a child. Prefer `DesktopWindow` `left` / `right` when the parent can pass them. */
|
|
209
|
+
export function WindowHeader({ left, right }: WindowHeaderProps) {
|
|
200
210
|
const ctx = useContext(WindowHeaderSlotContext);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
211
|
+
if (!ctx) return null;
|
|
212
|
+
return (
|
|
213
|
+
<>
|
|
214
|
+
{ctx.left && left != null ? createPortal(left, ctx.left) : null}
|
|
215
|
+
{ctx.right && right != null ? createPortal(right, ctx.right) : null}
|
|
216
|
+
</>
|
|
217
|
+
);
|
|
208
218
|
}
|
|
209
219
|
|
|
210
|
-
export
|
|
211
|
-
title,
|
|
212
|
-
expanded,
|
|
213
|
-
onClose,
|
|
214
|
-
onToggleExpand,
|
|
215
|
-
scroll = true,
|
|
216
|
-
scrollbar = "hover",
|
|
217
|
-
children,
|
|
218
|
-
}: {
|
|
220
|
+
export type DesktopWindowProps = {
|
|
219
221
|
title?: ReactNode;
|
|
222
|
+
left?: ReactNode;
|
|
223
|
+
right?: ReactNode;
|
|
220
224
|
expanded: boolean;
|
|
221
225
|
onClose: () => void;
|
|
222
226
|
onToggleExpand: () => void;
|
|
@@ -224,7 +228,19 @@ export function DesktopWindow({
|
|
|
224
228
|
scroll?: boolean;
|
|
225
229
|
scrollbar?: OverlayScrollVisibility;
|
|
226
230
|
children: ReactNode;
|
|
227
|
-
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export function DesktopWindow({
|
|
234
|
+
title,
|
|
235
|
+
left,
|
|
236
|
+
right,
|
|
237
|
+
expanded,
|
|
238
|
+
onClose,
|
|
239
|
+
onToggleExpand,
|
|
240
|
+
scroll = true,
|
|
241
|
+
scrollbar = "hover",
|
|
242
|
+
children,
|
|
243
|
+
}: DesktopWindowProps) {
|
|
228
244
|
const overlayRef = useRef<HTMLDivElement>(null);
|
|
229
245
|
const frameRef = useRef<HTMLElement>(null);
|
|
230
246
|
const closedRef = useRef(false);
|
|
@@ -253,9 +269,9 @@ export function DesktopWindow({
|
|
|
253
269
|
w: window.innerWidth,
|
|
254
270
|
h: Math.max(0, window.innerHeight - 42),
|
|
255
271
|
}));
|
|
256
|
-
const [
|
|
257
|
-
const [
|
|
258
|
-
const headerChrome = useMemo(() => ({
|
|
272
|
+
const [leftSlot, setLeftSlot] = useState<HTMLDivElement | null>(null);
|
|
273
|
+
const [rightSlot, setRightSlot] = useState<HTMLDivElement | null>(null);
|
|
274
|
+
const headerChrome = useMemo(() => ({ left: leftSlot, right: rightSlot }), [leftSlot, rightSlot]);
|
|
259
275
|
const requestCloseRef = useRef<() => void>(() => {});
|
|
260
276
|
const viewRef = useRef(view);
|
|
261
277
|
const offsetRef = useRef(offset);
|
|
@@ -325,7 +341,7 @@ export function DesktopWindow({
|
|
|
325
341
|
|
|
326
342
|
useEffect(() => {
|
|
327
343
|
const onKey = (e: KeyboardEvent) => {
|
|
328
|
-
if (e.key !== "Escape" || isEditableTarget(e.target)) return;
|
|
344
|
+
if (e.key !== "Escape" || e.defaultPrevented || isEditableTarget(e.target) || hasOpenDialog()) return;
|
|
329
345
|
e.preventDefault();
|
|
330
346
|
requestCloseRef.current();
|
|
331
347
|
};
|
|
@@ -475,10 +491,8 @@ export function DesktopWindow({
|
|
|
475
491
|
className={cn("absolute flex flex-col rounded-xl pointer-events-auto transform-gpu", glassSurfaceClass, "nonla-window-glass", leaving && "pointer-events-none")}
|
|
476
492
|
>
|
|
477
493
|
<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 */}
|
|
479
494
|
<header
|
|
480
495
|
onPointerDown={startDrag}
|
|
481
|
-
onDoubleClick={onTitleBarDoubleClick}
|
|
482
496
|
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"
|
|
483
497
|
>
|
|
484
498
|
<div className="group/traffic flex shrink-0 items-center gap-2">
|
|
@@ -490,8 +504,18 @@ export function DesktopWindow({
|
|
|
490
504
|
</TrafficLight>
|
|
491
505
|
</div>
|
|
492
506
|
<div className="flex min-w-0 flex-1 items-center">
|
|
493
|
-
|
|
494
|
-
|
|
507
|
+
<div className="flex min-w-0 items-center">
|
|
508
|
+
{left}
|
|
509
|
+
<div ref={setLeftSlot} className="contents" />
|
|
510
|
+
</div>
|
|
511
|
+
{/* biome-ignore lint/a11y/noStaticElementInteractions: middle strip double-clicks to expand */}
|
|
512
|
+
<div onDoubleClick={onTitleBarDoubleClick} className="flex min-w-8 flex-1 items-center self-stretch px-2">
|
|
513
|
+
{typeof title === "string" ? <span className="min-w-0 truncate text-xs font-semibold leading-none text-foreground/90">{title}</span> : title}
|
|
514
|
+
</div>
|
|
515
|
+
<div className="flex shrink-0 items-center">
|
|
516
|
+
{right}
|
|
517
|
+
<div ref={setRightSlot} className="contents" />
|
|
518
|
+
</div>
|
|
495
519
|
</div>
|
|
496
520
|
</header>
|
|
497
521
|
|
package/src/form/Form.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { type FieldValues, FormProvider, type UseFormReturn, useWatch } from "react-hook-form";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { FormExtraContext, type FormFetcher } from "./common/context";
|
|
5
5
|
import { fieldNameOf } from "./common/rules";
|
package/src/form/FormItem.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { type ComponentType, useState } from "react";
|
|
2
|
+
import { type Control, Controller, type FieldValues } from "react-hook-form";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import { EFormItemType } from "./common/enum";
|
|
5
5
|
import { fieldNameOf, hydrateRules, isRuleRequired } from "./common/rules";
|
package/src/form/index.ts
CHANGED
|
@@ -1,35 +1,34 @@
|
|
|
1
1
|
/** Schema-driven form (react-hook-form). Prefer this name over layout `Form`. */
|
|
2
|
-
|
|
3
|
-
export type { FormProps as SchemaFormProps } from "./Form";
|
|
4
|
-
/** @deprecated use SchemaForm — kept so existing demo imports keep typechecking during migrate */
|
|
5
|
-
export { Form as FormSchema } from "./Form";
|
|
6
|
-
export { FormItem } from "./FormItem";
|
|
7
|
-
export type { FormItemProps } from "./FormItem";
|
|
8
|
-
export { EFormItemType } from "./common/enum";
|
|
9
|
-
export { hydrateRules, fieldNameOf, isRuleRequired } from "./common/rules";
|
|
2
|
+
|
|
10
3
|
export type { FormFetcher } from "./common/context";
|
|
4
|
+
export { EFormItemType } from "./common/enum";
|
|
5
|
+
export { fieldNameOf, hydrateRules, isRuleRequired } from "./common/rules";
|
|
11
6
|
export type {
|
|
12
|
-
TForm,
|
|
13
|
-
TFormItemProps,
|
|
14
|
-
TFormRule,
|
|
15
|
-
TRuleValueMessage,
|
|
16
|
-
ISelectItemProps,
|
|
17
7
|
IFormItemHelpProps,
|
|
8
|
+
ISelectItemProps,
|
|
9
|
+
TForm,
|
|
10
|
+
TFormItemCheckbox,
|
|
11
|
+
TFormItemColor,
|
|
12
|
+
TFormItemCustom,
|
|
13
|
+
TFormItemDateTime,
|
|
14
|
+
TFormItemHidden,
|
|
18
15
|
TFormItemInput,
|
|
19
|
-
TFormItemTextarea,
|
|
20
16
|
TFormItemNumber,
|
|
17
|
+
TFormItemObject,
|
|
18
|
+
TFormItemProps,
|
|
19
|
+
TFormItemRadio,
|
|
20
|
+
TFormItemRenderCtx,
|
|
21
|
+
TFormItemRepeater,
|
|
21
22
|
TFormItemSelect,
|
|
22
23
|
TFormItemSelectMultiple,
|
|
23
24
|
TFormItemSelectRemote,
|
|
24
|
-
TFormItemDateTime,
|
|
25
|
-
TFormItemTime,
|
|
26
|
-
TFormItemRepeater,
|
|
27
|
-
TFormItemObject,
|
|
28
|
-
TFormItemRadio,
|
|
29
|
-
TFormItemColor,
|
|
30
|
-
TFormItemHidden,
|
|
31
25
|
TFormItemSwitch,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
TFormItemTextarea,
|
|
27
|
+
TFormItemTime,
|
|
28
|
+
TFormRule,
|
|
29
|
+
TRuleValueMessage,
|
|
35
30
|
} from "./common/types";
|
|
31
|
+
export type { FormProps as SchemaFormProps } from "./Form";
|
|
32
|
+
export { Form as SchemaForm, Form as FormSchema } from "./Form";
|
|
33
|
+
export type { FormItemProps } from "./FormItem";
|
|
34
|
+
export { FormItem } from "./FormItem";
|