@noya-app/noya-designsystem 0.1.47 → 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.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +10 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +205 -106
  5. package/dist/index.d.ts +205 -106
  6. package/dist/index.js +2108 -1589
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +2078 -1566
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +6 -7
  11. package/src/__tests__/validateDropIndicator.test.ts +263 -0
  12. package/src/components/ActionMenu.tsx +40 -0
  13. package/src/components/ActivityIndicator.tsx +7 -1
  14. package/src/components/Collection.tsx +12 -2
  15. package/src/components/Combobox.tsx +14 -1
  16. package/src/components/ComboboxMenu.tsx +14 -5
  17. package/src/components/Dialog.tsx +17 -12
  18. package/src/components/Drawer.tsx +98 -0
  19. package/src/components/Grid.tsx +57 -27
  20. package/src/components/GridView.tsx +39 -25
  21. package/src/components/InputField.tsx +87 -92
  22. package/src/components/Label.tsx +7 -7
  23. package/src/components/List.tsx +42 -16
  24. package/src/components/ListView.tsx +21 -17
  25. package/src/components/MediaThumbnail.tsx +117 -0
  26. package/src/components/SearchCompletionMenu.tsx +30 -16
  27. package/src/components/SelectMenu.tsx +19 -17
  28. package/src/components/Sortable.tsx +70 -44
  29. package/src/components/internal/Menu.tsx +38 -23
  30. package/src/components/internal/MenuViewport.tsx +7 -1
  31. package/src/components/internal/SelectItem.tsx +51 -27
  32. package/src/components/internal/__tests__/Menu.test.tsx +12 -0
  33. package/src/components/workspace/DrawerWorkspaceLayout.tsx +86 -0
  34. package/src/components/workspace/PanelWorkspaceLayout.tsx +102 -0
  35. package/src/components/{WorkspaceLayout.tsx → workspace/WorkspaceLayout.tsx} +109 -106
  36. package/src/components/workspace/types.ts +31 -0
  37. package/src/contexts/DialogContext.tsx +49 -34
  38. package/src/hooks/usePreservePanelSize.tsx +1 -5
  39. package/src/index.css +1 -0
  40. package/src/index.tsx +4 -1
  41. package/tailwind.config.ts +1 -0
@@ -0,0 +1,117 @@
1
+ import { ImageIcon, SpeakerLoudIcon, VideoIcon } from "@noya-app/noya-icons";
2
+ import React, { memo, useMemo } from "react";
3
+ import { cssVars } from "../theme";
4
+ import { cx } from "../utils/classNames";
5
+ import { IconName, Icons } from "./Icons";
6
+
7
+ type ColorScheme = "primary" | "icon" | "warning";
8
+
9
+ type AssetType = "image" | "video" | "audio";
10
+
11
+ const getAssetType = (contentType?: string): AssetType | undefined => {
12
+ if (!contentType) return undefined;
13
+ if (contentType.startsWith("image/")) return "image";
14
+ if (contentType.startsWith("video/")) return "video";
15
+ if (contentType.startsWith("audio/")) return "audio";
16
+ return undefined;
17
+ };
18
+
19
+ type MediaThumbnailProps = {
20
+ contentType?: string;
21
+ url?: string;
22
+ selected?: boolean;
23
+ className?: string;
24
+ /** Color scheme for the thumbnail. Affects icon color and background.
25
+ * @default "icon"
26
+ */
27
+ colorScheme?: ColorScheme;
28
+ /**
29
+ * If provided, use this icon instead of the default one for the asset type
30
+ */
31
+ iconName?: IconName;
32
+ };
33
+
34
+ const getColors = (colorScheme: ColorScheme, selected: boolean) => {
35
+ if (selected) {
36
+ return {
37
+ icon: cssVars.colors.primary,
38
+ background: cssVars.colors.primaryPastel,
39
+ };
40
+ }
41
+
42
+ switch (colorScheme) {
43
+ case "primary":
44
+ return {
45
+ icon: cssVars.colors.primary,
46
+ background: cssVars.colors.inputBackground,
47
+ };
48
+ case "warning":
49
+ return {
50
+ icon: cssVars.colors.warning,
51
+ background: cssVars.colors.inputBackground,
52
+ };
53
+ case "icon":
54
+ default:
55
+ return {
56
+ icon: cssVars.colors.icon,
57
+ background: cssVars.colors.inputBackground,
58
+ };
59
+ }
60
+ };
61
+
62
+ const iconStyles = {
63
+ width: "max(15px, 20%)",
64
+ height: "max(15px, 20%)",
65
+ };
66
+
67
+ export const MediaThumbnail = memo(function MediaThumbnail({
68
+ contentType: contentTypeProp,
69
+ selected = false,
70
+ className,
71
+ colorScheme = "icon",
72
+ url,
73
+ iconName = "FileIcon",
74
+ }: MediaThumbnailProps) {
75
+ const { icon: iconColor, background: backgroundColor } = getColors(
76
+ colorScheme,
77
+ selected
78
+ );
79
+ const contentType = getAssetType(contentTypeProp);
80
+
81
+ const content = useMemo(() => {
82
+ switch (contentType) {
83
+ case "image":
84
+ if (!url) return <ImageIcon color={iconColor} style={iconStyles} />;
85
+ return (
86
+ <img
87
+ src={url}
88
+ alt=""
89
+ className="object-contain w-full h-full"
90
+ tabIndex={-1}
91
+ />
92
+ );
93
+
94
+ case "video":
95
+ return <VideoIcon color={iconColor} style={iconStyles} />;
96
+
97
+ case "audio":
98
+ return <SpeakerLoudIcon color={iconColor} style={iconStyles} />;
99
+
100
+ default:
101
+ const Icon = Icons[iconName];
102
+ return <Icon color={iconColor} style={iconStyles} />;
103
+ }
104
+ }, [contentType, url, iconColor, iconName]);
105
+
106
+ return (
107
+ <div
108
+ className={cx(
109
+ "w-full h-full justify-center flex items-center aspect-square",
110
+ className
111
+ )}
112
+ style={{ backgroundColor }}
113
+ >
114
+ {content}
115
+ </div>
116
+ );
117
+ });
@@ -1,16 +1,3 @@
1
- import {
2
- ComboboxMenu,
3
- Divider,
4
- IScoredItem,
5
- InputField,
6
- ListView,
7
- SelectableMenuItem,
8
- Small,
9
- cssVars,
10
- fuzzyFilter,
11
- getNextIndex,
12
- isSelectableMenuItem,
13
- } from "@noya-app/noya-designsystem";
14
1
  import { getCurrentPlatform, handleKeyboardEvent } from "@noya-app/noya-keymap";
15
2
  import React, {
16
3
  forwardRef,
@@ -21,6 +8,21 @@ import React, {
21
8
  useRef,
22
9
  useState,
23
10
  } from "react";
11
+ import { cssVars } from "../theme";
12
+ import { getNextIndex } from "../utils/combobox";
13
+ import { fuzzyFilter, IScoredItem } from "../utils/fuzzyScorer";
14
+ import { ComboboxMenu } from "./ComboboxMenu";
15
+ import { Divider } from "./Divider";
16
+ import { InputField } from "./InputField";
17
+ import {
18
+ CHECKBOX_INDENT_WIDTH,
19
+ CHECKBOX_RIGHT_INSET,
20
+ CHECKBOX_WIDTH,
21
+ isSelectableMenuItem,
22
+ SelectableMenuItem,
23
+ } from "./internal/Menu";
24
+ import { ListView } from "./ListView";
25
+ import { Small } from "./Text";
24
26
 
25
27
  export interface ISearchCompletionMenu {
26
28
  focus: () => void;
@@ -33,17 +35,21 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
33
35
  onClose,
34
36
  items,
35
37
  width = 200,
38
+ testSelectedIndex,
39
+ testSearch,
36
40
  }: {
37
41
  onSelect: (item: SelectableMenuItem<string>) => void;
38
42
  onHover?: (item: SelectableMenuItem<string>) => void;
39
43
  onClose: () => void;
40
44
  items: SelectableMenuItem<string>[];
41
45
  width?: number;
46
+ testSelectedIndex?: number;
47
+ testSearch?: string;
42
48
  },
43
49
  forwardedRef: React.ForwardedRef<ISearchCompletionMenu>
44
50
  ) {
45
51
  const [state, setState] = useState({
46
- search: "",
52
+ search: testSearch ?? "",
47
53
  index: 0,
48
54
  });
49
55
 
@@ -51,6 +57,8 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
51
57
 
52
58
  const listRef = React.useRef<ListView.VirtualizedList>(null);
53
59
 
60
+ const hasCheckedItems = items.some((item) => item.checked);
61
+
54
62
  useEffect(() => {
55
63
  listRef.current?.scrollToIndex(index);
56
64
  }, [index]);
@@ -157,7 +165,12 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
157
165
  outline: "none",
158
166
  boxShadow: "none",
159
167
  backgroundColor: cssVars.colors.inputBackground,
160
- padding: "4px 12px",
168
+ paddingTop: "4px",
169
+ paddingBottom: "4px",
170
+ paddingLeft: hasCheckedItems
171
+ ? `${CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET + 12 + CHECKBOX_INDENT_WIDTH}px`
172
+ : "12px",
173
+ paddingRight: "12px",
161
174
  }}
162
175
  onChange={setSearch}
163
176
  onKeyDown={handleKeyDown}
@@ -169,13 +182,14 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
169
182
  ref={listRef}
170
183
  listSize={listSize}
171
184
  items={results}
172
- selectedIndex={index}
185
+ selectedIndex={testSelectedIndex ?? index}
173
186
  onSelectItem={(item) => {
174
187
  if (isSelectableMenuItem(item)) {
175
188
  selectAndClose(item);
176
189
  }
177
190
  }}
178
191
  onHoverIndex={setIndex}
192
+ indented={hasCheckedItems}
179
193
  />
180
194
  ) : (
181
195
  <div
@@ -93,7 +93,7 @@ const SelectMenuTrigger = memoGeneric(function SelectMenuTrigger({
93
93
  id={id}
94
94
  style={style}
95
95
  className={cx(
96
- className ?? "flex w-full gap-1.5",
96
+ className ?? "flex w-full",
97
97
  "flex-1 focus:z-interactable relative",
98
98
  isOpen && "ring-2 ring-primary"
99
99
  )}
@@ -108,15 +108,12 @@ const SelectMenuTrigger = memoGeneric(function SelectMenuTrigger({
108
108
  <span className="flex-1 flex">
109
109
  <Select.Value placeholder={placeholder} />
110
110
  </span>
111
-
112
- <span className="px-0.5">
113
- <DropdownChevronIcon
114
- className={cx(
115
- "transition-transform duration-100",
116
- isOpen ? "rotate-180" : "rotate-0"
117
- )}
118
- />
119
- </span>
111
+ <DropdownChevronIcon
112
+ className={cx(
113
+ "transition-transform duration-100 ml-1.5",
114
+ isOpen ? "rotate-180" : "rotate-0"
115
+ )}
116
+ />
120
117
  </Button>
121
118
  </Select.SelectTrigger>
122
119
  );
@@ -143,7 +140,7 @@ export const SelectMenu = memoGeneric(function SelectMenu<
143
140
  onFocus,
144
141
  onBlur,
145
142
  onOpenChange,
146
- contentStyle,
143
+ contentStyle: contentStyleProp,
147
144
  ...props
148
145
  }: Props<T>) {
149
146
  const selectedItem = items.find(
@@ -181,6 +178,15 @@ export const SelectMenu = memoGeneric(function SelectMenu<
181
178
  [icon, id, style, className, disabled, value, selectedItem]
182
179
  );
183
180
 
181
+ const contentStyle = useMemo(() => {
182
+ return {
183
+ width: "var(--radix-select-trigger-width)",
184
+ minWidth: "max-content",
185
+ maxHeight: "500px",
186
+ ...contentStyleProp,
187
+ };
188
+ }, [contentStyleProp]);
189
+
184
190
  const content = (
185
191
  <Select.Root
186
192
  value={value}
@@ -206,12 +212,8 @@ export const SelectMenu = memoGeneric(function SelectMenu<
206
212
  position="popper"
207
213
  sideOffset={6}
208
214
  collisionPadding={8}
209
- align="end"
210
- style={{
211
- width: "var(--radix-select-trigger-width)",
212
- maxHeight: "500px",
213
- ...contentStyle,
214
- }}
215
+ align="center"
216
+ style={contentStyle}
215
217
  >
216
218
  <Select.ScrollUpButton className={scrollButtonStyles}>
217
219
  <ChevronUpIcon
@@ -6,11 +6,13 @@ import {
6
6
  DragOverlay,
7
7
  DragStartEvent,
8
8
  PointerSensor,
9
- Translate,
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,
@@ -34,7 +36,7 @@ export const normalizeListDestinationIndex = (
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
  destinationIndex,
40
42
  position
@@ -51,34 +53,47 @@ const defaultAcceptsDrop: DropValidator = (
51
53
  };
52
54
 
53
55
  const SortableItemContext = React.createContext<{
54
- keys: string[];
55
- position: Translate;
56
+ keys: UniqueIdentifier[];
56
57
  acceptsDrop: DropValidator;
57
- setActivatorEvent: (event: PointerEvent) => void;
58
58
  axis: "x" | "y";
59
+ position: { x: number; y: number };
60
+ setActivatorEvent: (event: PointerEvent) => void;
59
61
  }>({
60
62
  keys: [],
61
- position: { x: 0, y: 0 },
62
63
  acceptsDrop: defaultAcceptsDrop,
63
- setActivatorEvent: () => {},
64
64
  axis: "y",
65
+ position: { x: 0, y: 0 },
66
+ setActivatorEvent: () => {},
65
67
  });
66
68
 
67
- function validateDropIndicator(
68
- acceptsDrop: DropValidator,
69
- keys: string[],
70
- activeId: string,
71
- overId: string,
72
- offsetStart: number,
73
- elementStart: number,
74
- elementSize: number
75
- ): RelativeDropPosition | undefined {
76
- const activeIndex = keys.indexOf(activeId);
77
- const overIndex = keys.indexOf(overId);
69
+ type ValidateDropIndicatorParams = {
70
+ acceptsDrop: DropValidator; // Function that validates if a drop is allowed
71
+ keys: UniqueIdentifier[]; // Array of all sortable item IDs
72
+ activeId: UniqueIdentifier; // ID of item being dragged
73
+ overId: UniqueIdentifier; // ID of item being dragged over
74
+ offsetStart: number; // Current drag position (x or y coordinate)
75
+ elementStart: number; // Start position of target element (left or top)
76
+ elementSize: number; // Size of target element (width or height)
77
+ };
78
+
79
+ export function validateDropIndicator({
80
+ acceptsDrop,
81
+ keys,
82
+ activeId,
83
+ overId,
84
+ offsetStart,
85
+ elementStart,
86
+ elementSize,
87
+ }: ValidateDropIndicatorParams): RelativeDropPosition | undefined {
88
+ const activeIndex = keys.findIndex((id) => id === activeId); // index of item being dragged
89
+ const overIndex = keys.findIndex((id) => id === overId); // index of item being dragged over
90
+
91
+ if (activeIndex === -1 || overIndex === -1) return undefined;
78
92
 
79
93
  const acceptsDropInside = acceptsDrop(activeIndex, overIndex, "inside");
80
94
 
81
- // If we're in the center of the element, prefer dropping inside
95
+ // If cursor is in the middle third of the element (between 1/3 and 2/3)
96
+ // and dropping inside is allowed, show "inside" indicator
82
97
  if (
83
98
  offsetStart >= elementStart + elementSize / 3 &&
84
99
  offsetStart <= elementStart + (elementSize * 2) / 3 &&
@@ -89,7 +104,6 @@ function validateDropIndicator(
89
104
  // Are we over the top or bottom half of the element?
90
105
  const indicator =
91
106
  offsetStart < elementStart + elementSize / 2 ? "above" : "below";
92
-
93
107
  // Drop above or below if possible, falling back to inside
94
108
  return acceptsDrop(activeIndex, overIndex, indicator)
95
109
  ? indicator
@@ -105,13 +119,13 @@ function validateDropIndicator(
105
119
  type UseSortableReturnType = ReturnType<typeof useSortable>;
106
120
 
107
121
  interface ItemProps<T> {
108
- id: string;
122
+ id: UniqueIdentifier;
109
123
  disabled?: boolean;
110
124
  children: (
111
125
  props: {
112
126
  ref: React.Ref<T>;
113
127
  relativeDropPosition?: RelativeDropPosition;
114
- [key: string]: any;
128
+ style?: React.CSSProperties;
115
129
  } & UseSortableReturnType["attributes"]
116
130
  ) => JSX.Element;
117
131
  }
@@ -124,10 +138,10 @@ function SortableItem<T extends HTMLElement>({
124
138
  const { keys, position, acceptsDrop, setActivatorEvent, axis } =
125
139
  React.useContext(SortableItemContext);
126
140
  const sortable = useSortable({ id, disabled });
141
+ const { activatorEvent } = useDndContext();
127
142
 
128
143
  const {
129
144
  active,
130
- activatorEvent,
131
145
  attributes,
132
146
  listeners,
133
147
  setNodeRef,
@@ -154,15 +168,15 @@ function SortableItem<T extends HTMLElement>({
154
168
  ...listeners,
155
169
  relativeDropPosition:
156
170
  index >= 0 && index === overIndex && !isDragging && active && over
157
- ? validateDropIndicator(
171
+ ? validateDropIndicator({
158
172
  acceptsDrop,
159
173
  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
- )
174
+ activeId: active.id,
175
+ overId: over.id,
176
+ offsetStart: axis === "x" ? offsetLeft : offsetTop,
177
+ elementStart: axis === "x" ? over.rect.left : over.rect.top,
178
+ elementSize: axis === "x" ? over.rect.width : over.rect.height,
179
+ })
166
180
  : undefined,
167
181
  });
168
182
  }
@@ -172,7 +186,7 @@ function SortableItem<T extends HTMLElement>({
172
186
  * ------------------------------------------------------------------------- */
173
187
 
174
188
  interface RootProps {
175
- keys: string[];
189
+ keys: UniqueIdentifier[];
176
190
  children: React.ReactNode;
177
191
  renderOverlay?: (index: number) => React.ReactNode;
178
192
  onMoveItem?: (
@@ -202,16 +216,19 @@ function SortableRoot({
202
216
 
203
217
  const [activeIndex, setActiveIndex] = React.useState<number | undefined>();
204
218
  const activatorEvent = React.useRef<PointerEvent | null>(null);
219
+ const [position, setPosition] = React.useState<{ x: number; y: number }>({
220
+ x: 0,
221
+ y: 0,
222
+ });
205
223
 
206
224
  const setActivatorEvent = React.useCallback((event: PointerEvent) => {
207
225
  activatorEvent.current = event;
208
226
  }, []);
209
227
 
210
- const [position, setPosition] = React.useState<Translate>({ x: 0, y: 0 });
211
-
212
228
  const handleDragStart = React.useCallback(
213
229
  (event: DragStartEvent) => {
214
- setActiveIndex(keys.indexOf(event.active.id));
230
+ const index = keys.findIndex((id) => id === event.active.id);
231
+ setActiveIndex(index);
215
232
  },
216
233
  [keys]
217
234
  );
@@ -227,23 +244,25 @@ function SortableRoot({
227
244
  setActiveIndex(undefined);
228
245
 
229
246
  if (over && active.id !== over.id) {
230
- const oldIndex = keys.indexOf(active.id);
231
- const newIndex = keys.indexOf(over.id);
247
+ const oldIndex = keys.findIndex((id) => id === active.id);
248
+ const newIndex = keys.findIndex((id) => id === over.id);
249
+
250
+ if (oldIndex === -1 || newIndex === -1) return;
232
251
 
233
252
  const eventX = activatorEvent.current?.clientX ?? 0;
234
253
  const eventY = activatorEvent.current?.clientY ?? 0;
235
254
  const offsetLeft = eventX + position.x;
236
255
  const offsetTop = eventY + position.y;
237
256
 
238
- const indicator = validateDropIndicator(
257
+ const indicator = validateDropIndicator({
239
258
  acceptsDrop,
240
259
  keys,
241
- active.id,
242
- over.id,
243
- axis === "x" ? offsetLeft : offsetTop,
244
- axis === "x" ? over.rect.offsetLeft : over.rect.offsetTop,
245
- axis === "x" ? over.rect.width : over.rect.height
246
- );
260
+ activeId: active.id,
261
+ overId: over.id,
262
+ offsetStart: axis === "x" ? offsetLeft : offsetTop,
263
+ elementStart: axis === "x" ? over.rect.left : over.rect.top,
264
+ elementSize: axis === "x" ? over.rect.width : over.rect.height,
265
+ });
247
266
 
248
267
  if (!indicator) return;
249
268
 
@@ -279,7 +298,14 @@ function SortableRoot({
279
298
  onDragMove={handleDragMove}
280
299
  onDragEnd={handleDragEnd}
281
300
  >
282
- <SortableContext items={keys} strategy={verticalListSortingStrategy}>
301
+ <SortableContext
302
+ items={keys}
303
+ strategy={
304
+ axis === "x"
305
+ ? horizontalListSortingStrategy
306
+ : verticalListSortingStrategy
307
+ }
308
+ >
283
309
  {children}
284
310
  </SortableContext>
285
311
  {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?: Exclude<ReactNode, string> | IconName;
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,43 @@ export const isSelectableMenuItemWithScore = (
132
141
  ): item is ScoredSelectableMenuItem<string> =>
133
142
  isSelectableMenuItem(item) && "index" in item && "score" in item;
134
143
 
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
+
135
160
  export const CHECKBOX_WIDTH = 16;
136
161
  export const CHECKBOX_RIGHT_INSET = 8;
162
+ export const CHECKBOX_INDENT_WIDTH = 6;
137
163
 
138
164
  export const styles = {
139
165
  separatorStyle: "h-px bg-divider mx-4 my-1",
140
-
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-3
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
- itemIndicatorStyle: `
154
- flex items-center
155
- relative
156
- -left-2.5
157
- -mr-2
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
+ itemIndicatorStyle: "flex items-center relative -left-2.5 -mr-2",
179
+ contentStyle:
180
+ "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
181
  };
169
182
 
170
183
  function getKeyboardShortcuts<T extends string>(
@@ -256,12 +269,14 @@ export const SectionHeader = memo(function SectionHeader({
256
269
  variant === "label"
257
270
  ? `text-label ${textStyles.label} !font-bold text-text-disabled`
258
271
  : "font-sans text-heading5 font-normal",
259
- "bg-listview-raised-background flex items-center py-1.5 px-4",
272
+ "bg-listview-raised-background flex items-center py-1.5 px-3",
260
273
  isFirst && "-mt-1"
261
274
  )}
262
275
  >
263
276
  {indented && (
264
- <Spacer.Horizontal size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET} />
277
+ <Spacer.Horizontal
278
+ size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET + CHECKBOX_INDENT_WIDTH}
279
+ />
265
280
  )}
266
281
  {title}
267
282
  </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={styles.itemStyle({ disabled: item.disabled })}
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>