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

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