@dxos/react-ui-menu 0.8.4-main.f5c0578 → 0.8.4-main.fcc0d83b33

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 (66) hide show
  1. package/dist/lib/browser/index.mjs +537 -403
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/browser/translations.mjs +14 -0
  5. package/dist/lib/browser/translations.mjs.map +7 -0
  6. package/dist/lib/node-esm/index.mjs +537 -403
  7. package/dist/lib/node-esm/index.mjs.map +4 -4
  8. package/dist/lib/node-esm/meta.json +1 -1
  9. package/dist/lib/node-esm/translations.mjs +16 -0
  10. package/dist/lib/node-esm/translations.mjs.map +7 -0
  11. package/dist/types/src/builder.d.ts +28 -3
  12. package/dist/types/src/builder.d.ts.map +1 -1
  13. package/dist/types/src/builder.test.d.ts +2 -0
  14. package/dist/types/src/builder.test.d.ts.map +1 -0
  15. package/dist/types/src/components/ActionLabel.d.ts +3 -3
  16. package/dist/types/src/components/ActionLabel.d.ts.map +1 -1
  17. package/dist/types/src/components/DropdownMenu.d.ts +4 -2
  18. package/dist/types/src/components/DropdownMenu.d.ts.map +1 -1
  19. package/dist/types/src/components/Menu.d.ts +50 -0
  20. package/dist/types/src/components/Menu.d.ts.map +1 -0
  21. package/dist/types/src/components/Menu.stories.d.ts +21 -0
  22. package/dist/types/src/components/Menu.stories.d.ts.map +1 -0
  23. package/dist/types/src/components/Menu.test.d.ts +2 -0
  24. package/dist/types/src/components/Menu.test.d.ts.map +1 -0
  25. package/dist/types/src/components/ToolbarMenu.d.ts +7 -6
  26. package/dist/types/src/components/ToolbarMenu.d.ts.map +1 -1
  27. package/dist/types/src/components/ToolbarMenu.stories.d.ts +22 -0
  28. package/dist/types/src/components/ToolbarMenu.stories.d.ts.map +1 -0
  29. package/dist/types/src/components/index.d.ts +1 -3
  30. package/dist/types/src/components/index.d.ts.map +1 -1
  31. package/dist/types/src/hooks/useMenuActions.d.ts +8 -7
  32. package/dist/types/src/hooks/useMenuActions.d.ts.map +1 -1
  33. package/dist/types/src/index.d.ts +1 -2
  34. package/dist/types/src/index.d.ts.map +1 -1
  35. package/dist/types/src/testing/generator.d.ts +20 -14
  36. package/dist/types/src/testing/generator.d.ts.map +1 -1
  37. package/dist/types/src/translations.d.ts +2 -2
  38. package/dist/types/src/translations.d.ts.map +1 -1
  39. package/dist/types/src/types.d.ts +38 -27
  40. package/dist/types/src/types.d.ts.map +1 -1
  41. package/dist/types/src/util.d.ts +14 -5
  42. package/dist/types/src/util.d.ts.map +1 -1
  43. package/dist/types/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +44 -32
  45. package/src/builder.test.ts +224 -0
  46. package/src/builder.ts +76 -26
  47. package/src/components/ActionLabel.tsx +7 -6
  48. package/src/components/DropdownMenu.tsx +12 -6
  49. package/src/components/Menu.stories.tsx +266 -0
  50. package/src/components/Menu.test.tsx +447 -0
  51. package/src/components/Menu.tsx +348 -0
  52. package/src/components/ToolbarMenu.stories.tsx +83 -0
  53. package/src/components/ToolbarMenu.tsx +155 -96
  54. package/src/components/index.ts +1 -3
  55. package/src/hooks/useMenuActions.ts +30 -53
  56. package/src/index.ts +1 -3
  57. package/src/testing/generator.ts +74 -64
  58. package/src/translations.ts +1 -1
  59. package/src/types.ts +39 -27
  60. package/src/util.ts +34 -15
  61. package/dist/types/src/components/MenuContext.d.ts +0 -13
  62. package/dist/types/src/components/MenuContext.d.ts.map +0 -1
  63. package/dist/types/src/components/Menus.stories.d.ts +0 -25
  64. package/dist/types/src/components/Menus.stories.d.ts.map +0 -1
  65. package/src/components/MenuContext.tsx +0 -50
  66. package/src/components/Menus.stories.tsx +0 -69
package/src/types.ts CHANGED
@@ -2,51 +2,63 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { ACTION_GROUP_TYPE, type Action, type ActionGroup, type Node } from '@dxos/app-graph';
6
- import { type ClassNameValue, type IconButtonProps, type Label, type ToolbarSeparatorProps } 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
- export type MenuItemGroupProperties = {
22
- label: Label;
23
- icon?: string;
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 === ACTION_GROUP_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
- export type MenuItemsResolver = (group?: MenuItemGroup) => MenuItem[] | null;
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
- useGroupItems: MenuItemsResolver;
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
+ /** TODO(burdon): Remove and assume always active if attendableId is undefined. */
54
+ /** If true, the menu is always active regardless of attention state. */
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;
62
+ /** Optional action executor. If provided, will be used instead of default execution. */
63
+ onAction?: ActionExecutor;
52
64
  };
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, type ActionLike, actionGroupSymbol } 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);
15
20
 
16
- export const getShortcut = (action: ActionLike) => {
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
+ };
28
+
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,13 @@ 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
+ relation: 'child' as const,
76
+ },
77
+ ],
59
78
  });
60
79
 
61
80
  export const createLineSeparator = (id: string = 'line', source: string = 'root') => ({
@@ -67,5 +86,5 @@ export const createLineSeparator = (id: string = 'line', source: string = 'root'
67
86
  data: undefined as never,
68
87
  } satisfies MenuSeparator,
69
88
  ],
70
- edges: [{ source, target: id }],
89
+ edges: [{ source, target: id, relation: 'child' as const }],
71
90
  });
@@ -1,13 +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
- declare const MenuProvider: ({ children, useGroupItems, iconSize, attendableId, }: PropsWithChildren<Partial<MenuContextValue>>) => React.JSX.Element;
11
- export declare const useMenuItems: (group?: MenuItemGroup, propsItems?: MenuItem[], consumerName?: string, __menuScope?: Scope) => MenuItem[] | undefined;
12
- export { useMenu, createMenuScope, MenuProvider };
13
- //# 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,gBAGjC,CAAC;AAIF,QAAA,MAAM,YAAY,GAAI,sDAKnB,iBAAiB,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,sBAO9C,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"}
@@ -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,50 +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 = 'GraphMenu';
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
- };
22
-
23
- const useMenuScope = createMenuScope();
24
-
25
- const MenuProvider = ({
26
- children,
27
- useGroupItems = menuContextDefaults.useGroupItems,
28
- iconSize = menuContextDefaults.iconSize,
29
- attendableId,
30
- }: PropsWithChildren<Partial<MenuContextValue>>) => {
31
- const { scope } = useMenuScope(undefined);
32
- return (
33
- <MenuContextProvider useGroupItems={useGroupItems} iconSize={iconSize} attendableId={attendableId} scope={scope}>
34
- {children}
35
- </MenuContextProvider>
36
- );
37
- };
38
-
39
- export const useMenuItems = (
40
- group?: MenuItemGroup,
41
- propsItems?: MenuItem[],
42
- consumerName: string = 'useMenuItemConsumer',
43
- __menuScope?: Scope,
44
- ) => {
45
- const { useGroupItems } = useMenu(consumerName, __menuScope);
46
- const groupItems = useGroupItems(group);
47
- return useMemo(() => propsItems ?? groupItems ?? undefined, [propsItems, groupItems]);
48
- };
49
-
50
- export { useMenu, createMenuScope, MenuProvider };
@@ -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 { MenuProvider, DropdownMenu as NaturalDropdownMenu, ToolbarMenu } 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
- };