@dxos/react-ui-list 0.8.4-main.f5c0578 → 0.8.4-main.fcfe5033a5

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 (54) hide show
  1. package/dist/lib/browser/index.mjs +684 -706
  2. package/dist/lib/browser/index.mjs.map +3 -3
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +684 -706
  5. package/dist/lib/node-esm/index.mjs.map +3 -3
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/Accordion/Accordion.stories.d.ts +7 -4
  8. package/dist/types/src/components/Accordion/Accordion.stories.d.ts.map +1 -1
  9. package/dist/types/src/components/Accordion/AccordionItem.d.ts +1 -1
  10. package/dist/types/src/components/Accordion/AccordionItem.d.ts.map +1 -1
  11. package/dist/types/src/components/List/List.d.ts +8 -6
  12. package/dist/types/src/components/List/List.d.ts.map +1 -1
  13. package/dist/types/src/components/List/List.stories.d.ts +14 -5
  14. package/dist/types/src/components/List/List.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/List/ListItem.d.ts +8 -6
  16. package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
  17. package/dist/types/src/components/List/ListRoot.d.ts +2 -2
  18. package/dist/types/src/components/List/ListRoot.d.ts.map +1 -1
  19. package/dist/types/src/components/List/testing.d.ts +1 -1
  20. package/dist/types/src/components/List/testing.d.ts.map +1 -1
  21. package/dist/types/src/components/Tree/Tree.d.ts +10 -6
  22. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  23. package/dist/types/src/components/Tree/Tree.stories.d.ts +18 -7
  24. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  25. package/dist/types/src/components/Tree/TreeContext.d.ts +24 -10
  26. package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
  27. package/dist/types/src/components/Tree/TreeItem.d.ts +25 -4
  28. package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
  29. package/dist/types/src/components/Tree/TreeItemHeading.d.ts +4 -3
  30. package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
  31. package/dist/types/src/components/Tree/TreeItemToggle.d.ts +3 -3
  32. package/dist/types/src/components/Tree/TreeItemToggle.d.ts.map +1 -1
  33. package/dist/types/src/components/Tree/index.d.ts +2 -0
  34. package/dist/types/src/components/Tree/index.d.ts.map +1 -1
  35. package/dist/types/src/components/Tree/testing.d.ts +3 -3
  36. package/dist/types/src/components/Tree/testing.d.ts.map +1 -1
  37. package/dist/types/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +31 -28
  39. package/src/components/Accordion/Accordion.stories.tsx +7 -9
  40. package/src/components/Accordion/AccordionItem.tsx +6 -5
  41. package/src/components/Accordion/AccordionRoot.tsx +1 -1
  42. package/src/components/List/List.stories.tsx +44 -30
  43. package/src/components/List/List.tsx +4 -9
  44. package/src/components/List/ListItem.tsx +58 -43
  45. package/src/components/List/ListRoot.tsx +3 -3
  46. package/src/components/List/testing.ts +7 -7
  47. package/src/components/Tree/Tree.stories.tsx +179 -88
  48. package/src/components/Tree/Tree.tsx +43 -40
  49. package/src/components/Tree/TreeContext.tsx +21 -9
  50. package/src/components/Tree/TreeItem.tsx +214 -127
  51. package/src/components/Tree/TreeItemHeading.tsx +10 -8
  52. package/src/components/Tree/TreeItemToggle.tsx +29 -18
  53. package/src/components/Tree/index.ts +2 -0
  54. package/src/components/Tree/testing.ts +10 -9
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-list",
3
- "version": "0.8.4-main.f5c0578",
3
+ "version": "0.8.4-main.fcfe5033a5",
4
4
  "description": "A list component.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
7
11
  "license": "MIT",
8
12
  "author": "DXOS.org",
9
13
  "type": "module",
@@ -24,39 +28,38 @@
24
28
  "src"
25
29
  ],
26
30
  "dependencies": {
27
- "@atlaskit/pragmatic-drag-and-drop": "^1.4.0",
28
- "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
29
- "@preact-signals/safe-react": "^0.9.0",
30
- "@preact/signals-core": "^1.9.0",
31
+ "@atlaskit/pragmatic-drag-and-drop": "1.7.7",
32
+ "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
33
+ "@effect-atom/atom-react": "^0.5.0",
31
34
  "@radix-ui/react-accordion": "1.2.3",
32
35
  "@radix-ui/react-context": "1.1.1",
33
- "@dxos/debug": "0.8.4-main.f5c0578",
34
- "@dxos/echo-schema": "0.8.4-main.f5c0578",
35
- "@dxos/invariant": "0.8.4-main.f5c0578",
36
- "@dxos/log": "0.8.4-main.f5c0578",
37
- "@dxos/react-ui": "0.8.4-main.f5c0578",
38
- "@dxos/react-ui-theme": "0.8.4-main.f5c0578",
39
- "@dxos/react-ui-text-tooltip": "0.8.4-main.f5c0578",
40
- "@dxos/live-object": "0.8.4-main.f5c0578",
41
- "@dxos/util": "0.8.4-main.f5c0578",
42
- "@dxos/react-ui-types": "0.8.4-main.f5c0578"
36
+ "@radix-ui/react-slot": "1.1.2",
37
+ "@dxos/debug": "0.8.4-main.fcfe5033a5",
38
+ "@dxos/invariant": "0.8.4-main.fcfe5033a5",
39
+ "@dxos/echo": "0.8.4-main.fcfe5033a5",
40
+ "@dxos/log": "0.8.4-main.fcfe5033a5",
41
+ "@dxos/ui-theme": "0.8.4-main.fcfe5033a5",
42
+ "@dxos/react-ui-text-tooltip": "0.8.4-main.fcfe5033a5",
43
+ "@dxos/util": "0.8.4-main.fcfe5033a5",
44
+ "@dxos/react-ui": "0.8.4-main.fcfe5033a5",
45
+ "@dxos/ui-types": "0.8.4-main.fcfe5033a5"
43
46
  },
44
47
  "devDependencies": {
45
- "@types/react": "~18.2.0",
46
- "@types/react-dom": "~18.2.0",
47
- "effect": "3.17.7",
48
- "react": "~18.2.0",
49
- "react-dom": "~18.2.0",
50
- "vite": "5.4.7",
51
- "@dxos/random": "0.8.4-main.f5c0578",
52
- "@dxos/storybook-utils": "0.8.4-main.f5c0578"
48
+ "@types/react": "~19.2.7",
49
+ "@types/react-dom": "~19.2.3",
50
+ "effect": "3.20.0",
51
+ "react": "~19.2.3",
52
+ "react-dom": "~19.2.3",
53
+ "vite": "^7.1.11",
54
+ "@dxos/random": "0.8.4-main.fcfe5033a5",
55
+ "@dxos/storybook-utils": "0.8.4-main.fcfe5033a5"
53
56
  },
54
57
  "peerDependencies": {
55
- "effect": "^3.13.3",
56
- "react": "~18.2.0",
57
- "react-dom": "~18.2.0",
58
- "@dxos/react-ui": "0.8.4-main.f5c0578",
59
- "@dxos/react-ui-theme": "0.8.4-main.f5c0578"
58
+ "effect": "3.20.0",
59
+ "react": "~19.2.3",
60
+ "react-dom": "~19.2.3",
61
+ "@dxos/react-ui": "0.8.4-main.fcfe5033a5",
62
+ "@dxos/ui-theme": "0.8.4-main.fcfe5033a5"
60
63
  },
61
64
  "publishConfig": {
62
65
  "access": "public"
@@ -2,24 +2,22 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
8
6
  import React from 'react';
9
7
 
10
- import { faker } from '@dxos/random';
11
- import { withLayout, withTheme } from '@dxos/storybook-utils';
8
+ import { random } from '@dxos/random';
9
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
12
10
 
13
11
  import { Accordion } from './Accordion';
14
12
 
15
- faker.seed(1);
13
+ random.seed(1);
16
14
 
17
15
  type TestItem = { id: string; name: string; text: string };
18
16
 
19
17
  const items: TestItem[] = Array.from({ length: 10 }, (_, i) => ({
20
18
  id: i.toString(),
21
19
  name: `Item ${i}`,
22
- text: faker.lorem.paragraphs(3),
20
+ text: random.lorem.paragraphs(3),
23
21
  }));
24
22
 
25
23
  const DefaultStory = () => {
@@ -41,11 +39,11 @@ const DefaultStory = () => {
41
39
  );
42
40
  };
43
41
 
44
- const meta: Meta<typeof Accordion> = {
42
+ const meta = {
45
43
  title: 'ui/react-ui-list/Accordion',
46
44
  render: DefaultStory,
47
- decorators: [withTheme, withLayout({ fullscreen: true, classNames: 'flex justify-center' })],
48
- };
45
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
46
+ } satisfies Meta<typeof Accordion>;
49
47
 
50
48
  export default meta;
51
49
 
@@ -7,10 +7,9 @@ import { createContext } from '@radix-ui/react-context';
7
7
  import React, { type PropsWithChildren } from 'react';
8
8
 
9
9
  import { Icon, type ThemedClassName } from '@dxos/react-ui';
10
- import { mx } from '@dxos/react-ui-theme';
10
+ import { mx } from '@dxos/ui-theme';
11
11
 
12
12
  import { type ListItemRecord } from '../List';
13
-
14
13
  import { useAccordionContext } from './AccordionRoot';
15
14
 
16
15
  const ACCORDION_ITEM_NAME = 'AccordionItem';
@@ -19,7 +18,9 @@ type AccordionItemContext<T extends ListItemRecord> = {
19
18
  item: T;
20
19
  };
21
20
 
22
- export const [AccordionItemProvider, useAccordionItemContext] =
21
+ // TODO(wittjosiah): This seems to be conflicting with something in the bundle.
22
+ // Perhaps @radix-ui/react-accordion?
23
+ export const [AccordionItemProvider, useDxAccordionItemContext] =
23
24
  createContext<AccordionItemContext<any>>(ACCORDION_ITEM_NAME);
24
25
 
25
26
  export type AccordionItemProps<T extends ListItemRecord> = ThemedClassName<PropsWithChildren<{ item: T }>>;
@@ -41,7 +42,7 @@ export type AccordionItemHeaderProps = ThemedClassName<AccordionPrimitive.Accord
41
42
  export const AccordionItemHeader = ({ classNames, children, ...props }: AccordionItemHeaderProps) => {
42
43
  return (
43
44
  <AccordionPrimitive.Header {...props} className={mx(classNames)}>
44
- <AccordionPrimitive.Trigger className='group flex items-center p-2 dx-focus-ring-inset is-full text-start'>
45
+ <AccordionPrimitive.Trigger className='group flex items-center p-2 dx-focus-ring-inset w-full text-start'>
45
46
  {children}
46
47
  <Icon
47
48
  icon='ph--caret-right--regular'
@@ -57,7 +58,7 @@ export type AccordionItemBodyProps = ThemedClassName<PropsWithChildren>;
57
58
 
58
59
  export const AccordionItemBody = ({ children, classNames }: AccordionItemBodyProps) => {
59
60
  return (
60
- <AccordionPrimitive.Content className='overflow-hidden data-[state=closed]:animate-slideUp data-[state=open]:animate-slideDown'>
61
+ <AccordionPrimitive.Content className='overflow-hidden data-[state=closed]:animate-slide-up data-[state=open]:animate-slide-down'>
61
62
  <div role='none' className={mx('p-2', classNames)}>
62
63
  {children}
63
64
  </div>
@@ -7,7 +7,7 @@ import { createContext } from '@radix-ui/react-context';
7
7
  import React, { type ReactNode } from 'react';
8
8
 
9
9
  import { type ThemedClassName } from '@dxos/react-ui';
10
- import { mx } from '@dxos/react-ui-theme';
10
+ import { mx } from '@dxos/ui-theme';
11
11
 
12
12
  import { type ListItemRecord } from '../List';
13
13
 
@@ -2,40 +2,43 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
5
+ import { Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
7
6
  import { type Meta, type StoryObj } from '@storybook/react-vite';
8
- import { Schema } from 'effect';
9
- import React from 'react';
7
+ import * as Schema from 'effect/Schema';
8
+ import React, { useContext, useMemo } from 'react';
10
9
 
11
- import { live } from '@dxos/live-object';
12
- import { ghostHover, mx } from '@dxos/react-ui-theme';
13
- import { withLayout, withTheme } from '@dxos/storybook-utils';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
+ import { withRegistry } from '@dxos/storybook-utils';
12
+ import { mx } from '@dxos/ui-theme';
14
13
  import { arrayMove } from '@dxos/util';
15
14
 
16
15
  import { List, type ListRootProps } from './List';
17
- import { TestItemSchema, type TestItemType, createList } from './testing';
16
+ import { TestItemSchema, type TestItemType, type TestList, createList } from './testing';
18
17
 
19
18
  // TODO(burdon): var-icon-size.
20
- const grid = 'grid grid-cols-[32px_1fr_32px] min-bs-[2rem] rounded';
19
+ const grid = 'grid grid-cols-[32px_1fr_32px] min-h-[2rem] rounded-sm';
21
20
 
22
- const meta: Meta = {
23
- title: 'ui/react-ui-list/List',
24
- decorators: [withTheme, withLayout({ fullscreen: true })],
25
- };
26
-
27
- export default meta;
21
+ const DefaultStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
22
+ const registry = useContext(RegistryContext);
23
+ const listAtom = useMemo(() => Atom.make<TestList>(createList(100)).pipe(Atom.keepAlive), []);
24
+ const list = useAtomValue(listAtom);
25
+ const items = list.items;
28
26
 
29
- const DefaultStory = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
30
27
  const handleSelect = (item: TestItemType) => {
31
28
  console.log('select', item);
32
29
  };
33
30
  const handleDelete = (item: TestItemType) => {
34
- const idx = items.findIndex((i) => i.id === item.id);
35
- items.splice(idx, 1);
31
+ const prev = registry.get(listAtom);
32
+ registry.set(listAtom, {
33
+ ...prev,
34
+ items: prev.items.filter((i) => i.id !== item.id),
35
+ });
36
36
  };
37
37
  const handleMove = (from: number, to: number) => {
38
- arrayMove(items, from, to);
38
+ const prev = registry.get(listAtom);
39
+ const newItems = [...prev.items];
40
+ arrayMove(newItems, from, to);
41
+ registry.set(listAtom, { ...prev, items: newItems });
39
42
  };
40
43
 
41
44
  return (
@@ -48,9 +51,9 @@ const DefaultStory = ({ items = [], ...props }: ListRootProps<TestItemType>) =>
48
51
  <div className='flex items-center text-sm'>Items</div>
49
52
  </div>
50
53
 
51
- <div role='list' className='w-full h-full overflow-auto'>
54
+ <div role='list' className='h-full w-full overflow-auto'>
52
55
  {items?.map((item) => (
53
- <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid, ghostHover)}>
56
+ <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid)}>
54
57
  <List.ItemDragHandle />
55
58
  <List.ItemTitle onClick={() => handleSelect(item)}>{item.name}</List.ItemTitle>
56
59
  <List.ItemDeleteButton onClick={() => handleDelete(item)} />
@@ -66,7 +69,7 @@ const DefaultStory = ({ items = [], ...props }: ListRootProps<TestItemType>) =>
66
69
 
67
70
  <List.ItemDragPreview<TestItemType>>
68
71
  {({ item }) => (
69
- <List.ItemWrapper classNames={mx(grid, 'bg-modalSurface border border-separator')}>
72
+ <List.ItemWrapper classNames={mx(grid, 'bg-modal-surface border border-separator')}>
70
73
  <List.ItemDragHandle />
71
74
  <div className='flex items-center'>{item.name}</div>
72
75
  </List.ItemWrapper>
@@ -78,13 +81,17 @@ const DefaultStory = ({ items = [], ...props }: ListRootProps<TestItemType>) =>
78
81
  );
79
82
  };
80
83
 
81
- const SimpleStory = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
84
+ const SimpleStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
85
+ const listAtom = useMemo(() => Atom.make<TestList>(createList(100)).pipe(Atom.keepAlive), []);
86
+ const list = useAtomValue(listAtom);
87
+ const items = list.items;
88
+
82
89
  return (
83
90
  <List.Root<TestItemType> dragPreview items={items} {...props}>
84
91
  {({ items }) => (
85
- <div role='list' className='w-full h-full overflow-auto'>
92
+ <div role='list' className='h-full w-full overflow-auto'>
86
93
  {items?.map((item) => (
87
- <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid, ghostHover)}>
94
+ <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid)}>
88
95
  <List.ItemDragHandle />
89
96
  <List.ItemTitle>{item.name}</List.ItemTitle>
90
97
  <List.ItemDeleteButton />
@@ -96,20 +103,27 @@ const SimpleStory = ({ items = [], ...props }: ListRootProps<TestItemType>) => {
96
103
  );
97
104
  };
98
105
 
99
- const list = live(createList(100));
106
+ const meta = {
107
+ title: 'ui/react-ui-list/List',
108
+ component: List.Root,
109
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' }), withRegistry],
110
+ parameters: {
111
+ layout: 'fullscreen',
112
+ },
113
+ } satisfies Meta<typeof List.Root>;
114
+
115
+ export default meta;
100
116
 
101
- export const Default: StoryObj<ListRootProps<TestItemType>> = {
117
+ export const Default: StoryObj<typeof DefaultStory> = {
102
118
  render: DefaultStory,
103
119
  args: {
104
- items: list.items,
105
120
  isItem: Schema.is(TestItemSchema),
106
121
  },
107
122
  };
108
123
 
109
- export const Simple: StoryObj<ListRootProps<TestItemType>> = {
124
+ export const Simple: StoryObj<typeof SimpleStory> = {
110
125
  render: SimpleStory,
111
126
  args: {
112
- items: list.items,
113
127
  isItem: Schema.is(TestItemSchema),
114
128
  },
115
129
  };
@@ -4,7 +4,7 @@
4
4
 
5
5
  import {
6
6
  ListItem,
7
- ListItemButton,
7
+ ListItemIconButton,
8
8
  ListItemDeleteButton,
9
9
  ListItemDragHandle,
10
10
  ListItemDragPreview,
@@ -15,17 +15,12 @@ import {
15
15
  } from './ListItem';
16
16
  import { ListRoot, type ListRootProps } from './ListRoot';
17
17
 
18
- // TODO(burdon): Multi-select model.
19
- // TODO(burdon): Key nav.
20
- // TODO(burdon): Animation.
21
- // TODO(burdon): Constrain axis.
22
- // TODO(burdon): Tree view.
23
- // TODO(burdon): Fix autoscroll while dragging.
24
-
25
18
  /**
26
19
  * Draggable list.
27
20
  * Ref: https://github.com/atlassian/pragmatic-drag-and-drop
28
21
  * Ref: https://github.com/alexreardon/pdnd-react-tailwind/blob/main/src/task.tsx
22
+ *
23
+ * @deprecated Use react-ui-mosaic.
29
24
  */
30
25
  export const List = {
31
26
  Root: ListRoot,
@@ -33,8 +28,8 @@ export const List = {
33
28
  ItemDragPreview: ListItemDragPreview,
34
29
  ItemWrapper: ListItemWrapper,
35
30
  ItemDragHandle: ListItemDragHandle,
31
+ ItemIconButton: ListItemIconButton,
36
32
  ItemDeleteButton: ListItemDeleteButton,
37
- ItemButton: ListItemButton,
38
33
  ItemTitle: ListItemTitle,
39
34
  };
40
35
 
@@ -2,21 +2,22 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
6
- import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
7
- import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
8
5
  import {
9
6
  type Edge,
10
7
  attachClosestEdge,
11
8
  extractClosestEdge,
12
9
  } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
10
+ import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
11
+ import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
12
+ import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
13
13
  import { createContext } from '@radix-ui/react-context';
14
+ import { Slot } from '@radix-ui/react-slot';
14
15
  import React, {
15
16
  type ComponentProps,
16
17
  type HTMLAttributes,
17
- type MutableRefObject,
18
18
  type PropsWithChildren,
19
19
  type ReactNode,
20
+ RefObject,
20
21
  useEffect,
21
22
  useRef,
22
23
  useState,
@@ -31,7 +32,7 @@ import {
31
32
  type ThemedClassName,
32
33
  useTranslation,
33
34
  } from '@dxos/react-ui';
34
- import { mx } from '@dxos/react-ui-theme';
35
+ import { mx, osTranslations } from '@dxos/ui-theme';
35
36
 
36
37
  import { useListContext } from './ListRoot';
37
38
 
@@ -61,7 +62,7 @@ const stateStyles: { [Key in ItemDragState['type']]?: HTMLAttributes<HTMLDivElem
61
62
 
62
63
  type ListItemContext<T extends ListItemRecord> = {
63
64
  item: T;
64
- dragHandleRef: MutableRefObject<HTMLElement | null>;
65
+ dragHandleRef: RefObject<HTMLButtonElement | null>;
65
66
  };
66
67
 
67
68
  /**
@@ -80,6 +81,8 @@ export type ListItemProps<T extends ListItemRecord> = ThemedClassName<
80
81
  PropsWithChildren<
81
82
  {
82
83
  item: T;
84
+ asChild?: boolean;
85
+ selected?: boolean;
83
86
  } & HTMLAttributes<HTMLDivElement>
84
87
  >
85
88
  >;
@@ -87,14 +90,22 @@ export type ListItemProps<T extends ListItemRecord> = ThemedClassName<
87
90
  /**
88
91
  * Draggable list item.
89
92
  */
90
- export const ListItem = <T extends ListItemRecord>({ children, classNames, item, ...props }: ListItemProps<T>) => {
93
+ export const ListItem = <T extends ListItemRecord>({
94
+ children,
95
+ classNames,
96
+ item,
97
+ asChild,
98
+ selected,
99
+ ...props
100
+ }: ListItemProps<T>) => {
101
+ const Comp = asChild ? Slot : 'div';
91
102
  const { isItem, readonly, dragPreview, setState: setRootState } = useListContext(LIST_ITEM_NAME);
92
- const ref = useRef<HTMLDivElement | null>(null);
93
- const dragHandleRef = useRef<HTMLElement | null>(null);
103
+ const rootRef = useRef<HTMLDivElement | null>(null);
104
+ const dragHandleRef = useRef<HTMLButtonElement | null>(null);
94
105
  const [state, setState] = useState<ItemDragState>(idle);
95
106
 
96
107
  useEffect(() => {
97
- const element = ref.current;
108
+ const element = rootRef.current;
98
109
  invariant(element);
99
110
  return combine(
100
111
  //
@@ -170,17 +181,18 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item,
170
181
 
171
182
  return (
172
183
  <ListItemProvider item={item} dragHandleRef={dragHandleRef}>
173
- <div
174
- ref={ref}
175
- role='listitem'
176
- className={mx('flex overflow-hidden relative', classNames, stateStyles[state.type])}
184
+ <Comp
177
185
  {...props}
186
+ role='listitem'
187
+ aria-selected={selected}
188
+ className={mx('relative p-1 dx-selected dx-hover', classNames, stateStyles[state.type])}
189
+ ref={rootRef}
178
190
  >
179
191
  {children}
180
- {state.type === 'is-dragging-over' && state.closestEdge && (
181
- <NaturalListItem.DropIndicator edge={state.closestEdge} />
182
- )}
183
- </div>
192
+ </Comp>
193
+ {state.type === 'is-dragging-over' && state.closestEdge && (
194
+ <NaturalListItem.DropIndicator edge={state.closestEdge} />
195
+ )}
184
196
  </ListItemProvider>
185
197
  );
186
198
  };
@@ -189,47 +201,48 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item,
189
201
  // List item components
190
202
  //
191
203
 
192
- export const ListItemDeleteButton = ({
204
+ export const ListItemIconButton = ({
193
205
  autoHide = true,
206
+ iconOnly = true,
207
+ variant = 'ghost',
194
208
  classNames,
195
209
  disabled,
196
- icon = 'ph--x--regular',
197
- label,
198
210
  ...props
199
- }: Partial<Pick<IconButtonProps, 'icon'>> &
200
- Omit<IconButtonProps, 'icon' | 'label'> & { autoHide?: boolean; label?: string }) => {
201
- const { state } = useListContext('DELETE_BUTTON');
211
+ }: IconButtonProps & { autoHide?: boolean }) => {
212
+ const { state } = useListContext('ITEM_BUTTON');
202
213
  const isDisabled = state.type !== 'idle' || disabled;
203
- const { t } = useTranslation('os');
204
214
  return (
205
215
  <IconButton
206
- iconOnly
207
- variant='ghost'
208
216
  {...props}
209
- icon={icon}
210
217
  disabled={isDisabled}
211
- label={label ?? t('delete label')}
218
+ iconOnly={iconOnly}
219
+ variant={variant}
212
220
  classNames={[classNames, autoHide && disabled && 'hidden']}
213
221
  />
214
222
  );
215
223
  };
216
224
 
217
- export const ListItemButton = ({
225
+ // TODO(burdon): Generalize to action button.
226
+ export const ListItemDeleteButton = ({
218
227
  autoHide = true,
219
- iconOnly = true,
220
- variant = 'ghost',
221
228
  classNames,
222
229
  disabled,
230
+ icon = 'ph--x--regular',
231
+ label,
223
232
  ...props
224
- }: IconButtonProps & { autoHide?: boolean }) => {
225
- const { state } = useListContext('ITEM_BUTTON');
233
+ }: Partial<Pick<IconButtonProps, 'icon'>> &
234
+ Omit<IconButtonProps, 'icon' | 'label'> & { autoHide?: boolean; label?: string }) => {
235
+ const { state } = useListContext('DELETE_BUTTON');
226
236
  const isDisabled = state.type !== 'idle' || disabled;
237
+ const { t } = useTranslation(osTranslations);
227
238
  return (
228
239
  <IconButton
229
240
  {...props}
241
+ variant='ghost'
230
242
  disabled={isDisabled}
231
- iconOnly={iconOnly}
232
- variant={variant}
243
+ icon={icon}
244
+ iconOnly
245
+ label={label ?? t('delete.label')}
233
246
  classNames={[classNames, autoHide && disabled && 'hidden']}
234
247
  />
235
248
  );
@@ -237,15 +250,15 @@ export const ListItemButton = ({
237
250
 
238
251
  export const ListItemDragHandle = ({ disabled }: Pick<IconButtonProps, 'disabled'>) => {
239
252
  const { dragHandleRef } = useListItemContext('DRAG_HANDLE');
240
- const { t } = useTranslation('os');
253
+ const { t } = useTranslation(osTranslations);
241
254
  return (
242
255
  <IconButton
243
- iconOnly
244
256
  variant='ghost'
245
- label={t('drag handle label')}
246
- ref={dragHandleRef as any}
247
- icon='ph--dots-six-vertical--regular'
248
257
  disabled={disabled}
258
+ icon='ph--dots-six-vertical--regular'
259
+ iconOnly
260
+ label={t('drag-handle.label')}
261
+ ref={dragHandleRef}
249
262
  />
250
263
  );
251
264
  };
@@ -260,7 +273,9 @@ export const ListItemDragPreview = <T extends ListItemRecord>({
260
273
  };
261
274
 
262
275
  export const ListItemWrapper = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => (
263
- <div className={mx('flex is-full gap-2', classNames)}>{children}</div>
276
+ <div role='none' className={mx('flex w-full gap-2', classNames)}>
277
+ {children}
278
+ </div>
264
279
  );
265
280
 
266
281
  export const ListItemTitle = ({
@@ -268,7 +283,7 @@ export const ListItemTitle = ({
268
283
  children,
269
284
  ...props
270
285
  }: ThemedClassName<PropsWithChildren<ComponentProps<'div'>>>) => (
271
- <div className={mx('flex grow items-center truncate', classNames)} {...props}>
286
+ <div role='none' className={mx('flex grow items-center truncate', classNames)} {...props}>
272
287
  {children}
273
288
  </div>
274
289
  );
@@ -2,9 +2,9 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
6
5
  import { extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
7
6
  import { getReorderDestinationIndex } from '@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index';
7
+ import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
8
8
  import { createContext } from '@radix-ui/react-context';
9
9
  import React, { type ReactNode, useCallback, useEffect, useState } from 'react';
10
10
 
@@ -26,14 +26,14 @@ export const [ListProvider, useListContext] = createContext<ListContext<any>>(LI
26
26
 
27
27
  export type ListRendererProps<T extends ListItemRecord> = {
28
28
  state: ListContext<T>['state'];
29
- items: T[];
29
+ items: readonly T[];
30
30
  };
31
31
 
32
32
  const defaultGetId = <T extends ListItemRecord>(item: T) => (item as any)?.id;
33
33
 
34
34
  export type ListRootProps<T extends ListItemRecord> = {
35
35
  children?: (props: ListRendererProps<T>) => ReactNode;
36
- items?: T[];
36
+ items?: readonly T[];
37
37
  onMove?: (fromIndex: number, toIndex: number) => void;
38
38
  } & Pick<ListContext<T>, 'isItem' | 'getId' | 'readonly' | 'dragPreview'>;
39
39
 
@@ -2,13 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
5
+ import * as Schema from 'effect/Schema';
6
6
 
7
- import { ObjectId } from '@dxos/echo-schema';
8
- import { faker } from '@dxos/random';
7
+ import { Obj } from '@dxos/echo';
8
+ import { random } from '@dxos/random';
9
9
 
10
10
  export const TestItemSchema = Schema.Struct({
11
- id: ObjectId,
11
+ id: Obj.ID,
12
12
  name: Schema.String,
13
13
  });
14
14
 
@@ -21,10 +21,10 @@ export const TestList = Schema.Struct({
21
21
  export type TestList = Schema.Schema.Type<typeof TestList>;
22
22
 
23
23
  export const createList = (n = 10): TestList => ({
24
- items: faker.helpers.multiple(
24
+ items: random.helpers.multiple(
25
25
  () => ({
26
- id: faker.string.uuid(),
27
- name: faker.commerce.productName(),
26
+ id: random.string.uuid(),
27
+ name: random.commerce.productName(),
28
28
  }),
29
29
  { count: n },
30
30
  ),