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