@dxos/react-ui-menu 0.8.4-main.8360d9e660 → 0.8.4-main.8baae0fced

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/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +43 -58
  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 +43 -58
  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 +23 -4
  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.map +1 -1
  18. package/dist/types/src/components/Menu.d.ts +3 -1
  19. package/dist/types/src/components/Menu.d.ts.map +1 -1
  20. package/dist/types/src/components/Menu.stories.d.ts.map +1 -1
  21. package/dist/types/src/components/ToolbarMenu.d.ts +3 -1
  22. package/dist/types/src/components/ToolbarMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/ToolbarMenu.stories.d.ts +15 -1
  24. package/dist/types/src/components/ToolbarMenu.stories.d.ts.map +1 -1
  25. package/dist/types/src/hooks/useMenuActions.d.ts +9 -1
  26. package/dist/types/src/hooks/useMenuActions.d.ts.map +1 -1
  27. package/dist/types/src/index.d.ts +1 -1
  28. package/dist/types/src/index.d.ts.map +1 -1
  29. package/dist/types/src/testing/generator.d.ts +2 -2
  30. package/dist/types/src/testing/generator.d.ts.map +1 -1
  31. package/dist/types/src/types.d.ts +1 -1
  32. package/dist/types/src/types.d.ts.map +1 -1
  33. package/dist/types/src/util.d.ts +2 -2
  34. package/dist/types/src/util.d.ts.map +1 -1
  35. package/dist/types/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +28 -22
  37. package/src/builder.test.ts +224 -0
  38. package/src/builder.ts +41 -18
  39. package/src/components/ActionLabel.tsx +2 -1
  40. package/src/components/DropdownMenu.tsx +0 -1
  41. package/src/components/Menu.stories.tsx +2 -1
  42. package/src/components/Menu.test.tsx +0 -1
  43. package/src/components/Menu.tsx +4 -3
  44. package/src/components/ToolbarMenu.stories.tsx +23 -9
  45. package/src/components/ToolbarMenu.tsx +32 -16
  46. package/src/hooks/useMenuActions.ts +14 -2
  47. package/src/index.ts +1 -1
  48. package/src/testing/generator.ts +7 -7
@@ -6,9 +6,11 @@ import React, { useCallback } from 'react';
6
6
 
7
7
  import { Icon, Toolbar as NaturalToolbar, type ToolbarRootProps, useTranslation } from '@dxos/react-ui';
8
8
  import { useAttention } from '@dxos/react-ui-attention';
9
+ import { composable, composableProps } from '@dxos/ui-theme';
9
10
  import { type MenuActionProperties } from '@dxos/ui-types';
10
11
 
11
- import { translationKey } from '../translations';
12
+ import { translationKey } from '#translations';
13
+
12
14
  import {
13
15
  type MenuAction,
14
16
  type MenuItem,
@@ -19,7 +21,6 @@ import {
19
21
  isSeparator,
20
22
  } from '../types';
21
23
  import { executeMenuAction } from '../util';
22
-
23
24
  import { ActionLabel, actionLabel } from './ActionLabel';
24
25
  import { DropdownMenu } from './DropdownMenu';
25
26
  import { type MenuScopedProps, useMenuItems, useMenuScoped } from './Menu';
@@ -56,7 +57,7 @@ const ActionToolbarItem = ({ __menuScope, action }: MenuScopedProps<{ action: Me
56
57
  const { iconSize, onAction } = useMenuScoped('ActionToolbarItem', __menuScope);
57
58
  const { t } = useTranslation(translationKey);
58
59
 
59
- const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
60
+ const { icon, iconOnly = true, disabled, testId, hidden, classNames, iconClassNames } = action.properties;
60
61
  const buttonVariant =
61
62
  (action.properties as { variant?: string }).variant === 'primary' ? ('primary' as const) : ('ghost' as const);
62
63
 
@@ -87,6 +88,7 @@ const ActionToolbarItem = ({ __menuScope, action }: MenuScopedProps<{ action: Me
87
88
  icon={icon}
88
89
  size={iconSize}
89
90
  iconOnly={iconOnly}
91
+ iconClassNames={iconClassNames}
90
92
  label={actionLabel(action, t)}
91
93
  />
92
94
  ) : (
@@ -111,6 +113,11 @@ const DropdownMenuToolbarItem = ({
111
113
  // TODO(thure): Handle other menu item types.
112
114
  (activeItem as MenuAction)?.properties.icon) ||
113
115
  group.properties.icon;
116
+ // Follow the same `applyActive` rule for `iconClassNames` so a per-item
117
+ // accent (e.g. tag colour) tracks the displayed icon.
118
+ const iconClassNames =
119
+ ((group.properties as any).applyActive && (activeItem as MenuAction)?.properties.iconClassNames) ||
120
+ (group.properties as { iconClassNames?: any }).iconClassNames;
114
121
  const Root = icon ? NaturalToolbar.IconButton : NaturalToolbar.Button;
115
122
  const labelAction = (group.properties as any).applyActive && activeItem ? (activeItem as MenuAction) : group;
116
123
  const rootProps = icon
@@ -118,6 +125,7 @@ const DropdownMenuToolbarItem = ({
118
125
  icon,
119
126
  size: iconSize,
120
127
  iconOnly,
128
+ iconClassNames,
121
129
  label: actionLabel(labelAction, t),
122
130
  caretDown: true,
123
131
  }
@@ -142,7 +150,7 @@ const DropdownMenuToolbarItem = ({
142
150
  const ToggleGroupItem = ({ __menuScope, group, action }: MenuScopedProps<ToolbarMenuActionProps>) => {
143
151
  const { iconSize, onAction } = useMenuScoped('ToggleGroupItem', __menuScope);
144
152
  const { t } = useTranslation(translationKey);
145
- const { icon, iconOnly = true, disabled, testId, hidden, classNames } = action.properties;
153
+ const { icon, iconOnly = true, disabled, testId, hidden, classNames, iconClassNames } = action.properties;
146
154
 
147
155
  const handleClick = useCallback(() => {
148
156
  if (onAction) {
@@ -167,6 +175,7 @@ const ToggleGroupItem = ({ __menuScope, group, action }: MenuScopedProps<Toolbar
167
175
  icon={icon}
168
176
  size={iconSize}
169
177
  iconOnly={iconOnly}
178
+ iconClassNames={iconClassNames}
170
179
  label={actionLabel(action, t)}
171
180
  />
172
181
  ) : (
@@ -204,19 +213,26 @@ const ToggleGroupToolbarItem = ({
204
213
  }
205
214
  };
206
215
 
207
- export const ToolbarMenu = ({ __menuScope, classNames, ...props }: MenuScopedProps<ToolbarMenuProps>) => {
208
- const items = useMenuItems(undefined, undefined, 'ToolbarMenu', __menuScope);
209
- const { attendableId, alwaysActive } = useMenuScoped('ToolbarMenu', __menuScope);
210
- const { hasAttention } = useAttention(attendableId);
216
+ export const ToolbarMenu = composable<HTMLDivElement, MenuScopedProps<ToolbarMenuProps>>(
217
+ ({ __menuScope, children, ...props }, forwardedRef) => {
218
+ const items = useMenuItems(undefined, undefined, 'ToolbarMenu', __menuScope);
219
+ const { attendableId, alwaysActive } = useMenuScoped('ToolbarMenu', __menuScope);
220
+ const { hasAttention } = useAttention(attendableId);
211
221
 
212
- return (
213
- <NaturalToolbar.Root {...props} classNames={[attendableId, classNames]} disabled={!alwaysActive && !hasAttention}>
214
- {items?.map((item: MenuItem) => (
215
- <ToolbarMenuItem key={item.id} __menuScope={__menuScope} item={item} />
216
- ))}
217
- </NaturalToolbar.Root>
218
- );
219
- };
222
+ return (
223
+ <NaturalToolbar.Root
224
+ {...composableProps(props, { classNames: attendableId })}
225
+ disabled={!alwaysActive && !hasAttention}
226
+ ref={forwardedRef}
227
+ >
228
+ {items?.map((item: MenuItem) => (
229
+ <ToolbarMenuItem key={item.id} __menuScope={__menuScope} item={item} />
230
+ ))}
231
+ {children}
232
+ </NaturalToolbar.Root>
233
+ );
234
+ },
235
+ );
220
236
 
221
237
  const ToolbarMenuItem = ({ __menuScope, item }: MenuScopedProps<{ item: MenuItem }>) => {
222
238
  if (isSeparator(item)) {
@@ -2,8 +2,8 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
6
- import { useCallback, useContext, useMemo } from 'react';
5
+ import { Atom, RegistryContext, useAtomValue } from '@effect-atom/atom-react';
6
+ import { type DependencyList, useCallback, useContext, useMemo } from 'react';
7
7
 
8
8
  import { Graph, Node } from '@dxos/app-graph';
9
9
 
@@ -44,3 +44,15 @@ export const useMenuActions = (props: Atom.Atom<ActionGraphProps>): MenuActions
44
44
 
45
45
  return { items };
46
46
  };
47
+
48
+ /**
49
+ * Convenience wrapper around `useMenuActions` that creates the backing atom inline.
50
+ * Pass a builder thunk and a dependency list — the hook memoizes `Atom.make(build)` and threads
51
+ * it through `useMenuActions`. Saves the `useMemo(() => Atom.make(...), deps)` boilerplate when
52
+ * the action graph is composed from local state (e.g. a toolbar driven by component state).
53
+ */
54
+ export const useMenuBuilder = (build: (get: Atom.Context) => ActionGraphProps, deps: DependencyList): MenuActions => {
55
+ // eslint-disable-next-line react-hooks/exhaustive-deps
56
+ const atom = useMemo(() => Atom.make(build), deps);
57
+ return useMenuActions(atom);
58
+ };
package/src/index.ts CHANGED
@@ -6,4 +6,4 @@ export * from './components';
6
6
  export * from './hooks';
7
7
  export * from './types';
8
8
  export { createMenuAction, createMenuItemGroup, createLineSeparator, createGapSeparator } from './util';
9
- export { MenuBuilder } from './builder';
9
+ export { type ActionGroupBuilder, type ActionGroupBuilderFn, MenuBuilder } from './builder';
@@ -6,7 +6,7 @@ import { Atom, Registry, RegistryContext } from '@effect-atom/atom-react';
6
6
  import { useContext, useEffect } from 'react';
7
7
 
8
8
  import { Graph, Node } from '@dxos/app-graph';
9
- import { faker } from '@dxos/random';
9
+ import { random } from '@dxos/random';
10
10
 
11
11
  import { type ActionGraphProps } from '../hooks/useMenuActions';
12
12
  import { type MenuItem, type MenuItemGroup, type MenuItemsAccessor } from '../types';
@@ -38,15 +38,15 @@ const icons = {
38
38
 
39
39
  export const createActions = (params?: CreateActionsProps) => {
40
40
  const { callback = () => console.log('invoke'), count = 12, type = Node.ActionType } = params ?? {};
41
- return faker.helpers.multiple(
41
+ return random.helpers.multiple(
42
42
  () => ({
43
- id: faker.string.uuid(),
43
+ id: random.string.uuid(),
44
44
  type,
45
45
  data: type === Node.ActionGroupType ? Node.actionGroupSymbol : callback,
46
46
  properties: {
47
- label: faker.lorem.words(2),
48
- icon: faker.helpers.arrayElement(icons[faker.helpers.arrayElement(Object.keys(icons)) as keyof typeof icons]),
49
- disabled: faker.helpers.arrayElement([true, false]),
47
+ label: random.lorem.words(2),
48
+ icon: random.helpers.arrayElement(icons[random.helpers.arrayElement(Object.keys(icons)) as keyof typeof icons]),
49
+ disabled: random.helpers.arrayElement([true, false]),
50
50
  ...(type === Node.ActionGroupType && { variant: 'dropdownMenu' }),
51
51
  },
52
52
  }),
@@ -112,7 +112,7 @@ export const useMutateActions = (actionsAtom: Atom.Writable<ActionGraphProps>) =
112
112
  ...node,
113
113
  properties: {
114
114
  ...node.properties,
115
- icon: faker.helpers.arrayElement(node.properties?.icon?.endsWith('regular') ? icons.fill : icons.regular),
115
+ icon: random.helpers.arrayElement(node.properties?.icon?.endsWith('regular') ? icons.fill : icons.regular),
116
116
  disabled: !node.properties?.disabled,
117
117
  },
118
118
  };