@noya-app/noya-designsystem 0.1.47 → 0.1.49
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 +18 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +389 -241
- package/dist/index.d.ts +389 -241
- package/dist/index.js +4802 -3990
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4715 -3915
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
- package/src/__tests__/validateDropIndicator.test.ts +263 -0
- package/src/components/ActionMenu.tsx +51 -0
- package/src/components/ActivityIndicator.tsx +7 -1
- package/src/components/BaseToolbar.tsx +2 -2
- package/src/components/Checkbox.tsx +2 -2
- package/src/components/Chip.tsx +7 -6
- package/src/components/Collection.tsx +30 -2
- package/src/components/Combobox.tsx +27 -15
- package/src/components/ComboboxMenu.tsx +15 -6
- package/src/components/Dialog.tsx +17 -12
- package/src/components/DimensionInput.tsx +14 -12
- package/src/components/Drawer.tsx +98 -0
- package/src/components/EditableText.tsx +3 -1
- package/src/components/FillInputField.tsx +2 -2
- package/src/components/Grid.tsx +161 -111
- package/src/components/GridView.tsx +73 -41
- package/src/components/InputField.tsx +148 -208
- package/src/components/Label.tsx +4 -68
- package/src/components/LabeledField.tsx +7 -1
- package/src/components/List.tsx +135 -50
- package/src/components/ListView.tsx +63 -43
- package/src/components/MediaThumbnail.tsx +117 -0
- package/src/components/Message.tsx +8 -8
- package/src/components/NoyaLogo.tsx +41 -0
- package/src/components/SearchCompletionMenu.tsx +30 -16
- package/src/components/SegmentedControl.tsx +1 -1
- package/src/components/SelectMenu.tsx +28 -21
- package/src/components/Slider.tsx +16 -7
- package/src/components/Sortable.tsx +125 -62
- package/src/components/internal/Menu.tsx +45 -25
- package/src/components/internal/MenuViewport.tsx +7 -1
- package/src/components/internal/SelectItem.tsx +53 -28
- 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 +49 -34
- package/src/hooks/usePreservePanelSize.tsx +1 -5
- package/src/index.css +8 -1
- package/src/index.tsx +6 -1
- package/src/theme/index.ts +2 -0
- package/src/utils/classNames.ts +51 -3
- package/src/utils/inputs.ts +21 -0
- package/tailwind.config.ts +8 -0
|
@@ -6,11 +6,13 @@ import {
|
|
|
6
6
|
DragOverlay,
|
|
7
7
|
DragStartEvent,
|
|
8
8
|
PointerSensor,
|
|
9
|
-
|
|
9
|
+
UniqueIdentifier,
|
|
10
|
+
useDndContext,
|
|
10
11
|
useSensor,
|
|
11
12
|
useSensors,
|
|
12
13
|
} from "@dnd-kit/core";
|
|
13
14
|
import {
|
|
15
|
+
horizontalListSortingStrategy,
|
|
14
16
|
SortableContext,
|
|
15
17
|
useSortable,
|
|
16
18
|
verticalListSortingStrategy,
|
|
@@ -23,25 +25,25 @@ export type RelativeDropPosition = "above" | "below" | "inside";
|
|
|
23
25
|
|
|
24
26
|
export type DropValidator = (
|
|
25
27
|
sourceIndex: number,
|
|
26
|
-
|
|
28
|
+
targetIndex: number,
|
|
27
29
|
position: RelativeDropPosition
|
|
28
30
|
) => boolean;
|
|
29
31
|
|
|
30
|
-
export const
|
|
32
|
+
export const normalizeListTargetIndex = (
|
|
31
33
|
index: number,
|
|
32
34
|
position: "above" | "below"
|
|
33
35
|
): number => {
|
|
34
36
|
return position === "above" ? index : index + 1;
|
|
35
37
|
};
|
|
36
38
|
|
|
37
|
-
const defaultAcceptsDrop: DropValidator = (
|
|
39
|
+
export const defaultAcceptsDrop: DropValidator = (
|
|
38
40
|
sourceIndex,
|
|
39
|
-
|
|
41
|
+
targetIndex,
|
|
40
42
|
position
|
|
41
43
|
) => {
|
|
42
44
|
if (position === "inside") return false;
|
|
43
45
|
|
|
44
|
-
const normalized =
|
|
46
|
+
const normalized = normalizeListTargetIndex(targetIndex, position);
|
|
45
47
|
|
|
46
48
|
if (sourceIndex === normalized || sourceIndex + 1 === normalized) {
|
|
47
49
|
return false;
|
|
@@ -50,35 +52,52 @@ const defaultAcceptsDrop: DropValidator = (
|
|
|
50
52
|
return true;
|
|
51
53
|
};
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
keys:
|
|
55
|
-
position: Translate;
|
|
55
|
+
export type SortableItemContextProps = {
|
|
56
|
+
keys: UniqueIdentifier[];
|
|
56
57
|
acceptsDrop: DropValidator;
|
|
57
|
-
setActivatorEvent: (event: PointerEvent) => void;
|
|
58
58
|
axis: "x" | "y";
|
|
59
|
-
}
|
|
59
|
+
position: { x: number; y: number };
|
|
60
|
+
setActivatorEvent: (event: PointerEvent) => void;
|
|
61
|
+
getDropTargetParentIndex?: (index: number) => number | undefined;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const SortableItemContext = React.createContext<SortableItemContextProps>({
|
|
60
65
|
keys: [],
|
|
61
|
-
position: { x: 0, y: 0 },
|
|
62
66
|
acceptsDrop: defaultAcceptsDrop,
|
|
63
|
-
setActivatorEvent: () => {},
|
|
64
67
|
axis: "y",
|
|
68
|
+
position: { x: 0, y: 0 },
|
|
69
|
+
setActivatorEvent: () => {},
|
|
70
|
+
getDropTargetParentIndex: undefined,
|
|
65
71
|
});
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
acceptsDrop: DropValidator
|
|
69
|
-
keys:
|
|
70
|
-
activeId:
|
|
71
|
-
overId:
|
|
72
|
-
offsetStart: number
|
|
73
|
-
elementStart: number
|
|
74
|
-
elementSize: number
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
type ValidateDropIndicatorParams = {
|
|
74
|
+
acceptsDrop: DropValidator; // Function that validates if a drop is allowed
|
|
75
|
+
keys: UniqueIdentifier[]; // Array of all sortable item IDs
|
|
76
|
+
activeId: UniqueIdentifier; // ID of item being dragged
|
|
77
|
+
overId: UniqueIdentifier; // ID of item being dragged over
|
|
78
|
+
offsetStart: number; // Current drag position (x or y coordinate)
|
|
79
|
+
elementStart: number; // Start position of target element (left or top)
|
|
80
|
+
elementSize: number; // Size of target element (width or height)
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export function validateDropIndicator({
|
|
84
|
+
acceptsDrop,
|
|
85
|
+
keys,
|
|
86
|
+
activeId,
|
|
87
|
+
overId,
|
|
88
|
+
offsetStart,
|
|
89
|
+
elementStart,
|
|
90
|
+
elementSize,
|
|
91
|
+
}: ValidateDropIndicatorParams): RelativeDropPosition | undefined {
|
|
92
|
+
const activeIndex = keys.findIndex((id) => id === activeId); // index of item being dragged
|
|
93
|
+
const overIndex = keys.findIndex((id) => id === overId); // index of item being dragged over
|
|
94
|
+
|
|
95
|
+
if (activeIndex === -1 || overIndex === -1) return undefined;
|
|
78
96
|
|
|
79
97
|
const acceptsDropInside = acceptsDrop(activeIndex, overIndex, "inside");
|
|
80
98
|
|
|
81
|
-
// If
|
|
99
|
+
// If cursor is in the middle third of the element (between 1/3 and 2/3)
|
|
100
|
+
// and dropping inside is allowed, show "inside" indicator
|
|
82
101
|
if (
|
|
83
102
|
offsetStart >= elementStart + elementSize / 3 &&
|
|
84
103
|
offsetStart <= elementStart + (elementSize * 2) / 3 &&
|
|
@@ -89,7 +108,6 @@ function validateDropIndicator(
|
|
|
89
108
|
// Are we over the top or bottom half of the element?
|
|
90
109
|
const indicator =
|
|
91
110
|
offsetStart < elementStart + elementSize / 2 ? "above" : "below";
|
|
92
|
-
|
|
93
111
|
// Drop above or below if possible, falling back to inside
|
|
94
112
|
return acceptsDrop(activeIndex, overIndex, indicator)
|
|
95
113
|
? indicator
|
|
@@ -105,13 +123,13 @@ function validateDropIndicator(
|
|
|
105
123
|
type UseSortableReturnType = ReturnType<typeof useSortable>;
|
|
106
124
|
|
|
107
125
|
interface ItemProps<T> {
|
|
108
|
-
id:
|
|
126
|
+
id: UniqueIdentifier;
|
|
109
127
|
disabled?: boolean;
|
|
110
128
|
children: (
|
|
111
129
|
props: {
|
|
112
130
|
ref: React.Ref<T>;
|
|
113
131
|
relativeDropPosition?: RelativeDropPosition;
|
|
114
|
-
|
|
132
|
+
style?: React.CSSProperties;
|
|
115
133
|
} & UseSortableReturnType["attributes"]
|
|
116
134
|
) => JSX.Element;
|
|
117
135
|
}
|
|
@@ -121,18 +139,25 @@ function SortableItem<T extends HTMLElement>({
|
|
|
121
139
|
disabled,
|
|
122
140
|
children,
|
|
123
141
|
}: ItemProps<T>) {
|
|
124
|
-
const {
|
|
125
|
-
|
|
142
|
+
const {
|
|
143
|
+
keys,
|
|
144
|
+
position,
|
|
145
|
+
acceptsDrop,
|
|
146
|
+
setActivatorEvent,
|
|
147
|
+
axis,
|
|
148
|
+
getDropTargetParentIndex,
|
|
149
|
+
} = React.useContext(SortableItemContext);
|
|
126
150
|
const sortable = useSortable({ id, disabled });
|
|
151
|
+
const { activatorEvent } = useDndContext();
|
|
127
152
|
|
|
128
153
|
const {
|
|
129
154
|
active,
|
|
130
|
-
activatorEvent,
|
|
131
155
|
attributes,
|
|
132
156
|
listeners,
|
|
133
157
|
setNodeRef,
|
|
134
158
|
isDragging,
|
|
135
159
|
index,
|
|
160
|
+
activeIndex,
|
|
136
161
|
overIndex,
|
|
137
162
|
over,
|
|
138
163
|
} = sortable;
|
|
@@ -147,23 +172,40 @@ function SortableItem<T extends HTMLElement>({
|
|
|
147
172
|
const offsetTop = eventY + position.y;
|
|
148
173
|
|
|
149
174
|
const ref = React.useCallback((node: T) => setNodeRef(node), [setNodeRef]);
|
|
175
|
+
const parentIndex =
|
|
176
|
+
overIndex === -1 ? undefined : getDropTargetParentIndex?.(overIndex);
|
|
177
|
+
const isValidParent =
|
|
178
|
+
parentIndex === undefined || parentIndex === -1
|
|
179
|
+
? false
|
|
180
|
+
: acceptsDrop(activeIndex, parentIndex, "inside");
|
|
181
|
+
const overIdAcceptsDrop =
|
|
182
|
+
overIndex === -1
|
|
183
|
+
? undefined
|
|
184
|
+
: acceptsDrop(activeIndex, overIndex, "inside");
|
|
185
|
+
|
|
186
|
+
const relativeDropPosition =
|
|
187
|
+
index >= 0 && index === overIndex && !isDragging && active && over
|
|
188
|
+
? validateDropIndicator({
|
|
189
|
+
acceptsDrop,
|
|
190
|
+
keys,
|
|
191
|
+
activeId: active.id,
|
|
192
|
+
overId: over.id,
|
|
193
|
+
offsetStart: axis === "x" ? offsetLeft : offsetTop,
|
|
194
|
+
elementStart: axis === "x" ? over.rect.left : over.rect.top,
|
|
195
|
+
elementSize: axis === "x" ? over.rect.width : over.rect.height,
|
|
196
|
+
})
|
|
197
|
+
: undefined;
|
|
198
|
+
|
|
199
|
+
const showDragInsideIndicatorOnParent =
|
|
200
|
+
!overIdAcceptsDrop && isValidParent && parentIndex === index;
|
|
150
201
|
|
|
151
202
|
return children({
|
|
152
203
|
ref,
|
|
153
204
|
...attributes,
|
|
154
205
|
...listeners,
|
|
155
206
|
relativeDropPosition:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
acceptsDrop,
|
|
159
|
-
keys,
|
|
160
|
-
active.id,
|
|
161
|
-
over.id,
|
|
162
|
-
axis === "x" ? offsetLeft : offsetTop,
|
|
163
|
-
axis === "x" ? over.rect.offsetLeft : over.rect.offsetTop,
|
|
164
|
-
axis === "x" ? over.rect.width : over.rect.height
|
|
165
|
-
)
|
|
166
|
-
: undefined,
|
|
207
|
+
relativeDropPosition ??
|
|
208
|
+
(showDragInsideIndicatorOnParent ? "inside" : undefined),
|
|
167
209
|
});
|
|
168
210
|
}
|
|
169
211
|
|
|
@@ -172,16 +214,17 @@ function SortableItem<T extends HTMLElement>({
|
|
|
172
214
|
* ------------------------------------------------------------------------- */
|
|
173
215
|
|
|
174
216
|
interface RootProps {
|
|
175
|
-
keys:
|
|
217
|
+
keys: UniqueIdentifier[];
|
|
176
218
|
children: React.ReactNode;
|
|
177
219
|
renderOverlay?: (index: number) => React.ReactNode;
|
|
178
220
|
onMoveItem?: (
|
|
179
221
|
sourceIndex: number,
|
|
180
|
-
|
|
222
|
+
targetIndex: number,
|
|
181
223
|
position: RelativeDropPosition
|
|
182
224
|
) => void;
|
|
183
|
-
acceptsDrop?:
|
|
184
|
-
axis?: "
|
|
225
|
+
acceptsDrop?: SortableItemContextProps["acceptsDrop"];
|
|
226
|
+
axis?: SortableItemContextProps["axis"];
|
|
227
|
+
getDropTargetParentIndex?: SortableItemContextProps["getDropTargetParentIndex"];
|
|
185
228
|
}
|
|
186
229
|
|
|
187
230
|
function SortableRoot({
|
|
@@ -191,6 +234,7 @@ function SortableRoot({
|
|
|
191
234
|
renderOverlay,
|
|
192
235
|
acceptsDrop = defaultAcceptsDrop,
|
|
193
236
|
axis = "y",
|
|
237
|
+
getDropTargetParentIndex,
|
|
194
238
|
}: RootProps) {
|
|
195
239
|
const sensors = useSensors(
|
|
196
240
|
useSensor(PointerSensor, {
|
|
@@ -202,16 +246,19 @@ function SortableRoot({
|
|
|
202
246
|
|
|
203
247
|
const [activeIndex, setActiveIndex] = React.useState<number | undefined>();
|
|
204
248
|
const activatorEvent = React.useRef<PointerEvent | null>(null);
|
|
249
|
+
const [position, setPosition] = React.useState<{ x: number; y: number }>({
|
|
250
|
+
x: 0,
|
|
251
|
+
y: 0,
|
|
252
|
+
});
|
|
205
253
|
|
|
206
254
|
const setActivatorEvent = React.useCallback((event: PointerEvent) => {
|
|
207
255
|
activatorEvent.current = event;
|
|
208
256
|
}, []);
|
|
209
257
|
|
|
210
|
-
const [position, setPosition] = React.useState<Translate>({ x: 0, y: 0 });
|
|
211
|
-
|
|
212
258
|
const handleDragStart = React.useCallback(
|
|
213
259
|
(event: DragStartEvent) => {
|
|
214
|
-
|
|
260
|
+
const index = keys.findIndex((id) => id === event.active.id);
|
|
261
|
+
setActiveIndex(index);
|
|
215
262
|
},
|
|
216
263
|
[keys]
|
|
217
264
|
);
|
|
@@ -225,25 +272,26 @@ function SortableRoot({
|
|
|
225
272
|
const { active, over } = event;
|
|
226
273
|
|
|
227
274
|
setActiveIndex(undefined);
|
|
228
|
-
|
|
229
275
|
if (over && active.id !== over.id) {
|
|
230
|
-
const oldIndex = keys.
|
|
231
|
-
const newIndex = keys.
|
|
276
|
+
const oldIndex = keys.findIndex((id) => id === active.id);
|
|
277
|
+
const newIndex = keys.findIndex((id) => id === over.id);
|
|
278
|
+
|
|
279
|
+
if (oldIndex === -1 || newIndex === -1) return;
|
|
232
280
|
|
|
233
281
|
const eventX = activatorEvent.current?.clientX ?? 0;
|
|
234
282
|
const eventY = activatorEvent.current?.clientY ?? 0;
|
|
235
283
|
const offsetLeft = eventX + position.x;
|
|
236
284
|
const offsetTop = eventY + position.y;
|
|
237
285
|
|
|
238
|
-
const indicator = validateDropIndicator(
|
|
286
|
+
const indicator = validateDropIndicator({
|
|
239
287
|
acceptsDrop,
|
|
240
288
|
keys,
|
|
241
|
-
active.id,
|
|
242
|
-
over.id,
|
|
243
|
-
axis === "x" ? offsetLeft : offsetTop,
|
|
244
|
-
axis === "x" ? over.rect.
|
|
245
|
-
axis === "x" ? over.rect.width : over.rect.height
|
|
246
|
-
);
|
|
289
|
+
activeId: active.id,
|
|
290
|
+
overId: over.id,
|
|
291
|
+
offsetStart: axis === "x" ? offsetLeft : offsetTop,
|
|
292
|
+
elementStart: axis === "x" ? over.rect.left : over.rect.top,
|
|
293
|
+
elementSize: axis === "x" ? over.rect.width : over.rect.height,
|
|
294
|
+
});
|
|
247
295
|
|
|
248
296
|
if (!indicator) return;
|
|
249
297
|
|
|
@@ -262,14 +310,22 @@ function SortableRoot({
|
|
|
262
310
|
return (
|
|
263
311
|
<SortableItemContext.Provider
|
|
264
312
|
value={React.useMemo(
|
|
265
|
-
() => ({
|
|
313
|
+
(): SortableItemContextProps => ({
|
|
266
314
|
keys,
|
|
267
315
|
acceptsDrop,
|
|
268
316
|
position,
|
|
269
317
|
setActivatorEvent,
|
|
270
318
|
axis,
|
|
319
|
+
getDropTargetParentIndex,
|
|
271
320
|
}),
|
|
272
|
-
[
|
|
321
|
+
[
|
|
322
|
+
acceptsDrop,
|
|
323
|
+
axis,
|
|
324
|
+
getDropTargetParentIndex,
|
|
325
|
+
keys,
|
|
326
|
+
position,
|
|
327
|
+
setActivatorEvent,
|
|
328
|
+
]
|
|
273
329
|
)}
|
|
274
330
|
>
|
|
275
331
|
<DndContext
|
|
@@ -279,7 +335,14 @@ function SortableRoot({
|
|
|
279
335
|
onDragMove={handleDragMove}
|
|
280
336
|
onDragEnd={handleDragEnd}
|
|
281
337
|
>
|
|
282
|
-
<SortableContext
|
|
338
|
+
<SortableContext
|
|
339
|
+
items={keys}
|
|
340
|
+
strategy={
|
|
341
|
+
axis === "x"
|
|
342
|
+
? horizontalListSortingStrategy
|
|
343
|
+
: verticalListSortingStrategy
|
|
344
|
+
}
|
|
345
|
+
>
|
|
283
346
|
{children}
|
|
284
347
|
</SortableContext>
|
|
285
348
|
{mounted &&
|
|
@@ -62,10 +62,18 @@ export type SeparatorItem = {
|
|
|
62
62
|
type: "separator";
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
export const separator: SeparatorItem = {
|
|
66
|
+
type: "separator",
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type FalsyReactNode = false | "" | null | undefined;
|
|
70
|
+
|
|
71
|
+
export type MenuItemIcon = IconName | React.ReactElement | FalsyReactNode;
|
|
72
|
+
|
|
65
73
|
type BaseMenuItem = {
|
|
66
74
|
checked?: boolean;
|
|
67
75
|
disabled?: boolean;
|
|
68
|
-
icon?:
|
|
76
|
+
icon?: MenuItemIcon;
|
|
69
77
|
role?: MenuItemRole;
|
|
70
78
|
alwaysInclude?: boolean;
|
|
71
79
|
title: NonNullable<ReactNode>;
|
|
@@ -83,6 +91,7 @@ export type SelectableMenuItem<T extends string> = BaseMenuItem & {
|
|
|
83
91
|
type?: undefined;
|
|
84
92
|
shortcut?: string;
|
|
85
93
|
value: T;
|
|
94
|
+
testSelected?: boolean;
|
|
86
95
|
};
|
|
87
96
|
|
|
88
97
|
type SubMenuItem<T extends string> = BaseMenuItem & {
|
|
@@ -132,39 +141,48 @@ export const isSelectableMenuItemWithScore = (
|
|
|
132
141
|
): item is ScoredSelectableMenuItem<string> =>
|
|
133
142
|
isSelectableMenuItem(item) && "index" in item && "score" in item;
|
|
134
143
|
|
|
135
|
-
export const
|
|
144
|
+
export const getMenuItemKey = <T extends string>(
|
|
145
|
+
item: MenuItem<T>,
|
|
146
|
+
index: number
|
|
147
|
+
): string => {
|
|
148
|
+
switch (item.type) {
|
|
149
|
+
case "separator":
|
|
150
|
+
return index.toString();
|
|
151
|
+
case "sectionHeader":
|
|
152
|
+
return item.id;
|
|
153
|
+
case "submenu":
|
|
154
|
+
return item.id;
|
|
155
|
+
default:
|
|
156
|
+
return item.value;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export const CHECKBOX_WIDTH = 15;
|
|
136
161
|
export const CHECKBOX_RIGHT_INSET = 8;
|
|
162
|
+
export const CHECKBOX_INDENT_WIDTH = 6;
|
|
137
163
|
|
|
138
164
|
export const styles = {
|
|
139
|
-
separatorStyle: "h-px bg-divider mx-
|
|
140
|
-
|
|
165
|
+
separatorStyle: "h-px bg-divider mx-3 my-1",
|
|
166
|
+
selectedItemStyle:
|
|
167
|
+
"focus:outline-none focus:text-white focus:bg-primary focus:kbd:text-white",
|
|
168
|
+
testSelectedItemStyle: "outline-none text-white bg-primary kbd:text-white",
|
|
141
169
|
itemStyle: ({ disabled }: { disabled?: boolean }) => `
|
|
142
170
|
flex-none select-none cursor-pointer rounded
|
|
143
|
-
py-1.5 px-
|
|
144
|
-
focus:outline-none focus:text-white focus:bg-primary
|
|
145
|
-
focus:kbd:text-white
|
|
171
|
+
py-1.5 px-2
|
|
146
172
|
active:bg-primary-light
|
|
147
173
|
transition-colors
|
|
148
174
|
flex items-center
|
|
149
175
|
font-sans text-button font-medium
|
|
150
176
|
${disabled ? "text-text-disabled" : ""}
|
|
151
177
|
`,
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
contentStyle: `
|
|
161
|
-
rounded
|
|
162
|
-
bg-popover-background
|
|
163
|
-
text-text
|
|
164
|
-
shadow-[0_2px_4px_rgba(0,0,0,0.2),_0_0_12px_rgba(0,0,0,0.1)]
|
|
165
|
-
z-menu
|
|
166
|
-
py-1
|
|
167
|
-
`,
|
|
178
|
+
itemIndicator: {
|
|
179
|
+
className: "flex items-center relative -left-2.5",
|
|
180
|
+
style: {
|
|
181
|
+
marginRight: -CHECKBOX_RIGHT_INSET,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
contentStyle:
|
|
185
|
+
"rounded bg-popover-background text-text shadow-[0_2px_4px_rgba(0,0,0,0.2),_0_0_12px_rgba(0,0,0,0.1)] z-menu py-1",
|
|
168
186
|
};
|
|
169
187
|
|
|
170
188
|
function getKeyboardShortcuts<T extends string>(
|
|
@@ -256,12 +274,14 @@ export const SectionHeader = memo(function SectionHeader({
|
|
|
256
274
|
variant === "label"
|
|
257
275
|
? `text-label ${textStyles.label} !font-bold text-text-disabled`
|
|
258
276
|
: "font-sans text-heading5 font-normal",
|
|
259
|
-
"bg-listview-raised-background flex items-center py-1.5 px-
|
|
277
|
+
"bg-listview-raised-background flex items-center py-1.5 px-3",
|
|
260
278
|
isFirst && "-mt-1"
|
|
261
279
|
)}
|
|
262
280
|
>
|
|
263
281
|
{indented && (
|
|
264
|
-
<Spacer.Horizontal
|
|
282
|
+
<Spacer.Horizontal
|
|
283
|
+
size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET + CHECKBOX_INDENT_WIDTH}
|
|
284
|
+
/>
|
|
265
285
|
)}
|
|
266
286
|
{title}
|
|
267
287
|
</span>
|
|
@@ -2,6 +2,7 @@ import { ChevronRightIcon } from "@noya-app/noya-icons";
|
|
|
2
2
|
import * as RadixContextMenu from "@radix-ui/react-context-menu";
|
|
3
3
|
import * as RadixDropdownMenu from "@radix-ui/react-dropdown-menu";
|
|
4
4
|
import React from "react";
|
|
5
|
+
import { cx } from "../..";
|
|
5
6
|
import { MenuItem, SectionHeader, styles } from "./Menu";
|
|
6
7
|
import { SelectItem } from "./SelectItem";
|
|
7
8
|
|
|
@@ -15,6 +16,7 @@ type MenuItemComponent = React.ForwardRefExoticComponent<
|
|
|
15
16
|
children?: React.ReactNode;
|
|
16
17
|
value: string;
|
|
17
18
|
onSelect?: (event: any) => void;
|
|
19
|
+
onClick?: (event: React.MouseEvent<any>) => void;
|
|
18
20
|
}>
|
|
19
21
|
>;
|
|
20
22
|
type MenuCheckboxItemComponent = React.FC<{
|
|
@@ -94,7 +96,10 @@ export const MenuViewport = <T extends string>({
|
|
|
94
96
|
return (
|
|
95
97
|
<Components.Sub key={item.id}>
|
|
96
98
|
<Components.SubTrigger
|
|
97
|
-
className={
|
|
99
|
+
className={cx(
|
|
100
|
+
styles.itemStyle({ disabled: item.disabled }),
|
|
101
|
+
styles.selectedItemStyle
|
|
102
|
+
)}
|
|
98
103
|
asChild
|
|
99
104
|
onClick={(e) => {
|
|
100
105
|
e.stopPropagation();
|
|
@@ -141,6 +146,7 @@ export const MenuViewport = <T extends string>({
|
|
|
141
146
|
onSelect={onSelect}
|
|
142
147
|
Components={Components}
|
|
143
148
|
indented={hasCheckedItem}
|
|
149
|
+
testSelected={item.testSelected}
|
|
144
150
|
>
|
|
145
151
|
{item.title ?? item.value}
|
|
146
152
|
</SelectItem>
|
|
@@ -3,8 +3,10 @@ import { renderIcon } from "../Icons";
|
|
|
3
3
|
import { CheckIcon } from "@noya-app/noya-icons";
|
|
4
4
|
import * as Select from "@radix-ui/react-select";
|
|
5
5
|
import React, { useCallback } from "react";
|
|
6
|
+
import { cx } from "../../utils/classNames";
|
|
6
7
|
import { Spacer } from "../Spacer";
|
|
7
8
|
import {
|
|
9
|
+
CHECKBOX_INDENT_WIDTH,
|
|
8
10
|
CHECKBOX_RIGHT_INSET,
|
|
9
11
|
CHECKBOX_WIDTH,
|
|
10
12
|
KeyboardShortcut,
|
|
@@ -24,6 +26,7 @@ export const SelectItem = React.forwardRef(
|
|
|
24
26
|
onSelect,
|
|
25
27
|
shortcut,
|
|
26
28
|
indented,
|
|
29
|
+
testSelected,
|
|
27
30
|
...props
|
|
28
31
|
}: Select.SelectItemProps & {
|
|
29
32
|
icon?: React.ReactNode;
|
|
@@ -32,6 +35,7 @@ export const SelectItem = React.forwardRef(
|
|
|
32
35
|
onSelect?: (value: any) => void;
|
|
33
36
|
shortcut?: string;
|
|
34
37
|
indented?: boolean;
|
|
38
|
+
testSelected?: boolean;
|
|
35
39
|
},
|
|
36
40
|
forwardedRef: React.ForwardedRef<HTMLDivElement>
|
|
37
41
|
) => {
|
|
@@ -40,50 +44,71 @@ export const SelectItem = React.forwardRef(
|
|
|
40
44
|
onSelect?.(value);
|
|
41
45
|
}, [onSelect, value]);
|
|
42
46
|
|
|
47
|
+
// Prevent the click from propagating outside the menu
|
|
48
|
+
const cancelClickPropagation = useCallback(
|
|
49
|
+
(event: React.MouseEvent<HTMLDivElement>) => {
|
|
50
|
+
event.stopPropagation();
|
|
51
|
+
},
|
|
52
|
+
[]
|
|
53
|
+
);
|
|
54
|
+
|
|
43
55
|
if (checked && Components.CheckboxItem) {
|
|
44
56
|
return (
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
57
|
+
<div className="px-1">
|
|
58
|
+
<Components.CheckboxItem
|
|
59
|
+
checked={checked}
|
|
60
|
+
disabled={disabled}
|
|
61
|
+
onSelect={handleSelectItem}
|
|
62
|
+
className={cx(
|
|
63
|
+
styles.itemStyle({ disabled }),
|
|
64
|
+
styles.selectedItemStyle,
|
|
65
|
+
testSelected && styles.testSelectedItemStyle
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
<Spacer.Horizontal size={CHECKBOX_INDENT_WIDTH} />
|
|
69
|
+
{Components.ItemIndicator && (
|
|
70
|
+
<Components.ItemIndicator {...styles.itemIndicator}>
|
|
71
|
+
<CheckIcon />
|
|
72
|
+
</Components.ItemIndicator>
|
|
73
|
+
)}
|
|
74
|
+
{icon && (
|
|
75
|
+
<>
|
|
76
|
+
{renderIcon(icon)}
|
|
77
|
+
<Spacer.Horizontal size={8} />
|
|
78
|
+
</>
|
|
79
|
+
)}
|
|
80
|
+
{children}
|
|
81
|
+
{shortcut && (
|
|
82
|
+
<>
|
|
83
|
+
<Spacer.Horizontal />
|
|
84
|
+
<Spacer.Horizontal size={24} />
|
|
85
|
+
<KeyboardShortcut shortcut={shortcut} />
|
|
86
|
+
</>
|
|
87
|
+
)}
|
|
88
|
+
</Components.CheckboxItem>
|
|
89
|
+
</div>
|
|
71
90
|
);
|
|
72
91
|
}
|
|
73
92
|
|
|
74
93
|
return (
|
|
75
94
|
<div className="px-1">
|
|
76
95
|
<Components.Item
|
|
77
|
-
className={
|
|
96
|
+
className={cx(
|
|
97
|
+
styles.itemStyle({ disabled }),
|
|
98
|
+
styles.selectedItemStyle,
|
|
99
|
+
testSelected && styles.testSelectedItemStyle
|
|
100
|
+
)}
|
|
78
101
|
disabled={disabled}
|
|
79
102
|
value={value}
|
|
80
103
|
ref={forwardedRef}
|
|
104
|
+
onClick={cancelClickPropagation}
|
|
81
105
|
onSelect={handleSelectItem}
|
|
82
106
|
{...props}
|
|
83
107
|
>
|
|
84
108
|
<div className="flex flex-1 items-center">
|
|
109
|
+
{indented && <Spacer.Horizontal size={CHECKBOX_INDENT_WIDTH} />}
|
|
85
110
|
{checked ? (
|
|
86
|
-
<div
|
|
111
|
+
<div {...styles.itemIndicator}>
|
|
87
112
|
<CheckIcon />
|
|
88
113
|
</div>
|
|
89
114
|
) : indented ? (
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { styles } from "../Menu";
|
|
3
|
+
|
|
4
|
+
describe("Menu", () => {
|
|
5
|
+
test("styles.selectedItemStyle should have the same styles as styles.testSelectedItemStyle without the focus: prefix", () => {
|
|
6
|
+
const selectedWithoutFocus = styles.selectedItemStyle.replace(
|
|
7
|
+
/focus:/g,
|
|
8
|
+
""
|
|
9
|
+
);
|
|
10
|
+
expect(selectedWithoutFocus).toEqual(styles.testSelectedItemStyle);
|
|
11
|
+
});
|
|
12
|
+
});
|