@dxos/react-ui-list 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6

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 (35) hide show
  1. package/dist/lib/browser/index.mjs +230 -191
  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 +230 -191
  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/List/List.d.ts +6 -4
  8. package/dist/types/src/components/List/List.d.ts.map +1 -1
  9. package/dist/types/src/components/List/ListItem.d.ts +8 -6
  10. package/dist/types/src/components/List/ListItem.d.ts.map +1 -1
  11. package/dist/types/src/components/Tree/Tree.d.ts +6 -5
  12. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  13. package/dist/types/src/components/Tree/Tree.stories.d.ts +1 -1
  14. package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/Tree/TreeContext.d.ts +21 -10
  16. package/dist/types/src/components/Tree/TreeContext.d.ts.map +1 -1
  17. package/dist/types/src/components/Tree/TreeItem.d.ts +8 -0
  18. package/dist/types/src/components/Tree/TreeItem.d.ts.map +1 -1
  19. package/dist/types/src/components/Tree/TreeItemHeading.d.ts.map +1 -1
  20. package/dist/types/src/components/Tree/index.d.ts +2 -0
  21. package/dist/types/src/components/Tree/index.d.ts.map +1 -1
  22. package/dist/types/tsconfig.tsbuildinfo +1 -1
  23. package/package.json +19 -18
  24. package/src/components/Accordion/Accordion.stories.tsx +3 -3
  25. package/src/components/Accordion/AccordionItem.tsx +1 -1
  26. package/src/components/List/List.stories.tsx +10 -10
  27. package/src/components/List/List.tsx +4 -9
  28. package/src/components/List/ListItem.tsx +55 -35
  29. package/src/components/Tree/Tree.stories.tsx +103 -27
  30. package/src/components/Tree/Tree.tsx +30 -40
  31. package/src/components/Tree/TreeContext.tsx +18 -9
  32. package/src/components/Tree/TreeItem.tsx +176 -101
  33. package/src/components/Tree/TreeItemHeading.tsx +3 -4
  34. package/src/components/Tree/TreeItemToggle.tsx +4 -4
  35. package/src/components/Tree/index.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-list",
3
- "version": "0.8.4-main.bc674ce",
3
+ "version": "0.8.4-main.bcb3aa67d6",
4
4
  "description": "A list component.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -30,35 +30,36 @@
30
30
  "dependencies": {
31
31
  "@atlaskit/pragmatic-drag-and-drop": "1.7.7",
32
32
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "1.1.0",
33
- "@effect-atom/atom-react": "^0.4.6",
33
+ "@effect-atom/atom-react": "^0.5.0",
34
34
  "@radix-ui/react-accordion": "1.2.3",
35
35
  "@radix-ui/react-context": "1.1.1",
36
- "@dxos/debug": "0.8.4-main.bc674ce",
37
- "@dxos/invariant": "0.8.4-main.bc674ce",
38
- "@dxos/log": "0.8.4-main.bc674ce",
39
- "@dxos/react-ui": "0.8.4-main.bc674ce",
40
- "@dxos/echo": "0.8.4-main.bc674ce",
41
- "@dxos/react-ui-text-tooltip": "0.8.4-main.bc674ce",
42
- "@dxos/ui-theme": "0.8.4-main.bc674ce",
43
- "@dxos/ui-types": "0.8.4-main.bc674ce",
44
- "@dxos/util": "0.8.4-main.bc674ce"
36
+ "@radix-ui/react-slot": "1.1.2",
37
+ "@dxos/debug": "0.8.4-main.bcb3aa67d6",
38
+ "@dxos/invariant": "0.8.4-main.bcb3aa67d6",
39
+ "@dxos/echo": "0.8.4-main.bcb3aa67d6",
40
+ "@dxos/log": "0.8.4-main.bcb3aa67d6",
41
+ "@dxos/react-ui-text-tooltip": "0.8.4-main.bcb3aa67d6",
42
+ "@dxos/ui-theme": "0.8.4-main.bcb3aa67d6",
43
+ "@dxos/ui-types": "0.8.4-main.bcb3aa67d6",
44
+ "@dxos/util": "0.8.4-main.bcb3aa67d6",
45
+ "@dxos/react-ui": "0.8.4-main.bcb3aa67d6"
45
46
  },
46
47
  "devDependencies": {
47
48
  "@types/react": "~19.2.7",
48
49
  "@types/react-dom": "~19.2.3",
49
- "effect": "3.19.11",
50
+ "effect": "3.20.0",
50
51
  "react": "~19.2.3",
51
52
  "react-dom": "~19.2.3",
52
- "vite": "7.1.9",
53
- "@dxos/random": "0.8.4-main.bc674ce",
54
- "@dxos/storybook-utils": "0.8.4-main.bc674ce"
53
+ "vite": "^7.1.11",
54
+ "@dxos/random": "0.8.4-main.bcb3aa67d6",
55
+ "@dxos/storybook-utils": "0.8.4-main.bcb3aa67d6"
55
56
  },
56
57
  "peerDependencies": {
57
- "effect": "3.19.11",
58
+ "effect": "3.20.0",
58
59
  "react": "~19.2.3",
59
60
  "react-dom": "~19.2.3",
60
- "@dxos/react-ui": "0.8.4-main.bc674ce",
61
- "@dxos/ui-theme": "0.8.4-main.bc674ce"
61
+ "@dxos/react-ui": "0.8.4-main.bcb3aa67d6",
62
+ "@dxos/ui-theme": "0.8.4-main.bcb3aa67d6"
62
63
  },
63
64
  "publishConfig": {
64
65
  "access": "public"
@@ -22,9 +22,9 @@ const items: TestItem[] = Array.from({ length: 10 }, (_, i) => ({
22
22
 
23
23
  const DefaultStory = () => {
24
24
  return (
25
- <Accordion.Root<TestItem> items={items} classNames='is-[40rem]'>
25
+ <Accordion.Root<TestItem> items={items} classNames='w-[40rem]'>
26
26
  {({ items }) => (
27
- <div className='flex flex-col is-full border-y border-separator divide-y divide-separator'>
27
+ <div className='flex flex-col w-full border-y border-separator divide-y divide-separator'>
28
28
  {items.map((item) => (
29
29
  <Accordion.Item key={item.id} item={item} classNames='border-x border-separator'>
30
30
  <Accordion.ItemHeader>{item.name}</Accordion.ItemHeader>
@@ -42,7 +42,7 @@ const DefaultStory = () => {
42
42
  const meta = {
43
43
  title: 'ui/react-ui-list/Accordion',
44
44
  render: DefaultStory,
45
- decorators: [withTheme, withLayout({ layout: 'column' })],
45
+ decorators: [withTheme(), withLayout({ layout: 'column' })],
46
46
  } satisfies Meta<typeof Accordion>;
47
47
 
48
48
  export default meta;
@@ -43,7 +43,7 @@ export type AccordionItemHeaderProps = ThemedClassName<AccordionPrimitive.Accord
43
43
  export const AccordionItemHeader = ({ classNames, children, ...props }: AccordionItemHeaderProps) => {
44
44
  return (
45
45
  <AccordionPrimitive.Header {...props} className={mx(classNames)}>
46
- <AccordionPrimitive.Trigger className='group flex items-center p-2 dx-focus-ring-inset is-full text-start'>
46
+ <AccordionPrimitive.Trigger className='group flex items-center p-2 dx-focus-ring-inset w-full text-start'>
47
47
  {children}
48
48
  <Icon
49
49
  icon='ph--caret-right--regular'
@@ -7,16 +7,16 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
7
7
  import * as Schema from 'effect/Schema';
8
8
  import React, { useContext, useMemo } from 'react';
9
9
 
10
- import { withTheme } from '@dxos/react-ui/testing';
10
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
11
  import { withRegistry } from '@dxos/storybook-utils';
12
- import { ghostHover, mx } from '@dxos/ui-theme';
12
+ import { mx } from '@dxos/ui-theme';
13
13
  import { arrayMove } from '@dxos/util';
14
14
 
15
15
  import { List, type ListRootProps } from './List';
16
16
  import { TestItemSchema, type TestItemType, type TestList, createList } from './testing';
17
17
 
18
18
  // TODO(burdon): var-icon-size.
19
- 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';
20
20
 
21
21
  const DefaultStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
22
22
  const registry = useContext(RegistryContext);
@@ -45,15 +45,15 @@ const DefaultStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
45
45
  <List.Root<TestItemType> dragPreview items={items} getId={(item) => item.id} onMove={handleMove} {...props}>
46
46
  {({ items }) => (
47
47
  <>
48
- <div className='flex flex-col is-full'>
48
+ <div className='flex flex-col w-full'>
49
49
  <div role='none' className={grid}>
50
50
  <div />
51
51
  <div className='flex items-center text-sm'>Items</div>
52
52
  </div>
53
53
 
54
- <div role='list' className='is-full bs-full overflow-auto'>
54
+ <div role='list' className='h-full w-full overflow-auto'>
55
55
  {items?.map((item) => (
56
- <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid, ghostHover)}>
56
+ <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid)}>
57
57
  <List.ItemDragHandle />
58
58
  <List.ItemTitle onClick={() => handleSelect(item)}>{item.name}</List.ItemTitle>
59
59
  <List.ItemDeleteButton onClick={() => handleDelete(item)} />
@@ -69,7 +69,7 @@ const DefaultStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
69
69
 
70
70
  <List.ItemDragPreview<TestItemType>>
71
71
  {({ item }) => (
72
- <List.ItemWrapper classNames={mx(grid, 'bg-modalSurface border border-separator')}>
72
+ <List.ItemWrapper classNames={mx(grid, 'bg-modal-surface border border-separator')}>
73
73
  <List.ItemDragHandle />
74
74
  <div className='flex items-center'>{item.name}</div>
75
75
  </List.ItemWrapper>
@@ -89,9 +89,9 @@ const SimpleStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
89
89
  return (
90
90
  <List.Root<TestItemType> dragPreview items={items} {...props}>
91
91
  {({ items }) => (
92
- <div role='list' className='is-full bs-full overflow-auto'>
92
+ <div role='list' className='h-full w-full overflow-auto'>
93
93
  {items?.map((item) => (
94
- <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid, ghostHover)}>
94
+ <List.Item<TestItemType> key={item.id} item={item} classNames={mx(grid)}>
95
95
  <List.ItemDragHandle />
96
96
  <List.ItemTitle>{item.name}</List.ItemTitle>
97
97
  <List.ItemDeleteButton />
@@ -106,7 +106,7 @@ const SimpleStory = (props: Omit<ListRootProps<TestItemType>, 'items'>) => {
106
106
  const meta = {
107
107
  title: 'ui/react-ui-list/List',
108
108
  component: List.Root,
109
- decorators: [withTheme, withRegistry],
109
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' }), withRegistry],
110
110
  parameters: {
111
111
  layout: 'fullscreen',
112
112
  },
@@ -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
 
@@ -11,12 +11,13 @@ import {
11
11
  extractClosestEdge,
12
12
  } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
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,
@@ -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,12 +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 ref={ref} role='listitem' className={mx('flex relative', classNames, stateStyles[state.type])} {...props}>
184
+ <Comp
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}
190
+ >
174
191
  {children}
175
- {state.type === 'is-dragging-over' && state.closestEdge && (
176
- <NaturalListItem.DropIndicator edge={state.closestEdge} />
177
- )}
178
- </div>
192
+ </Comp>
193
+ {state.type === 'is-dragging-over' && state.closestEdge && (
194
+ <NaturalListItem.DropIndicator edge={state.closestEdge} />
195
+ )}
179
196
  </ListItemProvider>
180
197
  );
181
198
  };
@@ -184,47 +201,48 @@ export const ListItem = <T extends ListItemRecord>({ children, classNames, item,
184
201
  // List item components
185
202
  //
186
203
 
187
- export const ListItemDeleteButton = ({
204
+ export const ListItemIconButton = ({
188
205
  autoHide = true,
206
+ iconOnly = true,
207
+ variant = 'ghost',
189
208
  classNames,
190
209
  disabled,
191
- icon = 'ph--x--regular',
192
- label,
193
210
  ...props
194
- }: Partial<Pick<IconButtonProps, 'icon'>> &
195
- Omit<IconButtonProps, 'icon' | 'label'> & { autoHide?: boolean; label?: string }) => {
196
- const { state } = useListContext('DELETE_BUTTON');
211
+ }: IconButtonProps & { autoHide?: boolean }) => {
212
+ const { state } = useListContext('ITEM_BUTTON');
197
213
  const isDisabled = state.type !== 'idle' || disabled;
198
- const { t } = useTranslation(osTranslations);
199
214
  return (
200
215
  <IconButton
201
- iconOnly
202
- variant='ghost'
203
216
  {...props}
204
- icon={icon}
205
217
  disabled={isDisabled}
206
- label={label ?? t('delete label')}
218
+ iconOnly={iconOnly}
219
+ variant={variant}
207
220
  classNames={[classNames, autoHide && disabled && 'hidden']}
208
221
  />
209
222
  );
210
223
  };
211
224
 
212
- export const ListItemButton = ({
225
+ // TODO(burdon): Generalize to action button.
226
+ export const ListItemDeleteButton = ({
213
227
  autoHide = true,
214
- iconOnly = true,
215
- variant = 'ghost',
216
228
  classNames,
217
229
  disabled,
230
+ icon = 'ph--x--regular',
231
+ label,
218
232
  ...props
219
- }: IconButtonProps & { autoHide?: boolean }) => {
220
- 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');
221
236
  const isDisabled = state.type !== 'idle' || disabled;
237
+ const { t } = useTranslation(osTranslations);
222
238
  return (
223
239
  <IconButton
224
240
  {...props}
241
+ variant='ghost'
225
242
  disabled={isDisabled}
226
- iconOnly={iconOnly}
227
- variant={variant}
243
+ icon={icon}
244
+ iconOnly
245
+ label={label ?? t('delete.label')}
228
246
  classNames={[classNames, autoHide && disabled && 'hidden']}
229
247
  />
230
248
  );
@@ -235,12 +253,12 @@ export const ListItemDragHandle = ({ disabled }: Pick<IconButtonProps, 'disabled
235
253
  const { t } = useTranslation(osTranslations);
236
254
  return (
237
255
  <IconButton
238
- iconOnly
239
256
  variant='ghost'
240
- label={t('drag handle label')}
241
- ref={dragHandleRef as any}
242
- icon='ph--dots-six-vertical--regular'
243
257
  disabled={disabled}
258
+ icon='ph--dots-six-vertical--regular'
259
+ iconOnly
260
+ label={t('drag-handle.label')}
261
+ ref={dragHandleRef}
244
262
  />
245
263
  );
246
264
  };
@@ -255,7 +273,9 @@ export const ListItemDragPreview = <T extends ListItemRecord>({
255
273
  };
256
274
 
257
275
  export const ListItemWrapper = ({ classNames, children }: ThemedClassName<PropsWithChildren>) => (
258
- <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>
259
279
  );
260
280
 
261
281
  export const ListItemTitle = ({
@@ -263,7 +283,7 @@ export const ListItemTitle = ({
263
283
  children,
264
284
  ...props
265
285
  }: ThemedClassName<PropsWithChildren<ComponentProps<'div'>>>) => (
266
- <div className={mx('flex grow items-center truncate', classNames)} {...props}>
286
+ <div role='none' className={mx('flex grow items-center truncate', classNames)} {...props}>
267
287
  {children}
268
288
  </div>
269
289
  );
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
6
6
  import { type Instruction, extractInstruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
7
- import { Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
7
+ import { Atom, RegistryContext } from '@effect-atom/atom-react';
8
8
  import { type Meta, type StoryObj } from '@storybook/react-vite';
9
9
  import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
10
10
 
@@ -17,6 +17,7 @@ import { Path } from '../../util';
17
17
 
18
18
  import { type TestItem, createTree, updateState } from './testing';
19
19
  import { Tree } from './Tree';
20
+ import { type TreeModel } from './TreeContext';
20
21
  import { type TreeData } from './TreeItem';
21
22
 
22
23
  faker.seed(1234);
@@ -27,27 +28,111 @@ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
27
28
  const registry = useContext(RegistryContext);
28
29
  const stateAtomsRef = useRef(new Map<string, Atom.Writable<{ open: boolean; current: boolean }>>());
29
30
 
30
- const getOrCreateStateAtom = useCallback((path: string) => {
31
- let atom = stateAtomsRef.current.get(path);
31
+ const getOrCreateStateAtom = useCallback((pathKey: string) => {
32
+ let atom = stateAtomsRef.current.get(pathKey);
32
33
  if (!atom) {
33
34
  atom = Atom.make({ open: false, current: false }).pipe(Atom.keepAlive);
34
- stateAtomsRef.current.set(path, atom);
35
+ stateAtomsRef.current.set(pathKey, atom);
35
36
  }
36
37
  return atom;
37
38
  }, []);
38
39
 
39
- const useItemState = useCallback(
40
- (_path: string[]) => {
41
- const path = useMemo(() => Path.create(..._path), [_path.join('~')]);
42
- const atom = getOrCreateStateAtom(path);
43
- return useAtomValue(atom);
44
- },
40
+ // Build a lookup map of all items by ID.
41
+ const itemMap = useMemo(() => {
42
+ const map = new Map<string, TestItem>();
43
+ const walk = (item: TestItem) => {
44
+ map.set(item.id, item);
45
+ item.items?.forEach(walk);
46
+ };
47
+ walk(tree);
48
+ return map;
49
+ }, []);
50
+
51
+ // Build a child IDs map keyed by parent ID.
52
+ const childIdsMap = useMemo(() => {
53
+ const map = new Map<string, string[]>();
54
+ const walk = (item: TestItem) => {
55
+ if (item.items) {
56
+ map.set(
57
+ item.id,
58
+ item.items.map((child) => child.id),
59
+ );
60
+ item.items.forEach(walk);
61
+ }
62
+ };
63
+ // Root children.
64
+ map.set(
65
+ tree.id,
66
+ (tree.items ?? []).map((child) => child.id),
67
+ );
68
+ walk(tree);
69
+ return map;
70
+ }, []);
71
+
72
+ const childIdsFamily = useMemo(
73
+ () => Atom.family((id: string) => Atom.make(() => childIdsMap.get(id) ?? []).pipe(Atom.keepAlive)),
74
+ [childIdsMap],
75
+ );
76
+
77
+ const itemFamily = useMemo(
78
+ () => Atom.family((id: string) => Atom.make(() => itemMap.get(id)).pipe(Atom.keepAlive)),
79
+ [itemMap],
80
+ );
81
+
82
+ const itemPropsFamily = useMemo(
83
+ () =>
84
+ Atom.family((pathKey: string) => {
85
+ const id = pathKey.split('~').pop()!;
86
+ return Atom.make(() => {
87
+ const parent = itemMap.get(id);
88
+ if (!parent) {
89
+ return { id, label: id };
90
+ }
91
+ return {
92
+ id: parent.id,
93
+ label: parent.name,
94
+ icon: parent.icon,
95
+ ...((parent.items?.length ?? 0) > 0 && {
96
+ parentOf: parent.items!.map(({ id }) => id),
97
+ }),
98
+ };
99
+ }).pipe(Atom.keepAlive);
100
+ }),
101
+ [itemMap],
102
+ );
103
+
104
+ const itemOpenFamily = useMemo(
105
+ () =>
106
+ Atom.family((pathKey: string) => {
107
+ const stateAtom = getOrCreateStateAtom(pathKey);
108
+ return Atom.make((get) => get(stateAtom).open).pipe(Atom.keepAlive);
109
+ }),
110
+ [getOrCreateStateAtom],
111
+ );
112
+
113
+ const itemCurrentFamily = useMemo(
114
+ () =>
115
+ Atom.family((pathKey: string) => {
116
+ const stateAtom = getOrCreateStateAtom(pathKey);
117
+ return Atom.make((get) => get(stateAtom).current).pipe(Atom.keepAlive);
118
+ }),
45
119
  [getOrCreateStateAtom],
46
120
  );
47
121
 
122
+ const model: TreeModel<TestItem> = useMemo(
123
+ () => ({
124
+ childIds: (parentId?: string) => childIdsFamily(parentId ?? tree.id),
125
+ item: (id: string) => itemFamily(id),
126
+ itemProps: (path: string[]) => itemPropsFamily(path.join('~')),
127
+ itemOpen: (path: string[]) => itemOpenFamily(Path.create(...path)),
128
+ itemCurrent: (path: string[]) => itemCurrentFamily(Path.create(...path)),
129
+ }),
130
+ [childIdsFamily, itemFamily, itemPropsFamily, itemOpenFamily, itemCurrentFamily],
131
+ );
132
+
48
133
  const handleOpenChange = useCallback(
49
- ({ path: _path, open }: { path: string[]; open: boolean }) => {
50
- const path = Path.create(..._path);
134
+ ({ path: pathProp, open }: { path: string[]; open: boolean }) => {
135
+ const path = Path.create(...pathProp);
51
136
  const atom = getOrCreateStateAtom(path);
52
137
  const prev = registry.get(atom);
53
138
  registry.set(atom, { ...prev, open });
@@ -56,8 +141,8 @@ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
56
141
  );
57
142
 
58
143
  const handleSelect = useCallback(
59
- ({ path: _path, current }: { path: string[]; current: boolean }) => {
60
- const path = Path.create(..._path);
144
+ ({ path: pathProp, current }: { path: string[]; current: boolean }) => {
145
+ const path = Path.create(...pathProp);
61
146
  const atom = getOrCreateStateAtom(path);
62
147
  const prev = registry.get(atom);
63
148
  registry.set(atom, { ...prev, current });
@@ -89,22 +174,13 @@ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
89
174
 
90
175
  return (
91
176
  <Tree
177
+ model={model}
92
178
  id={tree.id}
179
+ rootId={tree.id}
93
180
  draggable={draggable}
94
- useItems={(parent?: TestItem) => parent?.items ?? tree.items}
95
- getProps={(parent: TestItem) => ({
96
- id: parent.id,
97
- label: parent.name,
98
- icon: parent.icon,
99
- ...((parent.items?.length ?? 0) > 0 && {
100
- parentOf: parent.items!.map(({ id }) => id),
101
- }),
102
- })}
103
- useIsOpen={(_path: string[]) => useItemState(_path).open}
104
- useIsCurrent={(_path: string[]) => useItemState(_path).current}
105
181
  renderColumns={() => (
106
182
  <div className='flex items-center'>
107
- <Icon icon='ph--placeholder--regular' size={5} />
183
+ <Icon icon='ph--placeholder--regular' />
108
184
  </div>
109
185
  )}
110
186
  onOpenChange={handleOpenChange}
@@ -116,7 +192,7 @@ const DefaultStory = ({ draggable }: { draggable?: boolean }) => {
116
192
  const meta = {
117
193
  title: 'ui/react-ui-list/Tree',
118
194
 
119
- decorators: [withTheme, withRegistry],
195
+ decorators: [withTheme(), withRegistry],
120
196
  component: Tree,
121
197
  render: DefaultStory,
122
198
  } satisfies Meta<typeof Tree<TestItem>>;