@dxos/react-ui-menu 0.8.4-main.ef1bc66f44 → 0.8.4-main.f466a3d56e

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 (60) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +396 -217
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/browser/translations.mjs +14 -0
  6. package/dist/lib/browser/translations.mjs.map +7 -0
  7. package/dist/lib/node-esm/index.mjs +396 -217
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/lib/node-esm/translations.mjs +16 -0
  11. package/dist/lib/node-esm/translations.mjs.map +7 -0
  12. package/dist/types/src/builder.d.ts +26 -1
  13. package/dist/types/src/builder.d.ts.map +1 -1
  14. package/dist/types/src/builder.test.d.ts +2 -0
  15. package/dist/types/src/builder.test.d.ts.map +1 -0
  16. package/dist/types/src/components/ActionLabel.d.ts.map +1 -1
  17. package/dist/types/src/components/DropdownMenu.d.ts +4 -2
  18. package/dist/types/src/components/DropdownMenu.d.ts.map +1 -1
  19. package/dist/types/src/components/Menu.d.ts +50 -0
  20. package/dist/types/src/components/Menu.d.ts.map +1 -0
  21. package/dist/types/src/components/Menu.stories.d.ts +21 -0
  22. package/dist/types/src/components/Menu.stories.d.ts.map +1 -0
  23. package/dist/types/src/components/Menu.test.d.ts +2 -0
  24. package/dist/types/src/components/Menu.test.d.ts.map +1 -0
  25. package/dist/types/src/components/ToolbarMenu.d.ts +5 -5
  26. package/dist/types/src/components/ToolbarMenu.d.ts.map +1 -1
  27. package/dist/types/src/components/ToolbarMenu.stories.d.ts +15 -1
  28. package/dist/types/src/components/ToolbarMenu.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/index.d.ts +1 -3
  30. package/dist/types/src/components/index.d.ts.map +1 -1
  31. package/dist/types/src/hooks/useMenuActions.d.ts +11 -3
  32. package/dist/types/src/hooks/useMenuActions.d.ts.map +1 -1
  33. package/dist/types/src/index.d.ts +1 -1
  34. package/dist/types/src/index.d.ts.map +1 -1
  35. package/dist/types/src/testing/generator.d.ts +10 -6
  36. package/dist/types/src/testing/generator.d.ts.map +1 -1
  37. package/dist/types/src/types.d.ts +25 -3
  38. package/dist/types/src/types.d.ts.map +1 -1
  39. package/dist/types/src/util.d.ts +2 -0
  40. package/dist/types/src/util.d.ts.map +1 -1
  41. package/dist/types/tsconfig.tsbuildinfo +1 -1
  42. package/package.json +30 -21
  43. package/src/builder.test.ts +224 -0
  44. package/src/builder.ts +68 -18
  45. package/src/components/ActionLabel.tsx +4 -3
  46. package/src/components/DropdownMenu.tsx +3 -4
  47. package/src/components/Menu.stories.tsx +266 -0
  48. package/src/components/Menu.test.tsx +447 -0
  49. package/src/components/Menu.tsx +348 -0
  50. package/src/components/ToolbarMenu.stories.tsx +41 -27
  51. package/src/components/ToolbarMenu.tsx +123 -90
  52. package/src/components/index.ts +1 -3
  53. package/src/hooks/useMenuActions.ts +26 -15
  54. package/src/index.ts +1 -1
  55. package/src/testing/generator.ts +24 -19
  56. package/src/types.ts +26 -2
  57. package/src/util.ts +2 -1
  58. package/dist/types/src/components/MenuContext.d.ts +0 -15
  59. package/dist/types/src/components/MenuContext.d.ts.map +0 -1
  60. 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);
217
- 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
- };
180
+ var ActionToolbarItem = ({ __menuScope, action }) => {
181
+ const { iconSize, onAction } = useMenuScoped("ActionToolbarItem", __menuScope);
182
+ const { t } = useTranslation2(translationKey2);
183
+ const { icon, iconOnly = true, disabled, testId, hidden, classNames, iconClassNames } = action.properties;
184
+ const buttonVariant = action.properties.variant === "primary" ? "primary" : "ghost";
229
185
  const handleClick = useCallback2(() => {
230
186
  if (onAction) {
231
187
  onAction(action, {});
@@ -236,49 +192,66 @@ 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
+ iconClassNames,
214
+ label: actionLabel(action, t)
215
+ }) : /* @__PURE__ */ React3.createElement(NaturalToolbar.Button, {
216
+ key: action.id,
217
+ ...commonProps
218
+ }, /* @__PURE__ */ React3.createElement(ActionLabel, {
219
+ action
220
+ }));
250
221
  };
251
- var DropdownMenuToolbarItem = ({ group, items: propsItems, __menuScope }) => {
222
+ var DropdownMenuToolbarItem = ({ __menuScope, group, items: propsItems }) => {
252
223
  const { iconOnly, disabled, testId } = group.properties;
253
- const { t } = useTranslation2(translationKey);
254
- const { iconSize } = useMenu("DropdownMenuToolbarItem", __menuScope);
224
+ const { t } = useTranslation2(translationKey2);
225
+ const { iconSize } = useMenuScoped("DropdownMenuToolbarItem", __menuScope);
255
226
  const items = useMenuItems(group, propsItems, "DropdownMenuToolbarItem", __menuScope);
256
227
  const activeItem = items?.find((item) => !!item.properties.checked);
257
228
  const icon = group.properties.applyActive && // TODO(thure): Handle other menu item types.
258
229
  activeItem?.properties.icon || group.properties.icon;
230
+ const iconClassNames = group.properties.applyActive && activeItem?.properties.iconClassNames || group.properties.iconClassNames;
259
231
  const Root = icon ? NaturalToolbar.IconButton : NaturalToolbar.Button;
260
232
  const labelAction = group.properties.applyActive && activeItem ? activeItem : group;
261
233
  const rootProps = icon ? {
262
234
  icon,
263
235
  size: iconSize,
264
236
  iconOnly,
237
+ iconClassNames,
265
238
  label: actionLabel(labelAction, t),
266
239
  caretDown: true
267
240
  } : {
268
- children: /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(ActionLabel, {
241
+ children: /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(ActionLabel, {
269
242
  action: labelAction
270
- }), /* @__PURE__ */ React4.createElement(Icon2, {
243
+ }), /* @__PURE__ */ React3.createElement(Icon2, {
271
244
  size: 3,
272
245
  icon: "ph--caret-down--bold",
273
- classNames: "mis-1"
246
+ classNames: "ms-1"
274
247
  }))
275
248
  };
276
- return /* @__PURE__ */ React4.createElement(DropdownMenu.Root, {
249
+ return /* @__PURE__ */ React3.createElement(DropdownMenu.Root, {
277
250
  group,
278
251
  items
279
- }, /* @__PURE__ */ React4.createElement(DropdownMenu.Trigger, {
252
+ }, /* @__PURE__ */ React3.createElement(DropdownMenu.Trigger, {
280
253
  asChild: true
281
- }, /* @__PURE__ */ React4.createElement(Root, {
254
+ }, /* @__PURE__ */ React3.createElement(Root, {
282
255
  variant: "ghost",
283
256
  disabled,
284
257
  ...rootProps,
@@ -287,10 +260,10 @@ var DropdownMenuToolbarItem = ({ group, items: propsItems, __menuScope }) => {
287
260
  }
288
261
  })));
289
262
  };
290
- var ToggleGroupItem = ({ group, action, __menuScope }) => {
291
- const { iconSize, onAction } = useMenu("ToggleGroupItem", __menuScope);
292
- const { t } = useTranslation2(translationKey);
293
- const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
263
+ var ToggleGroupItem = ({ __menuScope, group, action }) => {
264
+ const { iconSize, onAction } = useMenuScoped("ToggleGroupItem", __menuScope);
265
+ const { t } = useTranslation2(translationKey2);
266
+ const { icon, iconOnly = true, disabled, testId, hidden, classNames, iconClassNames } = action.properties;
294
267
  const handleClick = useCallback2(() => {
295
268
  if (onAction) {
296
269
  onAction(action, {
@@ -306,168 +279,342 @@ var ToggleGroupItem = ({ group, action, __menuScope }) => {
306
279
  group,
307
280
  onAction
308
281
  ]);
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,
282
+ const commonProps = {
322
283
  value: action.id,
323
284
  disabled,
324
- onClick: handleClick,
325
285
  variant: "ghost",
286
+ classNames,
287
+ onClick: handleClick,
326
288
  ...testId && {
327
289
  "data-testid": testId
328
- },
329
- ...rootProps,
330
- classNames
331
- });
290
+ }
291
+ };
292
+ return hidden ? null : icon ? /* @__PURE__ */ React3.createElement(NaturalToolbar.ToggleGroupIconItem, {
293
+ ...commonProps,
294
+ icon,
295
+ size: iconSize,
296
+ iconOnly,
297
+ iconClassNames,
298
+ label: actionLabel(action, t)
299
+ }) : /* @__PURE__ */ React3.createElement(NaturalToolbar.ToggleGroupItem, commonProps, /* @__PURE__ */ React3.createElement(ActionLabel, {
300
+ action
301
+ }));
332
302
  };
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
- );
303
+ var ToggleGroupToolbarItem = ({ __menuScope, group, items: itemsProp }) => {
304
+ const items = useMenuItems(group, itemsProp, "ToggleGroupToolbarItem", __menuScope);
305
+ const { selectCardinality } = group.properties;
306
+ const children = items.map((action) => /* @__PURE__ */ React3.createElement(ToggleGroupItem, {
307
+ key: action.id,
308
+ group,
309
+ action
310
+ }));
311
+ if (selectCardinality === "multiple") {
312
+ return /* @__PURE__ */ React3.createElement(NaturalToolbar.ToggleGroup, {
313
+ type: "multiple",
314
+ value: group.properties.value
315
+ }, children);
316
+ } else {
317
+ return /* @__PURE__ */ React3.createElement(NaturalToolbar.ToggleGroup, {
318
+ type: "single",
319
+ value: group.properties.value
320
+ }, children);
321
+ }
353
322
  };
354
- var ToolbarMenu = ({ __menuScope, classNames, textBlockWidth: textBlockWidthProp, ...props }) => {
323
+ var ToolbarMenu = composable(({ __menuScope, children, ...props }, forwardedRef) => {
355
324
  const items = useMenuItems(void 0, void 0, "ToolbarMenu", __menuScope);
356
- const { attendableId, alwaysActive } = useMenu("ToolbarMenu", __menuScope);
325
+ const { attendableId, alwaysActive } = useMenuScoped("ToolbarMenu", __menuScope);
357
326
  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
- ],
327
+ return /* @__PURE__ */ React3.createElement(NaturalToolbar.Root, {
328
+ ...composableProps(props, {
329
+ classNames: attendableId
330
+ }),
369
331
  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,
332
+ ref: forwardedRef
333
+ }, items?.map((item) => /* @__PURE__ */ React3.createElement(ToolbarMenuItem, {
334
+ key: item.id,
335
+ __menuScope,
373
336
  item
374
- }))));
375
- };
376
- var ToolbarMenuItem = ({ item }) => {
337
+ })), children);
338
+ });
339
+ var ToolbarMenuItem = ({ __menuScope, item }) => {
377
340
  if (isSeparator(item)) {
378
- return /* @__PURE__ */ React4.createElement(NaturalToolbar.Separator, {
341
+ return /* @__PURE__ */ React3.createElement(NaturalToolbar.Separator, {
379
342
  variant: item.properties.variant
380
343
  });
381
344
  }
382
345
  if (isMenuGroup(item)) {
383
346
  if (item.properties.variant === "dropdownMenu") {
384
- return /* @__PURE__ */ React4.createElement(DropdownMenuToolbarItem, {
347
+ return /* @__PURE__ */ React3.createElement(DropdownMenuToolbarItem, {
348
+ __menuScope,
385
349
  group: item
386
350
  });
387
351
  } else {
388
- return /* @__PURE__ */ React4.createElement(ToggleGroupToolbarItem, {
352
+ return /* @__PURE__ */ React3.createElement(ToggleGroupToolbarItem, {
353
+ __menuScope,
389
354
  group: item
390
355
  });
391
356
  }
392
357
  }
393
- return /* @__PURE__ */ React4.createElement(ActionToolbarItem, {
358
+ return /* @__PURE__ */ React3.createElement(ActionToolbarItem, {
359
+ __menuScope,
394
360
  action: item
395
361
  });
396
362
  };
397
363
 
364
+ // src/components/Menu.tsx
365
+ var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-menu/src/components/Menu.tsx";
366
+ var MENU_NAME = "Menu";
367
+ var [createMenuContext, createMenuScope] = createContextScope(MENU_NAME, []);
368
+ var nullItemsAtom = Atom.make(null);
369
+ var defaultItemsAccessor = () => nullItemsAtom;
370
+ var menuContextDefaults = {
371
+ iconSize: 5,
372
+ items: defaultItemsAccessor,
373
+ onAction: void 0,
374
+ menuItemsAtom: Atom.make(/* @__PURE__ */ new Map()),
375
+ addMenuItems: () => {
376
+ },
377
+ removeMenuItems: () => {
378
+ }
379
+ };
380
+ var [MenuContextProvider, useMenuScoped] = createMenuContext(MENU_NAME, menuContextDefaults);
381
+ var useMenuScope = createMenuScope();
382
+ var MenuDropdownContext = /* @__PURE__ */ createContext({
383
+ closeMenu: () => {
384
+ }
385
+ });
386
+ var DEFAULT_PRIORITY = 100;
387
+ var sortMenuItems = (items) => [
388
+ ...items
389
+ ].sort((a, b) => {
390
+ if (a.priority !== b.priority) {
391
+ return a.priority - b.priority;
392
+ }
393
+ return a.id.localeCompare(b.id);
394
+ });
395
+ var MenuProvider = ({ children, items = menuContextDefaults.items, iconSize = menuContextDefaults.iconSize, attendableId, alwaysActive, onAction }) => {
396
+ const { scope } = useMenuScope(void 0);
397
+ const registry = useContext(RegistryContext);
398
+ const menuItemsAtom = useMemo(() => Atom.make(/* @__PURE__ */ new Map()).pipe(Atom.keepAlive), []);
399
+ const addMenuItems = useCallback3((props) => {
400
+ const priority = props.priority ?? DEFAULT_PRIORITY;
401
+ const prev = registry.get(menuItemsAtom);
402
+ const next = new Map(prev);
403
+ next.set(props.id, {
404
+ ...props,
405
+ priority
406
+ });
407
+ registry.set(menuItemsAtom, next);
408
+ }, [
409
+ registry,
410
+ menuItemsAtom
411
+ ]);
412
+ const removeMenuItems = useCallback3((id) => {
413
+ const prev = registry.get(menuItemsAtom);
414
+ const next = new Map(prev);
415
+ next.delete(id);
416
+ registry.set(menuItemsAtom, next);
417
+ }, [
418
+ registry,
419
+ menuItemsAtom
420
+ ]);
421
+ return /* @__PURE__ */ React4.createElement(MenuContextProvider, {
422
+ items,
423
+ iconSize,
424
+ attendableId,
425
+ alwaysActive,
426
+ menuItemsAtom,
427
+ addMenuItems,
428
+ removeMenuItems,
429
+ onAction,
430
+ scope
431
+ }, children);
432
+ };
433
+ var resolveItems = (baseItems, group, entries) => {
434
+ const applicable = [
435
+ ...entries.values()
436
+ ].filter((entry) => !entry.groupFilter || entry.groupFilter(group));
437
+ if (applicable.length === 0) {
438
+ return baseItems;
439
+ }
440
+ const sorted = sortMenuItems(applicable);
441
+ const replacements = sorted.filter((entry) => entry.mode === "replacement");
442
+ if (replacements.length > 0) {
443
+ if (replacements.length > 1) {
444
+ log.warn("multiple replacement entries found", {
445
+ ids: replacements.map((r) => r.id).join(", "),
446
+ using: replacements[0].id
447
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 94, S: void 0 });
448
+ }
449
+ return replacements[0].items;
450
+ }
451
+ const additive = sorted.filter((entry) => entry.mode === "additive");
452
+ const additiveItems = additive.flatMap((entry) => entry.items);
453
+ if (!baseItems || baseItems.length === 0) {
454
+ return additiveItems.length > 0 ? additiveItems : null;
455
+ }
456
+ return [
457
+ ...baseItems,
458
+ ...additiveItems
459
+ ];
460
+ };
461
+ var useMenuItems = (group, propsItems, consumerName = "useMenuItemConsumer", __menuScope) => {
462
+ const { items, menuItemsAtom } = useMenuScoped(consumerName, __menuScope);
463
+ const groupItems = useAtomValue(items(group));
464
+ const entries = useAtomValue(menuItemsAtom) ?? /* @__PURE__ */ new Map();
465
+ const baseItems = useMemo(() => propsItems ?? groupItems ?? null, [
466
+ propsItems,
467
+ groupItems
468
+ ]);
469
+ const resolved = useMemo(() => resolveItems(baseItems, group, entries), [
470
+ baseItems,
471
+ group,
472
+ entries
473
+ ]);
474
+ return resolved ?? void 0;
475
+ };
476
+ var useMenu = (consumerName) => {
477
+ return useMenuScoped(consumerName, void 0);
478
+ };
479
+ var MenuRoot = ({ children, open, defaultOpen, onOpenChange, caller, ...props }) => {
480
+ const [menuOpen, setMenuOpen] = useControllableState2({
481
+ prop: open,
482
+ defaultProp: defaultOpen,
483
+ onChange: onOpenChange
484
+ });
485
+ const closeMenu = useCallback3(() => setMenuOpen(false), [
486
+ setMenuOpen
487
+ ]);
488
+ return /* @__PURE__ */ React4.createElement(MenuProvider, props, /* @__PURE__ */ React4.createElement(MenuDropdownContext.Provider, {
489
+ value: {
490
+ closeMenu,
491
+ caller
492
+ }
493
+ }, /* @__PURE__ */ React4.createElement(NaturalDropdownMenu2.Root, {
494
+ open: menuOpen,
495
+ onOpenChange: setMenuOpen
496
+ }, children)));
497
+ };
498
+ var MenuContentItem = ({ item, onClick, __menuScope }) => {
499
+ const action = item;
500
+ const handleClick = useCallback3((event) => onClick(action, event), [
501
+ action,
502
+ onClick
503
+ ]);
504
+ const { iconSize } = useMenuScoped("MenuContentItem", __menuScope);
505
+ return /* @__PURE__ */ React4.createElement(NaturalDropdownMenu2.Item, {
506
+ onClick: handleClick,
507
+ classNames: "gap-2",
508
+ disabled: action.properties?.disabled,
509
+ ...action.properties?.testId && {
510
+ "data-testid": action.properties.testId
511
+ }
512
+ }, action.properties?.icon && /* @__PURE__ */ React4.createElement(Icon3, {
513
+ icon: action.properties.icon,
514
+ size: iconSize
515
+ }), /* @__PURE__ */ React4.createElement(ActionLabel, {
516
+ action
517
+ }));
518
+ };
519
+ var MenuContent = ({ group, items: propsItems, caller: callerOverride, __menuScope }) => {
520
+ const { closeMenu, caller: contextCaller } = useContext(MenuDropdownContext);
521
+ const caller = callerOverride ?? contextCaller;
522
+ const { onAction } = useMenuScoped("MenuContent", __menuScope);
523
+ const resolvedItems = useMenuItems(group, propsItems, "MenuContent", __menuScope);
524
+ const handleActionClick = useCallback3((action, event) => {
525
+ if (action.properties?.disabled) {
526
+ return;
527
+ }
528
+ event.stopPropagation();
529
+ closeMenu();
530
+ if (onAction) {
531
+ onAction(action, {
532
+ parent: group,
533
+ caller
534
+ });
535
+ } else {
536
+ void executeMenuAction(action, {
537
+ parent: group,
538
+ caller
539
+ });
540
+ }
541
+ }, [
542
+ group,
543
+ caller,
544
+ onAction,
545
+ closeMenu
546
+ ]);
547
+ 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, {
548
+ key: item.id,
549
+ item,
550
+ onClick: handleActionClick
551
+ }))), /* @__PURE__ */ React4.createElement(NaturalDropdownMenu2.Arrow, null)));
552
+ };
553
+ var Menu = {
554
+ Root: MenuRoot,
555
+ Trigger: NaturalDropdownMenu2.Trigger,
556
+ Content: MenuContent,
557
+ VirtualTrigger: NaturalDropdownMenu2.VirtualTrigger,
558
+ Toolbar: ToolbarMenu
559
+ };
560
+
398
561
  // src/hooks/useMenuActions.ts
399
- import { RegistryContext, useAtomValue } from "@effect-atom/atom-react";
400
- import { useCallback as useCallback3, useContext, useRef } from "react";
562
+ import { Atom as Atom2, RegistryContext as RegistryContext2, useAtomValue as useAtomValue2 } from "@effect-atom/atom-react";
563
+ import { useCallback as useCallback4, useContext as useContext2, useMemo as useMemo2 } from "react";
401
564
  import { Graph, Node as Node3 } from "@dxos/app-graph";
402
565
  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) {
566
+ const registry = useContext2(RegistryContext2);
567
+ const menuGraphProps = useAtomValue2(props);
568
+ const graph = useMemo2(() => {
407
569
  const newGraph = Graph.make({
408
570
  registry
409
571
  });
410
572
  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;
573
+ return newGraph;
574
+ }, [
575
+ registry,
576
+ menuGraphProps
577
+ ]);
578
+ const items = useCallback4((group) => {
579
+ return graph.connections(group?.id || Node3.RootId, "child");
420
580
  }, [
421
581
  graph
422
582
  ]);
423
583
  return {
424
- useGroupItems
584
+ items
425
585
  };
426
586
  };
587
+ var useMenuBuilder = (build, deps) => {
588
+ const atom = useMemo2(() => Atom2.make(build), deps);
589
+ return useMenuActions(atom);
590
+ };
427
591
 
428
592
  // src/builder.ts
429
593
  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
- });
594
+ var __dxlog_file2 = "/__w/dxos/dxos/packages/ui/react-ui-menu/src/builder.ts";
437
595
  var MenuBuilderImpl = class _MenuBuilderImpl {
438
596
  _data;
439
597
  _rootId;
598
+ _separatorCount = 0;
440
599
  constructor(_data, _rootId) {
441
600
  this._data = _data;
442
601
  this._rootId = _rootId;
443
602
  }
444
603
  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
- });
604
+ 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'"] });
605
+ 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
606
  this._data.nodes.push(createMenuItemGroup("root", props));
464
607
  return this;
465
608
  }
609
+ build() {
610
+ return this._data;
611
+ }
466
612
  action(id, props, invoke) {
467
613
  this._data.nodes.push(createMenuAction(id, invoke, props));
468
614
  this._data.edges.push({
469
615
  source: this._rootId,
470
- target: id
616
+ target: id,
617
+ relation: "child"
471
618
  });
472
619
  return this;
473
620
  }
@@ -475,31 +622,63 @@ var MenuBuilderImpl = class _MenuBuilderImpl {
475
622
  this._data.nodes.push(createMenuItemGroup(id, props));
476
623
  this._data.edges.push({
477
624
  source: this._rootId,
478
- target: id
625
+ target: id,
626
+ relation: "child"
479
627
  });
480
628
  cb(new _MenuBuilderImpl(this._data, id));
481
629
  return this;
482
630
  }
483
- build() {
484
- return this._data;
631
+ subgraph(subgraphOrCb) {
632
+ if (!subgraphOrCb) {
633
+ return this;
634
+ }
635
+ if (typeof subgraphOrCb === "function") {
636
+ subgraphOrCb(new _MenuBuilderImpl(this._data, this._rootId));
637
+ } else {
638
+ this._data.nodes.push(...subgraphOrCb.nodes);
639
+ this._data.edges.push(...subgraphOrCb.edges);
640
+ }
641
+ return this;
642
+ }
643
+ separator(variant = "gap") {
644
+ const id = `separator-${++this._separatorCount}`;
645
+ this._data.nodes.push({
646
+ id,
647
+ type: MenuSeparatorType,
648
+ properties: {
649
+ variant
650
+ },
651
+ data: void 0
652
+ });
653
+ this._data.edges.push({
654
+ source: this._rootId,
655
+ target: id,
656
+ relation: "child"
657
+ });
658
+ return this;
485
659
  }
486
660
  };
661
+ var MenuBuilder = Object.freeze({
662
+ make: () => new MenuBuilderImpl({
663
+ nodes: [],
664
+ edges: []
665
+ }, "root")
666
+ });
487
667
  export {
488
- DropdownMenu,
668
+ Menu,
489
669
  MenuBuilder,
490
- MenuProvider,
491
670
  MenuSeparatorType,
492
- ToolbarMenu,
493
671
  createGapSeparator,
494
672
  createLineSeparator,
495
673
  createMenuAction,
496
674
  createMenuItemGroup,
497
- createMenuScope,
498
675
  isMenuGroup,
499
676
  isSeparator,
500
677
  menuContextDefaults,
501
678
  useMenu,
502
679
  useMenuActions,
503
- useMenuItems
680
+ useMenuBuilder,
681
+ useMenuItems,
682
+ useMenuScoped
504
683
  };
505
684
  //# sourceMappingURL=index.mjs.map