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
@@ -0,0 +1,246 @@
1
+ export type ColorFormat = "hex" | "rgb" | "hsb";
2
+
3
+ export type HsbaColor = {
4
+ h: number;
5
+ s: number;
6
+ b: number;
7
+ a: number;
8
+ };
9
+
10
+ export type RgbaColor = {
11
+ r: number;
12
+ g: number;
13
+ b: number;
14
+ a: number;
15
+ };
16
+
17
+ export type ColorType = string | Color;
18
+
19
+ const HEX = /^#?([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
20
+ const RGB = /^rgba?\(\s*([0-9.]+%?)\s*[, ]\s*([0-9.]+%?)\s*[, ]\s*([0-9.]+%?)(?:\s*[,/]\s*([0-9.]+%?))?\s*\)$/i;
21
+ const HSL = /^hsla?\(\s*([0-9.]+)\s*[, ]\s*([0-9.]+)%\s*[, ]\s*([0-9.]+)%(?:\s*[,/]\s*([0-9.]+%?))?\s*\)$/i;
22
+ const HSB = /^hsba?\(\s*([0-9.]+)\s*[, ]\s*([0-9.]+)%\s*[, ]\s*([0-9.]+)%(?:\s*[,/]\s*([0-9.]+%?))?\s*\)$/i;
23
+ const HSV = /^hsva?\(\s*([0-9.]+)\s*[, ]\s*([0-9.]+)%\s*[, ]\s*([0-9.]+)%(?:\s*[,/]\s*([0-9.]+%?))?\s*\)$/i;
24
+
25
+ export function clamp(n: number, min: number, max: number) {
26
+ return Math.min(max, Math.max(min, n));
27
+ }
28
+
29
+ function round(n: number, digits = 0) {
30
+ const f = 10 ** digits;
31
+ return Math.round(n * f) / f;
32
+ }
33
+
34
+ function wrapHue(h: number) {
35
+ const n = ((h % 360) + 360) % 360;
36
+ return n;
37
+ }
38
+
39
+ function hexByte(n: number) {
40
+ return clamp(Math.round(n), 0, 255).toString(16).padStart(2, "0");
41
+ }
42
+
43
+ function parseChannel(raw: string, max: number) {
44
+ const t = raw.trim();
45
+ if (t.endsWith("%")) return clamp((parseFloat(t) / 100) * max, 0, max);
46
+ return clamp(parseFloat(t), 0, max);
47
+ }
48
+
49
+ function parseAlpha(raw: string | undefined) {
50
+ if (raw == null || raw === "") return 1;
51
+ const t = raw.trim();
52
+ if (t.endsWith("%")) return clamp(parseFloat(t) / 100, 0, 1);
53
+ const n = parseFloat(t);
54
+ return n > 1 ? clamp(n / 255, 0, 1) : clamp(n, 0, 1);
55
+ }
56
+
57
+ export function hsbaToRgba({ h, s, b, a }: HsbaColor): RgbaColor {
58
+ const hue = wrapHue(h);
59
+ const sat = clamp(s, 0, 1);
60
+ const val = clamp(b, 0, 1);
61
+ const c = val * sat;
62
+ const x = c * (1 - Math.abs(((hue / 60) % 2) - 1));
63
+ const m = val - c;
64
+ let r = 0;
65
+ let g = 0;
66
+ let bl = 0;
67
+ if (hue < 60) [r, g, bl] = [c, x, 0];
68
+ else if (hue < 120) [r, g, bl] = [x, c, 0];
69
+ else if (hue < 180) [r, g, bl] = [0, c, x];
70
+ else if (hue < 240) [r, g, bl] = [0, x, c];
71
+ else if (hue < 300) [r, g, bl] = [x, 0, c];
72
+ else [r, g, bl] = [c, 0, x];
73
+ return {
74
+ r: Math.round((r + m) * 255),
75
+ g: Math.round((g + m) * 255),
76
+ b: Math.round((bl + m) * 255),
77
+ a: clamp(a, 0, 1),
78
+ };
79
+ }
80
+
81
+ export function rgbaToHsba({ r, g, b, a }: RgbaColor): HsbaColor {
82
+ const R = clamp(r, 0, 255) / 255;
83
+ const G = clamp(g, 0, 255) / 255;
84
+ const B = clamp(b, 0, 255) / 255;
85
+ const max = Math.max(R, G, B);
86
+ const min = Math.min(R, G, B);
87
+ const d = max - min;
88
+ let h = 0;
89
+ if (d !== 0) {
90
+ if (max === R) h = ((G - B) / d) % 6;
91
+ else if (max === G) h = (B - R) / d + 2;
92
+ else h = (R - G) / d + 4;
93
+ h *= 60;
94
+ if (h < 0) h += 360;
95
+ }
96
+ return { h, s: max === 0 ? 0 : d / max, b: max, a: clamp(a, 0, 1) };
97
+ }
98
+
99
+ function hslToHsba(h: number, s: number, l: number, a: number): HsbaColor {
100
+ const sat = clamp(s, 0, 1);
101
+ const lit = clamp(l, 0, 1);
102
+ const v = lit + sat * Math.min(lit, 1 - lit);
103
+ const sv = v === 0 ? 0 : 2 * (1 - lit / v);
104
+ return { h: wrapHue(h), s: sv, b: v, a: clamp(a, 0, 1) };
105
+ }
106
+
107
+ function expandHex(hex: string) {
108
+ if (hex.length === 3 || hex.length === 4) {
109
+ return hex
110
+ .split("")
111
+ .map((c) => c + c)
112
+ .join("");
113
+ }
114
+ return hex;
115
+ }
116
+
117
+ function parseHex(raw: string): HsbaColor | null {
118
+ const m = HEX.exec(raw.trim());
119
+ if (!m) return null;
120
+ const hex = expandHex(m[1]!.toLowerCase());
121
+ const r = parseInt(hex.slice(0, 2), 16);
122
+ const g = parseInt(hex.slice(2, 4), 16);
123
+ const b = parseInt(hex.slice(4, 6), 16);
124
+ const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1;
125
+ return rgbaToHsba({ r, g, b, a });
126
+ }
127
+
128
+ function parseCss(raw: string): HsbaColor | null {
129
+ const t = raw.trim();
130
+ if (!t) return null;
131
+ if (t.toLowerCase() === "transparent") return { h: 0, s: 0, b: 0, a: 0 };
132
+ const hex = parseHex(t);
133
+ if (hex) return hex;
134
+ const rgb = RGB.exec(t);
135
+ if (rgb) {
136
+ return rgbaToHsba({
137
+ r: parseChannel(rgb[1]!, 255),
138
+ g: parseChannel(rgb[2]!, 255),
139
+ b: parseChannel(rgb[3]!, 255),
140
+ a: parseAlpha(rgb[4]),
141
+ });
142
+ }
143
+ const hsl = HSL.exec(t);
144
+ if (hsl) return hslToHsba(parseFloat(hsl[1]!), parseFloat(hsl[2]!) / 100, parseFloat(hsl[3]!) / 100, parseAlpha(hsl[4]));
145
+ const hsb = HSB.exec(t) ?? HSV.exec(t);
146
+ if (hsb) {
147
+ return {
148
+ h: wrapHue(parseFloat(hsb[1]!)),
149
+ s: clamp(parseFloat(hsb[2]!) / 100, 0, 1),
150
+ b: clamp(parseFloat(hsb[3]!) / 100, 0, 1),
151
+ a: parseAlpha(hsb[4]),
152
+ };
153
+ }
154
+ return null;
155
+ }
156
+
157
+ export function isColor(value: unknown): value is Color {
158
+ return value instanceof Color;
159
+ }
160
+
161
+ export class Color {
162
+ private readonly meta: HsbaColor;
163
+
164
+ constructor(input?: ColorType | HsbaColor | RgbaColor | null) {
165
+ this.meta = toHsba(input);
166
+ }
167
+
168
+ toHsb(): HsbaColor {
169
+ return { h: this.meta.h, s: this.meta.s, b: this.meta.b, a: this.meta.a };
170
+ }
171
+
172
+ toRgb(): RgbaColor {
173
+ return hsbaToRgba(this.meta);
174
+ }
175
+
176
+ toHex(ignoreAlpha = false): string {
177
+ const { r, g, b, a } = this.toRgb();
178
+ const hex = `${hexByte(r)}${hexByte(g)}${hexByte(b)}`;
179
+ if (ignoreAlpha || a >= 1) return hex;
180
+ return `${hex}${hexByte(a * 255)}`;
181
+ }
182
+
183
+ toHexString(ignoreAlpha = false): string {
184
+ return `#${this.toHex(ignoreAlpha)}`;
185
+ }
186
+
187
+ toRgbString(): string {
188
+ const { r, g, b, a } = this.toRgb();
189
+ if (a < 1) return `rgba(${r}, ${g}, ${b}, ${round(a, 2)})`;
190
+ return `rgb(${r}, ${g}, ${b})`;
191
+ }
192
+
193
+ toHsbString(): string {
194
+ const { h, s, b, a } = this.meta;
195
+ const H = Math.round(wrapHue(h));
196
+ const S = Math.round(s * 100);
197
+ const B = Math.round(b * 100);
198
+ if (a < 1) return `hsba(${H}, ${S}%, ${B}%, ${round(a, 2)})`;
199
+ return `hsb(${H}, ${S}%, ${B}%)`;
200
+ }
201
+
202
+ toCssString(): string {
203
+ return this.toRgbString();
204
+ }
205
+
206
+ toString(format: ColorFormat = "hex"): string {
207
+ if (format === "rgb") return this.toRgbString();
208
+ if (format === "hsb") return this.toHsbString();
209
+ return this.toHexString();
210
+ }
211
+
212
+ static fromHsba(hsba: HsbaColor): Color {
213
+ return new Color(hsba);
214
+ }
215
+ }
216
+
217
+ function isHsbaLike(value: object): value is HsbaColor {
218
+ return "h" in value && "s" in value && "b" in value;
219
+ }
220
+
221
+ function isRgbaLike(value: object): value is RgbaColor {
222
+ return "r" in value && "g" in value && "b" in value;
223
+ }
224
+
225
+ function toHsba(input?: ColorType | HsbaColor | RgbaColor | null): HsbaColor {
226
+ if (input == null || input === "") return { h: 215, s: 0.91, b: 1, a: 1 };
227
+ if (isColor(input)) return input.toHsb();
228
+ if (typeof input === "string") return parseCss(input) ?? { h: 215, s: 0.91, b: 1, a: 1 };
229
+ if (isHsbaLike(input)) {
230
+ return { h: wrapHue(input.h), s: clamp(input.s, 0, 1), b: clamp(input.b, 0, 1), a: clamp(input.a ?? 1, 0, 1) };
231
+ }
232
+ if (isRgbaLike(input)) return rgbaToHsba({ r: input.r, g: input.g, b: input.b, a: input.a ?? 1 });
233
+ return { h: 215, s: 0.91, b: 1, a: 1 };
234
+ }
235
+
236
+ export function parseColor(input?: ColorType | null): Color | null {
237
+ if (input == null || input === "") return null;
238
+ if (isColor(input)) return input;
239
+ if (typeof input === "string") {
240
+ const parsed = parseCss(input);
241
+ return parsed ? Color.fromHsba(parsed) : null;
242
+ }
243
+ return new Color(input);
244
+ }
245
+
246
+ export const DEFAULT_COLOR = new Color("#1677ff");
@@ -16,8 +16,9 @@ import {
16
16
  import type { DateRange } from "react-day-picker";
17
17
  import { Calendar } from "../calendar/Calendar";
18
18
  import { Button } from "../button/Button";
19
+ import { usePopupContainer } from "../app/context";
19
20
  import { cn } from "../lib/cn";
20
- import { type ControlSize, controlFieldFocusBorder, controlFieldStyle, controlFieldSurface, controlFieldTransition, controlStatusClass, getSizeTokens } from "../lib/sizes";
21
+ import { type ControlSize, controlFieldFocusBorder, controlFieldStyle, controlFieldSurface, controlFieldTransition, controlStatusClass, getSizeTokens, useControlSize } from "../lib/sizes";
21
22
  import { glassOverlayClass } from "../lib/surface";
22
23
 
23
24
  export type DatePickerProps = {
@@ -29,7 +30,7 @@ export type DatePickerProps = {
29
30
  allowClear?: boolean;
30
31
  format?: string;
31
32
  showTime?: boolean;
32
- /** Keep the panel open until OK (antd). */
33
+ /** Keep the panel open until OK. */
33
34
  needConfirm?: boolean;
34
35
  placeholder?: string;
35
36
  disabled?: boolean;
@@ -127,6 +128,7 @@ const TriggerChrome = forwardRef<
127
128
  children: ReactNode;
128
129
  } & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children" | "style">
129
130
  >(function TriggerChrome({ size, status, disabled, className, style, icon, clear, children, ...rest }, ref) {
131
+ const resolvedSize = useControlSize(size);
130
132
  return (
131
133
  <button
132
134
  ref={ref}
@@ -141,7 +143,7 @@ const TriggerChrome = forwardRef<
141
143
  controlStatusClass(status),
142
144
  className,
143
145
  )}
144
- style={{ ...controlFieldStyle(size), ...style }}
146
+ style={{ ...controlFieldStyle(resolvedSize), ...style }}
145
147
  {...rest}
146
148
  >
147
149
  {icon}
@@ -185,7 +187,9 @@ const DatePickerRoot = forwardRef<HTMLButtonElement, DatePickerProps>(function D
185
187
  };
186
188
  const [month, setMonth] = useState<Date>(() => selected ?? new Date());
187
189
  const [draft, setDraft] = useState<Date | undefined>(selected);
188
- const tok = getSizeTokens(size);
190
+ const resolvedSize = useControlSize(size);
191
+ const tok = getSizeTokens(resolvedSize);
192
+ const portal = usePopupContainer()?.();
189
193
  const confirm = Boolean(needConfirm);
190
194
 
191
195
  useEffect(() => {
@@ -253,7 +257,7 @@ const DatePickerRoot = forwardRef<HTMLButtonElement, DatePickerProps>(function D
253
257
  <PopoverPrimitive.Trigger asChild>
254
258
  <TriggerChrome
255
259
  ref={ref}
256
- size={size}
260
+ size={resolvedSize}
257
261
  status={status}
258
262
  disabled={disabled}
259
263
  className={className}
@@ -277,7 +281,7 @@ const DatePickerRoot = forwardRef<HTMLButtonElement, DatePickerProps>(function D
277
281
  <span className={cn(!selected && "text-quaternary-foreground")}>{selected ? label : placeholder}</span>
278
282
  </TriggerChrome>
279
283
  </PopoverPrimitive.Trigger>
280
- <PopoverPrimitive.Portal>
284
+ <PopoverPrimitive.Portal container={portal}>
281
285
  <PopoverPrimitive.Content
282
286
  align="start"
283
287
  sideOffset={6}
@@ -351,7 +355,9 @@ const RangePicker = forwardRef<HTMLButtonElement, RangePickerProps>(function Ran
351
355
  const selected = controlled ? parseRange(value) : inner;
352
356
  const [open, setOpen] = useState(false);
353
357
  const [month, setMonth] = useState<Date>(() => selected?.from ?? new Date());
354
- const tok = getSizeTokens(size);
358
+ const resolvedSize = useControlSize(size);
359
+ const tok = getSizeTokens(resolvedSize);
360
+ const portal = usePopupContainer()?.();
355
361
 
356
362
  useEffect(() => {
357
363
  if (selected?.from) setMonth(selected.from);
@@ -390,7 +396,7 @@ const RangePicker = forwardRef<HTMLButtonElement, RangePickerProps>(function Ran
390
396
  <PopoverPrimitive.Trigger asChild>
391
397
  <TriggerChrome
392
398
  ref={ref}
393
- size={size}
399
+ size={resolvedSize}
394
400
  status={status}
395
401
  disabled={disabled}
396
402
  className={className}
@@ -418,7 +424,7 @@ const RangePicker = forwardRef<HTMLButtonElement, RangePickerProps>(function Ran
418
424
  </span>
419
425
  </TriggerChrome>
420
426
  </PopoverPrimitive.Trigger>
421
- <PopoverPrimitive.Portal>
427
+ <PopoverPrimitive.Portal container={portal}>
422
428
  <PopoverPrimitive.Content
423
429
  align="start"
424
430
  sideOffset={6}
@@ -78,7 +78,7 @@ export function DesktopHeader({
78
78
  profile?: ReactNode;
79
79
  }) {
80
80
  return (
81
- <header className="fixed inset-x-0 top-0 z-40 flex h-desktop-bar items-center justify-between gap-3 border-0 bg-glass-bar px-3 backdrop-blur-lg">
81
+ <header className="nonla-header-layer fixed inset-x-0 top-0 flex h-desktop-bar items-center justify-between gap-3 border-0 bg-glass-bar px-3 backdrop-blur-lg">
82
82
  <div className="flex min-w-0 items-center">
83
83
  {logo}
84
84
  {leading}
@@ -6,9 +6,7 @@ const iconButtonClass =
6
6
 
7
7
  const labelClass = "text-xs leading-[18px] font-semibold text-center line-clamp-2 w-full";
8
8
 
9
- function plateClass(active: boolean) {
10
- return `w-11 h-11 flex items-center justify-center filter-[drop-shadow(0_1px_2px_rgba(0,0,0,0.45))] ${active ? "ring-2 ring-brand rounded-[10px]" : ""}`;
11
- }
9
+ const plateClass = "nonla-window-glass nonla-desktop-icon-plate w-11 h-11 flex items-center justify-center";
12
10
 
13
11
  export const DesktopIcon = forwardRef<
14
12
  HTMLButtonElement,
@@ -24,7 +22,7 @@ export const DesktopIcon = forwardRef<
24
22
  return (
25
23
  <button ref={ref} type="button" title={label} aria-label={label} aria-current={active ? "true" : undefined} dir="ltr" onClick={onClick} className={iconButtonClass}>
26
24
  {media ?? (
27
- <span className={plateClass(active)}>
25
+ <span className={plateClass}>
28
26
  <FluentIcon name={icon ?? "sparkle-24"} size={32} />
29
27
  </span>
30
28
  )}
@@ -1,6 +1,7 @@
1
1
  import { type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode, createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
  import { cn } from "../lib/cn";
4
+ import { OverlayScroll, type OverlayScrollVisibility } from "../scroll/OverlayScroll";
4
5
  import { glassSurfaceClass } from "../lib/surface";
5
6
 
6
7
  type Phase = "open" | "leaving";
@@ -211,12 +212,17 @@ export function DesktopWindow({
211
212
  expanded,
212
213
  onClose,
213
214
  onToggleExpand,
215
+ scroll = true,
216
+ scrollbar = "hover",
214
217
  children,
215
218
  }: {
216
219
  title?: ReactNode;
217
220
  expanded: boolean;
218
221
  onClose: () => void;
219
222
  onToggleExpand: () => void;
223
+ /** Overlay auto-hide scrollbar on the body. Pass `false` when children manage their own scroll (split panes, AgentPanel). */
224
+ scroll?: boolean;
225
+ scrollbar?: OverlayScrollVisibility;
220
226
  children: ReactNode;
221
227
  }) {
222
228
  const overlayRef = useRef<HTMLDivElement>(null);
@@ -444,7 +450,7 @@ export function DesktopWindow({
444
450
 
445
451
  return (
446
452
  <WindowHeaderSlotContext.Provider value={headerChrome}>
447
- <div ref={overlayRef} className="pointer-events-none absolute inset-x-0 bottom-0 top-desktop-bar z-30">
453
+ <div ref={overlayRef} className="nonla-window-layer pointer-events-none absolute inset-x-0 bottom-0 top-desktop-bar">
448
454
  <section
449
455
  ref={frameRef}
450
456
  aria-label={typeof title === "string" && title ? title : "Window"}
@@ -464,16 +470,16 @@ export function DesktopWindow({
464
470
  opacity: visible ? 1 : 0,
465
471
  transform: visible ? "scale(1)" : "scale(0.18)",
466
472
  transition: [geom, fade].filter(Boolean).join(", ") || undefined,
467
- ["--glass" as string]: "color-mix(in srgb, white 72%, transparent)",
468
473
  }}
469
474
  data-expanded={expanded || undefined}
470
- className={cn("absolute flex flex-col rounded-xl pointer-events-auto transform-gpu", glassSurfaceClass, leaving && "pointer-events-none")}
475
+ className={cn("absolute flex flex-col rounded-xl pointer-events-auto transform-gpu", glassSurfaceClass, "nonla-window-glass", leaving && "pointer-events-none")}
471
476
  >
472
477
  <div className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl">
478
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: title bar is a pointer drag surface */}
473
479
  <header
474
480
  onPointerDown={startDrag}
475
481
  onDoubleClick={onTitleBarDoubleClick}
476
- className="flex h-8 shrink-0 cursor-default items-center gap-4 border-0 border-b border-solid border-glass-border px-3 select-none touch-none"
482
+ className="flex h-8 shrink-0 cursor-default items-center gap-4 border-0 border-b border-solid border-ink-line px-3 select-none touch-none"
477
483
  >
478
484
  <div className="group/traffic flex shrink-0 items-center gap-2">
479
485
  <TrafficLight label="Close window" tone="close" onClick={requestClose} shortcut="Esc">
@@ -489,7 +495,15 @@ export function DesktopWindow({
489
495
  </div>
490
496
  </header>
491
497
 
492
- <div className="min-h-0 flex-1 overflow-hidden">{children}</div>
498
+ <div className="min-h-0 flex-1 overflow-hidden">
499
+ {scroll ? (
500
+ <OverlayScroll visibility={scrollbar} className="h-full">
501
+ {children}
502
+ </OverlayScroll>
503
+ ) : (
504
+ children
505
+ )}
506
+ </div>
493
507
  </div>
494
508
 
495
509
  {!expanded && !leaving
@@ -5,8 +5,8 @@ export function MeadowDesktop({ children, src }: { children?: ReactNode; src?: s
5
5
  return (
6
6
  <div className="absolute inset-0 bg-meadow">
7
7
  <MeadowWallpaper src={src} />
8
- <div className="absolute inset-x-0 bottom-0 top-desktop-bar z-20">
9
- <nav aria-label="Desktop" className="absolute inset-0 overflow-auto px-3 pt-8 pb-3">
8
+ <div className="nonla-desktop-layer absolute inset-x-0 bottom-0 top-desktop-bar">
9
+ <nav aria-label="Desktop" className="absolute inset-0 overflow-auto pl-2 pr-3 pt-2 pb-3">
10
10
  <div className="flex h-full flex-col flex-wrap content-start gap-x-2 gap-y-3">{children}</div>
11
11
  </nav>
12
12
  </div>
@@ -1,5 +1,6 @@
1
1
  import * as Dialog from "@radix-ui/react-dialog";
2
2
  import { type AnimationEvent, type CSSProperties, type ReactNode, useEffect, useRef, useState } from "react";
3
+ import { usePopupContainer } from "../app/context";
3
4
  import { cn } from "../lib/cn";
4
5
  import { glassSurfaceClass } from "../lib/surface";
5
6
 
@@ -17,7 +18,7 @@ export type DrawerProps = {
17
18
  extra?: ReactNode;
18
19
  children?: ReactNode;
19
20
  width?: number | string;
20
- /** antd: width for left/right, height for top/bottom. */
21
+ /** Width for left/right, height for top/bottom. */
21
22
  size?: number | string;
22
23
  placement?: DrawerPlacement;
23
24
  destroyOnClose?: boolean;
@@ -66,6 +67,7 @@ export function Drawer({
66
67
  }, [isOpen, shouldDestroy]);
67
68
 
68
69
  const showBody = !shouldDestroy || present;
70
+ const portal = usePopupContainer()?.();
69
71
 
70
72
  const onContentAnimationEnd = (e: AnimationEvent<HTMLDivElement>) => {
71
73
  if (e.target !== e.currentTarget) return;
@@ -80,17 +82,17 @@ export function Drawer({
80
82
  if (!next) onClose?.();
81
83
  }}
82
84
  >
83
- <Dialog.Portal>
84
- <Dialog.Overlay className="nonla-overlay z-[9980]" />
85
+ <Dialog.Portal container={portal}>
86
+ <Dialog.Overlay className="nonla-overlay" />
85
87
  <Dialog.Content
86
88
  data-side={placement}
87
89
  className={cn(
88
- "nonla-drawer-panel fixed z-[9981] flex flex-col outline-none",
90
+ "nonla-drawer-panel fixed flex flex-col outline-none",
89
91
  glassSurfaceClass,
90
92
  placement === "right" && "top-0 right-0 bottom-0 max-w-[100vw]",
91
93
  placement === "left" && "top-0 left-0 bottom-0 max-w-[100vw]",
92
- placement === "bottom" && "right-0 bottom-0 left-0 max-h-[100vh]",
93
- placement === "top" && "top-0 right-0 left-0 max-h-[100vh]",
94
+ placement === "bottom" && "right-0 bottom-0 left-0 max-h-screen",
95
+ placement === "top" && "top-0 right-0 left-0 max-h-screen",
94
96
  className,
95
97
  )}
96
98
  style={{
@@ -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
- /** antd 5.25+ alias — accepted, no-op (Radix unmounts when closed). */
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
- // eslint-disable-next-line eqeqeq
23
+ // biome-ignore lint/suspicious/noDoubleEquals: form condition operator is loose equality
24
24
  return fieldValue == compareValue;
25
25
  case "!=":
26
- // eslint-disable-next-line eqeqeq
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);
@@ -111,10 +111,10 @@ export function FormItem(props: FormItemProps) {
111
111
  </svg>
112
112
  </button>
113
113
  ) : null}
114
- <label className="text-sm text-foreground">
114
+ <div className="text-sm text-foreground">
115
115
  {label}
116
116
  {required ? <span className="ml-1 text-destructive">*</span> : null}
117
- </label>
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, index) => (
27
- <FormItemHelp key={`${index}-${item.text}`} text={item.text} className={item.className} iconClassName={item.iconClassName} />
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
  );