@noya-app/noya-designsystem 0.1.46 → 0.1.48
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +16 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +208 -98
- package/dist/index.d.ts +208 -98
- package/dist/index.js +2148 -1604
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2153 -1618
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -7
- package/src/__tests__/validateDropIndicator.test.ts +263 -0
- package/src/components/ActionMenu.tsx +40 -0
- package/src/components/ActivityIndicator.tsx +7 -1
- package/src/components/Collection.tsx +12 -2
- package/src/components/Combobox.tsx +14 -1
- package/src/components/ComboboxMenu.tsx +14 -5
- package/src/components/Dialog.tsx +28 -31
- package/src/components/Drawer.tsx +98 -0
- package/src/components/Grid.tsx +57 -27
- package/src/components/GridView.tsx +39 -25
- package/src/components/InputField.tsx +87 -92
- package/src/components/Label.tsx +7 -7
- package/src/components/List.tsx +42 -16
- package/src/components/ListView.tsx +21 -17
- package/src/components/MediaThumbnail.tsx +117 -0
- package/src/components/SearchCompletionMenu.tsx +31 -37
- package/src/components/SelectMenu.tsx +19 -17
- package/src/components/Sortable.tsx +70 -44
- package/src/components/Switch.tsx +1 -1
- package/src/components/internal/Menu.tsx +38 -23
- package/src/components/internal/MenuViewport.tsx +7 -1
- package/src/components/internal/SelectItem.tsx +51 -27
- package/src/components/internal/__tests__/Menu.test.tsx +12 -0
- package/src/components/workspace/DrawerWorkspaceLayout.tsx +86 -0
- package/src/components/workspace/PanelWorkspaceLayout.tsx +102 -0
- package/src/components/{WorkspaceLayout.tsx → workspace/WorkspaceLayout.tsx} +109 -106
- package/src/components/workspace/types.ts +31 -0
- package/src/contexts/DialogContext.tsx +91 -30
- package/src/hooks/usePreservePanelSize.tsx +1 -5
- package/src/index.css +3 -1
- package/src/index.tsx +4 -1
- package/tailwind.config.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noya-app/noya-designsystem",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"module": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -20,12 +20,11 @@
|
|
|
20
20
|
"lint": "eslint . --max-warnings 0"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@dnd-kit/core": "3.1
|
|
24
|
-
"@dnd-kit/
|
|
25
|
-
"@
|
|
26
|
-
"@noya-app/noya-
|
|
27
|
-
"@noya-app/noya-
|
|
28
|
-
"@noya-app/noya-geometry": "0.1.7",
|
|
23
|
+
"@dnd-kit/core": "6.3.1",
|
|
24
|
+
"@dnd-kit/sortable": "10.0.0",
|
|
25
|
+
"@noya-app/noya-colorpicker": "0.1.18",
|
|
26
|
+
"@noya-app/noya-utils": "0.1.4",
|
|
27
|
+
"@noya-app/noya-geometry": "0.1.8",
|
|
29
28
|
"@noya-app/noya-icons": "0.1.6",
|
|
30
29
|
"@noya-app/noya-keymap": "0.1.3",
|
|
31
30
|
"@radix-ui/primitive": "^1.0.0",
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
defaultAcceptsDrop,
|
|
4
|
+
validateDropIndicator,
|
|
5
|
+
} from "../components/Sortable";
|
|
6
|
+
|
|
7
|
+
describe("defaultAcceptsDrop", () => {
|
|
8
|
+
it("should return false if the active item is the same as the over item", () => {
|
|
9
|
+
const result = defaultAcceptsDrop(0, 0, "inside");
|
|
10
|
+
expect(result).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should return false for 'inside' position", () => {
|
|
14
|
+
const result = defaultAcceptsDrop(0, 1, "inside");
|
|
15
|
+
expect(result).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should return false when dropping above the same item", () => {
|
|
19
|
+
const result = defaultAcceptsDrop(0, 0, "above");
|
|
20
|
+
expect(result).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should return false when dropping below the same item", () => {
|
|
24
|
+
const result = defaultAcceptsDrop(0, 0, "below");
|
|
25
|
+
expect(result).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should return false when dropping above the item below the source", () => {
|
|
29
|
+
const result = defaultAcceptsDrop(0, 1, "above");
|
|
30
|
+
expect(result).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should return false when dropping below the item above the source", () => {
|
|
34
|
+
const result = defaultAcceptsDrop(1, 0, "below");
|
|
35
|
+
expect(result).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should return true when dropping above a valid target", () => {
|
|
39
|
+
const result = defaultAcceptsDrop(0, 2, "above");
|
|
40
|
+
expect(result).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should return true when dropping below a valid target", () => {
|
|
44
|
+
const result = defaultAcceptsDrop(2, 0, "below");
|
|
45
|
+
expect(result).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("validateDropIndicator", () => {
|
|
50
|
+
const mockAcceptsDrop = (
|
|
51
|
+
sourceIndex: number,
|
|
52
|
+
destinationIndex: number,
|
|
53
|
+
position: "above" | "below" | "inside"
|
|
54
|
+
) => {
|
|
55
|
+
// Mock that only allows dropping inside
|
|
56
|
+
if (position === "inside") return true;
|
|
57
|
+
return false;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const mockAcceptsDropAboveBelow = (
|
|
61
|
+
sourceIndex: number,
|
|
62
|
+
destinationIndex: number,
|
|
63
|
+
position: "above" | "below" | "inside"
|
|
64
|
+
) => {
|
|
65
|
+
// Mock that only allows dropping above/below
|
|
66
|
+
if (position === "inside") return false;
|
|
67
|
+
return true;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const mockAcceptsAll = (
|
|
71
|
+
sourceIndex: number,
|
|
72
|
+
destinationIndex: number,
|
|
73
|
+
position: "above" | "below" | "inside"
|
|
74
|
+
) => {
|
|
75
|
+
// Mock that allows all drop positions
|
|
76
|
+
return true;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const mockAcceptsAboveAndInside = (
|
|
80
|
+
sourceIndex: number,
|
|
81
|
+
destinationIndex: number,
|
|
82
|
+
position: "above" | "below" | "inside"
|
|
83
|
+
) => {
|
|
84
|
+
// Mock that allows dropping above and inside, but not below
|
|
85
|
+
return position !== "below";
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const keys = ["item1", "item2", "item3"];
|
|
89
|
+
|
|
90
|
+
describe("with above and inside positions allowed, but not below", () => {
|
|
91
|
+
it("should return 'inside' when cursor is in the middle third", () => {
|
|
92
|
+
const result = validateDropIndicator({
|
|
93
|
+
acceptsDrop: mockAcceptsAboveAndInside,
|
|
94
|
+
keys,
|
|
95
|
+
activeId: "item1",
|
|
96
|
+
overId: "item2",
|
|
97
|
+
offsetStart: 50, // offsetStart in middle third
|
|
98
|
+
elementStart: 0, // elementStart
|
|
99
|
+
elementSize: 100, // elementSize
|
|
100
|
+
});
|
|
101
|
+
expect(result).toBe("inside");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should return 'above' when cursor is in the top third", () => {
|
|
105
|
+
const result = validateDropIndicator({
|
|
106
|
+
acceptsDrop: mockAcceptsAboveAndInside,
|
|
107
|
+
keys,
|
|
108
|
+
activeId: "item1",
|
|
109
|
+
overId: "item2",
|
|
110
|
+
offsetStart: 20, // offsetStart in top third
|
|
111
|
+
elementStart: 0, // elementStart
|
|
112
|
+
elementSize: 100, // elementSize
|
|
113
|
+
});
|
|
114
|
+
expect(result).toBe("above");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should fallback to 'inside' when cursor is in bottom third since 'below' is not allowed", () => {
|
|
118
|
+
const result = validateDropIndicator({
|
|
119
|
+
acceptsDrop: mockAcceptsAboveAndInside,
|
|
120
|
+
keys,
|
|
121
|
+
activeId: "item1",
|
|
122
|
+
overId: "item2",
|
|
123
|
+
offsetStart: 80, // offsetStart in bottom third
|
|
124
|
+
elementStart: 0, // elementStart
|
|
125
|
+
elementSize: 100, // elementSize
|
|
126
|
+
});
|
|
127
|
+
expect(result).toBe("inside");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("with all drop positions allowed", () => {
|
|
132
|
+
it("should prefer 'inside' when cursor is in the middle third", () => {
|
|
133
|
+
const result = validateDropIndicator({
|
|
134
|
+
acceptsDrop: mockAcceptsAll,
|
|
135
|
+
keys,
|
|
136
|
+
activeId: "item1",
|
|
137
|
+
overId: "item2",
|
|
138
|
+
offsetStart: 50, // offsetStart in middle third
|
|
139
|
+
elementStart: 0, // elementStart
|
|
140
|
+
elementSize: 100, // elementSize
|
|
141
|
+
});
|
|
142
|
+
expect(result).toBe("inside");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should prefer 'above' when cursor is in the top third", () => {
|
|
146
|
+
const result = validateDropIndicator({
|
|
147
|
+
acceptsDrop: mockAcceptsAll,
|
|
148
|
+
keys,
|
|
149
|
+
activeId: "item1",
|
|
150
|
+
overId: "item2",
|
|
151
|
+
offsetStart: 20, // offsetStart in top third
|
|
152
|
+
elementStart: 0, // elementStart
|
|
153
|
+
elementSize: 100, // elementSize
|
|
154
|
+
});
|
|
155
|
+
expect(result).toBe("above");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should prefer 'below' when cursor is in the bottom third", () => {
|
|
159
|
+
const result = validateDropIndicator({
|
|
160
|
+
acceptsDrop: mockAcceptsAll,
|
|
161
|
+
keys,
|
|
162
|
+
activeId: "item1",
|
|
163
|
+
overId: "item2",
|
|
164
|
+
offsetStart: 80, // offsetStart in bottom third
|
|
165
|
+
elementStart: 0, // elementStart
|
|
166
|
+
elementSize: 100, // elementSize
|
|
167
|
+
});
|
|
168
|
+
expect(result).toBe("below");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should handle non-zero elementStart position", () => {
|
|
172
|
+
const result = validateDropIndicator({
|
|
173
|
+
acceptsDrop: mockAcceptsAll,
|
|
174
|
+
keys,
|
|
175
|
+
activeId: "item1",
|
|
176
|
+
overId: "item2",
|
|
177
|
+
offsetStart: 120, // offsetStart in middle third relative to elementStart
|
|
178
|
+
elementStart: 100, // elementStart
|
|
179
|
+
elementSize: 60, // elementSize
|
|
180
|
+
});
|
|
181
|
+
expect(result).toBe("inside");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("should return 'inside' when cursor is in the middle third and inside drops are allowed", () => {
|
|
186
|
+
const result = validateDropIndicator({
|
|
187
|
+
acceptsDrop: mockAcceptsDrop,
|
|
188
|
+
keys,
|
|
189
|
+
activeId: "item1",
|
|
190
|
+
overId: "item2",
|
|
191
|
+
offsetStart: 50, // offsetStart in middle third
|
|
192
|
+
elementStart: 0, // elementStart
|
|
193
|
+
elementSize: 100, // elementSize
|
|
194
|
+
});
|
|
195
|
+
expect(result).toBe("inside");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should return 'above' when cursor is in top half and above drops are allowed", () => {
|
|
199
|
+
const result = validateDropIndicator({
|
|
200
|
+
acceptsDrop: mockAcceptsDropAboveBelow,
|
|
201
|
+
keys,
|
|
202
|
+
activeId: "item1",
|
|
203
|
+
overId: "item2",
|
|
204
|
+
offsetStart: 20, // offsetStart in top half
|
|
205
|
+
elementStart: 0, // elementStart
|
|
206
|
+
elementSize: 100, // elementSize
|
|
207
|
+
});
|
|
208
|
+
expect(result).toBe("above");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should return 'below' when cursor is in bottom half and below drops are allowed", () => {
|
|
212
|
+
const result = validateDropIndicator({
|
|
213
|
+
acceptsDrop: mockAcceptsDropAboveBelow,
|
|
214
|
+
keys,
|
|
215
|
+
activeId: "item1",
|
|
216
|
+
overId: "item2",
|
|
217
|
+
offsetStart: 70, // offsetStart in bottom half
|
|
218
|
+
elementStart: 0, // elementStart
|
|
219
|
+
elementSize: 100, // elementSize
|
|
220
|
+
});
|
|
221
|
+
expect(result).toBe("below");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should fallback to inside when above/below not allowed and inside is allowed", () => {
|
|
225
|
+
const result = validateDropIndicator({
|
|
226
|
+
acceptsDrop: mockAcceptsDrop,
|
|
227
|
+
keys,
|
|
228
|
+
activeId: "item1",
|
|
229
|
+
overId: "item2",
|
|
230
|
+
offsetStart: 20, // offsetStart in top half
|
|
231
|
+
elementStart: 0, // elementStart
|
|
232
|
+
elementSize: 100, // elementSize
|
|
233
|
+
});
|
|
234
|
+
expect(result).toBe("inside");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("should return undefined when no drop positions are allowed", () => {
|
|
238
|
+
const neverAllowDrop = () => false;
|
|
239
|
+
const result = validateDropIndicator({
|
|
240
|
+
acceptsDrop: neverAllowDrop,
|
|
241
|
+
keys,
|
|
242
|
+
activeId: "item1",
|
|
243
|
+
overId: "item2",
|
|
244
|
+
offsetStart: 50,
|
|
245
|
+
elementStart: 0,
|
|
246
|
+
elementSize: 100,
|
|
247
|
+
});
|
|
248
|
+
expect(result).toBeUndefined();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("should ignore items not in the keys array", () => {
|
|
252
|
+
const result = validateDropIndicator({
|
|
253
|
+
acceptsDrop: mockAcceptsDrop,
|
|
254
|
+
keys,
|
|
255
|
+
activeId: "nonexistent",
|
|
256
|
+
overId: "item2",
|
|
257
|
+
offsetStart: 50,
|
|
258
|
+
elementStart: 0,
|
|
259
|
+
elementSize: 100,
|
|
260
|
+
});
|
|
261
|
+
expect(result).toBeUndefined();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { memoGeneric } from "@noya-app/react-utils";
|
|
2
|
+
import React, { useMemo } from "react";
|
|
3
|
+
import { cssVars } from "../theme";
|
|
4
|
+
import { DropdownMenu } from "./DropdownMenu";
|
|
5
|
+
import { IconButton } from "./IconButton";
|
|
6
|
+
import { MenuItem } from "./internal/Menu";
|
|
7
|
+
|
|
8
|
+
type ActionMenuProps<TMenu extends string> = {
|
|
9
|
+
menuItems: MenuItem<TMenu>[];
|
|
10
|
+
onSelect: (action: TMenu) => void;
|
|
11
|
+
selected: boolean;
|
|
12
|
+
onOpenChange: (open: boolean) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const ActionMenu = memoGeneric(function ActionMenu<
|
|
16
|
+
TMenu extends string,
|
|
17
|
+
>({ menuItems, onSelect, selected, onOpenChange }: ActionMenuProps<TMenu>) {
|
|
18
|
+
const style = useMemo(() => {
|
|
19
|
+
return {
|
|
20
|
+
backgroundColor: selected
|
|
21
|
+
? cssVars.colors.primaryPastel
|
|
22
|
+
: cssVars.colors.inputBackground,
|
|
23
|
+
padding: "4px",
|
|
24
|
+
};
|
|
25
|
+
}, [selected]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<DropdownMenu
|
|
29
|
+
items={menuItems}
|
|
30
|
+
onSelect={onSelect}
|
|
31
|
+
onOpenChange={onOpenChange}
|
|
32
|
+
>
|
|
33
|
+
<IconButton
|
|
34
|
+
style={style}
|
|
35
|
+
iconName="DotsVerticalIcon"
|
|
36
|
+
color={selected ? cssVars.colors.primary : undefined}
|
|
37
|
+
/>
|
|
38
|
+
</DropdownMenu>
|
|
39
|
+
);
|
|
40
|
+
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import { cx } from "../utils/classNames";
|
|
2
3
|
|
|
3
4
|
interface Props {
|
|
4
5
|
size?: number;
|
|
5
6
|
opacity?: number;
|
|
6
7
|
color?: string;
|
|
7
8
|
trackColor?: string;
|
|
9
|
+
className?: string;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export const ActivityIndicator = React.memo(function ActivityIndicator({
|
|
@@ -12,6 +14,7 @@ export const ActivityIndicator = React.memo(function ActivityIndicator({
|
|
|
12
14
|
opacity = 1,
|
|
13
15
|
color = "black",
|
|
14
16
|
trackColor = "rgba(0, 0, 0, 0.1)",
|
|
17
|
+
className,
|
|
15
18
|
}: Props) {
|
|
16
19
|
const opacityStyle = React.useMemo(() => ({ opacity }), [opacity]);
|
|
17
20
|
const dynamicStyles = React.useMemo(
|
|
@@ -24,7 +27,10 @@ export const ActivityIndicator = React.memo(function ActivityIndicator({
|
|
|
24
27
|
[size, color, trackColor]
|
|
25
28
|
);
|
|
26
29
|
return (
|
|
27
|
-
<div
|
|
30
|
+
<div
|
|
31
|
+
className={cx("flex justify-center items-center", className)}
|
|
32
|
+
style={opacityStyle}
|
|
33
|
+
>
|
|
28
34
|
<div style={dynamicStyles} className="animate-spin rounded-[50%]" />
|
|
29
35
|
</div>
|
|
30
36
|
);
|
|
@@ -11,6 +11,11 @@ export type CollectionRef = {
|
|
|
11
11
|
editName: (id: string) => void;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
export type CollectionThumbnailProps<T> = {
|
|
15
|
+
item: T;
|
|
16
|
+
selected: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
14
19
|
export interface CollectionProps<T, M extends string = string> {
|
|
15
20
|
className?: string;
|
|
16
21
|
items: T[];
|
|
@@ -30,8 +35,11 @@ export interface CollectionProps<T, M extends string = string> {
|
|
|
30
35
|
menuItems?: MenuItem<M>[];
|
|
31
36
|
onSelectMenuItem?: (action: M, selectedItems: T[]) => void;
|
|
32
37
|
onRename?: (item: T, newName: string) => void;
|
|
33
|
-
renderThumbnail?: (
|
|
34
|
-
|
|
38
|
+
renderThumbnail?: ({
|
|
39
|
+
item,
|
|
40
|
+
selected,
|
|
41
|
+
}: CollectionThumbnailProps<T>) => React.ReactNode;
|
|
42
|
+
renderAction?: "menu" | ((item: T, selected: boolean) => React.ReactNode);
|
|
35
43
|
renderDetail?: (item: T, selected: boolean) => React.ReactNode;
|
|
36
44
|
/** Callback when selection changes. If not provided, selection will be disabled. */
|
|
37
45
|
onSelectionChange?: (
|
|
@@ -57,7 +65,9 @@ export interface CollectionProps<T, M extends string = string> {
|
|
|
57
65
|
/** Currently selected file IDs */
|
|
58
66
|
selectedIds?: string[];
|
|
59
67
|
viewType?: CollectionViewType;
|
|
68
|
+
onClickItem?: (itemId: string) => void;
|
|
60
69
|
onDoubleClickItem?: (itemId: string) => void;
|
|
70
|
+
renderEmptyState?: () => React.ReactElement;
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
export const Collection = forwardRefGeneric(function Collection<
|
|
@@ -27,6 +27,10 @@ import {
|
|
|
27
27
|
import { ListView } from "./ListView";
|
|
28
28
|
import { Small } from "./Text";
|
|
29
29
|
|
|
30
|
+
const comboboxButtonStyle = {
|
|
31
|
+
right: "2px",
|
|
32
|
+
};
|
|
33
|
+
|
|
30
34
|
export type BaseComboboxProps<T extends string> = {
|
|
31
35
|
id?: string;
|
|
32
36
|
loading?: boolean;
|
|
@@ -441,6 +445,10 @@ export const Combobox = memoGeneric(
|
|
|
441
445
|
[filteredItems]
|
|
442
446
|
);
|
|
443
447
|
|
|
448
|
+
const hasCheckedItems = items.some(
|
|
449
|
+
(item) => isSelectableMenuItem(item) && item.checked
|
|
450
|
+
);
|
|
451
|
+
|
|
444
452
|
return (
|
|
445
453
|
<InputField.Root
|
|
446
454
|
id={id}
|
|
@@ -475,6 +483,7 @@ export const Combobox = memoGeneric(
|
|
|
475
483
|
style={{
|
|
476
484
|
flex: `0 0 ${height}px`,
|
|
477
485
|
}}
|
|
486
|
+
indented={hasCheckedItems}
|
|
478
487
|
/>
|
|
479
488
|
) : (
|
|
480
489
|
<div className="flex flex-col h-[50px] p-5 items-center justify-center">
|
|
@@ -525,7 +534,11 @@ export const Combobox = memoGeneric(
|
|
|
525
534
|
</InputField.Label>
|
|
526
535
|
)}
|
|
527
536
|
{!readOnly && (
|
|
528
|
-
<InputField.Button
|
|
537
|
+
<InputField.Button
|
|
538
|
+
variant="floating"
|
|
539
|
+
onClick={handleChevronClick}
|
|
540
|
+
style={comboboxButtonStyle}
|
|
541
|
+
>
|
|
529
542
|
<DropdownChevronIcon
|
|
530
543
|
className={cx(
|
|
531
544
|
"transition-transform duration-100",
|
|
@@ -9,6 +9,7 @@ import { renderIcon } from "./Icons";
|
|
|
9
9
|
import { IVirtualizedList, ListView } from "./ListView";
|
|
10
10
|
import { Spacer } from "./Spacer";
|
|
11
11
|
import {
|
|
12
|
+
CHECKBOX_INDENT_WIDTH,
|
|
12
13
|
CHECKBOX_RIGHT_INSET,
|
|
13
14
|
CHECKBOX_WIDTH,
|
|
14
15
|
KeyboardShortcut,
|
|
@@ -23,6 +24,7 @@ interface ComboboxMenuProps<T extends string> {
|
|
|
23
24
|
onHoverIndex: (index: number) => void;
|
|
24
25
|
listSize: Size;
|
|
25
26
|
style?: React.CSSProperties;
|
|
27
|
+
indented?: boolean;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export const ComboboxMenu = memoGeneric(
|
|
@@ -34,13 +36,10 @@ export const ComboboxMenu = memoGeneric(
|
|
|
34
36
|
onHoverIndex,
|
|
35
37
|
listSize,
|
|
36
38
|
style,
|
|
39
|
+
indented,
|
|
37
40
|
}: ComboboxMenuProps<T>,
|
|
38
41
|
forwardedRef: ForwardedRef<IVirtualizedList>
|
|
39
42
|
) {
|
|
40
|
-
const hasCheckedItems = items
|
|
41
|
-
.filter((item) => item.type === undefined)
|
|
42
|
-
.some((item) => item.checked);
|
|
43
|
-
|
|
44
43
|
return (
|
|
45
44
|
<ListView.Root
|
|
46
45
|
ref={forwardedRef}
|
|
@@ -59,6 +58,15 @@ export const ComboboxMenu = memoGeneric(
|
|
|
59
58
|
if (item.type === "sectionHeader") {
|
|
60
59
|
return (
|
|
61
60
|
<ListView.Row key={item.id} isSectionHeader>
|
|
61
|
+
{indented && (
|
|
62
|
+
<Spacer.Horizontal
|
|
63
|
+
size={
|
|
64
|
+
CHECKBOX_WIDTH -
|
|
65
|
+
CHECKBOX_RIGHT_INSET +
|
|
66
|
+
CHECKBOX_INDENT_WIDTH
|
|
67
|
+
}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
62
70
|
{item.title}
|
|
63
71
|
</ListView.Row>
|
|
64
72
|
);
|
|
@@ -82,11 +90,12 @@ export const ComboboxMenu = memoGeneric(
|
|
|
82
90
|
}}
|
|
83
91
|
>
|
|
84
92
|
<div className="flex flex-1 min-w-0 items-center">
|
|
93
|
+
{indented && <Spacer.Horizontal size={CHECKBOX_INDENT_WIDTH} />}
|
|
85
94
|
{item.checked ? (
|
|
86
95
|
<div className={styles.itemIndicatorStyle}>
|
|
87
96
|
<CheckIcon />
|
|
88
97
|
</div>
|
|
89
|
-
) :
|
|
98
|
+
) : indented ? (
|
|
90
99
|
<Spacer.Horizontal
|
|
91
100
|
size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET}
|
|
92
101
|
/>
|
|
@@ -25,22 +25,27 @@ const StyledOverlay = forwardRef<
|
|
|
25
25
|
const StyledContent = forwardRef<
|
|
26
26
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
|
27
27
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
|
28
|
-
>(({ className, ...props }, ref) =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
>(({ className, children, ...props }, ref) => {
|
|
29
|
+
return (
|
|
30
|
+
<DialogPrimitive.Content
|
|
31
|
+
ref={ref}
|
|
32
|
+
className={cx(
|
|
33
|
+
`
|
|
33
34
|
fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2
|
|
34
35
|
w-[90vw] max-w-[450px] max-h-[85vh] p-dialog-padding rounded-[2px]
|
|
35
36
|
font-sans text-heading5 font-normal leading-[19px] text-text-muted
|
|
36
|
-
bg-popover-background
|
|
37
|
-
shadow-[0_10px_38px_-10px_hsla(206,22%,7%,.35),0_10px_20px_-15px_hsla(206,22%,7%,.2)]
|
|
37
|
+
bg-popover-background pointer-events-all z-[1000] focus:outline-none
|
|
38
|
+
shadow-[0_10px_38px_-10px_hsla(206,22%,7%,.35),0_10px_20px_-15px_hsla(206,22%,7%,.2)]
|
|
39
|
+
flex flex-col
|
|
38
40
|
`,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
</DialogPrimitive.Content>
|
|
47
|
+
);
|
|
48
|
+
});
|
|
44
49
|
|
|
45
50
|
const StyledTitle = forwardRef<
|
|
46
51
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
|
@@ -70,20 +75,6 @@ const StyledDescription = forwardRef<
|
|
|
70
75
|
/>
|
|
71
76
|
));
|
|
72
77
|
|
|
73
|
-
const CloseButtonContainer = forwardRef<
|
|
74
|
-
HTMLDivElement,
|
|
75
|
-
React.HTMLAttributes<HTMLDivElement>
|
|
76
|
-
>(({ className, ...props }, ref) => (
|
|
77
|
-
<div
|
|
78
|
-
ref={ref}
|
|
79
|
-
className={cx(
|
|
80
|
-
`absolute top-dialog-padding right-dialog-padding z-[1] bg-popover-background p-[4px_6px] rounded-[2px] border border-[rgba(128,128,128,0.2)] `,
|
|
81
|
-
className
|
|
82
|
-
)}
|
|
83
|
-
{...props}
|
|
84
|
-
/>
|
|
85
|
-
));
|
|
86
|
-
|
|
87
78
|
export interface IDialog {
|
|
88
79
|
containsElement: (element: HTMLElement) => boolean;
|
|
89
80
|
}
|
|
@@ -141,11 +132,17 @@ export const Dialog = forwardRef(function Dialog(
|
|
|
141
132
|
})}
|
|
142
133
|
>
|
|
143
134
|
{showCloseButton && (
|
|
144
|
-
<
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
135
|
+
<DialogPrimitive.Close asChild>
|
|
136
|
+
<IconButton
|
|
137
|
+
iconName="Cross1Icon"
|
|
138
|
+
className="z-[1]"
|
|
139
|
+
style={{
|
|
140
|
+
position: "absolute",
|
|
141
|
+
top: "var(--dialog-padding)",
|
|
142
|
+
right: "var(--dialog-padding)",
|
|
143
|
+
}}
|
|
144
|
+
/>
|
|
145
|
+
</DialogPrimitive.Close>
|
|
149
146
|
)}
|
|
150
147
|
{title && (
|
|
151
148
|
<>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useControlledOrUncontrolled } from "@noya-app/react-utils";
|
|
2
|
+
import * as Dialog from "@radix-ui/react-dialog";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useImperativeHandle } from "react";
|
|
5
|
+
import { cx } from "../utils/classNames";
|
|
6
|
+
import { SidebarRef } from "./workspace/types";
|
|
7
|
+
|
|
8
|
+
export type DrawerProps = {
|
|
9
|
+
open?: boolean;
|
|
10
|
+
onOpenChange?: (open: boolean) => void;
|
|
11
|
+
trigger: React.ReactNode;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
title?: React.ReactNode;
|
|
14
|
+
className?: string;
|
|
15
|
+
id?: string;
|
|
16
|
+
style?: React.CSSProperties;
|
|
17
|
+
positioning?: "fixed" | "absolute";
|
|
18
|
+
side?: "left" | "right";
|
|
19
|
+
portalled?: boolean;
|
|
20
|
+
portalContainer?: HTMLElement | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const Drawer = React.memo(
|
|
24
|
+
React.forwardRef(function Drawer(
|
|
25
|
+
{
|
|
26
|
+
open,
|
|
27
|
+
onOpenChange,
|
|
28
|
+
trigger,
|
|
29
|
+
children,
|
|
30
|
+
title,
|
|
31
|
+
className,
|
|
32
|
+
id,
|
|
33
|
+
style,
|
|
34
|
+
positioning = "fixed",
|
|
35
|
+
side = "left",
|
|
36
|
+
portalled = true,
|
|
37
|
+
portalContainer,
|
|
38
|
+
}: DrawerProps,
|
|
39
|
+
forwardedRef: React.ForwardedRef<SidebarRef>
|
|
40
|
+
) {
|
|
41
|
+
const [internalOpen, setInternalOpen] = useControlledOrUncontrolled({
|
|
42
|
+
defaultValue: false,
|
|
43
|
+
value: open,
|
|
44
|
+
onChange: onOpenChange,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
useImperativeHandle(forwardedRef, () => ({
|
|
48
|
+
expand: () => {
|
|
49
|
+
setInternalOpen(true);
|
|
50
|
+
},
|
|
51
|
+
collapse: () => {
|
|
52
|
+
setInternalOpen(false);
|
|
53
|
+
},
|
|
54
|
+
isExpanded: () => {
|
|
55
|
+
return internalOpen;
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
const inner = (
|
|
60
|
+
<>
|
|
61
|
+
<Dialog.Overlay
|
|
62
|
+
className={cx(
|
|
63
|
+
"inset-0 bg-black/50",
|
|
64
|
+
positioning === "absolute" ? "absolute" : "fixed"
|
|
65
|
+
)}
|
|
66
|
+
/>
|
|
67
|
+
<Dialog.Content
|
|
68
|
+
id={id}
|
|
69
|
+
style={style}
|
|
70
|
+
className={cx(
|
|
71
|
+
`top-0 bottom-0 z-50 overflow-y-auto bg-sidebar-background rounded-none max-h-none max-w-none sm:max-w-sm w-full border-divider-strong`,
|
|
72
|
+
positioning === "absolute" ? "absolute" : "fixed",
|
|
73
|
+
side === "left" ? "left-0 border-r" : "right-0 border-l",
|
|
74
|
+
className
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
{title && <Dialog.Title>{title}</Dialog.Title>}
|
|
78
|
+
{children}
|
|
79
|
+
</Dialog.Content>
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Dialog.Root
|
|
85
|
+
key="dialog"
|
|
86
|
+
open={internalOpen}
|
|
87
|
+
onOpenChange={setInternalOpen}
|
|
88
|
+
>
|
|
89
|
+
<Dialog.Trigger key="trigger">{trigger}</Dialog.Trigger>
|
|
90
|
+
{portalled ? (
|
|
91
|
+
<Dialog.Portal container={portalContainer}>{inner}</Dialog.Portal>
|
|
92
|
+
) : (
|
|
93
|
+
inner
|
|
94
|
+
)}
|
|
95
|
+
</Dialog.Root>
|
|
96
|
+
);
|
|
97
|
+
})
|
|
98
|
+
);
|