@dxos/react-ui-menu 0.8.4-main.1068cf700f → 0.8.4-main.1c7ec43d41
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.
- package/dist/lib/browser/index.mjs +385 -215
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +14 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +385 -215
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/translations.mjs +16 -0
- package/dist/lib/node-esm/translations.mjs.map +7 -0
- package/dist/types/src/builder.d.ts +26 -1
- package/dist/types/src/builder.d.ts.map +1 -1
- package/dist/types/src/builder.test.d.ts +2 -0
- package/dist/types/src/builder.test.d.ts.map +1 -0
- package/dist/types/src/components/ActionLabel.d.ts.map +1 -1
- package/dist/types/src/components/DropdownMenu.d.ts +4 -2
- package/dist/types/src/components/DropdownMenu.d.ts.map +1 -1
- package/dist/types/src/components/Menu.d.ts +50 -0
- package/dist/types/src/components/Menu.d.ts.map +1 -0
- package/dist/types/src/components/Menu.stories.d.ts +21 -0
- package/dist/types/src/components/Menu.stories.d.ts.map +1 -0
- package/dist/types/src/components/Menu.test.d.ts +2 -0
- package/dist/types/src/components/Menu.test.d.ts.map +1 -0
- package/dist/types/src/components/ToolbarMenu.d.ts +5 -5
- package/dist/types/src/components/ToolbarMenu.d.ts.map +1 -1
- package/dist/types/src/components/ToolbarMenu.stories.d.ts +3 -1
- package/dist/types/src/components/ToolbarMenu.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -3
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useMenuActions.d.ts +2 -2
- package/dist/types/src/hooks/useMenuActions.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/testing/generator.d.ts +10 -6
- package/dist/types/src/testing/generator.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +25 -3
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +2 -0
- package/dist/types/src/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +29 -20
- package/src/builder.test.ts +224 -0
- package/src/builder.ts +68 -18
- package/src/components/ActionLabel.tsx +4 -3
- package/src/components/DropdownMenu.tsx +3 -4
- package/src/components/Menu.stories.tsx +266 -0
- package/src/components/Menu.test.tsx +447 -0
- package/src/components/Menu.tsx +348 -0
- package/src/components/ToolbarMenu.stories.tsx +29 -27
- package/src/components/ToolbarMenu.tsx +115 -90
- package/src/components/index.ts +1 -3
- package/src/hooks/useMenuActions.ts +13 -14
- package/src/index.ts +1 -1
- package/src/testing/generator.ts +24 -19
- package/src/types.ts +26 -2
- package/src/util.ts +2 -1
- package/dist/types/src/components/MenuContext.d.ts +0 -15
- package/dist/types/src/components/MenuContext.d.ts.map +0 -1
- package/src/components/MenuContext.tsx +0 -64
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { useCallback } from 'react';
|
|
6
6
|
|
|
7
7
|
import { Icon, Toolbar as NaturalToolbar, type ToolbarRootProps, useTranslation } from '@dxos/react-ui';
|
|
8
8
|
import { useAttention } from '@dxos/react-ui-attention';
|
|
9
|
-
import {
|
|
9
|
+
import { composable, composableProps } from '@dxos/ui-theme';
|
|
10
10
|
import { type MenuActionProperties } from '@dxos/ui-types';
|
|
11
11
|
|
|
12
|
-
import { translationKey } from '
|
|
12
|
+
import { translationKey } from '#translations';
|
|
13
|
+
|
|
13
14
|
import {
|
|
14
15
|
type MenuAction,
|
|
15
16
|
type MenuItem,
|
|
@@ -20,10 +21,9 @@ import {
|
|
|
20
21
|
isSeparator,
|
|
21
22
|
} from '../types';
|
|
22
23
|
import { executeMenuAction } from '../util';
|
|
23
|
-
|
|
24
24
|
import { ActionLabel, actionLabel } from './ActionLabel';
|
|
25
25
|
import { DropdownMenu } from './DropdownMenu';
|
|
26
|
-
import { type MenuScopedProps,
|
|
26
|
+
import { type MenuScopedProps, useMenuItems, useMenuScoped } from './Menu';
|
|
27
27
|
|
|
28
28
|
export type ToolbarMenuDropdownMenuActionGroup = Omit<MenuActionProperties, 'variant' | 'icon'> & {
|
|
29
29
|
variant: 'dropdownMenu';
|
|
@@ -41,7 +41,7 @@ export type ToolbarMenuActionGroupProperties = (
|
|
|
41
41
|
) &
|
|
42
42
|
(MenuSingleSelectActionGroup | MenuMultipleSelectActionGroup);
|
|
43
43
|
|
|
44
|
-
export type ToolbarMenuProps = ToolbarRootProps
|
|
44
|
+
export type ToolbarMenuProps = ToolbarRootProps;
|
|
45
45
|
|
|
46
46
|
export type ToolbarMenuActionGroupProps = {
|
|
47
47
|
group: MenuItemGroup<ToolbarMenuActionGroupProperties>;
|
|
@@ -53,15 +53,13 @@ export type ToolbarMenuActionProps = {
|
|
|
53
53
|
action: MenuAction;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
const ActionToolbarItem = ({
|
|
57
|
-
const { iconSize, onAction } =
|
|
56
|
+
const ActionToolbarItem = ({ __menuScope, action }: MenuScopedProps<{ action: MenuAction }>) => {
|
|
57
|
+
const { iconSize, onAction } = useMenuScoped('ActionToolbarItem', __menuScope);
|
|
58
58
|
const { t } = useTranslation(translationKey);
|
|
59
59
|
|
|
60
60
|
const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
? { icon, size: iconSize, iconOnly, label: actionLabel(action, t) }
|
|
64
|
-
: { children: <ActionLabel action={action} /> };
|
|
61
|
+
const buttonVariant =
|
|
62
|
+
(action.properties as { variant?: string }).variant === 'primary' ? ('primary' as const) : ('ghost' as const);
|
|
65
63
|
|
|
66
64
|
const handleClick = useCallback(() => {
|
|
67
65
|
if (onAction) {
|
|
@@ -71,27 +69,42 @@ const ActionToolbarItem = ({ action, __menuScope }: MenuScopedProps<{ action: Me
|
|
|
71
69
|
}
|
|
72
70
|
}, [action, onAction]);
|
|
73
71
|
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
const commonProps = {
|
|
73
|
+
variant: buttonVariant,
|
|
74
|
+
disabled,
|
|
75
|
+
classNames,
|
|
76
|
+
onClick: handleClick,
|
|
77
|
+
...(testId && { 'data-testid': testId }),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (hidden) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return icon ? (
|
|
85
|
+
<NaturalToolbar.IconButton
|
|
76
86
|
key={action.id}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
{
|
|
81
|
-
{
|
|
82
|
-
classNames={classNames}
|
|
87
|
+
{...commonProps}
|
|
88
|
+
icon={icon}
|
|
89
|
+
size={iconSize}
|
|
90
|
+
iconOnly={iconOnly}
|
|
91
|
+
label={actionLabel(action, t)}
|
|
83
92
|
/>
|
|
93
|
+
) : (
|
|
94
|
+
<NaturalToolbar.Button key={action.id} {...commonProps}>
|
|
95
|
+
<ActionLabel action={action} />
|
|
96
|
+
</NaturalToolbar.Button>
|
|
84
97
|
);
|
|
85
98
|
};
|
|
86
99
|
|
|
87
100
|
const DropdownMenuToolbarItem = ({
|
|
101
|
+
__menuScope,
|
|
88
102
|
group,
|
|
89
103
|
items: propsItems,
|
|
90
|
-
__menuScope,
|
|
91
104
|
}: MenuScopedProps<ToolbarMenuActionGroupProps>) => {
|
|
92
105
|
const { iconOnly, disabled, testId } = group.properties;
|
|
93
106
|
const { t } = useTranslation(translationKey);
|
|
94
|
-
const { iconSize } =
|
|
107
|
+
const { iconSize } = useMenuScoped('DropdownMenuToolbarItem', __menuScope);
|
|
95
108
|
const items = useMenuItems(group, propsItems, 'DropdownMenuToolbarItem', __menuScope);
|
|
96
109
|
const activeItem = items?.find((item) => !!(item as MenuAction).properties.checked);
|
|
97
110
|
const icon =
|
|
@@ -113,7 +126,7 @@ const DropdownMenuToolbarItem = ({
|
|
|
113
126
|
children: (
|
|
114
127
|
<>
|
|
115
128
|
<ActionLabel action={labelAction} />
|
|
116
|
-
<Icon size={3} icon='ph--caret-down--bold' classNames='
|
|
129
|
+
<Icon size={3} icon='ph--caret-down--bold' classNames='ms-1' />
|
|
117
130
|
</>
|
|
118
131
|
),
|
|
119
132
|
};
|
|
@@ -127,10 +140,11 @@ const DropdownMenuToolbarItem = ({
|
|
|
127
140
|
);
|
|
128
141
|
};
|
|
129
142
|
|
|
130
|
-
const ToggleGroupItem = ({ group, action
|
|
131
|
-
const { iconSize, onAction } =
|
|
143
|
+
const ToggleGroupItem = ({ __menuScope, group, action }: MenuScopedProps<ToolbarMenuActionProps>) => {
|
|
144
|
+
const { iconSize, onAction } = useMenuScoped('ToggleGroupItem', __menuScope);
|
|
132
145
|
const { t } = useTranslation(translationKey);
|
|
133
146
|
const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
|
|
147
|
+
|
|
134
148
|
const handleClick = useCallback(() => {
|
|
135
149
|
if (onAction) {
|
|
136
150
|
onAction(action, { parent: group });
|
|
@@ -138,91 +152,102 @@ const ToggleGroupItem = ({ group, action, __menuScope }: MenuScopedProps<Toolbar
|
|
|
138
152
|
void executeMenuAction(action, { parent: group });
|
|
139
153
|
}
|
|
140
154
|
}, [action, group, onAction]);
|
|
141
|
-
const Root = icon ? NaturalToolbar.ToggleGroupIconItem : NaturalToolbar.ToggleGroupItem;
|
|
142
|
-
const rootProps = icon
|
|
143
|
-
? { icon, size: iconSize, iconOnly, label: actionLabel(action, t) }
|
|
144
|
-
: { children: <ActionLabel action={action} /> };
|
|
145
155
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
+
const commonProps = {
|
|
157
|
+
value: action.id,
|
|
158
|
+
disabled,
|
|
159
|
+
variant: 'ghost' as const,
|
|
160
|
+
classNames,
|
|
161
|
+
onClick: handleClick,
|
|
162
|
+
...(testId && { 'data-testid': testId }),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return hidden ? null : icon ? (
|
|
166
|
+
<NaturalToolbar.ToggleGroupIconItem
|
|
167
|
+
{...commonProps}
|
|
168
|
+
icon={icon}
|
|
169
|
+
size={iconSize}
|
|
170
|
+
iconOnly={iconOnly}
|
|
171
|
+
label={actionLabel(action, t)}
|
|
156
172
|
/>
|
|
173
|
+
) : (
|
|
174
|
+
<NaturalToolbar.ToggleGroupItem {...commonProps}>
|
|
175
|
+
<ActionLabel action={action} />
|
|
176
|
+
</NaturalToolbar.ToggleGroupItem>
|
|
157
177
|
);
|
|
158
178
|
};
|
|
159
179
|
|
|
160
180
|
const ToggleGroupToolbarItem = ({
|
|
161
|
-
group,
|
|
162
|
-
items: propsItems,
|
|
163
181
|
__menuScope,
|
|
182
|
+
group,
|
|
183
|
+
items: itemsProp,
|
|
164
184
|
}: MenuScopedProps<ToolbarMenuActionGroupProps>) => {
|
|
165
|
-
const items = useMenuItems(group,
|
|
166
|
-
const { selectCardinality
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
185
|
+
const items = useMenuItems(group, itemsProp, 'ToggleGroupToolbarItem', __menuScope);
|
|
186
|
+
const { selectCardinality } = group.properties;
|
|
187
|
+
|
|
188
|
+
// TODO(thure): Handle other menu item types.
|
|
189
|
+
const children = (items as MenuAction[]).map((action) => (
|
|
190
|
+
<ToggleGroupItem key={action.id} group={group} action={action} />
|
|
191
|
+
));
|
|
192
|
+
|
|
193
|
+
if (selectCardinality === 'multiple') {
|
|
194
|
+
return (
|
|
195
|
+
<NaturalToolbar.ToggleGroup type='multiple' value={group.properties.value}>
|
|
196
|
+
{children}
|
|
197
|
+
</NaturalToolbar.ToggleGroup>
|
|
198
|
+
);
|
|
199
|
+
} else {
|
|
200
|
+
return (
|
|
201
|
+
<NaturalToolbar.ToggleGroup type='single' value={group.properties.value}>
|
|
202
|
+
{children}
|
|
203
|
+
</NaturalToolbar.ToggleGroup>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
180
206
|
};
|
|
181
207
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return (
|
|
198
|
-
<NaturalToolbar.Root
|
|
199
|
-
{...props}
|
|
200
|
-
classNames={[attendableId, classNames]}
|
|
201
|
-
disabled={!alwaysActive && !hasAttention}
|
|
202
|
-
layoutManaged={textBlockWidthProp}
|
|
203
|
-
>
|
|
204
|
-
<InnerRoot {...innerRootProps}>
|
|
205
|
-
{items?.map((item: MenuItem, i: number) => (
|
|
206
|
-
<ToolbarMenuItem key={item.id ?? i} item={item} />
|
|
208
|
+
export const ToolbarMenu = composable<HTMLDivElement, MenuScopedProps<ToolbarMenuProps>>(
|
|
209
|
+
({ __menuScope, children, ...props }, forwardedRef) => {
|
|
210
|
+
const items = useMenuItems(undefined, undefined, 'ToolbarMenu', __menuScope);
|
|
211
|
+
const { attendableId, alwaysActive } = useMenuScoped('ToolbarMenu', __menuScope);
|
|
212
|
+
const { hasAttention } = useAttention(attendableId);
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<NaturalToolbar.Root
|
|
216
|
+
{...composableProps(props, { classNames: attendableId })}
|
|
217
|
+
disabled={!alwaysActive && !hasAttention}
|
|
218
|
+
ref={forwardedRef}
|
|
219
|
+
>
|
|
220
|
+
{items?.map((item: MenuItem) => (
|
|
221
|
+
<ToolbarMenuItem key={item.id} __menuScope={__menuScope} item={item} />
|
|
207
222
|
))}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
223
|
+
{children}
|
|
224
|
+
</NaturalToolbar.Root>
|
|
225
|
+
);
|
|
226
|
+
},
|
|
227
|
+
);
|
|
212
228
|
|
|
213
|
-
const ToolbarMenuItem = ({ item }: { item: MenuItem }) => {
|
|
229
|
+
const ToolbarMenuItem = ({ __menuScope, item }: MenuScopedProps<{ item: MenuItem }>) => {
|
|
214
230
|
if (isSeparator(item)) {
|
|
215
231
|
return <NaturalToolbar.Separator variant={item.properties.variant} />;
|
|
216
232
|
}
|
|
217
233
|
|
|
218
|
-
// TODO(thure): Figure out type narrowing that doesn’t require so much use of `as`.
|
|
219
234
|
if (isMenuGroup(item)) {
|
|
220
235
|
if (item.properties.variant === 'dropdownMenu') {
|
|
221
|
-
return
|
|
236
|
+
return (
|
|
237
|
+
<DropdownMenuToolbarItem
|
|
238
|
+
__menuScope={__menuScope}
|
|
239
|
+
group={item as MenuItemGroup<ToolbarMenuActionGroupProperties>}
|
|
240
|
+
/>
|
|
241
|
+
);
|
|
222
242
|
} else {
|
|
223
|
-
return
|
|
243
|
+
return (
|
|
244
|
+
<ToggleGroupToolbarItem
|
|
245
|
+
__menuScope={__menuScope}
|
|
246
|
+
group={item as MenuItemGroup<ToolbarMenuActionGroupProperties>}
|
|
247
|
+
/>
|
|
248
|
+
);
|
|
224
249
|
}
|
|
225
250
|
}
|
|
226
251
|
|
|
227
|
-
return <ActionToolbarItem action={item as MenuAction} />;
|
|
252
|
+
return <ActionToolbarItem __menuScope={__menuScope} action={item as MenuAction} />;
|
|
228
253
|
};
|
package/src/components/index.ts
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
|
|
6
|
-
import { useCallback, useContext,
|
|
6
|
+
import { useCallback, useContext, useMemo } from 'react';
|
|
7
7
|
|
|
8
8
|
import { Graph, Node } from '@dxos/app-graph';
|
|
9
9
|
|
|
10
|
-
import { type MenuItem, type MenuItemGroup } from '../types';
|
|
10
|
+
import { type MenuItem, type MenuItemGroup, type MenuItemsAccessor } from '../types';
|
|
11
11
|
|
|
12
12
|
export type ActionGraphNodes = Node.NodeArg<any>[];
|
|
13
13
|
export type ActionGraphEdges = Graph.Edge[];
|
|
@@ -17,7 +17,7 @@ export type ActionGraphProps = {
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
export type MenuActions = {
|
|
20
|
-
|
|
20
|
+
items: MenuItemsAccessor;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export const useMenuActions = (props: Atom.Atom<ActionGraphProps>): MenuActions => {
|
|
@@ -26,22 +26,21 @@ export const useMenuActions = (props: Atom.Atom<ActionGraphProps>): MenuActions
|
|
|
26
26
|
|
|
27
27
|
// Create a new graph whenever props change to preserve correct order.
|
|
28
28
|
// (Graph.addEdges appends rather than replaces, which breaks ordering on updates.)
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// NOTE: Using useMemo rather than a ref-mutation pattern to avoid calling registry.set during render,
|
|
30
|
+
// which would trigger atom state updates in other components (setState-in-render React warning).
|
|
31
|
+
const graph = useMemo(() => {
|
|
31
32
|
const newGraph = Graph.make({ registry });
|
|
32
33
|
newGraph.pipe(Graph.addNodes(menuGraphProps.nodes as Node.NodeArg<any>[]), Graph.addEdges(menuGraphProps.edges));
|
|
33
|
-
|
|
34
|
-
}
|
|
34
|
+
return newGraph;
|
|
35
|
+
}, [registry, menuGraphProps]);
|
|
35
36
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const items = useAtomValue(graph.connections(sourceNode?.id || Node.RootId)) as MenuItem[];
|
|
41
|
-
return items;
|
|
37
|
+
const items: MenuItemsAccessor = useCallback(
|
|
38
|
+
(group?: MenuItemGroup) => {
|
|
39
|
+
// TODO(wittjosiah): Migrate to using action relation instead of child.
|
|
40
|
+
return graph.connections(group?.id || Node.RootId, 'child') as Atom.Atom<MenuItem[] | null>;
|
|
42
41
|
},
|
|
43
42
|
[graph],
|
|
44
43
|
);
|
|
45
44
|
|
|
46
|
-
return {
|
|
45
|
+
return { items };
|
|
47
46
|
};
|
package/src/index.ts
CHANGED
|
@@ -6,4 +6,4 @@ export * from './components';
|
|
|
6
6
|
export * from './hooks';
|
|
7
7
|
export * from './types';
|
|
8
8
|
export { createMenuAction, createMenuItemGroup, createLineSeparator, createGapSeparator } from './util';
|
|
9
|
-
export { MenuBuilder } from './builder';
|
|
9
|
+
export { type ActionGroupBuilder, type ActionGroupBuilderFn, MenuBuilder } from './builder';
|
package/src/testing/generator.ts
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Atom, RegistryContext } from '@effect-atom/atom-react';
|
|
5
|
+
import { Atom, Registry, RegistryContext } from '@effect-atom/atom-react';
|
|
6
6
|
import { useContext, useEffect } from 'react';
|
|
7
7
|
|
|
8
8
|
import { Graph, Node } from '@dxos/app-graph';
|
|
9
|
-
import {
|
|
9
|
+
import { random } from '@dxos/random';
|
|
10
10
|
|
|
11
11
|
import { type ActionGraphProps } from '../hooks/useMenuActions';
|
|
12
|
-
import { type MenuItem, type MenuItemGroup } from '../types';
|
|
12
|
+
import { type MenuItem, type MenuItemGroup, type MenuItemsAccessor } from '../types';
|
|
13
13
|
|
|
14
14
|
export type CreateActionsProps = Partial<{
|
|
15
15
|
type?: typeof Node.ActionType | typeof Node.ActionGroupType;
|
|
@@ -38,15 +38,15 @@ const icons = {
|
|
|
38
38
|
|
|
39
39
|
export const createActions = (params?: CreateActionsProps) => {
|
|
40
40
|
const { callback = () => console.log('invoke'), count = 12, type = Node.ActionType } = params ?? {};
|
|
41
|
-
return
|
|
41
|
+
return random.helpers.multiple(
|
|
42
42
|
() => ({
|
|
43
|
-
id:
|
|
43
|
+
id: random.string.uuid(),
|
|
44
44
|
type,
|
|
45
45
|
data: type === Node.ActionGroupType ? Node.actionGroupSymbol : callback,
|
|
46
46
|
properties: {
|
|
47
|
-
label:
|
|
48
|
-
icon:
|
|
49
|
-
disabled:
|
|
47
|
+
label: random.lorem.words(2),
|
|
48
|
+
icon: random.helpers.arrayElement(icons[random.helpers.arrayElement(Object.keys(icons)) as keyof typeof icons]),
|
|
49
|
+
disabled: random.helpers.arrayElement([true, false]),
|
|
50
50
|
...(type === Node.ActionGroupType && { variant: 'dropdownMenu' }),
|
|
51
51
|
},
|
|
52
52
|
}),
|
|
@@ -61,8 +61,8 @@ const buildNestedActions = (): ActionGraphProps => {
|
|
|
61
61
|
const actions = createActions();
|
|
62
62
|
result.nodes.push(group, ...actions);
|
|
63
63
|
result.edges.push(
|
|
64
|
-
{ source: 'root', target: group.id },
|
|
65
|
-
...actions.map((action) => ({ source: group.id, target: action.id })),
|
|
64
|
+
{ source: 'root', target: group.id, relation: 'child' },
|
|
65
|
+
...actions.map((action) => ({ source: group.id, target: action.id, relation: 'child' })),
|
|
66
66
|
);
|
|
67
67
|
});
|
|
68
68
|
return result;
|
|
@@ -70,23 +70,28 @@ const buildNestedActions = (): ActionGraphProps => {
|
|
|
70
70
|
|
|
71
71
|
export const createNestedActions = Atom.make(buildNestedActions()).pipe(Atom.keepAlive);
|
|
72
72
|
|
|
73
|
-
export const createNestedActionsResolver = (
|
|
74
|
-
|
|
73
|
+
export const createNestedActionsResolver = (props?: {
|
|
74
|
+
groupParams?: CreateActionsProps;
|
|
75
|
+
params?: CreateActionsProps;
|
|
76
|
+
registry?: Registry.Registry;
|
|
77
|
+
}) => {
|
|
78
|
+
const { groupParams, params, registry } = props ?? {};
|
|
79
|
+
const graph = Graph.make({ ...(registry && { registry }) });
|
|
75
80
|
const actionGroups = createActions({ type: Node.ActionGroupType, ...groupParams });
|
|
76
81
|
actionGroups.forEach((group) => {
|
|
77
82
|
const actions = createActions(params);
|
|
78
83
|
graph.pipe(
|
|
79
84
|
Graph.addNodes([group as Node.NodeArg<any>, ...(actions as Node.NodeArg<any>[])]),
|
|
80
85
|
Graph.addEdges([
|
|
81
|
-
{ source: 'root', target: group.id },
|
|
82
|
-
...actions.map((action) => ({ source: group.id, target: action.id })),
|
|
86
|
+
{ source: 'root', target: group.id, relation: 'child' },
|
|
87
|
+
...actions.map((action) => ({ source: group.id, target: action.id, relation: 'child' })),
|
|
83
88
|
]),
|
|
84
|
-
Graph.expand(group.id),
|
|
89
|
+
Graph.expand(group.id, 'child'),
|
|
85
90
|
);
|
|
86
91
|
});
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
return {
|
|
92
|
+
const items: MenuItemsAccessor = (group?: MenuItemGroup) =>
|
|
93
|
+
graph.connections(group?.id ?? Node.RootId, 'child') as Atom.Atom<MenuItem[] | null>;
|
|
94
|
+
return { items };
|
|
90
95
|
};
|
|
91
96
|
|
|
92
97
|
/**
|
|
@@ -107,7 +112,7 @@ export const useMutateActions = (actionsAtom: Atom.Writable<ActionGraphProps>) =
|
|
|
107
112
|
...node,
|
|
108
113
|
properties: {
|
|
109
114
|
...node.properties,
|
|
110
|
-
icon:
|
|
115
|
+
icon: random.helpers.arrayElement(node.properties?.icon?.endsWith('regular') ? icons.fill : icons.regular),
|
|
111
116
|
disabled: !node.properties?.disabled,
|
|
112
117
|
},
|
|
113
118
|
};
|
package/src/types.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
import { type Atom } from '@effect-atom/atom-react';
|
|
6
|
+
|
|
5
7
|
import { Node } from '@dxos/app-graph';
|
|
6
8
|
import { type IconButtonProps, type ToolbarSeparatorProps } from '@dxos/react-ui';
|
|
7
9
|
import { type MenuActionProperties } from '@dxos/ui-types';
|
|
@@ -25,16 +27,38 @@ export const isMenuGroup = (node: Node.Node): node is MenuItemGroup => node.type
|
|
|
25
27
|
|
|
26
28
|
export type MenuItem = MenuSeparator | MenuAction | MenuItemGroup;
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
/** Atom-family-style accessor: returns an atom of items for a given group (or root when undefined). */
|
|
31
|
+
export type MenuItemsAccessor = (group?: MenuItemGroup) => Atom.Atom<MenuItem[] | null>;
|
|
29
32
|
|
|
30
33
|
export type ActionExecutor = (action: MenuAction, params: Node.InvokeProps) => void;
|
|
31
34
|
|
|
35
|
+
export type MenuItemsMode = 'additive' | 'replacement';
|
|
36
|
+
|
|
37
|
+
export type AddMenuItemsProps = {
|
|
38
|
+
id: string;
|
|
39
|
+
mode: MenuItemsMode;
|
|
40
|
+
priority?: number;
|
|
41
|
+
items: MenuItem[];
|
|
42
|
+
groupFilter?: (group?: MenuItemGroup) => boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type MenuItems = Omit<AddMenuItemsProps, 'priority'> & { priority: number };
|
|
46
|
+
export type MenuItemsMap = Map<string, MenuItems>;
|
|
47
|
+
|
|
32
48
|
export type MenuContextValue = {
|
|
33
|
-
|
|
49
|
+
/** Atom-family accessor for base menu items, keyed by group (similar to Tree model). */
|
|
50
|
+
items: MenuItemsAccessor;
|
|
34
51
|
iconSize: IconButtonProps['size'];
|
|
35
52
|
attendableId?: string;
|
|
53
|
+
/** TODO(burdon): Remove and assume always active if attendableId is undefined. */
|
|
36
54
|
/** If true, the menu is always active regardless of attention state. */
|
|
37
55
|
alwaysActive?: boolean;
|
|
56
|
+
/** Atom holding the current set of imperatively added menu items. */
|
|
57
|
+
menuItemsAtom: Atom.Atom<MenuItemsMap>;
|
|
58
|
+
/** Imperatively add menu items to the nearest MenuProvider. */
|
|
59
|
+
addMenuItems: (props: AddMenuItemsProps) => void;
|
|
60
|
+
/** Remove previously added menu items by id. */
|
|
61
|
+
removeMenuItems: (id: string) => void;
|
|
38
62
|
/** Optional action executor. If provided, will be used instead of default execution. */
|
|
39
63
|
onAction?: ActionExecutor;
|
|
40
64
|
};
|
package/src/util.ts
CHANGED
|
@@ -72,6 +72,7 @@ export const createGapSeparator = (id: string = 'gap', source: string = 'root')
|
|
|
72
72
|
{
|
|
73
73
|
source,
|
|
74
74
|
target: id,
|
|
75
|
+
relation: 'child' as const,
|
|
75
76
|
},
|
|
76
77
|
],
|
|
77
78
|
});
|
|
@@ -85,5 +86,5 @@ export const createLineSeparator = (id: string = 'line', source: string = 'root'
|
|
|
85
86
|
data: undefined as never,
|
|
86
87
|
} satisfies MenuSeparator,
|
|
87
88
|
],
|
|
88
|
-
edges: [{ source, target: id }],
|
|
89
|
+
edges: [{ source, target: id, relation: 'child' as const }],
|
|
89
90
|
});
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { type Scope } from '@radix-ui/react-context';
|
|
2
|
-
import React, { type PropsWithChildren } from 'react';
|
|
3
|
-
import { type MenuContextValue, type MenuItem, type MenuItemGroup } from '../types';
|
|
4
|
-
export type MenuScopedProps<P> = P & {
|
|
5
|
-
__menuScope?: Scope;
|
|
6
|
-
};
|
|
7
|
-
declare const createMenuScope: import("@radix-ui/react-context").CreateScope;
|
|
8
|
-
declare const useMenu: (consumerName: string, scope: Scope<MenuContextValue | undefined>) => MenuContextValue;
|
|
9
|
-
export declare const menuContextDefaults: MenuContextValue;
|
|
10
|
-
type MenuProviderProps = PropsWithChildren<Partial<MenuContextValue>>;
|
|
11
|
-
declare const MenuProvider: ({ children, useGroupItems, iconSize, attendableId, alwaysActive, onAction, }: MenuProviderProps) => React.JSX.Element;
|
|
12
|
-
export declare const useMenuItems: (group?: MenuItemGroup, propsItems?: MenuItem[], consumerName?: string, __menuScope?: Scope) => MenuItem[] | undefined;
|
|
13
|
-
export { useMenu, createMenuScope, MenuProvider };
|
|
14
|
-
export type { MenuProviderProps };
|
|
15
|
-
//# sourceMappingURL=MenuContext.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"MenuContext.d.ts","sourceRoot":"","sources":["../../../../src/components/MenuContext.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,KAAK,EAAsB,MAAM,yBAAyB,CAAC;AACzE,OAAO,KAAK,EAAE,EAAE,KAAK,iBAAiB,EAAW,MAAM,OAAO,CAAC;AAE/D,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAEpF,MAAM,MAAM,eAAe,CAAC,CAAC,IAAI,CAAC,GAAG;IAAE,WAAW,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAI7D,QAAA,MAA0B,eAAe,+CAAqC,CAAC;AAE/E,QAAA,MAA4B,OAAO,wFAAkD,CAAC;AAEtF,eAAO,MAAM,mBAAmB,EAAE,gBAIjC,CAAC;AAIF,KAAK,iBAAiB,GAAG,iBAAiB,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC;AAEtE,QAAA,MAAM,YAAY,GAAI,8EAOnB,iBAAiB,sBAcnB,CAAC;AAEF,eAAO,MAAM,YAAY,GACvB,QAAQ,aAAa,EACrB,aAAa,QAAQ,EAAE,EACvB,eAAc,MAA8B,EAC5C,cAAc,KAAK,2BAKpB,CAAC;AAEF,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC;AAElD,YAAY,EAAE,iBAAiB,EAAE,CAAC"}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Copyright 2025 DXOS.org
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
import { type Scope, createContextScope } from '@radix-ui/react-context';
|
|
6
|
-
import React, { type PropsWithChildren, useMemo } from 'react';
|
|
7
|
-
|
|
8
|
-
import { type MenuContextValue, type MenuItem, type MenuItemGroup } from '../types';
|
|
9
|
-
|
|
10
|
-
export type MenuScopedProps<P> = P & { __menuScope?: Scope };
|
|
11
|
-
|
|
12
|
-
const MENU_NAME = 'Menu';
|
|
13
|
-
|
|
14
|
-
const [createMenuContext, createMenuScope] = createContextScope(MENU_NAME, []);
|
|
15
|
-
|
|
16
|
-
const [MenuContextProvider, useMenu] = createMenuContext<MenuContextValue>(MENU_NAME);
|
|
17
|
-
|
|
18
|
-
export const menuContextDefaults: MenuContextValue = {
|
|
19
|
-
iconSize: 5,
|
|
20
|
-
useGroupItems: () => null,
|
|
21
|
-
onAction: undefined,
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const useMenuScope = createMenuScope();
|
|
25
|
-
|
|
26
|
-
type MenuProviderProps = PropsWithChildren<Partial<MenuContextValue>>;
|
|
27
|
-
|
|
28
|
-
const MenuProvider = ({
|
|
29
|
-
children,
|
|
30
|
-
useGroupItems = menuContextDefaults.useGroupItems,
|
|
31
|
-
iconSize = menuContextDefaults.iconSize,
|
|
32
|
-
attendableId,
|
|
33
|
-
alwaysActive,
|
|
34
|
-
onAction,
|
|
35
|
-
}: MenuProviderProps) => {
|
|
36
|
-
const { scope } = useMenuScope(undefined);
|
|
37
|
-
return (
|
|
38
|
-
<MenuContextProvider
|
|
39
|
-
useGroupItems={useGroupItems}
|
|
40
|
-
iconSize={iconSize}
|
|
41
|
-
attendableId={attendableId}
|
|
42
|
-
alwaysActive={alwaysActive}
|
|
43
|
-
onAction={onAction}
|
|
44
|
-
scope={scope}
|
|
45
|
-
>
|
|
46
|
-
{children}
|
|
47
|
-
</MenuContextProvider>
|
|
48
|
-
);
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
export const useMenuItems = (
|
|
52
|
-
group?: MenuItemGroup,
|
|
53
|
-
propsItems?: MenuItem[],
|
|
54
|
-
consumerName: string = 'useMenuItemConsumer',
|
|
55
|
-
__menuScope?: Scope,
|
|
56
|
-
) => {
|
|
57
|
-
const { useGroupItems } = useMenu(consumerName, __menuScope);
|
|
58
|
-
const groupItems = useGroupItems(group);
|
|
59
|
-
return useMemo(() => propsItems ?? groupItems ?? undefined, [propsItems, groupItems]);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export { useMenu, createMenuScope, MenuProvider };
|
|
63
|
-
|
|
64
|
-
export type { MenuProviderProps };
|