@dxos/react-ui-menu 0.8.4-main.c4373fc → 0.8.4-main.d05673bc65

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