@dxos/plugin-navtree 0.7.5-main.9d26e3a → 0.7.5-main.b19bfc8

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 (127) hide show
  1. package/dist/lib/browser/app-graph-builder-BL7LRXCM.mjs +44 -0
  2. package/dist/lib/browser/app-graph-builder-BL7LRXCM.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-7OKF4GP3.mjs +1156 -0
  4. package/dist/lib/browser/chunk-7OKF4GP3.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-AOAGODQB.mjs +15 -0
  6. package/dist/lib/browser/chunk-AOAGODQB.mjs.map +7 -0
  7. package/dist/lib/browser/{chunk-VAXG4OAG.mjs → chunk-KOXMEAOS.mjs} +3 -3
  8. package/dist/lib/browser/{chunk-VAXG4OAG.mjs.map → chunk-KOXMEAOS.mjs.map} +3 -3
  9. package/dist/lib/browser/index.mjs +108 -1199
  10. package/dist/lib/browser/index.mjs.map +4 -4
  11. package/dist/lib/browser/intent-resolver-U25VBFH3.mjs +47 -0
  12. package/dist/lib/browser/intent-resolver-U25VBFH3.mjs.map +7 -0
  13. package/dist/lib/browser/keyboard-MUBRTPI5.mjs +46 -0
  14. package/dist/lib/browser/keyboard-MUBRTPI5.mjs.map +7 -0
  15. package/dist/lib/browser/meta.json +1 -1
  16. package/dist/lib/browser/react-surface-SIEGT4YS.mjs +57 -0
  17. package/dist/lib/browser/react-surface-SIEGT4YS.mjs.map +7 -0
  18. package/dist/lib/browser/state-IEFQTUPG.mjs +108 -0
  19. package/dist/lib/browser/state-IEFQTUPG.mjs.map +7 -0
  20. package/dist/types/src/NavTreePlugin.d.ts +1 -3
  21. package/dist/types/src/NavTreePlugin.d.ts.map +1 -1
  22. package/dist/types/src/capabilities/app-graph-builder.d.ts +181 -0
  23. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  24. package/dist/types/src/capabilities/capabilities.d.ts +18 -0
  25. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -0
  26. package/dist/types/src/capabilities/index.d.ts +251 -0
  27. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  28. package/dist/types/src/capabilities/intent-resolver.d.ts +4 -0
  29. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -0
  30. package/dist/types/src/capabilities/keyboard.d.ts +4 -0
  31. package/dist/types/src/capabilities/keyboard.d.ts.map +1 -0
  32. package/dist/types/src/capabilities/react-surface.d.ts +4 -0
  33. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  34. package/dist/types/src/capabilities/state.d.ts +17 -0
  35. package/dist/types/src/capabilities/state.d.ts.map +1 -0
  36. package/dist/types/src/components/CommandsDialog.d.ts +1 -2
  37. package/dist/types/src/components/CommandsDialog.d.ts.map +1 -1
  38. package/dist/types/src/components/CommandsTrigger.d.ts +1 -2
  39. package/dist/types/src/components/CommandsTrigger.d.ts.map +1 -1
  40. package/dist/types/src/components/HaloButton.d.ts +1 -2
  41. package/dist/types/src/components/HaloButton.d.ts.map +1 -1
  42. package/dist/types/src/components/HaloButton.stories.d.ts +1 -2
  43. package/dist/types/src/components/HaloButton.stories.d.ts.map +1 -1
  44. package/dist/types/src/components/L0Menu.d.ts +8 -0
  45. package/dist/types/src/components/L0Menu.d.ts.map +1 -0
  46. package/dist/types/src/components/L1Panels.d.ts +13 -0
  47. package/dist/types/src/components/L1Panels.d.ts.map +1 -0
  48. package/dist/types/src/components/NavTree.d.ts +2 -22
  49. package/dist/types/src/components/NavTree.d.ts.map +1 -1
  50. package/dist/types/src/components/NavTree.stories.d.ts +2 -2
  51. package/dist/types/src/components/NavTree.stories.d.ts.map +1 -1
  52. package/dist/types/src/components/NavTreeContainer.d.ts +4 -3
  53. package/dist/types/src/components/NavTreeContainer.d.ts.map +1 -1
  54. package/dist/types/src/components/NavTreeContext.d.ts +4 -0
  55. package/dist/types/src/components/NavTreeContext.d.ts.map +1 -0
  56. package/dist/types/src/components/NavTreeItemAction.d.ts +6 -10
  57. package/dist/types/src/components/NavTreeItemAction.d.ts.map +1 -1
  58. package/dist/types/src/components/NavTreeItemAction.stories.d.ts +2 -7
  59. package/dist/types/src/components/NavTreeItemAction.stories.d.ts.map +1 -1
  60. package/dist/types/src/components/NavTreeItemColumns.d.ts +4 -0
  61. package/dist/types/src/components/NavTreeItemColumns.d.ts.map +1 -0
  62. package/dist/types/src/components/NotchStart.d.ts +1 -2
  63. package/dist/types/src/components/NotchStart.d.ts.map +1 -1
  64. package/dist/types/src/components/index.d.ts +3 -0
  65. package/dist/types/src/components/index.d.ts.map +1 -1
  66. package/dist/types/src/components/types.d.ts +26 -0
  67. package/dist/types/src/components/types.d.ts.map +1 -0
  68. package/dist/types/src/events.d.ts +4 -0
  69. package/dist/types/src/events.d.ts.map +1 -0
  70. package/dist/types/src/experimental/Tree.d.ts +8 -8
  71. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  72. package/dist/types/src/experimental/Tree.stories.d.ts +2 -3
  73. package/dist/types/src/experimental/Tree.stories.d.ts.map +1 -1
  74. package/dist/types/src/hooks.d.ts +3 -0
  75. package/dist/types/src/hooks.d.ts.map +1 -0
  76. package/dist/types/src/index.d.ts +3 -2
  77. package/dist/types/src/index.d.ts.map +1 -1
  78. package/dist/types/src/meta.d.ts +1 -2
  79. package/dist/types/src/meta.d.ts.map +1 -1
  80. package/dist/types/src/stories/Graph.stories.d.ts +2 -2
  81. package/dist/types/src/stories/Graph.stories.d.ts.map +1 -1
  82. package/dist/types/src/stories/StorybookNavTree.d.ts +3 -0
  83. package/dist/types/src/stories/StorybookNavTree.d.ts.map +1 -0
  84. package/dist/types/src/translations.d.ts +0 -7
  85. package/dist/types/src/translations.d.ts.map +1 -1
  86. package/dist/types/src/types.d.ts +3 -2
  87. package/dist/types/src/types.d.ts.map +1 -1
  88. package/dist/types/src/util.d.ts +9 -0
  89. package/dist/types/src/util.d.ts.map +1 -1
  90. package/dist/types/tsconfig.tsbuildinfo +1 -1
  91. package/package.json +34 -39
  92. package/src/NavTreePlugin.tsx +67 -291
  93. package/src/capabilities/app-graph-builder.ts +42 -0
  94. package/src/capabilities/capabilities.ts +19 -0
  95. package/src/capabilities/index.ts +13 -0
  96. package/src/capabilities/intent-resolver.ts +36 -0
  97. package/src/capabilities/keyboard.ts +54 -0
  98. package/src/capabilities/react-surface.tsx +56 -0
  99. package/src/capabilities/state.ts +97 -0
  100. package/src/components/CommandsDialog.tsx +3 -4
  101. package/src/components/CommandsTrigger.tsx +7 -5
  102. package/src/components/L0Menu.tsx +238 -0
  103. package/src/components/L1Panels.tsx +119 -0
  104. package/src/components/NavTree.stories.tsx +6 -6
  105. package/src/components/NavTree.tsx +26 -124
  106. package/src/components/NavTreeContainer.tsx +114 -45
  107. package/src/components/NavTreeContext.tsx +13 -0
  108. package/src/components/NavTreeItemAction.stories.tsx +0 -2
  109. package/src/components/NavTreeItemAction.tsx +58 -316
  110. package/src/components/NavTreeItemColumns.tsx +71 -0
  111. package/src/components/NotchStart.tsx +6 -5
  112. package/src/components/index.ts +3 -0
  113. package/src/components/types.ts +35 -0
  114. package/src/events.ts +11 -0
  115. package/src/hooks.ts +21 -0
  116. package/src/index.ts +3 -4
  117. package/src/meta.ts +1 -1
  118. package/src/stories/Graph.stories.tsx +6 -5
  119. package/src/stories/StorybookNavTree.tsx +63 -0
  120. package/src/translations.ts +0 -7
  121. package/src/types.ts +3 -2
  122. package/src/util.ts +56 -2
  123. package/dist/lib/browser/meta.mjs +0 -13
  124. package/dist/lib/browser/meta.mjs.map +0 -7
  125. package/dist/types/src/components/NavTreeFooter.d.ts +0 -6
  126. package/dist/types/src/components/NavTreeFooter.d.ts.map +0 -1
  127. package/src/components/NavTreeFooter.tsx +0 -159
@@ -2,24 +2,12 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { useControllableState } from '@radix-ui/react-use-controllable-state';
6
- import React, { type MutableRefObject, type PropsWithChildren, useRef, useState } from 'react';
5
+ import React, { type MutableRefObject, useRef } from 'react';
7
6
 
8
- import { type Action, type ActionLike, type Node } from '@dxos/app-graph';
9
- import { keySymbols } from '@dxos/keyboard';
10
- import {
11
- Button,
12
- Dialog,
13
- DropdownMenu,
14
- ContextMenu,
15
- Tooltip,
16
- useTranslation,
17
- toLocalizedString,
18
- Icon,
19
- } from '@dxos/react-ui';
20
- import { SearchList } from '@dxos/react-ui-searchlist';
21
- import { descriptionText, hoverableControlItem, hoverableOpenControlItem, mx } from '@dxos/react-ui-theme';
22
- import { getHostPlatform } from '@dxos/util';
7
+ import { type Action, type Node } from '@dxos/app-graph';
8
+ import { useTranslation, toLocalizedString, IconButton } from '@dxos/react-ui';
9
+ import { DropdownMenu, MenuProvider, type MenuItem } from '@dxos/react-ui-menu';
10
+ import { hoverableControlItem, hoverableOpenControlItem, mx } from '@dxos/react-ui-theme';
23
11
 
24
12
  import { NAVTREE_PLUGIN } from '../meta';
25
13
  import { type ActionProperties } from '../types';
@@ -30,338 +18,92 @@ export type NavTreeItemActionMenuProps = ActionProperties & {
30
18
  monolithic?: boolean;
31
19
  menuActions?: Action[];
32
20
  suppressNextTooltip?: MutableRefObject<boolean>;
33
- menuOpen?: boolean;
34
- defaultMenuOpen?: boolean;
35
- onChangeMenuOpen?: (nextOpen: boolean) => void;
36
21
  onAction?: (action: Action) => void;
37
22
  };
38
23
 
39
- const getShortcut = (action: ActionLike) => {
40
- return typeof action.properties?.keyBinding === 'string'
41
- ? action.properties.keyBinding
42
- : action.properties?.keyBinding?.[getHostPlatform()];
43
- };
44
-
45
24
  const fallbackIcon = 'ph--placeholder--regular';
46
25
 
26
+ const actionButtonProps = {
27
+ size: 4 as const,
28
+ density: 'fine' as const,
29
+ };
30
+
47
31
  export const NavTreeItemActionDropdownMenu = ({
48
32
  label,
49
33
  icon,
50
34
  testId,
51
- defaultMenuOpen,
52
35
  menuActions,
53
- menuOpen,
54
- suppressNextTooltip,
55
- onChangeMenuOpen,
56
36
  onAction,
57
37
  }: NavTreeItemActionMenuProps) => {
58
38
  const { t } = useTranslation(NAVTREE_PLUGIN);
59
-
60
- const [optionsMenuOpen, setOptionsMenuOpen] = useControllableState({
61
- prop: menuOpen,
62
- defaultProp: defaultMenuOpen,
63
- onChange: onChangeMenuOpen,
64
- });
65
-
39
+ const suppressNextTooltip = useRef<boolean>(false);
66
40
  return (
67
- <DropdownMenu.Root
68
- {...{
69
- open: optionsMenuOpen,
70
- onOpenChange: (nextOpen: boolean) => {
71
- if (!nextOpen && suppressNextTooltip) {
72
- suppressNextTooltip.current = true;
73
- }
74
- return setOptionsMenuOpen(nextOpen);
75
- },
76
- }}
77
- >
78
- <Tooltip.Trigger asChild>
41
+ <MenuProvider onAction={onAction}>
42
+ <DropdownMenu.Root items={menuActions as MenuItem[]} suppressNextTooltip={suppressNextTooltip}>
79
43
  <DropdownMenu.Trigger asChild>
80
- <Button
44
+ <IconButton
45
+ {...actionButtonProps}
46
+ classNames={mx('shrink-0 pli-2 pointer-fine:pli-1', hoverableControlItem, hoverableOpenControlItem)}
81
47
  variant='ghost'
82
- density='fine'
83
- classNames={mx('shrink-0 !pli-2 pointer-fine:!pli-1', hoverableControlItem, hoverableOpenControlItem)}
48
+ icon={icon ?? fallbackIcon}
49
+ iconOnly
50
+ label={toLocalizedString(label, t)}
84
51
  data-testid={testId}
85
- aria-label={t('tree item actions label')}
86
- >
87
- <span className='sr-only'>{toLocalizedString(label, t)}</span>
88
- <Icon icon={icon ?? fallbackIcon} size={4} />
89
- </Button>
52
+ suppressNextTooltip={suppressNextTooltip}
53
+ />
90
54
  </DropdownMenu.Trigger>
91
- </Tooltip.Trigger>
92
- <DropdownMenu.Portal>
93
- <DropdownMenu.Content>
94
- <DropdownMenu.Viewport>
95
- {menuActions?.map((action) => {
96
- const shortcut = getShortcut(action);
97
- return (
98
- <DropdownMenu.Item
99
- key={action.id}
100
- onClick={(event) => {
101
- if (action.properties?.disabled) {
102
- return;
103
- }
104
- event.stopPropagation();
105
- // TODO(thure): Why does Dialog’s modal-ness cause issues if we don’t explicitly close the menu here?
106
- if (suppressNextTooltip) {
107
- suppressNextTooltip.current = true;
108
- }
109
- setOptionsMenuOpen(false);
110
- onAction?.(action);
111
- }}
112
- classNames='gap-2'
113
- disabled={action.properties?.disabled}
114
- {...(action.properties?.testId && { 'data-testid': action.properties.testId })}
115
- >
116
- {action.properties?.icon && <Icon icon={action.properties!.icon} size={4} />}
117
- <span className='grow truncate'>{toLocalizedString(action.properties!.label, t)}</span>
118
- {shortcut && <span className={mx('shrink-0', descriptionText)}>{keySymbols(shortcut).join('')}</span>}
119
- </DropdownMenu.Item>
120
- );
121
- })}
122
- </DropdownMenu.Viewport>
123
- <DropdownMenu.Arrow />
124
- </DropdownMenu.Content>
125
- </DropdownMenu.Portal>
126
- </DropdownMenu.Root>
127
- );
128
- };
129
-
130
- export const NavTreeItemActionContextMenu = (
131
- props: PropsWithChildren<Pick<NavTreeItemActionMenuProps, 'menuActions' | 'onAction'>>,
132
- ) => {
133
- return (props.menuActions?.length ?? 0) > 0 ? <NavTreeItemActionContextMenuImpl {...props} /> : <>{props.children}</>;
134
- };
135
-
136
- const NavTreeItemActionContextMenuImpl = ({
137
- menuActions,
138
- onAction,
139
- children,
140
- }: PropsWithChildren<Pick<NavTreeItemActionMenuProps, 'menuActions' | 'onAction'>>) => {
141
- const { t } = useTranslation(NAVTREE_PLUGIN);
142
-
143
- return (
144
- <ContextMenu.Root>
145
- <ContextMenu.Trigger asChild>{children}</ContextMenu.Trigger>
146
- <ContextMenu.Portal>
147
- <ContextMenu.Content>
148
- <ContextMenu.Viewport>
149
- {menuActions?.map((action) => {
150
- const shortcut = getShortcut(action);
151
- return (
152
- <ContextMenu.Item
153
- key={action.id}
154
- onClick={(event) => {
155
- if (action.properties?.disabled) {
156
- return;
157
- }
158
- event.stopPropagation();
159
- onAction?.(action);
160
- }}
161
- classNames='gap-2'
162
- disabled={action.properties?.disabled}
163
- {...(action.properties?.testId && { 'data-testid': action.properties.testId })}
164
- >
165
- {action.properties?.icon && <Icon icon={action.properties?.icon} size={4} />}
166
- <span className='grow truncate'>{toLocalizedString(action.properties!.label, t)}</span>
167
- {shortcut && <span className={mx('shrink-0', descriptionText)}>{keySymbols(shortcut).join('')}</span>}
168
- </ContextMenu.Item>
169
- );
170
- })}
171
- </ContextMenu.Viewport>
172
- <ContextMenu.Arrow />
173
- </ContextMenu.Content>
174
- </ContextMenu.Portal>
175
- </ContextMenu.Root>
176
- );
177
- };
178
-
179
- export const NavTreeItemActionSearchList = ({
180
- menuActions,
181
- icon,
182
- label,
183
- testId,
184
- suppressNextTooltip,
185
- onAction,
186
- }: Pick<
187
- NavTreeItemActionMenuProps,
188
- 'icon' | 'menuActions' | 'testId' | 'label' | 'onAction' | 'suppressNextTooltip'
189
- >) => {
190
- const { t } = useTranslation(NAVTREE_PLUGIN);
191
-
192
- const [optionsMenuOpen, setOptionsMenuOpen] = useState(false);
193
- const button = useRef<HTMLButtonElement | null>(null);
194
-
195
- // TODO(burdon): Optionally sort.
196
- const sortedActions = menuActions?.sort(({ properties: p1 }, { properties: p2 }) => {
197
- const t1 = toLocalizedString(p1!.label, t).toLowerCase();
198
- const t2 = toLocalizedString(p2!.label, t).toLowerCase();
199
- return t1.localeCompare(t2);
200
- });
201
-
202
- // TODO(thure): Use LayoutPlugin’s global Dialog.
203
- return (
204
- <Dialog.Root
205
- {...{
206
- open: optionsMenuOpen,
207
- onOpenChange: (nextOpen: boolean) => {
208
- if (!nextOpen && suppressNextTooltip) {
209
- suppressNextTooltip.current = true;
210
- }
211
- return setOptionsMenuOpen(nextOpen);
212
- },
213
- }}
214
- >
215
- <Tooltip.Trigger asChild>
216
- <Dialog.Trigger asChild>
217
- <Button
218
- variant='ghost'
219
- density='fine'
220
- classNames={mx('shrink-0 !pli-2 pointer-fine:!pli-1', hoverableControlItem, hoverableOpenControlItem)}
221
- data-testid={testId}
222
- onKeyDownCapture={(event) => {
223
- // TODO(thure): Why is only this `Button` affected and not `DropdownMenu.Trigger`’s?
224
- if (button.current && button.current === event.target) {
225
- switch (event.key) {
226
- case 'Enter':
227
- case ' ':
228
- // Capture this event and stop propagation to the drag start handler.
229
- event.stopPropagation();
230
- setOptionsMenuOpen(true);
231
- break;
232
- default:
233
- }
234
- }
235
- }}
236
- ref={button}
237
- >
238
- <Icon icon={icon ?? fallbackIcon} size={4} />
239
- </Button>
240
- </Dialog.Trigger>
241
- </Tooltip.Trigger>
242
- <Dialog.Portal>
243
- <Dialog.Overlay>
244
- <Dialog.Content classNames='is-full max-is-[24rem] pli-2 plb-1'>
245
- <SearchList.Root label={t('tree item searchlist input placeholder')}>
246
- <SearchList.Input placeholder={t('tree item searchlist input placeholder')} classNames='pli-3' />
247
- <SearchList.Content classNames='min-bs-[12rem] bs-[50dvh] max-bs-[30rem] overflow-auto'>
248
- {sortedActions?.map((action) => {
249
- const label = toLocalizedString(action.properties!.label, t);
250
- const shortcut = getShortcut(action);
251
- return (
252
- <SearchList.Item
253
- // TODO(burdon): Value should be id since label is not unique.
254
- value={label}
255
- key={action.id}
256
- onSelect={() => {
257
- if (action.properties?.disabled) {
258
- return;
259
- }
260
- if (suppressNextTooltip) {
261
- suppressNextTooltip.current = true;
262
- }
263
- setOptionsMenuOpen(false);
264
- onAction?.(action);
265
- }}
266
- classNames='flex items-center gap-2 pli-2'
267
- disabled={action.properties?.disabled}
268
- {...(action.properties?.testId && { 'data-testid': action.properties.testId })}
269
- >
270
- {action.properties?.icon && <Icon icon={action.properties?.icon} size={4} />}
271
- <span className='grow truncate'>{label}</span>
272
- {shortcut && (
273
- <span className={mx('shrink-0', descriptionText)}>{keySymbols(shortcut).join('')}</span>
274
- )}
275
- </SearchList.Item>
276
- );
277
- })}
278
- </SearchList.Content>
279
- <div role='none' className='flex items-center plb-2 pli-3'>
280
- <span className={descriptionText}>{toLocalizedString(label, t)}</span>
281
- </div>
282
- </SearchList.Root>
283
- </Dialog.Content>
284
- </Dialog.Overlay>
285
- </Dialog.Portal>
286
- </Dialog.Root>
55
+ </DropdownMenu.Root>
56
+ </MenuProvider>
287
57
  );
288
58
  };
289
59
 
290
60
  export const NavTreeItemMonolithicAction = ({
291
61
  parent,
292
- properties: { disabled, caller, testId, label, icon } = { label: 'never' },
62
+ properties: { disabled, caller, testId, icon, variant = 'ghost', iconOnly = true } = { label: 'never' },
293
63
  data: invoke,
294
- }: Action & { parent: Node; onAction?: (action: Action) => void }) => {
295
- const { t } = useTranslation(NAVTREE_PLUGIN);
64
+ baseLabel,
65
+ }: Action & { parent: Node; onAction?: (action: Action) => void; baseLabel: string }) => {
296
66
  return (
297
- <Tooltip.Trigger asChild>
298
- <Button
299
- variant='ghost'
300
- density='fine'
301
- classNames={mx('shrink-0 !pli-2 pointer-fine:!pli-1', hoverableControlItem, hoverableOpenControlItem)}
302
- disabled={disabled}
303
- onClick={(event) => {
304
- if (disabled) {
305
- return;
306
- }
307
- event.stopPropagation();
308
- void invoke?.(caller ? { node: parent, caller } : { node: parent });
309
- }}
310
- data-testid={testId}
311
- >
312
- <span className='sr-only'>{toLocalizedString(label, t)}</span>
313
- <Icon icon={icon ?? fallbackIcon} size={4} />
314
- </Button>
315
- </Tooltip.Trigger>
67
+ <IconButton
68
+ {...actionButtonProps}
69
+ variant={variant}
70
+ classNames={mx(
71
+ 'shrink-0',
72
+ hoverableControlItem,
73
+ hoverableOpenControlItem,
74
+ iconOnly ? 'pli-2 pointer-fine:pli-1' : 'p-2 pointer-fine:p-2 mie-1',
75
+ )}
76
+ icon={icon ?? fallbackIcon}
77
+ iconOnly={iconOnly}
78
+ label={baseLabel}
79
+ disabled={disabled}
80
+ onClick={(event) => {
81
+ if (disabled) {
82
+ return;
83
+ }
84
+ event.stopPropagation();
85
+ void invoke?.(caller ? { node: parent, caller } : { node: parent });
86
+ }}
87
+ data-testid={testId}
88
+ />
316
89
  );
317
90
  };
318
91
 
319
92
  export const NavTreeItemAction = ({ monolithic, menuActions, parent: node, ...props }: NavTreeItemActionMenuProps) => {
320
93
  const { t } = useTranslation(NAVTREE_PLUGIN);
321
- const suppressNextTooltip = useRef<boolean>(false);
322
- const [triggerTooltipOpen, setTriggerTooltipOpen] = useState(false);
323
94
 
324
95
  const monolithicAction = menuActions?.length === 1 && menuActions[0];
325
96
  const baseLabel = toLocalizedString(monolithicAction ? monolithicAction.properties!.label : props.label, t);
326
97
 
327
- return (
328
- <Tooltip.Root
329
- open={triggerTooltipOpen}
330
- onOpenChange={(nextOpen) => {
331
- if (suppressNextTooltip.current) {
332
- setTriggerTooltipOpen(false);
333
- suppressNextTooltip.current = false;
334
- } else {
335
- setTriggerTooltipOpen(nextOpen);
336
- }
337
- }}
338
- >
339
- {baseLabel && (
340
- <Tooltip.Portal>
341
- <Tooltip.Content classNames='z-[31]' side='bottom'>
342
- {baseLabel}
343
- <Tooltip.Arrow />
344
- </Tooltip.Content>
345
- </Tooltip.Portal>
346
- )}
347
- {monolithic && menuActions?.length === 1 ? (
348
- <NavTreeItemMonolithicAction parent={node} {...menuActions[0]} />
349
- ) : props.menuType === 'searchList' ? (
350
- <NavTreeItemActionSearchList
351
- {...props}
352
- menuActions={menuActions}
353
- suppressNextTooltip={suppressNextTooltip}
354
- onAction={(action) => action.data?.(props.caller ? { node, caller: props.caller } : { node })}
355
- />
356
- ) : (
357
- <NavTreeItemActionDropdownMenu
358
- {...props}
359
- parent={node}
360
- menuActions={menuActions}
361
- suppressNextTooltip={suppressNextTooltip}
362
- onAction={(action) => action.data?.(props.caller ? { node, caller: props.caller } : { node })}
363
- />
364
- )}
365
- </Tooltip.Root>
98
+ return monolithic && menuActions?.length === 1 ? (
99
+ <NavTreeItemMonolithicAction baseLabel={baseLabel} parent={node} {...menuActions[0]} />
100
+ ) : (
101
+ <NavTreeItemActionDropdownMenu
102
+ {...props}
103
+ label={baseLabel}
104
+ parent={node}
105
+ menuActions={menuActions}
106
+ onAction={(action) => action.data?.(props.caller ? { node, caller: props.caller } : { node })}
107
+ />
366
108
  );
367
109
  };
@@ -0,0 +1,71 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { Fragment, memo } from 'react';
6
+
7
+ import { isAction } from '@dxos/app-graph';
8
+ import { Popover, toLocalizedString, Treegrid, useTranslation } from '@dxos/react-ui';
9
+
10
+ import { NAV_TREE_ITEM } from './NavTree';
11
+ import { useNavTreeContext } from './NavTreeContext';
12
+ import { NavTreeItemAction } from './NavTreeItemAction';
13
+ import type { NavTreeItemColumnsProps } from './types';
14
+ import { useLoadDescendents } from '../hooks';
15
+ import { NAVTREE_PLUGIN } from '../meta';
16
+
17
+ export const NavTreeItemColumns = memo(({ path, item, open }: NavTreeItemColumnsProps) => {
18
+ const { t } = useTranslation(NAVTREE_PLUGIN);
19
+ const { getActions, renderItemEnd: ItemEnd, popoverAnchorId } = useNavTreeContext();
20
+
21
+ const level = path.length - 2;
22
+
23
+ const { actions: _actions, groupedActions } = getActions(item);
24
+ const [primaryAction, ...secondaryActions] = _actions.toSorted((a, b) =>
25
+ a.properties?.disposition === 'toolbar' ? -1 : 1,
26
+ );
27
+
28
+ const actions = (primaryAction?.properties?.disposition === 'toolbar' ? secondaryActions : _actions)
29
+ .flatMap((action) => (isAction(action) ? [action] : []))
30
+ .filter((action) => !action.properties?.hidden);
31
+
32
+ const ActionRoot = popoverAnchorId === `dxos.org/ui/${NAV_TREE_ITEM}/${item.id}` ? Popover.Anchor : Fragment;
33
+
34
+ useLoadDescendents(item);
35
+ useLoadDescendents(primaryAction && !isAction(primaryAction) ? primaryAction : undefined);
36
+
37
+ return (
38
+ <div role='none' className='contents app-no-drag'>
39
+ {primaryAction?.properties?.disposition === 'toolbar' && !primaryAction?.properties?.disabled ? (
40
+ <NavTreeItemAction
41
+ testId={primaryAction.properties?.testId}
42
+ label={toLocalizedString(primaryAction.properties?.label, t)}
43
+ icon={primaryAction.properties?.icon ?? 'ph--placeholder--regular'}
44
+ parent={item}
45
+ monolithic={isAction(primaryAction)}
46
+ menuActions={isAction(primaryAction) ? [primaryAction] : groupedActions[primaryAction.id]}
47
+ menuType={primaryAction.properties?.menuType}
48
+ caller={NAV_TREE_ITEM}
49
+ />
50
+ ) : (
51
+ <Treegrid.Cell />
52
+ )}
53
+ <ActionRoot>
54
+ {actions.length > 0 ? (
55
+ <NavTreeItemAction
56
+ testId={`navtree.treeItem.actionsLevel${level}`}
57
+ label={t('tree item actions label')}
58
+ icon='ph--dots-three-vertical--regular'
59
+ parent={item}
60
+ menuActions={actions}
61
+ menuType='dropdown'
62
+ caller={NAV_TREE_ITEM}
63
+ />
64
+ ) : (
65
+ <Treegrid.Cell />
66
+ )}
67
+ </ActionRoot>
68
+ {ItemEnd && <ItemEnd node={item} open={open} />}
69
+ </div>
70
+ );
71
+ });
@@ -4,9 +4,9 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { createIntent, useIntentDispatcher, useResolvePlugin } from '@dxos/app-framework';
7
+ import { createIntent, useCapabilities, useIntentDispatcher } from '@dxos/app-framework';
8
8
  import { ClientAction } from '@dxos/plugin-client/types';
9
- import { parseObservabilityPlugin } from '@dxos/plugin-observability';
9
+ import { ObservabilityCapabilities } from '@dxos/plugin-observability';
10
10
  import { useIdentity } from '@dxos/react-client/halo';
11
11
 
12
12
  import { HaloButton } from './HaloButton';
@@ -14,15 +14,16 @@ import { HaloButton } from './HaloButton';
14
14
  // TODO(thure): Refactor to be handled by a more appropriate plugin (ClientPlugin?).
15
15
  export const NotchStart = () => {
16
16
  const identity = useIdentity();
17
- const observabilityPlugin = useResolvePlugin(parseObservabilityPlugin);
17
+ // TODO(wittjosiah): Can this be removed now?
18
+ const [observability] = useCapabilities(ObservabilityCapabilities.Observability);
18
19
  const { dispatchPromise: dispatch } = useIntentDispatcher();
19
20
  return (
20
21
  <HaloButton
21
- size={8}
22
+ size={12}
22
23
  identityKey={identity?.identityKey.toHex()}
23
24
  hue={identity?.profile?.data?.hue}
24
25
  emoji={identity?.profile?.data?.emoji}
25
- internal={observabilityPlugin?.provides?.observability?.group === 'dxos'}
26
+ internal={observability?.group === 'dxos'}
26
27
  onClick={() => dispatch(createIntent(ClientAction.ShareIdentity))}
27
28
  />
28
29
  );
@@ -3,7 +3,10 @@
3
3
  //
4
4
 
5
5
  export * from './CommandsDialog';
6
+ export * from './CommandsTrigger';
6
7
  export * from './NavTree';
7
8
  export * from './NavTreeContainer';
9
+ export * from './NavTreeContext';
8
10
  export * from './NavTreeDocumentTitle';
9
11
  export * from './NotchStart';
12
+ export * from './types';
@@ -0,0 +1,35 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import type { FC } from 'react';
6
+
7
+ import type { Node } from '@dxos/app-graph';
8
+ import type { TreeProps } from '@dxos/react-ui-list';
9
+ import type { MaybePromise } from '@dxos/util';
10
+
11
+ import { type L1PanelProps } from './L1Panels';
12
+ import type { FlattenedActions, NavTreeItemGraphNode } from '../types';
13
+
14
+ export type NavTreeItemColumnsProps = {
15
+ path: string[];
16
+ item: Node;
17
+ open: boolean;
18
+ };
19
+
20
+ export type NavTreeProps = Pick<TreeProps<NavTreeItemGraphNode>, 'id' | 'root'>;
21
+
22
+ export type NavTreeContextValue = Pick<
23
+ TreeProps<NavTreeItemGraphNode>,
24
+ 'getProps' | 'isCurrent' | 'isOpen' | 'onOpenChange' | 'canDrop' | 'onSelect'
25
+ > &
26
+ Pick<L1PanelProps, 'onBack'> & {
27
+ tab: string;
28
+ onTabChange?: (node: NavTreeItemGraphNode) => void;
29
+ getItems: (node?: NavTreeItemGraphNode, disposition?: string) => NavTreeItemGraphNode[];
30
+ getActions: (node: Node) => FlattenedActions;
31
+ loadDescendents?: (node: Node) => MaybePromise<void>;
32
+ renderItemEnd?: FC<{ node: Node; open: boolean }>;
33
+ popoverAnchorId?: string;
34
+ topbar?: boolean;
35
+ };
package/src/events.ts ADDED
@@ -0,0 +1,11 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Events } from '@dxos/app-framework';
6
+
7
+ import { NAVTREE_PLUGIN } from './meta';
8
+
9
+ export namespace NavTreeEvents {
10
+ export const StateReady = Events.createStateEvent(NAVTREE_PLUGIN);
11
+ }
package/src/hooks.ts ADDED
@@ -0,0 +1,21 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { useEffect } from 'react';
6
+
7
+ import { type Node } from '@dxos/app-graph';
8
+
9
+ import { useNavTreeContext } from './components';
10
+
11
+ export const useLoadDescendents = (root?: Node) => {
12
+ const { loadDescendents } = useNavTreeContext();
13
+ useEffect(() => {
14
+ const frame = requestAnimationFrame(() => {
15
+ if (root) {
16
+ void loadDescendents?.(root);
17
+ }
18
+ });
19
+ return () => cancelAnimationFrame(frame);
20
+ }, [root]);
21
+ };
package/src/index.ts CHANGED
@@ -2,9 +2,8 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { NavTreePlugin } from './NavTreePlugin';
6
-
7
- export default NavTreePlugin;
8
-
5
+ export { NavTreeCapabilities } from './capabilities';
6
+ export { NavTreeEvents } from './events';
9
7
  export * from './NavTreePlugin';
8
+ export * from './meta';
10
9
  export * from './util';
package/src/meta.ts CHANGED
@@ -11,6 +11,6 @@ export const KEY_BINDING = 'KeyBinding';
11
11
  // TODO(wittjosiah): Factor out.
12
12
  export const COMMANDS_DIALOG = `${NAVTREE_PLUGIN}/CommandsDialog`;
13
13
 
14
- export default {
14
+ export const meta = {
15
15
  id: NAVTREE_PLUGIN,
16
16
  } satisfies PluginMeta;
@@ -15,10 +15,10 @@ import { registerSignalsRuntime } from '@dxos/echo-signals/react';
15
15
  import { create, type ReactiveObject } from '@dxos/live-object';
16
16
  import { faker } from '@dxos/random';
17
17
  import { isTreeData, type PropsFromTreeItem } from '@dxos/react-ui-list';
18
- import { Path } from '@dxos/react-ui-mosaic';
18
+ import { Path } from '@dxos/react-ui-list';
19
19
  import { withLayout, withTheme } from '@dxos/storybook-utils';
20
20
 
21
- import { NavTree } from '../components';
21
+ import { StorybookNavTree } from './StorybookNavTree';
22
22
  import { getActions, getChildren } from '../util';
23
23
 
24
24
  faker.seed(3);
@@ -53,9 +53,9 @@ const state = new Map<string, ReactiveObject<{ open: boolean; current: boolean }
53
53
 
54
54
  export const Default = {};
55
55
 
56
- const meta: Meta<typeof NavTree> = {
56
+ const meta: Meta<typeof StorybookNavTree> = {
57
57
  title: 'plugins/plugin-navtree/Graph',
58
- component: NavTree,
58
+ component: StorybookNavTree,
59
59
  decorators: [withTheme, withLayout({ tooltips: true })],
60
60
  render: (args) => {
61
61
  useEffect(() => {
@@ -78,9 +78,10 @@ const meta: Meta<typeof NavTree> = {
78
78
  });
79
79
  }, []);
80
80
 
81
- return <NavTree {...args} />;
81
+ return <StorybookNavTree {...args} />;
82
82
  },
83
83
  args: {
84
+ root: graph.root,
84
85
  id: graph.root.id,
85
86
  getActions: (node: Node) => getActions(graph, node),
86
87
  getItems: (node?: Node) => graph.nodes(node ?? graph.root),