@dxos/plugin-navtree 0.8.2-main.fbd8ed0 → 0.8.2-staging.7ac8446

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 (149) hide show
  1. package/dist/lib/browser/{app-graph-builder-P7AY3K72.mjs → app-graph-builder-26D3MUCA.mjs} +6 -7
  2. package/dist/lib/browser/app-graph-builder-26D3MUCA.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-TLQLMZXU.mjs → chunk-4RQJXI4D.mjs} +1 -1
  4. package/dist/lib/browser/chunk-4RQJXI4D.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-UM2C5XK3.mjs → chunk-DN43QXSX.mjs} +365 -444
  6. package/dist/lib/browser/chunk-DN43QXSX.mjs.map +7 -0
  7. package/dist/lib/browser/index.mjs +10 -5
  8. package/dist/lib/browser/index.mjs.map +3 -3
  9. package/dist/lib/browser/{intent-resolver-PW7B76HO.mjs → intent-resolver-BZMUS24C.mjs} +7 -7
  10. package/dist/lib/browser/intent-resolver-BZMUS24C.mjs.map +7 -0
  11. package/dist/lib/browser/keyboard-IQXHKUWM.mjs +46 -0
  12. package/dist/lib/browser/keyboard-IQXHKUWM.mjs.map +7 -0
  13. package/dist/lib/browser/meta.json +1 -1
  14. package/dist/lib/browser/{react-surface-M3CAS7ZP.mjs → react-surface-3HCM3SHJ.mjs} +3 -3
  15. package/dist/lib/browser/{state-3UBYOTQT.mjs → state-M5UJ3MHL.mjs} +12 -18
  16. package/dist/lib/browser/state-M5UJ3MHL.mjs.map +7 -0
  17. package/dist/types/src/capabilities/app-graph-builder.d.ts +179 -2
  18. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  19. package/dist/types/src/capabilities/capabilities.d.ts +4 -7
  20. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -1
  21. package/dist/types/src/capabilities/index.d.ts +183 -38
  22. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  23. package/dist/types/src/capabilities/intent-resolver.d.ts +2 -2
  24. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -1
  25. package/dist/types/src/capabilities/keyboard.d.ts +2 -2
  26. package/dist/types/src/capabilities/keyboard.d.ts.map +1 -1
  27. package/dist/types/src/capabilities/state.d.ts +6 -9
  28. package/dist/types/src/capabilities/state.d.ts.map +1 -1
  29. package/dist/types/src/components/CommandsDialog.d.ts.map +1 -0
  30. package/dist/types/src/components/CommandsTrigger.d.ts.map +1 -0
  31. package/dist/types/src/components/L0Menu.d.ts +10 -0
  32. package/dist/types/src/components/L0Menu.d.ts.map +1 -0
  33. package/dist/types/src/components/L1Panels.d.ts +14 -0
  34. package/dist/types/src/components/L1Panels.d.ts.map +1 -0
  35. package/dist/types/src/components/NavTree.d.ts +5 -0
  36. package/dist/types/src/components/NavTree.d.ts.map +1 -0
  37. package/dist/types/src/components/NavTree.stories.d.ts +7 -0
  38. package/dist/types/src/components/NavTree.stories.d.ts.map +1 -0
  39. package/dist/types/src/components/NavTreeContainer.d.ts.map +1 -1
  40. package/dist/types/src/components/NavTreeContext.d.ts.map +1 -1
  41. package/dist/types/src/components/NavTreeDocumentTitle.d.ts.map +1 -1
  42. package/dist/types/src/components/{NavTreeItem/NavTreeItemAction.d.ts → NavTreeItemAction.d.ts} +3 -2
  43. package/dist/types/src/components/NavTreeItemAction.d.ts.map +1 -0
  44. package/dist/types/src/components/NavTreeItemAction.stories.d.ts +41 -0
  45. package/dist/types/src/components/NavTreeItemAction.stories.d.ts.map +1 -0
  46. package/dist/types/src/components/NavTreeItemColumns.d.ts +4 -0
  47. package/dist/types/src/components/NavTreeItemColumns.d.ts.map +1 -0
  48. package/dist/types/src/components/{UserAccountAvatar/UserAccountAvatar.d.ts → UserAccountAvatar.d.ts} +1 -1
  49. package/dist/types/src/components/UserAccountAvatar.d.ts.map +1 -0
  50. package/dist/types/src/components/{UserAccountAvatar/UserAccountAvatar.stories.d.ts → UserAccountAvatar.stories.d.ts} +2 -1
  51. package/dist/types/src/components/UserAccountAvatar.stories.d.ts.map +1 -0
  52. package/dist/types/src/components/index.d.ts +1 -0
  53. package/dist/types/src/components/index.d.ts.map +1 -1
  54. package/dist/types/src/components/types.d.ts +14 -20
  55. package/dist/types/src/components/types.d.ts.map +1 -1
  56. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  57. package/dist/types/src/hooks.d.ts.map +1 -1
  58. package/dist/types/src/stories/Graph.stories.d.ts +7 -0
  59. package/dist/types/src/stories/Graph.stories.d.ts.map +1 -0
  60. package/dist/types/src/stories/StorybookNavTree.d.ts +4 -0
  61. package/dist/types/src/stories/StorybookNavTree.d.ts.map +1 -0
  62. package/dist/types/src/testing/generator.d.ts +6 -7
  63. package/dist/types/src/testing/generator.d.ts.map +1 -1
  64. package/dist/types/src/testing/index.d.ts +0 -1
  65. package/dist/types/src/testing/index.d.ts.map +1 -1
  66. package/dist/types/src/testing/update-state.d.ts.map +1 -1
  67. package/dist/types/src/translations.d.ts +0 -1
  68. package/dist/types/src/translations.d.ts.map +1 -1
  69. package/dist/types/src/util.d.ts +10 -7
  70. package/dist/types/src/util.d.ts.map +1 -1
  71. package/dist/types/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +31 -40
  73. package/src/NavTreePlugin.tsx +2 -2
  74. package/src/capabilities/app-graph-builder.ts +28 -38
  75. package/src/capabilities/capabilities.ts +4 -5
  76. package/src/capabilities/intent-resolver.ts +7 -8
  77. package/src/capabilities/keyboard.ts +32 -37
  78. package/src/capabilities/state.ts +17 -23
  79. package/src/components/{CommandsDialog/CommandsDialog.tsx → CommandsDialog.tsx} +7 -8
  80. package/src/components/{CommandsDialog/CommandsTrigger.tsx → CommandsTrigger.tsx} +1 -1
  81. package/src/components/{Sidebar/L0Menu.tsx → L0Menu.tsx} +131 -188
  82. package/src/components/L1Panels.tsx +115 -0
  83. package/src/components/NavTree.stories.tsx +107 -0
  84. package/src/components/NavTree.tsx +51 -0
  85. package/src/components/NavTreeContainer.tsx +74 -94
  86. package/src/components/NavTreeContext.tsx +1 -1
  87. package/src/components/{NavTreeItem/NavTreeItemAction.stories.tsx → NavTreeItemAction.stories.tsx} +6 -12
  88. package/src/components/{NavTreeItem/NavTreeItemAction.tsx → NavTreeItemAction.tsx} +18 -23
  89. package/src/components/NavTreeItemColumns.tsx +71 -0
  90. package/src/components/{UserAccountAvatar/UserAccountAvatar.stories.tsx → UserAccountAvatar.stories.tsx} +5 -5
  91. package/src/components/{UserAccountAvatar/UserAccountAvatar.tsx → UserAccountAvatar.tsx} +7 -7
  92. package/src/components/index.ts +1 -0
  93. package/src/components/types.ts +15 -20
  94. package/src/hooks.ts +11 -12
  95. package/src/stories/Graph.stories.tsx +130 -0
  96. package/src/stories/StorybookNavTree.tsx +63 -0
  97. package/src/testing/generator.ts +12 -15
  98. package/src/testing/index.ts +0 -1
  99. package/src/translations.ts +0 -1
  100. package/src/util.ts +32 -10
  101. package/dist/lib/browser/app-graph-builder-P7AY3K72.mjs.map +0 -7
  102. package/dist/lib/browser/chunk-TLQLMZXU.mjs.map +0 -7
  103. package/dist/lib/browser/chunk-UM2C5XK3.mjs.map +0 -7
  104. package/dist/lib/browser/intent-resolver-PW7B76HO.mjs.map +0 -7
  105. package/dist/lib/browser/keyboard-DDFATRUG.mjs +0 -51
  106. package/dist/lib/browser/keyboard-DDFATRUG.mjs.map +0 -7
  107. package/dist/lib/browser/state-3UBYOTQT.mjs.map +0 -7
  108. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts.map +0 -1
  109. package/dist/types/src/components/CommandsDialog/CommandsTrigger.d.ts.map +0 -1
  110. package/dist/types/src/components/CommandsDialog/index.d.ts +0 -3
  111. package/dist/types/src/components/CommandsDialog/index.d.ts.map +0 -1
  112. package/dist/types/src/components/NavTree/NavTree.d.ts +0 -8
  113. package/dist/types/src/components/NavTree/NavTree.d.ts.map +0 -1
  114. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +0 -8
  115. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +0 -1
  116. package/dist/types/src/components/NavTree/index.d.ts +0 -2
  117. package/dist/types/src/components/NavTree/index.d.ts.map +0 -1
  118. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +0 -1
  119. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +0 -8
  120. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +0 -1
  121. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts +0 -4
  122. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +0 -1
  123. package/dist/types/src/components/NavTreeItem/index.d.ts +0 -3
  124. package/dist/types/src/components/NavTreeItem/index.d.ts.map +0 -1
  125. package/dist/types/src/components/Sidebar/L0Menu.d.ts +0 -13
  126. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +0 -1
  127. package/dist/types/src/components/Sidebar/L1Panel.d.ts +0 -15
  128. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +0 -1
  129. package/dist/types/src/components/Sidebar/index.d.ts +0 -3
  130. package/dist/types/src/components/Sidebar/index.d.ts.map +0 -1
  131. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.d.ts.map +0 -1
  132. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +0 -1
  133. package/dist/types/src/components/UserAccountAvatar/index.d.ts +0 -2
  134. package/dist/types/src/components/UserAccountAvatar/index.d.ts.map +0 -1
  135. package/dist/types/src/testing/app-graph-builder.d.ts +0 -3
  136. package/dist/types/src/testing/app-graph-builder.d.ts.map +0 -1
  137. package/src/components/CommandsDialog/index.ts +0 -6
  138. package/src/components/NavTree/NavTree.stories.tsx +0 -159
  139. package/src/components/NavTree/NavTree.tsx +0 -62
  140. package/src/components/NavTree/index.ts +0 -5
  141. package/src/components/NavTreeItem/NavTreeItemColumns.tsx +0 -73
  142. package/src/components/NavTreeItem/index.ts +0 -6
  143. package/src/components/Sidebar/L1Panel.tsx +0 -175
  144. package/src/components/Sidebar/index.ts +0 -6
  145. package/src/components/UserAccountAvatar/index.ts +0 -5
  146. package/src/testing/app-graph-builder.ts +0 -224
  147. /package/dist/lib/browser/{react-surface-M3CAS7ZP.mjs.map → react-surface-3HCM3SHJ.mjs.map} +0 -0
  148. /package/dist/types/src/components/{CommandsDialog/CommandsDialog.d.ts → CommandsDialog.d.ts} +0 -0
  149. /package/dist/types/src/components/{CommandsDialog/CommandsTrigger.d.ts → CommandsTrigger.d.ts} +0 -0
@@ -7,92 +7,88 @@ import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-d
7
7
  import { preserveOffsetOnSource } from '@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source';
8
8
  import { scrollJustEnoughIntoView } from '@atlaskit/pragmatic-drag-and-drop/element/scroll-just-enough-into-view';
9
9
  import {
10
- type Edge,
11
10
  attachClosestEdge,
11
+ type Edge,
12
12
  extractClosestEdge,
13
13
  } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
14
- import React, {
15
- forwardRef,
16
- type MouseEvent,
17
- type PropsWithChildren,
18
- useCallback,
19
- useLayoutEffect,
20
- useMemo,
21
- useRef,
22
- useState,
23
- } from 'react';
14
+ import React, { type MouseEvent, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
24
15
 
16
+ import { createIntent, LayoutAction, useIntentDispatcher } from '@dxos/app-framework';
25
17
  import { type Node } from '@dxos/app-graph';
26
18
  import { invariant } from '@dxos/invariant';
27
19
  import {
28
- Icon,
29
- IconButton,
30
- ListItem,
20
+ Popover,
31
21
  ScrollArea,
32
- Tooltip,
33
22
  toLocalizedString,
23
+ Tooltip,
34
24
  useMediaQuery,
25
+ useSidebars,
35
26
  useTranslation,
27
+ ListItem,
28
+ Icon,
36
29
  } from '@dxos/react-ui';
37
- import { MenuProvider, DropdownMenu, type MenuItem } from '@dxos/react-ui-menu';
38
30
  import type { StackItemRearrangeHandler } from '@dxos/react-ui-stack';
39
31
  import { Tabs } from '@dxos/react-ui-tabs';
40
32
  import { mx } from '@dxos/react-ui-theme';
41
33
  import { arrayMove, getFirstTwoRenderableChars } from '@dxos/util';
42
34
 
43
- import { useLoadDescendents } from '../../hooks';
44
- import { NAVTREE_PLUGIN } from '../../meta';
45
- import { l0ItemType } from '../../util';
46
- import { useNavTreeContext } from '../NavTreeContext';
47
- import { UserAccountAvatar } from '../UserAccountAvatar';
48
-
49
- //
50
- // L0Item
51
- //
52
-
53
- type L0ItemData = {
54
- id: L0ItemProps['item']['id'];
55
- type: 'l0Item';
56
- };
35
+ import { useNavTreeContext } from './NavTreeContext';
36
+ import { UserAccountAvatar } from './UserAccountAvatar';
37
+ import { useLoadDescendents } from '../hooks';
38
+ import { NAVTREE_PLUGIN } from '../meta';
39
+ import { l0ItemType } from '../util';
57
40
 
58
- type L0ItemRootProps = {
59
- item: Node<any>;
60
- parent?: Node<any>;
61
- path: string[];
62
- };
41
+ type L0ItemData = { id: L0ItemProps['item']['id']; type: 'l0Item' };
63
42
 
64
- type L0ItemProps = L0ItemRootProps & {
43
+ type L0ItemProps = {
65
44
  item: Node<any>;
66
45
  parent?: Node<any>;
67
46
  path: string[];
68
47
  pinned?: boolean;
48
+ userAccount?: boolean;
69
49
  onRearrange?: StackItemRearrangeHandler<L0ItemData>;
70
50
  };
71
51
 
72
52
  const useL0ItemClick = ({ item, parent, path }: L0ItemProps, type: string) => {
73
- const { tab, isCurrent, onSelect, onTabChange } = useNavTreeContext();
53
+ const { tab, onTabChange, onSelect, isCurrent } = useNavTreeContext();
54
+ const { dispatchPromise: dispatch } = useIntentDispatcher();
55
+ const { navigationSidebarState } = useSidebars(NAVTREE_PLUGIN);
74
56
  const [isLg] = useMediaQuery('lg', { ssr: false });
75
57
 
76
58
  return useCallback(
77
59
  (event: MouseEvent) => {
78
60
  switch (type) {
61
+ case 'tab':
62
+ // TODO(thure): This dispatch should rightly be in `onTabChange`, but that callback wasn’t reacting to changes
63
+ // to its dependencies.
64
+ void dispatch(
65
+ createIntent(LayoutAction.UpdateSidebar, {
66
+ part: 'sidebar',
67
+ options: {
68
+ state:
69
+ item.id === tab
70
+ ? navigationSidebarState === 'expanded'
71
+ ? isLg
72
+ ? 'collapsed'
73
+ : 'closed'
74
+ : 'expanded'
75
+ : 'expanded',
76
+ },
77
+ }),
78
+ );
79
+ return onTabChange?.(item);
80
+ case 'link':
81
+ return onSelect?.({ item, path, current: !isCurrent(path, item), option: event.altKey });
79
82
  case 'action': {
80
83
  const {
81
84
  data: invoke,
82
85
  properties: { caller },
83
86
  } = item;
84
-
85
87
  return invoke?.(caller ? { node: parent, caller } : { node: parent });
86
88
  }
87
-
88
- case 'tab':
89
- return onTabChange?.(item);
90
-
91
- case 'link':
92
- return onSelect?.({ item, path, current: !isCurrent(path, item), option: event.altKey });
93
89
  }
94
90
  },
95
- [item, parent, type, tab, isCurrent, onSelect, onTabChange, isLg],
91
+ [item, type, onSelect, isCurrent, parent, tab, navigationSidebarState, isLg, dispatch],
96
92
  );
97
93
  };
98
94
 
@@ -100,70 +96,28 @@ const l0Breakpoints: Record<string, string> = {
100
96
  lg: 'hidden lg:grid',
101
97
  };
102
98
 
103
- const L0ItemRoot = forwardRef<HTMLElement, PropsWithChildren<L0ItemRootProps>>(
104
- ({ item, parent, path, children }, forwardedRef) => {
105
- const { getProps } = useNavTreeContext();
106
- const { id, testId } = getProps?.(item, path) ?? {};
107
- const type = l0ItemType(item);
108
- const itemPath = useMemo(() => [...path, item.id], [item.id, path]);
109
-
110
- const { t } = useTranslation(NAVTREE_PLUGIN);
111
- const localizedString = toLocalizedString(item.properties.label, t);
112
-
113
- const handleClick = useL0ItemClick({ item, parent, path: itemPath }, type);
114
- const rootProps =
115
- type === 'tab'
116
- ? { value: item.id, tabIndex: 0, onClick: handleClick, 'data-testid': testId, 'data-itemid': id }
117
- : type !== 'collection'
118
- ? { onClick: handleClick, 'data-testid': testId, 'data-itemid': id }
119
- : { onClick: handleClick };
120
-
121
- const Root = type === 'collection' ? 'h2' : type === 'tab' ? Tabs.TabPrimitive : 'button';
122
-
123
- return (
124
- <Tooltip.Trigger asChild delayDuration={0} side='right' content={localizedString}>
125
- <Root
126
- {...(rootProps as any)}
127
- data-type={type}
128
- className={mx(
129
- 'group/l0item flex w-full justify-center items-center relative data[type!="collection"]:cursor-pointer app-no-drag dx-focus-ring-group',
130
- l0Breakpoints[item.properties.l0Breakpoint],
131
- )}
132
- ref={forwardedRef}
133
- >
134
- {children}
135
- </Root>
136
- </Tooltip.Trigger>
137
- );
138
- },
139
- );
140
-
141
- const L0Avator = ({ item }: Pick<L0ItemProps, 'item'>) => {
99
+ const L0Item = ({ item, parent, path, pinned, userAccount, onRearrange }: L0ItemProps) => {
142
100
  const { t } = useTranslation(NAVTREE_PLUGIN);
101
+ const itemElement = useRef<HTMLElement | null>(null);
102
+ const [closestEdge, setEdge] = useState<Edge | null>(null);
143
103
  const type = l0ItemType(item);
144
- const hue = item.properties.hue ?? null;
145
- const hueFgStyle = hue && { style: { color: `var(--dx-${hue}SurfaceText)` } };
104
+ const itemPath = useMemo(() => [...path, item.id], [item.id, path]);
105
+ const { getProps, popoverAnchorId } = useNavTreeContext();
106
+ const { id, testId } = getProps?.(item, path) ?? {};
107
+ const Root = type === 'collection' ? 'h2' : type === 'tab' ? Tabs.TabPrimitive : 'button';
108
+ const handleClick = useL0ItemClick({ item, path: itemPath, parent }, type);
109
+ const rootProps =
110
+ type === 'tab'
111
+ ? { value: item.id, tabIndex: 0, onClick: handleClick, 'data-testid': testId, 'data-itemid': id }
112
+ : type !== 'collection'
113
+ ? { onClick: handleClick, 'data-testid': testId, 'data-itemid': id }
114
+ : { onClick: handleClick };
146
115
  const localizedString = toLocalizedString(item.properties.label, t);
116
+ const hue = item.properties.hue ?? null;
147
117
  const avatarValue = useMemo(
148
118
  () => (type === 'tab' ? getFirstTwoRenderableChars(localizedString).join('').toUpperCase() : []),
149
119
  [type, item.properties.label, t],
150
120
  );
151
-
152
- return (
153
- <span role='img' className='place-self-center text-xl font-light' {...hueFgStyle}>
154
- {avatarValue}
155
- </span>
156
- );
157
- };
158
-
159
- // TODO(burdon): Factor out pinned (non-draggable) items.
160
- const L0Item = ({ item, parent, path, pinned, onRearrange }: L0ItemProps) => {
161
- const { t } = useTranslation(NAVTREE_PLUGIN);
162
- const itemElement = useRef<HTMLElement | null>(null);
163
- const [closestEdge, setEdge] = useState<Edge | null>(null);
164
- const type = l0ItemType(item);
165
- const localizedString = toLocalizedString(item.properties.label, t);
166
- const hue = item.properties.hue ?? null;
167
121
  const hueFgStyle = hue && { style: { color: `var(--dx-${hue}SurfaceText)` } };
168
122
 
169
123
  useLayoutEffect(() => {
@@ -171,7 +125,6 @@ const L0Item = ({ item, parent, path, pinned, onRearrange }: L0ItemProps) => {
171
125
  if (!itemElement.current || !onRearrange) {
172
126
  return;
173
127
  }
174
-
175
128
  return combine(
176
129
  draggable({
177
130
  element: itemElement.current,
@@ -222,43 +175,80 @@ const L0Item = ({ item, parent, path, pinned, onRearrange }: L0ItemProps) => {
222
175
  );
223
176
  }, [item, onRearrange]);
224
177
 
225
- return (
226
- <L0ItemRoot ref={itemElement} item={item} parent={parent} path={path}>
227
- <div
228
- role='none'
229
- data-frame={true}
230
- className={mx(
231
- 'flex justify-center items-center dx-focus-ring-group-indicator transition-colors rounded',
232
- // TODO(burdon): Create reusable button/component and/or create var for size.
233
- pinned
234
- ? 'bg-transparent w-[50px] p-2 group-hover/l0item:bg-groupSurface'
235
- : 'bg-groupSurface w-[50px] h-[50px]',
236
- )}
237
- {...(hue && { style: { background: `var(--dx-${hue}Surface)` } })}
238
- >
239
- {(item.properties.icon && <Icon icon={item.properties.icon} size={pinned ? 5 : 6} {...hueFgStyle} />) ||
240
- (type === 'tab' && item.properties.disposition !== 'pin-end' && <L0Avator item={item} />)}
241
- </div>
178
+ const l0ItemTrigger = (
179
+ <Root
180
+ {...(rootProps as any)}
181
+ data-type={type}
182
+ className={mx(
183
+ 'group/l0i dx-focus-ring-group grid relative data[type!="collection"]:cursor-pointer app-no-drag',
184
+ l0Breakpoints[item.properties.l0Breakpoint],
185
+ )}
186
+ ref={itemElement}
187
+ >
188
+ {userAccount ? (
189
+ <UserAccountAvatar
190
+ userId={item.properties.userId}
191
+ hue={item.properties.hue}
192
+ emoji={item.properties.emoji}
193
+ status={item.properties.status}
194
+ />
195
+ ) : (
196
+ <div
197
+ role='none'
198
+ data-frame={true}
199
+ className={mx(
200
+ 'absolute grid dx-focus-ring-group-indicator transition-colors',
201
+ type === 'tab' || pinned ? 'rounded' : 'rounded-full',
202
+ pinned
203
+ ? 'bg-transparent group-hover/l0i:bg-groupSurface inset-inline-3 inset-block-0.5'
204
+ : 'bg-groupSurface inset-inline-3 inset-block-2',
205
+ )}
206
+ {...(hue && { style: { background: `var(--dx-${hue}Surface)` } })}
207
+ >
208
+ {(item.properties.icon && (
209
+ <Icon icon={item.properties.icon} size={pinned ? 5 : 7} classNames='place-self-center' {...hueFgStyle} />
210
+ )) ||
211
+ (type === 'tab' && item.properties.disposition !== 'pin-end' ? (
212
+ <span role='img' className='place-self-center text-3xl font-light' {...hueFgStyle}>
213
+ {avatarValue}
214
+ </span>
215
+ ) : (
216
+ item.properties.icon && (
217
+ <Icon icon='ph--planet--regular' size={pinned ? 5 : 7} classNames='place-self-center' {...hueFgStyle} />
218
+ )
219
+ ))}
220
+ </div>
221
+ )}
242
222
  <div
243
223
  role='none'
244
- className='hidden group-aria-selected/l0item:block absolute inline-start-0 inset-block-4 is-1 bg-accentSurface rounded-ie'
224
+ className='hidden group-aria-selected/l0i:block absolute inline-start-0 inset-block-4 is-1 bg-accentSurface rounded-ie'
245
225
  />
246
226
  <span id={`${item.id}__label`} className='sr-only'>
247
227
  {localizedString}
248
228
  </span>
249
229
  {onRearrange && closestEdge && <ListItem.DropIndicator edge={closestEdge} />}
250
- </L0ItemRoot>
230
+ </Root>
251
231
  );
252
- };
253
232
 
254
- //
255
- // L0Collection
256
- //
233
+ return popoverAnchorId === id ? (
234
+ <Popover.Anchor asChild>{l0ItemTrigger}</Popover.Anchor>
235
+ ) : (
236
+ <Tooltip.Root delayDuration={0}>
237
+ <Tooltip.Trigger asChild>{l0ItemTrigger}</Tooltip.Trigger>
238
+ <Tooltip.Portal>
239
+ <Tooltip.Content side='right'>
240
+ {localizedString}
241
+ <Tooltip.Arrow />
242
+ </Tooltip.Content>
243
+ </Tooltip.Portal>
244
+ </Tooltip.Root>
245
+ );
246
+ };
257
247
 
258
- const L0Collection = ({ item, path }: L0ItemProps) => {
248
+ const L0Collection = ({ item, path, parent }: L0ItemProps) => {
259
249
  const navTreeContext = useNavTreeContext();
260
250
  useLoadDescendents(item);
261
- const collectionItems = navTreeContext.useItems(item);
251
+ const collectionItems = navTreeContext.getItems(item);
262
252
  const groupPath = useMemo(() => [...path, item.id], [item.id, path]);
263
253
  const { id, testId } = navTreeContext.getProps?.(item, path) ?? {};
264
254
  const handleRearrange = useCallback<StackItemRearrangeHandler<L0ItemData>>(
@@ -276,7 +266,6 @@ const L0Collection = ({ item, path }: L0ItemProps) => {
276
266
  },
277
267
  [collectionItems, item.properties.onRearrangeChildren],
278
268
  );
279
-
280
269
  return (
281
270
  <div
282
271
  role='group'
@@ -298,61 +287,26 @@ const L0Collection = ({ item, path }: L0ItemProps) => {
298
287
  );
299
288
  };
300
289
 
301
- //
302
- // L0Menu
303
- //
304
-
305
- export type L0MenuProps = {
306
- menuActions: MenuItem[];
290
+ export const L0Menu = ({
291
+ topLevelItems,
292
+ pinnedItems,
293
+ userAccountItem,
294
+ parent,
295
+ path,
296
+ }: {
307
297
  topLevelItems: Node<any>[];
308
298
  pinnedItems: Node<any>[];
309
299
  userAccountItem: Node<any>;
310
300
  parent?: Node<any>;
311
301
  path: string[];
312
- };
313
-
314
- export const L0Menu = ({ menuActions, topLevelItems, pinnedItems, userAccountItem, parent, path }: L0MenuProps) => {
315
- const { t } = useTranslation(NAVTREE_PLUGIN);
316
- const suppressNextTooltip = useRef(true);
317
-
302
+ }) => {
318
303
  return (
319
- <Tabs.Tablist
320
- classNames={[
321
- 'group/l0 absolute z-[1] inset-block-0 inline-start-0 rounded-is-lg',
322
- 'grid grid-cols-[var(--l0-size)] grid-rows-[var(--rail-size)_1fr_min-content_var(--l0-size)] gap-1 contain-layout',
323
- '!is-[--l0-size] bg-baseSurface border-ie border-separator app-drag pbe-[env(safe-area-inset-bottom)]',
324
- ]}
325
- >
326
- <div role='none' className='flex justify-center p-1'>
327
- <MenuProvider>
328
- <DropdownMenu.Root group={parent} items={menuActions}>
329
- <DropdownMenu.Trigger asChild>
330
- {/* TODO(wittjosiah): Use L0Item trigger. */}
331
- {/* TODO(burdon): Replace with Sigil. */}
332
- <IconButton
333
- iconOnly
334
- icon='ph--dots-three--regular'
335
- size={5}
336
- label={t('app menu label')}
337
- tooltipSide='right'
338
- classNames='w-[50px] _bg-primary-500'
339
- suppressNextTooltip={suppressNextTooltip}
340
- data-testid='spacePlugin.addSpace'
341
- />
342
- </DropdownMenu.Trigger>
343
- </DropdownMenu.Root>
344
- </MenuProvider>
345
- </div>
346
-
304
+ <Tabs.Tablist classNames='group/l0 absolute z-[1] inset-block-0 inline-start-0 rounded-is-lg grid grid-cols-[var(--l0-size)] grid-rows-[1fr_min-content_var(--l0-size)] contain-layout !is-[--l0-size] bg-baseSurface border-ie border-separator app-drag pbe-[env(safe-area-inset-bottom)]'>
347
305
  <ScrollArea.Root>
348
306
  <ScrollArea.Viewport>
349
307
  <div
350
308
  role='none'
351
- className={mx([
352
- 'flex flex-col gap-2 pbs-1',
353
- '[body[data-platform="darwin"]_&]:pbs-[calc(30px+0.25rem)]',
354
- '[body[data-platform="ios"]_&]:pbs-[max(env(safe-area-inset-top),0.25rem)]',
355
- ])}
309
+ className='grid auto-rows-[calc(var(--l0-size)-.5rem)] plb-1 [body[data-platform="darwin"]_&]:pbs-[calc(30px+0.25rem)] [body[data-platform="ios"]_&]:pbs-[max(env(safe-area-inset-top),0.25rem)]'
356
310
  >
357
311
  {topLevelItems.map((item) => {
358
312
  if (l0ItemType(item) === 'collection') {
@@ -367,7 +321,6 @@ export const L0Menu = ({ menuActions, topLevelItems, pinnedItems, userAccountIte
367
321
  </ScrollArea.Scrollbar>
368
322
  </ScrollArea.Viewport>
369
323
  </ScrollArea.Root>
370
-
371
324
  <div role='none' className='grid grid-cols-1 auto-rows-[--rail-action] pbs-2'>
372
325
  {pinnedItems
373
326
  .filter((item) => l0ItemType(item) !== 'collection')
@@ -375,27 +328,17 @@ export const L0Menu = ({ menuActions, topLevelItems, pinnedItems, userAccountIte
375
328
  <L0Item key={item.id} item={item} parent={parent} path={path} pinned />
376
329
  ))}
377
330
  </div>
378
-
379
331
  {userAccountItem && (
380
- <div role='none' className='grid app-no-drag'>
381
- <L0ItemRoot key={userAccountItem.id} item={userAccountItem} parent={parent} path={path}>
382
- <UserAccountAvatar
383
- userId={userAccountItem.properties.userId}
384
- hue={userAccountItem.properties.hue}
385
- emoji={userAccountItem.properties.emoji}
386
- status={userAccountItem.properties.status}
387
- size={11}
388
- />
389
- </L0ItemRoot>
332
+ <div role='none' className='grid py-2 app-no-drag'>
333
+ <L0Item key={userAccountItem.id} item={userAccountItem} parent={parent} path={path} userAccount />
390
334
  </div>
391
335
  )}
392
-
393
336
  <div
394
337
  role='none'
395
338
  className='hidden [body[data-platform="darwin"]_&]:block absolute block-start-0 is-[calc(var(--l0-size)-1px)] bs-[calc(40px+0.25rem)]'
396
339
  style={{
397
340
  background:
398
- 'linear-gradient(to bottom, var(--dx-sidebarSurface) 0%, var(--dx-sidebarSurface) 70%, transparent 100%)',
341
+ 'linear-gradient(to bottom, var(--dx-baseSurface) 0%, var(--dx-baseSurface) 70%, transparent 100%)',
399
342
  }}
400
343
  />
401
344
  </Tabs.Tablist>
@@ -0,0 +1,115 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useMemo } from 'react';
6
+
7
+ import { useLayout } from '@dxos/app-framework';
8
+ import { type Node } from '@dxos/app-graph';
9
+ import { Button, Icon, toLocalizedString, useTranslation } from '@dxos/react-ui';
10
+ import { Tree } from '@dxos/react-ui-list';
11
+ import { Tabs } from '@dxos/react-ui-tabs';
12
+ import { hoverableControlItem, hoverableOpenControlItem, mx } from '@dxos/react-ui-theme';
13
+
14
+ import { useNavTreeContext } from './NavTreeContext';
15
+ import { NavTreeItemColumns } from './NavTreeItemColumns';
16
+ import { useLoadDescendents } from '../hooks';
17
+ import { NAVTREE_PLUGIN } from '../meta';
18
+ import { l0ItemType } from '../util';
19
+
20
+ export type L1PanelProps = { item: Node<any>; path: string[]; currentItemId: string; onBack?: () => void };
21
+
22
+ const L1Panel = ({ item, path, currentItemId, onBack }: L1PanelProps) => {
23
+ const layout = useLayout();
24
+ const navTreeContext = useNavTreeContext();
25
+ const { t } = useTranslation(NAVTREE_PLUGIN);
26
+ return (
27
+ <Tabs.Tabpanel
28
+ key={item.id}
29
+ value={item.id}
30
+ classNames={[
31
+ 'absolute inset-block-0 inline-end-0 is-[calc(100%-var(--l0-size))] lg:is-[--l1-size] grid-cols-1 grid-rows-[var(--rail-size)_1fr] pbs-[env(safe-area-inset-top)]',
32
+ item.id === currentItemId && 'grid',
33
+ ]}
34
+ tabIndex={-1}
35
+ {...(!layout.sidebarOpen && { inert: 'true' })}
36
+ >
37
+ {item.id === currentItemId && (
38
+ <>
39
+ <h2 className='flex items-center border-be border-separator app-drag'>
40
+ <Button
41
+ variant='ghost'
42
+ density='fine'
43
+ classNames={mx(
44
+ 'is-6 pli-0 dx-focus-ring-inset',
45
+ !item.id.startsWith('!') && 'invisible',
46
+ hoverableControlItem,
47
+ hoverableOpenControlItem,
48
+ )}
49
+ onClick={onBack}
50
+ >
51
+ <Icon icon='ph--caret-left--regular' size={3} />
52
+ </Button>
53
+ <span className='flex-1 truncate cursor-default'>{toLocalizedString(item.properties.label, t)}</span>
54
+ <NavTreeItemColumns path={path} item={item} open />
55
+ </h2>
56
+ <div role='none' className='overflow-y-auto'>
57
+ <Tree
58
+ {...navTreeContext}
59
+ id={item.id}
60
+ root={item}
61
+ path={path}
62
+ levelOffset={5}
63
+ draggable
64
+ gridTemplateColumns='[tree-row-start] 1fr min-content min-content min-content [tree-row-end]'
65
+ renderColumns={NavTreeItemColumns}
66
+ />
67
+ </div>
68
+ </>
69
+ )}
70
+ </Tabs.Tabpanel>
71
+ );
72
+ };
73
+
74
+ const L1PanelCollection = ({ item, path, currentItemId }: L1PanelProps) => {
75
+ const { getItems } = useNavTreeContext();
76
+ useLoadDescendents(item);
77
+ const collectionItems = getItems(item);
78
+ const groupPath = useMemo(() => [...path, item.id], [item.id, path]);
79
+ return (
80
+ <>
81
+ {collectionItems
82
+ .filter((item) => l0ItemType(item) === 'tab')
83
+ .map((item) => (
84
+ <L1Panel key={item.id} item={item} path={groupPath} currentItemId={currentItemId} />
85
+ ))}
86
+ </>
87
+ );
88
+ };
89
+
90
+ export const L1Panels = ({
91
+ topLevelItems,
92
+ path,
93
+ currentItemId,
94
+ onBack,
95
+ }: Pick<L1PanelProps, 'onBack'> & {
96
+ topLevelItems: Node<any>[];
97
+ path: string[];
98
+ currentItemId: string;
99
+ }) => {
100
+ return (
101
+ <>
102
+ {topLevelItems.map((item) => {
103
+ const type = l0ItemType(item);
104
+ switch (type) {
105
+ case 'collection':
106
+ return <L1PanelCollection key={item.id} item={item} path={path} currentItemId={currentItemId} />;
107
+ case 'tab':
108
+ return <L1Panel key={item.id} item={item} path={path} currentItemId={currentItemId} onBack={onBack} />;
109
+ default:
110
+ return null;
111
+ }
112
+ })}
113
+ </>
114
+ );
115
+ };
@@ -0,0 +1,107 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
8
+ import { extractInstruction, type Instruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
9
+ import { type Meta } from '@storybook/react';
10
+ import React, { useEffect } from 'react';
11
+
12
+ import { isActionLike, type NodeArg } from '@dxos/app-graph';
13
+ import { create, type ReactiveObject } from '@dxos/live-object';
14
+ import { faker } from '@dxos/random';
15
+ import { isTreeData, type TreeData } from '@dxos/react-ui-list';
16
+ import { Path } from '@dxos/react-ui-list';
17
+ import { withLayout, withTheme } from '@dxos/storybook-utils';
18
+
19
+ import { StorybookNavTree } from '../stories/StorybookNavTree';
20
+ import { createTree, updateState } from '../testing';
21
+
22
+ faker.seed(1234);
23
+
24
+ const tree = create<NodeArg<any>>(createTree());
25
+ const state = new Map<string, ReactiveObject<{ open: boolean; current: boolean }>>();
26
+
27
+ const meta: Meta<typeof StorybookNavTree> = {
28
+ title: 'plugins/plugin-navtree/NavTree',
29
+ component: StorybookNavTree,
30
+ decorators: [withTheme, withLayout({ tooltips: true })],
31
+ render: (args) => {
32
+ useEffect(() => {
33
+ return monitorForElements({
34
+ canMonitor: ({ source }) => isTreeData(source),
35
+ onDrop: ({ location, source }) => {
36
+ // Didn't drop on anything.
37
+ if (!location.current.dropTargets.length) {
38
+ return;
39
+ }
40
+
41
+ const target = location.current.dropTargets[0];
42
+ const instruction: Instruction | null = extractInstruction(target.data);
43
+ if (instruction !== null) {
44
+ updateState({
45
+ state: tree,
46
+ instruction,
47
+ source: source.data as TreeData,
48
+ target: target.data as TreeData,
49
+ });
50
+ }
51
+ },
52
+ });
53
+ }, []);
54
+
55
+ return <StorybookNavTree {...args} />;
56
+ },
57
+ args: {
58
+ id: tree.id,
59
+ getActions: (arg: NodeArg<any>) => ({
60
+ actions: arg.nodes?.filter((node) => isActionLike(node)) ?? [],
61
+ groupedActions: {},
62
+ }),
63
+ getItems: (testItem?: any) => {
64
+ return testItem?.nodes ?? tree.nodes ?? [];
65
+ },
66
+ getProps: (testItem: NodeArg<any>) => ({
67
+ id: testItem.id,
68
+ label: testItem.properties?.label ?? testItem.id,
69
+ icon: testItem.properties?.icon,
70
+ ...((testItem.nodes?.length ?? 0) > 0 && {
71
+ parentOf: testItem.nodes!.map(({ id }) => id),
72
+ }),
73
+ }),
74
+ isOpen: (_path: string[]) => {
75
+ const path = Path.create(..._path);
76
+ const value = state.get(path) ?? create({ open: false, current: false });
77
+ if (!state.has(path)) {
78
+ state.set(path, value);
79
+ }
80
+
81
+ return value.open;
82
+ },
83
+ isCurrent: (_path: string[]) => {
84
+ const path = Path.create(..._path);
85
+ const value = state.get(path) ?? create({ open: false, current: false });
86
+ if (!state.has(path)) {
87
+ state.set(path, value);
88
+ }
89
+
90
+ return value.current;
91
+ },
92
+ onOpenChange: ({ path: _path, open }) => {
93
+ const path = Path.create(..._path);
94
+ const object = state.get(path);
95
+ object!.open = open;
96
+ },
97
+ onSelect: ({ path: _path, current }) => {
98
+ const path = Path.create(..._path);
99
+ const object = state.get(path);
100
+ object!.current = current;
101
+ },
102
+ },
103
+ };
104
+
105
+ export default meta;
106
+
107
+ export const Default = {};