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.
- package/README.md +22 -10
- package/package.json +2 -2
- package/src/alert/Alert.tsx +1 -1
- package/src/app/App.tsx +38 -18
- package/src/app/context.ts +32 -0
- package/src/button/Button.tsx +2 -2
- package/src/calendar/Calendar.tsx +4 -4
- package/src/chat/AgentPanel.tsx +15 -6
- package/src/chat/common/types.ts +27 -2
- package/src/chat/common/useAgentStream.ts +78 -12
- package/src/chat/common/utils.ts +13 -0
- package/src/chat/index.ts +5 -3
- package/src/chat/message-ui/ChatAgentMessage.tsx +1 -1
- package/src/chat/message-ui/ChatInput.tsx +1 -1
- package/src/chat/message-ui/ChatMarkdown.tsx +6 -6
- package/src/chat/message-ui/ChatMarkdownTable.tsx +1 -1
- package/src/chat/message-ui/ChatThinking.tsx +4 -3
- package/src/chat/message-ui/ChatUserMessage.tsx +0 -8
- package/src/chat/message-ui/MermaidBlock.tsx +11 -1
- package/src/chat/tool-ui/BackgroundTaskToolUI.tsx +2 -1
- package/src/chat/tool-ui/CallAgentToolUI.tsx +3 -2
- package/src/chat/tool-ui/ChatToolCall.tsx +3 -2
- package/src/chat/tool-ui/GetCurrentTimeToolUI.tsx +2 -1
- package/src/chat/tool-ui/ReadSkillToolUI.tsx +3 -2
- package/src/chat/tool-ui/RunJsToolUI.tsx +3 -2
- package/src/chat/tool-ui/WebFetchToolUI.tsx +3 -2
- package/src/chat/tool-ui/registry.ts +24 -12
- package/src/checkbox/Checkbox.tsx +5 -4
- package/src/codeblock/CodeBlock.tsx +1 -0
- package/src/datepicker/DatePicker.tsx +15 -9
- package/src/desktop/DesktopHeader.tsx +1 -1
- package/src/desktop/DesktopIcon.tsx +2 -4
- package/src/desktop/DesktopWindow.tsx +19 -5
- package/src/desktop/MeadowDesktop.tsx +2 -2
- package/src/drawer/Drawer.tsx +8 -6
- package/src/dropdown/ContextMenu.tsx +3 -1
- package/src/dropdown/Dropdown.tsx +8 -6
- package/src/form/Form.tsx +2 -2
- package/src/form/FormItem.tsx +2 -2
- package/src/form/partials/FormItemHelps.tsx +2 -2
- package/src/form/variants/FieldColor.tsx +1 -1
- package/src/form/variants/FieldObject.tsx +2 -2
- package/src/form/variants/FieldRepeaterItem.tsx +4 -4
- package/src/form-layout/FormLayout.tsx +6 -6
- package/src/index.ts +20 -6
- package/src/input/Input.tsx +11 -10
- package/src/lib/sizes.ts +13 -3
- package/src/lib/surface.ts +2 -1
- package/src/menu/Menu.tsx +2 -2
- package/src/message/message.tsx +35 -2
- package/src/modal/Modal.tsx +49 -3
- package/src/pagination/Pagination.tsx +6 -5
- package/src/popconfirm/Popconfirm.tsx +1 -1
- package/src/popover/Popover.tsx +6 -8
- package/src/scroll/OverlayScroll.tsx +5 -3
- package/src/segmented/Segmented.tsx +6 -5
- package/src/select/Select.tsx +9 -6
- package/src/shimmer/Shimmer.tsx +17 -0
- package/src/skeleton/Skeleton.tsx +3 -3
- package/src/spin/Spin.tsx +8 -5
- package/src/styles.css +81 -27
- package/src/switch/Switch.tsx +6 -5
- package/src/table/Table.tsx +6 -7
- package/src/tag/Tag.tsx +2 -2
- package/src/theme.ts +23 -3
- package/src/timepicker/TimePicker.tsx +13 -11
- package/src/tooltip/Tooltip.tsx +6 -7
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
|
+
import { usePopupContainer } from "../app/context";
|
|
3
4
|
import { cn } from "../lib/cn";
|
|
4
5
|
import type { MenuItemType, MenuProps } from "./Dropdown";
|
|
5
6
|
import { menuContentClass, menuIconClass, menuItemClass } from "./menuClasses";
|
|
@@ -80,6 +81,7 @@ function MenuItems({
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
export function ContextMenu({ menu, children, open, onOpenChange, className, overlayClassName, disabled }: ContextMenuProps) {
|
|
84
|
+
const portal = usePopupContainer()?.();
|
|
83
85
|
if (disabled) return children;
|
|
84
86
|
|
|
85
87
|
const contentClassName = cn(menuContentClass, className, overlayClassName, menu?.className);
|
|
@@ -87,7 +89,7 @@ export function ContextMenu({ menu, children, open, onOpenChange, className, ove
|
|
|
87
89
|
return (
|
|
88
90
|
<ContextMenuPrimitive.Root modal open={open} onOpenChange={onOpenChange}>
|
|
89
91
|
<ContextMenuPrimitive.Trigger asChild>{children}</ContextMenuPrimitive.Trigger>
|
|
90
|
-
<ContextMenuPrimitive.Portal>
|
|
92
|
+
<ContextMenuPrimitive.Portal container={portal}>
|
|
91
93
|
<ContextMenuPrimitive.Content collisionPadding={8} className={contentClassName} style={menu?.style}>
|
|
92
94
|
<MenuItems items={menu?.items ?? []} onClick={menu?.onClick} contentClassName={contentClassName} />
|
|
93
95
|
</ContextMenuPrimitive.Content>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
|
2
2
|
import { type CSSProperties, type MouseEvent, type ReactNode, useState } from "react";
|
|
3
|
+
import { usePopupContainer } from "../app/context";
|
|
3
4
|
import { cn } from "../lib/cn";
|
|
4
5
|
import { type PopperPlacement, placementToRadix } from "../lib/placement";
|
|
5
6
|
import { menuContentClass, menuIconClass, menuItemClass } from "./menuClasses";
|
|
@@ -34,7 +35,7 @@ export type DropdownProps = {
|
|
|
34
35
|
className?: string;
|
|
35
36
|
overlayClassName?: string;
|
|
36
37
|
disabled?: boolean;
|
|
37
|
-
/**
|
|
38
|
+
/** Alias — accepted, no-op (Radix unmounts when closed). */
|
|
38
39
|
destroyOnHidden?: boolean;
|
|
39
40
|
classNames?: { root?: string; overlay?: string };
|
|
40
41
|
};
|
|
@@ -51,7 +52,7 @@ function itemKey(item: MenuItemType, i: number) {
|
|
|
51
52
|
return item.key ?? (item.type === "divider" ? `divider-${i}` : `item-${i}`);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
function MenuItems({ items, onClick }: { items: (MenuItemType | null | undefined)[]; onClick?: MenuProps["onClick"] }) {
|
|
55
|
+
function MenuItems({ items, onClick, portal }: { items: (MenuItemType | null | undefined)[]; onClick?: MenuProps["onClick"]; portal?: HTMLElement }) {
|
|
55
56
|
return (
|
|
56
57
|
<>
|
|
57
58
|
{items.map((item, i) => {
|
|
@@ -68,9 +69,9 @@ function MenuItems({ items, onClick }: { items: (MenuItemType | null | undefined
|
|
|
68
69
|
<span className="min-w-0 flex-1">{item.label}</span>
|
|
69
70
|
<ChevronRight />
|
|
70
71
|
</DropdownMenu.SubTrigger>
|
|
71
|
-
<DropdownMenu.Portal>
|
|
72
|
+
<DropdownMenu.Portal container={portal}>
|
|
72
73
|
<DropdownMenu.SubContent sideOffset={4} className={cn(menuContentClass, "nonla-popper")}>
|
|
73
|
-
<MenuItems items={item.children} onClick={onClick} />
|
|
74
|
+
<MenuItems items={item.children} onClick={onClick} portal={portal} />
|
|
74
75
|
</DropdownMenu.SubContent>
|
|
75
76
|
</DropdownMenu.Portal>
|
|
76
77
|
</DropdownMenu.Sub>
|
|
@@ -98,6 +99,7 @@ function MenuItems({ items, onClick }: { items: (MenuItemType | null | undefined
|
|
|
98
99
|
|
|
99
100
|
export function Dropdown({ menu, children, trigger = ["click"], open, onOpenChange, placement = "bottomLeft", className, overlayClassName, disabled }: DropdownProps) {
|
|
100
101
|
const { side, align } = placementToRadix(placement);
|
|
102
|
+
const portal = usePopupContainer()?.();
|
|
101
103
|
const hover = trigger.includes("hover");
|
|
102
104
|
const contextMenu = trigger.includes("contextMenu");
|
|
103
105
|
const click = trigger.includes("click") || (!hover && !contextMenu);
|
|
@@ -119,7 +121,7 @@ export function Dropdown({ menu, children, trigger = ["click"], open, onOpenChan
|
|
|
119
121
|
<DropdownMenu.Trigger asChild disabled={disabled} onClick={click || contextMenu ? undefined : (e) => e.preventDefault()} onMouseEnter={hover && !disabled ? () => setIsOpen(true) : undefined} onMouseLeave={hover ? () => setIsOpen(false) : undefined} onContextMenu={handleContext}>
|
|
120
122
|
{children}
|
|
121
123
|
</DropdownMenu.Trigger>
|
|
122
|
-
<DropdownMenu.Portal>
|
|
124
|
+
<DropdownMenu.Portal container={portal}>
|
|
123
125
|
<DropdownMenu.Content
|
|
124
126
|
side={side}
|
|
125
127
|
align={align === "center" ? "start" : align}
|
|
@@ -129,7 +131,7 @@ export function Dropdown({ menu, children, trigger = ["click"], open, onOpenChan
|
|
|
129
131
|
onMouseEnter={hover ? () => setIsOpen(true) : undefined}
|
|
130
132
|
onMouseLeave={hover ? () => setIsOpen(false) : undefined}
|
|
131
133
|
>
|
|
132
|
-
<MenuItems items={menu?.items ?? []} onClick={menu?.onClick} />
|
|
134
|
+
<MenuItems items={menu?.items ?? []} onClick={menu?.onClick} portal={portal} />
|
|
133
135
|
</DropdownMenu.Content>
|
|
134
136
|
</DropdownMenu.Portal>
|
|
135
137
|
</DropdownMenu.Root>
|
package/src/form/Form.tsx
CHANGED
|
@@ -20,10 +20,10 @@ export type FormProps<T extends FieldValues = FieldValues> = {
|
|
|
20
20
|
function evalCondition(fieldValue: unknown, operator: string, compareValue: unknown): boolean {
|
|
21
21
|
switch (operator) {
|
|
22
22
|
case "==":
|
|
23
|
-
//
|
|
23
|
+
// biome-ignore lint/suspicious/noDoubleEquals: form condition operator is loose equality
|
|
24
24
|
return fieldValue == compareValue;
|
|
25
25
|
case "!=":
|
|
26
|
-
//
|
|
26
|
+
// biome-ignore lint/suspicious/noDoubleEquals: form condition operator is loose equality
|
|
27
27
|
return fieldValue != compareValue;
|
|
28
28
|
case ">=":
|
|
29
29
|
return (fieldValue as number) >= (compareValue as number);
|
package/src/form/FormItem.tsx
CHANGED
|
@@ -111,10 +111,10 @@ export function FormItem(props: FormItemProps) {
|
|
|
111
111
|
</svg>
|
|
112
112
|
</button>
|
|
113
113
|
) : null}
|
|
114
|
-
<
|
|
114
|
+
<div className="text-sm text-foreground">
|
|
115
115
|
{label}
|
|
116
116
|
{required ? <span className="ml-1 text-destructive">*</span> : null}
|
|
117
|
-
</
|
|
117
|
+
</div>
|
|
118
118
|
</div>
|
|
119
119
|
) : null}
|
|
120
120
|
|
|
@@ -23,8 +23,8 @@ export function FormItemHelps({ items, className }: FormItemHelpsProps) {
|
|
|
23
23
|
if (!items?.length) return null;
|
|
24
24
|
return (
|
|
25
25
|
<div className={cn("mt-1 flex flex-col gap-1 pl-2.75", className)}>
|
|
26
|
-
{items.map((item
|
|
27
|
-
<FormItemHelp key={
|
|
26
|
+
{items.map((item) => (
|
|
27
|
+
<FormItemHelp key={item.text} text={item.text} className={item.className} iconClassName={item.iconClassName} />
|
|
28
28
|
))}
|
|
29
29
|
</div>
|
|
30
30
|
);
|
|
@@ -24,7 +24,7 @@ export function FieldColor({ field, options, status }: Props) {
|
|
|
24
24
|
trigger="click"
|
|
25
25
|
placement="bottom"
|
|
26
26
|
content={
|
|
27
|
-
<div className="flex flex-col gap-2 p-1 min-w-
|
|
27
|
+
<div className="flex flex-col gap-2 p-1 min-w-45">
|
|
28
28
|
<input
|
|
29
29
|
type="color"
|
|
30
30
|
value={hex}
|
|
@@ -13,11 +13,11 @@ export function FieldObject({ control, name, childItems }: Props) {
|
|
|
13
13
|
const prefix = fieldNameOf(name);
|
|
14
14
|
return (
|
|
15
15
|
<div className="grid grid-cols-12 gap-x-4">
|
|
16
|
-
{(childItems ?? []).map((child
|
|
16
|
+
{(childItems ?? []).map((child) => {
|
|
17
17
|
const childName = fieldNameOf(child.name);
|
|
18
18
|
return (
|
|
19
19
|
<FormItem
|
|
20
|
-
key={`${prefix}.${childName}
|
|
20
|
+
key={`${prefix}.${childName}`}
|
|
21
21
|
{...child}
|
|
22
22
|
name={`${prefix}.${childName}`}
|
|
23
23
|
control={control}
|
|
@@ -17,7 +17,7 @@ export function FieldRepeaterItem({ index, remove, namePrefix, childItems, contr
|
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
19
|
<div className="relative">
|
|
20
|
-
<div className={`absolute bottom-0 left-0 border-l border-dashed border-border ${index === 0 ? "top-0" : "top-
|
|
20
|
+
<div className={`absolute bottom-0 left-0 border-l border-dashed border-border ${index === 0 ? "top-0" : "-top-5"}`} />
|
|
21
21
|
|
|
22
22
|
<div className="flex items-center gap-2">
|
|
23
23
|
<div className="flex flex-1 items-center gap-2">
|
|
@@ -37,7 +37,7 @@ export function FieldRepeaterItem({ index, remove, namePrefix, childItems, contr
|
|
|
37
37
|
<button
|
|
38
38
|
type="button"
|
|
39
39
|
aria-label="Remove item"
|
|
40
|
-
className="inline-flex size-5 items-center justify-center rounded border-0 bg-muted text-muted-foreground cursor-pointer hover:bg-destructive hover:text-
|
|
40
|
+
className="inline-flex size-5 items-center justify-center rounded border-0 bg-muted text-muted-foreground cursor-pointer hover:bg-destructive hover:text-destructive-foreground"
|
|
41
41
|
onClick={() => remove(index)}
|
|
42
42
|
>
|
|
43
43
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden>
|
|
@@ -47,11 +47,11 @@ export function FieldRepeaterItem({ index, remove, namePrefix, childItems, contr
|
|
|
47
47
|
</div>
|
|
48
48
|
|
|
49
49
|
<div className={`mt-2 grid grid-cols-12 gap-x-4 overflow-hidden pl-8 ${collapsed ? "max-h-0" : ""}`}>
|
|
50
|
-
{(childItems ?? []).map((child
|
|
50
|
+
{(childItems ?? []).map((child) => {
|
|
51
51
|
const childName = fieldNameOf(child.name);
|
|
52
52
|
return (
|
|
53
53
|
<FormItem
|
|
54
|
-
key={`${namePrefix}.${childName}
|
|
54
|
+
key={`${namePrefix}.${childName}`}
|
|
55
55
|
{...child}
|
|
56
56
|
name={`${namePrefix}.${childName}`}
|
|
57
57
|
control={control}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
2
|
import { cn } from "../lib/cn";
|
|
3
3
|
|
|
4
4
|
export type FormLayoutProps = HTMLAttributes<HTMLDivElement> & {
|
|
5
|
-
/**
|
|
5
|
+
/** Form layout — only vertical is used in this app. */
|
|
6
6
|
layout?: "horizontal" | "vertical" | "inline";
|
|
7
7
|
children?: ReactNode;
|
|
8
8
|
};
|
|
@@ -25,7 +25,7 @@ function FormRoot({ layout = "vertical", className, children, ...rest }: FormLay
|
|
|
25
25
|
export type FormLayoutItemProps = {
|
|
26
26
|
label?: ReactNode;
|
|
27
27
|
required?: boolean;
|
|
28
|
-
/**
|
|
28
|
+
/** Help text under the control. */
|
|
29
29
|
extra?: ReactNode;
|
|
30
30
|
help?: ReactNode;
|
|
31
31
|
validateStatus?: "success" | "warning" | "error" | "validating";
|
|
@@ -53,10 +53,10 @@ function FormItem({
|
|
|
53
53
|
)}
|
|
54
54
|
>
|
|
55
55
|
{label != null && label !== false ? (
|
|
56
|
-
<
|
|
56
|
+
<div className={cn("text-sm text-foreground", layout === "horizontal" && "pt-1.5 shrink-0")}>
|
|
57
57
|
{label}
|
|
58
58
|
{required ? <span className="ml-1 text-destructive">*</span> : null}
|
|
59
|
-
</
|
|
59
|
+
</div>
|
|
60
60
|
) : null}
|
|
61
61
|
<div className="min-w-0 flex-1">
|
|
62
62
|
{children}
|
|
@@ -69,6 +69,6 @@ function FormItem({
|
|
|
69
69
|
);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/** Layout-only Form (
|
|
72
|
+
/** Layout-only Form (`Form` / `Form.Item`). Schema forms use `SchemaForm`. */
|
|
73
73
|
export const Form = Object.assign(FormRoot, { Item: FormItem });
|
|
74
74
|
export { FormItem };
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { App, useAppConfig } from "./app/App";
|
|
1
|
+
export { App, useApp, useAppConfig, usePopupContainer, useToken } from "./app/App";
|
|
2
2
|
export type { AppProps, NonlaAppConfig } from "./app/App";
|
|
3
3
|
|
|
4
4
|
export { Button } from "./button/Button";
|
|
@@ -10,7 +10,7 @@ export { Input, TextArea, InputNumber } from "./input/Input";
|
|
|
10
10
|
export type { InputProps, TextAreaProps, InputNumberProps, InputSize, TextAreaRef, PasswordProps } from "./input/Input";
|
|
11
11
|
export { SearchInput } from "./input/SearchInput";
|
|
12
12
|
export type { SearchInputProps } from "./input/SearchInput";
|
|
13
|
-
/**
|
|
13
|
+
/** Native input element ref. */
|
|
14
14
|
export type InputRef = HTMLInputElement;
|
|
15
15
|
|
|
16
16
|
export { Select, SelectOption } from "./select/Select";
|
|
@@ -55,7 +55,7 @@ export type {
|
|
|
55
55
|
IFormItemHelpProps,
|
|
56
56
|
} from "./form";
|
|
57
57
|
|
|
58
|
-
/** Layout-only Form + Form.Item
|
|
58
|
+
/** Layout-only Form + Form.Item for labeled fields. */
|
|
59
59
|
export { Form } from "./form-layout/FormLayout";
|
|
60
60
|
export type { FormLayoutProps, FormLayoutItemProps } from "./form-layout/FormLayout";
|
|
61
61
|
|
|
@@ -102,6 +102,9 @@ export type { SpinProps, SpinVariant } from "./spin/Spin";
|
|
|
102
102
|
export { Skeleton } from "./skeleton/Skeleton";
|
|
103
103
|
export type { SkeletonProps } from "./skeleton/Skeleton";
|
|
104
104
|
|
|
105
|
+
export { Shimmer } from "./shimmer/Shimmer";
|
|
106
|
+
export type { ShimmerProps } from "./shimmer/Shimmer";
|
|
107
|
+
|
|
105
108
|
export { Segmented } from "./segmented/Segmented";
|
|
106
109
|
export type { SegmentedProps, SegmentedOption } from "./segmented/Segmented";
|
|
107
110
|
|
|
@@ -143,9 +146,14 @@ export {
|
|
|
143
146
|
prettyJson,
|
|
144
147
|
isCallAgentToolName,
|
|
145
148
|
parseCallAgentToolTargetId,
|
|
149
|
+
matchesToolName,
|
|
150
|
+
matchesToolHook,
|
|
146
151
|
parseBgTaskRef,
|
|
147
152
|
formatBgElapsed,
|
|
148
153
|
resolveToolUI,
|
|
154
|
+
builtinToolUis,
|
|
155
|
+
matchesToolUIName,
|
|
156
|
+
isToolRunning,
|
|
149
157
|
CallAgentToolUI,
|
|
150
158
|
WebFetchToolUI,
|
|
151
159
|
GetCurrentTimeToolUI,
|
|
@@ -173,20 +181,26 @@ export type {
|
|
|
173
181
|
AgentStreamRequest,
|
|
174
182
|
AgentPanelEndpoint,
|
|
175
183
|
AgentToolAction,
|
|
184
|
+
AgentToolHook,
|
|
185
|
+
AgentToolCallEvent,
|
|
186
|
+
AgentToolResultEvent,
|
|
187
|
+
AgentToolNameMatch,
|
|
176
188
|
ToolUIProps,
|
|
177
189
|
ChatToolMessage,
|
|
190
|
+
AgentToolUI,
|
|
191
|
+
AgentToolUIName,
|
|
178
192
|
ChatBgTask,
|
|
179
193
|
BackgroundTasksBarProps,
|
|
180
194
|
} from "./chat";
|
|
181
195
|
|
|
182
196
|
export { cn } from "./lib/cn";
|
|
183
197
|
export { glassSurfaceClass, glassOverlayClass, meadowSurfaceClass } from "./lib/surface";
|
|
184
|
-
export { CONTROL_SIZES, normalizeSize, getSizeTokens, controlHeightVar, controlRadiusVar, controlStatusClass } from "./lib/sizes";
|
|
198
|
+
export { CONTROL_SIZES, normalizeSize, getSizeTokens, controlHeightVar, controlRadiusVar, controlStatusClass, useControlSize } from "./lib/sizes";
|
|
185
199
|
export type { ControlSize, CanonicalSize, ControlSizeTokens } from "./lib/sizes";
|
|
186
200
|
export { placementToRadix } from "./lib/placement";
|
|
187
201
|
export type { PopperPlacement } from "./lib/placement";
|
|
188
|
-
export { NONLA_THEME_KNOBS, NONLA_THEME_KEYS, applyNonlaTheme } from "./theme";
|
|
189
|
-
export type { NonlaThemeKnob, NonlaThemeKnobName, NonlaThemeColorName, NonlaThemeColors, NonlaThemeConfig } from "./theme";
|
|
202
|
+
export { NONLA_THEME_KNOBS, NONLA_THEME_KEYS, applyNonlaTheme, getDesignToken } from "./theme";
|
|
203
|
+
export type { NonlaThemeKnob, NonlaThemeKnobName, NonlaThemeColorName, NonlaThemeColors, NonlaThemeConfig, NonlaTokenSnapshot } from "./theme";
|
|
190
204
|
|
|
191
205
|
export { FluentIcon } from "./icon/FluentIcon";
|
|
192
206
|
export {
|
package/src/input/Input.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type CSSProperties, type InputHTMLAttributes, type KeyboardEvent, type ReactNode, type TextareaHTMLAttributes, forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import { cn } from "../lib/cn";
|
|
3
|
-
import { type ControlSize, controlFieldFocusBorder, controlFieldStyle, controlFieldSurface, controlFieldTransition, controlHeightVar, controlRadiusVar, controlStatusClass, getSizeTokens,
|
|
3
|
+
import { type ControlSize, controlFieldFocusBorder, controlFieldStyle, controlFieldSurface, controlFieldTransition, controlHeightVar, controlRadiusVar, controlStatusClass, getSizeTokens, useControlSize } from "../lib/sizes";
|
|
4
4
|
|
|
5
5
|
export type InputSize = ControlSize;
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ export type InputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "p
|
|
|
11
11
|
suffix?: ReactNode;
|
|
12
12
|
allowClear?: boolean;
|
|
13
13
|
variant?: "outlined" | "borderless" | "filled";
|
|
14
|
-
/**
|
|
14
|
+
/** Fires on Enter (ignored while composing). */
|
|
15
15
|
onPressEnter?: (e: KeyboardEvent<HTMLInputElement>) => void;
|
|
16
16
|
};
|
|
17
17
|
|
|
@@ -24,7 +24,7 @@ function variantClass(variant: InputProps["variant"]) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const InputRoot = forwardRef<HTMLInputElement, InputProps>(function Input({ className, size, status, prefix, suffix, allowClear, variant = "outlined", disabled, value, onChange, onPressEnter, onKeyDown, style, ...rest }, ref) {
|
|
27
|
-
const fieldStyle = controlFieldStyle(size);
|
|
27
|
+
const fieldStyle = controlFieldStyle(useControlSize(size));
|
|
28
28
|
const showClear = allowClear && !disabled && value != null && String(value).length > 0;
|
|
29
29
|
const wrapped = Boolean(prefix || suffix || showClear);
|
|
30
30
|
|
|
@@ -72,13 +72,13 @@ export type TextAreaProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
|
|
72
72
|
size?: ControlSize;
|
|
73
73
|
};
|
|
74
74
|
|
|
75
|
-
/**
|
|
75
|
+
/** Ref used by chat InputArea (`resizableTextArea.textArea`). */
|
|
76
76
|
export type TextAreaRef = HTMLTextAreaElement & {
|
|
77
77
|
resizableTextArea?: { textArea: HTMLTextAreaElement };
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
const TextArea = forwardRef<TextAreaRef, TextAreaProps>(function TextArea({ className, status, variant = "outlined", autoSize, rows, style, size, onChange, value, ...rest }, ref) {
|
|
81
|
-
const tok = getSizeTokens(size);
|
|
81
|
+
const tok = getSizeTokens(useControlSize(size));
|
|
82
82
|
const innerRef = useRef<HTMLTextAreaElement | null>(null);
|
|
83
83
|
const minRows = typeof autoSize === "object" ? (autoSize.minRows ?? 1) : autoSize ? 1 : undefined;
|
|
84
84
|
const maxRows = typeof autoSize === "object" ? autoSize.maxRows : undefined;
|
|
@@ -139,7 +139,7 @@ export type InputNumberProps = Omit<InputProps, "type" | "onChange" | "value" |
|
|
|
139
139
|
min?: number;
|
|
140
140
|
max?: number;
|
|
141
141
|
step?: number;
|
|
142
|
-
/** Show
|
|
142
|
+
/** Show up/down handlers (default true). */
|
|
143
143
|
controls?: boolean;
|
|
144
144
|
/** Precision after decimal; omit to keep free typing. */
|
|
145
145
|
precision?: number;
|
|
@@ -175,8 +175,9 @@ const InputNumber = forwardRef<HTMLInputElement, InputNumberProps>(function Inpu
|
|
|
175
175
|
const [text, setText] = useState(() => (numeric == null ? "" : String(numeric)));
|
|
176
176
|
const [focused, setFocused] = useState(false);
|
|
177
177
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
178
|
-
const
|
|
179
|
-
const
|
|
178
|
+
const resolvedSize = useControlSize(size);
|
|
179
|
+
const tok = getSizeTokens(resolvedSize);
|
|
180
|
+
const fieldStyle = controlFieldStyle(resolvedSize);
|
|
180
181
|
|
|
181
182
|
// Sync display from external value when not editing
|
|
182
183
|
useEffect(() => {
|
|
@@ -215,7 +216,7 @@ const InputNumber = forwardRef<HTMLInputElement, InputNumberProps>(function Inpu
|
|
|
215
216
|
|
|
216
217
|
const atMin = numeric != null && min != null && numeric <= min;
|
|
217
218
|
const atMax = numeric != null && max != null && numeric >= max;
|
|
218
|
-
const handlerW =
|
|
219
|
+
const handlerW = resolvedSize === "small" ? 18 : 22;
|
|
219
220
|
|
|
220
221
|
const setRefs = (node: HTMLInputElement | null) => {
|
|
221
222
|
inputRef.current = node;
|
|
@@ -254,7 +255,7 @@ const InputNumber = forwardRef<HTMLInputElement, InputNumberProps>(function Inpu
|
|
|
254
255
|
onChange={(e) => {
|
|
255
256
|
const raw = e.target.value;
|
|
256
257
|
setText(raw);
|
|
257
|
-
// Live update when parseable
|
|
258
|
+
// Live update when parseable; keep typing free otherwise
|
|
258
259
|
if (raw === "" || raw === "-" || raw === "." || raw === "-.") return;
|
|
259
260
|
const n = parseRaw(raw);
|
|
260
261
|
if (n != null) {
|
package/src/lib/sizes.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { createContext, useContext, type CSSProperties } from "react";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Canonical control sizes for NonlaUI.
|
|
5
|
-
*
|
|
5
|
+
* Live height / radius come from `--nonla-height*` / `--nonla-radius*` (seed).
|
|
6
|
+
* Numbers here are fallbacks for padding / font / icon — keep in sync with CSS 24 / 32 / 40.
|
|
6
7
|
* Radius small/large = `--nonla-radius` ± 2px.
|
|
7
8
|
*/
|
|
8
9
|
export const CONTROL_SIZES = {
|
|
@@ -45,7 +46,7 @@ export type ControlSize = CanonicalSize | "middle" | "medium" | "xs";
|
|
|
45
46
|
|
|
46
47
|
export type ControlSizeTokens = (typeof CONTROL_SIZES)[CanonicalSize];
|
|
47
48
|
|
|
48
|
-
/** Map legacy
|
|
49
|
+
/** Map legacy aliases → small | default | large */
|
|
49
50
|
export function normalizeSize(size: ControlSize | undefined): CanonicalSize {
|
|
50
51
|
if (size === "small" || size === "xs") return "small";
|
|
51
52
|
if (size === "large") return "large";
|
|
@@ -53,6 +54,15 @@ export function normalizeSize(size: ControlSize | undefined): CanonicalSize {
|
|
|
53
54
|
return "default";
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/** App `componentSize` — prop on the control wins, then this. */
|
|
58
|
+
export const ControlSizeContext = createContext<CanonicalSize | undefined>(undefined);
|
|
59
|
+
|
|
60
|
+
/** Resolve size: control prop → App `componentSize` → default. */
|
|
61
|
+
export function useControlSize(size?: ControlSize): CanonicalSize {
|
|
62
|
+
const fromApp = useContext(ControlSizeContext);
|
|
63
|
+
return normalizeSize(size ?? fromApp);
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
export function getSizeTokens(size: ControlSize | undefined): ControlSizeTokens {
|
|
57
67
|
return CONTROL_SIZES[normalizeSize(size)];
|
|
58
68
|
}
|
package/src/lib/surface.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
export const glassSurfaceClass = "nonla-glass";
|
|
3
3
|
|
|
4
4
|
/** Floating overlay chrome (Popover / Dropdown / Select / pickers / Tooltip). */
|
|
5
|
-
export const glassOverlayClass =
|
|
5
|
+
export const glassOverlayClass =
|
|
6
|
+
"nonla-popup-layer z-[var(--nonla-z-popup,1050)] rounded-xl nonla-glass outline-none nonla-popper";
|
|
6
7
|
|
|
7
8
|
/** Meadow menus — same panel as overlays. */
|
|
8
9
|
export const meadowSurfaceClass = "rounded-xl nonla-glass";
|
package/src/menu/Menu.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
2
2
|
import { FluentIcon } from "../icon/FluentIcon";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
4
|
import type { PopperPlacement } from "../lib/placement";
|
|
@@ -20,7 +20,7 @@ export function Menu({ open, onOpenChange, trigger, children, contentClassName,
|
|
|
20
20
|
onOpenChange={onOpenChange}
|
|
21
21
|
trigger="click"
|
|
22
22
|
placement={placement}
|
|
23
|
-
contentClassName={cn("w-
|
|
23
|
+
contentClassName={cn("w-55 p-1", contentClassName)}
|
|
24
24
|
content={<div className="flex flex-col gap-px">{children}</div>}
|
|
25
25
|
>
|
|
26
26
|
{trigger}
|
package/src/message/message.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CSSProperties, type ReactNode } from "react";
|
|
1
|
+
import { type CSSProperties, type ReactNode, useLayoutEffect, useState } from "react";
|
|
2
2
|
import { createRoot, type Root } from "react-dom/client";
|
|
3
3
|
|
|
4
4
|
export type MessageType = "success" | "error" | "info" | "warning" | "loading";
|
|
@@ -117,8 +117,19 @@ function MessageList({ list }: { list: Active[] }) {
|
|
|
117
117
|
);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
type MessageRenderer = (list: Active[]) => void;
|
|
121
|
+
let renderer: MessageRenderer | null = null;
|
|
122
|
+
|
|
123
|
+
function dropFallbackHost() {
|
|
124
|
+
if (!hostEl) return;
|
|
125
|
+
root?.unmount();
|
|
126
|
+
hostEl.remove();
|
|
127
|
+
hostEl = null;
|
|
128
|
+
root = null;
|
|
129
|
+
}
|
|
130
|
+
|
|
120
131
|
function ensureHost() {
|
|
121
|
-
if (hostEl && root) return;
|
|
132
|
+
if (renderer || (hostEl && root)) return;
|
|
122
133
|
hostEl = document.createElement("div");
|
|
123
134
|
hostEl.className = "nonla-message-host";
|
|
124
135
|
document.body.appendChild(hostEl);
|
|
@@ -126,10 +137,32 @@ function ensureHost() {
|
|
|
126
137
|
}
|
|
127
138
|
|
|
128
139
|
function render() {
|
|
140
|
+
if (renderer) {
|
|
141
|
+
renderer(items);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
129
144
|
ensureHost();
|
|
130
145
|
root?.render(<MessageList list={items} />);
|
|
131
146
|
}
|
|
132
147
|
|
|
148
|
+
/** Mounted by `App` so toasts inherit theme. Falls back to `document.body` if no App. */
|
|
149
|
+
export function MessageHolder() {
|
|
150
|
+
const [list, setList] = useState<Active[]>(items);
|
|
151
|
+
useLayoutEffect(() => {
|
|
152
|
+
dropFallbackHost();
|
|
153
|
+
renderer = setList;
|
|
154
|
+
setList(items);
|
|
155
|
+
return () => {
|
|
156
|
+
renderer = null;
|
|
157
|
+
};
|
|
158
|
+
}, []);
|
|
159
|
+
return (
|
|
160
|
+
<div className="nonla-message-host" aria-live="polite" aria-relevant="additions">
|
|
161
|
+
<MessageList list={list} />
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
133
166
|
function beginLeave(id: string) {
|
|
134
167
|
const found = items.find((i) => i.id === id);
|
|
135
168
|
if (!found || found.leaving) return;
|
package/src/modal/Modal.tsx
CHANGED
|
@@ -4,10 +4,12 @@ import {
|
|
|
4
4
|
type CSSProperties,
|
|
5
5
|
type ReactNode,
|
|
6
6
|
useEffect,
|
|
7
|
+
useLayoutEffect,
|
|
7
8
|
useRef,
|
|
8
9
|
useState,
|
|
9
10
|
} from "react";
|
|
10
11
|
import { createRoot, type Root } from "react-dom/client";
|
|
12
|
+
import { usePopupContainer } from "../app/context";
|
|
11
13
|
import { Button } from "../button/Button";
|
|
12
14
|
import { cn } from "../lib/cn";
|
|
13
15
|
import { glassSurfaceClass } from "../lib/surface";
|
|
@@ -37,7 +39,7 @@ export type ModalProps = {
|
|
|
37
39
|
closable?: boolean;
|
|
38
40
|
maskClosable?: boolean;
|
|
39
41
|
className?: string;
|
|
40
|
-
/** Root positioning (
|
|
42
|
+
/** Root positioning (`style={{ top }}`). */
|
|
41
43
|
style?: CSSProperties;
|
|
42
44
|
styles?: { body?: CSSProperties; content?: CSSProperties; header?: CSSProperties; footer?: CSSProperties; container?: CSSProperties };
|
|
43
45
|
onOpenChange?: (open: boolean) => void;
|
|
@@ -154,6 +156,8 @@ function ModalView({
|
|
|
154
156
|
if (!isOpen) finishExit();
|
|
155
157
|
};
|
|
156
158
|
|
|
159
|
+
const portal = usePopupContainer()?.();
|
|
160
|
+
|
|
157
161
|
return (
|
|
158
162
|
<Dialog.Root
|
|
159
163
|
open={isOpen}
|
|
@@ -166,7 +170,7 @@ function ModalView({
|
|
|
166
170
|
}}
|
|
167
171
|
>
|
|
168
172
|
{isOpen || present ? (
|
|
169
|
-
<Dialog.Portal>
|
|
173
|
+
<Dialog.Portal container={portal}>
|
|
170
174
|
<Dialog.Overlay className="nonla-modal-overlay" />
|
|
171
175
|
<Dialog.Content
|
|
172
176
|
className={cn("nonla-modal-content", glassSurfaceClass, className)}
|
|
@@ -210,6 +214,10 @@ function ConfirmHost({ initial, onDone }: { initial: ModalConfirmProps; onDone:
|
|
|
210
214
|
const [props, setProps] = useState(initial);
|
|
211
215
|
const [loading, setLoading] = useState(false);
|
|
212
216
|
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
setProps(initial);
|
|
219
|
+
}, [initial]);
|
|
220
|
+
|
|
213
221
|
useEffect(() => {
|
|
214
222
|
(ConfirmHost as unknown as { _update?: (p: Partial<ModalConfirmProps>) => void })._update = (p) => setProps((prev) => ({ ...prev, ...p }));
|
|
215
223
|
}, []);
|
|
@@ -253,7 +261,12 @@ function ConfirmHost({ initial, onDone }: { initial: ModalConfirmProps; onDone:
|
|
|
253
261
|
);
|
|
254
262
|
}
|
|
255
263
|
|
|
256
|
-
|
|
264
|
+
type ConfirmJob = { id: number; props: ModalConfirmProps };
|
|
265
|
+
type ConfirmOpener = (props: ModalConfirmProps) => ConfirmHandle;
|
|
266
|
+
|
|
267
|
+
let openConfirm: ConfirmOpener | null = null;
|
|
268
|
+
|
|
269
|
+
function mountFallback(props: ModalConfirmProps): ConfirmHandle {
|
|
257
270
|
const el = document.createElement("div");
|
|
258
271
|
document.body.appendChild(el);
|
|
259
272
|
const root: Root = createRoot(el);
|
|
@@ -268,6 +281,39 @@ function mountConfirm(props: ModalConfirmProps): ConfirmHandle {
|
|
|
268
281
|
};
|
|
269
282
|
}
|
|
270
283
|
|
|
284
|
+
function mountConfirm(props: ModalConfirmProps): ConfirmHandle {
|
|
285
|
+
return openConfirm ? openConfirm(props) : mountFallback(props);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Mounted by `App` so `Modal.confirm` shares theme / context. */
|
|
289
|
+
export function ConfirmHolder() {
|
|
290
|
+
const [jobs, setJobs] = useState<ConfirmJob[]>([]);
|
|
291
|
+
const seq = useRef(0);
|
|
292
|
+
|
|
293
|
+
useLayoutEffect(() => {
|
|
294
|
+
openConfirm = (props) => {
|
|
295
|
+
const id = ++seq.current;
|
|
296
|
+
setJobs((list) => [...list, { id, props }]);
|
|
297
|
+
return {
|
|
298
|
+
destroy: () => setJobs((list) => list.filter((job) => job.id !== id)),
|
|
299
|
+
update: (p) =>
|
|
300
|
+
setJobs((list) => list.map((job) => (job.id === id ? { ...job, props: { ...job.props, ...p } } : job))),
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
return () => {
|
|
304
|
+
openConfirm = null;
|
|
305
|
+
};
|
|
306
|
+
}, []);
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<>
|
|
310
|
+
{jobs.map((job) => (
|
|
311
|
+
<ConfirmHost key={job.id} initial={job.props} onDone={() => setJobs((list) => list.filter((j) => j.id !== job.id))} />
|
|
312
|
+
))}
|
|
313
|
+
</>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
271
317
|
export const Modal = Object.assign(ModalView, {
|
|
272
318
|
confirm: (props: ModalConfirmProps) => mountConfirm(props),
|
|
273
319
|
info: (props: ModalConfirmProps) => mountConfirm(props),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { cn } from "../lib/cn";
|
|
3
|
-
import { type ControlSize, controlHeightVar,
|
|
3
|
+
import { type ControlSize, controlHeightVar, useControlSize } from "../lib/sizes";
|
|
4
4
|
|
|
5
5
|
export type PaginationItemType = "page" | "prev" | "next" | "jump-prev" | "jump-next";
|
|
6
6
|
|
|
@@ -20,8 +20,8 @@ export type PaginationProps = {
|
|
|
20
20
|
function pageList(current: number, pages: number): (number | "ellipsis")[] {
|
|
21
21
|
if (pages <= 7) return Array.from({ length: pages }, (_, i) => i + 1);
|
|
22
22
|
const set = new Set<number>([1, pages, current, current - 1, current + 1]);
|
|
23
|
-
if (current <= 3) [2, 3, 4]
|
|
24
|
-
if (current >= pages - 2) [pages - 1, pages - 2, pages - 3]
|
|
23
|
+
if (current <= 3) for (const n of [2, 3, 4]) set.add(n);
|
|
24
|
+
if (current >= pages - 2) for (const n of [pages - 1, pages - 2, pages - 3]) set.add(n);
|
|
25
25
|
const nums = [...set].filter((n) => n >= 1 && n <= pages).sort((a, b) => a - b);
|
|
26
26
|
const out: (number | "ellipsis")[] = [];
|
|
27
27
|
for (let i = 0; i < nums.length; i++) {
|
|
@@ -58,9 +58,10 @@ function ItemBtn({
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export function Pagination({ current = 1, pageSize = 10, total = 0, onChange, className, disabled, size, itemRender }: PaginationProps) {
|
|
61
|
+
const resolvedSize = useControlSize(size);
|
|
62
|
+
const compact = resolvedSize === "small";
|
|
63
|
+
const h = compact ? controlHeightVar(resolvedSize) : 28;
|
|
61
64
|
const pages = Math.max(1, Math.ceil(total / pageSize));
|
|
62
|
-
const compact = normalizeSize(size) === "small";
|
|
63
|
-
const h = compact ? controlHeightVar(size) : 28;
|
|
64
65
|
|
|
65
66
|
const wrap = (page: number, type: PaginationItemType, node: ReactNode) => (itemRender ? itemRender(page, type, node) : node);
|
|
66
67
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type CSSProperties, type ReactNode, useState } from "react";
|
|
2
2
|
import { Button } from "../button/Button";
|
|
3
3
|
import { cn } from "../lib/cn";
|
|
4
|
-
import {
|
|
4
|
+
import type { PopperPlacement } from "../lib/placement";
|
|
5
5
|
import { Popover } from "../popover/Popover";
|
|
6
6
|
|
|
7
7
|
export type PopconfirmProps = {
|