@dxos/react-ui-list 0.6.13-main.548ca8d → 0.6.14-main.1366248

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 (52) hide show
  1. package/dist/lib/browser/index.mjs +413 -31
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/List/List.d.ts +1 -1
  5. package/dist/types/src/components/List/List.d.ts.map +1 -1
  6. package/dist/types/src/components/List/List.stories.d.ts +6 -10
  7. package/dist/types/src/components/List/List.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/List/ListItem.d.ts +1 -3
  9. package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
  10. package/dist/types/src/components/List/ListRoot.d.ts +5 -3
  11. package/dist/types/src/components/List/ListRoot.d.ts.map +1 -1
  12. package/dist/types/src/components/List/testing.d.ts.map +1 -0
  13. package/dist/types/src/components/Tree/DropIndicator.d.ts +7 -0
  14. package/dist/types/src/components/Tree/DropIndicator.d.ts.map +1 -0
  15. package/dist/types/src/components/Tree/Tree.d.ts +24 -0
  16. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -0
  17. package/dist/types/src/components/Tree/Tree.stories.d.ts +8 -0
  18. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -0
  19. package/dist/types/src/components/Tree/TreeItem.d.ts +34 -0
  20. package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -0
  21. package/dist/types/src/components/Tree/TreeItemHeading.d.ts +12 -0
  22. package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -0
  23. package/dist/types/src/components/Tree/TreeItemToggle.d.ts +8 -0
  24. package/dist/types/src/components/Tree/TreeItemToggle.d.ts.map +1 -0
  25. package/dist/types/src/components/Tree/helpers.d.ts +8 -0
  26. package/dist/types/src/components/Tree/helpers.d.ts.map +1 -0
  27. package/dist/types/src/components/Tree/index.d.ts +4 -0
  28. package/dist/types/src/components/Tree/index.d.ts.map +1 -0
  29. package/dist/types/src/components/Tree/testing.d.ts +26 -0
  30. package/dist/types/src/components/Tree/testing.d.ts.map +1 -0
  31. package/dist/types/src/components/Tree/types.d.ts +18 -0
  32. package/dist/types/src/components/Tree/types.d.ts.map +1 -0
  33. package/dist/types/src/components/index.d.ts +1 -0
  34. package/dist/types/src/components/index.d.ts.map +1 -1
  35. package/package.json +20 -15
  36. package/src/components/List/List.stories.tsx +25 -19
  37. package/src/components/List/ListItem.tsx +11 -9
  38. package/src/components/List/ListRoot.tsx +47 -23
  39. package/src/components/Tree/DropIndicator.tsx +79 -0
  40. package/src/components/Tree/Tree.stories.tsx +116 -0
  41. package/src/components/Tree/Tree.tsx +56 -0
  42. package/src/components/Tree/TreeItem.tsx +237 -0
  43. package/src/components/Tree/TreeItemHeading.tsx +62 -0
  44. package/src/components/Tree/TreeItemToggle.tsx +35 -0
  45. package/src/components/Tree/helpers.ts +25 -0
  46. package/src/components/Tree/index.ts +7 -0
  47. package/src/components/Tree/testing.ts +170 -0
  48. package/src/components/Tree/types.ts +34 -0
  49. package/src/components/index.ts +1 -0
  50. package/dist/types/src/testing.d.ts.map +0 -1
  51. /package/dist/types/src/{testing.d.ts → components/List/testing.d.ts} +0 -0
  52. /package/src/{testing.ts → components/List/testing.ts} +0 -0
@@ -1,2 +1,3 @@
1
1
  export * from './List';
2
+ export * from './Tree';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAIA,cAAc,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/index.ts"],"names":[],"mappings":"AAIA,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-list",
3
- "version": "0.6.13-main.548ca8d",
3
+ "version": "0.6.14-main.1366248",
4
4
  "description": "A list component.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -8,8 +8,8 @@
8
8
  "author": "DXOS.org",
9
9
  "exports": {
10
10
  ".": {
11
- "browser": "./dist/lib/browser/index.mjs",
12
- "types": "./dist/types/src/index.d.ts"
11
+ "types": "./dist/types/src/index.d.ts",
12
+ "browser": "./dist/lib/browser/index.mjs"
13
13
  }
14
14
  },
15
15
  "types": "dist/types/src/index.d.ts",
@@ -25,16 +25,18 @@
25
25
  "@atlaskit/pragmatic-drag-and-drop-flourish": "^1.1.2",
26
26
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
27
27
  "@atlaskit/pragmatic-drag-and-drop-react-drop-indicator": "^1.1.3",
28
+ "@preact/signals-core": "^1.6.0",
28
29
  "@radix-ui/react-context": "^1.0.0",
29
30
  "effect": "^3.9.1",
30
- "@dxos/debug": "0.6.13-main.548ca8d",
31
- "@dxos/invariant": "0.6.13-main.548ca8d",
32
- "@dxos/echo-schema": "0.6.13-main.548ca8d",
33
- "@dxos/react-ui": "0.6.13-main.548ca8d",
34
- "@dxos/react-ui-types": "0.6.13-main.548ca8d",
35
- "@dxos/react-ui-theme": "0.6.13-main.548ca8d",
36
- "@dxos/log": "0.6.13-main.548ca8d",
37
- "@dxos/util": "0.6.13-main.548ca8d"
31
+ "@dxos/debug": "0.6.14-main.1366248",
32
+ "@dxos/invariant": "0.6.14-main.1366248",
33
+ "@dxos/echo-schema": "0.6.14-main.1366248",
34
+ "@dxos/log": "0.6.14-main.1366248",
35
+ "@dxos/react-ui-mosaic": "0.6.14-main.1366248",
36
+ "@dxos/react-ui-attention": "0.6.14-main.1366248",
37
+ "@dxos/react-ui-text-tooltip": "0.6.14-main.1366248",
38
+ "@dxos/react-ui-types": "0.6.14-main.1366248",
39
+ "@dxos/util": "0.6.14-main.1366248"
38
40
  },
39
41
  "devDependencies": {
40
42
  "@phosphor-icons/react": "^2.1.5",
@@ -43,15 +45,18 @@
43
45
  "react": "~18.2.0",
44
46
  "react-dom": "~18.2.0",
45
47
  "vite": "5.4.7",
46
- "@dxos/react-ui-theme": "0.6.13-main.548ca8d",
47
- "@dxos/random": "0.6.13-main.548ca8d",
48
- "@dxos/storybook-utils": "0.6.13-main.548ca8d"
48
+ "@dxos/random": "0.6.14-main.1366248",
49
+ "@dxos/react-ui": "0.6.14-main.1366248",
50
+ "@dxos/react-ui-theme": "0.6.14-main.1366248",
51
+ "@dxos/storybook-utils": "0.6.14-main.1366248"
49
52
  },
50
53
  "peerDependencies": {
51
54
  "@phosphor-icons/react": "^2.1.5",
52
55
  "effect": "^3.9.1",
53
56
  "react": "~18.2.0",
54
- "react-dom": "~18.2.0"
57
+ "react-dom": "~18.2.0",
58
+ "@dxos/react-ui-theme": "0.6.14-main.1366248",
59
+ "@dxos/react-ui": "0.6.14-main.1366248"
55
60
  },
56
61
  "publishConfig": {
57
62
  "access": "public"
@@ -4,19 +4,21 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
+ import { type Meta, type StoryObj } from '@storybook/react';
7
8
  import React from 'react';
8
9
 
9
10
  import { create, S } from '@dxos/echo-schema';
10
11
  import { ghostHover, mx } from '@dxos/react-ui-theme';
11
12
  import { withTheme, withLayout } from '@dxos/storybook-utils';
13
+ import { arrayMove } from '@dxos/util';
12
14
 
13
15
  import { List, type ListRootProps } from './List';
14
- import { createList, TestItemSchema, type TestItemType } from '../../testing';
16
+ import { createList, TestItemSchema, type TestItemType } from './testing';
15
17
 
16
18
  // TODO(burdon): var-icon-size.
17
19
  const grid = 'grid grid-cols-[32px_1fr_32px] min-bs-[2rem] rounded';
18
20
 
19
- const Story = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
21
+ const DefaultStory = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
20
22
  const handleSelect = (item: TestItemType) => {
21
23
  console.log('select', item);
22
24
  };
@@ -24,9 +26,12 @@ const Story = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
24
26
  const idx = items.findIndex((i) => i.id === item.id);
25
27
  items.splice(idx, 1);
26
28
  };
29
+ const handleMove = (from: number, to: number) => {
30
+ arrayMove(items, from, to);
31
+ };
27
32
 
28
33
  return (
29
- <List.Root<TestItemType> dragPreview items={items} {...props}>
34
+ <List.Root<TestItemType> dragPreview items={items} getId={(item) => item.id} onMove={handleMove} {...props}>
30
35
  {({ items }) => (
31
36
  <>
32
37
  <div className='flex flex-col w-full'>
@@ -36,7 +41,7 @@ const Story = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
36
41
  </div>
37
42
 
38
43
  <div role='list' className='w-full h-full overflow-auto'>
39
- {items.map((item) => (
44
+ {items?.map((item) => (
40
45
  <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid, ghostHover)}>
41
46
  <List.ItemDragHandle />
42
47
  <List.ItemTitle onClick={() => handleSelect(item)}>{item.name}</List.ItemTitle>
@@ -47,7 +52,7 @@ const Story = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
47
52
 
48
53
  <div role='none' className={grid}>
49
54
  <div />
50
- <div className='flex items-center text-sm'>{items.length} Items</div>
55
+ <div className='flex items-center text-sm'>{items?.length} Items</div>
51
56
  </div>
52
57
  </div>
53
58
 
@@ -70,7 +75,7 @@ const SimpleStory = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
70
75
  <List.Root<TestItemType> dragPreview items={items} {...props}>
71
76
  {({ items }) => (
72
77
  <div role='list' className='w-full h-full overflow-auto'>
73
- {items.map((item) => (
78
+ {items?.map((item) => (
74
79
  <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid, ghostHover)}>
75
80
  <List.ItemDragHandle />
76
81
  <List.ItemTitle>{item.name}</List.ItemTitle>
@@ -83,26 +88,27 @@ const SimpleStory = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
83
88
  );
84
89
  };
85
90
 
86
- export default {
87
- // TODO(burdon): Storybook collides with react-ui/List.
88
- title: 'react-ui-list/List',
89
- decorators: [withTheme, withLayout({ fullscreen: true })],
90
- render: Story,
91
- };
92
-
93
91
  const list = create(createList(100));
94
92
 
95
- export const Default = {
93
+ export const Default: StoryObj<ListRootProps<TestItemType>> = {
94
+ render: DefaultStory,
96
95
  args: {
97
96
  items: list.items,
98
97
  isItem: S.is(TestItemSchema),
99
- } satisfies ListRootProps<TestItemType>,
100
- } as any; // TODO(burdon): TS2742: The inferred type of Default cannot be named without a reference to... (AST)
98
+ },
99
+ };
101
100
 
102
- export const Simple = {
101
+ export const Simple: StoryObj<ListRootProps<TestItemType>> = {
103
102
  render: SimpleStory,
104
103
  args: {
105
104
  items: list.items,
106
105
  isItem: S.is(TestItemSchema),
107
- } satisfies ListRootProps<TestItemType>,
108
- } as any; // TODO(burdon): TS2742: The inferred type of Default cannot be named without a reference to... (AST)
106
+ },
107
+ };
108
+
109
+ const meta: Meta = {
110
+ title: 'ui/react-ui-list/List',
111
+ decorators: [withTheme, withLayout({ fullscreen: true })],
112
+ };
113
+
114
+ export default meta;
@@ -5,8 +5,11 @@
5
5
  import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
6
6
  import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
7
7
  import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
8
- import { type Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
9
- import { attachClosestEdge, extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
8
+ import {
9
+ type Edge,
10
+ attachClosestEdge,
11
+ extractClosestEdge,
12
+ } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
10
13
  import { createContext } from '@radix-ui/react-context';
11
14
  import React, {
12
15
  type ComponentProps,
@@ -22,14 +25,13 @@ import React, {
22
25
  import { createPortal } from 'react-dom';
23
26
 
24
27
  import { invariant } from '@dxos/invariant';
25
- import { type ThemedClassName } from '@dxos/react-ui';
26
- import { Icon } from '@dxos/react-ui';
28
+ import { Icon, type ThemedClassName } from '@dxos/react-ui';
27
29
  import { mx } from '@dxos/react-ui-theme';
28
30
 
29
31
  import { DropIndicator } from './DropIndicator';
30
32
  import { useListContext } from './ListRoot';
31
33
 
32
- export type ListItemRecord = { id: string };
34
+ export type ListItemRecord = {};
33
35
 
34
36
  export type ItemState =
35
37
  | {
@@ -163,7 +165,7 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item
163
165
  return (
164
166
  <ListItemProvider item={item} dragHandleRef={dragHandleRef}>
165
167
  <div className='relative'>
166
- <div ref={ref} role='listitem' className={mx('flex', classNames, stateStyles[state.type])}>
168
+ <div ref={ref} role='listitem' className={mx('flex overflow-hidden', classNames, stateStyles[state.type])}>
167
169
  {children}
168
170
  </div>
169
171
  {state.type === 'is-dragging-over' && state.closestEdge && <DropIndicator edge={state.closestEdge} />}
@@ -207,7 +209,7 @@ export const ListItemDeleteButton = ({
207
209
 
208
210
  export const ListItemDragHandle = () => {
209
211
  const { dragHandleRef } = useListItemContext('DRAG_HANDLE');
210
- return <IconButton ref={dragHandleRef as any} icon='ph--dots-six--regular' />;
212
+ return <IconButton ref={dragHandleRef as any} icon='ph--dots-six-vertical--regular' />;
211
213
  };
212
214
 
213
215
  export const ListItemDragPreview = <T extends ListItemRecord>({
@@ -220,7 +222,7 @@ export const ListItemDragPreview = <T extends ListItemRecord>({
220
222
  };
221
223
 
222
224
  export const ListItemWrapper = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => (
223
- <div className={mx('flex w-full', classNames)}>{children}</div>
225
+ <div className={mx('flex is-full gap-2', classNames)}>{children}</div>
224
226
  );
225
227
 
226
228
  export const ListItemTitle = ({
@@ -228,7 +230,7 @@ export const ListItemTitle = ({
228
230
  children,
229
231
  ...props
230
232
  }: ThemedClassName<PropsWithChildren<ComponentProps<'div'>>>) => (
231
- <div className={mx('flex w-full items-center', classNames)} {...props}>
233
+ <div className={mx('flex grow items-center truncate', classNames)} {...props}>
232
234
  {children}
233
235
  </div>
234
236
  );
@@ -4,17 +4,17 @@
4
4
 
5
5
  import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
6
6
  import { extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
7
- import { reorderWithEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/util/reorder-with-edge';
7
+ import { getReorderDestinationIndex } from '@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index';
8
8
  import { createContext } from '@radix-ui/react-context';
9
- import React, { type ReactNode, useEffect, useState } from 'react';
10
- import { flushSync } from 'react-dom';
9
+ import React, { type ReactNode, useCallback, useEffect, useState } from 'react';
11
10
 
12
- import { type ThemedClassName, useControlledValue } from '@dxos/react-ui';
11
+ import { type ThemedClassName } from '@dxos/react-ui';
13
12
 
14
- import { type ListItemRecord, idle, type ItemState } from './ListItem';
13
+ import { idle, type ItemState, type ListItemRecord } from './ListItem';
15
14
 
16
15
  type ListContext<T extends ListItemRecord> = {
17
16
  isItem: (item: any) => boolean;
17
+ getId?: (item: T) => string; // TODO(burdon): Require if T doesn't conform to type.
18
18
  dragPreview?: boolean;
19
19
  state: ItemState & { item?: T };
20
20
  setState: (state: ItemState & { item?: T }) => void;
@@ -32,19 +32,44 @@ export type ListRendererProps<T extends ListItemRecord> = {
32
32
  export type ListRootProps<T extends ListItemRecord> = ThemedClassName<{
33
33
  children?: (props: ListRendererProps<T>) => ReactNode;
34
34
  items?: T[];
35
+ onMove?: (fromIndex: number, toIndex: number) => void;
35
36
  }> &
36
- Pick<ListContext<T>, 'isItem' | 'dragPreview'>;
37
+ Pick<ListContext<T>, 'isItem' | 'getId' | 'dragPreview'>;
38
+
39
+ const defaultGetId = <T extends ListItemRecord>(item: T) => (item as any)?.id;
37
40
 
38
41
  export const ListRoot = <T extends ListItemRecord>({
39
42
  classNames,
40
43
  children,
41
- items: _items = [],
44
+ items,
42
45
  isItem,
46
+ getId = defaultGetId,
47
+ onMove,
43
48
  ...props
44
49
  }: ListRootProps<T>) => {
45
- const [items, setItems] = useControlledValue<T[]>(_items);
50
+ const isEqual = useCallback(
51
+ (a: T, b: T) => {
52
+ const idA = getId?.(a);
53
+ const idB = getId?.(b);
54
+
55
+ if (idA !== undefined && idB !== undefined) {
56
+ return idA === idB;
57
+ } else {
58
+ // Fallback for primitive values or when getId fails.
59
+ // NOTE(ZaymonFC): After drag and drop, pragmatic internally serializes drop targets which breaks reference equality.
60
+ // You must provide an `getId` function that returns a stable identifier for your items.
61
+ return a === b;
62
+ }
63
+ },
64
+ [getId],
65
+ );
66
+
46
67
  const [state, setState] = useState<ListContext<T>['state']>(idle);
47
68
  useEffect(() => {
69
+ if (!items) {
70
+ return;
71
+ }
72
+
48
73
  return monitorForElements({
49
74
  canMonitor: ({ source }) => isItem(source.data),
50
75
  onDrop: ({ location, source }) => {
@@ -55,31 +80,30 @@ export const ListRoot = <T extends ListItemRecord>({
55
80
 
56
81
  const sourceData = source.data;
57
82
  const targetData = target.data;
83
+
58
84
  if (!isItem(sourceData) || !isItem(targetData)) {
59
85
  return;
60
86
  }
61
87
 
62
- const sourceIdx = items.findIndex((item) => item.id === sourceData.id);
63
- const targetIdx = items.findIndex((item) => item.id === targetData.id);
88
+ const sourceIdx = items.findIndex((item) => isEqual(item, sourceData as T));
89
+ const targetIdx = items.findIndex((item) => isEqual(item, targetData as T));
64
90
  if (targetIdx < 0 || sourceIdx < 0) {
65
91
  return;
66
92
  }
67
-
68
93
  const closestEdgeOfTarget = extractClosestEdge(targetData);
69
- flushSync(() => {
70
- setItems(
71
- reorderWithEdge({
72
- list: items,
73
- startIndex: sourceIdx,
74
- indexOfTarget: targetIdx,
75
- axis: 'vertical',
76
- closestEdgeOfTarget,
77
- }),
78
- );
94
+ const destinationIndex = getReorderDestinationIndex({
95
+ closestEdgeOfTarget,
96
+ startIndex: sourceIdx,
97
+ indexOfTarget: targetIdx,
98
+ axis: 'vertical',
79
99
  });
100
+
101
+ onMove?.(sourceIdx, destinationIndex);
80
102
  },
81
103
  });
82
- }, [items]);
104
+ }, [items, isEqual, onMove]);
83
105
 
84
- return <ListProvider {...{ isItem, state, setState, ...props }}>{children?.({ state, items })}</ListProvider>;
106
+ return (
107
+ <ListProvider {...{ isItem, state, setState, ...props }}>{children?.({ state, items: items ?? [] })}</ListProvider>
108
+ );
85
109
  };
@@ -0,0 +1,79 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { type Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
6
+ import React, { type HTMLAttributes, type CSSProperties } from 'react';
7
+
8
+ import { mx } from '@dxos/react-ui-theme';
9
+
10
+ // Tree item hitbox
11
+ // https://github.com/atlassian/pragmatic-drag-and-drop/blob/main/packages/hitbox/constellation/index/about.mdx#tree-item
12
+
13
+ export type DropIndicatorProps = {
14
+ instruction: Instruction;
15
+ };
16
+
17
+ type InstructionType = Exclude<Instruction, { type: 'instruction-blocked' }>['type'];
18
+ type Orientation = 'sibling' | 'child';
19
+
20
+ const edgeToOrientationMap: Record<InstructionType, Orientation> = {
21
+ 'reorder-above': 'sibling',
22
+ 'reorder-below': 'sibling',
23
+ 'make-child': 'child',
24
+ reparent: 'child',
25
+ };
26
+
27
+ const orientationStyles: Record<Orientation, HTMLAttributes<HTMLElement>['className']> = {
28
+ // TODO(wittjosiah): Stop using left/right here.
29
+ sibling:
30
+ 'bs-[--line-thickness] left-[--horizontal-indent] right-0 bg-accentSurface before:left-[--negative-terminal-size]',
31
+ child: 'is-full block-start-0 block-end-0 border-[length:--line-thickness] before:invisible',
32
+ };
33
+
34
+ const instructionStyles: Record<InstructionType, HTMLAttributes<HTMLElement>['className']> = {
35
+ 'reorder-above': 'block-start-[--line-offset] before:block-start-[--offset-terminal]',
36
+ 'reorder-below': 'block-end-[--line-offset] before:block-end-[--offset-terminal]',
37
+ 'make-child': 'border-accentSurface',
38
+ // TODO(wittjosiah): This is not occurring in the current implementation.
39
+ reparent: '',
40
+ };
41
+
42
+ const strokeSize = 2;
43
+ const terminalSize = 8;
44
+ const offsetToAlignTerminalWithLine = (strokeSize - terminalSize) / 2;
45
+ const gap = '0px';
46
+
47
+ export const DropIndicator = ({ instruction }: DropIndicatorProps) => {
48
+ const lineOffset = `calc(-0.5 * (${gap} + ${strokeSize}px))`;
49
+ const isBlocked = instruction.type === 'instruction-blocked';
50
+ const desiredInstruction = isBlocked ? instruction.desired : instruction;
51
+ const orientation = edgeToOrientationMap[desiredInstruction.type];
52
+
53
+ if (isBlocked) {
54
+ return null;
55
+ }
56
+
57
+ return (
58
+ <div
59
+ style={
60
+ {
61
+ '--line-thickness': `${strokeSize}px`,
62
+ '--line-offset': `${lineOffset}`,
63
+ '--terminal-size': `${terminalSize}px`,
64
+ '--terminal-radius': `${terminalSize / 2}px`,
65
+ '--negative-terminal-size': `-${terminalSize}px`,
66
+ '--offset-terminal': `${offsetToAlignTerminalWithLine}px`,
67
+ '--horizontal-indent': `${desiredInstruction.currentLevel * desiredInstruction.indentPerLevel + 4}px`,
68
+ } as CSSProperties
69
+ }
70
+ className={mx(
71
+ 'absolute z-10 pointer-events-none',
72
+ 'before:is-[--terminal-size] before:bs-[--terminal-size] box-border before:absolute',
73
+ 'before:border-[length:--line-thickness] before:border-solid before:border-accentSurface before:rounded-full',
74
+ orientationStyles[orientation],
75
+ instructionStyles[desiredInstruction.type],
76
+ )}
77
+ ></div>
78
+ );
79
+ };
@@ -0,0 +1,116 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
8
+ import { extractInstruction, type Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
9
+ import { type StoryObj, type Meta } from '@storybook/react';
10
+ import React, { useEffect } from 'react';
11
+
12
+ import { create } from '@dxos/echo-schema';
13
+ import { faker } from '@dxos/random';
14
+ import { Icon } from '@dxos/react-ui';
15
+ import { Path } from '@dxos/react-ui-mosaic';
16
+ import { withLayout, withTheme } from '@dxos/storybook-utils';
17
+
18
+ import { Tree, type TreeProps } from './Tree';
19
+ import { createTree, flattenTree, getItem, updateState, type TestItem } from './testing';
20
+ import { isItem, type ItemType } from './types';
21
+
22
+ faker.seed(1234);
23
+
24
+ type State = {
25
+ tree: TestItem;
26
+ open: string[];
27
+ current: string[];
28
+ flatTree: ItemType[];
29
+ };
30
+
31
+ const state = create<State>({
32
+ tree: createTree(),
33
+ open: [],
34
+ current: [],
35
+ get flatTree() {
36
+ return flattenTree(this.tree, this.open, getItem);
37
+ },
38
+ });
39
+
40
+ const Story = (args: Partial<TreeProps>) => {
41
+ // NOTE: If passed directly to args, this won't be reactive.
42
+ const items = state.flatTree;
43
+
44
+ useEffect(() => {
45
+ return monitorForElements({
46
+ canMonitor: ({ source }) => isItem(source.data),
47
+ onDrop: ({ location, source }) => {
48
+ // Didn't drop on anything.
49
+ if (!location.current.dropTargets.length) {
50
+ return;
51
+ }
52
+
53
+ const target = location.current.dropTargets[0];
54
+
55
+ const instruction: Instruction | null = extractInstruction(target.data);
56
+ if (instruction !== null) {
57
+ updateState({
58
+ state: state.tree,
59
+ instruction,
60
+ source: source.data as ItemType,
61
+ target: target.data as ItemType,
62
+ });
63
+ }
64
+ },
65
+ });
66
+ }, []);
67
+
68
+ return <Tree items={items} open={state.open} current={state.current} {...args} />;
69
+ };
70
+
71
+ const meta: Meta<typeof Tree> = {
72
+ title: 'ui/react-ui-list/Tree',
73
+ component: Tree,
74
+ render: Story,
75
+ decorators: [withTheme, withLayout({ tooltips: true })],
76
+ args: {
77
+ renderColumns: () => {
78
+ return (
79
+ <div className='flex items-center'>
80
+ <Icon icon='ph--placeholder--regular' size={5} />
81
+ </div>
82
+ );
83
+ },
84
+ onOpenChange: (item: ItemType, open: boolean) => {
85
+ const path = Path.create(...item.path);
86
+ if (open) {
87
+ state.open.push(path);
88
+ } else {
89
+ const index = state.open.indexOf(path);
90
+ if (index > -1) {
91
+ state.open.splice(index, 1);
92
+ }
93
+ }
94
+ },
95
+ onSelect: (item: ItemType, current: boolean) => {
96
+ if (current) {
97
+ state.current.push(item.id);
98
+ } else {
99
+ const index = state.current.indexOf(item.id);
100
+ if (index > -1) {
101
+ state.current.splice(index, 1);
102
+ }
103
+ }
104
+ },
105
+ },
106
+ };
107
+
108
+ export default meta;
109
+
110
+ export const Default = {};
111
+
112
+ export const Draggable: StoryObj<typeof Tree> = {
113
+ args: {
114
+ draggable: true,
115
+ },
116
+ };
@@ -0,0 +1,56 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { Treegrid, type TreegridRootProps } from '@dxos/react-ui';
8
+ import { Path } from '@dxos/react-ui-mosaic';
9
+
10
+ import { TreeItem, type TreeItemProps } from './TreeItem';
11
+ import { getMode } from './helpers';
12
+ import { type ItemType } from './types';
13
+
14
+ export type TreeProps<T extends ItemType = ItemType> = {
15
+ items: T[];
16
+ open: string[];
17
+ current: string[];
18
+ } & Partial<Pick<TreegridRootProps, 'gridTemplateColumns' | 'classNames'>> &
19
+ Pick<TreeItemProps<T>, 'draggable' | 'renderColumns' | 'canDrop' | 'onOpenChange' | 'onSelect'>;
20
+
21
+ export const Tree = <T extends ItemType = ItemType>({
22
+ items,
23
+ open,
24
+ current,
25
+ draggable = false,
26
+ gridTemplateColumns = '[tree-row-start] 1fr min-content [tree-row-end]',
27
+ classNames,
28
+ renderColumns,
29
+ canDrop,
30
+ onOpenChange,
31
+ onSelect,
32
+ }: TreeProps<T>) => {
33
+ return (
34
+ <Treegrid.Root gridTemplateColumns={gridTemplateColumns} classNames={classNames}>
35
+ {items.map((item, i) => {
36
+ const path = Path.create(...item.path);
37
+
38
+ return (
39
+ <TreeItem<T>
40
+ key={item.id}
41
+ item={item}
42
+ mode={getMode(items, i)}
43
+ open={open.includes(path)}
44
+ // TODO(wittjosiah): This should also be path-based.
45
+ current={current.includes(item.id)}
46
+ draggable={draggable}
47
+ renderColumns={renderColumns}
48
+ canDrop={canDrop}
49
+ onOpenChange={onOpenChange}
50
+ onSelect={onSelect}
51
+ />
52
+ );
53
+ })}
54
+ </Treegrid.Root>
55
+ );
56
+ };