@dxos/react-ui-menu 0.8.4-main.72ec0f3 → 0.8.4-main.74a063c4e0

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