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

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