@dxos/plugin-navtree 0.8.2-main.f11618f → 0.8.2-staging.42af850

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 (141) hide show
  1. package/dist/lib/browser/{app-graph-builder-26D3MUCA.mjs → app-graph-builder-P7AY3K72.mjs} +7 -6
  2. package/dist/lib/browser/app-graph-builder-P7AY3K72.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-TLQLMZXU.mjs → chunk-33VLL4IJ.mjs} +2 -2
  4. package/dist/lib/browser/{chunk-TLQLMZXU.mjs.map → chunk-33VLL4IJ.mjs.map} +1 -1
  5. package/dist/lib/browser/chunk-MFAWMFJC.mjs +1418 -0
  6. package/dist/lib/browser/chunk-MFAWMFJC.mjs.map +7 -0
  7. package/dist/lib/browser/index.mjs +6 -12
  8. package/dist/lib/browser/index.mjs.map +3 -3
  9. package/dist/lib/browser/{intent-resolver-OBGGMBVJ.mjs → intent-resolver-NAYKKAOP.mjs} +7 -7
  10. package/dist/lib/browser/intent-resolver-NAYKKAOP.mjs.map +7 -0
  11. package/dist/lib/browser/keyboard-DDFATRUG.mjs +51 -0
  12. package/dist/lib/browser/keyboard-DDFATRUG.mjs.map +7 -0
  13. package/dist/lib/browser/meta.json +1 -1
  14. package/dist/lib/browser/{react-surface-DWMOF2TM.mjs → react-surface-HGA3TAQU.mjs} +3 -3
  15. package/dist/lib/browser/{state-EW62QYEN.mjs → state-R2UMYAHN.mjs} +3 -3
  16. package/dist/lib/browser/state-R2UMYAHN.mjs.map +7 -0
  17. package/dist/types/src/capabilities/app-graph-builder.d.ts +2 -179
  18. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  19. package/dist/types/src/capabilities/index.d.ts +4 -180
  20. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  21. package/dist/types/src/capabilities/intent-resolver.d.ts +2 -2
  22. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -1
  23. package/dist/types/src/capabilities/keyboard.d.ts +2 -2
  24. package/dist/types/src/capabilities/keyboard.d.ts.map +1 -1
  25. package/dist/types/src/capabilities/state.d.ts +2 -2
  26. package/dist/types/src/capabilities/state.d.ts.map +1 -1
  27. package/dist/types/src/components/CommandsDialog/CommandsDialog.d.ts.map +1 -0
  28. package/dist/types/src/components/CommandsDialog/CommandsTrigger.d.ts.map +1 -0
  29. package/dist/types/src/components/CommandsDialog/index.d.ts +3 -0
  30. package/dist/types/src/components/CommandsDialog/index.d.ts.map +1 -0
  31. package/dist/types/src/components/NavTree/NavTree.d.ts +8 -0
  32. package/dist/types/src/components/NavTree/NavTree.d.ts.map +1 -0
  33. package/dist/types/src/components/NavTree/NavTree.stories.d.ts +8 -0
  34. package/dist/types/src/components/NavTree/NavTree.stories.d.ts.map +1 -0
  35. package/dist/types/src/components/NavTree/index.d.ts +2 -0
  36. package/dist/types/src/components/NavTree/index.d.ts.map +1 -0
  37. package/dist/types/src/components/NavTreeContainer.d.ts.map +1 -1
  38. package/dist/types/src/components/NavTreeContext.d.ts.map +1 -1
  39. package/dist/types/src/components/NavTreeDocumentTitle.d.ts.map +1 -1
  40. package/dist/types/src/components/{NavTreeItemAction.d.ts → NavTreeItem/NavTreeItemAction.d.ts} +3 -5
  41. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.d.ts.map +1 -0
  42. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts +8 -0
  43. package/dist/types/src/components/NavTreeItem/NavTreeItemAction.stories.d.ts.map +1 -0
  44. package/dist/types/src/components/{NavTreeItemColumns.d.ts → NavTreeItem/NavTreeItemColumns.d.ts} +1 -1
  45. package/dist/types/src/components/NavTreeItem/NavTreeItemColumns.d.ts.map +1 -0
  46. package/dist/types/src/components/NavTreeItem/index.d.ts +3 -0
  47. package/dist/types/src/components/NavTreeItem/index.d.ts.map +1 -0
  48. package/dist/types/src/components/Sidebar/L0Menu.d.ts +13 -0
  49. package/dist/types/src/components/Sidebar/L0Menu.d.ts.map +1 -0
  50. package/dist/types/src/components/Sidebar/L1Panel.d.ts +15 -0
  51. package/dist/types/src/components/Sidebar/L1Panel.d.ts.map +1 -0
  52. package/dist/types/src/components/Sidebar/index.d.ts +3 -0
  53. package/dist/types/src/components/Sidebar/index.d.ts.map +1 -0
  54. package/dist/types/src/components/{UserAccountAvatar.d.ts → UserAccountAvatar/UserAccountAvatar.d.ts} +1 -1
  55. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.d.ts.map +1 -0
  56. package/dist/types/src/components/{UserAccountAvatar.stories.d.ts → UserAccountAvatar/UserAccountAvatar.stories.d.ts} +1 -2
  57. package/dist/types/src/components/UserAccountAvatar/UserAccountAvatar.stories.d.ts.map +1 -0
  58. package/dist/types/src/components/UserAccountAvatar/index.d.ts +2 -0
  59. package/dist/types/src/components/UserAccountAvatar/index.d.ts.map +1 -0
  60. package/dist/types/src/components/index.d.ts +0 -1
  61. package/dist/types/src/components/index.d.ts.map +1 -1
  62. package/dist/types/src/components/types.d.ts +17 -15
  63. package/dist/types/src/components/types.d.ts.map +1 -1
  64. package/dist/types/src/experimental/Tree.d.ts.map +1 -1
  65. package/dist/types/src/hooks.d.ts.map +1 -1
  66. package/dist/types/src/testing/app-graph-builder.d.ts +3 -0
  67. package/dist/types/src/testing/app-graph-builder.d.ts.map +1 -0
  68. package/dist/types/src/testing/generator.d.ts +5 -4
  69. package/dist/types/src/testing/generator.d.ts.map +1 -1
  70. package/dist/types/src/testing/index.d.ts +1 -0
  71. package/dist/types/src/testing/index.d.ts.map +1 -1
  72. package/dist/types/src/testing/update-state.d.ts.map +1 -1
  73. package/dist/types/src/translations.d.ts +1 -1
  74. package/dist/types/src/util.d.ts +7 -10
  75. package/dist/types/src/util.d.ts.map +1 -1
  76. package/dist/types/tsconfig.tsbuildinfo +1 -1
  77. package/package.json +42 -32
  78. package/src/NavTreePlugin.tsx +2 -2
  79. package/src/capabilities/app-graph-builder.ts +38 -28
  80. package/src/capabilities/intent-resolver.ts +8 -7
  81. package/src/capabilities/keyboard.ts +37 -32
  82. package/src/capabilities/state.ts +3 -3
  83. package/src/components/{CommandsDialog.tsx → CommandsDialog/CommandsDialog.tsx} +8 -7
  84. package/src/components/{CommandsTrigger.tsx → CommandsDialog/CommandsTrigger.tsx} +1 -1
  85. package/src/components/CommandsDialog/index.ts +6 -0
  86. package/src/components/NavTree/NavTree.stories.tsx +159 -0
  87. package/src/components/NavTree/NavTree.tsx +62 -0
  88. package/src/components/NavTree/index.ts +5 -0
  89. package/src/components/NavTreeContainer.tsx +76 -72
  90. package/src/components/NavTreeContext.tsx +1 -1
  91. package/src/components/{NavTreeItemAction.stories.tsx → NavTreeItem/NavTreeItemAction.stories.tsx} +12 -6
  92. package/src/components/{NavTreeItemAction.tsx → NavTreeItem/NavTreeItemAction.tsx} +9 -18
  93. package/src/components/{NavTreeItemColumns.tsx → NavTreeItem/NavTreeItemColumns.tsx} +5 -5
  94. package/src/components/NavTreeItem/index.ts +6 -0
  95. package/src/components/{L0Menu.tsx → Sidebar/L0Menu.tsx} +186 -131
  96. package/src/components/{L1Panels.tsx → Sidebar/L1Panel.tsx} +40 -65
  97. package/src/components/Sidebar/index.ts +6 -0
  98. package/src/components/{UserAccountAvatar.stories.tsx → UserAccountAvatar/UserAccountAvatar.stories.tsx} +5 -5
  99. package/src/components/{UserAccountAvatar.tsx → UserAccountAvatar/UserAccountAvatar.tsx} +6 -6
  100. package/src/components/UserAccountAvatar/index.ts +5 -0
  101. package/src/components/index.ts +0 -1
  102. package/src/components/types.ts +17 -16
  103. package/src/testing/app-graph-builder.ts +224 -0
  104. package/src/testing/generator.ts +12 -9
  105. package/src/testing/index.ts +1 -0
  106. package/src/translations.ts +1 -1
  107. package/src/util.ts +10 -32
  108. package/dist/lib/browser/app-graph-builder-26D3MUCA.mjs.map +0 -7
  109. package/dist/lib/browser/chunk-FHOAKMRS.mjs +0 -1309
  110. package/dist/lib/browser/chunk-FHOAKMRS.mjs.map +0 -7
  111. package/dist/lib/browser/intent-resolver-OBGGMBVJ.mjs.map +0 -7
  112. package/dist/lib/browser/keyboard-IQXHKUWM.mjs +0 -46
  113. package/dist/lib/browser/keyboard-IQXHKUWM.mjs.map +0 -7
  114. package/dist/lib/browser/state-EW62QYEN.mjs.map +0 -7
  115. package/dist/types/src/components/CommandsDialog.d.ts.map +0 -1
  116. package/dist/types/src/components/CommandsTrigger.d.ts.map +0 -1
  117. package/dist/types/src/components/L0Menu.d.ts +0 -10
  118. package/dist/types/src/components/L0Menu.d.ts.map +0 -1
  119. package/dist/types/src/components/L1Panels.d.ts +0 -14
  120. package/dist/types/src/components/L1Panels.d.ts.map +0 -1
  121. package/dist/types/src/components/NavTree.d.ts +0 -5
  122. package/dist/types/src/components/NavTree.d.ts.map +0 -1
  123. package/dist/types/src/components/NavTree.stories.d.ts +0 -7
  124. package/dist/types/src/components/NavTree.stories.d.ts.map +0 -1
  125. package/dist/types/src/components/NavTreeItemAction.d.ts.map +0 -1
  126. package/dist/types/src/components/NavTreeItemAction.stories.d.ts +0 -41
  127. package/dist/types/src/components/NavTreeItemAction.stories.d.ts.map +0 -1
  128. package/dist/types/src/components/NavTreeItemColumns.d.ts.map +0 -1
  129. package/dist/types/src/components/UserAccountAvatar.d.ts.map +0 -1
  130. package/dist/types/src/components/UserAccountAvatar.stories.d.ts.map +0 -1
  131. package/dist/types/src/stories/Graph.stories.d.ts +0 -7
  132. package/dist/types/src/stories/Graph.stories.d.ts.map +0 -1
  133. package/dist/types/src/stories/StorybookNavTree.d.ts +0 -4
  134. package/dist/types/src/stories/StorybookNavTree.d.ts.map +0 -1
  135. package/src/components/NavTree.stories.tsx +0 -107
  136. package/src/components/NavTree.tsx +0 -51
  137. package/src/stories/Graph.stories.tsx +0 -130
  138. package/src/stories/StorybookNavTree.tsx +0 -63
  139. /package/dist/lib/browser/{react-surface-DWMOF2TM.mjs.map → react-surface-HGA3TAQU.mjs.map} +0 -0
  140. /package/dist/types/src/components/{CommandsDialog.d.ts → CommandsDialog/CommandsDialog.d.ts} +0 -0
  141. /package/dist/types/src/components/{CommandsTrigger.d.ts → CommandsDialog/CommandsTrigger.d.ts} +0 -0
@@ -7,88 +7,92 @@ 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
- attachClosestEdge,
11
10
  type Edge,
11
+ attachClosestEdge,
12
12
  extractClosestEdge,
13
13
  } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
14
- import React, { type MouseEvent, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
14
+ import React, {
15
+ forwardRef,
16
+ type MouseEvent,
17
+ type PropsWithChildren,
18
+ useCallback,
19
+ useLayoutEffect,
20
+ useMemo,
21
+ useRef,
22
+ useState,
23
+ } from 'react';
15
24
 
16
- import { createIntent, LayoutAction, useIntentDispatcher } from '@dxos/app-framework';
17
25
  import { type Node } from '@dxos/app-graph';
18
26
  import { invariant } from '@dxos/invariant';
19
27
  import {
20
- Popover,
28
+ Icon,
29
+ IconButton,
30
+ ListItem,
21
31
  ScrollArea,
22
- toLocalizedString,
23
32
  Tooltip,
33
+ toLocalizedString,
24
34
  useMediaQuery,
25
- useSidebars,
26
35
  useTranslation,
27
- ListItem,
28
- Icon,
29
36
  } from '@dxos/react-ui';
37
+ import { MenuProvider, DropdownMenu, type MenuItem } from '@dxos/react-ui-menu';
30
38
  import type { StackItemRearrangeHandler } from '@dxos/react-ui-stack';
31
39
  import { Tabs } from '@dxos/react-ui-tabs';
32
40
  import { mx } from '@dxos/react-ui-theme';
33
41
  import { arrayMove, getFirstTwoRenderableChars } from '@dxos/util';
34
42
 
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';
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
+ };
40
57
 
41
- type L0ItemData = { id: L0ItemProps['item']['id']; type: 'l0Item' };
58
+ type L0ItemRootProps = {
59
+ item: Node<any>;
60
+ parent?: Node<any>;
61
+ path: string[];
62
+ };
42
63
 
43
- type L0ItemProps = {
64
+ type L0ItemProps = L0ItemRootProps & {
44
65
  item: Node<any>;
45
66
  parent?: Node<any>;
46
67
  path: string[];
47
68
  pinned?: boolean;
48
- userAccount?: boolean;
49
69
  onRearrange?: StackItemRearrangeHandler<L0ItemData>;
50
70
  };
51
71
 
52
72
  const useL0ItemClick = ({ item, parent, path }: L0ItemProps, type: string) => {
53
- const { tab, onTabChange, onSelect, isCurrent } = useNavTreeContext();
54
- const { dispatchPromise: dispatch } = useIntentDispatcher();
55
- const { navigationSidebarState } = useSidebars(NAVTREE_PLUGIN);
73
+ const { tab, isCurrent, onSelect, onTabChange } = useNavTreeContext();
56
74
  const [isLg] = useMediaQuery('lg', { ssr: false });
57
75
 
58
76
  return useCallback(
59
77
  (event: MouseEvent) => {
60
78
  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 });
82
79
  case 'action': {
83
80
  const {
84
81
  data: invoke,
85
82
  properties: { caller },
86
83
  } = item;
84
+
87
85
  return invoke?.(caller ? { node: parent, caller } : { node: parent });
88
86
  }
87
+
88
+ case 'tab':
89
+ return onTabChange?.(item);
90
+
91
+ case 'link':
92
+ return onSelect?.({ item, path, current: !isCurrent(path, item), option: event.altKey });
89
93
  }
90
94
  },
91
- [item, type, onSelect, isCurrent, parent, tab, navigationSidebarState, isLg, dispatch],
95
+ [item, parent, type, tab, isCurrent, onSelect, onTabChange, isLg],
92
96
  );
93
97
  };
94
98
 
@@ -96,28 +100,70 @@ const l0Breakpoints: Record<string, string> = {
96
100
  lg: 'hidden lg:grid',
97
101
  };
98
102
 
99
- const L0Item = ({ item, parent, path, pinned, userAccount, onRearrange }: L0ItemProps) => {
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'>) => {
100
142
  const { t } = useTranslation(NAVTREE_PLUGIN);
101
- const itemElement = useRef<HTMLElement | null>(null);
102
- const [closestEdge, setEdge] = useState<Edge | null>(null);
103
143
  const type = l0ItemType(item);
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 };
115
- const localizedString = toLocalizedString(item.properties.label, t);
116
144
  const hue = item.properties.hue ?? null;
145
+ const hueFgStyle = hue && { style: { color: `var(--dx-${hue}SurfaceText)` } };
146
+ const localizedString = toLocalizedString(item.properties.label, t);
117
147
  const avatarValue = useMemo(
118
148
  () => (type === 'tab' ? getFirstTwoRenderableChars(localizedString).join('').toUpperCase() : []),
119
149
  [type, item.properties.label, t],
120
150
  );
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;
121
167
  const hueFgStyle = hue && { style: { color: `var(--dx-${hue}SurfaceText)` } };
122
168
 
123
169
  useLayoutEffect(() => {
@@ -125,6 +171,7 @@ const L0Item = ({ item, parent, path, pinned, userAccount, onRearrange }: L0Item
125
171
  if (!itemElement.current || !onRearrange) {
126
172
  return;
127
173
  }
174
+
128
175
  return combine(
129
176
  draggable({
130
177
  element: itemElement.current,
@@ -175,80 +222,43 @@ const L0Item = ({ item, parent, path, pinned, userAccount, onRearrange }: L0Item
175
222
  );
176
223
  }, [item, onRearrange]);
177
224
 
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
- )}
225
+ return (
226
+ <L0ItemRoot ref={itemElement} item={item} parent={parent} path={path}>
222
227
  <div
223
228
  role='none'
224
- className='hidden group-aria-selected/l0i:block absolute inline-start-0 inset-block-4 is-1 bg-accentSurface rounded-ie'
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-activeSurface'
235
+ : 'bg-activeSurface 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>
242
+ <div
243
+ role='none'
244
+ className='hidden group-aria-selected/l0item:block absolute inline-start-0 inset-block-4 is-1 bg-accentSurface rounded-ie'
225
245
  />
226
246
  <span id={`${item.id}__label`} className='sr-only'>
227
247
  {localizedString}
228
248
  </span>
229
249
  {onRearrange && closestEdge && <ListItem.DropIndicator edge={closestEdge} />}
230
- </Root>
231
- );
232
-
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>
250
+ </L0ItemRoot>
245
251
  );
246
252
  };
247
253
 
248
- const L0Collection = ({ item, path, parent }: L0ItemProps) => {
254
+ //
255
+ // L0Collection
256
+ //
257
+
258
+ const L0Collection = ({ item, path }: L0ItemProps) => {
249
259
  const navTreeContext = useNavTreeContext();
250
260
  useLoadDescendents(item);
251
- const collectionItems = navTreeContext.getItems(item);
261
+ const collectionItems = navTreeContext.useItems(item);
252
262
  const groupPath = useMemo(() => [...path, item.id], [item.id, path]);
253
263
  const { id, testId } = navTreeContext.getProps?.(item, path) ?? {};
254
264
  const handleRearrange = useCallback<StackItemRearrangeHandler<L0ItemData>>(
@@ -266,6 +276,7 @@ const L0Collection = ({ item, path, parent }: L0ItemProps) => {
266
276
  },
267
277
  [collectionItems, item.properties.onRearrangeChildren],
268
278
  );
279
+
269
280
  return (
270
281
  <div
271
282
  role='group'
@@ -287,26 +298,59 @@ const L0Collection = ({ item, path, parent }: L0ItemProps) => {
287
298
  );
288
299
  };
289
300
 
290
- export const L0Menu = ({
291
- topLevelItems,
292
- pinnedItems,
293
- userAccountItem,
294
- parent,
295
- path,
296
- }: {
301
+ //
302
+ // L0Menu
303
+ //
304
+
305
+ export type L0MenuProps = {
306
+ menuActions: MenuItem[];
297
307
  topLevelItems: Node<any>[];
298
308
  pinnedItems: Node<any>[];
299
309
  userAccountItem: Node<any>;
300
310
  parent?: Node<any>;
301
311
  path: string[];
302
- }) => {
312
+ };
313
+
314
+ export const L0Menu = ({ menuActions, topLevelItems, pinnedItems, userAccountItem, parent, path }: L0MenuProps) => {
315
+ const { t } = useTranslation(NAVTREE_PLUGIN);
316
+
303
317
  return (
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)]'>
318
+ <Tabs.Tablist
319
+ classNames={[
320
+ 'group/l0 absolute z-[1] inset-block-0 inline-start-0 rounded-is-lg',
321
+ 'grid grid-cols-[var(--l0-size)] grid-rows-[var(--rail-size)_1fr_min-content_var(--l0-size)] gap-1 contain-layout',
322
+ '!is-[--l0-size] bg-baseSurface border-ie border-subduedSeparator app-drag pbe-[env(safe-area-inset-bottom)]',
323
+ ]}
324
+ >
325
+ <div role='none' className='flex justify-center p-1'>
326
+ <MenuProvider>
327
+ <DropdownMenu.Root group={parent} items={menuActions}>
328
+ <DropdownMenu.Trigger asChild>
329
+ {/* TODO(wittjosiah): Use L0Item trigger. */}
330
+ {/* TODO(burdon): Replace with Sigil. */}
331
+ <IconButton
332
+ iconOnly
333
+ icon='ph--dots-three--regular'
334
+ size={5}
335
+ label={t('app menu label')}
336
+ tooltipSide='right'
337
+ classNames='w-[50px] _bg-primary-500'
338
+ data-testid='spacePlugin.addSpace'
339
+ />
340
+ </DropdownMenu.Trigger>
341
+ </DropdownMenu.Root>
342
+ </MenuProvider>
343
+ </div>
344
+
305
345
  <ScrollArea.Root>
306
346
  <ScrollArea.Viewport>
307
347
  <div
308
348
  role='none'
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)]'
349
+ className={mx([
350
+ 'flex flex-col gap-2 pbs-1',
351
+ '[body[data-platform="darwin"]_&]:pbs-[calc(30px+0.25rem)]',
352
+ '[body[data-platform="ios"]_&]:pbs-[max(env(safe-area-inset-top),0.25rem)]',
353
+ ])}
310
354
  >
311
355
  {topLevelItems.map((item) => {
312
356
  if (l0ItemType(item) === 'collection') {
@@ -321,6 +365,7 @@ export const L0Menu = ({
321
365
  </ScrollArea.Scrollbar>
322
366
  </ScrollArea.Viewport>
323
367
  </ScrollArea.Root>
368
+
324
369
  <div role='none' className='grid grid-cols-1 auto-rows-[--rail-action] pbs-2'>
325
370
  {pinnedItems
326
371
  .filter((item) => l0ItemType(item) !== 'collection')
@@ -328,17 +373,27 @@ export const L0Menu = ({
328
373
  <L0Item key={item.id} item={item} parent={parent} path={path} pinned />
329
374
  ))}
330
375
  </div>
376
+
331
377
  {userAccountItem && (
332
- <div role='none' className='grid py-2 app-no-drag'>
333
- <L0Item key={userAccountItem.id} item={userAccountItem} parent={parent} path={path} userAccount />
378
+ <div role='none' className='grid app-no-drag'>
379
+ <L0ItemRoot key={userAccountItem.id} item={userAccountItem} parent={parent} path={path}>
380
+ <UserAccountAvatar
381
+ userId={userAccountItem.properties.userId}
382
+ hue={userAccountItem.properties.hue}
383
+ emoji={userAccountItem.properties.emoji}
384
+ status={userAccountItem.properties.status}
385
+ size={11}
386
+ />
387
+ </L0ItemRoot>
334
388
  </div>
335
389
  )}
390
+
336
391
  <div
337
392
  role='none'
338
393
  className='hidden [body[data-platform="darwin"]_&]:block absolute block-start-0 is-[calc(var(--l0-size)-1px)] bs-[calc(40px+0.25rem)]'
339
394
  style={{
340
395
  background:
341
- 'linear-gradient(to bottom, var(--dx-baseSurface) 0%, var(--dx-baseSurface) 70%, transparent 100%)',
396
+ 'linear-gradient(to bottom, var(--dx-sidebarSurface) 0%, var(--dx-sidebarSurface) 70%, transparent 100%)',
342
397
  }}
343
398
  />
344
399
  </Tabs.Tablist>
@@ -4,77 +4,54 @@
4
4
 
5
5
  import React, { useCallback, useMemo } from 'react';
6
6
 
7
- import { useLayout } from '@dxos/app-framework';
8
7
  import { type Node } from '@dxos/app-graph';
9
8
  import { Button, type ButtonProps, Icon, IconButton, toLocalizedString, useTranslation } from '@dxos/react-ui';
10
9
  import { Tree } from '@dxos/react-ui-list';
11
10
  import { Tabs } from '@dxos/react-ui-tabs';
12
11
  import { hoverableControlItem, hoverableOpenControlItem, mx } from '@dxos/react-ui-theme';
13
- import { byPosition } from '@dxos/util';
14
12
 
15
- import { useNavTreeContext } from './NavTreeContext';
16
- import { NavTreeItemColumns } from './NavTreeItemColumns';
17
- import { useLoadDescendents } from '../hooks';
18
- import { NAVTREE_PLUGIN } from '../meta';
19
- import { l0ItemType } from '../util';
13
+ import { useLoadDescendents } from '../../hooks';
14
+ import { NAVTREE_PLUGIN } from '../../meta';
15
+ import { l0ItemType } from '../../util';
16
+ import { useNavTreeContext } from '../NavTreeContext';
17
+ import { NavTreeItemColumns } from '../NavTreeItem';
20
18
 
21
- export type L1PanelProps = {
22
- item: Node<any>;
23
- path: string[];
24
- currentItemId: string;
25
- onBack?: () => void;
26
- };
27
-
28
- const headingBackButtonLabel =
29
- 'absolute inset-0 min-is-0 truncate flex items-center pis-6 transition-[transform,opacity] ease-in-out duration-200';
19
+ const headingBackButtonLabel = 'inset-0 min-is-0 truncate flex items-center pis-2';
30
20
 
31
- const HeadingBackButton = ({ title, onClick }: { title: string } & Pick<ButtonProps, 'onClick'>) => {
32
- const { t } = useTranslation(NAVTREE_PLUGIN);
21
+ const TitleButton = ({ title, onClick }: { title: string } & Pick<ButtonProps, 'onClick'>) => {
33
22
  return (
34
23
  <Button
35
24
  variant='ghost'
36
- classNames='pli-1 flex-1 relative group text-start justify-start bs-[--rail-action] font-normal'
25
+ classNames='pli-1 flex-1 group text-start justify-start bs-[--rail-action] font-normal'
37
26
  onClick={onClick}
38
27
  >
39
28
  <Icon icon='ph--caret-left--bold' size={3} />
40
- <span
41
- className={mx(
42
- headingBackButtonLabel,
43
- 'translate-y-2 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 group-focus:translate-y-0 group-focus:opacity-100',
44
- )}
45
- >
46
- {t('back label')}
47
- </span>
48
- <span
49
- className={mx(
50
- headingBackButtonLabel,
51
- 'translate-y-0 opacity-100 group-hover:-translate-y-2 group-hover:opacity-0 group-focus:-translate-y-2 group-focus:opacity-0',
52
- )}
53
- >
54
- {title}
55
- </span>
29
+ <span className={mx(headingBackButtonLabel)}>{title}</span>
56
30
  </Button>
57
31
  );
58
32
  };
59
33
 
60
- const L1Panel = ({ item, path, currentItemId, onBack }: L1PanelProps) => {
61
- const layout = useLayout();
34
+ export type L1PanelProps = {
35
+ open?: boolean;
36
+ item: Node<any>;
37
+ path: string[];
38
+ currentItemId: string;
39
+ onBack?: () => void;
40
+ };
41
+
42
+ const L1Panel = ({ open, item, path, currentItemId, onBack }: L1PanelProps) => {
62
43
  const { isAlternateTree, setAlternateTree, ...navTreeContext } = useNavTreeContext();
63
44
  const { t } = useTranslation(NAVTREE_PLUGIN);
64
45
 
65
46
  // TODO(wittjosiah): Support multiple alternate trees.
66
- const alternateTree = navTreeContext.getItems(item, 'alternate-tree')[0];
47
+ const alternateTree = navTreeContext.useItems(item, { disposition: 'alternate-tree' })[0];
67
48
  const alternatePath = useMemo(() => [...path, item.id], [item.id, path]);
68
49
  const handleOpen = useCallback(() => setAlternateTree?.(alternatePath, true), [alternatePath, setAlternateTree]);
69
50
  const isAlternate = isAlternateTree?.(alternatePath, item) ?? false;
70
- const alternateGetItems = useCallback(
71
- (node?: Node, disposition?: string) => {
72
- const items = navTreeContext.getItems(node, disposition);
73
- if (node === alternateTree) {
74
- // TODO(wittjosiah): Sorting is expensive, limit to necessary items for now.
75
- return items.toSorted((a, b) => byPosition(a.properties, b.properties));
76
- }
77
- return items;
51
+ const useAlternateItems = useCallback(
52
+ (node?: Node, { disposition }: { disposition?: string } = {}) => {
53
+ // TODO(wittjosiah): Sorting is expensive, limit to necessary items for now.
54
+ return navTreeContext.useItems(node, { disposition, sort: node?.id === alternateTree.id });
78
55
  },
79
56
  [navTreeContext, alternateTree],
80
57
  );
@@ -95,18 +72,20 @@ const L1Panel = ({ item, path, currentItemId, onBack }: L1PanelProps) => {
95
72
  key={item.id}
96
73
  value={item.id}
97
74
  classNames={[
98
- '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)]',
75
+ 'absolute inset-block-0 inline-end-0',
76
+ 'is-[calc(100%-var(--l0-size))] lg:is-[--l1-size] grid-cols-1 grid-rows-[var(--rail-size)_1fr]',
77
+ 'pbs-[env(safe-area-inset-top)]',
99
78
  item.id === currentItemId && 'grid',
100
79
  ]}
101
80
  tabIndex={-1}
102
81
  aria-label={title}
103
- {...(!layout.sidebarOpen && { inert: 'true' })}
82
+ {...(!open && { inert: 'true' })}
104
83
  >
105
84
  {item.id === currentItemId && (
106
85
  <>
107
- <h2 className='flex items-center border-be border-separator app-drag pis-1'>
86
+ <h2 className='flex items-center border-be border-subduedSeparator app-drag pli-1'>
108
87
  {backCapable ? (
109
- <HeadingBackButton title={title} onClick={handleBack} />
88
+ <TitleButton title={title} onClick={handleBack} />
110
89
  ) : (
111
90
  <span className='flex-1 truncate min-is-0 pis-6'>{title}</span>
112
91
  )}
@@ -129,7 +108,7 @@ const L1Panel = ({ item, path, currentItemId, onBack }: L1PanelProps) => {
129
108
  {isAlternate ? (
130
109
  <Tree
131
110
  {...navTreeContext}
132
- getItems={alternateGetItems}
111
+ useItems={useAlternateItems}
133
112
  id={alternateTree.id}
134
113
  root={alternateTree}
135
114
  path={alternatePath}
@@ -156,41 +135,37 @@ const L1Panel = ({ item, path, currentItemId, onBack }: L1PanelProps) => {
156
135
  );
157
136
  };
158
137
 
159
- const L1PanelCollection = ({ item, path, currentItemId }: L1PanelProps) => {
160
- const { getItems } = useNavTreeContext();
138
+ const L1PanelCollection = ({ item, path, ...props }: L1PanelProps) => {
139
+ const { useItems } = useNavTreeContext();
161
140
  useLoadDescendents(item);
162
- const collectionItems = getItems(item);
141
+ const collectionItems = useItems(item);
163
142
  const groupPath = useMemo(() => [...path, item.id], [item.id, path]);
164
143
  return (
165
144
  <>
166
145
  {collectionItems
167
146
  .filter((item) => l0ItemType(item) === 'tab')
168
147
  .map((item) => (
169
- <L1Panel key={item.id} item={item} path={groupPath} currentItemId={currentItemId} />
148
+ <L1Panel key={item.id} item={item} path={groupPath} {...props} />
170
149
  ))}
171
150
  </>
172
151
  );
173
152
  };
174
153
 
175
- export const L1Panels = ({
176
- topLevelItems,
177
- path,
178
- currentItemId,
179
- onBack,
180
- }: Pick<L1PanelProps, 'onBack'> & {
154
+ export type L1PanelsProps = Pick<L1PanelProps, 'open' | 'currentItemId' | 'onBack'> & {
181
155
  topLevelItems: Node<any>[];
182
156
  path: string[];
183
- currentItemId: string;
184
- }) => {
157
+ };
158
+
159
+ export const L1Panels = ({ topLevelItems, onBack, ...props }: L1PanelsProps) => {
185
160
  return (
186
161
  <>
187
162
  {topLevelItems.map((item) => {
188
163
  const type = l0ItemType(item);
189
164
  switch (type) {
190
165
  case 'collection':
191
- return <L1PanelCollection key={item.id} item={item} path={path} currentItemId={currentItemId} />;
166
+ return <L1PanelCollection key={item.id} item={item} {...props} />;
192
167
  case 'tab':
193
- return <L1Panel key={item.id} item={item} path={path} currentItemId={currentItemId} onBack={onBack} />;
168
+ return <L1Panel key={item.id} item={item} {...props} onBack={onBack} />;
194
169
  default:
195
170
  return null;
196
171
  }
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './L0Menu';
6
+ export * from './L1Panel';
@@ -5,17 +5,17 @@
5
5
  import '@dxos-theme';
6
6
 
7
7
  import { type Meta } from '@storybook/react';
8
- import React from 'react';
9
8
 
10
- import { UserAccountAvatar } from './UserAccountAvatar';
9
+ import { withTheme } from '@dxos/storybook-utils';
11
10
 
12
- export const Default = (props: any) => {
13
- return <UserAccountAvatar {...props} />;
14
- };
11
+ import { UserAccountAvatar } from './UserAccountAvatar';
15
12
 
16
13
  const meta: Meta<typeof UserAccountAvatar> = {
17
14
  title: 'plugins/plugin-navtree/UserAccountAvatar',
18
15
  component: UserAccountAvatar,
16
+ decorators: [withTheme],
19
17
  };
20
18
 
21
19
  export default meta;
20
+
21
+ export const Default = {};