@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.
Files changed (55) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +18 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +389 -241
  5. package/dist/index.d.ts +389 -241
  6. package/dist/index.js +4802 -3990
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +4715 -3915
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +7 -7
  11. package/src/__tests__/validateDropIndicator.test.ts +263 -0
  12. package/src/components/ActionMenu.tsx +51 -0
  13. package/src/components/ActivityIndicator.tsx +7 -1
  14. package/src/components/BaseToolbar.tsx +2 -2
  15. package/src/components/Checkbox.tsx +2 -2
  16. package/src/components/Chip.tsx +7 -6
  17. package/src/components/Collection.tsx +30 -2
  18. package/src/components/Combobox.tsx +27 -15
  19. package/src/components/ComboboxMenu.tsx +15 -6
  20. package/src/components/Dialog.tsx +17 -12
  21. package/src/components/DimensionInput.tsx +14 -12
  22. package/src/components/Drawer.tsx +98 -0
  23. package/src/components/EditableText.tsx +3 -1
  24. package/src/components/FillInputField.tsx +2 -2
  25. package/src/components/Grid.tsx +161 -111
  26. package/src/components/GridView.tsx +73 -41
  27. package/src/components/InputField.tsx +148 -208
  28. package/src/components/Label.tsx +4 -68
  29. package/src/components/LabeledField.tsx +7 -1
  30. package/src/components/List.tsx +135 -50
  31. package/src/components/ListView.tsx +63 -43
  32. package/src/components/MediaThumbnail.tsx +117 -0
  33. package/src/components/Message.tsx +8 -8
  34. package/src/components/NoyaLogo.tsx +41 -0
  35. package/src/components/SearchCompletionMenu.tsx +30 -16
  36. package/src/components/SegmentedControl.tsx +1 -1
  37. package/src/components/SelectMenu.tsx +28 -21
  38. package/src/components/Slider.tsx +16 -7
  39. package/src/components/Sortable.tsx +125 -62
  40. package/src/components/internal/Menu.tsx +45 -25
  41. package/src/components/internal/MenuViewport.tsx +7 -1
  42. package/src/components/internal/SelectItem.tsx +53 -28
  43. package/src/components/internal/__tests__/Menu.test.tsx +12 -0
  44. package/src/components/workspace/DrawerWorkspaceLayout.tsx +86 -0
  45. package/src/components/workspace/PanelWorkspaceLayout.tsx +102 -0
  46. package/src/components/{WorkspaceLayout.tsx → workspace/WorkspaceLayout.tsx} +109 -106
  47. package/src/components/workspace/types.ts +31 -0
  48. package/src/contexts/DialogContext.tsx +49 -34
  49. package/src/hooks/usePreservePanelSize.tsx +1 -5
  50. package/src/index.css +8 -1
  51. package/src/index.tsx +6 -1
  52. package/src/theme/index.ts +2 -0
  53. package/src/utils/classNames.ts +51 -3
  54. package/src/utils/inputs.ts +21 -0
  55. package/tailwind.config.ts +8 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noya-app/noya-designsystem",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
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.1",
24
- "@dnd-kit/modifiers": "3.0.0",
25
- "@dnd-kit/sortable": "4.0.0",
26
- "@noya-app/noya-colorpicker": "0.1.17",
27
- "@noya-app/noya-utils": "0.1.3",
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.9",
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",
@@ -51,6 +50,7 @@
51
50
  "vscode-fuzzy-scorer": "^0.0.4"
52
51
  },
53
52
  "devDependencies": {
53
+ "@tailwindcss/container-queries": "^0.1.1",
54
54
  "@types/react": "*",
55
55
  "@types/react-dom": "*",
56
56
  "@types/react-virtualized": "^9.21.21",
@@ -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
+ targetIndex: 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
+ targetIndex: 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
+ targetIndex: 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
+ targetIndex: 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,51 @@
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
+ className?: string;
14
+ style?: React.CSSProperties;
15
+ };
16
+
17
+ export const ActionMenu = memoGeneric(function ActionMenu<
18
+ TMenu extends string,
19
+ >({
20
+ menuItems,
21
+ onSelect,
22
+ selected,
23
+ onOpenChange,
24
+ className,
25
+ style,
26
+ }: ActionMenuProps<TMenu>) {
27
+ const internalStyle = useMemo(() => {
28
+ return {
29
+ backgroundColor: selected
30
+ ? cssVars.colors.primaryPastel
31
+ : cssVars.colors.inputBackground,
32
+ padding: "4px",
33
+ ...style,
34
+ };
35
+ }, [selected, style]);
36
+
37
+ return (
38
+ <DropdownMenu
39
+ items={menuItems}
40
+ onSelect={onSelect}
41
+ onOpenChange={onOpenChange}
42
+ >
43
+ <IconButton
44
+ style={internalStyle}
45
+ className={className}
46
+ iconName="DotsVerticalIcon"
47
+ color={selected ? cssVars.colors.primary : undefined}
48
+ />
49
+ </DropdownMenu>
50
+ );
51
+ });
@@ -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 className="flex justify-center items-center" style={opacityStyle}>
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
  );
@@ -21,7 +21,7 @@ export function BaseToolbar({
21
21
  return (
22
22
  <div className="flex flex-col">
23
23
  <div
24
- className="flex items-center pr-2.5 bg-sidebar-background flex-none relative"
24
+ className="flex items-center pr-2.5 bg-sidebar-background flex-none relative @container/toolbar"
25
25
  style={{
26
26
  flex: `0 0 ${cssVars.spacing.toolbarHeight}`,
27
27
  }}
@@ -39,7 +39,7 @@ export function BaseToolbar({
39
39
  <Spacer.Horizontal size={10} />
40
40
  </>
41
41
  )}
42
- <div className="flex items-center justify-center text-text-muted pointer-events-none lg:absolute inset-0">
42
+ <div className="flex items-center justify-center text-text-muted pointer-events-none @2xl/toolbar:absolute inset-0">
43
43
  <div
44
44
  style={{
45
45
  display: "flex",
@@ -58,8 +58,8 @@ export const Checkbox = React.memo(
58
58
  className={`
59
59
  absolute
60
60
  inset-0
61
- h-[27px]
62
- w-[27px]
61
+ h-input-height
62
+ w-input-height
63
63
  pointer-events-none
64
64
  stroke-white
65
65
  opacity-0
@@ -52,16 +52,16 @@ const ICON_BASE_STYLES = "relative opacity-50 cursor-pointer hover:opacity-85";
52
52
 
53
53
  const ICON_SIZE_STYLES = {
54
54
  large: {
55
- add: "ml-[-2px] mr-[2px] -top-[1px] scale-75",
56
- delete: "mr-[-2px] ml-[2px] scale-75",
55
+ add: "ml-[-2px] mr-[2px] -top-[1px] scale-[0.75]",
56
+ delete: "mr-[-2px] ml-[2px] scale-[0.75]",
57
57
  },
58
58
  medium: {
59
- add: "ml-[-2px] mr-[-2px] scale-60",
60
- delete: "mr-[-2px] scale-60",
59
+ add: "ml-[-2px] mr-[-2px] scale-[0.60]",
60
+ delete: "mr-[-2px] scale-[0.60]",
61
61
  },
62
62
  small: {
63
- add: "ml-[-2px] mr-[-2px] scale-60",
64
- delete: "mr-[-2px] scale-60",
63
+ add: "ml-[-2px] mr-[-2px] scale-[0.60]",
64
+ delete: "mr-[-2px] scale-[0.60]",
65
65
  },
66
66
  } as const;
67
67
 
@@ -82,6 +82,7 @@ export interface ChipProps {
82
82
  onHoverDeleteChange?: (hovering: boolean) => void;
83
83
  style?: React.CSSProperties;
84
84
  tabIndex?: number;
85
+ role?: string;
85
86
  }
86
87
 
87
88
  export const Chip = memo(
@@ -7,16 +7,30 @@ import { List } from "./List";
7
7
 
8
8
  export type CollectionViewType = "grid" | "list";
9
9
 
10
+ export type CollectionThumbnailSize = "auto" | "custom";
11
+
10
12
  export type CollectionRef = {
11
13
  editName: (id: string) => void;
12
14
  };
13
15
 
16
+ export type CollectionThumbnailProps<T> = {
17
+ item: T;
18
+ selected: boolean;
19
+ };
20
+
21
+ export type CollectionRenderActionProps<T> = {
22
+ item: T;
23
+ selected: boolean;
24
+ onOpenChange: (open: boolean) => void;
25
+ };
26
+
14
27
  export interface CollectionProps<T, M extends string = string> {
15
28
  className?: string;
16
29
  items: T[];
17
30
  getId: (item: T) => string;
18
31
  getName: (item: T) => string;
19
32
  getExpanded?: (item: T) => boolean | undefined;
33
+ getPlaceholder?: (item: T) => string;
20
34
  /**
21
35
  * Whether directories should be expandable.
22
36
  * @default true
@@ -30,8 +44,13 @@ export interface CollectionProps<T, M extends string = string> {
30
44
  menuItems?: MenuItem<M>[];
31
45
  onSelectMenuItem?: (action: M, selectedItems: T[]) => void;
32
46
  onRename?: (item: T, newName: string) => void;
33
- renderThumbnail?: (item: T, selected: boolean) => React.ReactNode;
34
- renderAction?: (item: T, selected: boolean) => React.ReactNode;
47
+ renderThumbnail?: ({
48
+ item,
49
+ selected,
50
+ }: CollectionThumbnailProps<T>) => React.ReactNode;
51
+ renderAction?:
52
+ | "menu"
53
+ | ((props: CollectionRenderActionProps<T>) => React.ReactNode);
35
54
  renderDetail?: (item: T, selected: boolean) => React.ReactNode;
36
55
  /** Callback when selection changes. If not provided, selection will be disabled. */
37
56
  onSelectionChange?: (
@@ -44,20 +63,29 @@ export interface CollectionProps<T, M extends string = string> {
44
63
  detailPosition?: "end" | "below";
45
64
  /** Size of the list items. Defaults to 'medium'. */
46
65
  size?: GridViewSize;
66
+ /** Size of the thumbnail. Defaults to 'auto', which will be based on the size prop. */
67
+ thumbnailSize?: CollectionThumbnailSize;
47
68
  /** For testing: Override the hover state with a specific item ID */
48
69
  testHoveredId?: string;
49
70
  /** For testing: Override the renaming state with a specific item ID */
50
71
  testRenamingId?: string;
72
+ /** For testing: Override the drop indicator state with a specific item ID */
73
+ testShowDropIndicatorId?: string;
51
74
  setExpanded?: (item: T, expanded: boolean) => void;
52
75
  acceptsDrop?: ListViewRootProps["acceptsDrop"];
53
76
  onMoveItem?: ListViewRootProps["onMoveItem"];
77
+ getDropTargetParentIndex?: ListViewRootProps["getDropTargetParentIndex"];
54
78
  getDepth?: (item: T) => number;
55
79
  onFilesDrop?: (event: React.DragEvent<Element>) => void;
56
80
  getRenamable?: (item: T) => boolean;
57
81
  /** Currently selected file IDs */
58
82
  selectedIds?: string[];
59
83
  viewType?: CollectionViewType;
84
+ onClickItem?: (itemId: string) => void;
60
85
  onDoubleClickItem?: (itemId: string) => void;
86
+ renderEmptyState?: () => React.ReactElement;
87
+ /** @default false */
88
+ sortable?: boolean;
61
89
  }
62
90
 
63
91
  export const Collection = forwardRefGeneric(function Collection<
@@ -441,11 +441,37 @@ export const Combobox = memoGeneric(
441
441
  [filteredItems]
442
442
  );
443
443
 
444
+ const hasCheckedItems = items.some(
445
+ (item) => isSelectableMenuItem(item) && item.checked
446
+ );
447
+
444
448
  return (
445
449
  <InputField.Root
446
450
  id={id}
447
451
  size={size}
448
452
  sideOffset={6}
453
+ end={
454
+ <>
455
+ {loading && (
456
+ <div className="pr-1.5">
457
+ <ActivityIndicator />
458
+ </div>
459
+ )}
460
+ {!readOnly && (
461
+ <InputField.Button
462
+ variant="floating"
463
+ onClick={handleChevronClick}
464
+ >
465
+ <DropdownChevronIcon
466
+ className={cx(
467
+ "transition-transform duration-100",
468
+ shouldShowMenu ? "rotate-180" : "rotate-0"
469
+ )}
470
+ />
471
+ </InputField.Button>
472
+ )}
473
+ </>
474
+ }
449
475
  renderPopoverContent={({ width }) => {
450
476
  const height = Math.min(
451
477
  ListView.calculateHeight({
@@ -475,6 +501,7 @@ export const Combobox = memoGeneric(
475
501
  style={{
476
502
  flex: `0 0 ${height}px`,
477
503
  }}
504
+ indented={hasCheckedItems}
478
505
  />
479
506
  ) : (
480
507
  <div className="flex flex-col h-[50px] p-5 items-center justify-center">
@@ -519,21 +546,6 @@ export const Combobox = memoGeneric(
519
546
  />
520
547
  )}
521
548
  {children}
522
- {loading && (
523
- <InputField.Label htmlFor={id}>
524
- <ActivityIndicator />
525
- </InputField.Label>
526
- )}
527
- {!readOnly && (
528
- <InputField.Button variant="floating" onClick={handleChevronClick}>
529
- <DropdownChevronIcon
530
- className={cx(
531
- "transition-transform duration-100",
532
- shouldShowMenu ? "rotate-180" : "rotate-0"
533
- )}
534
- />
535
- </InputField.Button>
536
- )}
537
549
  </InputField.Root>
538
550
  );
539
551
  })
@@ -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
- <div className={styles.itemIndicatorStyle}>
95
+ <div {...styles.itemIndicator}>
87
96
  <CheckIcon />
88
97
  </div>
89
- ) : hasCheckedItems ? (
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
- <DialogPrimitive.Content
30
- ref={ref}
31
- className={cx(
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 overflow-y-auto pointer-events-all z-[1000] focus:outline-none
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
- className
40
- )}
41
- {...props}
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>,