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

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 +537 -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 +537 -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 +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 +4 -2
  12. package/dist/types/src/components/DropdownMenu.d.ts.map +1 -1
  13. package/dist/types/src/components/Menu.d.ts +54 -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 +11 -6
  20. package/dist/types/src/components/ToolbarMenu.d.ts.map +1 -1
  21. package/dist/types/src/components/ToolbarMenu.stories.d.ts +8 -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 +5 -5
  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 +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.stories.tsx +265 -0
  43. package/src/components/Menu.test.tsx +448 -0
  44. package/src/components/Menu.tsx +349 -0
  45. package/src/components/ToolbarMenu.stories.tsx +34 -25
  46. package/src/components/ToolbarMenu.tsx +136 -91
  47. package/src/components/index.ts +1 -3
  48. package/src/hooks/useMenuActions.ts +21 -51
  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 +28 -14
  54. package/dist/types/src/components/MenuContext.d.ts +0 -15
  55. package/dist/types/src/components/MenuContext.d.ts.map +0 -1
  56. package/src/components/MenuContext.tsx +0 -54
@@ -1,46 +1,37 @@
1
- // src/index.ts
2
- import { atomFromSignal } 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, { createContext, 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 = "Menu";
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
+ import { composable, composableProps } 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,
@@ -204,290 +187,430 @@ var DropdownMenu = {
204
187
  };
205
188
 
206
189
  // 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, toolbarInactive, 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 { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
226
- const Root = icon ? NaturalToolbar.IconButton : NaturalToolbar.Button;
227
- const rootProps = icon ? {
228
- icon,
229
- size: iconSize,
230
- iconOnly,
231
- label: actionLabel(action, t)
232
- } : {
233
- children: /* @__PURE__ */ React4.createElement(ActionLabel, {
234
- action
235
- })
236
- };
237
- const handleClick = useCallback2(() => action.data?.(), [
238
- action
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();
190
+ var ActionToolbarItem = ({ __menuScope, action }) => {
191
+ const { iconSize, onAction } = useMenuScoped("ActionToolbarItem", __menuScope);
192
+ const { t } = useTranslation2(translationKey);
193
+ const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
194
+ const buttonVariant = action.properties.variant === "primary" ? "primary" : "ghost";
195
+ const handleClick = useCallback2(() => {
196
+ if (onAction) {
197
+ onAction(action, {});
198
+ } else {
199
+ void executeMenuAction(action);
200
+ }
201
+ }, [
202
+ action,
203
+ onAction
204
+ ]);
205
+ const commonProps = {
206
+ variant: buttonVariant,
207
+ disabled,
208
+ classNames,
209
+ onClick: handleClick,
210
+ ...testId && {
211
+ "data-testid": testId
212
+ }
213
+ };
214
+ if (hidden) {
215
+ return null;
253
216
  }
217
+ return icon ? /* @__PURE__ */ React3.createElement(NaturalToolbar.IconButton, {
218
+ key: action.id,
219
+ ...commonProps,
220
+ icon,
221
+ size: iconSize,
222
+ iconOnly,
223
+ label: actionLabel(action, t)
224
+ }) : /* @__PURE__ */ React3.createElement(NaturalToolbar.Button, {
225
+ key: action.id,
226
+ ...commonProps
227
+ }, /* @__PURE__ */ React3.createElement(ActionLabel, {
228
+ action
229
+ }));
230
+ };
231
+ var DropdownMenuToolbarItem = ({ __menuScope, group, items: propsItems }) => {
232
+ const { iconOnly, disabled, testId } = group.properties;
233
+ const { t } = useTranslation2(translationKey);
234
+ const { iconSize } = useMenuScoped("DropdownMenuToolbarItem", __menuScope);
235
+ const items = useMenuItems(group, propsItems, "DropdownMenuToolbarItem", __menuScope);
236
+ const activeItem = items?.find((item) => !!item.properties.checked);
237
+ const icon = group.properties.applyActive && // TODO(thure): Handle other menu item types.
238
+ activeItem?.properties.icon || group.properties.icon;
239
+ const Root = icon ? NaturalToolbar.IconButton : NaturalToolbar.Button;
240
+ const labelAction = group.properties.applyActive && activeItem ? activeItem : group;
241
+ const rootProps = icon ? {
242
+ icon,
243
+ size: iconSize,
244
+ iconOnly,
245
+ label: actionLabel(labelAction, t),
246
+ caretDown: true
247
+ } : {
248
+ children: /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(ActionLabel, {
249
+ action: labelAction
250
+ }), /* @__PURE__ */ React3.createElement(Icon2, {
251
+ size: 3,
252
+ icon: "ph--caret-down--bold",
253
+ classNames: "ms-1"
254
+ }))
255
+ };
256
+ return /* @__PURE__ */ React3.createElement(DropdownMenu.Root, {
257
+ group,
258
+ items
259
+ }, /* @__PURE__ */ React3.createElement(DropdownMenu.Trigger, {
260
+ asChild: true
261
+ }, /* @__PURE__ */ React3.createElement(Root, {
262
+ variant: "ghost",
263
+ disabled,
264
+ ...rootProps,
265
+ ...testId && {
266
+ "data-testid": testId
267
+ }
268
+ })));
269
+ };
270
+ var ToggleGroupItem = ({ __menuScope, group, action }) => {
271
+ const { iconSize, onAction } = useMenuScoped("ToggleGroupItem", __menuScope);
272
+ const { t } = useTranslation2(translationKey);
273
+ const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
274
+ const handleClick = useCallback2(() => {
275
+ if (onAction) {
276
+ onAction(action, {
277
+ parent: group
278
+ });
279
+ } else {
280
+ void executeMenuAction(action, {
281
+ parent: group
282
+ });
283
+ }
284
+ }, [
285
+ action,
286
+ group,
287
+ onAction
288
+ ]);
289
+ const commonProps = {
290
+ value: action.id,
291
+ disabled,
292
+ variant: "ghost",
293
+ classNames,
294
+ onClick: handleClick,
295
+ ...testId && {
296
+ "data-testid": testId
297
+ }
298
+ };
299
+ return hidden ? null : icon ? /* @__PURE__ */ React3.createElement(NaturalToolbar.ToggleGroupIconItem, {
300
+ ...commonProps,
301
+ icon,
302
+ size: iconSize,
303
+ iconOnly,
304
+ label: actionLabel(action, t)
305
+ }) : /* @__PURE__ */ React3.createElement(NaturalToolbar.ToggleGroupItem, commonProps, /* @__PURE__ */ React3.createElement(ActionLabel, {
306
+ action
307
+ }));
254
308
  };
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();
309
+ var ToggleGroupToolbarItem = ({ __menuScope, group, items: itemsProp }) => {
310
+ const items = useMenuItems(group, itemsProp, "ToggleGroupToolbarItem", __menuScope);
311
+ const { selectCardinality } = group.properties;
312
+ const children = items.map((action) => /* @__PURE__ */ React3.createElement(ToggleGroupItem, {
313
+ key: action.id,
314
+ group,
315
+ action
316
+ }));
317
+ if (selectCardinality === "multiple") {
318
+ return /* @__PURE__ */ React3.createElement(NaturalToolbar.ToggleGroup, {
319
+ type: "multiple",
320
+ value: group.properties.value
321
+ }, children);
322
+ } else {
323
+ return /* @__PURE__ */ React3.createElement(NaturalToolbar.ToggleGroup, {
324
+ type: "single",
325
+ value: group.properties.value
326
+ }, children);
297
327
  }
298
328
  };
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,
332
- classNames
329
+ var ToolbarMenu = composable(({ __menuScope, children, ...props }, forwardedRef) => {
330
+ const items = useMenuItems(void 0, void 0, "ToolbarMenu", __menuScope);
331
+ const { attendableId, alwaysActive } = useMenuScoped("ToolbarMenu", __menuScope);
332
+ const { hasAttention } = useAttention(attendableId);
333
+ return /* @__PURE__ */ React3.createElement(NaturalToolbar.Root, {
334
+ ...composableProps(props, {
335
+ classNames: attendableId
336
+ }),
337
+ disabled: !alwaysActive && !hasAttention,
338
+ ref: forwardedRef
339
+ }, items?.map((item) => /* @__PURE__ */ React3.createElement(ToolbarMenuItem, {
340
+ key: item.id,
341
+ __menuScope,
342
+ item
343
+ })), children);
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): 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();
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: textBlockWidthParam, ...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 = textBlockWidthParam ? "div" : Fragment;
371
- const innerRootProps = textBlockWidthParam ? {
372
- role: "none",
373
- className: mx2(textBlockWidth, toolbarLayout, "bs-full")
374
- } : {};
375
- return /* @__PURE__ */ React4.createElement(NaturalToolbar.Root, {
376
- ...props,
377
- classNames: [
378
- attendableId && !hasAttention && toolbarInactive,
379
- classNames
380
- ],
381
- layoutManaged: textBlockWidthParam
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();
386
+ var [MenuContextProvider, useMenuScoped] = createMenuContext(MENU_NAME, menuContextDefaults);
387
+ var useMenuScope = createMenuScope();
388
+ var MenuDropdownContext = /* @__PURE__ */ createContext({
389
+ closeMenu: () => {
388
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, {
410
+ ...props,
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);
389
438
  };
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
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)
396
458
  });
397
459
  }
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();
460
+ return replacements[0].items;
414
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
+ ];
415
471
  };
416
-
417
- // src/hooks/useMenuActions.ts
418
- import { RegistryContext, useAtomValue } from "@effect-atom/atom-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
- };
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
428
503
  }
429
- if (!acc[target]) {
430
- acc[target] = {
431
- inbound: [],
432
- outbound: []
433
- };
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
434
522
  }
435
- const sourceEdges = acc[source];
436
- if (!sourceEdges.outbound.includes(target)) {
437
- 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;
438
538
  }
439
- const targetEdges = acc[target];
440
- if (!targetEdges.inbound.includes(source)) {
441
- 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
+ });
442
551
  }
443
- return acc;
444
- }, {})));
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)));
445
563
  };
564
+ var Menu = {
565
+ Root: MenuRoot,
566
+ Trigger: NaturalDropdownMenu2.Trigger,
567
+ Content: MenuContent,
568
+ VirtualTrigger: NaturalDropdownMenu2.VirtualTrigger,
569
+ Toolbar: ToolbarMenu
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";
446
576
  var useMenuActions = (props) => {
447
- const registry = useContext(RegistryContext);
448
- const menuGraphProps = useAtomValue(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);
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;
457
585
  }, [
586
+ registry,
458
587
  menuGraphProps
459
588
  ]);
460
- const useGroupItems = useCallback3((sourceNode) => {
461
- const items = useAtomValue(graph.connections(sourceNode?.id || ROOT_ID));
462
- return items;
589
+ const items = useCallback4((group) => {
590
+ return graph.connections(group?.id || Node3.RootId, "child");
463
591
  }, [
464
592
  graph
465
593
  ]);
466
594
  return {
467
- useGroupItems
595
+ items
468
596
  };
469
597
  };
470
598
 
471
599
  // src/builder.ts
472
600
  import { invariant } from "@dxos/invariant";
473
- var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-menu/src/builder.ts";
474
- var MenuBuilder = Object.freeze({
475
- make: () => new MenuBuilderImpl({
476
- nodes: [],
477
- edges: []
478
- }, "root")
479
- });
601
+ var __dxlog_file2 = "/__w/dxos/dxos/packages/ui/react-ui-menu/src/builder.ts";
480
602
  var MenuBuilderImpl = class _MenuBuilderImpl {
481
603
  _data;
482
604
  _rootId;
605
+ _separatorCount = 0;
483
606
  constructor(_data, _rootId) {
484
607
  this._data = _data;
485
608
  this._rootId = _rootId;
486
609
  }
487
610
  root(props) {
488
611
  invariant(this._rootId === "root", "Root group can only be at the top level", {
489
- F: __dxlog_file,
490
- L: 36,
612
+ F: __dxlog_file2,
613
+ L: 41,
491
614
  S: this,
492
615
  A: [
493
616
  "this._rootId === 'root'",
@@ -495,8 +618,8 @@ var MenuBuilderImpl = class _MenuBuilderImpl {
495
618
  ]
496
619
  });
497
620
  invariant(this._data.nodes.find((node) => node.id === "root") === void 0, "Root group can only be created once", {
498
- F: __dxlog_file,
499
- L: 37,
621
+ F: __dxlog_file2,
622
+ L: 42,
500
623
  S: this,
501
624
  A: [
502
625
  "this._data.nodes.find((node) => node.id === 'root') === undefined",
@@ -506,11 +629,15 @@ var MenuBuilderImpl = class _MenuBuilderImpl {
506
629
  this._data.nodes.push(createMenuItemGroup("root", props));
507
630
  return this;
508
631
  }
632
+ build() {
633
+ return this._data;
634
+ }
509
635
  action(id, props, invoke) {
510
636
  this._data.nodes.push(createMenuAction(id, invoke, props));
511
637
  this._data.edges.push({
512
638
  source: this._rootId,
513
- target: id
639
+ target: id,
640
+ relation: "child"
514
641
  });
515
642
  return this;
516
643
  }
@@ -518,32 +645,55 @@ var MenuBuilderImpl = class _MenuBuilderImpl {
518
645
  this._data.nodes.push(createMenuItemGroup(id, props));
519
646
  this._data.edges.push({
520
647
  source: this._rootId,
521
- target: id
648
+ target: id,
649
+ relation: "child"
522
650
  });
523
651
  cb(new _MenuBuilderImpl(this._data, id));
524
652
  return this;
525
653
  }
526
- build() {
527
- 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;
528
675
  }
529
676
  };
677
+ var MenuBuilder = Object.freeze({
678
+ make: () => new MenuBuilderImpl({
679
+ nodes: [],
680
+ edges: []
681
+ }, "root")
682
+ });
530
683
  export {
531
- DropdownMenu,
684
+ Menu,
532
685
  MenuBuilder,
533
- MenuProvider,
534
686
  MenuSeparatorType,
535
- ToolbarMenu,
536
- atomFromSignal,
537
687
  createGapSeparator,
538
688
  createLineSeparator,
539
689
  createMenuAction,
540
690
  createMenuItemGroup,
541
- createMenuScope,
542
691
  isMenuGroup,
543
692
  isSeparator,
544
693
  menuContextDefaults,
545
694
  useMenu,
546
695
  useMenuActions,
547
- useMenuItems
696
+ useMenuItems,
697
+ useMenuScoped
548
698
  };
549
699
  //# sourceMappingURL=index.mjs.map