@dxos/react-ui-menu 0.8.4-main.937b3ca → 0.8.4-main.9be5663bfe

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 +388 -212
  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 +388 -212
  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 +50 -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 +5 -5
  20. package/dist/types/src/components/ToolbarMenu.d.ts.map +1 -1
  21. package/dist/types/src/components/ToolbarMenu.stories.d.ts +3 -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 -4
  41. package/src/components/Menu.stories.tsx +265 -0
  42. package/src/components/Menu.test.tsx +447 -0
  43. package/src/components/Menu.tsx +348 -0
  44. package/src/components/ToolbarMenu.stories.tsx +28 -27
  45. package/src/components/ToolbarMenu.tsx +113 -88
  46. package/src/components/index.ts +1 -3
  47. package/src/hooks/useMenuActions.ts +18 -54
  48. package/src/index.ts +1 -1
  49. package/src/testing/generator.ts +24 -19
  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,171 +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, 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: 151,
456
+ S: void 0,
457
+ C: (f, a) => f(...a)
458
+ });
407
459
  }
408
- if (!acc[target]) {
409
- acc[target] = {
410
- inbound: [],
411
- outbound: []
412
- };
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
413
503
  }
414
- const sourceEdges = acc[source];
415
- if (!sourceEdges.outbound.includes(target)) {
416
- sourceEdges.outbound.push(target);
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
417
522
  }
418
- const targetEdges = acc[target];
419
- if (!targetEdges.inbound.includes(source)) {
420
- targetEdges.inbound.push(source);
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;
421
538
  }
422
- return acc;
423
- }, {})));
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
+ });
551
+ }
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
- if (prevPropsRef.current !== menuGraphProps) {
439
- prevPropsRef.current = menuGraphProps;
440
- graph.pipe(Graph.addNodes(menuGraphProps.nodes), Graph.addEdges(menuGraphProps.edges));
441
- }
442
- const useGroupItems = useCallback3((sourceNode) => {
443
- const items = useAtomValue(graph.connections(sourceNode?.id || Node3.RootId));
444
- return items;
583
+ newGraph.pipe(Graph.addNodes(menuGraphProps.nodes), Graph.addEdges(menuGraphProps.edges));
584
+ return newGraph;
585
+ }, [
586
+ registry,
587
+ menuGraphProps
588
+ ]);
589
+ const items = useCallback4((group) => {
590
+ return graph.connections(group?.id || Node3.RootId, "child");
445
591
  }, [
446
592
  graph
447
593
  ]);
448
594
  return {
449
- useGroupItems
595
+ items
450
596
  };
451
597
  };
452
598
 
453
599
  // src/builder.ts
454
600
  import { invariant } from "@dxos/invariant";
455
- var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-menu/src/builder.ts";
456
- var MenuBuilder = Object.freeze({
457
- make: () => new MenuBuilderImpl({
458
- nodes: [],
459
- edges: []
460
- }, "root")
461
- });
601
+ var __dxlog_file2 = "/__w/dxos/dxos/packages/ui/react-ui-menu/src/builder.ts";
462
602
  var MenuBuilderImpl = class _MenuBuilderImpl {
463
603
  _data;
464
604
  _rootId;
605
+ _separatorCount = 0;
465
606
  constructor(_data, _rootId) {
466
607
  this._data = _data;
467
608
  this._rootId = _rootId;
468
609
  }
469
610
  root(props) {
470
611
  invariant(this._rootId === "root", "Root group can only be at the top level", {
471
- F: __dxlog_file,
472
- L: 36,
612
+ F: __dxlog_file2,
613
+ L: 61,
473
614
  S: this,
474
615
  A: [
475
616
  "this._rootId === 'root'",
@@ -477,8 +618,8 @@ var MenuBuilderImpl = class _MenuBuilderImpl {
477
618
  ]
478
619
  });
479
620
  invariant(this._data.nodes.find((node) => node.id === "root") === void 0, "Root group can only be created once", {
480
- F: __dxlog_file,
481
- L: 37,
621
+ F: __dxlog_file2,
622
+ L: 62,
482
623
  S: this,
483
624
  A: [
484
625
  "this._data.nodes.find((node) => node.id === 'root') === undefined",
@@ -488,11 +629,15 @@ var MenuBuilderImpl = class _MenuBuilderImpl {
488
629
  this._data.nodes.push(createMenuItemGroup("root", props));
489
630
  return this;
490
631
  }
632
+ build() {
633
+ return this._data;
634
+ }
491
635
  action(id, props, invoke) {
492
636
  this._data.nodes.push(createMenuAction(id, invoke, props));
493
637
  this._data.edges.push({
494
638
  source: this._rootId,
495
- target: id
639
+ target: id,
640
+ relation: "child"
496
641
  });
497
642
  return this;
498
643
  }
@@ -500,31 +645,62 @@ var MenuBuilderImpl = class _MenuBuilderImpl {
500
645
  this._data.nodes.push(createMenuItemGroup(id, props));
501
646
  this._data.edges.push({
502
647
  source: this._rootId,
503
- target: id
648
+ target: id,
649
+ relation: "child"
504
650
  });
505
651
  cb(new _MenuBuilderImpl(this._data, id));
506
652
  return this;
507
653
  }
508
- build() {
509
- 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;
510
682
  }
511
683
  };
684
+ var MenuBuilder = Object.freeze({
685
+ make: () => new MenuBuilderImpl({
686
+ nodes: [],
687
+ edges: []
688
+ }, "root")
689
+ });
512
690
  export {
513
- DropdownMenu,
691
+ Menu,
514
692
  MenuBuilder,
515
- MenuProvider,
516
693
  MenuSeparatorType,
517
- ToolbarMenu,
518
694
  createGapSeparator,
519
695
  createLineSeparator,
520
696
  createMenuAction,
521
697
  createMenuItemGroup,
522
- createMenuScope,
523
698
  isMenuGroup,
524
699
  isSeparator,
525
700
  menuContextDefaults,
526
701
  useMenu,
527
702
  useMenuActions,
528
- useMenuItems
703
+ useMenuItems,
704
+ useMenuScoped
529
705
  };
530
706
  //# sourceMappingURL=index.mjs.map