@vector-im/compound-web 9.0.1 → 9.2.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 (191) hide show
  1. package/dist/components/ActivityMarker/Pill.cjs +1 -1
  2. package/dist/components/ActivityMarker/Unread.cjs +1 -1
  3. package/dist/components/ActivityMarker/UnreadCounter.cjs +1 -1
  4. package/dist/components/Alert/Alert.cjs +6 -6
  5. package/dist/components/Alert/Alert.module.cjs +5 -11
  6. package/dist/components/Alert/Alert.module.js +5 -11
  7. package/dist/components/Avatar/Avatar.cjs +2 -2
  8. package/dist/components/Avatar/Avatar.module.cjs +5 -5
  9. package/dist/components/Avatar/Avatar.module.cjs.map +1 -1
  10. package/dist/components/Avatar/Avatar.module.js +5 -5
  11. package/dist/components/Avatar/Avatar.module.js.map +1 -1
  12. package/dist/components/Avatar/AvatarStack.cjs +2 -2
  13. package/dist/components/Badge/Badge.cjs +10 -6
  14. package/dist/components/Badge/Badge.cjs.map +1 -1
  15. package/dist/components/Badge/Badge.d.ts +5 -1
  16. package/dist/components/Badge/Badge.d.ts.map +1 -1
  17. package/dist/components/Badge/Badge.js +9 -5
  18. package/dist/components/Badge/Badge.js.map +1 -1
  19. package/dist/components/Badge/Badge.module.cjs +4 -1
  20. package/dist/components/Badge/Badge.module.cjs.map +1 -1
  21. package/dist/components/Badge/Badge.module.js +4 -1
  22. package/dist/components/Badge/Badge.module.js.map +1 -1
  23. package/dist/components/Breadcrumb/Breadcrumb.cjs +3 -3
  24. package/dist/components/Button/Button.cjs +2 -2
  25. package/dist/components/Button/Button.module.cjs +2 -5
  26. package/dist/components/Button/Button.module.js +2 -5
  27. package/dist/components/Button/IconButton/IconButton.cjs +4 -3
  28. package/dist/components/Button/IconButton/IconButton.cjs.map +1 -1
  29. package/dist/components/Button/IconButton/IconButton.d.ts +5 -0
  30. package/dist/components/Button/IconButton/IconButton.d.ts.map +1 -1
  31. package/dist/components/Button/IconButton/IconButton.js +2 -1
  32. package/dist/components/Button/IconButton/IconButton.js.map +1 -1
  33. package/dist/components/Button/IconButton/IconButton.module.cjs +1 -3
  34. package/dist/components/Button/IconButton/IconButton.module.js +1 -3
  35. package/dist/components/Button/UnstyledButton.cjs +1 -1
  36. package/dist/components/ChatFilter/ChatFilter.cjs +1 -1
  37. package/dist/components/Dropdown/Dropdown.cjs +5 -5
  38. package/dist/components/Dropdown/Dropdown.module.cjs +7 -7
  39. package/dist/components/Dropdown/Dropdown.module.cjs.map +1 -1
  40. package/dist/components/Dropdown/Dropdown.module.js +7 -7
  41. package/dist/components/Dropdown/Dropdown.module.js.map +1 -1
  42. package/dist/components/Form/Controls/Action/Action.cjs +2 -2
  43. package/dist/components/Form/Controls/Checkbox/Checkbox.cjs +3 -3
  44. package/dist/components/Form/Controls/EditInPlace/EditInPlace.cjs +3 -3
  45. package/dist/components/Form/Controls/MFA/MFA.cjs +2 -2
  46. package/dist/components/Form/Controls/MFA/MFA.module.cjs +3 -3
  47. package/dist/components/Form/Controls/MFA/MFA.module.cjs.map +1 -1
  48. package/dist/components/Form/Controls/MFA/MFA.module.js +3 -3
  49. package/dist/components/Form/Controls/MFA/MFA.module.js.map +1 -1
  50. package/dist/components/Form/Controls/Password/Password.cjs +3 -3
  51. package/dist/components/Form/Controls/Radio/Radio.cjs +2 -2
  52. package/dist/components/Form/Controls/SettingsToggle/SettingsToggle.cjs +1 -1
  53. package/dist/components/Form/Controls/Text/Text.cjs +2 -2
  54. package/dist/components/Form/Controls/Text/Text.module.cjs +2 -2
  55. package/dist/components/Form/Controls/Text/Text.module.cjs.map +1 -1
  56. package/dist/components/Form/Controls/Text/Text.module.js +2 -2
  57. package/dist/components/Form/Controls/Text/Text.module.js.map +1 -1
  58. package/dist/components/Form/Controls/Toggle/Toggle.cjs +2 -2
  59. package/dist/components/Form/Field.cjs +2 -2
  60. package/dist/components/Form/InlineField.cjs +2 -2
  61. package/dist/components/Form/Label.cjs +2 -2
  62. package/dist/components/Form/Message.cjs +4 -4
  63. package/dist/components/Form/Root.cjs +2 -2
  64. package/dist/components/Form/Submit.cjs +1 -1
  65. package/dist/components/Form/form.module.cjs +4 -9
  66. package/dist/components/Form/form.module.js +4 -9
  67. package/dist/components/Glass/Glass.cjs +2 -2
  68. package/dist/components/Icon/BigIcon/BigIcon.cjs +2 -2
  69. package/dist/components/Icon/IndicatorIcon/IndicatorIcon.cjs +2 -2
  70. package/dist/components/InlineSpinner/InlineSpinner.cjs +3 -3
  71. package/dist/components/InlineSpinner/InlineSpinner.module.cjs +2 -2
  72. package/dist/components/InlineSpinner/InlineSpinner.module.cjs.map +1 -1
  73. package/dist/components/InlineSpinner/InlineSpinner.module.js +2 -2
  74. package/dist/components/InlineSpinner/InlineSpinner.module.js.map +1 -1
  75. package/dist/components/Link/Link.cjs +2 -2
  76. package/dist/components/Menu/CheckboxMenuItem.cjs +3 -5
  77. package/dist/components/Menu/CheckboxMenuItem.cjs.map +1 -1
  78. package/dist/components/Menu/CheckboxMenuItem.js +2 -4
  79. package/dist/components/Menu/CheckboxMenuItem.js.map +1 -1
  80. package/dist/components/Menu/ContextMenu.cjs +19 -2
  81. package/dist/components/Menu/ContextMenu.cjs.map +1 -1
  82. package/dist/components/Menu/ContextMenu.d.ts.map +1 -1
  83. package/dist/components/Menu/ContextMenu.js +18 -1
  84. package/dist/components/Menu/ContextMenu.js.map +1 -1
  85. package/dist/components/Menu/DrawerMenu.cjs +2 -2
  86. package/dist/components/Menu/FloatingMenu.cjs +2 -2
  87. package/dist/components/Menu/FloatingMenu.module.cjs +5 -8
  88. package/dist/components/Menu/FloatingMenu.module.cjs.map +1 -1
  89. package/dist/components/Menu/FloatingMenu.module.js +5 -8
  90. package/dist/components/Menu/FloatingMenu.module.js.map +1 -1
  91. package/dist/components/Menu/Menu.cjs +30 -2
  92. package/dist/components/Menu/Menu.cjs.map +1 -1
  93. package/dist/components/Menu/Menu.d.ts.map +1 -1
  94. package/dist/components/Menu/Menu.js +30 -2
  95. package/dist/components/Menu/Menu.js.map +1 -1
  96. package/dist/components/Menu/MenuContext.cjs.map +1 -1
  97. package/dist/components/Menu/MenuContext.d.ts +22 -0
  98. package/dist/components/Menu/MenuContext.d.ts.map +1 -1
  99. package/dist/components/Menu/MenuContext.js.map +1 -1
  100. package/dist/components/Menu/MenuItem.cjs +3 -3
  101. package/dist/components/Menu/MenuItem.module.cjs +8 -14
  102. package/dist/components/Menu/MenuItem.module.cjs.map +1 -1
  103. package/dist/components/Menu/MenuItem.module.js +8 -14
  104. package/dist/components/Menu/MenuItem.module.js.map +1 -1
  105. package/dist/components/Menu/MenuTitle.cjs +2 -2
  106. package/dist/components/Menu/RadioMenuItem.cjs +3 -5
  107. package/dist/components/Menu/RadioMenuItem.cjs.map +1 -1
  108. package/dist/components/Menu/RadioMenuItem.js +2 -4
  109. package/dist/components/Menu/RadioMenuItem.js.map +1 -1
  110. package/dist/components/Menu/SubMenu.cjs +24 -0
  111. package/dist/components/Menu/SubMenu.cjs.map +1 -0
  112. package/dist/components/Menu/SubMenu.d.ts +26 -0
  113. package/dist/components/Menu/SubMenu.d.ts.map +1 -0
  114. package/dist/components/Menu/SubMenu.js +22 -0
  115. package/dist/components/Menu/SubMenu.js.map +1 -0
  116. package/dist/components/Menu/ToggleMenuItem.cjs +3 -5
  117. package/dist/components/Menu/ToggleMenuItem.cjs.map +1 -1
  118. package/dist/components/Menu/ToggleMenuItem.js +2 -4
  119. package/dist/components/Menu/ToggleMenuItem.js.map +1 -1
  120. package/dist/components/Nav/Nav.module.cjs +4 -4
  121. package/dist/components/Nav/Nav.module.cjs.map +1 -1
  122. package/dist/components/Nav/Nav.module.js +4 -4
  123. package/dist/components/Nav/Nav.module.js.map +1 -1
  124. package/dist/components/Nav/NavBar.cjs +2 -2
  125. package/dist/components/Nav/NavItem.cjs +1 -1
  126. package/dist/components/PageHeader/PageHeader.cjs +36 -0
  127. package/dist/components/PageHeader/PageHeader.cjs.map +1 -0
  128. package/dist/components/PageHeader/PageHeader.js +33 -0
  129. package/dist/components/PageHeader/PageHeader.js.map +1 -0
  130. package/dist/components/PageHeader/PageHeader.module.cjs +8 -0
  131. package/dist/components/PageHeader/PageHeader.module.cjs.map +1 -0
  132. package/dist/components/PageHeader/PageHeader.module.js +8 -0
  133. package/dist/components/PageHeader/PageHeader.module.js.map +1 -0
  134. package/dist/components/Progress/Progress.cjs +2 -2
  135. package/dist/components/Progress/Progress.module.cjs +4 -4
  136. package/dist/components/Progress/Progress.module.cjs.map +1 -1
  137. package/dist/components/Progress/Progress.module.js +4 -4
  138. package/dist/components/Progress/Progress.module.js.map +1 -1
  139. package/dist/components/ReleaseAnnouncement/ReleaseAnnouncement.cjs +1 -1
  140. package/dist/components/Search/Search.cjs +3 -3
  141. package/dist/components/Search/Search.module.cjs +3 -3
  142. package/dist/components/Search/Search.module.cjs.map +1 -1
  143. package/dist/components/Search/Search.module.js +3 -3
  144. package/dist/components/Search/Search.module.js.map +1 -1
  145. package/dist/components/Separator/Separator.cjs +3 -3
  146. package/dist/components/Toast/Toast.cjs +31 -6
  147. package/dist/components/Toast/Toast.cjs.map +1 -1
  148. package/dist/components/Toast/Toast.d.ts +13 -1
  149. package/dist/components/Toast/Toast.d.ts.map +1 -1
  150. package/dist/components/Toast/Toast.js +29 -5
  151. package/dist/components/Toast/Toast.js.map +1 -1
  152. package/dist/components/Toast/Toast.module.cjs +7 -2
  153. package/dist/components/Toast/Toast.module.cjs.map +1 -1
  154. package/dist/components/Toast/Toast.module.js +7 -2
  155. package/dist/components/Toast/Toast.module.js.map +1 -1
  156. package/dist/components/Tooltip/Tooltip.cjs +2 -2
  157. package/dist/components/Tooltip/TooltipProvider.cjs +1 -1
  158. package/dist/components/Typography/Body.cjs +1 -1
  159. package/dist/components/Typography/Heading.cjs +1 -1
  160. package/dist/components/Typography/Text.cjs +1 -1
  161. package/dist/components/Typography/Typography.cjs +2 -2
  162. package/dist/components/Typography/Typography.module.cjs +1 -3
  163. package/dist/components/Typography/Typography.module.js +1 -3
  164. package/dist/components/VisualList/VisualList.cjs +2 -2
  165. package/dist/components/VisualList/VisualListItem.cjs +2 -2
  166. package/dist/index.cjs +4 -0
  167. package/dist/index.d.ts +2 -0
  168. package/dist/index.d.ts.map +1 -1
  169. package/dist/index.js +3 -1
  170. package/dist/style.css +288 -145
  171. package/package.json +3 -3
  172. package/src/components/Avatar/Avatar.module.css +1 -1
  173. package/src/components/Badge/Badge.module.css +44 -11
  174. package/src/components/Badge/Badge.tsx +10 -2
  175. package/src/components/Button/IconButton/IconButton.tsx +12 -1
  176. package/src/components/Dropdown/Dropdown.module.css +3 -1
  177. package/src/components/Form/Controls/MFA/MFA.module.css +1 -0
  178. package/src/components/Form/Controls/Text/Text.module.css +1 -0
  179. package/src/components/InlineSpinner/InlineSpinner.module.css +4 -1
  180. package/src/components/Menu/ContextMenu.tsx +24 -0
  181. package/src/components/Menu/FloatingMenu.module.css +2 -0
  182. package/src/components/Menu/Menu.tsx +56 -1
  183. package/src/components/Menu/MenuContext.tsx +23 -0
  184. package/src/components/Menu/MenuItem.module.css +27 -5
  185. package/src/components/Menu/SubMenu.tsx +62 -0
  186. package/src/components/Nav/Nav.module.css +4 -1
  187. package/src/components/Progress/Progress.module.css +5 -1
  188. package/src/components/Search/Search.module.css +1 -0
  189. package/src/components/Toast/Toast.module.css +32 -2
  190. package/src/components/Toast/Toast.tsx +68 -6
  191. package/src/index.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vector-im/compound-web",
3
- "version": "9.0.1",
3
+ "version": "9.2.0",
4
4
  "description": "Compound components for the Web",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "homepage": "https://github.com/vector-im/compound-web#readme",
53
53
  "devDependencies": {
54
- "@element-hq/element-web-playwright-common": "^2.0.0",
54
+ "@element-hq/element-web-playwright-common": "^3.0.0",
55
55
  "@fontsource/inconsolata": "^5.0.8",
56
56
  "@fontsource/inter": "^5.0.8",
57
57
  "@playwright/test": "^1.41.1",
@@ -83,7 +83,7 @@
83
83
  "eslint-plugin-react": "^7.33.2",
84
84
  "eslint-plugin-storybook": "^10.0.0",
85
85
  "jsdom": "^29.0.0",
86
- "prettier": "3.8.1",
86
+ "prettier": "3.8.2",
87
87
  "react": "^19.1.0",
88
88
  "react-dom": "^19.1.0",
89
89
  "resize-observer-polyfill": "^1.5.1",
@@ -18,7 +18,7 @@ Please see LICENSE files in the repository root for full details.
18
18
  font-family: var(--cpd-font-family-sans);
19
19
  font-weight: bold;
20
20
  overflow: hidden;
21
- user-select: none;
21
+ user-select: none; /* stylelint-disable-line defensive-css/no-user-select-none */
22
22
 
23
23
  /* Set a background color to help with visual consistency when displaying
24
24
  * avatars with a translucent background */
@@ -11,37 +11,70 @@ Please see LICENSE files in the repository root for full details.
11
11
  align-items: center;
12
12
  border-radius: 9999px; /* pill effect */
13
13
  padding: var(--cpd-space-1x) var(--cpd-space-3x);
14
+ box-sizing: border-box;
15
+ min-block-size: 28px;
16
+ }
17
+
18
+ .has-icon {
19
+ padding-inline-start: var(--cpd-space-2x);
14
20
  }
15
21
 
16
22
  .badge[data-kind="default"] {
17
- border: 1px solid var(--cpd-color-alpha-gray-400);
23
+ border: 1px solid var(--cpd-color-border-interactive-secondary);
24
+
25
+ /* To keep the same height than the other badges despite the border */
26
+ padding-block: calc(var(--cpd-space-1x) - 1px);
18
27
  outline: none;
19
- color: var(--cpd-color-gray-1100);
28
+ color: var(--cpd-color-text-primary);
29
+
30
+ svg {
31
+ color: var(--cpd-color-icon-primary);
32
+ }
20
33
  }
21
34
 
22
35
  .badge[data-kind="grey"] {
23
- background: var(--cpd-color-alpha-gray-300);
24
- color: var(--cpd-color-gray-1100);
36
+ background: var(--cpd-color-bg-badge-secondary);
37
+ color: var(--cpd-color-text-primary);
38
+
39
+ svg {
40
+ color: var(--cpd-color-icon-primary);
41
+ }
25
42
  }
26
43
 
27
44
  .badge[data-kind="on-solid"] {
28
- background: var(--cpd-color-alpha-gray-1200);
45
+ background: var(--cpd-color-bg-badge-primary);
29
46
  color: var(--cpd-color-text-on-solid-primary);
47
+
48
+ svg {
49
+ color: var(--cpd-color-icon-on-solid-primary);
50
+ }
30
51
  }
31
52
 
32
53
  .badge[data-kind="blue"] {
33
- background: var(--cpd-color-alpha-blue-300);
34
- color: var(--cpd-color-blue-1100);
54
+ background: var(--cpd-color-bg-badge-info);
55
+ color: var(--cpd-color-text-badge-info);
56
+
57
+ svg {
58
+ color: var(--cpd-color-icon-info-primary);
59
+ }
35
60
  }
36
61
 
37
62
  .badge[data-kind="green"] {
38
- background: var(--cpd-color-alpha-green-300);
39
- color: var(--cpd-color-green-1100);
63
+ background: var(--cpd-color-bg-badge-accent);
64
+ color: var(--cpd-color-text-badge-accent);
65
+
66
+ svg {
67
+ color: var(--cpd-color-icon-accent-primary);
68
+ }
40
69
  }
41
70
 
42
71
  .badge[data-kind="red"] {
43
- background: var(--cpd-color-alpha-red-300);
44
- color: var(--cpd-color-red-1100);
72
+ background: var(--cpd-color-bg-badge-critical);
73
+ color: var(--cpd-color-text-critical-primary);
74
+
75
+ svg {
76
+ color: var(--cpd-color-icon-critical-primary);
77
+ }
45
78
  }
46
79
 
47
80
  @media (forced-colors: active) {
@@ -6,7 +6,7 @@ Please see LICENSE files in the repository root for full details.
6
6
  */
7
7
 
8
8
  import classnames from "classnames";
9
- import React, { type PropsWithChildren } from "react";
9
+ import React, { type ComponentType, type PropsWithChildren } from "react";
10
10
  import styles from "./Badge.module.css";
11
11
  import { Typography } from "../Typography/Typography";
12
12
 
@@ -19,6 +19,10 @@ type BadgeProps = {
19
19
  * The type of badge.
20
20
  */
21
21
  kind?: "default" | "grey" | "on-solid" | "blue" | "green" | "red";
22
+ /**
23
+ * An icon to display within the badge.
24
+ */
25
+ Icon?: ComponentType<React.SVGAttributes<SVGElement>>;
22
26
  };
23
27
 
24
28
  /**
@@ -26,10 +30,13 @@ type BadgeProps = {
26
30
  */
27
31
  export const Badge: React.FC<PropsWithChildren<BadgeProps>> = ({
28
32
  children,
33
+ Icon,
29
34
  kind = "default",
30
35
  className,
31
36
  }) => {
32
- const classes = classnames(styles.badge, className);
37
+ const classes = classnames(styles.badge, className, {
38
+ [styles["has-icon"]]: !!Icon,
39
+ });
33
40
  return (
34
41
  <Typography
35
42
  as="span"
@@ -38,6 +45,7 @@ export const Badge: React.FC<PropsWithChildren<BadgeProps>> = ({
38
45
  className={classes}
39
46
  data-kind={kind}
40
47
  >
48
+ {Icon && <Icon width="16" height="16" aria-hidden={true} />}
41
49
  {children}
42
50
  </Typography>
43
51
  );
@@ -47,6 +47,10 @@ type IconButtonProps = UnstyledButtonPropsFor<"button"> & {
47
47
  * Optional tooltip for the button
48
48
  */
49
49
  tooltip?: string;
50
+ /**
51
+ * The placement of the tooltip, if `tooltip` is provided.
52
+ */
53
+ tooltipPlacement?: React.ComponentProps<typeof Tooltip>["placement"];
50
54
  /**
51
55
  * Hide the background when the button is not active or hovered.
52
56
  * @default false
@@ -71,6 +75,7 @@ export const IconButton = forwardRef<
71
75
  disabled,
72
76
  destructive,
73
77
  tooltip,
78
+ tooltipPlacement,
74
79
  noBackground = false,
75
80
  ...props
76
81
  },
@@ -106,5 +111,11 @@ export const IconButton = forwardRef<
106
111
  </UnstyledButton>
107
112
  );
108
113
 
109
- return tooltip ? <Tooltip label={tooltip}>{button}</Tooltip> : button;
114
+ return tooltip ? (
115
+ <Tooltip label={tooltip} placement={tooltipPlacement}>
116
+ {button}
117
+ </Tooltip>
118
+ ) : (
119
+ button
120
+ );
110
121
  });
@@ -31,7 +31,9 @@ Please see LICENSE files in the repository root for full details.
31
31
  gap: var(--cpd-space-4x);
32
32
 
33
33
  svg {
34
- transition: transform 0.1s linear;
34
+ @media (prefers-reduced-motion: no-preference) {
35
+ transition: transform 0.1s linear;
36
+ }
35
37
  }
36
38
  }
37
39
 
@@ -90,6 +90,7 @@ Please see LICENSE files in the repository root for full details.
90
90
  border-color: var(--cpd-color-text-critical-primary);
91
91
  }
92
92
 
93
+ /* stylelint-disable-next-line defensive-css/require-focus-visible */
93
94
  .control:focus ~ .digit:not([data-filled]) {
94
95
  outline: 2px solid var(--cpd-color-border-focused);
95
96
  border-color: transparent;
@@ -35,6 +35,7 @@ Please see LICENSE files in the repository root for full details.
35
35
  border-color: var(--cpd-color-border-interactive-hovered);
36
36
  }
37
37
 
38
+ /* stylelint-disable-next-line defensive-css/require-focus-visible */
38
39
  .control:focus {
39
40
  outline: 2px solid var(--cpd-color-border-focused);
40
41
  border-color: transparent;
@@ -22,5 +22,8 @@ Please see LICENSE files in the repository root for full details.
22
22
  align-items: center;
23
23
  inline-size: 100%;
24
24
  block-size: 100%;
25
- animation: 1s linear spin infinite;
25
+
26
+ @media (prefers-reduced-motion: no-preference) {
27
+ animation: 1s linear spin infinite;
28
+ }
26
29
  }
@@ -18,6 +18,10 @@ import {
18
18
  Portal,
19
19
  Content,
20
20
  ContextMenuItem,
21
+ ContextMenuSub,
22
+ ContextMenuSubTrigger,
23
+ ContextMenuSubContent,
24
+ ContextMenuPortal,
21
25
  } from "@radix-ui/react-context-menu";
22
26
  import { FloatingMenu } from "./FloatingMenu";
23
27
  import { Drawer } from "vaul";
@@ -27,6 +31,7 @@ import {
27
31
  MenuContext,
28
32
  type MenuData,
29
33
  type MenuItemWrapperProps,
34
+ type SubMenuWrapperProps,
30
35
  } from "./MenuContext";
31
36
  import { DrawerMenu } from "./DrawerMenu";
32
37
  import { getPlatform } from "../../utils/platform";
@@ -73,6 +78,24 @@ const ContextMenuItemWrapper: FC<MenuItemWrapperProps> = ({
73
78
  </ContextMenuItem>
74
79
  );
75
80
 
81
+ const ContextSubMenuWrapper: FC<SubMenuWrapperProps> = ({
82
+ trigger,
83
+ children,
84
+ open,
85
+ onOpenChange,
86
+ }) => (
87
+ <ContextMenuSub open={open} onOpenChange={onOpenChange}>
88
+ <ContextMenuSubTrigger asChild>{trigger}</ContextMenuSubTrigger>
89
+ <ContextMenuPortal>
90
+ <ContextMenuSubContent asChild alignOffset={-20}>
91
+ <FloatingMenu title="" showTitle={false}>
92
+ {children}
93
+ </FloatingMenu>
94
+ </ContextMenuSubContent>
95
+ </ContextMenuPortal>
96
+ </ContextMenuSub>
97
+ );
98
+
76
99
  /**
77
100
  * A menu opened by right-clicking or long-pressing another UI element.
78
101
  */
@@ -100,6 +123,7 @@ export const ContextMenu: FC<Props> = ({
100
123
  const context: MenuData = useMemo(
101
124
  () => ({
102
125
  MenuItemWrapper: drawer ? null : ContextMenuItemWrapper,
126
+ SubMenuWrapper: drawer ? null : ContextSubMenuWrapper,
103
127
  onOpenChange,
104
128
  }),
105
129
  [onOpenChange],
@@ -35,6 +35,8 @@ Please see LICENSE files in the repository root for full details.
35
35
  }
36
36
 
37
37
  .menu[data-state="open"] {
38
+ /* Disable linter, we have a reduced motion style below */
39
+ /* stylelint-disable-next-line defensive-css/require-prefers-reduced-motion */
38
40
  animation: slide-in 180ms;
39
41
  }
40
42
 
@@ -5,13 +5,23 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5
5
  Please see LICENSE files in the repository root for full details.
6
6
  */
7
7
 
8
- import React, { type FC, type ReactNode, useMemo } from "react";
8
+ import React, {
9
+ type FC,
10
+ type ReactNode,
11
+ useMemo,
12
+ useEffect,
13
+ useState,
14
+ } from "react";
9
15
  import {
10
16
  Root,
11
17
  Trigger,
12
18
  Portal,
13
19
  Content,
14
20
  DropdownMenuItem,
21
+ DropdownMenuSub,
22
+ DropdownMenuSubTrigger,
23
+ DropdownMenuSubContent,
24
+ DropdownMenuPortal,
15
25
  } from "@radix-ui/react-dropdown-menu";
16
26
  import { FloatingMenu } from "./FloatingMenu";
17
27
  import { Drawer } from "vaul";
@@ -21,6 +31,7 @@ import {
21
31
  MenuContext,
22
32
  type MenuData,
23
33
  type MenuItemWrapperProps,
34
+ type SubMenuWrapperProps,
24
35
  } from "./MenuContext";
25
36
  import { DrawerMenu } from "./DrawerMenu";
26
37
  import { getPlatform } from "../../utils/platform";
@@ -84,6 +95,49 @@ const DropdownMenuItemWrapper: FC<MenuItemWrapperProps> = ({
84
95
  </DropdownMenuItem>
85
96
  );
86
97
 
98
+ /** Duration of the parent menu's slide-in animation (ms). */
99
+ const MENU_ANIMATION_DURATION = 180;
100
+
101
+ const DropdownSubMenuWrapper: FC<SubMenuWrapperProps> = ({
102
+ trigger,
103
+ children,
104
+ open: openProp,
105
+ onOpenChange,
106
+ }) => {
107
+ // When the submenu is programmatically opened at the same time as the parent
108
+ // menu (e.g. open={true} on mount), the parent is still mid-animation and
109
+ // the trigger position hasn't settled. Defer the open so the submenu
110
+ // positions correctly after the parent animation completes.
111
+ const [deferredOpen, setDeferredOpen] = useState(false);
112
+
113
+ useEffect(() => {
114
+ if (openProp) {
115
+ const timer = setTimeout(
116
+ () => setDeferredOpen(true),
117
+ MENU_ANIMATION_DURATION,
118
+ );
119
+ return () => clearTimeout(timer);
120
+ } else {
121
+ setDeferredOpen(false);
122
+ }
123
+ }, [openProp]);
124
+
125
+ const open = openProp ? deferredOpen : openProp;
126
+
127
+ return (
128
+ <DropdownMenuSub open={open} onOpenChange={onOpenChange}>
129
+ <DropdownMenuSubTrigger asChild>{trigger}</DropdownMenuSubTrigger>
130
+ <DropdownMenuPortal>
131
+ <DropdownMenuSubContent asChild alignOffset={-20}>
132
+ <FloatingMenu title="" showTitle={false}>
133
+ {children}
134
+ </FloatingMenu>
135
+ </DropdownMenuSubContent>
136
+ </DropdownMenuPortal>
137
+ </DropdownMenuSub>
138
+ );
139
+ };
140
+
87
141
  /**
88
142
  * A menu opened by pressing a button.
89
143
  */
@@ -105,6 +159,7 @@ export const Menu: FC<Props> = ({
105
159
  const context: MenuData = useMemo(
106
160
  () => ({
107
161
  MenuItemWrapper: drawer ? null : DropdownMenuItemWrapper,
162
+ SubMenuWrapper: drawer ? null : DropdownSubMenuWrapper,
108
163
  onOpenChange,
109
164
  }),
110
165
  [onOpenChange],
@@ -17,11 +17,34 @@ export interface MenuItemWrapperProps {
17
17
  children: ReactNode;
18
18
  }
19
19
 
20
+ export interface SubMenuWrapperProps {
21
+ /**
22
+ * The trigger element that opens the submenu (typically a MenuItem).
23
+ */
24
+ trigger: ReactNode;
25
+ /**
26
+ * The submenu contents.
27
+ */
28
+ children: ReactNode;
29
+ /**
30
+ * Whether the submenu is open (controlled).
31
+ */
32
+ open?: boolean;
33
+ /**
34
+ * Event handler called when the open state of the submenu changes.
35
+ */
36
+ onOpenChange?: (open: boolean) => void;
37
+ }
38
+
20
39
  export interface MenuData {
21
40
  /**
22
41
  * A component that wraps interactive menu items.
23
42
  */
24
43
  MenuItemWrapper: ComponentType<MenuItemWrapperProps> | null;
44
+ /**
45
+ * A component that wraps submenus.
46
+ */
47
+ SubMenuWrapper: ComponentType<SubMenuWrapperProps> | null;
25
48
  /**
26
49
  * Event handler called when the open state of the menu changes.
27
50
  */
@@ -23,7 +23,8 @@ Please see LICENSE files in the repository root for full details.
23
23
  background: var(--cpd-color-bg-action-secondary-rest);
24
24
  }
25
25
 
26
- .item.interactive {
26
+ .item.interactive,
27
+ .item[data-state] {
27
28
  cursor: pointer;
28
29
  }
29
30
 
@@ -88,12 +89,31 @@ button.item {
88
89
  color: var(--cpd-color-icon-critical-primary);
89
90
  }
90
91
 
92
+ /* Submenu triggers: always show the chevron and apply hover style when open */
93
+ .item[data-state] > .nav-hint {
94
+ display: initial;
95
+ }
96
+
97
+ .item[data-state] > .nav-hint ~ * {
98
+ display: none;
99
+ }
100
+
101
+ .item[data-state="open"][data-kind="primary"] {
102
+ background: var(--cpd-color-bg-action-secondary-hovered);
103
+ }
104
+
105
+ .item[data-state="open"][data-kind="critical"] {
106
+ background: var(--cpd-color-bg-critical-subtle);
107
+ }
108
+
91
109
  @media (hover) {
92
- .item.interactive[data-kind="primary"]:hover {
110
+ .item.interactive[data-kind="primary"]:hover,
111
+ .item[data-state][data-kind="primary"]:hover {
93
112
  background: var(--cpd-color-bg-action-secondary-hovered);
94
113
  }
95
114
 
96
- .item.interactive[data-kind="critical"]:hover {
115
+ .item.interactive[data-kind="critical"]:hover,
116
+ .item[data-state][data-kind="critical"]:hover {
97
117
  background: var(--cpd-color-bg-critical-subtle);
98
118
  }
99
119
 
@@ -107,11 +127,13 @@ button.item {
107
127
  }
108
128
  }
109
129
 
110
- .item.interactive[data-kind="primary"]:active {
130
+ .item.interactive[data-kind="primary"]:active,
131
+ .item[data-state][data-kind="primary"]:active {
111
132
  background: var(--cpd-color-bg-action-secondary-pressed);
112
133
  }
113
134
 
114
- .item.interactive[data-kind="critical"]:active {
135
+ .item.interactive[data-kind="critical"]:active,
136
+ .item[data-state][data-kind="critical"]:active {
115
137
  background: var(--cpd-color-bg-critical-subtle-hovered);
116
138
  }
117
139
 
@@ -0,0 +1,62 @@
1
+ /*
2
+ Copyright 2026 Element Creations Ltd.
3
+
4
+ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5
+ Please see LICENSE files in the repository root for full details.
6
+ */
7
+
8
+ import React, { type FC, type ReactNode, useContext } from "react";
9
+ import { MenuContext } from "./MenuContext";
10
+
11
+ interface Props {
12
+ /**
13
+ * The trigger element that opens the submenu. This should be a MenuItem.
14
+ */
15
+ trigger: ReactNode;
16
+ /**
17
+ * Whether the submenu is open (controlled).
18
+ */
19
+ open?: boolean;
20
+ /**
21
+ * Event handler called when the open state of the submenu changes.
22
+ */
23
+ onOpenChange?: (open: boolean) => void;
24
+ /**
25
+ * The submenu contents (typically MenuItem elements).
26
+ */
27
+ children: ReactNode;
28
+ }
29
+
30
+ /**
31
+ * A submenu within a Menu or ContextMenu. The trigger should be a MenuItem
32
+ * component and the children are the submenu items.
33
+ */
34
+ export const SubMenu: FC<Props> = ({
35
+ trigger,
36
+ open,
37
+ onOpenChange,
38
+ children,
39
+ }) => {
40
+ const context = useContext(MenuContext);
41
+
42
+ // When there's no SubMenuWrapper (e.g. drawer on mobile), flatten the
43
+ // submenu items inline — nested flyouts don't work well in drawers.
44
+ if (context?.SubMenuWrapper == null) {
45
+ return (
46
+ <>
47
+ {trigger}
48
+ {children}
49
+ </>
50
+ );
51
+ }
52
+
53
+ return (
54
+ <context.SubMenuWrapper
55
+ trigger={trigger}
56
+ open={open}
57
+ onOpenChange={onOpenChange}
58
+ >
59
+ {children}
60
+ </context.SubMenuWrapper>
61
+ );
62
+ };
@@ -36,7 +36,10 @@
36
36
  block-size: 0;
37
37
  border-radius: var(--cpd-radius-pill-effect) var(--cpd-radius-pill-effect) 0 0;
38
38
  background-color: var(--cpd-color-bg-action-primary-rest);
39
- transition: height 0.1s ease-in-out;
39
+
40
+ @media (prefers-reduced-motion: no-preference) {
41
+ transition: height 0.1s ease-in-out;
42
+ }
40
43
  }
41
44
 
42
45
  .nav-tab[data-current]::before {
@@ -59,7 +59,11 @@ Please see LICENSE files in the repository root for full details.
59
59
  .progress-bar-indicator {
60
60
  position: absolute;
61
61
  inset: 0;
62
- transition: transform 0.2s ease-in-out;
62
+
63
+ @media (prefers-reduced-motion: no-preference) {
64
+ transition: transform 0.2s ease-in-out;
65
+ }
66
+
63
67
  background-image: linear-gradient(
64
68
  135deg,
65
69
  var(--cpd-progress-bar-muted) 0%,
@@ -61,6 +61,7 @@ Please see LICENSE files in the repository root for full details.
61
61
  color: var(--cpd-color-text-secondary);
62
62
  }
63
63
 
64
+ /* stylelint-disable-next-line defensive-css/require-focus-visible */
64
65
  .input:focus::placeholder {
65
66
  color: var(--cpd-color-text-secondary);
66
67
  }
@@ -7,11 +7,14 @@ Please see LICENSE files in the repository root for full details.
7
7
 
8
8
  .toast-container {
9
9
  inline-size: fit-content;
10
- background-color: var(--cpd-color-alpha-gray-1300);
10
+ background-color: var(--cpd-color-bg-action-primary-rest);
11
11
  color: var(--cpd-color-text-on-solid-primary);
12
12
  border-radius: 99px;
13
- font-size: var(--cpd-font-body-sm-medium);
14
13
  padding: var(--cpd-space-2x) var(--cpd-space-4x);
14
+ display: flex;
15
+ flex-wrap: nowrap;
16
+ align-items: center;
17
+ gap: var(--cpd-space-2x);
15
18
  }
16
19
 
17
20
  @media (forced-colors: active) {
@@ -19,3 +22,30 @@ Please see LICENSE files in the repository root for full details.
19
22
  outline: 1px solid transparent;
20
23
  }
21
24
  }
25
+
26
+ .icon {
27
+ flex-shrink: 0;
28
+ }
29
+
30
+ .has-close {
31
+ gap: var(--cpd-space-3x);
32
+ }
33
+
34
+ .content {
35
+ display: flex;
36
+ flex-wrap: nowrap;
37
+ align-items: flex-start;
38
+ gap: var(--cpd-space-2x);
39
+ }
40
+
41
+ .close {
42
+ align-self: flex-start;
43
+ padding: var(--cpd-space-0-5x) !important;
44
+
45
+ &:not(:hover, :focus-visible) {
46
+ * {
47
+ /* Override default color of icon button. The container background is different than the default canvas color */
48
+ color: var(--cpd-color-icon-on-solid-primary) !important;
49
+ }
50
+ }
51
+ }