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.
- 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/colorpicker/ColorPicker.tsx +694 -30
- package/src/colorpicker/color.ts +246 -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 +16 -51
- 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 +25 -8
- 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/splitter/Splitter.tsx +330 -0
- package/src/splitter/sizes.ts +67 -0
- package/src/styles.css +96 -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
|
@@ -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
|
|
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(
|
|
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
|
|
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={
|
|
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
|
|
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={
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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-
|
|
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">
|
|
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
|
|
9
|
-
<nav aria-label="Desktop" className="absolute inset-0 overflow-auto
|
|
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>
|
package/src/drawer/Drawer.tsx
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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
|
|
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-
|
|
93
|
-
placement === "top" && "top-0 right-0 left-0 max-h-
|
|
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
|
-
/**
|
|
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
|
);
|