@dxos/react-ui-menu 0.8.4-main.c1de068 → 0.8.4-main.c85a9c8dae
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 +556 -390
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +556 -390
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/builder.d.ts +9 -3
- package/dist/types/src/builder.d.ts.map +1 -1
- package/dist/types/src/components/ActionLabel.d.ts +3 -3
- package/dist/types/src/components/ActionLabel.d.ts.map +1 -1
- package/dist/types/src/components/DropdownMenu.d.ts +1 -1
- package/dist/types/src/components/DropdownMenu.d.ts.map +1 -1
- package/dist/types/src/components/Menu.d.ts +46 -0
- package/dist/types/src/components/Menu.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/MenuItems.stories.d.ts +21 -0
- package/dist/types/src/components/MenuItems.stories.d.ts.map +1 -0
- package/dist/types/src/components/ToolbarMenu.d.ts +4 -3
- package/dist/types/src/components/ToolbarMenu.d.ts.map +1 -1
- package/dist/types/src/components/ToolbarMenu.stories.d.ts +20 -0
- package/dist/types/src/components/ToolbarMenu.stories.d.ts.map +1 -0
- 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 +8 -7
- package/dist/types/src/hooks/useMenuActions.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/testing/generator.d.ts +14 -12
- package/dist/types/src/testing/generator.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +2 -2
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +36 -26
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +14 -5
- package/dist/types/src/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +36 -29
- package/src/builder.ts +45 -18
- package/src/components/ActionLabel.tsx +6 -6
- package/src/components/DropdownMenu.tsx +15 -7
- package/src/components/Menu.test.tsx +448 -0
- package/src/components/Menu.tsx +349 -0
- package/src/components/MenuItems.stories.tsx +265 -0
- package/src/components/ToolbarMenu.stories.tsx +80 -0
- package/src/components/ToolbarMenu.tsx +72 -37
- package/src/components/index.ts +1 -3
- package/src/hooks/useMenuActions.ts +31 -53
- package/src/index.ts +0 -2
- package/src/testing/generator.ts +67 -62
- package/src/translations.ts +1 -1
- package/src/types.ts +38 -27
- package/src/util.ts +34 -15
- package/dist/types/src/components/MenuContext.d.ts +0 -13
- package/dist/types/src/components/MenuContext.d.ts.map +0 -1
- package/dist/types/src/components/Menus.stories.d.ts +0 -25
- package/dist/types/src/components/Menus.stories.d.ts.map +0 -1
- package/src/components/MenuContext.tsx +0 -50
- package/src/components/Menus.stories.tsx +0 -69
|
@@ -6,22 +6,24 @@ import React, { Fragment, 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 { mx, textBlockWidth, toolbarLayout } from '@dxos/
|
|
9
|
+
import { mx, textBlockWidth, toolbarLayout } from '@dxos/ui-theme';
|
|
10
|
+
import { type MenuActionProperties } from '@dxos/ui-types';
|
|
10
11
|
|
|
11
|
-
import { actionLabel, ActionLabel } from './ActionLabel';
|
|
12
|
-
import { DropdownMenu } from './DropdownMenu';
|
|
13
|
-
import { type MenuScopedProps, useMenu, useMenuItems } from './MenuContext';
|
|
14
12
|
import { translationKey } from '../translations';
|
|
15
13
|
import {
|
|
16
14
|
type MenuAction,
|
|
17
15
|
type MenuItem,
|
|
18
|
-
isMenuGroup,
|
|
19
|
-
isSeparator,
|
|
20
16
|
type MenuItemGroup,
|
|
21
|
-
type MenuActionProperties,
|
|
22
17
|
type MenuMultipleSelectActionGroup,
|
|
23
18
|
type MenuSingleSelectActionGroup,
|
|
19
|
+
isMenuGroup,
|
|
20
|
+
isSeparator,
|
|
24
21
|
} from '../types';
|
|
22
|
+
import { executeMenuAction } from '../util';
|
|
23
|
+
|
|
24
|
+
import { ActionLabel, actionLabel } from './ActionLabel';
|
|
25
|
+
import { DropdownMenu } from './DropdownMenu';
|
|
26
|
+
import { type MenuScopedProps, useMenuItems, useMenuScoped } from './Menu';
|
|
25
27
|
|
|
26
28
|
export type ToolbarMenuDropdownMenuActionGroup = Omit<MenuActionProperties, 'variant' | 'icon'> & {
|
|
27
29
|
variant: 'dropdownMenu';
|
|
@@ -52,14 +54,23 @@ export type ToolbarMenuActionProps = {
|
|
|
52
54
|
};
|
|
53
55
|
|
|
54
56
|
const ActionToolbarItem = ({ action, __menuScope }: MenuScopedProps<{ action: MenuAction }>) => {
|
|
55
|
-
const { iconSize } =
|
|
57
|
+
const { iconSize, onAction } = useMenuScoped('ActionToolbarItem', __menuScope);
|
|
56
58
|
const { t } = useTranslation(translationKey);
|
|
57
|
-
|
|
59
|
+
|
|
58
60
|
const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
|
|
59
61
|
const Root = icon ? NaturalToolbar.IconButton : NaturalToolbar.Button;
|
|
60
62
|
const rootProps = icon
|
|
61
63
|
? { icon, size: iconSize, iconOnly, label: actionLabel(action, t) }
|
|
62
64
|
: { children: <ActionLabel action={action} /> };
|
|
65
|
+
|
|
66
|
+
const handleClick = useCallback(() => {
|
|
67
|
+
if (onAction) {
|
|
68
|
+
onAction(action, {});
|
|
69
|
+
} else {
|
|
70
|
+
void executeMenuAction(action);
|
|
71
|
+
}
|
|
72
|
+
}, [action, onAction]);
|
|
73
|
+
|
|
63
74
|
return hidden ? null : (
|
|
64
75
|
<Root
|
|
65
76
|
key={action.id}
|
|
@@ -80,7 +91,7 @@ const DropdownMenuToolbarItem = ({
|
|
|
80
91
|
}: MenuScopedProps<ToolbarMenuActionGroupProps>) => {
|
|
81
92
|
const { iconOnly, disabled, testId } = group.properties;
|
|
82
93
|
const { t } = useTranslation(translationKey);
|
|
83
|
-
const { iconSize } =
|
|
94
|
+
const { iconSize } = useMenuScoped('DropdownMenuToolbarItem', __menuScope);
|
|
84
95
|
const items = useMenuItems(group, propsItems, 'DropdownMenuToolbarItem', __menuScope);
|
|
85
96
|
const activeItem = items?.find((item) => !!(item as MenuAction).properties.checked);
|
|
86
97
|
const icon =
|
|
@@ -91,15 +102,22 @@ const DropdownMenuToolbarItem = ({
|
|
|
91
102
|
const Root = icon ? NaturalToolbar.IconButton : NaturalToolbar.Button;
|
|
92
103
|
const labelAction = (group.properties as any).applyActive && activeItem ? (activeItem as MenuAction) : group;
|
|
93
104
|
const rootProps = icon
|
|
94
|
-
? {
|
|
105
|
+
? {
|
|
106
|
+
icon,
|
|
107
|
+
size: iconSize,
|
|
108
|
+
iconOnly,
|
|
109
|
+
label: actionLabel(labelAction, t),
|
|
110
|
+
caretDown: true,
|
|
111
|
+
}
|
|
95
112
|
: {
|
|
96
113
|
children: (
|
|
97
114
|
<>
|
|
98
115
|
<ActionLabel action={labelAction} />
|
|
99
|
-
<Icon size={3} icon='ph--caret-down--bold' classNames='
|
|
116
|
+
<Icon size={3} icon='ph--caret-down--bold' classNames='ms-1' />
|
|
100
117
|
</>
|
|
101
118
|
),
|
|
102
119
|
};
|
|
120
|
+
|
|
103
121
|
return (
|
|
104
122
|
<DropdownMenu.Root group={group} items={items}>
|
|
105
123
|
<DropdownMenu.Trigger asChild>
|
|
@@ -110,14 +128,21 @@ const DropdownMenuToolbarItem = ({
|
|
|
110
128
|
};
|
|
111
129
|
|
|
112
130
|
const ToggleGroupItem = ({ group, action, __menuScope }: MenuScopedProps<ToolbarMenuActionProps>) => {
|
|
113
|
-
const { iconSize } =
|
|
131
|
+
const { iconSize, onAction } = useMenuScoped('ToggleGroupItem', __menuScope);
|
|
114
132
|
const { t } = useTranslation(translationKey);
|
|
115
133
|
const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
|
|
116
|
-
const handleClick = useCallback(() =>
|
|
134
|
+
const handleClick = useCallback(() => {
|
|
135
|
+
if (onAction) {
|
|
136
|
+
onAction(action, { parent: group });
|
|
137
|
+
} else {
|
|
138
|
+
void executeMenuAction(action, { parent: group });
|
|
139
|
+
}
|
|
140
|
+
}, [action, group, onAction]);
|
|
117
141
|
const Root = icon ? NaturalToolbar.ToggleGroupIconItem : NaturalToolbar.ToggleGroupItem;
|
|
118
142
|
const rootProps = icon
|
|
119
143
|
? { icon, size: iconSize, iconOnly, label: actionLabel(action, t) }
|
|
120
144
|
: { children: <ActionLabel action={action} /> };
|
|
145
|
+
|
|
121
146
|
return hidden ? null : (
|
|
122
147
|
<Root
|
|
123
148
|
key={action.id}
|
|
@@ -139,8 +164,9 @@ const ToggleGroupToolbarItem = ({
|
|
|
139
164
|
}: MenuScopedProps<ToolbarMenuActionGroupProps>) => {
|
|
140
165
|
const items = useMenuItems(group, propsItems, 'ToggleGroupToolbarItem', __menuScope);
|
|
141
166
|
const { selectCardinality, value } = group.properties;
|
|
167
|
+
|
|
142
168
|
return (
|
|
143
|
-
// TODO(thure):
|
|
169
|
+
// TODO(thure): Fix.
|
|
144
170
|
// @ts-ignore
|
|
145
171
|
<NaturalToolbar.ToggleGroup type={selectCardinality} value={value}>
|
|
146
172
|
{
|
|
@@ -153,41 +179,50 @@ const ToggleGroupToolbarItem = ({
|
|
|
153
179
|
);
|
|
154
180
|
};
|
|
155
181
|
|
|
182
|
+
// TODO(burdon): Reconcile with react-ui/Toolbar (incl. textBlockWidth)
|
|
156
183
|
export const ToolbarMenu = ({
|
|
157
184
|
__menuScope,
|
|
158
185
|
classNames,
|
|
159
|
-
textBlockWidth:
|
|
186
|
+
textBlockWidth: textBlockWidthProp,
|
|
160
187
|
...props
|
|
161
188
|
}: MenuScopedProps<ToolbarMenuProps>) => {
|
|
162
189
|
const items = useMenuItems(undefined, undefined, 'ToolbarMenu', __menuScope);
|
|
163
|
-
const { attendableId } =
|
|
190
|
+
const { attendableId, alwaysActive } = useMenuScoped('ToolbarMenu', __menuScope);
|
|
164
191
|
const { hasAttention } = useAttention(attendableId);
|
|
165
|
-
const InnerRoot =
|
|
166
|
-
const innerRootProps =
|
|
192
|
+
const InnerRoot = textBlockWidthProp ? 'div' : Fragment;
|
|
193
|
+
const innerRootProps = textBlockWidthProp
|
|
194
|
+
? { role: 'none', className: mx(textBlockWidth, toolbarLayout, 'h-full') }
|
|
195
|
+
: {};
|
|
196
|
+
|
|
167
197
|
return (
|
|
168
198
|
<NaturalToolbar.Root
|
|
169
199
|
{...props}
|
|
170
|
-
|
|
171
|
-
|
|
200
|
+
classNames={[attendableId, classNames]}
|
|
201
|
+
disabled={!alwaysActive && !hasAttention}
|
|
202
|
+
layoutManaged={textBlockWidthProp}
|
|
172
203
|
>
|
|
173
204
|
<InnerRoot {...innerRootProps}>
|
|
174
|
-
{items?.map((item: MenuItem, i: number) =>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
) : isMenuGroup(item) ? (
|
|
178
|
-
item.properties.variant === 'dropdownMenu' ? (
|
|
179
|
-
// TODO(thure): Figure out type narrowing that doesn’t require so much use of `as`.
|
|
180
|
-
<DropdownMenuToolbarItem key={item.id} group={item as MenuItemGroup<ToolbarMenuActionGroupProperties>} />
|
|
181
|
-
) : (
|
|
182
|
-
// TODO(thure): Figure out type narrowing that doesn’t require so much use of `as`.
|
|
183
|
-
<ToggleGroupToolbarItem key={item.id} group={item as MenuItemGroup<ToolbarMenuActionGroupProperties>} />
|
|
184
|
-
)
|
|
185
|
-
) : (
|
|
186
|
-
// TODO(thure): Figure out type narrowing that doesn’t require so much use of `as`.
|
|
187
|
-
<ActionToolbarItem key={item.id} action={item as MenuAction} />
|
|
188
|
-
),
|
|
189
|
-
)}
|
|
205
|
+
{items?.map((item: MenuItem, i: number) => (
|
|
206
|
+
<ToolbarMenuItem key={item.id ?? i} item={item} />
|
|
207
|
+
))}
|
|
190
208
|
</InnerRoot>
|
|
191
209
|
</NaturalToolbar.Root>
|
|
192
210
|
);
|
|
193
211
|
};
|
|
212
|
+
|
|
213
|
+
const ToolbarMenuItem = ({ item }: { item: MenuItem }) => {
|
|
214
|
+
if (isSeparator(item)) {
|
|
215
|
+
return <NaturalToolbar.Separator variant={item.properties.variant} />;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// TODO(thure): Figure out type narrowing that doesn’t require so much use of `as`.
|
|
219
|
+
if (isMenuGroup(item)) {
|
|
220
|
+
if (item.properties.variant === 'dropdownMenu') {
|
|
221
|
+
return <DropdownMenuToolbarItem group={item as MenuItemGroup<ToolbarMenuActionGroupProperties>} />;
|
|
222
|
+
} else {
|
|
223
|
+
return <ToggleGroupToolbarItem group={item as MenuItemGroup<ToolbarMenuActionGroupProperties>} />;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return <ActionToolbarItem action={item as MenuAction} />;
|
|
228
|
+
};
|
package/src/components/index.ts
CHANGED
|
@@ -2,68 +2,46 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { useCallback, useContext,
|
|
5
|
+
import { type Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
|
|
6
|
+
import { useCallback, useContext, useRef } from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
acc[source] = { inbound: [], outbound: [] };
|
|
18
|
-
}
|
|
19
|
-
if (!acc[target]) {
|
|
20
|
-
acc[target] = { inbound: [], outbound: [] };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const sourceEdges = acc[source];
|
|
24
|
-
if (!sourceEdges.outbound.includes(target)) {
|
|
25
|
-
sourceEdges.outbound.push(target);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const targetEdges = acc[target];
|
|
29
|
-
if (!targetEdges.inbound.includes(source)) {
|
|
30
|
-
targetEdges.inbound.push(source);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return acc;
|
|
34
|
-
}, {}),
|
|
35
|
-
),
|
|
36
|
-
);
|
|
12
|
+
export type ActionGraphNodes = Node.NodeArg<any>[];
|
|
13
|
+
export type ActionGraphEdges = Graph.Edge[];
|
|
14
|
+
export type ActionGraphProps = {
|
|
15
|
+
nodes: ActionGraphNodes;
|
|
16
|
+
edges: ActionGraphEdges;
|
|
37
17
|
};
|
|
38
18
|
|
|
39
|
-
export type
|
|
40
|
-
|
|
41
|
-
|
|
19
|
+
export type MenuActions = {
|
|
20
|
+
items: MenuItemsAccessor;
|
|
21
|
+
};
|
|
42
22
|
|
|
43
|
-
export const useMenuActions = (props:
|
|
23
|
+
export const useMenuActions = (props: Atom.Atom<ActionGraphProps>): MenuActions => {
|
|
44
24
|
const registry = useContext(RegistryContext);
|
|
45
|
-
const menuGraphProps =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const items = useRxValue(graph.connections(sourceNode?.id || ROOT_ID)) as MenuItem[];
|
|
63
|
-
return items;
|
|
25
|
+
const menuGraphProps = useAtomValue(props);
|
|
26
|
+
|
|
27
|
+
// Create a new graph whenever props change to preserve correct order.
|
|
28
|
+
// (Graph.addEdges appends rather than replaces, which breaks ordering on updates.)
|
|
29
|
+
const graphRef = useRef<{ graph: Graph.WritableGraph; props: ActionGraphProps } | null>(null);
|
|
30
|
+
if (!graphRef.current || graphRef.current.props !== menuGraphProps) {
|
|
31
|
+
const newGraph = Graph.make({ registry });
|
|
32
|
+
newGraph.pipe(Graph.addNodes(menuGraphProps.nodes as Node.NodeArg<any>[]), Graph.addEdges(menuGraphProps.edges));
|
|
33
|
+
graphRef.current = { graph: newGraph, props: menuGraphProps };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const graph = graphRef.current.graph;
|
|
37
|
+
|
|
38
|
+
const items: MenuItemsAccessor = useCallback(
|
|
39
|
+
(group?: MenuItemGroup) => {
|
|
40
|
+
// TODO(wittjosiah): Migrate to using action relation instead of child.
|
|
41
|
+
return graph.connections(group?.id || Node.RootId, 'child') as Atom.Atom<MenuItem[] | null>;
|
|
64
42
|
},
|
|
65
43
|
[graph],
|
|
66
44
|
);
|
|
67
45
|
|
|
68
|
-
return {
|
|
46
|
+
return { items };
|
|
69
47
|
};
|
package/src/index.ts
CHANGED
package/src/testing/generator.ts
CHANGED
|
@@ -2,27 +2,17 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { useEffect } from 'react';
|
|
5
|
+
import { Atom, RegistryContext } from '@effect-atom/atom-react';
|
|
6
|
+
import { useContext, useEffect } from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
type Action,
|
|
10
|
-
Graph,
|
|
11
|
-
type NodeArg,
|
|
12
|
-
ACTION_TYPE,
|
|
13
|
-
ACTION_GROUP_TYPE,
|
|
14
|
-
actionGroupSymbol,
|
|
15
|
-
ROOT_ID,
|
|
16
|
-
} from '@dxos/app-graph';
|
|
17
|
-
import { live } from '@dxos/live-object';
|
|
8
|
+
import { Graph, Node } from '@dxos/app-graph';
|
|
18
9
|
import { faker } from '@dxos/random';
|
|
19
|
-
import { type DeepWriteable } from '@dxos/util';
|
|
20
10
|
|
|
21
11
|
import { type ActionGraphProps } from '../hooks/useMenuActions';
|
|
22
|
-
import { type
|
|
12
|
+
import { type MenuItem, type MenuItemGroup, type MenuItemsAccessor } from '../types';
|
|
23
13
|
|
|
24
|
-
export type
|
|
25
|
-
type?: typeof
|
|
14
|
+
export type CreateActionsProps = Partial<{
|
|
15
|
+
type?: typeof Node.ActionType | typeof Node.ActionGroupType;
|
|
26
16
|
callback: () => void;
|
|
27
17
|
count: number;
|
|
28
18
|
}>;
|
|
@@ -46,71 +36,86 @@ const icons = {
|
|
|
46
36
|
],
|
|
47
37
|
};
|
|
48
38
|
|
|
49
|
-
export const createActions = (params?:
|
|
50
|
-
|
|
51
|
-
const { callback = () => console.log('invoke'), count = 12, type = ACTION_TYPE } = params ?? {};
|
|
39
|
+
export const createActions = (params?: CreateActionsProps) => {
|
|
40
|
+
const { callback = () => console.log('invoke'), count = 12, type = Node.ActionType } = params ?? {};
|
|
52
41
|
return faker.helpers.multiple(
|
|
53
|
-
() =>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}),
|
|
42
|
+
() => ({
|
|
43
|
+
id: faker.string.uuid(),
|
|
44
|
+
type,
|
|
45
|
+
data: type === Node.ActionGroupType ? Node.actionGroupSymbol : callback,
|
|
46
|
+
properties: {
|
|
47
|
+
label: faker.lorem.words(2),
|
|
48
|
+
icon: faker.helpers.arrayElement(icons[faker.helpers.arrayElement(Object.keys(icons)) as keyof typeof icons]),
|
|
49
|
+
disabled: faker.helpers.arrayElement([true, false]),
|
|
50
|
+
...(type === Node.ActionGroupType && { variant: 'dropdownMenu' }),
|
|
51
|
+
},
|
|
52
|
+
}),
|
|
65
53
|
{ count },
|
|
66
54
|
);
|
|
67
55
|
};
|
|
68
56
|
|
|
69
|
-
|
|
57
|
+
const buildNestedActions = (): ActionGraphProps => {
|
|
70
58
|
const result: ActionGraphProps = { edges: [], nodes: [] };
|
|
71
|
-
const actionGroups = createActions({ type:
|
|
59
|
+
const actionGroups = createActions({ type: Node.ActionGroupType });
|
|
72
60
|
actionGroups.forEach((group) => {
|
|
73
61
|
const actions = createActions();
|
|
74
62
|
result.nodes.push(group, ...actions);
|
|
75
63
|
result.edges.push(
|
|
76
|
-
{ source: 'root', target: group.id },
|
|
77
|
-
...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' })),
|
|
78
66
|
);
|
|
79
67
|
});
|
|
80
68
|
return result;
|
|
81
|
-
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const createNestedActions = Atom.make(buildNestedActions()).pipe(Atom.keepAlive);
|
|
82
72
|
|
|
83
|
-
export const createNestedActionsResolver = (groupParams?:
|
|
84
|
-
const graph =
|
|
85
|
-
const actionGroups = createActions({ type:
|
|
73
|
+
export const createNestedActionsResolver = (groupParams?: CreateActionsProps, params?: CreateActionsProps) => {
|
|
74
|
+
const graph = Graph.make();
|
|
75
|
+
const actionGroups = createActions({ type: Node.ActionGroupType, ...groupParams });
|
|
86
76
|
actionGroups.forEach((group) => {
|
|
87
77
|
const actions = createActions(params);
|
|
88
|
-
graph.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
78
|
+
graph.pipe(
|
|
79
|
+
Graph.addNodes([group as Node.NodeArg<any>, ...(actions as Node.NodeArg<any>[])]),
|
|
80
|
+
Graph.addEdges([
|
|
81
|
+
{ source: 'root', target: group.id, relation: 'child' },
|
|
82
|
+
...actions.map((action) => ({ source: group.id, target: action.id, relation: 'child' })),
|
|
83
|
+
]),
|
|
84
|
+
Graph.expand(group.id, 'child'),
|
|
85
|
+
);
|
|
94
86
|
});
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
return {
|
|
87
|
+
const items: MenuItemsAccessor = (group?: MenuItemGroup) =>
|
|
88
|
+
graph.connections(group?.id ?? Node.RootId, 'child') as Atom.Atom<MenuItem[] | null>;
|
|
89
|
+
return { items };
|
|
98
90
|
};
|
|
99
91
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
action.properties.icon.endsWith('regular') ? icons.fill : icons.regular,
|
|
106
|
-
);
|
|
107
|
-
action.properties.disabled = !action.properties.disabled;
|
|
108
|
-
cursor = (cursor + actions.length + 1) % actions.length;
|
|
109
|
-
}, 1_000);
|
|
110
|
-
};
|
|
92
|
+
/**
|
|
93
|
+
* Hook to mutate actions in an atom on an interval for testing reactivity.
|
|
94
|
+
*/
|
|
95
|
+
export const useMutateActions = (actionsAtom: Atom.Writable<ActionGraphProps>) => {
|
|
96
|
+
const registry = useContext(RegistryContext);
|
|
111
97
|
|
|
112
|
-
export const useMutateActions = (actions: MenuAction[]) =>
|
|
113
98
|
useEffect(() => {
|
|
114
|
-
|
|
99
|
+
let cursor = 0;
|
|
100
|
+
const interval = setInterval(() => {
|
|
101
|
+
const current = registry.get(actionsAtom);
|
|
102
|
+
const nodes = current.nodes.map((node, index) => {
|
|
103
|
+
if (index !== cursor) {
|
|
104
|
+
return node;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
...node,
|
|
108
|
+
properties: {
|
|
109
|
+
...node.properties,
|
|
110
|
+
icon: faker.helpers.arrayElement(node.properties?.icon?.endsWith('regular') ? icons.fill : icons.regular),
|
|
111
|
+
disabled: !node.properties?.disabled,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
registry.set(actionsAtom, { ...current, nodes });
|
|
116
|
+
cursor = (cursor + 1) % current.nodes.length;
|
|
117
|
+
}, 1_000);
|
|
118
|
+
|
|
115
119
|
return () => clearInterval(interval);
|
|
116
|
-
}, []);
|
|
120
|
+
}, [actionsAtom, registry]);
|
|
121
|
+
};
|
package/src/translations.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -2,51 +2,62 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { type
|
|
6
|
-
import { type ToolbarSeparatorProps, type Label, type IconButtonProps, type ClassNameValue } from '@dxos/react-ui';
|
|
7
|
-
|
|
8
|
-
export type MenuActionProperties = {
|
|
9
|
-
label: Label;
|
|
10
|
-
icon?: string;
|
|
11
|
-
value?: string;
|
|
12
|
-
disabled?: boolean;
|
|
13
|
-
hidden?: boolean;
|
|
14
|
-
iconOnly?: boolean;
|
|
15
|
-
testId?: string;
|
|
16
|
-
variant?: 'action' | 'toggle';
|
|
17
|
-
checked?: boolean;
|
|
18
|
-
classNames?: ClassNameValue;
|
|
19
|
-
};
|
|
5
|
+
import { type Atom } from '@effect-atom/atom-react';
|
|
20
6
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
iconOnly?: boolean;
|
|
25
|
-
};
|
|
7
|
+
import { Node } from '@dxos/app-graph';
|
|
8
|
+
import { type IconButtonProps, type ToolbarSeparatorProps } from '@dxos/react-ui';
|
|
9
|
+
import { type MenuActionProperties } from '@dxos/ui-types';
|
|
26
10
|
|
|
27
|
-
export type MenuAction<P extends {} = {}> = Action<P & MenuActionProperties>;
|
|
11
|
+
export type MenuAction<P extends {} = {}> = Node.Action<P & MenuActionProperties>;
|
|
28
12
|
|
|
29
13
|
export const MenuSeparatorType = '@dxos/react-ui-toolbar/separator' as const;
|
|
30
14
|
|
|
31
|
-
export type MenuSeparator = Node<never, Pick<ToolbarSeparatorProps, 'variant'>> & {
|
|
15
|
+
export type MenuSeparator = Node.Node<never, Pick<ToolbarSeparatorProps, 'variant'>> & {
|
|
32
16
|
type: typeof MenuSeparatorType;
|
|
33
17
|
};
|
|
34
18
|
|
|
35
|
-
export const isSeparator = (node: Node): node is MenuSeparator => node.type === MenuSeparatorType;
|
|
19
|
+
export const isSeparator = (node: Node.Node): node is MenuSeparator => node.type === MenuSeparatorType;
|
|
36
20
|
|
|
37
21
|
export type MenuSingleSelectActionGroup = { selectCardinality: 'single'; value: string };
|
|
38
22
|
export type MenuMultipleSelectActionGroup = { selectCardinality: 'multiple'; value: string[] };
|
|
39
23
|
|
|
40
|
-
export type MenuItemGroup<P extends Record<string, any> = Record<string, any>> = ActionGroup<P>;
|
|
24
|
+
export type MenuItemGroup<P extends Record<string, any> = Record<string, any>> = Node.ActionGroup<P>;
|
|
41
25
|
|
|
42
|
-
export const isMenuGroup = (node: Node): node is MenuItemGroup => node.type ===
|
|
26
|
+
export const isMenuGroup = (node: Node.Node): node is MenuItemGroup => node.type === Node.ActionGroupType;
|
|
43
27
|
|
|
44
28
|
export type MenuItem = MenuSeparator | MenuAction | MenuItemGroup;
|
|
45
29
|
|
|
46
|
-
|
|
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>;
|
|
32
|
+
|
|
33
|
+
export type ActionExecutor = (action: MenuAction, params: Node.InvokeProps) => void;
|
|
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
47
|
|
|
48
48
|
export type MenuContextValue = {
|
|
49
|
-
|
|
49
|
+
/** Atom-family accessor for base menu items, keyed by group (similar to Tree model). */
|
|
50
|
+
items: MenuItemsAccessor;
|
|
50
51
|
iconSize: IconButtonProps['size'];
|
|
51
52
|
attendableId?: string;
|
|
53
|
+
/** If true, the menu is always active regardless of attention state. */
|
|
54
|
+
alwaysActive?: boolean;
|
|
55
|
+
/** Atom holding the current set of imperatively added menu items. */
|
|
56
|
+
menuItemsAtom: Atom.Atom<MenuItemsMap>;
|
|
57
|
+
/** Imperatively add menu items to the nearest MenuProvider. */
|
|
58
|
+
addMenuItems: (props: AddMenuItemsProps) => void;
|
|
59
|
+
/** Remove previously added menu items by id. */
|
|
60
|
+
removeMenuItems: (id: string) => void;
|
|
61
|
+
/** Optional action executor. If provided, will be used instead of default execution. */
|
|
62
|
+
onAction?: ActionExecutor;
|
|
52
63
|
};
|