@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
@@ -3,12 +3,14 @@ import {
3
3
  memoGeneric,
4
4
  useFileDropTarget,
5
5
  } from "@noya-app/react-utils";
6
- import React, { useImperativeHandle, useRef, useState } from "react";
6
+ import React, { memo, useImperativeHandle, useRef, useState } from "react";
7
7
  import { cssVars } from "../theme";
8
8
  import { cx } from "../utils/classNames";
9
9
  import { updateSelection } from "../utils/selection";
10
+ import { ActionMenu } from "./ActionMenu";
10
11
  import { CollectionProps, CollectionRef } from "./Collection";
11
- import { ListView } from "./ListView";
12
+ import { GridViewSize } from "./Grid";
13
+ import { ListView, ListViewItemInfo } from "./ListView";
12
14
  import { TreeView } from "./TreeView";
13
15
 
14
16
  const cssVarOverrides = {
@@ -19,7 +21,7 @@ const emptyArray: string[] = [];
19
21
 
20
22
  export const List = memoGeneric(
21
23
  forwardRefGeneric(function List<T, M extends string = string>(
22
- props: CollectionProps<T, M> & { ref?: React.ForwardedRef<CollectionRef> },
24
+ props: CollectionProps<T, M>,
23
25
  ref: React.ForwardedRef<CollectionRef>
24
26
  ) {
25
27
  const {
@@ -42,8 +44,10 @@ export const List = memoGeneric(
42
44
  itemRoleDescription = "clickable item",
43
45
  detailPosition = "end",
44
46
  size = "medium",
47
+ thumbnailSize = "auto",
45
48
  testHoveredId,
46
49
  testRenamingId,
50
+ testShowDropIndicatorId,
47
51
  scrollable = false,
48
52
  acceptsDrop,
49
53
  onMoveItem,
@@ -51,11 +55,16 @@ export const List = memoGeneric(
51
55
  onDoubleClickItem,
52
56
  renamable,
53
57
  selectedIds = emptyArray,
58
+ renderEmptyState,
59
+ sortable,
60
+ getDropTargetParentIndex,
61
+ getPlaceholder,
62
+ onClickItem,
54
63
  } = props;
55
64
 
56
65
  const [internalHoveredId, setHoveredId] = useState<string>();
57
66
  const [internalRenamingId, setRenamingId] = useState<string>();
58
-
67
+ const [dropdownOpenId, setDropdownOpenId] = useState<string>();
59
68
  const hoveredId = internalHoveredId ?? testHoveredId;
60
69
  const renamingId = testRenamingId ?? internalRenamingId;
61
70
 
@@ -72,13 +81,17 @@ export const List = memoGeneric(
72
81
  onSelectionChange(newSelectedIds, event);
73
82
  };
74
83
 
75
- const handleMenuAction = (action: M) => {
84
+ const handleMenuAction = (action: M, actionItem: T) => {
76
85
  if (!onSelectMenuItem) return;
77
86
 
78
87
  const selectedItems = items.filter((item) =>
79
88
  selectedIds.includes(getId(item))
80
89
  );
81
- onSelectMenuItem(action, selectedItems);
90
+
91
+ onSelectMenuItem(
92
+ action,
93
+ selectedItems.length === 0 ? [actionItem] : selectedItems
94
+ );
82
95
  };
83
96
 
84
97
  const ViewComponent = expandable ? TreeView : ListView;
@@ -98,7 +111,11 @@ export const List = memoGeneric(
98
111
  return (
99
112
  <ViewComponent.Root
100
113
  variant="bare"
101
- className={cx(className, isDropTargetActive && "bg-primary-pastel")}
114
+ className={cx(
115
+ className,
116
+ isDropTargetActive && "bg-primary-pastel",
117
+ scrollable && "basis-0 min-h-0"
118
+ )}
102
119
  containerRef={fileDropTargetRef}
103
120
  scrollable={scrollable}
104
121
  data={items}
@@ -107,28 +124,53 @@ export const List = memoGeneric(
107
124
  aria-orientation="vertical"
108
125
  aria-multiselectable={true}
109
126
  expandable={expandable}
110
- sortable={expandable}
127
+ sortable={sortable}
111
128
  acceptsDrop={acceptsDrop}
112
129
  onMoveItem={onMoveItem}
130
+ renderEmptyState={renderEmptyState}
131
+ getDropTargetParentIndex={getDropTargetParentIndex}
113
132
  {...dropTargetProps}
114
133
  renderItem={(
115
134
  item: T,
116
135
  _: number,
117
- { isDragging }: { isDragging: boolean }
136
+ { isDragOverlay }: ListViewItemInfo
118
137
  ) => {
119
138
  const id = getId(item);
120
- const isHovered = hoveredId === id && !isDragging;
139
+ const isDropdownOpen = dropdownOpenId === id;
140
+ const isHovered =
141
+ (hoveredId === id || isDropdownOpen) && !isDragOverlay;
121
142
  const expanded = getExpanded?.(item);
122
143
  const itemIsRenamable = getRenamable?.(item) ?? renamable;
123
144
  const isSelected = selectedIds.includes(id);
124
145
  const isRenaming = itemIsRenamable && onRename && renamingId === id;
125
146
  const isTestingRenaming = testRenamingId === id;
126
147
 
127
- const thumbnail = renderThumbnail?.(item, isSelected);
148
+ const thumbnail = renderThumbnail?.({
149
+ item,
150
+ selected: isSelected,
151
+ });
152
+ const onOpenChange = (open: boolean) => {
153
+ setDropdownOpenId(open ? id : undefined);
154
+
155
+ if (open && !isSelected) {
156
+ handleSelect(id);
157
+ }
158
+ };
128
159
  const action =
129
160
  (isHovered || isSelected) &&
130
161
  !isRenaming &&
131
- renderAction?.(item, isSelected);
162
+ (typeof renderAction === "function"
163
+ ? renderAction({ item, selected: isSelected, onOpenChange })
164
+ : renderAction === "menu" &&
165
+ menuItems && (
166
+ <ActionMenu
167
+ menuItems={menuItems}
168
+ onSelect={(action) => handleMenuAction(action, item)}
169
+ selected={isSelected}
170
+ onOpenChange={onOpenChange}
171
+ />
172
+ ));
173
+
132
174
  const end =
133
175
  action ||
134
176
  (detailPosition === "end" && renderDetail?.(item, isSelected));
@@ -140,14 +182,17 @@ export const List = memoGeneric(
140
182
  id={id}
141
183
  key={id}
142
184
  role="option"
143
- sortable={expandable}
185
+ sortable={sortable}
144
186
  aria-roledescription={itemRoleDescription}
145
187
  aria-label={getName(item)}
146
188
  aria-selected={isSelected}
147
- className={cx("cursor-pointer p-1 rounded")}
189
+ className={cx("cursor-pointer rounded px-1", pyMap[size])}
148
190
  backgroundColor={
149
191
  isSelected ? cssVars.colors.primaryPastel : undefined
150
192
  }
193
+ testRelativeDropPosition={
194
+ testShowDropIndicatorId === id ? "inside" : undefined
195
+ }
151
196
  chevronClassName={cx(expandable && "relative left-2")}
152
197
  style={cssVarOverrides as React.CSSProperties}
153
198
  hovered={isHovered}
@@ -159,6 +204,7 @@ export const List = memoGeneric(
159
204
  }
160
205
  onPress={(e: ListView.ClickInfo) => {
161
206
  handleSelect(id, e);
207
+ onClickItem?.(id);
162
208
  }}
163
209
  onDoubleClick={() => {
164
210
  onDoubleClickItem?.(id);
@@ -172,15 +218,10 @@ export const List = memoGeneric(
172
218
  }
173
219
  }}
174
220
  menuItems={menuItems}
175
- onSelectMenuItem={handleMenuAction}
221
+ onSelectMenuItem={(action) => handleMenuAction(action, item)}
176
222
  onContextMenu={() => {
177
223
  if (!isSelected) {
178
- handleSelect(id, {
179
- shiftKey: false,
180
- altKey: false,
181
- metaKey: false,
182
- ctrlKey: false,
183
- });
224
+ handleSelect(id);
184
225
  }
185
226
  }}
186
227
  tabIndex={0}
@@ -189,44 +230,44 @@ export const List = memoGeneric(
189
230
  setExpanded?.(item, !expanded);
190
231
  }}
191
232
  >
192
- <div className="flex flex-1 items-center px-2 gap-3">
233
+ <div
234
+ className={cx("flex flex-1 items-center px-2", rowGapMap[size])}
235
+ >
193
236
  {thumbnail && (
194
237
  <div
195
238
  className={cx(
196
239
  "rounded overflow-hidden flex-none flex items-center justify-center",
197
- size === "medium" ? "w-8 h-8" : "w-16 h-16",
240
+ thumbnailSize === "auto" && thumbnailSizeMap[size],
198
241
  isSelected && "text-primary"
199
242
  )}
200
- style={{
201
- backgroundColor: isSelected
202
- ? cssVars.colors.primaryPastel
203
- : cssVars.colors.inputBackground,
204
- }}
205
243
  tabIndex={-1}
206
244
  >
207
245
  {thumbnail}
208
246
  </div>
209
247
  )}
210
248
  <div className="flex flex-col flex-1 w-0">
211
- <div className="flex items-center min-h-[27px] flex-1 gap-2">
249
+ <div className="flex items-center min-h-input-height flex-1 gap-2">
212
250
  <div className="flex-1 w-0 flex items-center" tabIndex={-1}>
213
251
  {isRenaming ? (
214
252
  <ViewComponent.EditableRowTitle
215
253
  value={getName(item)}
216
- placeholder="Enter name"
217
- className="font-medium"
254
+ placeholder={getPlaceholder?.(item) ?? "Enter name"}
255
+ className={fontStyleMap[size]}
218
256
  autoFocus={!isTestingRenaming}
219
257
  onSubmitEditing={(value: string) => {
220
- if (value !== getName(item)) {
258
+ if (value !== getName(item) && value !== "") {
221
259
  onRename?.(item, value);
222
260
  }
223
261
  setRenamingId(undefined);
224
262
  }}
263
+ onKeyDown={(e: React.KeyboardEvent) => {
264
+ e.stopPropagation();
265
+ }}
225
266
  />
226
267
  ) : (
227
268
  <ViewComponent.RowTitle
228
269
  className={cx(
229
- "font-medium",
270
+ fontStyleMap[size],
230
271
  isSelected && "text-primary"
231
272
  )}
232
273
  >
@@ -234,30 +275,21 @@ export const List = memoGeneric(
234
275
  </ViewComponent.RowTitle>
235
276
  )}
236
277
  </div>
237
- {end && (
238
- <div
239
- className={cx(
240
- "flex items-center gap-2",
241
- isSelected && "text-primary"
242
- )}
243
- tabIndex={-1}
244
- >
278
+ {end && detailPosition === "end" && (
279
+ <DetailContainer selected={isSelected}>
245
280
  {end}
246
- </div>
281
+ </DetailContainer>
247
282
  )}
248
283
  </div>
249
284
  {below && (
250
- <div
251
- className={cx(
252
- "flex items-center",
253
- isSelected && "text-primary"
254
- )}
255
- tabIndex={-1}
256
- >
285
+ <DetailContainer selected={isSelected}>
257
286
  {below}
258
- </div>
287
+ </DetailContainer>
259
288
  )}
260
289
  </div>
290
+ {end && detailPosition === "below" && (
291
+ <DetailContainer selected={isSelected}>{end}</DetailContainer>
292
+ )}
261
293
  </div>
262
294
  </ViewComponent.Row>
263
295
  );
@@ -266,3 +298,56 @@ export const List = memoGeneric(
266
298
  );
267
299
  })
268
300
  );
301
+
302
+ const DetailContainer = memo(function DetailContainer({
303
+ children,
304
+ selected,
305
+ }: {
306
+ children: React.ReactNode;
307
+ selected: boolean;
308
+ }) {
309
+ return (
310
+ <div
311
+ className={cx("flex items-center gap-2", selected && "text-primary")}
312
+ tabIndex={-1}
313
+ >
314
+ {children}
315
+ </div>
316
+ );
317
+ });
318
+
319
+ const pyMap: Record<GridViewSize, string> = {
320
+ xxs: "py-1",
321
+ xs: "py-1",
322
+ small: "py-1",
323
+ medium: "py-1",
324
+ large: "py-2",
325
+ xl: "py-3",
326
+ };
327
+
328
+ const thumbnailSizeMap: Record<GridViewSize, string> = {
329
+ xxs: "w-4 h-4",
330
+ xs: "w-4 h-4",
331
+ small: "w-6 h-6",
332
+ medium: "w-8 h-8",
333
+ large: "w-16 h-16",
334
+ xl: "w-24 h-24",
335
+ };
336
+
337
+ const rowGapMap: Record<GridViewSize, string> = {
338
+ xxs: "gap-3",
339
+ xs: "gap-3",
340
+ small: "gap-3",
341
+ medium: "gap-3",
342
+ large: "gap-3",
343
+ xl: "gap-4",
344
+ };
345
+
346
+ const fontStyleMap: Record<GridViewSize, string> = {
347
+ xxs: "font-medium",
348
+ xs: "font-medium",
349
+ small: "font-medium",
350
+ medium: "font-medium",
351
+ large: "font-medium",
352
+ xl: "font-medium text-heading4 text-text",
353
+ };
@@ -25,7 +25,7 @@ import { WindowScroller, WindowScrollerChildProps } from "react-virtualized";
25
25
  import { ListChildComponentProps, VariableSizeList } from "react-window";
26
26
  import { mergeEventHandlers } from "../hooks/mergeEventHandlers";
27
27
  import { useHover } from "../hooks/useHover";
28
- import { cssVars } from "../theme";
28
+ import { cssVars, INPUT_HEIGHT } from "../theme";
29
29
  import { cx } from "../utils/classNames";
30
30
  import { ContextMenu } from "./ContextMenu";
31
31
  import { InputField } from "./InputField";
@@ -33,9 +33,10 @@ import { MenuItem } from "./internal/Menu";
33
33
  import { ScrollArea } from "./ScrollArea";
34
34
  import {
35
35
  DropValidator,
36
- normalizeListDestinationIndex,
36
+ normalizeListTargetIndex,
37
37
  RelativeDropPosition,
38
38
  Sortable,
39
+ type SortableItemContextProps,
39
40
  } from "./Sortable";
40
41
  import { Spacer } from "./Spacer";
41
42
 
@@ -43,7 +44,7 @@ export type ListRowMarginType = "none" | "top" | "bottom" | "vertical";
43
44
  export type ListRowPosition = "only" | "first" | "middle" | "last";
44
45
 
45
46
  const ROW_HEIGHT = 31;
46
- const SECTION_HEADER_LABEL_HEIGHT = 27;
47
+ const SECTION_HEADER_LABEL_HEIGHT = INPUT_HEIGHT;
47
48
 
48
49
  type ListColorScheme = "primary" | "secondary";
49
50
 
@@ -71,7 +72,7 @@ type ListRowContextValue = {
71
72
  // and the dragged row doesn't have a ListRowContextValue.
72
73
  type ListViewDraggingContextValue = {
73
74
  indentation: number;
74
- isDragging?: boolean;
75
+ isDragOverlay?: boolean;
75
76
  };
76
77
 
77
78
  const ListViewDraggingContext = createContext<ListViewDraggingContextValue>({
@@ -127,6 +128,7 @@ export interface EditableRowProps {
127
128
  autoFocus: boolean;
128
129
  placeholder?: string;
129
130
  className?: string;
131
+ onKeyDown?: (e: React.KeyboardEvent) => void;
130
132
  }
131
133
 
132
134
  function ListViewEditableRowTitle({
@@ -135,6 +137,7 @@ function ListViewEditableRowTitle({
135
137
  autoFocus,
136
138
  placeholder,
137
139
  className,
140
+ onKeyDown,
138
141
  }: EditableRowProps) {
139
142
  const inputRef = useRef<HTMLInputElement | null>(null);
140
143
 
@@ -161,6 +164,7 @@ function ListViewEditableRowTitle({
161
164
  onSubmit={onSubmitEditing}
162
165
  allowSubmittingWithSameValue
163
166
  className={cx("-mx-1.5 -my-1 bg-listview-editing-background", className)}
167
+ onKeyDown={onKeyDown}
164
168
  />
165
169
  );
166
170
  }
@@ -266,9 +270,10 @@ const RowContainer = forwardRef<
266
270
  borderBottomRightRadius: "0px",
267
271
  borderBottomLeftRadius: "0px",
268
272
  }),
269
- ...($hovered && {
270
- boxShadow: `0 0 0 1px ${cssVars.colors[$colorScheme]} inset`,
271
- }),
273
+ ...($hovered &&
274
+ !$selected && {
275
+ background: cssVars.colors.listViewHoverBackground,
276
+ }),
272
277
  ...($showsActiveState && {
273
278
  "&:active": {
274
279
  backgroundColor: $selected
@@ -301,7 +306,7 @@ const RowContainer = forwardRef<
301
306
  const ListViewDragIndicatorElement = forwardRef<
302
307
  HTMLDivElement,
303
308
  React.HTMLAttributes<HTMLDivElement> & {
304
- $relativeDropPosition: RelativeDropPosition;
309
+ $relativeDropPosition?: RelativeDropPosition;
305
310
  $gap: number;
306
311
  $offsetLeft: number;
307
312
  $colorScheme: ListColorScheme;
@@ -398,13 +403,16 @@ interface ListViewRowProps<MenuItemType extends string = string> {
398
403
  className?: string;
399
404
  dragIndicatorStyle?:
400
405
  | CSSProperties
401
- | ((props: {
402
- depth: number;
403
- indentation: number;
404
- position: RelativeDropPosition;
405
- }) => CSSProperties);
406
+ | ((props: DragIndicatorStyleProps) => CSSProperties);
407
+ testRelativeDropPosition?: RelativeDropPosition;
406
408
  }
407
409
 
410
+ export type DragIndicatorStyleProps = {
411
+ depth: number;
412
+ indentation: number;
413
+ position: RelativeDropPosition;
414
+ };
415
+
408
416
  const ListViewRow = forwardRefGeneric(function ListViewRow<
409
417
  MenuItemType extends string,
410
418
  >(
@@ -432,6 +440,7 @@ const ListViewRow = forwardRefGeneric(function ListViewRow<
432
440
  style,
433
441
  dragIndicatorStyle,
434
442
  className,
443
+ testRelativeDropPosition,
435
444
  }: ListViewRowProps<MenuItemType>,
436
445
  forwardedRef: ForwardedRef<HTMLElement>
437
446
  ) {
@@ -451,7 +460,7 @@ const ListViewRow = forwardRefGeneric(function ListViewRow<
451
460
  const { hoverProps } = useHover({
452
461
  onHoverChange,
453
462
  });
454
- const { indentation, isDragging } = useContext(ListViewDraggingContext);
463
+ const { indentation, isDragOverlay } = useContext(ListViewDraggingContext);
455
464
 
456
465
  const handlePress = useCallback(
457
466
  (event: React.MouseEvent) => {
@@ -478,7 +487,7 @@ const ListViewRow = forwardRefGeneric(function ListViewRow<
478
487
  );
479
488
 
480
489
  const mergedStyle = useMemo(() => {
481
- return isDragging
490
+ return isDragOverlay
482
491
  ? {
483
492
  // TODO: Where do these offsets actually come from?
484
493
  transform: `translate3d(-36px, -6px, 0)`,
@@ -486,11 +495,11 @@ const ListViewRow = forwardRefGeneric(function ListViewRow<
486
495
  ...style,
487
496
  }
488
497
  : style;
489
- }, [isDragging, style]);
498
+ }, [isDragOverlay, style]);
490
499
 
491
500
  const renderContent = (
492
501
  {
493
- relativeDropPosition,
502
+ relativeDropPosition: relativeDropPositionProp,
494
503
  ...renderProps
495
504
  }: React.ComponentProps<typeof RowContainer> & {
496
505
  relativeDropPosition?: RelativeDropPosition;
@@ -498,6 +507,8 @@ const ListViewRow = forwardRefGeneric(function ListViewRow<
498
507
  ref: Ref<HTMLElement>
499
508
  ) => {
500
509
  const Component = RowContainer;
510
+ const relativeDropPosition =
511
+ testRelativeDropPosition ?? relativeDropPositionProp;
501
512
 
502
513
  const element = (
503
514
  <Component
@@ -520,7 +531,7 @@ const ListViewRow = forwardRefGeneric(function ListViewRow<
520
531
  $selectedPosition={selectedPosition}
521
532
  $showsActiveState={pressEventName === "onClick"}
522
533
  aria-selected={selected}
523
- $divider={!isDragging && divider}
534
+ $divider={!isDragOverlay && divider}
524
535
  onKeyDown={onKeyDown}
525
536
  style={mergedStyle}
526
537
  {...(renderProps as Partial<React.ComponentProps<typeof RowContainer>>)}
@@ -534,9 +545,10 @@ const ListViewRow = forwardRefGeneric(function ListViewRow<
534
545
  >
535
546
  {relativeDropPosition && (
536
547
  <ListViewDragIndicatorElement
548
+ key={relativeDropPosition}
537
549
  $colorScheme={colorScheme}
538
550
  $relativeDropPosition={relativeDropPosition}
539
- $offsetLeft={33 + depth * indentation}
551
+ $offsetLeft={depth * indentation}
540
552
  $gap={listGap + (divider ? 1 : 0)}
541
553
  style={
542
554
  typeof dragIndicatorStyle === "function"
@@ -724,8 +736,9 @@ const VirtualizedList = memo(
724
736
  * Root
725
737
  * ------------------------------------------------------------------------- */
726
738
 
727
- type ListViewItemInfo = {
728
- isDragging: boolean;
739
+ export type ListViewItemInfo = {
740
+ isDragOverlay: boolean;
741
+ activeDragIndex?: number;
729
742
  };
730
743
 
731
744
  type ChildrenProps = {
@@ -762,7 +775,7 @@ export type ListViewRootProps = {
762
775
  expandable?: boolean;
763
776
  onMoveItem?: (
764
777
  sourceIndex: number,
765
- destinationIndex: number,
778
+ targetIndex: number,
766
779
  position: RelativeDropPosition
767
780
  ) => void;
768
781
  indentation?: number;
@@ -778,6 +791,8 @@ export type ListViewRootProps = {
778
791
  onDragEnter?: React.DragEventHandler<HTMLDivElement>;
779
792
  onDragLeave?: React.DragEventHandler<HTMLDivElement>;
780
793
  onDrop?: React.DragEventHandler<HTMLDivElement>;
794
+ renderEmptyState?: () => ReactNode;
795
+ getDropTargetParentIndex?: SortableItemContextProps["getDropTargetParentIndex"];
781
796
  };
782
797
 
783
798
  const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
@@ -811,6 +826,8 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
811
826
  onDragEnter,
812
827
  onDragLeave,
813
828
  onDrop,
829
+ renderEmptyState,
830
+ getDropTargetParentIndex,
814
831
  }: RenderProps<T> & ListViewRootProps,
815
832
  forwardedRef: ForwardedRef<IVirtualizedList>
816
833
  ) {
@@ -832,7 +849,7 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
832
849
  );
833
850
 
834
851
  const renderChild = useCallback(
835
- (index: number) => renderItem(data[index], index, { isDragging: false }),
852
+ (index: number) => renderItem(data[index], index, { isDragOverlay: false }),
836
853
  [data, renderItem]
837
854
  );
838
855
 
@@ -947,7 +964,7 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
947
964
  );
948
965
 
949
966
  const draggingContextOverlayValue = useMemo(
950
- () => ({ indentation, isDragging: true }),
967
+ (): ListViewDraggingContextValue => ({ indentation, isDragOverlay: true }),
951
968
  [indentation]
952
969
  );
953
970
 
@@ -955,7 +972,7 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
955
972
  (index: number) => (
956
973
  <div style={{ opacity: 0.25 }}>
957
974
  <ListViewDraggingContext.Provider value={draggingContextOverlayValue}>
958
- {renderItem(data[index], index, { isDragging: true })}
975
+ {renderItem(data[index], index, { isDragOverlay: true })}
959
976
  </ListViewDraggingContext.Provider>
960
977
  </div>
961
978
  ),
@@ -987,6 +1004,7 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
987
1004
  keys={ids}
988
1005
  renderOverlay={renderOverlay}
989
1006
  acceptsDrop={acceptsDrop}
1007
+ getDropTargetParentIndex={getDropTargetParentIndex}
990
1008
  >
991
1009
  {children}
992
1010
  </Sortable.Root>
@@ -1040,23 +1058,25 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
1040
1058
  onDragLeave={onDragLeave}
1041
1059
  onDrop={onDrop}
1042
1060
  >
1043
- {withScrollable((scrollElementRef: HTMLDivElement | null) =>
1044
- withSortable(
1045
- virtualized ? (
1046
- <VirtualizedList<T>
1047
- ref={forwardedRef}
1048
- scrollElement={scrollElementRef!}
1049
- items={data}
1050
- size={virtualized}
1051
- getItemHeight={getItemHeight}
1052
- keyExtractor={getKey}
1053
- renderItem={renderWrappedChild}
1054
- />
1055
- ) : (
1056
- range(0, data.length).map(renderWrappedChild)
1057
- )
1058
- )
1059
- )}
1061
+ {data.length === 0 && renderEmptyState
1062
+ ? renderEmptyState()
1063
+ : withScrollable((scrollElementRef: HTMLDivElement | null) =>
1064
+ withSortable(
1065
+ virtualized ? (
1066
+ <VirtualizedList<T>
1067
+ ref={forwardedRef}
1068
+ scrollElement={scrollElementRef!}
1069
+ items={data}
1070
+ size={virtualized}
1071
+ getItemHeight={getItemHeight}
1072
+ keyExtractor={getKey}
1073
+ renderItem={renderWrappedChild}
1074
+ />
1075
+ ) : (
1076
+ range(0, data.length).map(renderWrappedChild)
1077
+ )
1078
+ )
1079
+ )}
1060
1080
  </div>
1061
1081
  </ListViewDraggingContext.Provider>
1062
1082
  );
@@ -1136,5 +1156,5 @@ export namespace ListView {
1136
1156
  menuItemsCount * rowHeight + sectionHeaderCount * sectionHeaderLabelHeight
1137
1157
  );
1138
1158
  };
1139
- export const normalizeDestinationIndex = normalizeListDestinationIndex;
1159
+ export const normalizeTargetIndex = normalizeListTargetIndex;
1140
1160
  }