devnonla-ui 0.1.0 → 0.3.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 (71) 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/colorpicker/ColorPicker.tsx +694 -30
  31. package/src/colorpicker/color.ts +246 -0
  32. package/src/datepicker/DatePicker.tsx +15 -9
  33. package/src/desktop/DesktopHeader.tsx +1 -1
  34. package/src/desktop/DesktopIcon.tsx +2 -4
  35. package/src/desktop/DesktopWindow.tsx +19 -5
  36. package/src/desktop/MeadowDesktop.tsx +2 -2
  37. package/src/drawer/Drawer.tsx +8 -6
  38. package/src/dropdown/ContextMenu.tsx +3 -1
  39. package/src/dropdown/Dropdown.tsx +8 -6
  40. package/src/form/Form.tsx +2 -2
  41. package/src/form/FormItem.tsx +2 -2
  42. package/src/form/partials/FormItemHelps.tsx +2 -2
  43. package/src/form/variants/FieldColor.tsx +16 -51
  44. package/src/form/variants/FieldObject.tsx +2 -2
  45. package/src/form/variants/FieldRepeaterItem.tsx +4 -4
  46. package/src/form-layout/FormLayout.tsx +6 -6
  47. package/src/index.ts +25 -8
  48. package/src/input/Input.tsx +11 -10
  49. package/src/lib/sizes.ts +13 -3
  50. package/src/lib/surface.ts +2 -1
  51. package/src/menu/Menu.tsx +2 -2
  52. package/src/message/message.tsx +35 -2
  53. package/src/modal/Modal.tsx +49 -3
  54. package/src/pagination/Pagination.tsx +6 -5
  55. package/src/popconfirm/Popconfirm.tsx +1 -1
  56. package/src/popover/Popover.tsx +6 -8
  57. package/src/scroll/OverlayScroll.tsx +5 -3
  58. package/src/segmented/Segmented.tsx +6 -5
  59. package/src/select/Select.tsx +9 -6
  60. package/src/shimmer/Shimmer.tsx +17 -0
  61. package/src/skeleton/Skeleton.tsx +3 -3
  62. package/src/spin/Spin.tsx +8 -5
  63. package/src/splitter/Splitter.tsx +330 -0
  64. package/src/splitter/sizes.ts +67 -0
  65. package/src/styles.css +96 -27
  66. package/src/switch/Switch.tsx +6 -5
  67. package/src/table/Table.tsx +6 -7
  68. package/src/tag/Tag.tsx +2 -2
  69. package/src/theme.ts +23 -3
  70. package/src/timepicker/TimePicker.tsx +13 -11
  71. package/src/tooltip/Tooltip.tsx +6 -7
@@ -1,7 +1,6 @@
1
- import { useState } from "react";
2
1
  import type { ControllerRenderProps, FieldValues } from "react-hook-form";
3
- import { Input } from "../../input/Input";
4
- import { Popover } from "../../popover/Popover";
2
+ import { ColorPicker, type PresetColorType } from "../../colorpicker/ColorPicker";
3
+ import type { ControlSize } from "../../lib/sizes";
5
4
 
6
5
  type Props = {
7
6
  field: ControllerRenderProps<FieldValues, string>;
@@ -9,55 +8,21 @@ type Props = {
9
8
  status?: "error" | "warning";
10
9
  };
11
10
 
12
- const FALLBACK = "#000000";
13
-
14
11
  export function FieldColor({ field, options, status }: Props) {
15
- const raw = typeof field.value === "string" ? field.value : field.value ? String(field.value) : "";
16
- const hex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(raw) ? raw : FALLBACK;
17
- const [open, setOpen] = useState(false);
18
-
12
+ const opts = options ?? {};
13
+ const raw = typeof field.value === "string" ? field.value : field.value != null ? String(field.value) : "";
19
14
  return (
20
- <div className="flex items-center gap-2">
21
- <Popover
22
- open={open}
23
- onOpenChange={setOpen}
24
- trigger="click"
25
- placement="bottom"
26
- content={
27
- <div className="flex flex-col gap-2 p-1 min-w-[180px]">
28
- <input
29
- type="color"
30
- value={hex}
31
- onChange={(e) => field.onChange(e.target.value)}
32
- className="h-24 w-full cursor-pointer border-0 bg-transparent p-0"
33
- aria-label="Pick color"
34
- />
35
- <Input
36
- value={raw || hex}
37
- onChange={(e) => field.onChange(e.target.value)}
38
- placeholder="#000000"
39
- status={status}
40
- className="font-mono text-sm"
41
- {...(options as object)}
42
- />
43
- </div>
44
- }
45
- >
46
- <button
47
- type="button"
48
- className="size-9 shrink-0 cursor-pointer rounded border border-input p-0"
49
- style={{ backgroundColor: raw || hex }}
50
- aria-label="Color picker"
51
- />
52
- </Popover>
53
- <Input
54
- className="flex-1 font-mono text-sm"
55
- value={raw}
56
- onChange={(e) => field.onChange(e.target.value)}
57
- onBlur={field.onBlur}
58
- placeholder="#000000"
59
- status={status}
60
- />
61
- </div>
15
+ <ColorPicker
16
+ value={raw || null}
17
+ onChange={(color) => field.onChange(color.toHexString())}
18
+ onClear={() => field.onChange("")}
19
+ showText
20
+ allowClear={opts.allowClear !== false}
21
+ disabled={opts.disabled as boolean | undefined}
22
+ disabledAlpha={opts.disabledAlpha as boolean | undefined}
23
+ size={opts.size as ControlSize | undefined}
24
+ status={status}
25
+ presets={opts.presets as PresetColorType[] | undefined}
26
+ />
62
27
  );
63
28
  }
@@ -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, index) => {
16
+ {(childItems ?? []).map((child) => {
17
17
  const childName = fieldNameOf(child.name);
18
18
  return (
19
19
  <FormItem
20
- key={`${prefix}.${childName}-${index}`}
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-[-20px]"}`} />
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-[var(--destructive-foreground)]"
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, childIndex) => {
50
+ {(childItems ?? []).map((child) => {
51
51
  const childName = fieldNameOf(child.name);
52
52
  return (
53
53
  <FormItem
54
- key={`${namePrefix}.${childName}-${childIndex}`}
54
+ key={`${namePrefix}.${childName}`}
55
55
  {...child}
56
56
  name={`${namePrefix}.${childName}`}
57
57
  control={control}
@@ -1,8 +1,8 @@
1
- import { type HTMLAttributes, type ReactNode } from "react";
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
2
  import { cn } from "../lib/cn";
3
3
 
4
4
  export type FormLayoutProps = HTMLAttributes<HTMLDivElement> & {
5
- /** antd Form layout — only vertical is used in this app. */
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
- /** antd `extra` — help text under the control. */
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
- <label className={cn("text-sm text-foreground", layout === "horizontal" && "pt-1.5 shrink-0")}>
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
- </label>
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 (antd Form / Form.Item drop-in). Schema forms use `SchemaForm`. */
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
- /** antd `InputRef` compatibility — native input element. */
13
+ /** Native input element ref. */
14
14
  export type InputRef = HTMLInputElement;
15
15
 
16
16
  export { Select, SelectOption } from "./select/Select";
@@ -22,8 +22,8 @@ export type { SwitchProps, SwitchVariant } from "./switch/Switch";
22
22
  export { Checkbox } from "./checkbox/Checkbox";
23
23
  export type { CheckboxProps } from "./checkbox/Checkbox";
24
24
 
25
- export { ColorPicker } from "./colorpicker/ColorPicker";
26
- export type { ColorPickerProps } from "./colorpicker/ColorPicker";
25
+ export { ColorPicker, Color } from "./colorpicker/ColorPicker";
26
+ export type { ColorPickerProps, ColorFormat, ColorType, PresetColorType, ColorPickerSemanticSlot } from "./colorpicker/ColorPicker";
27
27
 
28
28
  export { Tooltip } from "./tooltip/Tooltip";
29
29
  export type { TooltipProps, TooltipPlacement } from "./tooltip/Tooltip";
@@ -55,7 +55,7 @@ export type {
55
55
  IFormItemHelpProps,
56
56
  } from "./form";
57
57
 
58
- /** Layout-only Form + Form.Item (antd drop-in for labeled fields). */
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
 
@@ -77,6 +77,9 @@ export type { PopconfirmProps } from "./popconfirm/Popconfirm";
77
77
  export { Drawer } from "./drawer/Drawer";
78
78
  export type { DrawerProps, DrawerPlacement } from "./drawer/Drawer";
79
79
 
80
+ export { Splitter, SplitterPanel } from "./splitter/Splitter";
81
+ export type { SplitterProps, SplitterPanelProps, SplitterOrientation, SplitterSize, SplitterSemanticSlot } from "./splitter/Splitter";
82
+
80
83
  export { Table } from "./table/Table";
81
84
  export type {
82
85
  TableProps,
@@ -102,6 +105,9 @@ export type { SpinProps, SpinVariant } from "./spin/Spin";
102
105
  export { Skeleton } from "./skeleton/Skeleton";
103
106
  export type { SkeletonProps } from "./skeleton/Skeleton";
104
107
 
108
+ export { Shimmer } from "./shimmer/Shimmer";
109
+ export type { ShimmerProps } from "./shimmer/Shimmer";
110
+
105
111
  export { Segmented } from "./segmented/Segmented";
106
112
  export type { SegmentedProps, SegmentedOption } from "./segmented/Segmented";
107
113
 
@@ -143,9 +149,14 @@ export {
143
149
  prettyJson,
144
150
  isCallAgentToolName,
145
151
  parseCallAgentToolTargetId,
152
+ matchesToolName,
153
+ matchesToolHook,
146
154
  parseBgTaskRef,
147
155
  formatBgElapsed,
148
156
  resolveToolUI,
157
+ builtinToolUis,
158
+ matchesToolUIName,
159
+ isToolRunning,
149
160
  CallAgentToolUI,
150
161
  WebFetchToolUI,
151
162
  GetCurrentTimeToolUI,
@@ -173,20 +184,26 @@ export type {
173
184
  AgentStreamRequest,
174
185
  AgentPanelEndpoint,
175
186
  AgentToolAction,
187
+ AgentToolHook,
188
+ AgentToolCallEvent,
189
+ AgentToolResultEvent,
190
+ AgentToolNameMatch,
176
191
  ToolUIProps,
177
192
  ChatToolMessage,
193
+ AgentToolUI,
194
+ AgentToolUIName,
178
195
  ChatBgTask,
179
196
  BackgroundTasksBarProps,
180
197
  } from "./chat";
181
198
 
182
199
  export { cn } from "./lib/cn";
183
200
  export { glassSurfaceClass, glassOverlayClass, meadowSurfaceClass } from "./lib/surface";
184
- export { CONTROL_SIZES, normalizeSize, getSizeTokens, controlHeightVar, controlRadiusVar, controlStatusClass } from "./lib/sizes";
201
+ export { CONTROL_SIZES, normalizeSize, getSizeTokens, controlHeightVar, controlRadiusVar, controlStatusClass, useControlSize } from "./lib/sizes";
185
202
  export type { ControlSize, CanonicalSize, ControlSizeTokens } from "./lib/sizes";
186
203
  export { placementToRadix } from "./lib/placement";
187
204
  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";
205
+ export { NONLA_THEME_KNOBS, NONLA_THEME_KEYS, applyNonlaTheme, getDesignToken } from "./theme";
206
+ export type { NonlaThemeKnob, NonlaThemeKnobName, NonlaThemeColorName, NonlaThemeColors, NonlaThemeConfig, NonlaTokenSnapshot } from "./theme";
190
207
 
191
208
  export { FluentIcon } from "./icon/FluentIcon";
192
209
  export {
@@ -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, normalizeSize } from "../lib/sizes";
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
- /** antd — fires on Enter (ignored while composing). */
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
- /** antd-compatible ref used by chat InputArea (`resizableTextArea.textArea`). */
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 antd-style up/down handlers (default true). */
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 tok = getSizeTokens(size);
179
- const fieldStyle = controlFieldStyle(size);
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 = normalizeSize(size) === "small" ? 18 : 22;
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 (antd-like); keep typing free otherwise
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 { CSSProperties } from "react";
1
+ import { createContext, useContext, type CSSProperties } from "react";
2
2
 
3
3
  /**
4
4
  * Canonical control sizes for NonlaUI.
5
- * Height + radius live as `--nonla-*` CSS knobs (theme). Other metrics stay here.
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 / antd aliases → small | default | large */
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
  }
@@ -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 = "z-[9999] rounded-xl nonla-glass outline-none nonla-popper";
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 { type ButtonHTMLAttributes, type ReactNode } from "react";
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-[220px] p-1", contentClassName)}
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}
@@ -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;
@@ -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 (antd `style={{ top }}`). */
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
- function mountConfirm(props: ModalConfirmProps): ConfirmHandle {
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, normalizeSize } from "../lib/sizes";
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].forEach((n) => set.add(n));
24
- if (current >= pages - 2) [pages - 1, pages - 2, pages - 3].forEach((n) => set.add(n));
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 { type PopperPlacement } from "../lib/placement";
4
+ import type { PopperPlacement } from "../lib/placement";
5
5
  import { Popover } from "../popover/Popover";
6
6
 
7
7
  export type PopconfirmProps = {