@dxos/react-ui-menu 0.8.4-main.c1de068 → 0.8.4-main.c351d160a8

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 (59) hide show
  1. package/dist/lib/browser/index.mjs +550 -390
  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 +550 -390
  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 +9 -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 +4 -2
  12. package/dist/types/src/components/DropdownMenu.d.ts.map +1 -1
  13. package/dist/types/src/components/Menu.d.ts +48 -0
  14. package/dist/types/src/components/Menu.d.ts.map +1 -0
  15. package/dist/types/src/components/Menu.stories.d.ts +21 -0
  16. package/dist/types/src/components/Menu.stories.d.ts.map +1 -0
  17. package/dist/types/src/components/Menu.test.d.ts +2 -0
  18. package/dist/types/src/components/Menu.test.d.ts.map +1 -0
  19. package/dist/types/src/components/ToolbarMenu.d.ts +5 -6
  20. package/dist/types/src/components/ToolbarMenu.d.ts.map +1 -1
  21. package/dist/types/src/components/ToolbarMenu.stories.d.ts +20 -0
  22. package/dist/types/src/components/ToolbarMenu.stories.d.ts.map +1 -0
  23. package/dist/types/src/components/index.d.ts +1 -3
  24. package/dist/types/src/components/index.d.ts.map +1 -1
  25. package/dist/types/src/hooks/useMenuActions.d.ts +8 -7
  26. package/dist/types/src/hooks/useMenuActions.d.ts.map +1 -1
  27. package/dist/types/src/index.d.ts +0 -1
  28. package/dist/types/src/index.d.ts.map +1 -1
  29. package/dist/types/src/testing/generator.d.ts +19 -13
  30. package/dist/types/src/testing/generator.d.ts.map +1 -1
  31. package/dist/types/src/translations.d.ts +2 -2
  32. package/dist/types/src/translations.d.ts.map +1 -1
  33. package/dist/types/src/types.d.ts +37 -26
  34. package/dist/types/src/types.d.ts.map +1 -1
  35. package/dist/types/src/util.d.ts +14 -5
  36. package/dist/types/src/util.d.ts.map +1 -1
  37. package/dist/types/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +36 -29
  39. package/src/builder.ts +45 -18
  40. package/src/components/ActionLabel.tsx +6 -6
  41. package/src/components/DropdownMenu.tsx +15 -7
  42. package/src/components/Menu.stories.tsx +265 -0
  43. package/src/components/Menu.test.tsx +448 -0
  44. package/src/components/Menu.tsx +347 -0
  45. package/src/components/ToolbarMenu.stories.tsx +81 -0
  46. package/src/components/ToolbarMenu.tsx +145 -93
  47. package/src/components/index.ts +1 -3
  48. package/src/hooks/useMenuActions.ts +30 -53
  49. package/src/index.ts +0 -2
  50. package/src/testing/generator.ts +72 -62
  51. package/src/translations.ts +1 -1
  52. package/src/types.ts +39 -27
  53. package/src/util.ts +34 -15
  54. package/dist/types/src/components/MenuContext.d.ts +0 -13
  55. package/dist/types/src/components/MenuContext.d.ts.map +0 -1
  56. package/dist/types/src/components/Menus.stories.d.ts +0 -25
  57. package/dist/types/src/components/Menus.stories.d.ts.map +0 -1
  58. package/src/components/MenuContext.tsx +0 -50
  59. package/src/components/Menus.stories.tsx +0 -69
@@ -1,46 +1,37 @@
1
- // src/index.ts
2
- import { rxFromSignal } from "@dxos/app-graph";
3
-
4
- // src/components/DropdownMenu.tsx
5
- import { useSignals as _useSignals3 } from "@preact-signals/safe-react/tracking";
6
- import { useControllableState } from "@radix-ui/react-use-controllable-state";
7
- import React3, { useCallback } from "react";
8
- import { DropdownMenu as NaturalDropdownMenu, Icon } from "@dxos/react-ui";
9
-
10
- // src/components/ActionLabel.tsx
11
- import { useSignals as _useSignals } from "@preact-signals/safe-react/tracking";
12
- import React from "react";
13
- import { keySymbols } from "@dxos/keyboard";
14
- import { toLocalizedString, useTranslation } from "@dxos/react-ui";
15
- import { mx, descriptionText } from "@dxos/react-ui-theme";
16
-
17
- // src/translations.ts
18
- var translationKey = "react-ui-menu";
19
- var translations = [
20
- {
21
- "en-US": {
22
- [translationKey]: {}
23
- }
24
- }
25
- ];
1
+ // src/components/Menu.tsx
2
+ import { Atom, RegistryContext, useAtomValue } from "@effect-atom/atom-react";
3
+ import { createContextScope } from "@radix-ui/react-context";
4
+ import { useControllableState as useControllableState2 } from "@radix-ui/react-use-controllable-state";
5
+ import React4, { useCallback as useCallback3, useContext, useMemo } from "react";
6
+ import { log } from "@dxos/log";
7
+ import { Icon as Icon3, DropdownMenu as NaturalDropdownMenu2 } from "@dxos/react-ui";
26
8
 
27
9
  // src/util.ts
28
- import { ACTION_GROUP_TYPE, ACTION_TYPE, actionGroupSymbol } from "@dxos/app-graph";
10
+ import * as Effect from "effect/Effect";
11
+ import { Node } from "@dxos/app-graph";
12
+ import { runAndForwardErrors } from "@dxos/effect";
29
13
  import { getHostPlatform } from "@dxos/util";
14
+ var executeMenuAction = async (action, params = {}) => {
15
+ let effect = action.data(params);
16
+ if (action._actionContext) {
17
+ effect = effect.pipe(Effect.provide(action._actionContext));
18
+ }
19
+ await runAndForwardErrors(effect);
20
+ };
30
21
  var getShortcut = (action) => {
31
22
  return typeof action.properties?.keyBinding === "string" ? action.properties.keyBinding : action.properties?.keyBinding?.[getHostPlatform()];
32
23
  };
33
24
  var createMenuAction = (id, invoke, properties) => ({
34
25
  id,
35
- type: ACTION_TYPE,
26
+ type: Node.ActionType,
36
27
  properties,
37
- data: invoke
28
+ data: () => Effect.sync(invoke)
38
29
  });
39
30
  var createMenuItemGroup = (id, properties) => ({
40
31
  id,
41
- type: ACTION_GROUP_TYPE,
32
+ type: Node.ActionGroupType,
42
33
  properties,
43
- data: actionGroupSymbol
34
+ data: Node.actionGroupSymbol
44
35
  });
45
36
  var createGapSeparator = (id = "gap", source = "root") => ({
46
37
  nodes: [
@@ -56,7 +47,8 @@ var createGapSeparator = (id = "gap", source = "root") => ({
56
47
  edges: [
57
48
  {
58
49
  source,
59
- target: id
50
+ target: id,
51
+ relation: "child"
60
52
  }
61
53
  ]
62
54
  });
@@ -74,128 +66,118 @@ var createLineSeparator = (id = "line", source = "root") => ({
74
66
  edges: [
75
67
  {
76
68
  source,
77
- target: id
69
+ target: id,
70
+ relation: "child"
78
71
  }
79
72
  ]
80
73
  });
81
74
 
82
75
  // src/components/ActionLabel.tsx
83
- var ActionLabel = ({ action }) => {
84
- var _effect = _useSignals();
85
- try {
86
- const { t } = useTranslation(translationKey);
87
- const shortcut = getShortcut(action);
88
- return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", {
89
- className: "grow truncate"
90
- }, toLocalizedString(action.properties.label, t)), shortcut && /* @__PURE__ */ React.createElement("span", {
91
- className: mx("shrink-0", descriptionText)
92
- }, keySymbols(shortcut).join("")));
93
- } finally {
94
- _effect.f();
76
+ import React from "react";
77
+ import { keySymbols } from "@dxos/keyboard";
78
+ import { toLocalizedString, useTranslation } from "@dxos/react-ui";
79
+ import { mx } from "@dxos/ui-theme";
80
+
81
+ // src/translations.ts
82
+ var translationKey = "@dxos/react-ui-menu";
83
+ var translations = [
84
+ {
85
+ "en-US": {
86
+ [translationKey]: {}
87
+ }
95
88
  }
89
+ ];
90
+
91
+ // src/components/ActionLabel.tsx
92
+ var ActionLabel = ({ action }) => {
93
+ const { t } = useTranslation(translationKey);
94
+ const shortcut = getShortcut(action);
95
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", {
96
+ className: "grow truncate"
97
+ }, toLocalizedString(action.properties.label, t)), shortcut && /* @__PURE__ */ React.createElement("span", {
98
+ className: mx("shrink-0", "text-description")
99
+ }, keySymbols(shortcut).join("")));
96
100
  };
97
101
  var actionLabel = (action, t) => {
98
102
  const shortcut = getShortcut(action);
99
103
  return `${toLocalizedString(action.properties.label, t)}${shortcut ? ` (${keySymbols(shortcut).join("")})` : ""}`;
100
104
  };
101
105
 
102
- // src/components/MenuContext.tsx
103
- import { useSignals as _useSignals2 } from "@preact-signals/safe-react/tracking";
104
- import { createContextScope } from "@radix-ui/react-context";
105
- import React2, { useMemo } from "react";
106
- var MENU_NAME = "GraphMenu";
107
- var [createMenuContext, createMenuScope] = createContextScope(MENU_NAME, []);
108
- var [MenuContextProvider, useMenu] = createMenuContext(MENU_NAME);
109
- var menuContextDefaults = {
110
- iconSize: 5,
111
- useGroupItems: () => null
112
- };
113
- var useMenuScope = createMenuScope();
114
- var MenuProvider = ({ children, useGroupItems = menuContextDefaults.useGroupItems, iconSize = menuContextDefaults.iconSize, attendableId }) => {
115
- var _effect = _useSignals2();
116
- try {
117
- const { scope } = useMenuScope(void 0);
118
- return /* @__PURE__ */ React2.createElement(MenuContextProvider, {
119
- useGroupItems,
120
- iconSize,
121
- attendableId,
122
- scope
123
- }, children);
124
- } finally {
125
- _effect.f();
126
- }
127
- };
128
- var useMenuItems = (group, propsItems, consumerName = "useMenuItemConsumer", __menuScope) => {
129
- const { useGroupItems } = useMenu(consumerName, __menuScope);
130
- const groupItems = useGroupItems(group);
131
- return useMemo(() => propsItems ?? groupItems ?? void 0, [
132
- propsItems,
133
- groupItems
134
- ]);
135
- };
106
+ // src/components/ToolbarMenu.tsx
107
+ import React3, { useCallback as useCallback2 } from "react";
108
+ import { Icon as Icon2, Toolbar as NaturalToolbar, useTranslation as useTranslation2 } from "@dxos/react-ui";
109
+ import { useAttention } from "@dxos/react-ui-attention";
110
+
111
+ // src/types.ts
112
+ import { Node as Node2 } from "@dxos/app-graph";
113
+ var MenuSeparatorType = "@dxos/react-ui-toolbar/separator";
114
+ var isSeparator = (node) => node.type === MenuSeparatorType;
115
+ var isMenuGroup = (node) => node.type === Node2.ActionGroupType;
136
116
 
137
117
  // src/components/DropdownMenu.tsx
118
+ import { useControllableState } from "@radix-ui/react-use-controllable-state";
119
+ import React2, { useCallback } from "react";
120
+ import { Icon, DropdownMenu as NaturalDropdownMenu } from "@dxos/react-ui";
138
121
  var DropdownMenuItem = ({ item, onClick, __menuScope }) => {
139
- var _effect = _useSignals3();
140
- try {
141
- const action = item;
142
- const handleClick = useCallback((event) => onClick(action, event), [
143
- action,
144
- onClick
145
- ]);
146
- const { iconSize } = useMenu("DropdownMenuItem", __menuScope);
147
- return /* @__PURE__ */ React3.createElement(NaturalDropdownMenu.Item, {
148
- onClick: handleClick,
149
- classNames: "gap-2",
150
- disabled: action.properties?.disabled,
151
- ...action.properties?.testId && {
152
- "data-testid": action.properties.testId
153
- }
154
- }, action.properties?.icon && /* @__PURE__ */ React3.createElement(Icon, {
155
- icon: action.properties.icon,
156
- size: iconSize
157
- }), /* @__PURE__ */ React3.createElement(ActionLabel, {
158
- action
159
- }));
160
- } finally {
161
- _effect.f();
162
- }
122
+ const action = item;
123
+ const handleClick = useCallback((event) => onClick(action, event), [
124
+ action,
125
+ onClick
126
+ ]);
127
+ const { iconSize } = useMenuScoped("DropdownMenuItem", __menuScope);
128
+ return /* @__PURE__ */ React2.createElement(NaturalDropdownMenu.Item, {
129
+ onClick: handleClick,
130
+ classNames: "gap-2",
131
+ disabled: action.properties?.disabled,
132
+ ...action.properties?.testId && {
133
+ "data-testid": action.properties.testId
134
+ }
135
+ }, action.properties?.icon && /* @__PURE__ */ React2.createElement(Icon, {
136
+ icon: action.properties.icon,
137
+ size: iconSize
138
+ }), /* @__PURE__ */ React2.createElement(ActionLabel, {
139
+ action
140
+ }));
163
141
  };
164
142
  var DropdownMenuRoot = ({ items: propsItems, group, open, defaultOpen, onOpenChange, caller, children, __menuScope, ...naturalProps }) => {
165
- var _effect = _useSignals3();
166
- try {
167
- const [optionsMenuOpen, setOptionsMenuOpen] = useControllableState({
168
- prop: open,
169
- defaultProp: defaultOpen,
170
- onChange: onOpenChange
171
- });
172
- const handleActionClick = useCallback((action, event) => {
173
- if (action.properties?.disabled) {
174
- return;
175
- }
176
- event.stopPropagation();
177
- setOptionsMenuOpen(false);
178
- void action.data?.({
143
+ const [optionsMenuOpen, setOptionsMenuOpen] = useControllableState({
144
+ prop: open,
145
+ defaultProp: defaultOpen,
146
+ onChange: onOpenChange
147
+ });
148
+ const { onAction } = useMenuScoped("DropdownMenuRoot", __menuScope);
149
+ const handleActionClick = useCallback((action, event) => {
150
+ if (action.properties?.disabled) {
151
+ return;
152
+ }
153
+ event.stopPropagation();
154
+ setOptionsMenuOpen(false);
155
+ if (onAction) {
156
+ onAction(action, {
179
157
  parent: group,
180
158
  caller
181
159
  });
182
- }, [
183
- group,
184
- caller
185
- ]);
186
- const items = useMenuItems(group, propsItems);
187
- return /* @__PURE__ */ React3.createElement(NaturalDropdownMenu.Root, {
188
- open: optionsMenuOpen,
189
- onOpenChange: setOptionsMenuOpen,
190
- ...naturalProps
191
- }, children, /* @__PURE__ */ React3.createElement(NaturalDropdownMenu.Portal, null, /* @__PURE__ */ React3.createElement(NaturalDropdownMenu.Content, null, /* @__PURE__ */ React3.createElement(NaturalDropdownMenu.Viewport, null, items?.map((item) => /* @__PURE__ */ React3.createElement(DropdownMenuItem, {
192
- key: item.id,
193
- item,
194
- onClick: handleActionClick
195
- }))), /* @__PURE__ */ React3.createElement(NaturalDropdownMenu.Arrow, null))));
196
- } finally {
197
- _effect.f();
198
- }
160
+ } else {
161
+ void executeMenuAction(action, {
162
+ parent: group,
163
+ caller
164
+ });
165
+ }
166
+ }, [
167
+ group,
168
+ caller,
169
+ onAction
170
+ ]);
171
+ const items = useMenuItems(group, propsItems);
172
+ return /* @__PURE__ */ React2.createElement(NaturalDropdownMenu.Root, {
173
+ open: optionsMenuOpen,
174
+ onOpenChange: setOptionsMenuOpen,
175
+ ...naturalProps
176
+ }, children, /* @__PURE__ */ React2.createElement(NaturalDropdownMenu.Portal, null, /* @__PURE__ */ React2.createElement(NaturalDropdownMenu.Content, null, /* @__PURE__ */ React2.createElement(NaturalDropdownMenu.Viewport, null, items?.map((item) => /* @__PURE__ */ React2.createElement(DropdownMenuItem, {
177
+ key: item.id,
178
+ item,
179
+ onClick: handleActionClick
180
+ }))), /* @__PURE__ */ React2.createElement(NaturalDropdownMenu.Arrow, null))));
199
181
  };
200
182
  var DropdownMenu = {
201
183
  Root: DropdownMenuRoot,
@@ -204,288 +186,431 @@ var DropdownMenu = {
204
186
  };
205
187
 
206
188
  // src/components/ToolbarMenu.tsx
207
- import { useSignals as _useSignals4 } from "@preact-signals/safe-react/tracking";
208
- import React4, { Fragment, useCallback as useCallback2 } from "react";
209
- import { Icon as Icon2, Toolbar as NaturalToolbar, useTranslation as useTranslation2 } from "@dxos/react-ui";
210
- import { useAttention } from "@dxos/react-ui-attention";
211
- import { mx as mx2, textBlockWidth, toolbarLayout } from "@dxos/react-ui-theme";
212
-
213
- // src/types.ts
214
- import { ACTION_GROUP_TYPE as ACTION_GROUP_TYPE2 } from "@dxos/app-graph";
215
- var MenuSeparatorType = "@dxos/react-ui-toolbar/separator";
216
- var isSeparator = (node) => node.type === MenuSeparatorType;
217
- var isMenuGroup = (node) => node.type === ACTION_GROUP_TYPE2;
218
-
219
- // src/components/ToolbarMenu.tsx
220
- var ActionToolbarItem = ({ action, __menuScope }) => {
221
- var _effect = _useSignals4();
222
- try {
223
- const { iconSize } = useMenu("ActionToolbarItem", __menuScope);
224
- const { t } = useTranslation2(translationKey);
225
- const handleClick = useCallback2(() => action.data?.(), [
226
- action
227
- ]);
228
- const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
229
- const Root = icon ? NaturalToolbar.IconButton : NaturalToolbar.Button;
230
- const rootProps = icon ? {
231
- icon,
232
- size: iconSize,
233
- iconOnly,
234
- label: actionLabel(action, t)
235
- } : {
236
- children: /* @__PURE__ */ React4.createElement(ActionLabel, {
237
- action
238
- })
239
- };
240
- return hidden ? null : /* @__PURE__ */ React4.createElement(Root, {
241
- key: action.id,
242
- disabled,
243
- onClick: handleClick,
244
- variant: "ghost",
245
- ...testId && {
246
- "data-testid": testId
247
- },
248
- ...rootProps,
249
- classNames
250
- });
251
- } finally {
252
- _effect.f();
189
+ var ActionToolbarItem = ({ __menuScope, action }) => {
190
+ const { iconSize, onAction } = useMenuScoped("ActionToolbarItem", __menuScope);
191
+ const { t } = useTranslation2(translationKey);
192
+ const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
193
+ const buttonVariant = action.properties.variant === "primary" ? "primary" : "ghost";
194
+ const handleClick = useCallback2(() => {
195
+ if (onAction) {
196
+ onAction(action, {});
197
+ } else {
198
+ void executeMenuAction(action);
199
+ }
200
+ }, [
201
+ action,
202
+ onAction
203
+ ]);
204
+ const commonProps = {
205
+ variant: buttonVariant,
206
+ disabled,
207
+ classNames,
208
+ onClick: handleClick,
209
+ ...testId && {
210
+ "data-testid": testId
211
+ }
212
+ };
213
+ if (hidden) {
214
+ return null;
253
215
  }
216
+ return icon ? /* @__PURE__ */ React3.createElement(NaturalToolbar.IconButton, {
217
+ key: action.id,
218
+ ...commonProps,
219
+ icon,
220
+ size: iconSize,
221
+ iconOnly,
222
+ label: actionLabel(action, t)
223
+ }) : /* @__PURE__ */ React3.createElement(NaturalToolbar.Button, {
224
+ key: action.id,
225
+ ...commonProps
226
+ }, /* @__PURE__ */ React3.createElement(ActionLabel, {
227
+ action
228
+ }));
229
+ };
230
+ var DropdownMenuToolbarItem = ({ __menuScope, group, items: propsItems }) => {
231
+ const { iconOnly, disabled, testId } = group.properties;
232
+ const { t } = useTranslation2(translationKey);
233
+ const { iconSize } = useMenuScoped("DropdownMenuToolbarItem", __menuScope);
234
+ const items = useMenuItems(group, propsItems, "DropdownMenuToolbarItem", __menuScope);
235
+ const activeItem = items?.find((item) => !!item.properties.checked);
236
+ const icon = group.properties.applyActive && // TODO(thure): Handle other menu item types.
237
+ activeItem?.properties.icon || group.properties.icon;
238
+ const Root = icon ? NaturalToolbar.IconButton : NaturalToolbar.Button;
239
+ const labelAction = group.properties.applyActive && activeItem ? activeItem : group;
240
+ const rootProps = icon ? {
241
+ icon,
242
+ size: iconSize,
243
+ iconOnly,
244
+ label: actionLabel(labelAction, t),
245
+ caretDown: true
246
+ } : {
247
+ children: /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(ActionLabel, {
248
+ action: labelAction
249
+ }), /* @__PURE__ */ React3.createElement(Icon2, {
250
+ size: 3,
251
+ icon: "ph--caret-down--bold",
252
+ classNames: "ms-1"
253
+ }))
254
+ };
255
+ return /* @__PURE__ */ React3.createElement(DropdownMenu.Root, {
256
+ group,
257
+ items
258
+ }, /* @__PURE__ */ React3.createElement(DropdownMenu.Trigger, {
259
+ asChild: true
260
+ }, /* @__PURE__ */ React3.createElement(Root, {
261
+ variant: "ghost",
262
+ disabled,
263
+ ...rootProps,
264
+ ...testId && {
265
+ "data-testid": testId
266
+ }
267
+ })));
254
268
  };
255
- var DropdownMenuToolbarItem = ({ group, items: propsItems, __menuScope }) => {
256
- var _effect = _useSignals4();
257
- try {
258
- const { iconOnly, disabled, testId } = group.properties;
259
- const { t } = useTranslation2(translationKey);
260
- const { iconSize } = useMenu("DropdownMenuToolbarItem", __menuScope);
261
- const items = useMenuItems(group, propsItems, "DropdownMenuToolbarItem", __menuScope);
262
- const activeItem = items?.find((item) => !!item.properties.checked);
263
- const icon = group.properties.applyActive && // TODO(thure): Handle other menu item types.
264
- activeItem?.properties.icon || group.properties.icon;
265
- const Root = icon ? NaturalToolbar.IconButton : NaturalToolbar.Button;
266
- const labelAction = group.properties.applyActive && activeItem ? activeItem : group;
267
- const rootProps = icon ? {
268
- icon,
269
- size: iconSize,
270
- iconOnly,
271
- label: actionLabel(labelAction, t),
272
- caretDown: true
273
- } : {
274
- children: /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(ActionLabel, {
275
- action: labelAction
276
- }), /* @__PURE__ */ React4.createElement(Icon2, {
277
- size: 3,
278
- icon: "ph--caret-down--bold",
279
- classNames: "mis-1"
280
- }))
281
- };
282
- return /* @__PURE__ */ React4.createElement(DropdownMenu.Root, {
283
- group,
284
- items
285
- }, /* @__PURE__ */ React4.createElement(DropdownMenu.Trigger, {
286
- asChild: true
287
- }, /* @__PURE__ */ React4.createElement(Root, {
288
- variant: "ghost",
289
- disabled,
290
- ...rootProps,
291
- ...testId && {
292
- "data-testid": testId
293
- }
294
- })));
295
- } finally {
296
- _effect.f();
269
+ var ToggleGroupItem = ({ __menuScope, group, action }) => {
270
+ const { iconSize, onAction } = useMenuScoped("ToggleGroupItem", __menuScope);
271
+ const { t } = useTranslation2(translationKey);
272
+ const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
273
+ const handleClick = useCallback2(() => {
274
+ if (onAction) {
275
+ onAction(action, {
276
+ parent: group
277
+ });
278
+ } else {
279
+ void executeMenuAction(action, {
280
+ parent: group
281
+ });
282
+ }
283
+ }, [
284
+ action,
285
+ group,
286
+ onAction
287
+ ]);
288
+ const commonProps = {
289
+ value: action.id,
290
+ disabled,
291
+ variant: "ghost",
292
+ classNames,
293
+ onClick: handleClick,
294
+ ...testId && {
295
+ "data-testid": testId
296
+ }
297
+ };
298
+ return hidden ? null : icon ? /* @__PURE__ */ React3.createElement(NaturalToolbar.ToggleGroupIconItem, {
299
+ ...commonProps,
300
+ icon,
301
+ size: iconSize,
302
+ iconOnly,
303
+ label: actionLabel(action, t)
304
+ }) : /* @__PURE__ */ React3.createElement(NaturalToolbar.ToggleGroupItem, commonProps, /* @__PURE__ */ React3.createElement(ActionLabel, {
305
+ action
306
+ }));
307
+ };
308
+ var ToggleGroupToolbarItem = ({ __menuScope, group, items: itemsProp }) => {
309
+ const items = useMenuItems(group, itemsProp, "ToggleGroupToolbarItem", __menuScope);
310
+ const { selectCardinality } = group.properties;
311
+ const children = items.map((action) => /* @__PURE__ */ React3.createElement(ToggleGroupItem, {
312
+ key: action.id,
313
+ group,
314
+ action
315
+ }));
316
+ if (selectCardinality === "multiple") {
317
+ return /* @__PURE__ */ React3.createElement(NaturalToolbar.ToggleGroup, {
318
+ type: "multiple",
319
+ value: group.properties.value
320
+ }, children);
321
+ } else {
322
+ return /* @__PURE__ */ React3.createElement(NaturalToolbar.ToggleGroup, {
323
+ type: "single",
324
+ value: group.properties.value
325
+ }, children);
297
326
  }
298
327
  };
299
- var ToggleGroupItem = ({ group, action, __menuScope }) => {
300
- var _effect = _useSignals4();
301
- try {
302
- const { iconSize } = useMenu("ToggleGroupItem", __menuScope);
303
- const { t } = useTranslation2(translationKey);
304
- const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
305
- const handleClick = useCallback2(() => action.data?.({
306
- parent: group
307
- }), [
308
- action,
309
- group
310
- ]);
311
- const Root = icon ? NaturalToolbar.ToggleGroupIconItem : NaturalToolbar.ToggleGroupItem;
312
- const rootProps = icon ? {
313
- icon,
314
- size: iconSize,
315
- iconOnly,
316
- label: actionLabel(action, t)
317
- } : {
318
- children: /* @__PURE__ */ React4.createElement(ActionLabel, {
319
- action
320
- })
321
- };
322
- return hidden ? null : /* @__PURE__ */ React4.createElement(Root, {
323
- key: action.id,
324
- value: action.id,
325
- disabled,
326
- onClick: handleClick,
327
- variant: "ghost",
328
- ...testId && {
329
- "data-testid": testId
330
- },
331
- ...rootProps,
328
+ var ToolbarMenu = ({ __menuScope, classNames, ...props }) => {
329
+ const items = useMenuItems(void 0, void 0, "ToolbarMenu", __menuScope);
330
+ const { attendableId, alwaysActive } = useMenuScoped("ToolbarMenu", __menuScope);
331
+ const { hasAttention } = useAttention(attendableId);
332
+ return /* @__PURE__ */ React3.createElement(NaturalToolbar.Root, {
333
+ ...props,
334
+ classNames: [
335
+ attendableId,
332
336
  classNames
337
+ ],
338
+ disabled: !alwaysActive && !hasAttention
339
+ }, items?.map((item) => /* @__PURE__ */ React3.createElement(ToolbarMenuItem, {
340
+ key: item.id,
341
+ __menuScope,
342
+ item
343
+ })));
344
+ };
345
+ var ToolbarMenuItem = ({ __menuScope, item }) => {
346
+ if (isSeparator(item)) {
347
+ return /* @__PURE__ */ React3.createElement(NaturalToolbar.Separator, {
348
+ variant: item.properties.variant
333
349
  });
334
- } finally {
335
- _effect.f();
336
350
  }
351
+ if (isMenuGroup(item)) {
352
+ if (item.properties.variant === "dropdownMenu") {
353
+ return /* @__PURE__ */ React3.createElement(DropdownMenuToolbarItem, {
354
+ __menuScope,
355
+ group: item
356
+ });
357
+ } else {
358
+ return /* @__PURE__ */ React3.createElement(ToggleGroupToolbarItem, {
359
+ __menuScope,
360
+ group: item
361
+ });
362
+ }
363
+ }
364
+ return /* @__PURE__ */ React3.createElement(ActionToolbarItem, {
365
+ __menuScope,
366
+ action: item
367
+ });
337
368
  };
338
- var ToggleGroupToolbarItem = ({ group, items: propsItems, __menuScope }) => {
339
- var _effect = _useSignals4();
340
- try {
341
- const items = useMenuItems(group, propsItems, "ToggleGroupToolbarItem", __menuScope);
342
- const { selectCardinality, value } = group.properties;
343
- return (
344
- // TODO(thure): The type here is difficult to manage, what do?
345
- // @ts-ignore
346
- /* @__PURE__ */ React4.createElement(
347
- NaturalToolbar.ToggleGroup,
348
- {
349
- type: selectCardinality,
350
- value
351
- },
352
- // TODO(thure): Handle other menu item types.
353
- items.map((action) => /* @__PURE__ */ React4.createElement(ToggleGroupItem, {
354
- key: action.id,
355
- group,
356
- action
357
- }))
358
- )
359
- );
360
- } finally {
361
- _effect.f();
369
+
370
+ // src/components/Menu.tsx
371
+ var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-menu/src/components/Menu.tsx";
372
+ var MENU_NAME = "Menu";
373
+ var [createMenuContext, createMenuScope] = createContextScope(MENU_NAME, []);
374
+ var nullItemsAtom = Atom.make(null);
375
+ var defaultItemsAccessor = () => nullItemsAtom;
376
+ var menuContextDefaults = {
377
+ iconSize: 5,
378
+ items: defaultItemsAccessor,
379
+ onAction: void 0,
380
+ menuItemsAtom: Atom.make(/* @__PURE__ */ new Map()),
381
+ addMenuItems: () => {
382
+ },
383
+ removeMenuItems: () => {
362
384
  }
363
385
  };
364
- var ToolbarMenu = ({ __menuScope, classNames, textBlockWidth: wrapContents, ...props }) => {
365
- var _effect = _useSignals4();
366
- try {
367
- const items = useMenuItems(void 0, void 0, "ToolbarMenu", __menuScope);
368
- const { attendableId } = useMenu("ToolbarMenu", __menuScope);
369
- const { hasAttention } = useAttention(attendableId);
370
- const InnerRoot = wrapContents ? "div" : Fragment;
371
- const innerRootProps = wrapContents ? {
372
- role: "none",
373
- className: mx2(textBlockWidth, toolbarLayout, "bs-full")
374
- } : {};
375
- return /* @__PURE__ */ React4.createElement(NaturalToolbar.Root, {
386
+ var [MenuContextProvider, useMenuScoped] = createMenuContext(MENU_NAME, menuContextDefaults);
387
+ var useMenuScope = createMenuScope();
388
+ var MenuDropdownContext = /* @__PURE__ */ React4.createContext({
389
+ closeMenu: () => {
390
+ }
391
+ });
392
+ var DEFAULT_PRIORITY = 100;
393
+ var sortMenuItems = (items) => [
394
+ ...items
395
+ ].sort((a, b) => {
396
+ if (a.priority !== b.priority) {
397
+ return a.priority - b.priority;
398
+ }
399
+ return a.id.localeCompare(b.id);
400
+ });
401
+ var MenuProvider = ({ children, items = menuContextDefaults.items, iconSize = menuContextDefaults.iconSize, attendableId, alwaysActive, onAction }) => {
402
+ const { scope } = useMenuScope(void 0);
403
+ const registry = useContext(RegistryContext);
404
+ const menuItemsAtom = useMemo(() => Atom.make(/* @__PURE__ */ new Map()).pipe(Atom.keepAlive), []);
405
+ const addMenuItems = useCallback3((props) => {
406
+ const priority = props.priority ?? DEFAULT_PRIORITY;
407
+ const prev = registry.get(menuItemsAtom);
408
+ const next = new Map(prev);
409
+ next.set(props.id, {
376
410
  ...props,
377
- layoutManaged: wrapContents,
378
- classNames: [
379
- attendableId && !hasAttention && "*:opacity-20 !bg-transparent",
380
- classNames
381
- ]
382
- }, /* @__PURE__ */ React4.createElement(InnerRoot, innerRootProps, items?.map((item, i) => isSeparator(item) ? /* @__PURE__ */ React4.createElement(NaturalToolbar.Separator, {
383
- key: i,
384
- variant: item.properties.variant
385
- }) : isMenuGroup(item) ? item.properties.variant === "dropdownMenu" ? (
386
- // TODO(thure): Figure out type narrowing that doesn’t require so much use of `as`.
387
- /* @__PURE__ */ React4.createElement(DropdownMenuToolbarItem, {
388
- key: item.id,
389
- group: item
390
- })
391
- ) : (
392
- // TODO(thure): Figure out type narrowing that doesn’t require so much use of `as`.
393
- /* @__PURE__ */ React4.createElement(ToggleGroupToolbarItem, {
394
- key: item.id,
395
- group: item
396
- })
397
- ) : (
398
- // TODO(thure): Figure out type narrowing that doesn’t require so much use of `as`.
399
- /* @__PURE__ */ React4.createElement(ActionToolbarItem, {
400
- key: item.id,
401
- action: item
402
- })
403
- ))));
404
- } finally {
405
- _effect.f();
411
+ priority
412
+ });
413
+ registry.set(menuItemsAtom, next);
414
+ }, [
415
+ registry,
416
+ menuItemsAtom
417
+ ]);
418
+ const removeMenuItems = useCallback3((id) => {
419
+ const prev = registry.get(menuItemsAtom);
420
+ const next = new Map(prev);
421
+ next.delete(id);
422
+ registry.set(menuItemsAtom, next);
423
+ }, [
424
+ registry,
425
+ menuItemsAtom
426
+ ]);
427
+ return /* @__PURE__ */ React4.createElement(MenuContextProvider, {
428
+ items,
429
+ iconSize,
430
+ attendableId,
431
+ alwaysActive,
432
+ menuItemsAtom,
433
+ addMenuItems,
434
+ removeMenuItems,
435
+ onAction,
436
+ scope
437
+ }, children);
438
+ };
439
+ var resolveItems = (baseItems, group, entries) => {
440
+ const applicable = [
441
+ ...entries.values()
442
+ ].filter((entry) => !entry.groupFilter || entry.groupFilter(group));
443
+ if (applicable.length === 0) {
444
+ return baseItems;
445
+ }
446
+ const sorted = sortMenuItems(applicable);
447
+ const replacements = sorted.filter((entry) => entry.mode === "replacement");
448
+ if (replacements.length > 0) {
449
+ if (replacements.length > 1) {
450
+ log.warn("multiple replacement entries found", {
451
+ ids: replacements.map((r) => r.id).join(", "),
452
+ using: replacements[0].id
453
+ }, {
454
+ F: __dxlog_file,
455
+ L: 152,
456
+ S: void 0,
457
+ C: (f, a) => f(...a)
458
+ });
459
+ }
460
+ return replacements[0].items;
406
461
  }
462
+ const additive = sorted.filter((entry) => entry.mode === "additive");
463
+ const additiveItems = additive.flatMap((entry) => entry.items);
464
+ if (!baseItems || baseItems.length === 0) {
465
+ return additiveItems.length > 0 ? additiveItems : null;
466
+ }
467
+ return [
468
+ ...baseItems,
469
+ ...additiveItems
470
+ ];
407
471
  };
408
-
409
- // src/hooks/useMenuActions.ts
410
- import { RegistryContext, useRxValue } from "@effect-rx/rx-react";
411
- import { useCallback as useCallback3, useContext, useEffect, useState } from "react";
412
- import { Graph, ROOT_ID } from "@dxos/app-graph";
413
- var edgesArrayToRecord = (edges) => {
414
- return Object.fromEntries(Object.entries(edges.reduce((acc, { source, target }) => {
415
- if (!acc[source]) {
416
- acc[source] = {
417
- inbound: [],
418
- outbound: []
419
- };
472
+ var useMenuItems = (group, propsItems, consumerName = "useMenuItemConsumer", __menuScope) => {
473
+ const { items, menuItemsAtom } = useMenuScoped(consumerName, __menuScope);
474
+ const groupItems = useAtomValue(items(group));
475
+ const entries = useAtomValue(menuItemsAtom) ?? /* @__PURE__ */ new Map();
476
+ const baseItems = useMemo(() => propsItems ?? groupItems ?? null, [
477
+ propsItems,
478
+ groupItems
479
+ ]);
480
+ const resolved = useMemo(() => resolveItems(baseItems, group, entries), [
481
+ baseItems,
482
+ group,
483
+ entries
484
+ ]);
485
+ return resolved ?? void 0;
486
+ };
487
+ var useMenu = (consumerName) => {
488
+ return useMenuScoped(consumerName, void 0);
489
+ };
490
+ var MenuRoot = ({ children, open, defaultOpen, onOpenChange, caller, ...props }) => {
491
+ const [menuOpen, setMenuOpen] = useControllableState2({
492
+ prop: open,
493
+ defaultProp: defaultOpen,
494
+ onChange: onOpenChange
495
+ });
496
+ const closeMenu = useCallback3(() => setMenuOpen(false), [
497
+ setMenuOpen
498
+ ]);
499
+ return /* @__PURE__ */ React4.createElement(MenuProvider, props, /* @__PURE__ */ React4.createElement(MenuDropdownContext.Provider, {
500
+ value: {
501
+ closeMenu,
502
+ caller
420
503
  }
421
- if (!acc[target]) {
422
- acc[target] = {
423
- inbound: [],
424
- outbound: []
425
- };
504
+ }, /* @__PURE__ */ React4.createElement(NaturalDropdownMenu2.Root, {
505
+ open: menuOpen,
506
+ onOpenChange: setMenuOpen
507
+ }, children)));
508
+ };
509
+ var MenuContentItem = ({ item, onClick, __menuScope }) => {
510
+ const action = item;
511
+ const handleClick = useCallback3((event) => onClick(action, event), [
512
+ action,
513
+ onClick
514
+ ]);
515
+ const { iconSize } = useMenuScoped("MenuContentItem", __menuScope);
516
+ return /* @__PURE__ */ React4.createElement(NaturalDropdownMenu2.Item, {
517
+ onClick: handleClick,
518
+ classNames: "gap-2",
519
+ disabled: action.properties?.disabled,
520
+ ...action.properties?.testId && {
521
+ "data-testid": action.properties.testId
426
522
  }
427
- const sourceEdges = acc[source];
428
- if (!sourceEdges.outbound.includes(target)) {
429
- sourceEdges.outbound.push(target);
523
+ }, action.properties?.icon && /* @__PURE__ */ React4.createElement(Icon3, {
524
+ icon: action.properties.icon,
525
+ size: iconSize
526
+ }), /* @__PURE__ */ React4.createElement(ActionLabel, {
527
+ action
528
+ }));
529
+ };
530
+ var MenuContent = ({ group, items: propsItems, caller: callerOverride, __menuScope }) => {
531
+ const { closeMenu, caller: contextCaller } = useContext(MenuDropdownContext);
532
+ const caller = callerOverride ?? contextCaller;
533
+ const { onAction } = useMenuScoped("MenuContent", __menuScope);
534
+ const resolvedItems = useMenuItems(group, propsItems, "MenuContent", __menuScope);
535
+ const handleActionClick = useCallback3((action, event) => {
536
+ if (action.properties?.disabled) {
537
+ return;
430
538
  }
431
- const targetEdges = acc[target];
432
- if (!targetEdges.inbound.includes(source)) {
433
- targetEdges.inbound.push(source);
539
+ event.stopPropagation();
540
+ closeMenu();
541
+ if (onAction) {
542
+ onAction(action, {
543
+ parent: group,
544
+ caller
545
+ });
546
+ } else {
547
+ void executeMenuAction(action, {
548
+ parent: group,
549
+ caller
550
+ });
434
551
  }
435
- return acc;
436
- }, {})));
552
+ }, [
553
+ group,
554
+ caller,
555
+ onAction,
556
+ closeMenu
557
+ ]);
558
+ return /* @__PURE__ */ React4.createElement(NaturalDropdownMenu2.Portal, null, /* @__PURE__ */ React4.createElement(NaturalDropdownMenu2.Content, null, /* @__PURE__ */ React4.createElement(NaturalDropdownMenu2.Viewport, null, resolvedItems?.map((item) => /* @__PURE__ */ React4.createElement(MenuContentItem, {
559
+ key: item.id,
560
+ item,
561
+ onClick: handleActionClick
562
+ }))), /* @__PURE__ */ React4.createElement(NaturalDropdownMenu2.Arrow, null)));
563
+ };
564
+ var Menu = {
565
+ Root: MenuRoot,
566
+ Trigger: NaturalDropdownMenu2.Trigger,
567
+ Content: MenuContent,
568
+ VirtualTrigger: NaturalDropdownMenu2.VirtualTrigger,
569
+ Toolbar: ToolbarMenu
437
570
  };
571
+
572
+ // src/hooks/useMenuActions.ts
573
+ import { RegistryContext as RegistryContext2, useAtomValue as useAtomValue2 } from "@effect-atom/atom-react";
574
+ import { useCallback as useCallback4, useContext as useContext2, useMemo as useMemo2 } from "react";
575
+ import { Graph, Node as Node3 } from "@dxos/app-graph";
438
576
  var useMenuActions = (props) => {
439
- const registry = useContext(RegistryContext);
440
- const menuGraphProps = useRxValue(props);
441
- const [graph] = useState(new Graph({
442
- registry,
443
- nodes: menuGraphProps.nodes,
444
- edges: edgesArrayToRecord(menuGraphProps.edges)
445
- }));
446
- useEffect(() => {
447
- graph.addNodes(menuGraphProps.nodes);
448
- graph.addEdges(menuGraphProps.edges);
577
+ const registry = useContext2(RegistryContext2);
578
+ const menuGraphProps = useAtomValue2(props);
579
+ const graph = useMemo2(() => {
580
+ const newGraph = Graph.make({
581
+ registry
582
+ });
583
+ newGraph.pipe(Graph.addNodes(menuGraphProps.nodes), Graph.addEdges(menuGraphProps.edges));
584
+ return newGraph;
449
585
  }, [
586
+ registry,
450
587
  menuGraphProps
451
588
  ]);
452
- const useGroupItems = useCallback3((sourceNode) => {
453
- const items = useRxValue(graph.connections(sourceNode?.id || ROOT_ID));
454
- return items;
589
+ const items = useCallback4((group) => {
590
+ return graph.connections(group?.id || Node3.RootId, "child");
455
591
  }, [
456
592
  graph
457
593
  ]);
458
594
  return {
459
- useGroupItems
595
+ items
460
596
  };
461
597
  };
462
598
 
463
599
  // src/builder.ts
464
600
  import { invariant } from "@dxos/invariant";
465
- var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-menu/src/builder.ts";
466
- var MenuBuilder = Object.freeze({
467
- make: () => new MenuBuilderImpl({
468
- nodes: [],
469
- edges: []
470
- }, "root")
471
- });
601
+ var __dxlog_file2 = "/__w/dxos/dxos/packages/ui/react-ui-menu/src/builder.ts";
472
602
  var MenuBuilderImpl = class _MenuBuilderImpl {
603
+ _data;
604
+ _rootId;
605
+ _separatorCount = 0;
473
606
  constructor(_data, _rootId) {
474
607
  this._data = _data;
475
608
  this._rootId = _rootId;
476
609
  }
477
- action(id, invoke, properties) {
478
- this._data.nodes.push(createMenuAction(id, invoke, properties));
479
- this._data.edges.push({
480
- source: this._rootId,
481
- target: id
482
- });
483
- return this;
484
- }
485
610
  root(props) {
486
611
  invariant(this._rootId === "root", "Root group can only be at the top level", {
487
- F: __dxlog_file,
488
- L: 42,
612
+ F: __dxlog_file2,
613
+ L: 41,
489
614
  S: this,
490
615
  A: [
491
616
  "this._rootId === 'root'",
@@ -493,8 +618,8 @@ var MenuBuilderImpl = class _MenuBuilderImpl {
493
618
  ]
494
619
  });
495
620
  invariant(this._data.nodes.find((node) => node.id === "root") === void 0, "Root group can only be created once", {
496
- F: __dxlog_file,
497
- L: 43,
621
+ F: __dxlog_file2,
622
+ L: 42,
498
623
  S: this,
499
624
  A: [
500
625
  "this._data.nodes.find((node) => node.id === 'root') === undefined",
@@ -504,36 +629,71 @@ var MenuBuilderImpl = class _MenuBuilderImpl {
504
629
  this._data.nodes.push(createMenuItemGroup("root", props));
505
630
  return this;
506
631
  }
507
- group(id, properties, cb) {
508
- this._data.nodes.push(createMenuItemGroup(id, properties));
632
+ build() {
633
+ return this._data;
634
+ }
635
+ action(id, props, invoke) {
636
+ this._data.nodes.push(createMenuAction(id, invoke, props));
637
+ this._data.edges.push({
638
+ source: this._rootId,
639
+ target: id,
640
+ relation: "child"
641
+ });
642
+ return this;
643
+ }
644
+ group(id, props, cb) {
645
+ this._data.nodes.push(createMenuItemGroup(id, props));
509
646
  this._data.edges.push({
510
647
  source: this._rootId,
511
- target: id
648
+ target: id,
649
+ relation: "child"
512
650
  });
513
651
  cb(new _MenuBuilderImpl(this._data, id));
514
652
  return this;
515
653
  }
516
- build() {
517
- return this._data;
654
+ subgraph(subgraph) {
655
+ this._data.nodes.push(...subgraph.nodes);
656
+ this._data.edges.push(...subgraph.edges);
657
+ return this;
658
+ }
659
+ separator(id, variant = "gap") {
660
+ id ??= `separator-${++this._separatorCount}`;
661
+ this._data.nodes.push({
662
+ id,
663
+ type: MenuSeparatorType,
664
+ properties: {
665
+ variant
666
+ },
667
+ data: void 0
668
+ });
669
+ this._data.edges.push({
670
+ source: this._rootId,
671
+ target: id,
672
+ relation: "child"
673
+ });
674
+ return this;
518
675
  }
519
676
  };
677
+ var MenuBuilder = Object.freeze({
678
+ make: () => new MenuBuilderImpl({
679
+ nodes: [],
680
+ edges: []
681
+ }, "root")
682
+ });
520
683
  export {
521
- DropdownMenu,
684
+ Menu,
522
685
  MenuBuilder,
523
- MenuProvider,
524
686
  MenuSeparatorType,
525
- ToolbarMenu,
526
687
  createGapSeparator,
527
688
  createLineSeparator,
528
689
  createMenuAction,
529
690
  createMenuItemGroup,
530
- createMenuScope,
531
691
  isMenuGroup,
532
692
  isSeparator,
533
693
  menuContextDefaults,
534
- rxFromSignal,
535
694
  useMenu,
536
695
  useMenuActions,
537
- useMenuItems
696
+ useMenuItems,
697
+ useMenuScoped
538
698
  };
539
699
  //# sourceMappingURL=index.mjs.map