@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,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, { createContext, 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,40 @@ 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
+ import { composable, composableProps } from "@dxos/ui-theme";
113
+
114
+ // src/types.ts
115
+ import { Node as Node2 } from "@dxos/app-graph";
116
+ var MenuSeparatorType = "@dxos/react-ui-toolbar/separator";
117
+ var isSeparator = (node) => node.type === MenuSeparatorType;
118
+ var isMenuGroup = (node) => node.type === Node2.ActionGroupType;
133
119
 
134
120
  // src/components/DropdownMenu.tsx
121
+ import { useControllableState } from "@radix-ui/react-use-controllable-state";
122
+ import React2, { useCallback } from "react";
123
+ import { Icon, DropdownMenu as NaturalDropdownMenu } from "@dxos/react-ui";
135
124
  var DropdownMenuItem = ({ item, onClick, __menuScope }) => {
136
125
  const action = item;
137
126
  const handleClick = useCallback((event) => onClick(action, event), [
138
127
  action,
139
128
  onClick
140
129
  ]);
141
- const { iconSize } = useMenu("DropdownMenuItem", __menuScope);
142
- return /* @__PURE__ */ React3.createElement(NaturalDropdownMenu.Item, {
130
+ const { iconSize } = useMenuScoped("DropdownMenuItem", __menuScope);
131
+ return /* @__PURE__ */ React2.createElement(NaturalDropdownMenu.Item, {
143
132
  onClick: handleClick,
144
133
  classNames: "gap-2",
145
134
  disabled: action.properties?.disabled,
146
135
  ...action.properties?.testId && {
147
136
  "data-testid": action.properties.testId
148
137
  }
149
- }, action.properties?.icon && /* @__PURE__ */ React3.createElement(Icon, {
138
+ }, action.properties?.icon && /* @__PURE__ */ React2.createElement(Icon, {
150
139
  icon: action.properties.icon,
151
140
  size: iconSize
152
- }), /* @__PURE__ */ React3.createElement(ActionLabel, {
141
+ }), /* @__PURE__ */ React2.createElement(ActionLabel, {
153
142
  action
154
143
  }));
155
144
  };
@@ -159,7 +148,7 @@ var DropdownMenuRoot = ({ items: propsItems, group, open, defaultOpen, onOpenCha
159
148
  defaultProp: defaultOpen,
160
149
  onChange: onOpenChange
161
150
  });
162
- const { onAction } = useMenu("DropdownMenuRoot", __menuScope);
151
+ const { onAction } = useMenuScoped("DropdownMenuRoot", __menuScope);
163
152
  const handleActionClick = useCallback((action, event) => {
164
153
  if (action.properties?.disabled) {
165
154
  return;
@@ -183,15 +172,15 @@ var DropdownMenuRoot = ({ items: propsItems, group, open, defaultOpen, onOpenCha
183
172
  onAction
184
173
  ]);
185
174
  const items = useMenuItems(group, propsItems);
186
- return /* @__PURE__ */ React3.createElement(NaturalDropdownMenu.Root, {
175
+ return /* @__PURE__ */ React2.createElement(NaturalDropdownMenu.Root, {
187
176
  open: optionsMenuOpen,
188
177
  onOpenChange: setOptionsMenuOpen,
189
178
  ...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, {
179
+ }, 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
180
  key: item.id,
192
181
  item,
193
182
  onClick: handleActionClick
194
- }))), /* @__PURE__ */ React3.createElement(NaturalDropdownMenu.Arrow, null))));
183
+ }))), /* @__PURE__ */ React2.createElement(NaturalDropdownMenu.Arrow, null))));
195
184
  };
196
185
  var DropdownMenu = {
197
186
  Root: DropdownMenuRoot,
@@ -200,33 +189,11 @@ var DropdownMenu = {
200
189
  };
201
190
 
202
191
  // 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);
192
+ var ActionToolbarItem = ({ __menuScope, action }) => {
193
+ const { iconSize, onAction } = useMenuScoped("ActionToolbarItem", __menuScope);
217
194
  const { t } = useTranslation2(translationKey);
218
195
  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
- };
196
+ const buttonVariant = action.properties.variant === "primary" ? "primary" : "ghost";
230
197
  const handleClick = useCallback2(() => {
231
198
  if (onAction) {
232
199
  onAction(action, {});
@@ -237,22 +204,36 @@ var ActionToolbarItem = ({ action, __menuScope }) => {
237
204
  action,
238
205
  onAction
239
206
  ]);
240
- return hidden ? null : /* @__PURE__ */ React4.createElement(Root, {
241
- key: action.id,
207
+ const commonProps = {
208
+ variant: buttonVariant,
242
209
  disabled,
210
+ classNames,
243
211
  onClick: handleClick,
244
- variant: "ghost",
245
212
  ...testId && {
246
213
  "data-testid": testId
247
- },
248
- ...rootProps,
249
- classNames
250
- });
214
+ }
215
+ };
216
+ if (hidden) {
217
+ return null;
218
+ }
219
+ return icon ? /* @__PURE__ */ React3.createElement(NaturalToolbar.IconButton, {
220
+ key: action.id,
221
+ ...commonProps,
222
+ icon,
223
+ size: iconSize,
224
+ iconOnly,
225
+ label: actionLabel(action, t)
226
+ }) : /* @__PURE__ */ React3.createElement(NaturalToolbar.Button, {
227
+ key: action.id,
228
+ ...commonProps
229
+ }, /* @__PURE__ */ React3.createElement(ActionLabel, {
230
+ action
231
+ }));
251
232
  };
252
- var DropdownMenuToolbarItem = ({ group, items: propsItems, __menuScope }) => {
233
+ var DropdownMenuToolbarItem = ({ __menuScope, group, items: propsItems }) => {
253
234
  const { iconOnly, disabled, testId } = group.properties;
254
235
  const { t } = useTranslation2(translationKey);
255
- const { iconSize } = useMenu("DropdownMenuToolbarItem", __menuScope);
236
+ const { iconSize } = useMenuScoped("DropdownMenuToolbarItem", __menuScope);
256
237
  const items = useMenuItems(group, propsItems, "DropdownMenuToolbarItem", __menuScope);
257
238
  const activeItem = items?.find((item) => !!item.properties.checked);
258
239
  const icon = group.properties.applyActive && // TODO(thure): Handle other menu item types.
@@ -266,20 +247,20 @@ var DropdownMenuToolbarItem = ({ group, items: propsItems, __menuScope }) => {
266
247
  label: actionLabel(labelAction, t),
267
248
  caretDown: true
268
249
  } : {
269
- children: /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(ActionLabel, {
250
+ children: /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(ActionLabel, {
270
251
  action: labelAction
271
- }), /* @__PURE__ */ React4.createElement(Icon2, {
252
+ }), /* @__PURE__ */ React3.createElement(Icon2, {
272
253
  size: 3,
273
254
  icon: "ph--caret-down--bold",
274
- classNames: "mis-1"
255
+ classNames: "ms-1"
275
256
  }))
276
257
  };
277
- return /* @__PURE__ */ React4.createElement(DropdownMenu.Root, {
258
+ return /* @__PURE__ */ React3.createElement(DropdownMenu.Root, {
278
259
  group,
279
260
  items
280
- }, /* @__PURE__ */ React4.createElement(DropdownMenu.Trigger, {
261
+ }, /* @__PURE__ */ React3.createElement(DropdownMenu.Trigger, {
281
262
  asChild: true
282
- }, /* @__PURE__ */ React4.createElement(Root, {
263
+ }, /* @__PURE__ */ React3.createElement(Root, {
283
264
  variant: "ghost",
284
265
  disabled,
285
266
  ...rootProps,
@@ -288,8 +269,8 @@ var DropdownMenuToolbarItem = ({ group, items: propsItems, __menuScope }) => {
288
269
  }
289
270
  })));
290
271
  };
291
- var ToggleGroupItem = ({ group, action, __menuScope }) => {
292
- const { iconSize, onAction } = useMenu("ToggleGroupItem", __menuScope);
272
+ var ToggleGroupItem = ({ __menuScope, group, action }) => {
273
+ const { iconSize, onAction } = useMenuScoped("ToggleGroupItem", __menuScope);
293
274
  const { t } = useTranslation2(translationKey);
294
275
  const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
295
276
  const handleClick = useCallback2(() => {
@@ -307,171 +288,331 @@ var ToggleGroupItem = ({ group, action, __menuScope }) => {
307
288
  group,
308
289
  onAction
309
290
  ]);
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,
291
+ const commonProps = {
323
292
  value: action.id,
324
293
  disabled,
325
- onClick: handleClick,
326
294
  variant: "ghost",
295
+ classNames,
296
+ onClick: handleClick,
327
297
  ...testId && {
328
298
  "data-testid": testId
329
- },
330
- ...rootProps,
331
- classNames
332
- });
299
+ }
300
+ };
301
+ return hidden ? null : icon ? /* @__PURE__ */ React3.createElement(NaturalToolbar.ToggleGroupIconItem, {
302
+ ...commonProps,
303
+ icon,
304
+ size: iconSize,
305
+ iconOnly,
306
+ label: actionLabel(action, t)
307
+ }) : /* @__PURE__ */ React3.createElement(NaturalToolbar.ToggleGroupItem, commonProps, /* @__PURE__ */ React3.createElement(ActionLabel, {
308
+ action
309
+ }));
333
310
  };
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
- );
311
+ var ToggleGroupToolbarItem = ({ __menuScope, group, items: itemsProp }) => {
312
+ const items = useMenuItems(group, itemsProp, "ToggleGroupToolbarItem", __menuScope);
313
+ const { selectCardinality } = group.properties;
314
+ const children = items.map((action) => /* @__PURE__ */ React3.createElement(ToggleGroupItem, {
315
+ key: action.id,
316
+ group,
317
+ action
318
+ }));
319
+ if (selectCardinality === "multiple") {
320
+ return /* @__PURE__ */ React3.createElement(NaturalToolbar.ToggleGroup, {
321
+ type: "multiple",
322
+ value: group.properties.value
323
+ }, children);
324
+ } else {
325
+ return /* @__PURE__ */ React3.createElement(NaturalToolbar.ToggleGroup, {
326
+ type: "single",
327
+ value: group.properties.value
328
+ }, children);
329
+ }
354
330
  };
355
- var ToolbarMenu = ({ __menuScope, classNames, textBlockWidth: textBlockWidthProp, ...props }) => {
331
+ var ToolbarMenu = composable(({ __menuScope, children, ...props }, forwardedRef) => {
356
332
  const items = useMenuItems(void 0, void 0, "ToolbarMenu", __menuScope);
357
- const { attendableId } = useMenu("ToolbarMenu", __menuScope);
333
+ const { attendableId, alwaysActive } = useMenuScoped("ToolbarMenu", __menuScope);
358
334
  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, {
365
- ...props,
366
- classNames: [
367
- attendableId && !hasAttention && toolbarInactive,
368
- classNames
369
- ],
370
- layoutManaged: textBlockWidthProp
371
- }, /* @__PURE__ */ React4.createElement(InnerRoot, innerRootProps, items?.map((item, i) => /* @__PURE__ */ React4.createElement(ToolbarMenuItem, {
372
- key: item.id ?? i,
335
+ return /* @__PURE__ */ React3.createElement(NaturalToolbar.Root, {
336
+ ...composableProps(props, {
337
+ classNames: attendableId
338
+ }),
339
+ disabled: !alwaysActive && !hasAttention,
340
+ ref: forwardedRef
341
+ }, items?.map((item) => /* @__PURE__ */ React3.createElement(ToolbarMenuItem, {
342
+ key: item.id,
343
+ __menuScope,
373
344
  item
374
- }))));
375
- };
376
- var ToolbarMenuItem = ({ item }) => {
345
+ })), children);
346
+ });
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, 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__ */ 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: 151,
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
415
505
  }
416
- const sourceEdges = acc[source];
417
- if (!sourceEdges.outbound.includes(target)) {
418
- sourceEdges.outbound.push(target);
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
419
524
  }
420
- const targetEdges = acc[target];
421
- if (!targetEdges.inbound.includes(source)) {
422
- targetEdges.inbound.push(source);
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;
423
540
  }
424
- return acc;
425
- }, {})));
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
+ });
553
+ }
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
- if (prevPropsRef.current !== menuGraphProps) {
441
- prevPropsRef.current = menuGraphProps;
442
- graph.pipe(Graph.addNodes(menuGraphProps.nodes), Graph.addEdges(menuGraphProps.edges));
443
- }
444
- const useGroupItems = useCallback3((sourceNode) => {
445
- const items = useAtomValue(graph.connections(sourceNode?.id || Node3.RootId));
446
- return items;
585
+ newGraph.pipe(Graph.addNodes(menuGraphProps.nodes), Graph.addEdges(menuGraphProps.edges));
586
+ return newGraph;
587
+ }, [
588
+ registry,
589
+ menuGraphProps
590
+ ]);
591
+ const items = useCallback4((group) => {
592
+ return graph.connections(group?.id || Node3.RootId, "child");
447
593
  }, [
448
594
  graph
449
595
  ]);
450
596
  return {
451
- useGroupItems
597
+ items
452
598
  };
453
599
  };
454
600
 
455
601
  // src/builder.ts
456
602
  import { invariant } from "@dxos/invariant";
457
- var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-menu/src/builder.ts";
458
- var MenuBuilder = Object.freeze({
459
- make: () => new MenuBuilderImpl({
460
- nodes: [],
461
- edges: []
462
- }, "root")
463
- });
603
+ var __dxlog_file2 = "/__w/dxos/dxos/packages/ui/react-ui-menu/src/builder.ts";
464
604
  var MenuBuilderImpl = class _MenuBuilderImpl {
465
605
  _data;
466
606
  _rootId;
607
+ _separatorCount = 0;
467
608
  constructor(_data, _rootId) {
468
609
  this._data = _data;
469
610
  this._rootId = _rootId;
470
611
  }
471
612
  root(props) {
472
613
  invariant(this._rootId === "root", "Root group can only be at the top level", {
473
- F: __dxlog_file,
474
- L: 36,
614
+ F: __dxlog_file2,
615
+ L: 61,
475
616
  S: this,
476
617
  A: [
477
618
  "this._rootId === 'root'",
@@ -479,8 +620,8 @@ var MenuBuilderImpl = class _MenuBuilderImpl {
479
620
  ]
480
621
  });
481
622
  invariant(this._data.nodes.find((node) => node.id === "root") === void 0, "Root group can only be created once", {
482
- F: __dxlog_file,
483
- L: 37,
623
+ F: __dxlog_file2,
624
+ L: 62,
484
625
  S: this,
485
626
  A: [
486
627
  "this._data.nodes.find((node) => node.id === 'root') === undefined",
@@ -490,11 +631,15 @@ var MenuBuilderImpl = class _MenuBuilderImpl {
490
631
  this._data.nodes.push(createMenuItemGroup("root", props));
491
632
  return this;
492
633
  }
634
+ build() {
635
+ return this._data;
636
+ }
493
637
  action(id, props, invoke) {
494
638
  this._data.nodes.push(createMenuAction(id, invoke, props));
495
639
  this._data.edges.push({
496
640
  source: this._rootId,
497
- target: id
641
+ target: id,
642
+ relation: "child"
498
643
  });
499
644
  return this;
500
645
  }
@@ -502,31 +647,62 @@ var MenuBuilderImpl = class _MenuBuilderImpl {
502
647
  this._data.nodes.push(createMenuItemGroup(id, props));
503
648
  this._data.edges.push({
504
649
  source: this._rootId,
505
- target: id
650
+ target: id,
651
+ relation: "child"
506
652
  });
507
653
  cb(new _MenuBuilderImpl(this._data, id));
508
654
  return this;
509
655
  }
510
- build() {
511
- return this._data;
656
+ subgraph(subgraphOrCb) {
657
+ if (!subgraphOrCb) {
658
+ return this;
659
+ }
660
+ if (typeof subgraphOrCb === "function") {
661
+ subgraphOrCb(new _MenuBuilderImpl(this._data, this._rootId));
662
+ } else {
663
+ this._data.nodes.push(...subgraphOrCb.nodes);
664
+ this._data.edges.push(...subgraphOrCb.edges);
665
+ }
666
+ return this;
667
+ }
668
+ separator(variant = "gap") {
669
+ const id = `separator-${++this._separatorCount}`;
670
+ this._data.nodes.push({
671
+ id,
672
+ type: MenuSeparatorType,
673
+ properties: {
674
+ variant
675
+ },
676
+ data: void 0
677
+ });
678
+ this._data.edges.push({
679
+ source: this._rootId,
680
+ target: id,
681
+ relation: "child"
682
+ });
683
+ return this;
512
684
  }
513
685
  };
686
+ var MenuBuilder = Object.freeze({
687
+ make: () => new MenuBuilderImpl({
688
+ nodes: [],
689
+ edges: []
690
+ }, "root")
691
+ });
514
692
  export {
515
- DropdownMenu,
693
+ Menu,
516
694
  MenuBuilder,
517
- MenuProvider,
518
695
  MenuSeparatorType,
519
- ToolbarMenu,
520
696
  createGapSeparator,
521
697
  createLineSeparator,
522
698
  createMenuAction,
523
699
  createMenuItemGroup,
524
- createMenuScope,
525
700
  isMenuGroup,
526
701
  isSeparator,
527
702
  menuContextDefaults,
528
703
  useMenu,
529
704
  useMenuActions,
530
- useMenuItems
705
+ useMenuItems,
706
+ useMenuScoped
531
707
  };
532
708
  //# sourceMappingURL=index.mjs.map