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