@dxos/react-ui-menu 0.8.4-main.3eb6e50203 → 0.8.4-main.3fbcb4aa9b

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