@dxos/react-ui-menu 0.7.5-main.937ce75
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/LICENSE +8 -0
- package/README.md +3 -0
- package/dist/lib/browser/index.mjs +231 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/node/index.cjs +248 -0
- package/dist/lib/node/index.cjs.map +7 -0
- package/dist/lib/node/meta.json +1 -0
- package/dist/lib/node-esm/index.mjs +233 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/types/src/components/ActionLabel.d.ts +7 -0
- package/dist/types/src/components/ActionLabel.d.ts.map +1 -0
- package/dist/types/src/components/DropdownMenu.d.ts +19 -0
- package/dist/types/src/components/DropdownMenu.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +4 -0
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/ToolbarDropdownMenu.d.ts +4 -0
- package/dist/types/src/components/Toolbar/ToolbarDropdownMenu.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/ToolbarToggleGroup.d.ts +4 -0
- package/dist/types/src/components/Toolbar/ToolbarToggleGroup.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/defs.d.ts +30 -0
- package/dist/types/src/components/Toolbar/defs.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/index.d.ts +3 -0
- package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +3 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/defs.d.ts +18 -0
- package/dist/types/src/defs.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +3 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/testing/Menus.stories.d.ts +22 -0
- package/dist/types/src/testing/Menus.stories.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +26 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +8 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/util.d.ts +4 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +52 -0
- package/src/components/ActionLabel.tsx +30 -0
- package/src/components/DropdownMenu.tsx +104 -0
- package/src/components/Toolbar/Toolbar.tsx +71 -0
- package/src/components/Toolbar/ToolbarDropdownMenu.tsx +36 -0
- package/src/components/Toolbar/ToolbarToggleGroup.tsx +39 -0
- package/src/components/Toolbar/defs.ts +36 -0
- package/src/components/Toolbar/index.ts +6 -0
- package/src/components/index.ts +6 -0
- package/src/defs.ts +23 -0
- package/src/index.ts +6 -0
- package/src/testing/Menus.stories.tsx +55 -0
- package/src/testing/index.ts +92 -0
- package/src/translations.ts +13 -0
- package/src/util.ts +14 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
|
6
|
+
import React, { type MouseEvent, type MutableRefObject, type PropsWithChildren, useCallback } from 'react';
|
|
7
|
+
|
|
8
|
+
import { DropdownMenu as NaturalDropdownMenu, Icon } from '@dxos/react-ui';
|
|
9
|
+
|
|
10
|
+
import { ActionLabel } from './ActionLabel';
|
|
11
|
+
import { type MenuAction, type MenuProps } from '../defs';
|
|
12
|
+
|
|
13
|
+
export type DropdownMenuProps = MenuProps &
|
|
14
|
+
PropsWithChildren<
|
|
15
|
+
Partial<{
|
|
16
|
+
defaultMenuOpen: boolean;
|
|
17
|
+
menuOpen: boolean;
|
|
18
|
+
onMenuOpenChange: (nextOpen: boolean) => void;
|
|
19
|
+
suppressNextTooltip?: MutableRefObject<boolean>;
|
|
20
|
+
}>
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
const DropdownMenuItem = ({
|
|
24
|
+
action,
|
|
25
|
+
onClick,
|
|
26
|
+
}: {
|
|
27
|
+
action: MenuAction;
|
|
28
|
+
onClick: (action: MenuAction, event: MouseEvent) => void;
|
|
29
|
+
}) => {
|
|
30
|
+
const handleClick = useCallback((event: MouseEvent) => onClick(action, event), [action, onClick]);
|
|
31
|
+
return (
|
|
32
|
+
<NaturalDropdownMenu.Item
|
|
33
|
+
onClick={handleClick}
|
|
34
|
+
classNames='gap-2'
|
|
35
|
+
disabled={action.properties?.disabled}
|
|
36
|
+
{...(action.properties?.testId && { 'data-testid': action.properties.testId })}
|
|
37
|
+
>
|
|
38
|
+
{action.properties?.icon && <Icon icon={action.properties!.icon} size={4} />}
|
|
39
|
+
<ActionLabel action={action} />
|
|
40
|
+
</NaturalDropdownMenu.Item>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const DropdownMenuRoot = ({
|
|
45
|
+
defaultMenuOpen,
|
|
46
|
+
actions,
|
|
47
|
+
menuOpen,
|
|
48
|
+
suppressNextTooltip,
|
|
49
|
+
onMenuOpenChange,
|
|
50
|
+
onAction,
|
|
51
|
+
children,
|
|
52
|
+
}: DropdownMenuProps) => {
|
|
53
|
+
const [optionsMenuOpen, setOptionsMenuOpen] = useControllableState({
|
|
54
|
+
prop: menuOpen,
|
|
55
|
+
defaultProp: defaultMenuOpen,
|
|
56
|
+
onChange: onMenuOpenChange,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const handleActionClick = useCallback(
|
|
60
|
+
(action: MenuAction, event: MouseEvent) => {
|
|
61
|
+
if (action.properties?.disabled) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
event.stopPropagation();
|
|
65
|
+
// TODO(thure): Why does Dialog’s modal-ness cause issues if we don’t explicitly close the menu here?
|
|
66
|
+
if (suppressNextTooltip) {
|
|
67
|
+
suppressNextTooltip.current = true;
|
|
68
|
+
}
|
|
69
|
+
setOptionsMenuOpen(false);
|
|
70
|
+
onAction?.(action);
|
|
71
|
+
},
|
|
72
|
+
[onAction],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<NaturalDropdownMenu.Root
|
|
77
|
+
{...{
|
|
78
|
+
open: optionsMenuOpen,
|
|
79
|
+
onOpenChange: (nextOpen: boolean) => {
|
|
80
|
+
if (!nextOpen && suppressNextTooltip) {
|
|
81
|
+
suppressNextTooltip.current = true;
|
|
82
|
+
}
|
|
83
|
+
return setOptionsMenuOpen(nextOpen);
|
|
84
|
+
},
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{children}
|
|
88
|
+
<NaturalDropdownMenu.Portal>
|
|
89
|
+
<NaturalDropdownMenu.Content>
|
|
90
|
+
<NaturalDropdownMenu.Viewport>
|
|
91
|
+
{actions?.map((action) => <DropdownMenuItem key={action.id} action={action} onClick={handleActionClick} />)}
|
|
92
|
+
</NaturalDropdownMenu.Viewport>
|
|
93
|
+
<NaturalDropdownMenu.Arrow />
|
|
94
|
+
</NaturalDropdownMenu.Content>
|
|
95
|
+
</NaturalDropdownMenu.Portal>
|
|
96
|
+
</NaturalDropdownMenu.Root>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const DropdownMenu = {
|
|
101
|
+
Root: DropdownMenuRoot,
|
|
102
|
+
Trigger: NaturalDropdownMenu.Trigger,
|
|
103
|
+
VirtualTrigger: NaturalDropdownMenu.VirtualTrigger,
|
|
104
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Toolbar as NaturalToolbar } from '@dxos/react-ui';
|
|
8
|
+
|
|
9
|
+
import { ToolbarDropdownMenu } from './ToolbarDropdownMenu';
|
|
10
|
+
import { ToolbarToggleGroup } from './ToolbarToggleGroup';
|
|
11
|
+
import { isMenu, isSeparator, type ToolbarProps } from './defs';
|
|
12
|
+
import { type MenuAction } from '../../defs';
|
|
13
|
+
import { ActionLabel } from '../ActionLabel';
|
|
14
|
+
|
|
15
|
+
const ToolbarItem = ({
|
|
16
|
+
iconSize,
|
|
17
|
+
action,
|
|
18
|
+
onAction,
|
|
19
|
+
}: Pick<ToolbarProps, 'iconSize' | 'onAction'> & {
|
|
20
|
+
action: MenuAction;
|
|
21
|
+
}) => {
|
|
22
|
+
const handleClick = useCallback(() => {
|
|
23
|
+
onAction?.(action);
|
|
24
|
+
}, [action, onAction]);
|
|
25
|
+
const { icon, iconOnly = true, disabled, testId } = action.properties;
|
|
26
|
+
return (
|
|
27
|
+
<NaturalToolbar.IconButton
|
|
28
|
+
key={action.id}
|
|
29
|
+
iconOnly={iconOnly}
|
|
30
|
+
variant='ghost'
|
|
31
|
+
icon={icon}
|
|
32
|
+
size={iconSize}
|
|
33
|
+
label={<ActionLabel action={action} />}
|
|
34
|
+
disabled={disabled}
|
|
35
|
+
onClick={handleClick}
|
|
36
|
+
{...(testId && { 'data-testid': testId })}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const Toolbar = ({ actions: items, onAction, iconSize = 5, graph, ...props }: ToolbarProps) => {
|
|
42
|
+
return (
|
|
43
|
+
<NaturalToolbar.Root {...props}>
|
|
44
|
+
{items?.map((item, i) =>
|
|
45
|
+
isSeparator(item) ? (
|
|
46
|
+
<NaturalToolbar.Separator key={i} variant={item.properties.variant} />
|
|
47
|
+
) : isMenu(item) ? (
|
|
48
|
+
item.properties.variant === 'dropdownMenu' ? (
|
|
49
|
+
<ToolbarDropdownMenu
|
|
50
|
+
key={item.id}
|
|
51
|
+
actionGroup={item}
|
|
52
|
+
onAction={onAction}
|
|
53
|
+
iconSize={iconSize}
|
|
54
|
+
graph={graph}
|
|
55
|
+
/>
|
|
56
|
+
) : (
|
|
57
|
+
<ToolbarToggleGroup
|
|
58
|
+
key={item.id}
|
|
59
|
+
actionGroup={item}
|
|
60
|
+
onAction={onAction}
|
|
61
|
+
iconSize={iconSize}
|
|
62
|
+
graph={graph}
|
|
63
|
+
/>
|
|
64
|
+
)
|
|
65
|
+
) : (
|
|
66
|
+
<ToolbarItem key={item.id} action={item as MenuAction} onAction={onAction} iconSize={iconSize} />
|
|
67
|
+
),
|
|
68
|
+
)}
|
|
69
|
+
</NaturalToolbar.Root>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useMemo, useRef } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Toolbar as NaturalToolbar } from '@dxos/react-ui';
|
|
8
|
+
|
|
9
|
+
import { type ToolbarActionGroupProps } from './defs';
|
|
10
|
+
import { type MenuAction } from '../../defs';
|
|
11
|
+
import { ActionLabel } from '../ActionLabel';
|
|
12
|
+
import { DropdownMenu } from '../DropdownMenu';
|
|
13
|
+
|
|
14
|
+
export const ToolbarDropdownMenu = ({ actionGroup, graph, onAction, applyActiveIcon }: ToolbarActionGroupProps) => {
|
|
15
|
+
const menuActions = useMemo(() => (graph ? graph.actions(actionGroup) : []) as MenuAction[], [actionGroup, graph]);
|
|
16
|
+
const { icon, iconOnly = true, disabled, testId } = actionGroup.properties;
|
|
17
|
+
const suppressNextTooltip = useRef(false);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<DropdownMenu.Root actions={menuActions} onAction={onAction} suppressNextTooltip={suppressNextTooltip}>
|
|
21
|
+
<DropdownMenu.Trigger asChild>
|
|
22
|
+
<NaturalToolbar.IconButton
|
|
23
|
+
size={5}
|
|
24
|
+
variant='ghost'
|
|
25
|
+
iconOnly={iconOnly}
|
|
26
|
+
disabled={disabled}
|
|
27
|
+
icon={(applyActiveIcon && menuActions.find((action) => !!action.properties.checked)?.properties.icon) || icon}
|
|
28
|
+
label={<ActionLabel action={actionGroup} />}
|
|
29
|
+
{...(testId && { 'data-testid': testId })}
|
|
30
|
+
suppressNextTooltip={suppressNextTooltip}
|
|
31
|
+
caretDown
|
|
32
|
+
/>
|
|
33
|
+
</DropdownMenu.Trigger>
|
|
34
|
+
</DropdownMenu.Root>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useMemo } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Toolbar as NaturalToolbar } from '@dxos/react-ui';
|
|
8
|
+
|
|
9
|
+
import type { ToolbarActionGroupProps } from './defs';
|
|
10
|
+
import { type MenuAction } from '../../defs';
|
|
11
|
+
import { ActionLabel } from '../ActionLabel';
|
|
12
|
+
|
|
13
|
+
const ToolbarToggleGroupItem = ({ action }: { action: MenuAction }) => {
|
|
14
|
+
const { icon, iconOnly, disabled } = action.properties;
|
|
15
|
+
return (
|
|
16
|
+
<NaturalToolbar.ToggleGroupIconItem
|
|
17
|
+
key={action.id}
|
|
18
|
+
value={action.id}
|
|
19
|
+
icon={icon}
|
|
20
|
+
iconOnly={iconOnly}
|
|
21
|
+
disabled={disabled}
|
|
22
|
+
label={<ActionLabel action={action} />}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const ToolbarToggleGroup = ({ actionGroup, graph }: ToolbarActionGroupProps) => {
|
|
28
|
+
const menuActions = useMemo(() => (graph ? (graph.actions(actionGroup) as MenuAction[]) : []), [actionGroup, graph]);
|
|
29
|
+
const { selectCardinality, value } = actionGroup.properties;
|
|
30
|
+
return (
|
|
31
|
+
// TODO(thure): This is extremely difficult to manage, what do?
|
|
32
|
+
// @ts-ignore
|
|
33
|
+
<NaturalToolbar.ToggleGroup type={selectCardinality} value={value}>
|
|
34
|
+
{menuActions.map((action) => (
|
|
35
|
+
<ToolbarToggleGroupItem key={action.id} action={action} />
|
|
36
|
+
))}
|
|
37
|
+
</NaturalToolbar.ToggleGroup>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Node, type ActionGroup, type Graph, ACTION_GROUP_TYPE } from '@dxos/app-graph';
|
|
6
|
+
import { type ToolbarRootProps, type ToolbarSeparatorProps, type IconButtonProps } from '@dxos/react-ui';
|
|
7
|
+
|
|
8
|
+
import { type MenuAction, type MenuActionProperties, type MenuProps } from '../../defs';
|
|
9
|
+
|
|
10
|
+
export const ToolbarSeparatorType = '@dxos/react-ui-toolbar/separator' as const;
|
|
11
|
+
|
|
12
|
+
export type ToolbarSeparatorNode = Node<never, Pick<ToolbarSeparatorProps, 'variant'>> & {
|
|
13
|
+
type: typeof ToolbarSeparatorType;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const isSeparator = (node: Node): node is ToolbarSeparatorNode => node.type === ToolbarSeparatorType;
|
|
17
|
+
|
|
18
|
+
export type ToolbarActionGroupProperties = Omit<MenuActionProperties, 'variant'> & {
|
|
19
|
+
variant: 'dropdownMenu' | 'toggleGroup';
|
|
20
|
+
} & ({ selectCardinality?: 'single'; value?: string } | { selectCardinality?: 'multiple'; value?: string[] });
|
|
21
|
+
|
|
22
|
+
export type ToolbarActionGroup = ActionGroup<ToolbarActionGroupProperties>;
|
|
23
|
+
|
|
24
|
+
export const isMenu = (node: Node): node is ToolbarActionGroup => node.type === ACTION_GROUP_TYPE;
|
|
25
|
+
|
|
26
|
+
export type ToolbarItem = ToolbarSeparatorNode | MenuAction | ToolbarActionGroup;
|
|
27
|
+
|
|
28
|
+
export type ToolbarProps = ToolbarRootProps &
|
|
29
|
+
MenuProps<ToolbarItem, MenuAction> & { iconSize?: IconButtonProps['size']; graph?: Graph };
|
|
30
|
+
|
|
31
|
+
export type ToolbarActionGroupProps = Pick<ToolbarProps, 'iconSize'> &
|
|
32
|
+
Omit<MenuProps<MenuAction>, 'actions'> & {
|
|
33
|
+
actionGroup: ToolbarActionGroup;
|
|
34
|
+
graph?: Graph;
|
|
35
|
+
applyActiveIcon?: boolean;
|
|
36
|
+
};
|
package/src/defs.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
import { type Node, type Action } from '@dxos/app-graph';
|
|
5
|
+
import { type Label } from '@dxos/react-ui';
|
|
6
|
+
|
|
7
|
+
export type MenuActionProperties = {
|
|
8
|
+
label: Label;
|
|
9
|
+
icon: string;
|
|
10
|
+
value?: string;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
iconOnly?: boolean;
|
|
13
|
+
testId?: string;
|
|
14
|
+
variant?: 'action' | 'toggle';
|
|
15
|
+
checked?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type MenuAction = Action<MenuActionProperties>;
|
|
19
|
+
|
|
20
|
+
export type MenuProps<A extends Node = Action<MenuActionProperties>, P extends Node = A> = {
|
|
21
|
+
actions?: A[];
|
|
22
|
+
onAction?: (action: P) => void;
|
|
23
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxos-theme';
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
import { faker } from '@dxos/random';
|
|
10
|
+
import { IconButton } from '@dxos/react-ui';
|
|
11
|
+
import { withTheme, withLayout, withSignals } from '@dxos/storybook-utils';
|
|
12
|
+
|
|
13
|
+
import { createActions, createNestedActionGraph, useMutateActions } from './index';
|
|
14
|
+
import { Toolbar as NaturalToolbar, DropdownMenu as NaturalDropdownMenu } from '../components';
|
|
15
|
+
import { type MenuAction } from '../defs';
|
|
16
|
+
import translations from '../translations';
|
|
17
|
+
|
|
18
|
+
faker.seed(1234);
|
|
19
|
+
|
|
20
|
+
export default {
|
|
21
|
+
title: 'ui/react-ui-menu/Menus',
|
|
22
|
+
component: NaturalToolbar,
|
|
23
|
+
decorators: [withTheme, withLayout({ tooltips: true }), withSignals],
|
|
24
|
+
parameters: { translations },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const menuActions = createActions() as MenuAction[];
|
|
28
|
+
const nestedMenuActions = createNestedActionGraph();
|
|
29
|
+
|
|
30
|
+
const handleAction = (action: MenuAction) => console.log('[on action]', action);
|
|
31
|
+
|
|
32
|
+
export const DropdownMenu = {
|
|
33
|
+
render: () => {
|
|
34
|
+
useMutateActions(menuActions);
|
|
35
|
+
return (
|
|
36
|
+
<NaturalDropdownMenu.Root actions={menuActions} onAction={handleAction}>
|
|
37
|
+
<NaturalDropdownMenu.Trigger asChild>
|
|
38
|
+
<IconButton icon='ph--list-checks--regular' size={5} label='Options' />
|
|
39
|
+
</NaturalDropdownMenu.Trigger>
|
|
40
|
+
</NaturalDropdownMenu.Root>
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const Toolbar = {
|
|
46
|
+
render: () => {
|
|
47
|
+
return (
|
|
48
|
+
<NaturalToolbar
|
|
49
|
+
actions={nestedMenuActions.topLevelActions}
|
|
50
|
+
onAction={handleAction}
|
|
51
|
+
graph={nestedMenuActions.graph}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useEffect } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type Action, Graph, type NodeArg, ACTION_TYPE, ACTION_GROUP_TYPE, actionGroupSymbol } from '@dxos/app-graph';
|
|
8
|
+
import { create } from '@dxos/live-object';
|
|
9
|
+
import { faker } from '@dxos/random';
|
|
10
|
+
import { type DeepWriteable } from '@dxos/util';
|
|
11
|
+
|
|
12
|
+
import { type ToolbarItem } from '../components';
|
|
13
|
+
import { type MenuAction } from '../defs';
|
|
14
|
+
|
|
15
|
+
export type CreateActionsParams = Partial<{
|
|
16
|
+
type?: typeof ACTION_TYPE | typeof ACTION_GROUP_TYPE;
|
|
17
|
+
callback: () => void;
|
|
18
|
+
count: number;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
const icons = {
|
|
22
|
+
regular: [
|
|
23
|
+
'ph--text-b--regular',
|
|
24
|
+
'ph--text-italic--regular',
|
|
25
|
+
'ph--text-h-five--regular',
|
|
26
|
+
'ph--chat-text--regular',
|
|
27
|
+
'ph--clipboard-text--regular',
|
|
28
|
+
'ph--link-simple--regular',
|
|
29
|
+
],
|
|
30
|
+
fill: [
|
|
31
|
+
'ph--text-b--fill',
|
|
32
|
+
'ph--text-italic--fill',
|
|
33
|
+
'ph--text-h-five--fill',
|
|
34
|
+
'ph--chat-text--fill',
|
|
35
|
+
'ph--clipboard-text--fill',
|
|
36
|
+
'ph--link-simple--fill',
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const createActions = (params?: CreateActionsParams) => {
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
const { callback = () => console.log('invoke'), count = 12, type = ACTION_TYPE } = params ?? {};
|
|
43
|
+
return faker.helpers.multiple(
|
|
44
|
+
() =>
|
|
45
|
+
create({
|
|
46
|
+
id: faker.string.uuid(),
|
|
47
|
+
type,
|
|
48
|
+
data: type === ACTION_GROUP_TYPE ? actionGroupSymbol : callback,
|
|
49
|
+
properties: {
|
|
50
|
+
label: faker.lorem.words(2),
|
|
51
|
+
icon: faker.helpers.arrayElement(icons[faker.helpers.arrayElement(Object.keys(icons)) as keyof typeof icons]),
|
|
52
|
+
disabled: faker.helpers.arrayElement([true, false]),
|
|
53
|
+
...(type === ACTION_GROUP_TYPE && { variant: 'dropdownMenu' }),
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
{ count },
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const createNestedActionGraph = (groupParams?: CreateActionsParams, params?: CreateActionsParams) => {
|
|
61
|
+
const graph = new Graph();
|
|
62
|
+
const actionGroups = createActions({ type: ACTION_GROUP_TYPE, ...groupParams });
|
|
63
|
+
actionGroups.forEach((group) => {
|
|
64
|
+
const actions = createActions(params);
|
|
65
|
+
// TODO(thure): these methods exist on `graph` but are marked as `@internal`; how should consumers properly build
|
|
66
|
+
// graph “islands” for the purposes of these components?
|
|
67
|
+
// @ts-ignore
|
|
68
|
+
graph._addNodes([group as NodeArg<any>, ...(actions as NodeArg<any>[])]);
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
graph._addEdges(actions.map((action) => ({ source: group.id, target: action.id })));
|
|
71
|
+
void graph.expand(group);
|
|
72
|
+
});
|
|
73
|
+
return { graph, topLevelActions: actionGroups as ToolbarItem[] };
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const mutateActionsOnInterval = (actions: Action[]) => {
|
|
77
|
+
let cursor = 0;
|
|
78
|
+
return setInterval(() => {
|
|
79
|
+
const action = actions[cursor] as DeepWriteable<Action>;
|
|
80
|
+
action.properties.icon = faker.helpers.arrayElement(
|
|
81
|
+
action.properties.icon.endsWith('regular') ? icons.fill : icons.regular,
|
|
82
|
+
);
|
|
83
|
+
action.properties.disabled = !action.properties.disabled;
|
|
84
|
+
cursor = (cursor + actions.length + 1) % actions.length;
|
|
85
|
+
}, 1_000);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const useMutateActions = (actions: MenuAction[]) =>
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const interval = mutateActionsOnInterval(actions);
|
|
91
|
+
return () => clearInterval(interval);
|
|
92
|
+
}, []);
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type ActionLike } from '@dxos/app-graph';
|
|
6
|
+
import { getHostPlatform } from '@dxos/util';
|
|
7
|
+
|
|
8
|
+
export const getShortcut = (action: ActionLike) => {
|
|
9
|
+
return typeof action.properties?.keyBinding === 'string'
|
|
10
|
+
? action.properties.keyBinding
|
|
11
|
+
: action.properties?.keyBinding?.[getHostPlatform()];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const fallbackIcon = 'ph--placeholder--regular';
|