@dxos/react-ui-menu 0.8.4-main.bc674ce → 0.8.4-main.bcb3aa67d6

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