@dxos/plugin-navtree 0.8.4-main.422d1c7879 → 0.8.4-main.43cb759274

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 (180) hide show
  1. package/LICENSE +102 -5
  2. package/PLUGIN.mdl +376 -0
  3. package/README.md +1 -1
  4. package/dist/lib/neutral/CommandsDialogContent-UBGIQ5FI.mjs +112 -0
  5. package/dist/lib/neutral/CommandsDialogContent-UBGIQ5FI.mjs.map +7 -0
  6. package/dist/lib/neutral/CommandsTrigger-BH5WLMHK.mjs +27 -0
  7. package/dist/lib/neutral/CommandsTrigger-BH5WLMHK.mjs.map +7 -0
  8. package/dist/lib/neutral/NavTreeContainer-6AR4BFDD.mjs +12 -0
  9. package/dist/lib/neutral/NavTreeContainer-6AR4BFDD.mjs.map +7 -0
  10. package/dist/lib/neutral/NavTreeDocumentTitle-H3E6ZLYE.mjs +22 -0
  11. package/dist/lib/neutral/NavTreeDocumentTitle-H3E6ZLYE.mjs.map +7 -0
  12. package/dist/lib/neutral/NavTreePlugin.mjs +68 -0
  13. package/dist/lib/neutral/NavTreePlugin.mjs.map +7 -0
  14. package/dist/lib/neutral/NavTreePlugin.node.mjs +18 -0
  15. package/dist/lib/neutral/NavTreePlugin.node.mjs.map +7 -0
  16. package/dist/lib/neutral/NavTreePlugin.workerd.mjs +16 -0
  17. package/dist/lib/neutral/NavTreePlugin.workerd.mjs.map +7 -0
  18. package/dist/lib/neutral/app-graph-builder-Z347LKSF.mjs +42 -0
  19. package/dist/lib/neutral/app-graph-builder-Z347LKSF.mjs.map +7 -0
  20. package/dist/lib/neutral/capabilities/index.mjs +17 -0
  21. package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
  22. package/dist/lib/neutral/chunk-5OPDKH6B.mjs +273 -0
  23. package/dist/lib/neutral/chunk-5OPDKH6B.mjs.map +7 -0
  24. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  25. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
  26. package/dist/lib/neutral/chunk-JOSNCOV4.mjs +79 -0
  27. package/dist/lib/neutral/chunk-JOSNCOV4.mjs.map +7 -0
  28. package/dist/lib/neutral/chunk-MDGKDQMZ.mjs +37 -0
  29. package/dist/lib/neutral/chunk-MDGKDQMZ.mjs.map +7 -0
  30. package/dist/lib/neutral/chunk-T7QS2EFI.mjs +8 -0
  31. package/dist/lib/neutral/chunk-T7QS2EFI.mjs.map +7 -0
  32. package/dist/lib/neutral/chunk-ZSWYB7JW.mjs +27 -0
  33. package/dist/lib/neutral/chunk-ZSWYB7JW.mjs.map +7 -0
  34. package/dist/lib/neutral/components/index.mjs +807 -0
  35. package/dist/lib/neutral/components/index.mjs.map +7 -0
  36. package/dist/lib/neutral/containers/index.mjs +20 -0
  37. package/dist/lib/neutral/containers/index.mjs.map +7 -0
  38. package/dist/lib/neutral/expose-26FV4W5D.mjs +38 -0
  39. package/dist/lib/neutral/expose-26FV4W5D.mjs.map +7 -0
  40. package/dist/lib/neutral/hooks/index.mjs +178 -0
  41. package/dist/lib/neutral/hooks/index.mjs.map +7 -0
  42. package/dist/lib/neutral/index.mjs +34 -0
  43. package/dist/lib/neutral/index.mjs.map +7 -0
  44. package/dist/lib/neutral/keyboard-EJ65BVTC.mjs +56 -0
  45. package/dist/lib/neutral/keyboard-EJ65BVTC.mjs.map +7 -0
  46. package/dist/lib/neutral/meta.json +1 -0
  47. package/dist/lib/neutral/meta.mjs +12 -0
  48. package/dist/lib/neutral/meta.mjs.map +7 -0
  49. package/dist/lib/neutral/operation-handler-ORDOJUL2.mjs +13 -0
  50. package/dist/lib/neutral/operation-handler-ORDOJUL2.mjs.map +7 -0
  51. package/dist/lib/neutral/operations/index.mjs +8 -0
  52. package/dist/lib/neutral/operations/index.mjs.map +7 -0
  53. package/dist/lib/neutral/plugin.mjs +16 -0
  54. package/dist/lib/neutral/plugin.mjs.map +7 -0
  55. package/dist/lib/neutral/react-surface-Q5XMAG7Z.mjs +50 -0
  56. package/dist/lib/neutral/react-surface-Q5XMAG7Z.mjs.map +7 -0
  57. package/dist/lib/neutral/state-VVITVXWM.mjs +127 -0
  58. package/dist/lib/neutral/state-VVITVXWM.mjs.map +7 -0
  59. package/dist/lib/neutral/testing/index.mjs +439 -0
  60. package/dist/lib/neutral/testing/index.mjs.map +7 -0
  61. package/dist/lib/neutral/translations.mjs +23 -0
  62. package/dist/lib/neutral/translations.mjs.map +7 -0
  63. package/dist/lib/neutral/types/index.mjs +10 -0
  64. package/dist/lib/neutral/types/index.mjs.map +7 -0
  65. package/dist/types/src/NavTreePlugin.d.ts +1 -0
  66. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  67. package/dist/types/src/NavTreePlugin.node.d.ts +4 -0
  68. package/dist/types/src/NavTreePlugin.node.d.ts.map +1 -0
  69. package/dist/types/src/NavTreePlugin.test.d.ts +2 -0
  70. package/dist/types/src/NavTreePlugin.test.d.ts.map +1 -0
  71. package/dist/types/src/NavTreePlugin.workerd.d.ts +4 -0
  72. package/dist/types/src/NavTreePlugin.workerd.d.ts.map +1 -0
  73. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  74. package/dist/types/src/capabilities/index.d.ts +2 -2
  75. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  76. package/dist/types/src/capabilities/keyboard.d.ts.map +1 -1
  77. package/dist/types/src/capabilities/operation-handler.d.ts +1 -1
  78. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -1
  79. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  80. package/dist/types/src/capabilities/state.d.ts +2 -2
  81. package/dist/types/src/capabilities/state.d.ts.map +1 -1
  82. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +8 -9
  83. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -1
  84. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts +2 -2
  85. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -1
  86. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +1 -1
  87. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -1
  88. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +1 -1
  89. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -1
  90. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -1
  91. package/dist/types/src/components/Sidebar/L1Tabs.d.ts.map +1 -1
  92. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.d.ts.map +1 -1
  93. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -1
  94. package/dist/types/src/components/types.d.ts +0 -1
  95. package/dist/types/src/components/types.d.ts.map +1 -1
  96. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts +8 -9
  97. package/dist/types/src/containers/CommandsDialogContent/CommandsDialogContent.stories.d.ts.map +1 -1
  98. package/dist/types/src/containers/NavTreeContainer/NavTreeContainer.d.ts.map +1 -1
  99. package/dist/types/src/containers/NavTreeDocumentTitle/NavTreeDocumentTitle.d.ts.map +1 -1
  100. package/dist/types/src/experimental/Tree.d.ts +2 -2
  101. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  102. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  103. package/dist/types/src/hooks/useActions.d.ts +6 -0
  104. package/dist/types/src/hooks/useActions.d.ts.map +1 -1
  105. package/dist/types/src/hooks/useLoadDescendents.d.ts.map +1 -1
  106. package/dist/types/src/hooks/useNavTreeModel.d.ts.map +1 -1
  107. package/dist/types/src/hooks/useNavTreeState.d.ts +1 -5
  108. package/dist/types/src/hooks/useNavTreeState.d.ts.map +1 -1
  109. package/dist/types/src/index.d.ts +1 -2
  110. package/dist/types/src/index.d.ts.map +1 -1
  111. package/dist/types/src/meta.d.ts +3 -2
  112. package/dist/types/src/meta.d.ts.map +1 -1
  113. package/dist/types/src/operations/expose.d.ts +1 -1
  114. package/dist/types/src/operations/expose.d.ts.map +1 -1
  115. package/dist/types/src/operations/index.d.ts +1 -1
  116. package/dist/types/src/operations/index.d.ts.map +1 -1
  117. package/dist/types/src/plugin.d.ts +4 -0
  118. package/dist/types/src/plugin.d.ts.map +1 -0
  119. package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -1
  120. package/dist/types/src/testing/generator.d.ts.map +1 -1
  121. package/dist/types/src/testing/update-state.d.ts.map +1 -1
  122. package/dist/types/src/translations.d.ts +8 -10
  123. package/dist/types/src/translations.d.ts.map +1 -1
  124. package/dist/types/src/types/NavTreeCapabilities.d.ts +13 -0
  125. package/dist/types/src/types/NavTreeCapabilities.d.ts.map +1 -0
  126. package/dist/types/src/types/NavTreeEvents.d.ts +3 -0
  127. package/dist/types/src/types/NavTreeEvents.d.ts.map +1 -0
  128. package/dist/types/src/types/index.d.ts +2 -2
  129. package/dist/types/src/types/index.d.ts.map +1 -1
  130. package/dist/types/src/types/schema.d.ts +4 -0
  131. package/dist/types/src/types/schema.d.ts.map +1 -1
  132. package/dist/types/src/util.d.ts +1 -1
  133. package/dist/types/src/util.d.ts.map +1 -1
  134. package/dist/types/tsconfig.tsbuildinfo +1 -1
  135. package/package.json +108 -55
  136. package/src/NavTreePlugin.node.ts +17 -0
  137. package/src/NavTreePlugin.test.ts +27 -0
  138. package/src/NavTreePlugin.tsx +10 -22
  139. package/src/NavTreePlugin.workerd.ts +16 -0
  140. package/src/capabilities/app-graph-builder.ts +1 -1
  141. package/src/capabilities/index.ts +1 -1
  142. package/src/capabilities/operation-handler.ts +1 -1
  143. package/src/capabilities/react-surface.tsx +3 -3
  144. package/src/capabilities/state.ts +12 -8
  145. package/src/components/NavTree/NavTree.stories.tsx +8 -10
  146. package/src/components/NavTree/NavTree.tsx +2 -2
  147. package/src/components/NavTreeItem/NavTreeItemAction.stories.tsx +2 -2
  148. package/src/components/NavTreeItem/NavTreeItemAction.tsx +12 -12
  149. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +17 -60
  150. package/src/components/Sidebar/L0Menu.tsx +17 -11
  151. package/src/components/Sidebar/L1Panel.tsx +87 -168
  152. package/src/components/types.ts +0 -1
  153. package/src/containers/CommandsDialogContent/CommandsDialogContent.stories.tsx +2 -1
  154. package/src/containers/NavTreeContainer/NavTreeContainer.tsx +0 -9
  155. package/src/experimental/Tree.tsx +2 -2
  156. package/src/hooks/useActions.ts +10 -0
  157. package/src/hooks/useNavTreeModel.ts +3 -1
  158. package/src/hooks/useNavTreeState.ts +1 -8
  159. package/src/index.ts +1 -2
  160. package/src/meta.ts +22 -7
  161. package/src/operations/expose.ts +1 -1
  162. package/src/operations/index.ts +1 -1
  163. package/src/plugin.ts +11 -0
  164. package/src/testing/app-graph-builder.ts +7 -5
  165. package/src/testing/generator.ts +2 -7
  166. package/src/translations.ts +0 -1
  167. package/src/types/NavTreeCapabilities.ts +21 -0
  168. package/src/types/{events.ts → NavTreeEvents.ts} +3 -3
  169. package/src/types/index.ts +3 -2
  170. package/src/types/schema.ts +4 -0
  171. package/src/util.ts +1 -1
  172. package/src/vite-env.d.ts +10 -0
  173. package/dist/lib/browser/index.mjs +0 -202
  174. package/dist/lib/browser/index.mjs.map +0 -7
  175. package/dist/lib/browser/meta.json +0 -1
  176. package/dist/types/src/types/capabilities.d.ts +0 -16
  177. package/dist/types/src/types/capabilities.d.ts.map +0 -1
  178. package/dist/types/src/types/events.d.ts +0 -5
  179. package/dist/types/src/types/events.d.ts.map +0 -1
  180. package/src/types/capabilities.ts +0 -21
@@ -4,90 +4,47 @@
4
4
 
5
5
  import React, { Fragment, memo, useMemo } from 'react';
6
6
 
7
- import { Node } from '@dxos/app-graph';
8
7
  import { Popover, Treegrid, toLocalizedString, useTranslation } from '@dxos/react-ui';
9
8
 
10
- import { useActions } from '#hooks';
9
+ import { getListActions, useActions } from '#hooks';
11
10
  import { meta } from '#meta';
12
11
 
13
12
  import { NAV_TREE_ITEM } from '../NavTree';
14
13
  import { useNavTreeContext } from '../NavTreeContext';
15
14
  import { type NavTreeItemColumnsProps } from '../types';
16
- import { NavTreeItemAction } from './NavTreeItemAction';
15
+ import { NavTreeItemActionDropdownMenu, NavTreeItemMonolithicAction } from './NavTreeItemAction';
17
16
 
18
17
  export const NavTreeItemColumns = memo(({ path, item, open }: NavTreeItemColumnsProps) => {
19
18
  const { t } = useTranslation(meta.id);
20
19
  const { renderItemEnd: ItemEnd, popoverAnchorId } = useNavTreeContext();
21
20
 
22
21
  const level = path.length - 2;
23
- const { actions: actionsProp, groupedActions } = useActions(item);
24
- const sortedActions = useMemo(
25
- () =>
26
- actionsProp.toSorted((actionA, actionB) => {
27
- const primaryA = actionA.properties?.disposition === 'list-item-primary';
28
- const primaryB = actionB.properties?.disposition === 'list-item-primary';
29
- if (primaryA && !primaryB) {
30
- return -1;
31
- }
32
- if (primaryB && !primaryA) {
33
- return 1;
34
- }
35
- return 0;
36
- }),
37
- [actionsProp],
38
- );
39
- const [primaryAction, ...secondaryActions] = sortedActions;
40
-
41
- const actions = useMemo(
42
- () =>
43
- (primaryAction?.properties?.disposition === 'list-item-primary' ? secondaryActions : sortedActions)
44
- .flatMap((action) => (Node.isAction(action) ? [action] : []))
45
- .filter((a) => ['list-item', 'list-item-primary'].includes(a.properties?.disposition)),
46
- [sortedActions, primaryAction],
47
- );
48
-
49
- const primaryMenuActions = useMemo(
50
- () =>
51
- primaryAction
52
- ? Node.isAction(primaryAction)
53
- ? [primaryAction]
54
- : groupedActions[primaryAction?.id ?? '']
55
- : undefined,
56
- [primaryAction, groupedActions],
57
- );
22
+ const flattenedActions = useActions(item);
23
+ const allActions = useMemo(() => getListActions(flattenedActions), [flattenedActions]);
58
24
 
59
25
  const ActionRoot = popoverAnchorId === `${NAV_TREE_ITEM}:${item.id}` ? Popover.Anchor : Fragment;
60
26
 
61
27
  return (
62
- <div role='none' className='contents dx-app-no-drag'>
63
- {primaryAction?.properties?.disposition === 'list-item-primary' && !primaryAction?.properties?.disabled ? (
64
- <Treegrid.Cell classNames='contents'>
65
- <NavTreeItemAction
66
- testId={primaryAction.properties?.testId}
67
- label={toLocalizedString(primaryAction.properties?.label, t)}
68
- icon={primaryAction.properties?.icon ?? 'ph--placeholder--regular'}
69
- parent={item}
70
- path={path}
71
- monolithic={Node.isAction(primaryAction)}
72
- menuActions={primaryMenuActions}
73
- menuType={primaryAction.properties?.menuType}
74
- caller={NAV_TREE_ITEM}
75
- />
76
- </Treegrid.Cell>
77
- ) : (
78
- <Treegrid.Cell role='none' />
79
- )}
28
+ <div className='contents dx-app-no-drag'>
80
29
  <ActionRoot>
81
- {actions.length > 0 ? (
30
+ {allActions.length === 1 ? (
31
+ <Treegrid.Cell classNames='contents'>
32
+ <NavTreeItemMonolithicAction
33
+ baseLabel={toLocalizedString(allActions[0].properties?.label, t)}
34
+ parent={item}
35
+ path={path}
36
+ {...allActions[0]}
37
+ />
38
+ </Treegrid.Cell>
39
+ ) : allActions.length > 1 ? (
82
40
  <Treegrid.Cell classNames='contents'>
83
- <NavTreeItemAction
41
+ <NavTreeItemActionDropdownMenu
84
42
  testId={`navtree.treeItem.actionsLevel${level}`}
85
43
  label={t('tree-item-actions.label')}
86
44
  icon='ph--dots-three-vertical--regular'
87
45
  parent={item}
88
46
  path={path}
89
- menuActions={actions}
90
- menuType='dropdown'
47
+ menuActions={allActions}
91
48
  caller={NAV_TREE_ITEM}
92
49
  />
93
50
  </Treegrid.Cell>
@@ -142,9 +142,8 @@ const L0ItemRoot = memo(
142
142
 
143
143
  export const L0ItemActiveTabIndicator = ({ classNames }: ThemedClassName<{}>) => (
144
144
  <div
145
- role='none'
146
145
  className={mx(
147
- 'hidden group-aria-selected/l0item:block absolute start-0 h-6 w-1.5 bg-accent-surface rounded-sm',
146
+ 'hidden group-aria-selected/l0item:block absolute start-0 h-6 w-1.5 bg-accent-bg rounded-sm',
148
147
  classNames,
149
148
  )}
150
149
  />
@@ -218,14 +217,13 @@ const L0Item = memo(({ item, parent, path, pinned, onRearrange, onItemHover }: L
218
217
  return (
219
218
  <L0ItemRoot ref={itemElement} item={item} parent={parent} path={path} onMouseEnter={handleMouseEnter}>
220
219
  <div
221
- role='none'
222
220
  data-frame={true}
223
221
  {...(hue && { style: { background: `var(--color-${hue}-surface)` } })}
224
222
  className={mx(
225
223
  'flex justify-center items-center dx-focus-ring-group-indicator transition-colors rounded-sm',
226
224
  pinned
227
- ? 'p-2 group-hover/l0item:bg-active-surface'
228
- : 'w-(--dx-l0-avatar-size) h-(--dx-l0-avatar-size) bg-active-surface',
225
+ ? 'p-2 group-hover/l0item:bg-current-surface'
226
+ : 'w-(--dx-l0-avatar-size) h-(--dx-l0-avatar-size) bg-current-surface',
229
227
  )}
230
228
  >
231
229
  <ItemAvatar item={item} />
@@ -245,7 +243,7 @@ const ItemAvatar = ({ item }: Pick<L0ItemProps, 'item'>) => {
245
243
  // Actions.
246
244
  if (item.properties.icon) {
247
245
  const hue = item.properties.hue ?? null;
248
- const hueFgStyle = hue && { style: { color: `var(--color-${hue}-surface-text)` } };
246
+ const hueFgStyle = hue && { style: { color: `var(--color-${hue}-fg)` } };
249
247
  return <Icon icon={item.properties.icon} size={6} {...hueFgStyle} />;
250
248
  }
251
249
 
@@ -320,7 +318,7 @@ export const L0Menu = ({
320
318
  classNames={[
321
319
  'group/l0 absolute z-[1] inset-y-0 start-0 rounded-is',
322
320
  'grid grid-cols-[var(--dx-l0-size)] grid-rows-[var(--dx-rail-size)_1fr_min-content_var(--dx-l0-size)] dx-contain-layout',
323
- 'w-(--dx-l0-size) bg-toolbar-surface dx-app-drag pb-[env(safe-area-inset-bottom)]',
321
+ 'w-(--dx-l0-size) bg-l0-surface dx-app-drag pb-[env(safe-area-inset-bottom)]',
324
322
  '[body[data-platform="macos"]_&]:pt-[30px]',
325
323
  '[body[data-platform="ios"]_&]:pt-[max(env(safe-area-inset-top),0.25rem)]',
326
324
  ]}
@@ -328,8 +326,16 @@ export const L0Menu = ({
328
326
  {/* TODO(wittjosiah): Use L0Item trigger. */}
329
327
  <Menu.Root onAction={handleAction}>
330
328
  <Menu.Trigger asChild data-testid='spacePlugin.addSpace'>
331
- <div role='none' className='grid place-items-center'>
332
- <IconButton variant='ghost' icon='ph--list--regular' iconOnly label={t('app-menu.label')} />
329
+ <div className='grid place-items-center'>
330
+ <IconButton
331
+ density='lg'
332
+ variant='ghost'
333
+ size={5}
334
+ icon='ph--list--regular'
335
+ iconOnly
336
+ square
337
+ label={t('app-menu.label')}
338
+ />
333
339
  </div>
334
340
  </Menu.Trigger>
335
341
  <Menu.Content group={parent} items={menuActions} />
@@ -352,14 +358,14 @@ export const L0Menu = ({
352
358
  </ScrollArea.Root>
353
359
 
354
360
  {/* Actions. */}
355
- <div role='none' className='grid grid-cols-1 auto-rows-(--dx-rail-action) pt-2'>
361
+ <div className='grid grid-cols-1 auto-rows-(--dx-rail-action) pt-2'>
356
362
  {pinnedItems.map((item) => (
357
363
  <L0Item key={item.id} item={item} parent={parent} path={path} pinned />
358
364
  ))}
359
365
  </div>
360
366
 
361
367
  {userAccountItem && (
362
- <div role='none' className='grid dx-app-no-drag'>
368
+ <div className='grid dx-app-no-drag'>
363
369
  <L0ItemRoot key={userAccountItem.id} item={userAccountItem} parent={parent} path={path}>
364
370
  <UserAccountAvatar
365
371
  userId={userAccountItem.properties.userId}
@@ -2,27 +2,25 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import * as Effect from 'effect/Effect';
6
5
  import * as Option from 'effect/Option';
7
6
  import React, { memo, useCallback, useMemo } from 'react';
8
7
 
9
8
  import { Node } from '@dxos/app-graph';
10
9
  import { isPinnedWorkspace } from '@dxos/app-toolkit';
11
10
  import { useAppGraph } from '@dxos/app-toolkit/ui';
12
- import { Graph, useActionRunner, useConnections, useEdges } from '@dxos/plugin-graph';
11
+ import { Graph, useActionRunner, useEdges } from '@dxos/plugin-graph';
13
12
  import { DensityProvider, IconButton, ScrollArea, toLocalizedString, useTranslation } from '@dxos/react-ui';
14
- import { useAttended } from '@dxos/react-ui-attention';
15
13
  import { Tree } from '@dxos/react-ui-list';
16
14
  import { Menu, type MenuItem } from '@dxos/react-ui-menu';
17
15
  import { Tabs } from '@dxos/react-ui-tabs';
18
16
  import { hoverableControlItem, hoverableOpenControlItem } from '@dxos/ui-theme';
19
17
 
20
- import { useActions, useIsAlternateTree, useLoadDescendents } from '#hooks';
18
+ import { getListActions, useActions, useLoadDescendents } from '#hooks';
21
19
  import { meta } from '#meta';
22
20
 
23
21
  import { NAV_TREE_ITEM } from '../NavTree';
24
22
  import { useNavTreeContext } from '../NavTreeContext';
25
- import { NavTreeItemAction, NavTreeItemColumns } from '../NavTreeItem';
23
+ import { NavTreeItemColumns } from '../NavTreeItem/NavTreeItemColumns';
26
24
 
27
25
  export type L1PanelProps = {
28
26
  open?: boolean;
@@ -84,49 +82,28 @@ const useIsActivatedWorkspace = (item: Node.Node): boolean => {
84
82
  const L1PanelContent = ({ path, item, onBack }: Pick<L1PanelProps, 'open' | 'path' | 'item' | 'onBack'>) => {
85
83
  const navTreeContext = useNavTreeContext();
86
84
 
87
- const alternateTree = useAlternateTreeItem(item);
88
- const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
89
- const isAlternate = useIsAlternateTree(alternatePath, item);
90
-
91
85
  return (
92
- <DensityProvider density='fine'>
86
+ <DensityProvider density='md'>
93
87
  <L1PanelHeader path={path} item={item} onBack={onBack} />
94
88
  <ScrollArea.Root thin orientation='vertical'>
95
89
  <ScrollArea.Viewport>
96
- {isAlternate && alternateTree ? (
97
- <Tree
98
- model={navTreeContext.model}
99
- id={alternateTree.id}
100
- rootId={alternateTree.id}
101
- path={alternatePath}
102
- levelOffset={5}
103
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
104
- renderColumns={NavTreeItemColumns}
105
- blockInstruction={navTreeContext.blockInstruction}
106
- canDrop={navTreeContext.canDrop}
107
- canSelect={navTreeContext.canSelect}
108
- onOpenChange={navTreeContext.onOpenChange}
109
- onSelect={navTreeContext.onSelect}
110
- onItemHover={navTreeContext.onItemHover}
111
- />
112
- ) : (
113
- <Tree
114
- model={navTreeContext.model}
115
- id={item.id}
116
- rootId={item.id}
117
- path={path}
118
- levelOffset={5}
119
- gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
120
- draggable
121
- renderColumns={NavTreeItemColumns}
122
- blockInstruction={navTreeContext.blockInstruction}
123
- canDrop={navTreeContext.canDrop}
124
- canSelect={navTreeContext.canSelect}
125
- onOpenChange={navTreeContext.onOpenChange}
126
- onSelect={navTreeContext.onSelect}
127
- onItemHover={navTreeContext.onItemHover}
128
- />
129
- )}
90
+ <Tree
91
+ classNames='pt-[2px]'
92
+ model={navTreeContext.model}
93
+ id={item.id}
94
+ rootId={item.id}
95
+ path={path}
96
+ levelOffset={5}
97
+ draggable
98
+ gridTemplateColumns='[tree-row-start] minmax(0, 1fr) min-content min-content [tree-row-end]'
99
+ renderColumns={NavTreeItemColumns}
100
+ blockInstruction={navTreeContext.blockInstruction}
101
+ canDrop={navTreeContext.canDrop}
102
+ canSelect={navTreeContext.canSelect}
103
+ onOpenChange={navTreeContext.onOpenChange}
104
+ onSelect={navTreeContext.onSelect}
105
+ onItemHover={navTreeContext.onItemHover}
106
+ />
130
107
  </ScrollArea.Viewport>
131
108
  </ScrollArea.Root>
132
109
  </DensityProvider>
@@ -142,169 +119,111 @@ const L1PanelHeader = ({ item, path, onBack }: Pick<L1PanelProps, 'item' | 'path
142
119
  const title = toLocalizedString(item.properties.label, t);
143
120
  const backCapableWorkspace = isPinnedWorkspace(item.id);
144
121
 
145
- const { primaryAction, groupedActions, menuActions, onAction } = useL1MenuActions({ item, path });
122
+ const { menuActions, onAction } = useL1MenuActions({ item, path });
146
123
  useLoadDescendents(item);
147
124
 
148
- // TODO(wittjosiah): Hack to only show containsAttended when attended item is within alternate tree (e.g. settings).
149
- // Since every object in the space is a descendant of the space node, we'd otherwise always show containsAttended.
150
- const alternateTree = useAlternateTreeItem(item);
151
- const attended = useAttended();
152
- const alternateTreeOpen = useMemo(() => {
153
- if (!alternateTree) {
154
- return true;
155
- }
156
- const hasAlternateAttended = attended.some(
157
- (attendedId) => attendedId === alternateTree.id || attendedId.startsWith(alternateTree.id + '/'),
158
- );
159
- return !hasAlternateAttended;
160
- }, [alternateTree, attended]);
161
-
162
125
  return (
163
126
  <div
164
127
  data-tauri-drag-region
165
- className='flex w-full items-center border-b border-subdued-separator dx-app-drag dx-density-coarse pe-1'
128
+ className='grid grid-cols-[28px_1fr_min-content_min-content] w-full items-center dx-app-drag dx-density-lg'
166
129
  >
167
130
  {backCapableWorkspace ? (
168
131
  <IconButton
169
- density='coarse'
170
- classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
132
+ classNames={[hoverableControlItem, hoverableOpenControlItem]}
171
133
  variant='ghost'
172
134
  icon='ph--caret-left--regular'
173
135
  iconOnly
136
+ size={4}
174
137
  label={t('button-back.button')}
175
138
  data-testid='treeView.primaryTreeButton'
176
139
  onClick={() => onBack?.()}
177
140
  />
178
141
  ) : (
179
- <div data-tauri-drag-region className='w-6' />
142
+ <div />
180
143
  )}
181
144
  <h2 data-tauri-drag-region className='flex-1 truncate min-w-0'>
182
145
  {title}
183
146
  </h2>
184
- {/* TODO(wittjosiah): Reconcile with NavTreeItemColumns. */}
185
- <div role='none' className='contents dx-app-no-drag'>
186
- {primaryAction?.properties?.disposition === 'list-item-primary' && !primaryAction?.properties?.disabled && (
187
- <NavTreeItemAction
188
- testId={primaryAction.properties?.testId}
189
- label={toLocalizedString(primaryAction.properties?.label, t)}
190
- icon={primaryAction.properties?.icon ?? 'ph--placeholder--regular'}
191
- parent={item}
192
- path={path}
193
- monolithic={Node.isAction(primaryAction)}
194
- menuActions={Node.isAction(primaryAction) ? [primaryAction] : groupedActions[primaryAction?.id ?? '']}
195
- menuType={primaryAction.properties?.menuType}
196
- caller={NAV_TREE_ITEM}
197
- />
198
- )}
199
- {menuActions.length === 1 && (
200
- <IconButton
201
- density='coarse'
202
- classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
203
- variant='ghost'
204
- icon={menuActions[0].properties?.icon ?? 'ph--placeholder--regular'}
205
- iconOnly
206
- label={toLocalizedString(menuActions[0].properties?.label, t)}
207
- data-testid={menuActions[0].properties?.testId}
208
- onClick={() => onAction(menuActions[0] as Node.Action)}
209
- />
210
- )}
211
- {menuActions.length > 1 && (
212
- <Menu.Root caller={NAV_TREE_ITEM} onAction={onAction}>
213
- <Menu.Trigger asChild>
214
- <IconButton
215
- density='coarse'
216
- classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
217
- variant='ghost'
218
- icon='ph--dots-three-vertical--regular'
219
- iconOnly
220
- label={t('tree-item-actions.label')}
221
- data-testid='navtree.treeItem.actionsLevel0'
222
- />
223
- </Menu.Trigger>
224
- <Menu.Content group={item} items={menuActions as MenuItem[]} />
225
- </Menu.Root>
226
- )}
227
- {ItemEnd && <ItemEnd node={item} open={alternateTreeOpen} />}
147
+ <div className='contents dx-app-no-drag'>
148
+ <MenuActions item={item} menuActions={menuActions} onAction={onAction} />
149
+ {ItemEnd && <ItemEnd node={item} open />}
228
150
  </div>
229
151
  </div>
230
152
  );
231
153
  };
232
154
 
155
+ type L1MenuActions = {
156
+ menuActions: Node.Action[];
157
+ onAction: (action: Node.Action, params?: Node.InvokeProps) => void;
158
+ };
159
+
233
160
  /**
234
- * Builds the menu actions for the L1 panel header, combining graph actions with a synthetic settings/back action.
161
+ * Header menu actions for an L1 workspace tab. Renders nothing for an empty
162
+ * `menuActions`, a single inline icon button for one action, and a
163
+ * `…`-menu trigger for multiple.
235
164
  */
236
- const useL1MenuActions = ({ item, path }: Pick<L1PanelProps, 'item' | 'path'>) => {
165
+ const MenuActions = ({
166
+ item,
167
+ menuActions,
168
+ onAction,
169
+ }: {
170
+ item: Node.Node;
171
+ } & Pick<L1MenuActions, 'menuActions' | 'onAction'>) => {
237
172
  const { t } = useTranslation(meta.id);
238
- const { setAlternateTree } = useNavTreeContext();
239
- const { graph } = useAppGraph();
240
- const runAction = useActionRunner();
241
173
 
242
- const alternateTree = useAlternateTreeItem(item);
243
- const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
244
- const isAlternate = useIsAlternateTree(alternatePath, item);
174
+ if (menuActions.length === 0) {
175
+ return null;
176
+ }
177
+
178
+ if (menuActions.length === 1) {
179
+ return (
180
+ <IconButton
181
+ classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
182
+ variant='ghost'
183
+ icon={menuActions[0].properties?.icon ?? 'ph--circle-dashed--regular'}
184
+ iconOnly
185
+ size={4}
186
+ label={toLocalizedString(menuActions[0].properties?.label, t)}
187
+ data-testid={menuActions[0].properties?.testId}
188
+ onClick={() => onAction(menuActions[0] as Node.Action)}
189
+ />
190
+ );
191
+ }
245
192
 
246
- // Graph actions.
247
- const { actions: actionsProp, groupedActions } = useActions(item);
248
- const [primaryAction, ...secondaryActions] = actionsProp.toSorted((a, _b) =>
249
- a.properties?.disposition === 'list-item-primary' ? -1 : 1,
193
+ return (
194
+ <Menu.Root caller={NAV_TREE_ITEM} onAction={onAction}>
195
+ <Menu.Trigger asChild>
196
+ <IconButton
197
+ classNames={['shrink-0 px-2 pointer-fine:px-1', hoverableControlItem, hoverableOpenControlItem]}
198
+ variant='ghost'
199
+ icon='ph--dots-three-vertical--regular'
200
+ iconOnly
201
+ size={4}
202
+ label={t('tree-item-actions.label')}
203
+ data-testid='navtree.treeItem.actionsLevel0'
204
+ />
205
+ </Menu.Trigger>
206
+ <Menu.Content group={item} items={menuActions as MenuItem[]} />
207
+ </Menu.Root>
250
208
  );
209
+ };
251
210
 
252
- const graphMenuActions = (
253
- primaryAction?.properties?.disposition === 'list-item-primary' ? secondaryActions : actionsProp
254
- )
255
- .flatMap((action) => (Node.isAction(action) ? [action] : []))
256
- .filter((a) => ['list-item', 'list-item-primary'].includes(a.properties?.disposition));
257
-
258
- // Synthetic settings/back action.
259
- const settingsActionId = `${item.id}~settings`;
260
- const settingsAction = useMemo((): Node.Action | undefined => {
261
- if (!alternateTree) {
262
- return undefined;
263
- }
264
- return {
265
- id: settingsActionId,
266
- type: Node.ActionType,
267
- data: () => Effect.void,
268
- properties: {
269
- label: isAlternate
270
- ? ['button-back.button', { ns: meta.id }]
271
- : (alternateTree.properties.label ?? alternateTree.id),
272
- icon: isAlternate
273
- ? 'ph--arrow-u-down-left--regular'
274
- : (alternateTree.properties.icon ?? 'ph--placeholder--regular'),
275
- disposition: 'list-item',
276
- testId: isAlternate ? 'navtree.backToSpace' : 'navtree.spaceSettings',
277
- },
278
- } as Node.Action;
279
- }, [alternateTree, isAlternate, settingsActionId, t]);
211
+ /**
212
+ * Builds the menu actions for the L1 panel header.
213
+ */
214
+ const useL1MenuActions = ({ item, path }: Pick<L1PanelProps, 'item' | 'path'>): L1MenuActions => {
215
+ const runAction = useActionRunner();
280
216
 
281
- const menuActions = useMemo(
282
- () => [...graphMenuActions, ...(settingsAction ? [settingsAction] : [])],
283
- [graphMenuActions, settingsAction],
284
- );
217
+ const menuActions = getListActions(useActions(item));
285
218
 
286
219
  const onAction = useCallback(
287
220
  (action: Node.Action, params?: Node.InvokeProps) => {
288
- if (action.id === settingsActionId) {
289
- if (alternateTree && !isAlternate) {
290
- Graph.expand(graph, alternateTree.id, 'child');
291
- }
292
- setAlternateTree?.(alternatePath, !isAlternate);
293
- } else {
294
- void runAction(action, { ...params, path });
295
- }
221
+ void runAction(action, { ...params, path });
296
222
  },
297
- [settingsActionId, setAlternateTree, alternatePath, isAlternate, runAction, graph, alternateTree, path],
223
+ [runAction, path],
298
224
  );
299
225
 
300
- return { primaryAction, groupedActions, menuActions, onAction };
301
- };
302
-
303
- /** Finds the first child with disposition 'alternate-tree' using graph connections directly. */
304
- const useAlternateTreeItem = (item: Node.Node): Node.Node | undefined => {
305
- const { graph } = useAppGraph();
306
- const connections = useConnections(graph, item.id, 'child');
307
- return connections.find((node) => node.properties.disposition === 'alternate-tree');
226
+ return { menuActions, onAction };
308
227
  };
309
228
 
310
229
  export const L1Panel = memo(L1Panel$);
@@ -16,7 +16,6 @@ export type NavTreeContextValue = {
16
16
  model: TreeModel<NavTreeItemGraphNode>;
17
17
  popoverAnchorId?: string;
18
18
  renderItemEnd?: FC<{ node: Node.Node; open: boolean }>;
19
- setAlternateTree?: (path: string[], open: boolean) => void;
20
19
  onTabChange?: (node: NavTreeItemGraphNode) => void;
21
20
  } & Pick<
22
21
  TreeProps<NavTreeItemGraphNode>,
@@ -10,8 +10,9 @@ import { StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
10
10
  import { Dialog } from '@dxos/react-ui';
11
11
  import { withLayout } from '@dxos/react-ui/testing';
12
12
 
13
+ import { translations } from '#translations';
14
+
13
15
  import { NavTreePlugin } from '../../NavTreePlugin';
14
- import { translations } from '../../translations';
15
16
  import { CommandsDialogContent } from './CommandsDialogContent';
16
17
 
17
18
  const DefaultStory = () => (
@@ -230,13 +230,6 @@ export const NavTreeContainer$ = forwardRef<HTMLDivElement, NavTreeContainerProp
230
230
  });
231
231
  }, [graph]);
232
232
 
233
- const setAlternateTree = useCallback(
234
- (path: string[], open: boolean) => {
235
- setItem(path, 'alternateTree', open);
236
- },
237
- [setItem],
238
- );
239
-
240
233
  const onItemHover = useCallback(({ item }: { item: Node.Node }) => Graph.expand(graph, item.id, 'child'), [graph]);
241
234
 
242
235
  const navTreeContextValue = useMemo(
@@ -247,7 +240,6 @@ export const NavTreeContainer$ = forwardRef<HTMLDivElement, NavTreeContainerProp
247
240
  blockInstruction,
248
241
  canDrop,
249
242
  canSelect,
250
- setAlternateTree,
251
243
  onBack: handleBack,
252
244
  onOpenChange: handleOpenChange,
253
245
  onSelect: handleSelect,
@@ -260,7 +252,6 @@ export const NavTreeContainer$ = forwardRef<HTMLDivElement, NavTreeContainerProp
260
252
  blockInstruction,
261
253
  canDrop,
262
254
  canSelect,
263
- setAlternateTree,
264
255
  handleBack,
265
256
  handleOpenChange,
266
257
  handleSelect,
@@ -195,7 +195,7 @@ export const TreeNodeRow = (props: TreeNodeProps & { className?: string }) => {
195
195
  className={mx(
196
196
  'group w-full items-center cursor-pointer bg-hover-surface',
197
197
  // TODO(burdon): Use data-active.
198
- selected?.[id] && 'bg-active-surface',
198
+ selected?.[id] && 'bg-current-surface',
199
199
  className,
200
200
  )}
201
201
  onClick={() => selected && onChangeSelected?.(id, !selected[id])}
@@ -220,7 +220,7 @@ export const TreeNode = (props: TreeNodeProps & { ancestors?: TreeNodeData[] })
220
220
  const { root, header } = getSlots?.(props.node, open?.[id] ?? false, depth, ancestors) ?? {};
221
221
 
222
222
  return (
223
- <div role='none' className={mx('flex flex-col', root)}>
223
+ <div className={mx('flex flex-col', root)}>
224
224
  <TreeNodeRow {...props} className={header} />
225
225
  {(children?.length ?? 0) > 0 && open?.[id] && <TreeChildNodes {...props} ancestors={ancestors} />}
226
226
  </div>
@@ -9,6 +9,16 @@ import { Graph, Node, useActions as useGraphActions } from '@dxos/plugin-graph';
9
9
 
10
10
  import { type FlattenedActions } from '#types';
11
11
 
12
+ /**
13
+ * Flattens grouped actions and filters to the list-item dispositions rendered as
14
+ * navtree item actions. Shared by row (`NavTreeItemColumns`) and header (`L1Panel`)
15
+ * so both apply the same normalization rule.
16
+ */
17
+ export const getListActions = ({ actions, groupedActions }: FlattenedActions): Node.Action[] =>
18
+ actions
19
+ .flatMap((action) => (Node.isAction(action) ? [action] : (groupedActions[action.id] ?? [])))
20
+ .filter((action) => ['list-item', 'list-item-primary'].includes(action.properties?.disposition));
21
+
12
22
  /** Returns flattened actions and grouped sub-actions for a given graph node. */
13
23
  export const useActions = (node: Node.Node): FlattenedActions => {
14
24
  const { graph } = useAppGraph();
@@ -7,7 +7,7 @@ import * as Option from 'effect/Option';
7
7
  import { useMemo } from 'react';
8
8
 
9
9
  import { useAppGraph } from '@dxos/app-toolkit/ui';
10
- import { PLANK_COMPANION_TYPE } from '@dxos/plugin-deck/types';
10
+ import { PLANK_COMPANION_TYPE } from '@dxos/plugin-deck';
11
11
  import { type Node } from '@dxos/plugin-graph';
12
12
  import { Path, type TreeModel } from '@dxos/react-ui-list';
13
13
  import { mx } from '@dxos/ui-theme';
@@ -57,6 +57,8 @@ const createItemPropsFamily = (graph: ReturnType<typeof useAppGraph>['graph']) =
57
57
  icon: node.properties.icon,
58
58
  iconHue: node.properties.iconHue,
59
59
  testId: node.properties.testId,
60
+ count: node.properties.count,
61
+ modifiedCount: node.properties.modifiedCount,
60
62
  };
61
63
  }).pipe(Atom.keepAlive);
62
64
  });
@@ -12,7 +12,7 @@ import { NavTreeCapabilities } from '#types';
12
12
  export type UseNavTreeStateResult = {
13
13
  getItem: (path: string[]) => NavTreeCapabilities.NavTreeItemState;
14
14
  getItemAtom: (path: string[]) => Atom.Atom<NavTreeCapabilities.NavTreeItemState>;
15
- setItem: (path: string[], key: 'open' | 'current' | 'alternateTree', next: boolean) => void;
15
+ setItem: (path: string[], key: 'open' | 'current', next: boolean) => void;
16
16
  };
17
17
 
18
18
  /**
@@ -33,10 +33,3 @@ export const useNavTreeItemState = (path: string[]): NavTreeCapabilities.NavTree
33
33
  const atom = useMemo(() => getItemAtom(path), [getItemAtom, pathKey]);
34
34
  return useAtomValue(atom);
35
35
  };
36
-
37
- /**
38
- * Hook that subscribes to the alternateTree state for a tree item.
39
- */
40
- export const useIsAlternateTree = (path: string[], _item?: any): boolean => {
41
- return useNavTreeItemState(path).alternateTree ?? false;
42
- };
package/src/index.ts CHANGED
@@ -2,7 +2,6 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export { NavTreeCapabilities, NavTreeEvents } from './types';
6
- export * from './NavTreePlugin';
7
5
  export * from './meta';
6
+ export * from './types';
8
7
  export * from './util';