@dxos/react-ui-menu 0.9.1-staging.ee54ba693a → 0.11.0

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 (61) hide show
  1. package/dist/lib/index.mjs +747 -0
  2. package/dist/lib/index.mjs.map +1 -0
  3. package/dist/lib/translations.mjs +7 -0
  4. package/dist/lib/translations.mjs.map +1 -0
  5. package/dist/types/src/builder.d.ts +1 -1
  6. package/dist/types/src/builder.d.ts.map +1 -1
  7. package/dist/types/src/components/ActionLabel.d.ts +0 -2
  8. package/dist/types/src/components/ActionLabel.d.ts.map +1 -1
  9. package/dist/types/src/components/DropdownMenu.d.ts +2 -4
  10. package/dist/types/src/components/DropdownMenu.d.ts.map +1 -1
  11. package/dist/types/src/components/Menu.d.ts +5 -15
  12. package/dist/types/src/components/Menu.d.ts.map +1 -1
  13. package/dist/types/src/components/MenuContext.d.ts +28 -0
  14. package/dist/types/src/components/MenuContext.d.ts.map +1 -0
  15. package/dist/types/src/components/ToolbarMenu.d.ts +2 -2
  16. package/dist/types/src/components/ToolbarMenu.d.ts.map +1 -1
  17. package/dist/types/src/components/ToolbarMenu.stories.d.ts +2 -2
  18. package/dist/types/src/components/action-label.d.ts +7 -0
  19. package/dist/types/src/components/action-label.d.ts.map +1 -0
  20. package/dist/types/src/components/index.d.ts +2 -0
  21. package/dist/types/src/components/index.d.ts.map +1 -1
  22. package/dist/types/src/hooks/useGraphMenuActions.d.ts +13 -1
  23. package/dist/types/src/hooks/useGraphMenuActions.d.ts.map +1 -1
  24. package/dist/types/src/index.d.ts +2 -0
  25. package/dist/types/src/index.d.ts.map +1 -1
  26. package/dist/types/src/presentation.d.ts +8 -0
  27. package/dist/types/src/presentation.d.ts.map +1 -0
  28. package/dist/types/src/toolbar.d.ts +6 -0
  29. package/dist/types/src/toolbar.d.ts.map +1 -0
  30. package/dist/types/src/types.d.ts +1 -1
  31. package/dist/types/src/types.d.ts.map +1 -1
  32. package/dist/types/src/util.d.ts +5 -3
  33. package/dist/types/src/util.d.ts.map +1 -1
  34. package/dist/types/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +19 -20
  36. package/src/builder.ts +3 -3
  37. package/src/components/ActionLabel.tsx +1 -6
  38. package/src/components/DropdownMenu.tsx +30 -8
  39. package/src/components/Menu.stories.tsx +1 -1
  40. package/src/components/Menu.test.tsx +3 -2
  41. package/src/components/Menu.tsx +32 -122
  42. package/src/components/MenuContext.ts +129 -0
  43. package/src/components/ToolbarMenu.tsx +27 -13
  44. package/src/components/action-label.ts +20 -0
  45. package/src/components/index.ts +3 -0
  46. package/src/hooks/useGraphMenuActions.ts +16 -3
  47. package/src/index.ts +2 -0
  48. package/src/presentation.ts +19 -0
  49. package/src/toolbar.ts +11 -0
  50. package/src/types.ts +1 -1
  51. package/src/util.ts +2 -2
  52. package/dist/lib/browser/index.mjs +0 -797
  53. package/dist/lib/browser/index.mjs.map +0 -7
  54. package/dist/lib/browser/meta.json +0 -1
  55. package/dist/lib/browser/translations.mjs +0 -16
  56. package/dist/lib/browser/translations.mjs.map +0 -7
  57. package/dist/lib/node-esm/index.mjs +0 -799
  58. package/dist/lib/node-esm/index.mjs.map +0 -7
  59. package/dist/lib/node-esm/meta.json +0 -1
  60. package/dist/lib/node-esm/translations.mjs +0 -18
  61. package/dist/lib/node-esm/translations.mjs.map +0 -7
@@ -0,0 +1,747 @@
1
+ import { Atom, RegistryContext, useAtomValue } from "@effect-atom/atom-react";
2
+ import { createContextScope } from "@radix-ui/react-context";
3
+ import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
4
+ import { log } from "@dxos/log";
5
+ import { keySymbols } from "@dxos/keyboard";
6
+ import { DropdownMenu, Icon, Input, Toolbar, Tooltip, composable, composableProps, toLocalizedString, useTranslation } from "@dxos/react-ui";
7
+ import * as Effect from "effect/Effect";
8
+ import { Graph, Node } from "@dxos/app-graph";
9
+ import { EffectEx } from "@dxos/effect";
10
+ import { getHostPlatform } from "@dxos/util";
11
+ import { useControllableState } from "@radix-ui/react-use-controllable-state";
12
+ import { mx } from "@dxos/ui-theme";
13
+ import { translationKey } from "#translations";
14
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
15
+ import { useAttention } from "@dxos/react-ui-attention";
16
+ import { Atom as Atom$1 } from "@effect-atom/atom";
17
+ import { invariant } from "@dxos/invariant";
18
+ //#region src/components/MenuContext.ts
19
+ var __dxlog_file$1 = "/__w/dxos/dxos/packages/ui/react-ui-menu/src/components/MenuContext.ts";
20
+ var MENU_NAME = "Menu";
21
+ var [createMenuContext, createMenuScope] = createContextScope(MENU_NAME, []);
22
+ var nullItemsAtom = Atom.make(null);
23
+ var defaultItemsAccessor = () => nullItemsAtom;
24
+ var menuContextDefaults = {
25
+ iconSize: 5,
26
+ items: defaultItemsAccessor,
27
+ onAction: void 0,
28
+ menuItemsAtom: Atom.make(/* @__PURE__ */ new Map()),
29
+ addMenuItems: () => {},
30
+ removeMenuItems: () => {}
31
+ };
32
+ var [MenuContextProvider, useMenuScoped] = createMenuContext(MENU_NAME, menuContextDefaults);
33
+ var useMenuScope = createMenuScope();
34
+ var MenuDropdownContext = createContext({ closeMenu: () => {} });
35
+ var sortMenuItems = (items) => [...items].sort((a, b) => a.priority !== b.priority ? a.priority - b.priority : a.id.localeCompare(b.id));
36
+ var resolveItems = (baseItems, group, entries) => {
37
+ const applicable = [...entries.values()].filter((entry) => !entry.groupFilter || entry.groupFilter(group));
38
+ if (applicable.length === 0) return baseItems;
39
+ const sorted = sortMenuItems(applicable);
40
+ const replacements = sorted.filter((entry) => entry.mode === "replacement");
41
+ if (replacements.length > 0) {
42
+ if (replacements.length > 1) log.warn("multiple replacement entries found", {
43
+ ids: replacements.map((r) => r.id).join(", "),
44
+ using: replacements[0].id
45
+ }, {
46
+ "~LogMeta": "~LogMeta",
47
+ F: __dxlog_file$1,
48
+ L: 84,
49
+ S: void 0
50
+ });
51
+ return replacements[0].items;
52
+ }
53
+ const additiveItems = sorted.filter((entry) => entry.mode === "additive").flatMap((entry) => entry.items);
54
+ if (!baseItems || baseItems.length === 0) return additiveItems.length > 0 ? additiveItems : null;
55
+ return [...baseItems, ...additiveItems];
56
+ };
57
+ var useMenuItems = (group, propsItems, consumerName = "useMenuItemConsumer", __menuScope) => {
58
+ const { items, menuItemsAtom } = useMenuScoped(consumerName, __menuScope);
59
+ const groupItems = useAtomValue(items(group));
60
+ const entries = useAtomValue(menuItemsAtom) ?? /* @__PURE__ */ new Map();
61
+ const baseItems = useMemo(() => propsItems ?? groupItems ?? null, [propsItems, groupItems]);
62
+ return useMemo(() => resolveItems(baseItems, group, entries), [
63
+ baseItems,
64
+ group,
65
+ entries
66
+ ]) ?? void 0;
67
+ };
68
+ /** Returns the menu context without Radix scope. */
69
+ var useMenu = (consumerName) => {
70
+ return useMenuScoped(consumerName, void 0);
71
+ };
72
+ //#endregion
73
+ //#region src/util.ts
74
+ /**
75
+ * Execute a menu action's Effect with its captured context.
76
+ * This provides the `_actionContext` layer if available.
77
+ */
78
+ var executeMenuAction = async (action, params = {}) => {
79
+ let effect = action.data(params);
80
+ if (action._actionContext) effect = effect.pipe(Effect.provide(action._actionContext));
81
+ await EffectEx.runAndForwardErrors(effect);
82
+ };
83
+ var getShortcut = (action) => {
84
+ return typeof action.properties?.keyBinding === "string" ? action.properties.keyBinding : action.properties?.keyBinding?.[getHostPlatform()];
85
+ };
86
+ var createMenuAction = (id, invoke, properties) => ({
87
+ id,
88
+ type: Node.ActionType,
89
+ properties,
90
+ data: (params) => Effect.sync(() => invoke(params))
91
+ });
92
+ var createMenuItemGroup = (id, properties) => ({
93
+ id,
94
+ type: Node.ActionGroupType,
95
+ properties,
96
+ data: Node.actionGroupSymbol
97
+ });
98
+ var createGapSeparator = (id = "gap", source = "root") => ({
99
+ nodes: [{
100
+ id,
101
+ type: "@dxos/react-ui-toolbar/separator",
102
+ properties: { variant: "gap" },
103
+ data: void 0
104
+ }],
105
+ edges: [{
106
+ source,
107
+ target: id,
108
+ relation: "child"
109
+ }]
110
+ });
111
+ var createLineSeparator = (id = "line", source = "root") => ({
112
+ nodes: [{
113
+ id,
114
+ type: "@dxos/react-ui-toolbar/separator",
115
+ properties: { variant: "line" },
116
+ data: void 0
117
+ }],
118
+ edges: [{
119
+ source,
120
+ target: id,
121
+ relation: "child"
122
+ }]
123
+ });
124
+ //#endregion
125
+ //#region src/components/action-label.ts
126
+ var actionLabel = (action, t) => {
127
+ const shortcut = getShortcut(action);
128
+ return `${toLocalizedString(action.properties.label, t)}${shortcut ? ` (${keySymbols(shortcut).join("")})` : ""}`;
129
+ };
130
+ //#endregion
131
+ //#region src/types.ts
132
+ var MenuSeparatorType = "@dxos/react-ui-toolbar/separator";
133
+ var isSeparator = (node) => node.type === MenuSeparatorType;
134
+ var isMenuGroup = (node) => node.type === Node.ActionGroupType;
135
+ //#endregion
136
+ //#region src/components/ActionLabel.tsx
137
+ var ActionLabel = ({ action }) => {
138
+ const { t } = useTranslation(translationKey);
139
+ const shortcut = getShortcut(action);
140
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
141
+ className: "grow truncate",
142
+ children: toLocalizedString(action.properties.label, t)
143
+ }), shortcut && /* @__PURE__ */ jsx("span", {
144
+ className: mx("shrink-0", "text-description"),
145
+ children: keySymbols(shortcut).join("")
146
+ })] });
147
+ };
148
+ //#endregion
149
+ //#region src/components/DropdownMenu.tsx
150
+ var DropdownMenuItem = ({ item, group, onClick, __menuScope }) => {
151
+ const action = item;
152
+ const handleClick = useCallback((event) => onClick(action, event), [action, onClick]);
153
+ const { iconSize } = useMenuScoped("DropdownMenuItem", __menuScope);
154
+ const checkable = typeof action.properties?.checked === "boolean";
155
+ const role = group?.properties?.selectCardinality === "multiple" ? "menuitemcheckbox" : "menuitemradio";
156
+ return /* @__PURE__ */ jsxs(DropdownMenu.Item, {
157
+ onClick: handleClick,
158
+ classNames: "gap-2",
159
+ disabled: action.properties?.disabled,
160
+ ...checkable && {
161
+ role,
162
+ "aria-checked": !!action.properties?.checked
163
+ },
164
+ ...action.properties?.testId && { "data-testid": action.properties.testId },
165
+ children: [
166
+ action.properties?.icon && /* @__PURE__ */ jsx(Icon, {
167
+ icon: action.properties.icon,
168
+ size: iconSize,
169
+ classNames: mx(action.properties.spin && "animate-spin", action.properties.iconClassNames)
170
+ }),
171
+ /* @__PURE__ */ jsx(ActionLabel, { action }),
172
+ action.properties?.checked && /* @__PURE__ */ jsx(Icon, {
173
+ icon: "ph--check--regular",
174
+ size: iconSize,
175
+ classNames: "mis-auto"
176
+ })
177
+ ]
178
+ });
179
+ };
180
+ var DropdownMenuRoot = ({ items: propsItems, group, open, defaultOpen, onOpenChange, caller, children, __menuScope, ...naturalProps }) => {
181
+ const [optionsMenuOpen, setOptionsMenuOpen] = useControllableState({
182
+ prop: open,
183
+ defaultProp: defaultOpen,
184
+ onChange: onOpenChange
185
+ });
186
+ const { onAction } = useMenuScoped("DropdownMenuRoot", __menuScope);
187
+ const handleActionClick = useCallback((action, event) => {
188
+ if (action.properties?.disabled) return;
189
+ event.stopPropagation();
190
+ setOptionsMenuOpen(false);
191
+ const params = {
192
+ parent: group,
193
+ caller,
194
+ modifiers: { shift: event.shiftKey }
195
+ };
196
+ if (onAction) onAction(action, params);
197
+ else executeMenuAction(action, params);
198
+ }, [
199
+ group,
200
+ caller,
201
+ onAction
202
+ ]);
203
+ const items = useMenuItems(group, propsItems);
204
+ return /* @__PURE__ */ jsxs(DropdownMenu.Root, {
205
+ open: optionsMenuOpen,
206
+ onOpenChange: setOptionsMenuOpen,
207
+ ...naturalProps,
208
+ children: [children, /* @__PURE__ */ jsx(DropdownMenu.Portal, { children: /* @__PURE__ */ jsxs(DropdownMenu.Content, { children: [/* @__PURE__ */ jsx(DropdownMenu.Viewport, { children: items?.map((item) => isSeparator(item) ? /* @__PURE__ */ jsx(DropdownMenu.Separator, {}, item.id) : /* @__PURE__ */ jsx(DropdownMenuItem, {
209
+ item,
210
+ group,
211
+ onClick: handleActionClick
212
+ }, item.id)) }), /* @__PURE__ */ jsx(DropdownMenu.Arrow, {})] }) })]
213
+ });
214
+ };
215
+ var DropdownMenu$1 = {
216
+ Root: DropdownMenuRoot,
217
+ Trigger: DropdownMenu.Trigger,
218
+ VirtualTrigger: DropdownMenu.VirtualTrigger
219
+ };
220
+ //#endregion
221
+ //#region src/components/ToolbarMenu.tsx
222
+ var ActionToolbarItem = ({ __menuScope, action }) => {
223
+ const { iconSize, onAction } = useMenuScoped("ActionToolbarItem", __menuScope);
224
+ const { t } = useTranslation(translationKey);
225
+ const [pending, setPending] = useState(false);
226
+ const pendingRef = useRef(false);
227
+ const { icon, iconOnly = true, disabled, testId, hidden, classNames, iconClassNames, spin } = action.properties;
228
+ const buttonVariant = action.properties.variant === "primary" ? "primary" : "ghost";
229
+ const handleClick = useCallback(() => {
230
+ if (pendingRef.current) return;
231
+ pendingRef.current = true;
232
+ setPending(true);
233
+ const done = () => {
234
+ pendingRef.current = false;
235
+ setPending(false);
236
+ };
237
+ if (onAction) Promise.resolve(onAction(action, {})).then(done, done);
238
+ else executeMenuAction(action).then(done, done);
239
+ }, [action, onAction]);
240
+ const commonProps = {
241
+ variant: buttonVariant,
242
+ disabled: disabled || pending,
243
+ classNames,
244
+ onClick: handleClick,
245
+ ...testId && { "data-testid": testId }
246
+ };
247
+ if (hidden) return null;
248
+ return icon ? /* @__PURE__ */ jsx(Toolbar.IconButton, {
249
+ ...commonProps,
250
+ icon,
251
+ size: iconSize,
252
+ iconOnly,
253
+ iconClassNames: mx(spin && "animate-spin", iconClassNames),
254
+ label: actionLabel(action, t)
255
+ }, action.id) : /* @__PURE__ */ jsx(Toolbar.Button, {
256
+ ...commonProps,
257
+ children: /* @__PURE__ */ jsx(ActionLabel, { action })
258
+ }, action.id);
259
+ };
260
+ var SwitchToolbarItem = ({ __menuScope, action }) => {
261
+ const { onAction } = useMenuScoped("SwitchToolbarItem", __menuScope);
262
+ const { t } = useTranslation(translationKey);
263
+ const { label, iconOnly, disabled, testId, hidden, checked } = action.properties;
264
+ const labelStr = toLocalizedString(label, t);
265
+ const handleCheckedChange = useCallback(() => {
266
+ if (onAction) onAction(action, {});
267
+ else executeMenuAction(action);
268
+ }, [action, onAction]);
269
+ if (hidden) return null;
270
+ const switchInput = /* @__PURE__ */ jsx(Input.Switch, {
271
+ checked,
272
+ disabled,
273
+ "aria-label": iconOnly ? labelStr : void 0,
274
+ onCheckedChange: handleCheckedChange,
275
+ ...testId && { "data-testid": testId }
276
+ });
277
+ return /* @__PURE__ */ jsxs(Input.Root, { children: [iconOnly ? /* @__PURE__ */ jsx(Tooltip.Trigger, {
278
+ asChild: true,
279
+ content: labelStr,
280
+ children: /* @__PURE__ */ jsx(Input.Block, { children: switchInput })
281
+ }) : /* @__PURE__ */ jsx(Input.Block, { children: switchInput }), !iconOnly && /* @__PURE__ */ jsx(Input.Label, { children: labelStr })] });
282
+ };
283
+ var DropdownMenuToolbarItem = ({ __menuScope, group, items: propsItems }) => {
284
+ const { t } = useTranslation(translationKey);
285
+ const { iconSize } = useMenuScoped("DropdownMenuToolbarItem", __menuScope);
286
+ const items = useMenuItems(group, propsItems, "DropdownMenuToolbarItem", __menuScope);
287
+ const { iconOnly, disabled, testId, applyActive, caretDown = true, icon: groupIcon, iconClassNames: groupIconClassNames, spin: groupSpin } = group.properties;
288
+ const activeItem = items?.find((item) => !!item.properties.checked);
289
+ const icon = applyActive && activeItem?.properties.icon || groupIcon;
290
+ const iconClassNames = applyActive && activeItem?.properties.iconClassNames || groupIconClassNames;
291
+ const spin = applyActive && activeItem?.properties.spin || groupSpin;
292
+ const labelAction = applyActive && activeItem ? activeItem : group;
293
+ return /* @__PURE__ */ jsx(DropdownMenu$1.Root, {
294
+ group,
295
+ items,
296
+ children: /* @__PURE__ */ jsx(DropdownMenu$1.Trigger, {
297
+ asChild: true,
298
+ children: icon ? /* @__PURE__ */ jsx(Toolbar.IconButton, {
299
+ variant: "ghost",
300
+ disabled,
301
+ icon,
302
+ size: iconSize,
303
+ iconOnly,
304
+ iconClassNames: mx(spin && "animate-spin", iconClassNames),
305
+ label: actionLabel(labelAction, t),
306
+ caretDown,
307
+ ...testId && { "data-testid": testId }
308
+ }) : /* @__PURE__ */ jsx(Toolbar.Button, {
309
+ variant: "ghost",
310
+ disabled,
311
+ caretDown,
312
+ ...testId && { "data-testid": testId },
313
+ children: /* @__PURE__ */ jsx(ActionLabel, { action: labelAction })
314
+ })
315
+ })
316
+ });
317
+ };
318
+ var ToggleGroupItem = ({ __menuScope, group, action }) => {
319
+ const { iconSize, onAction } = useMenuScoped("ToggleGroupItem", __menuScope);
320
+ const { t } = useTranslation(translationKey);
321
+ const { icon, iconOnly = true, disabled, testId, hidden, classNames, iconClassNames, spin } = action.properties;
322
+ const handleClick = useCallback(() => {
323
+ if (onAction) onAction(action, { parent: group });
324
+ else executeMenuAction(action, { parent: group });
325
+ }, [
326
+ action,
327
+ group,
328
+ onAction
329
+ ]);
330
+ const commonProps = {
331
+ value: action.id,
332
+ disabled,
333
+ variant: "ghost",
334
+ classNames,
335
+ onClick: handleClick,
336
+ ...testId && { "data-testid": testId }
337
+ };
338
+ return hidden ? null : icon ? /* @__PURE__ */ jsx(Toolbar.ToggleGroupIconItem, {
339
+ ...commonProps,
340
+ icon,
341
+ size: iconSize,
342
+ iconOnly,
343
+ iconClassNames: mx(spin && "animate-spin", iconClassNames),
344
+ label: actionLabel(action, t)
345
+ }) : /* @__PURE__ */ jsx(Toolbar.ToggleGroupItem, {
346
+ ...commonProps,
347
+ children: /* @__PURE__ */ jsx(ActionLabel, { action })
348
+ });
349
+ };
350
+ var ToggleGroupToolbarItem = ({ __menuScope, group, items: itemsProp }) => {
351
+ const items = useMenuItems(group, itemsProp, "ToggleGroupToolbarItem", __menuScope);
352
+ const { selectCardinality } = group.properties;
353
+ const children = items.map((action) => /* @__PURE__ */ jsx(ToggleGroupItem, {
354
+ group,
355
+ action
356
+ }, action.id));
357
+ if (selectCardinality === "multiple") return /* @__PURE__ */ jsx(Toolbar.ToggleGroup, {
358
+ type: "multiple",
359
+ value: group.properties.value,
360
+ children
361
+ });
362
+ else return /* @__PURE__ */ jsx(Toolbar.ToggleGroup, {
363
+ type: "single",
364
+ value: group.properties.value,
365
+ children
366
+ });
367
+ };
368
+ var ToolbarMenu = composable(({ __menuScope, children, ...props }, forwardedRef) => {
369
+ const items = useMenuItems(void 0, void 0, "ToolbarMenu", __menuScope);
370
+ const { attendableId, alwaysActive } = useMenuScoped("ToolbarMenu", __menuScope);
371
+ const { hasAttention } = useAttention(attendableId);
372
+ return /* @__PURE__ */ jsxs(Toolbar.Root, {
373
+ ...composableProps(props, { classNames: attendableId }),
374
+ disabled: !alwaysActive && !hasAttention,
375
+ ref: forwardedRef,
376
+ children: [items?.map((item) => /* @__PURE__ */ jsx(ToolbarMenuItem, {
377
+ __menuScope,
378
+ item
379
+ }, item.id)), children]
380
+ });
381
+ });
382
+ var ToolbarMenuItem = ({ __menuScope, item }) => {
383
+ if (isSeparator(item)) return /* @__PURE__ */ jsx(Toolbar.Separator, { variant: item.properties.variant });
384
+ if (isMenuGroup(item)) {
385
+ if (item.properties.variant === "dropdownMenu") return /* @__PURE__ */ jsx(DropdownMenuToolbarItem, {
386
+ __menuScope,
387
+ group: item
388
+ });
389
+ return /* @__PURE__ */ jsx(ToggleGroupToolbarItem, {
390
+ __menuScope,
391
+ group: item
392
+ });
393
+ }
394
+ const action = item;
395
+ if (action.properties?.variant === "switch") return /* @__PURE__ */ jsx(SwitchToolbarItem, {
396
+ __menuScope,
397
+ action
398
+ });
399
+ if (action.properties?.variant === "custom" && action.properties.render) return /* @__PURE__ */ jsx(Fragment, { children: action.properties.render() });
400
+ return /* @__PURE__ */ jsx(ActionToolbarItem, {
401
+ __menuScope,
402
+ action
403
+ });
404
+ };
405
+ //#endregion
406
+ //#region src/components/Menu.tsx
407
+ var DEFAULT_PRIORITY = 100;
408
+ var MenuProvider = ({ children, items = menuContextDefaults.items, iconSize = menuContextDefaults.iconSize, attendableId, alwaysActive, onAction }) => {
409
+ const { scope } = useMenuScope(void 0);
410
+ const registry = useContext(RegistryContext);
411
+ const menuItemsAtom = useMemo(() => Atom.make(/* @__PURE__ */ new Map()).pipe(Atom.keepAlive), []);
412
+ return /* @__PURE__ */ jsx(MenuContextProvider, {
413
+ items,
414
+ iconSize,
415
+ attendableId,
416
+ alwaysActive,
417
+ menuItemsAtom,
418
+ addMenuItems: useCallback((props) => {
419
+ const priority = props.priority ?? DEFAULT_PRIORITY;
420
+ const prev = registry.get(menuItemsAtom);
421
+ const next = new Map(prev);
422
+ next.set(props.id, {
423
+ ...props,
424
+ priority
425
+ });
426
+ registry.set(menuItemsAtom, next);
427
+ }, [registry, menuItemsAtom]),
428
+ removeMenuItems: useCallback((id) => {
429
+ const prev = registry.get(menuItemsAtom);
430
+ const next = new Map(prev);
431
+ next.delete(id);
432
+ registry.set(menuItemsAtom, next);
433
+ }, [registry, menuItemsAtom]),
434
+ onAction,
435
+ scope,
436
+ children
437
+ });
438
+ };
439
+ /**
440
+ * Menu context boundary.
441
+ *
442
+ * NOTE: This component is headless since it's root div has `contents`.
443
+ *
444
+ * Provides the menu context (action dispatch, contribution registry, icon size, etc.)
445
+ * and an optional dropdown root for use with `Menu.Trigger` + `Menu.Content`.
446
+ */
447
+ var MenuRoot = ({ children, open, defaultOpen, onOpenChange, caller, ...props }) => {
448
+ const [menuOpen, setMenuOpen] = useControllableState({
449
+ prop: open,
450
+ defaultProp: defaultOpen,
451
+ onChange: onOpenChange
452
+ });
453
+ const closeMenu = useCallback(() => setMenuOpen(false), [setMenuOpen]);
454
+ return /* @__PURE__ */ jsx(MenuProvider, {
455
+ ...props,
456
+ children: /* @__PURE__ */ jsx(MenuDropdownContext.Provider, {
457
+ value: {
458
+ closeMenu,
459
+ caller
460
+ },
461
+ children: /* @__PURE__ */ jsx(DropdownMenu.Root, {
462
+ open: menuOpen,
463
+ onOpenChange: setMenuOpen,
464
+ children
465
+ })
466
+ })
467
+ });
468
+ };
469
+ var MenuContentItem = ({ item, onClick, __menuScope }) => {
470
+ const action = item;
471
+ const handleClick = useCallback((event) => onClick(action, event), [action, onClick]);
472
+ const { iconSize } = useMenuScoped("MenuContentItem", __menuScope);
473
+ return /* @__PURE__ */ jsxs(DropdownMenu.Item, {
474
+ onClick: handleClick,
475
+ classNames: "gap-2",
476
+ disabled: action.properties?.disabled,
477
+ ...action.properties?.testId && { "data-testid": action.properties.testId },
478
+ children: [action.properties?.icon && /* @__PURE__ */ jsx(Icon, {
479
+ icon: action.properties.icon,
480
+ size: iconSize,
481
+ classNames: mx(action.properties.spin && "animate-spin", action.properties.iconClassNames)
482
+ }), /* @__PURE__ */ jsx(ActionLabel, { action })]
483
+ });
484
+ };
485
+ /**
486
+ * Renders the dropdown menu portal, content, and graph-backed items.
487
+ *
488
+ * Must be a descendant of `Menu.Root`. Reads items via `useMenuItems` from the
489
+ * nearest menu context, with optional `group`/`items` prop overrides.
490
+ */
491
+ var MenuContent = ({ group, items: propsItems, caller: callerOverride, __menuScope }) => {
492
+ const { closeMenu, caller: contextCaller } = useContext(MenuDropdownContext);
493
+ const caller = callerOverride ?? contextCaller;
494
+ const { onAction } = useMenuScoped("MenuContent", __menuScope);
495
+ const resolvedItems = useMenuItems(group, propsItems, "MenuContent", __menuScope);
496
+ const handleActionClick = useCallback((action, event) => {
497
+ if (action.properties?.disabled) return;
498
+ event.stopPropagation();
499
+ closeMenu();
500
+ const params = {
501
+ parent: group,
502
+ caller,
503
+ modifiers: { shift: event.shiftKey }
504
+ };
505
+ if (onAction) onAction(action, params);
506
+ else executeMenuAction(action, params);
507
+ }, [
508
+ group,
509
+ caller,
510
+ onAction,
511
+ closeMenu
512
+ ]);
513
+ return /* @__PURE__ */ jsx(DropdownMenu.Portal, { children: /* @__PURE__ */ jsxs(DropdownMenu.Content, { children: [/* @__PURE__ */ jsx(DropdownMenu.Viewport, { children: resolvedItems?.map((item) => isSeparator(item) ? /* @__PURE__ */ jsx(DropdownMenu.Separator, {}, item.id) : /* @__PURE__ */ jsx(MenuContentItem, {
514
+ item,
515
+ onClick: handleActionClick
516
+ }, item.id)) }), /* @__PURE__ */ jsx(DropdownMenu.Arrow, {})] }) });
517
+ };
518
+ /**
519
+ * Primary namespace export for the menu system.
520
+ *
521
+ * - `Menu.Root` — context boundary (replaces `MenuProvider`); also provides a dropdown root.
522
+ * - `Menu.Trigger` / `Menu.VirtualTrigger` — dropdown trigger (use with `Menu.Content`).
523
+ * - `Menu.Content` — renders graph-backed dropdown items inside a portal.
524
+ * - `Menu.Toolbar` — flat toolbar component with graph-backed item rendering.
525
+ */
526
+ var Menu = {
527
+ Root: MenuRoot,
528
+ Trigger: DropdownMenu.Trigger,
529
+ Content: MenuContent,
530
+ VirtualTrigger: DropdownMenu.VirtualTrigger,
531
+ Toolbar: ToolbarMenu
532
+ };
533
+ //#endregion
534
+ //#region src/presentation.ts
535
+ /**
536
+ * Merges an action's `presentation[surface]` chrome override (if any) into its properties, letting a
537
+ * single action declare multiple dispositions yet render appropriately in each surface — e.g. a
538
+ * primary toolbar button and a plain nav-tree row from the same definition.
539
+ */
540
+ var applyPresentation = (action, surface) => {
541
+ const presentation = action.properties.presentation?.[surface];
542
+ if (!presentation) return action;
543
+ return {
544
+ ...action,
545
+ properties: {
546
+ ...action.properties,
547
+ ...presentation
548
+ }
549
+ };
550
+ };
551
+ //#endregion
552
+ //#region src/hooks/useMenuActions.ts
553
+ var useMenuActions = (props) => {
554
+ const registry = useContext(RegistryContext);
555
+ const menuGraphProps = useAtomValue(props);
556
+ const graph = useMemo(() => {
557
+ const newGraph = Graph.make({ registry });
558
+ newGraph.pipe(Graph.addNodes(menuGraphProps.nodes), Graph.addEdges(menuGraphProps.edges));
559
+ return newGraph;
560
+ }, [registry, menuGraphProps]);
561
+ return { items: useCallback((group) => {
562
+ return graph.connections(group?.id || Node.RootId, "child");
563
+ }, [graph]) };
564
+ };
565
+ /**
566
+ * Convenience wrapper around `useMenuActions` that creates the backing atom inline.
567
+ * Pass a builder thunk and a dependency list — the hook memoizes `Atom.make(build)` and threads
568
+ * it through `useMenuActions`. Saves the `useMemo(() => Atom.make(...), deps)` boilerplate when
569
+ * the action graph is composed from local state (e.g. a toolbar driven by component state).
570
+ *
571
+ * Read reactive state via `get` inside the builder; `deps` should hold only stable references.
572
+ */
573
+ var useMenuBuilder = (build, deps) => {
574
+ return useMenuActions(useMemo(() => Atom.make(build), deps));
575
+ };
576
+ //#endregion
577
+ //#region src/hooks/useGraphMenuActions.ts
578
+ /**
579
+ * Flatten a node's contributed actions (and nested {@link Node.ActionGroup}s) into the
580
+ * `{ nodes, edges }` an {@link useMenuActions} / `Menu` consumes. The action nodes are spliced
581
+ * verbatim, so their Effect `data` (and captured `_actionContext`) execute via `executeMenuAction`
582
+ * without any `onAction` wiring.
583
+ *
584
+ * Pure over a `resolve` function so it is testable without a live graph; `resolve(id)` returns the
585
+ * direct actions for `id` (a node id at the top level, a group id when expanding a group).
586
+ */
587
+ var buildGraphMenu = (resolve, nodeId, options = {}) => {
588
+ const { rootId = Node.RootId, filter, surface } = options;
589
+ const nodes = [];
590
+ const edges = [];
591
+ const seen = /* @__PURE__ */ new Set();
592
+ const visit = (parentId, sourceId) => {
593
+ for (const action of resolve(sourceId)) {
594
+ if (filter && !filter(action)) continue;
595
+ edges.push({
596
+ source: parentId,
597
+ target: action.id,
598
+ relation: "child"
599
+ });
600
+ if (seen.has(action.id)) continue;
601
+ seen.add(action.id);
602
+ nodes.push(surface ? applyPresentation(action, surface) : action);
603
+ if (Node.isActionGroup(action)) visit(action.id, action.id);
604
+ }
605
+ };
606
+ visit(rootId, nodeId);
607
+ return {
608
+ nodes,
609
+ edges
610
+ };
611
+ };
612
+ /**
613
+ * Read a node's contributed actions from the app-graph as menu `ActionGraphProps`, for splicing into a
614
+ * hand-built toolbar via `MenuBuilder.subgraph(graphActions(graph, get, nodeId))`. Reactive: reads the
615
+ * graph's action atoms through `get`, so contributed actions appear/disappear as extensions update.
616
+ *
617
+ * @idiom org.dxos.react-ui-menu.graphActionsToolbar
618
+ * applies: Splicing a node's app-graph-contributed toolbar actions into a hand-built object toolbar
619
+ * instead-of: Reading `graph.actions(nodeId)` and filtering/wiring `nodes`/`edges` by hand in every toolbar
620
+ * uses: {@link graphActions}, {@link MenuBuilder}, {@link useMenuBuilder}
621
+ * related: org.dxos.react-ui-menu.toolbarMenu
622
+ */
623
+ var graphActions = (graph, get, nodeId, options) => graph && nodeId ? buildGraphMenu((id) => get(graph.actions(id)), nodeId, options) : {
624
+ nodes: [],
625
+ edges: []
626
+ };
627
+ /**
628
+ * Menu actions sourced entirely from a node's app-graph contributions — the common "show every action
629
+ * the graph offers for this object" toolbar/menu. Compose with hand-built items via {@link graphActions}.
630
+ */
631
+ var useGraphMenuActions = (graph, nodeId, options) => {
632
+ return useMenuActions(useMemo(() => Atom$1.make((get) => graphActions(graph, get, nodeId, options)), [
633
+ graph,
634
+ nodeId,
635
+ options?.rootId
636
+ ]));
637
+ };
638
+ //#endregion
639
+ //#region src/builder.ts
640
+ var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-menu/src/builder.ts";
641
+ /** Default trigger for toolbar overflow (⋮) dropdown menus. */
642
+ var overflowMenuProperties = {
643
+ label: ["toolbar-overflow.menu", { ns: translationKey }],
644
+ icon: "ph--dots-three-vertical--regular",
645
+ iconOnly: true,
646
+ caretDown: false,
647
+ variant: "dropdownMenu"
648
+ };
649
+ var MenuBuilderImpl = class MenuBuilderImpl {
650
+ _data;
651
+ _rootId;
652
+ _separatorCount = 0;
653
+ constructor(_data, _rootId) {
654
+ this._data = _data;
655
+ this._rootId = _rootId;
656
+ }
657
+ root(props) {
658
+ invariant(this._rootId === "root", "Root group can only be at the top level", {
659
+ "~LogMeta": "~LogMeta",
660
+ F: __dxlog_file,
661
+ L: 84,
662
+ S: this,
663
+ A: ["this._rootId === 'root'", "'Root group can only be at the top level'"]
664
+ });
665
+ invariant(this._data.nodes.find((node) => node.id === "root") === void 0, "Root group can only be created once", {
666
+ "~LogMeta": "~LogMeta",
667
+ F: __dxlog_file,
668
+ L: 85,
669
+ S: this,
670
+ A: ["this._data.nodes.find((node) => node.id === 'root') === undefined", "'Root group can only be created once'"]
671
+ });
672
+ this._data.nodes.push(createMenuItemGroup("root", props));
673
+ return this;
674
+ }
675
+ build() {
676
+ return this._data;
677
+ }
678
+ action(id, props, invoke) {
679
+ this._data.nodes.push(createMenuAction(id, invoke, props));
680
+ this._data.edges.push({
681
+ source: this._rootId,
682
+ target: id,
683
+ relation: "child"
684
+ });
685
+ return this;
686
+ }
687
+ switch(id, props, invoke) {
688
+ return this.action(id, {
689
+ ...props,
690
+ variant: "switch"
691
+ }, invoke);
692
+ }
693
+ group(id, props, cb) {
694
+ this._data.nodes.push(createMenuItemGroup(id, props));
695
+ this._data.edges.push({
696
+ source: this._rootId,
697
+ target: id,
698
+ relation: "child"
699
+ });
700
+ cb(new MenuBuilderImpl(this._data, id));
701
+ return this;
702
+ }
703
+ menu(id, cb, testId) {
704
+ return this.group(id, testId === void 0 ? overflowMenuProperties : {
705
+ ...overflowMenuProperties,
706
+ testId
707
+ }, cb);
708
+ }
709
+ subgraph(subgraphOrCb) {
710
+ if (!subgraphOrCb) return this;
711
+ if (typeof subgraphOrCb === "function") subgraphOrCb(new MenuBuilderImpl(this._data, this._rootId));
712
+ else {
713
+ this._data.nodes.push(...subgraphOrCb.nodes);
714
+ this._data.edges.push(...subgraphOrCb.edges);
715
+ }
716
+ return this;
717
+ }
718
+ separator(variant = "gap") {
719
+ const id = `separator-${++this._separatorCount}`;
720
+ this._data.nodes.push({
721
+ id,
722
+ type: MenuSeparatorType,
723
+ properties: { variant },
724
+ data: void 0
725
+ });
726
+ this._data.edges.push({
727
+ source: this._rootId,
728
+ target: id,
729
+ relation: "child"
730
+ });
731
+ return this;
732
+ }
733
+ };
734
+ var MenuBuilder = Object.freeze({ make: () => new MenuBuilderImpl({
735
+ nodes: [],
736
+ edges: []
737
+ }, "root") });
738
+ //#endregion
739
+ //#region src/toolbar.ts
740
+ /** `disposition` value an action opts into to appear in an object toolbar (vs context-menu-only). */
741
+ var TOOLBAR_DISPOSITION = "toolbar";
742
+ /** Filter for {@link graphActions}: keeps only actions a producer opted into the toolbar. */
743
+ var isToolbarAction = (action) => Node.hasDisposition(action, TOOLBAR_DISPOSITION);
744
+ //#endregion
745
+ export { Menu, MenuBuilder, MenuSeparatorType, TOOLBAR_DISPOSITION, actionLabel, applyPresentation, buildGraphMenu, createGapSeparator, createLineSeparator, createMenuAction, createMenuItemGroup, graphActions, isMenuGroup, isSeparator, isToolbarAction, menuContextDefaults, useGraphMenuActions, useMenu, useMenuActions, useMenuBuilder, useMenuItems, useMenuScoped };
746
+
747
+ //# sourceMappingURL=index.mjs.map