@dxos/react-ui-menu 0.8.4-main.67995b8 → 0.8.4-main.69d29f4

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 (47) hide show
  1. package/dist/lib/browser/index.mjs +308 -317
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +308 -317
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/builder.d.ts +3 -3
  8. package/dist/types/src/builder.d.ts.map +1 -1
  9. package/dist/types/src/components/ActionLabel.d.ts +3 -3
  10. package/dist/types/src/components/ActionLabel.d.ts.map +1 -1
  11. package/dist/types/src/components/DropdownMenu.d.ts +1 -1
  12. package/dist/types/src/components/DropdownMenu.d.ts.map +1 -1
  13. package/dist/types/src/components/MenuContext.d.ts +3 -1
  14. package/dist/types/src/components/MenuContext.d.ts.map +1 -1
  15. package/dist/types/src/components/ToolbarMenu.d.ts +3 -2
  16. package/dist/types/src/components/ToolbarMenu.d.ts.map +1 -1
  17. package/dist/types/src/components/ToolbarMenu.stories.d.ts +20 -0
  18. package/dist/types/src/components/ToolbarMenu.stories.d.ts.map +1 -0
  19. package/dist/types/src/hooks/useMenuActions.d.ts +6 -5
  20. package/dist/types/src/hooks/useMenuActions.d.ts.map +1 -1
  21. package/dist/types/src/index.d.ts +0 -1
  22. package/dist/types/src/index.d.ts.map +1 -1
  23. package/dist/types/src/testing/generator.d.ts +13 -11
  24. package/dist/types/src/testing/generator.d.ts.map +1 -1
  25. package/dist/types/src/translations.d.ts +2 -2
  26. package/dist/types/src/translations.d.ts.map +1 -1
  27. package/dist/types/src/types.d.ts +11 -24
  28. package/dist/types/src/types.d.ts.map +1 -1
  29. package/dist/types/src/util.d.ts +12 -5
  30. package/dist/types/src/util.d.ts.map +1 -1
  31. package/dist/types/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +34 -31
  33. package/src/builder.ts +11 -11
  34. package/src/components/ActionLabel.tsx +5 -5
  35. package/src/components/DropdownMenu.tsx +13 -5
  36. package/src/components/MenuContext.tsx +16 -4
  37. package/src/components/ToolbarMenu.stories.tsx +81 -0
  38. package/src/components/ToolbarMenu.tsx +68 -34
  39. package/src/hooks/useMenuActions.ts +32 -19
  40. package/src/index.ts +0 -2
  41. package/src/testing/generator.ts +63 -58
  42. package/src/translations.ts +1 -1
  43. package/src/types.ts +12 -26
  44. package/src/util.ts +32 -14
  45. package/dist/types/src/components/Menus.stories.d.ts +0 -25
  46. package/dist/types/src/components/Menus.stories.d.ts.map +0 -1
  47. package/src/components/Menus.stories.tsx +0 -69
@@ -2,27 +2,17 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Rx } from '@effect-rx/rx-react';
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 MenuAction, type MenuItemGroup, type MenuItem } from '../types';
12
+ import { type MenuItem, type MenuItemGroup } from '../types';
23
13
 
24
- export type CreateActionsParams = Partial<{
25
- type?: typeof ACTION_TYPE | typeof ACTION_GROUP_TYPE;
14
+ export type CreateActionsProps = Partial<{
15
+ type?: typeof Node.ActionType | typeof Node.ActionGroupType;
26
16
  callback: () => void;
27
17
  count: number;
28
18
  }>;
@@ -46,29 +36,27 @@ const icons = {
46
36
  ],
47
37
  };
48
38
 
49
- export const createActions = (params?: CreateActionsParams) => {
50
- // eslint-disable-next-line no-console
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
- live({
55
- id: faker.string.uuid(),
56
- type,
57
- data: type === ACTION_GROUP_TYPE ? actionGroupSymbol : callback,
58
- properties: {
59
- label: faker.lorem.words(2),
60
- icon: faker.helpers.arrayElement(icons[faker.helpers.arrayElement(Object.keys(icons)) as keyof typeof icons]),
61
- disabled: faker.helpers.arrayElement([true, false]),
62
- ...(type === ACTION_GROUP_TYPE && { variant: 'dropdownMenu' }),
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
- export const createNestedActions = Rx.make(() => {
57
+ const buildNestedActions = (): ActionGraphProps => {
70
58
  const result: ActionGraphProps = { edges: [], nodes: [] };
71
- const actionGroups = createActions({ type: ACTION_GROUP_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);
@@ -78,39 +66,56 @@ export const createNestedActions = Rx.make(() => {
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?: CreateActionsParams, params?: CreateActionsParams) => {
84
- const graph = new Graph();
85
- const actionGroups = createActions({ type: ACTION_GROUP_TYPE, ...groupParams });
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.addNodes([group as NodeArg<any>, ...(actions as NodeArg<any>[])]);
89
- graph.addEdges([
90
- { source: 'root', target: group.id },
91
- ...actions.map((action) => ({ source: group.id, target: action.id })),
92
- ]);
93
- void graph.expand(group.id);
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 },
82
+ ...actions.map((action) => ({ source: group.id, target: action.id })),
83
+ ]),
84
+ Graph.expand(group.id),
85
+ );
94
86
  });
95
87
  const resolveGroupItems = (groupNode?: MenuItemGroup) =>
96
- (graph.getActions(groupNode?.id ?? ROOT_ID) || null) as MenuItem[] | null;
88
+ (Graph.getActions(graph, groupNode?.id ?? Node.RootId) || null) as MenuItem[] | null;
97
89
  return { resolveGroupItems };
98
90
  };
99
91
 
100
- export const mutateActionsOnInterval = (actions: Action[]) => {
101
- let cursor = 0;
102
- return setInterval(() => {
103
- const action = actions[cursor] as DeepWriteable<Action>;
104
- action.properties.icon = faker.helpers.arrayElement(
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
- const interval = mutateActionsOnInterval(actions);
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
+ };
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { type Resource } from '@dxos/react-ui';
6
6
 
7
- export const translationKey = 'react-ui-menu';
7
+ export const translationKey = '@dxos/react-ui-menu';
8
8
 
9
9
  export const translations = [
10
10
  {
package/src/types.ts CHANGED
@@ -2,51 +2,37 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type Node, type Action, type ActionGroup, ACTION_GROUP_TYPE } from '@dxos/app-graph';
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
- };
20
-
21
- export type MenuItemGroupProperties = {
22
- label: Label;
23
- icon?: string;
24
- iconOnly?: boolean;
25
- };
5
+ import { Node } from '@dxos/app-graph';
6
+ import { type IconButtonProps, type ToolbarSeparatorProps } from '@dxos/react-ui';
7
+ import { type MenuActionProperties } from '@dxos/ui-types';
26
8
 
27
- export type MenuAction<P extends {} = {}> = Action<P & MenuActionProperties>;
9
+ export type MenuAction<P extends {} = {}> = Node.Action<P & MenuActionProperties>;
28
10
 
29
11
  export const MenuSeparatorType = '@dxos/react-ui-toolbar/separator' as const;
30
12
 
31
- export type MenuSeparator = Node<never, Pick<ToolbarSeparatorProps, 'variant'>> & {
13
+ export type MenuSeparator = Node.Node<never, Pick<ToolbarSeparatorProps, 'variant'>> & {
32
14
  type: typeof MenuSeparatorType;
33
15
  };
34
16
 
35
- export const isSeparator = (node: Node): node is MenuSeparator => node.type === MenuSeparatorType;
17
+ export const isSeparator = (node: Node.Node): node is MenuSeparator => node.type === MenuSeparatorType;
36
18
 
37
19
  export type MenuSingleSelectActionGroup = { selectCardinality: 'single'; value: string };
38
20
  export type MenuMultipleSelectActionGroup = { selectCardinality: 'multiple'; value: string[] };
39
21
 
40
- export type MenuItemGroup<P extends Record<string, any> = Record<string, any>> = ActionGroup<P>;
22
+ export type MenuItemGroup<P extends Record<string, any> = Record<string, any>> = Node.ActionGroup<P>;
41
23
 
42
- export const isMenuGroup = (node: Node): node is MenuItemGroup => node.type === ACTION_GROUP_TYPE;
24
+ export const isMenuGroup = (node: Node.Node): node is MenuItemGroup => node.type === Node.ActionGroupType;
43
25
 
44
26
  export type MenuItem = MenuSeparator | MenuAction | MenuItemGroup;
45
27
 
46
28
  export type MenuItemsResolver = (group?: MenuItemGroup) => MenuItem[] | null;
47
29
 
30
+ export type ActionExecutor = (action: MenuAction, params: Node.InvokeProps) => void;
31
+
48
32
  export type MenuContextValue = {
49
33
  useGroupItems: MenuItemsResolver;
50
34
  iconSize: IconButtonProps['size'];
51
35
  attendableId?: string;
36
+ /** Optional action executor. If provided, will be used instead of default execution. */
37
+ onAction?: ActionExecutor;
52
38
  };
package/src/util.ts CHANGED
@@ -2,18 +2,31 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { ACTION_GROUP_TYPE, ACTION_TYPE, actionGroupSymbol, type ActionLike } from '@dxos/app-graph';
5
+ import * as Effect from 'effect/Effect';
6
+
7
+ import { Node } from '@dxos/app-graph';
8
+ import { runAndForwardErrors } from '@dxos/effect';
9
+ import { type MenuActionProperties, type MenuItemGroupProperties } from '@dxos/ui-types';
6
10
  import { getHostPlatform } from '@dxos/util';
7
11
 
8
- import {
9
- type MenuAction,
10
- type MenuActionProperties,
11
- type MenuItemGroup,
12
- type MenuItemGroupProperties,
13
- type MenuSeparator,
14
- } from './types';
12
+ import { type MenuAction, type MenuItemGroup, type MenuSeparator } from './types';
13
+
14
+ /**
15
+ * Execute a menu action's Effect with its captured context.
16
+ * This provides the `_actionContext` layer if available.
17
+ */
18
+ export const executeMenuAction = async (action: MenuAction, params: Node.InvokeProps = {}): Promise<void> => {
19
+ let effect = action.data(params);
20
+
21
+ // Provide captured action context if available.
22
+ if (action._actionContext) {
23
+ effect = effect.pipe(Effect.provide(action._actionContext));
24
+ }
25
+
26
+ await runAndForwardErrors(effect);
27
+ };
15
28
 
16
- export const getShortcut = (action: ActionLike) => {
29
+ export const getShortcut = (action: Node.ActionLike) => {
17
30
  return typeof action.properties?.keyBinding === 'string'
18
31
  ? action.properties.keyBinding
19
32
  : action.properties?.keyBinding?.[getHostPlatform()];
@@ -28,9 +41,9 @@ export const createMenuAction = <P extends {} = {}>(
28
41
  ) =>
29
42
  ({
30
43
  id,
31
- type: ACTION_TYPE,
44
+ type: Node.ActionType,
32
45
  properties,
33
- data: invoke,
46
+ data: () => Effect.sync(invoke),
34
47
  }) satisfies MenuAction;
35
48
 
36
49
  export const createMenuItemGroup = <
@@ -41,9 +54,9 @@ export const createMenuItemGroup = <
41
54
  ) =>
42
55
  ({
43
56
  id,
44
- type: ACTION_GROUP_TYPE,
57
+ type: Node.ActionGroupType,
45
58
  properties,
46
- data: actionGroupSymbol,
59
+ data: Node.actionGroupSymbol,
47
60
  }) satisfies MenuItemGroup;
48
61
 
49
62
  export const createGapSeparator = (id: string = 'gap', source: string = 'root') => ({
@@ -55,7 +68,12 @@ export const createGapSeparator = (id: string = 'gap', source: string = 'root')
55
68
  data: undefined as never,
56
69
  } satisfies MenuSeparator,
57
70
  ],
58
- edges: [{ source, target: id }],
71
+ edges: [
72
+ {
73
+ source,
74
+ target: id,
75
+ },
76
+ ],
59
77
  });
60
78
 
61
79
  export const createLineSeparator = (id: string = 'line', source: string = 'root') => ({
@@ -1,25 +0,0 @@
1
- import '@dxos-theme';
2
- import React from 'react';
3
- declare const _default: {
4
- title: string;
5
- component: ({ __menuScope, classNames, textBlockWidth: wrapContents, ...props }: import("./MenuContext").MenuScopedProps<import("./ToolbarMenu").ToolbarMenuProps>) => React.JSX.Element;
6
- decorators: import("@storybook/react").Decorator[];
7
- parameters: {
8
- translations: [{
9
- readonly 'en-US': {
10
- readonly "react-ui-menu": {};
11
- };
12
- }];
13
- };
14
- };
15
- export default _default;
16
- export declare const DropdownMenu: {
17
- render: () => React.JSX.Element;
18
- };
19
- export declare const Toolbar: {
20
- render: () => React.JSX.Element;
21
- };
22
- export declare const UseMenuActionsToolbar: {
23
- render: () => React.JSX.Element;
24
- };
25
- //# sourceMappingURL=Menus.stories.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Menus.stories.d.ts","sourceRoot":"","sources":["../../../../src/components/Menus.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,aAAa,CAAC;AACrB,OAAO,KAA+B,MAAM,OAAO,CAAC;;;;;;;;;;;;;AAcpD,wBAOE;AAEF,eAAO,MAAM,YAAY;;CAgBxB,CAAC;AAEF,eAAO,MAAM,OAAO;;CAUnB,CAAC;AAEF,eAAO,MAAM,qBAAqB;;CAUjC,CAAC"}
@@ -1,69 +0,0 @@
1
- //
2
- // Copyright 2024 DXOS.org
3
- //
4
-
5
- import '@dxos-theme';
6
- import React, { useCallback, useMemo } from 'react';
7
-
8
- import { faker } from '@dxos/random';
9
- import { IconButton } from '@dxos/react-ui';
10
- import { withTheme } from '@dxos/storybook-utils';
11
-
12
- import { DropdownMenu as NaturalDropdownMenu, ToolbarMenu, MenuProvider } from '../components';
13
- import { useMenuActions } from '../hooks';
14
- import { createActions, createNestedActions, createNestedActionsResolver, useMutateActions } from '../testing';
15
- import { translations } from '../translations';
16
- import { type MenuAction } from '../types';
17
-
18
- faker.seed(1234);
19
-
20
- export default {
21
- title: 'ui/react-ui-menu/Menus',
22
- component: ToolbarMenu,
23
- decorators: [withTheme],
24
- parameters: {
25
- translations,
26
- },
27
- };
28
-
29
- export const DropdownMenu = {
30
- render: () => {
31
- const menuActions = useMemo(() => createActions() as MenuAction[], []);
32
- const resolveGroupItems = useCallback(() => menuActions, [menuActions]);
33
- useMutateActions(menuActions);
34
-
35
- return (
36
- <MenuProvider useGroupItems={resolveGroupItems}>
37
- <NaturalDropdownMenu.Root>
38
- <NaturalDropdownMenu.Trigger asChild>
39
- <IconButton icon='ph--list-checks--regular' size={5} label='Options' />
40
- </NaturalDropdownMenu.Trigger>
41
- </NaturalDropdownMenu.Root>
42
- </MenuProvider>
43
- );
44
- },
45
- };
46
-
47
- export const Toolbar = {
48
- render: () => {
49
- const nestedMenuActions = useMemo(() => createNestedActionsResolver(), []);
50
-
51
- return (
52
- <MenuProvider {...nestedMenuActions}>
53
- <ToolbarMenu />
54
- </MenuProvider>
55
- );
56
- },
57
- };
58
-
59
- export const UseMenuActionsToolbar = {
60
- render: () => {
61
- const menu = useMenuActions(createNestedActions);
62
-
63
- return (
64
- <MenuProvider {...menu}>
65
- <ToolbarMenu />
66
- </MenuProvider>
67
- );
68
- },
69
- };