diff --git a/code/ui/components/package.json b/code/ui/components/package.json
index b46cd262b0a7..b5e51b7b6de7 100644
--- a/code/ui/components/package.json
+++ b/code/ui/components/package.json
@@ -68,6 +68,7 @@
},
"dependencies": {
"@radix-ui/react-select": "^1.2.2",
+ "@radix-ui/react-toolbar": "^1.0.4",
"@storybook/client-logger": "workspace:*",
"@storybook/csf": "^0.1.0",
"@storybook/global": "^5.0.0",
diff --git a/code/ui/components/src/experimental.ts b/code/ui/components/src/experimental.ts
index 7280862f230f..a210f26c37a6 100644
--- a/code/ui/components/src/experimental.ts
+++ b/code/ui/components/src/experimental.ts
@@ -13,3 +13,4 @@ export { Select } from './new/Select/Select';
export { Link } from './new/Link/Link';
export { Icon } from './new/Icon/Icon';
export { IconButton } from './new/IconButton/IconButton';
+export { Toolbar } from './new/Toolbar/Toolbar';
diff --git a/code/ui/components/src/new/IconButton/IconButton.stories.tsx b/code/ui/components/src/new/IconButton/IconButton.stories.tsx
index 4d4b82cf7e22..4e239b9fb1b3 100644
--- a/code/ui/components/src/new/IconButton/IconButton.stories.tsx
+++ b/code/ui/components/src/new/IconButton/IconButton.stories.tsx
@@ -52,6 +52,20 @@ export const Disabled: Story = {
},
};
+export const Animated: Story = {
+ args: {
+ ...Base.args,
+ icon: 'FaceHappy',
+ },
+ render: () => (
+
+
+
+
+
+ ),
+};
+
export const WithHref: Story = {
render: () => (
diff --git a/code/ui/components/src/new/IconButton/IconButton.tsx b/code/ui/components/src/new/IconButton/IconButton.tsx
index ae20fa9e38ad..18385bf3609d 100644
--- a/code/ui/components/src/new/IconButton/IconButton.tsx
+++ b/code/ui/components/src/new/IconButton/IconButton.tsx
@@ -1,32 +1,53 @@
-import React, { forwardRef } from 'react';
+import type { SyntheticEvent } from 'react';
+import React, { forwardRef, useEffect, useState } from 'react';
import { styled } from '@storybook/theming';
import { darken, lighten, rgba, transparentize } from 'polished';
import type { Icons } from '@storybook/icons';
import type { PropsOf } from '../utils/types';
import { Icon } from '../Icon/Icon';
-interface ButtonProps
{
+interface IconButtonProps {
icon: Icons;
as?: T;
size?: 'small' | 'medium';
variant?: 'solid' | 'outline' | 'ghost';
- onClick?: () => void;
+ onClick?: (event: SyntheticEvent) => void;
disabled?: boolean;
active?: boolean;
+ onClickAnimation?: 'none' | 'rotate360' | 'glow' | 'jiggle';
}
export const IconButton: {
(
- props: ButtonProps & Omit, keyof ButtonProps>
+ props: IconButtonProps & Omit, keyof IconButtonProps>
): JSX.Element;
displayName?: string;
} = forwardRef(
- ({ as, icon = 'FaceHappy', ...props }: ButtonProps, ref: React.Ref) => {
+ (
+ { as, icon = 'FaceHappy', onClickAnimation = 'none', onClick, ...props }: IconButtonProps,
+ ref: React.Ref
+ ) => {
const LocalIcon = Icon[icon];
+ const [isAnimating, setIsAnimating] = useState(false);
+
+ const handleClick = (event: SyntheticEvent) => {
+ if (onClick) onClick(event);
+ if (onClickAnimation === 'none') return;
+ setIsAnimating(true);
+ };
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ if (isAnimating) setIsAnimating(false);
+ }, 1000);
+ return () => clearTimeout(timer);
+ }, [isAnimating]);
return (
-
- {icon && }
+
+
+
+
);
}
@@ -34,7 +55,7 @@ export const IconButton: {
IconButton.displayName = 'IconButton';
-const StyledButton = styled.button>(
+const StyledButton = styled.button>(
({ theme, variant = 'solid', size = 'medium', disabled = false, active = false }) => ({
border: 0,
cursor: disabled ? 'not-allowed' : 'pointer',
@@ -109,3 +130,12 @@ const StyledButton = styled.button>(
},
})
);
+
+const IconWrapper = styled.div<{
+ isAnimating: boolean;
+ animation: IconButtonProps['onClickAnimation'];
+}>(({ theme, isAnimating, animation }) => ({
+ width: 14,
+ height: 14,
+ animation: isAnimating && animation !== 'none' && `${theme.animation[animation]} 1000ms ease-out`,
+}));
diff --git a/code/ui/components/src/new/Toolbar/Toolbar.stories.tsx b/code/ui/components/src/new/Toolbar/Toolbar.stories.tsx
new file mode 100644
index 000000000000..d50a8f2bab23
--- /dev/null
+++ b/code/ui/components/src/new/Toolbar/Toolbar.stories.tsx
@@ -0,0 +1,117 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import React from 'react';
+
+import { Toolbar } from './Toolbar';
+import { IconButton } from '../IconButton/IconButton';
+import { Button } from '../Button/Button';
+
+const meta: Meta = {
+ title: 'Toolbar',
+ component: Toolbar.Root,
+ tags: ['autodocs'],
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Base: Story = {
+ args: {
+ hasPadding: true,
+ borderTop: false,
+ borderBottom: true,
+ },
+ render: (_, { args }) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+};
+
+export const NoMargin: Story = {
+ args: {
+ ...Base.args,
+ hasPadding: false,
+ },
+ render: Base.render,
+};
+
+export const BorderTop: Story = {
+ args: {
+ ...Base.args,
+ borderTop: true,
+ borderBottom: false,
+ },
+ render: Base.render,
+};
+
+export const BorderBottom: Story = {
+ args: {
+ ...Base.args,
+ borderTop: false,
+ borderBottom: true,
+ },
+ render: Base.render,
+};
+
+export const BorderTopBottom: Story = {
+ args: {
+ ...Base.args,
+ borderTop: true,
+ borderBottom: true,
+ },
+ render: Base.render,
+};
diff --git a/code/ui/components/src/new/Toolbar/Toolbar.tsx b/code/ui/components/src/new/Toolbar/Toolbar.tsx
new file mode 100644
index 000000000000..827011ba06fa
--- /dev/null
+++ b/code/ui/components/src/new/Toolbar/Toolbar.tsx
@@ -0,0 +1,83 @@
+import type { ComponentPropsWithoutRef, ElementRef } from 'react';
+import React, { forwardRef } from 'react';
+import * as ToolbarPrimitive from '@radix-ui/react-toolbar';
+import { styled } from '@storybook/theming';
+
+interface RootProps extends ComponentPropsWithoutRef {
+ hasPadding?: boolean;
+ borderBottom?: boolean;
+ borderTop?: boolean;
+}
+
+const ToolbarRoot = forwardRef, RootProps>(
+ ({ className, children, ...props }, ref) => (
+
+ {children}
+
+ )
+);
+ToolbarRoot.displayName = ToolbarPrimitive.Root.displayName;
+
+const ToolbarSeparator = React.forwardRef<
+ ElementRef,
+ ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => );
+ToolbarSeparator.displayName = ToolbarPrimitive.Separator.displayName;
+
+const ToolbarToggleGroup = React.forwardRef<
+ ElementRef,
+ ToolbarPrimitive.ToolbarToggleGroupSingleProps | ToolbarPrimitive.ToolbarToggleGroupMultipleProps
+>(({ className, ...props }, ref) => );
+ToolbarToggleGroup.displayName = ToolbarPrimitive.ToggleGroup.displayName;
+
+const ToolbarToggleItem = React.forwardRef<
+ ElementRef,
+ ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => );
+ToolbarToggleItem.displayName = ToolbarPrimitive.ToggleItem.displayName;
+
+const StyledRoot = styled(ToolbarPrimitive.Root)(
+ ({ theme, hasPadding = true, borderBottom = true, borderTop = false }) => ({
+ display: 'flex',
+ padding: hasPadding ? '0 10px' : 0,
+ justifyContent: 'space-between',
+ height: 40,
+ borderBottom: borderBottom ? `1px solid ${theme.appBorderColor}` : 'none',
+ borderTop: borderTop ? `1px solid ${theme.appBorderColor}` : 'none',
+ boxSizing: 'border-box',
+ backgroundColor: theme.barBg,
+ })
+);
+
+const StyledSeparator = styled(ToolbarPrimitive.Separator)(({ theme }) => ({
+ width: 1,
+ height: 20,
+ backgroundColor: theme.appBorderColor,
+}));
+
+const StyledToggleGroup = styled(ToolbarPrimitive.ToggleGroup)({
+ display: 'flex',
+ gap: 5,
+ alignItems: 'center',
+});
+
+const Left = styled.div({
+ display: 'flex',
+ gap: 5,
+ alignItems: 'center',
+});
+
+const Right = styled.div({
+ display: 'flex',
+ gap: 5,
+ alignItems: 'center',
+});
+
+export const Toolbar = {
+ Root: ToolbarRoot,
+ Left,
+ Right,
+ ToogleGroup: ToolbarToggleGroup,
+ ToggleItem: ToolbarToggleItem,
+ Separator: ToolbarSeparator,
+};
diff --git a/code/ui/manager/src/globals/exports.ts b/code/ui/manager/src/globals/exports.ts
index 6ad6218c0fff..793165aaf103 100644
--- a/code/ui/manager/src/globals/exports.ts
+++ b/code/ui/manager/src/globals/exports.ts
@@ -114,7 +114,15 @@ export default {
'resetComponents',
'withReset',
],
- '@storybook/components/experimental': ['Button', 'Icon', 'IconButton', 'Input', 'Link', 'Select'],
+ '@storybook/components/experimental': [
+ 'Button',
+ 'Icon',
+ 'IconButton',
+ 'Input',
+ 'Link',
+ 'Select',
+ 'Toolbar',
+ ],
'@storybook/channels': [
'Channel',
'PostMessageTransport',
diff --git a/code/yarn.lock b/code/yarn.lock
index 2aff024414fb..3d8fd9362ec7 100644
--- a/code/yarn.lock
+++ b/code/yarn.lock
@@ -5204,6 +5204,34 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-roving-focus@npm:1.0.4":
+ version: 1.0.4
+ resolution: "@radix-ui/react-roving-focus@npm:1.0.4"
+ dependencies:
+ "@babel/runtime": ^7.13.10
+ "@radix-ui/primitive": 1.0.1
+ "@radix-ui/react-collection": 1.0.3
+ "@radix-ui/react-compose-refs": 1.0.1
+ "@radix-ui/react-context": 1.0.1
+ "@radix-ui/react-direction": 1.0.1
+ "@radix-ui/react-id": 1.0.1
+ "@radix-ui/react-primitive": 1.0.3
+ "@radix-ui/react-use-callback-ref": 1.0.1
+ "@radix-ui/react-use-controllable-state": 1.0.1
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 61e3ddfd1647e64fba855434ff41e8e7ba707244fe8841f78c450fbdce525383b64259279475615d030dbf1625cbffd8eeebee72d91bf6978794f5dbcf887fc0
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-select@npm:^1.2.2":
version: 1.2.2
resolution: "@radix-ui/react-select@npm:1.2.2"
@@ -5244,6 +5272,26 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-separator@npm:1.0.3":
+ version: 1.0.3
+ resolution: "@radix-ui/react-separator@npm:1.0.3"
+ dependencies:
+ "@babel/runtime": ^7.13.10
+ "@radix-ui/react-primitive": 1.0.3
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 87bcde47343f2bc4439a0dc34381f557905d9b3c1e8c5a0d32ceea62a8ef84f3abf671c5cb29309fc87759ad41d39af619ba546cf54109d64c8746e3ca683de3
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-slot@npm:1.0.2":
version: 1.0.2
resolution: "@radix-ui/react-slot@npm:1.0.2"
@@ -5260,6 +5308,80 @@ __metadata:
languageName: node
linkType: hard
+"@radix-ui/react-toggle-group@npm:1.0.4":
+ version: 1.0.4
+ resolution: "@radix-ui/react-toggle-group@npm:1.0.4"
+ dependencies:
+ "@babel/runtime": ^7.13.10
+ "@radix-ui/primitive": 1.0.1
+ "@radix-ui/react-context": 1.0.1
+ "@radix-ui/react-direction": 1.0.1
+ "@radix-ui/react-primitive": 1.0.3
+ "@radix-ui/react-roving-focus": 1.0.4
+ "@radix-ui/react-toggle": 1.0.3
+ "@radix-ui/react-use-controllable-state": 1.0.1
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 4f4761965022759ac0950ac026029b64049e1f18ef07a01ddde788b7606efcb262c9ae3a418de0c0756bf7285182ed0d268502c6f17ba86d2ff27eee5507bbf7
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-toggle@npm:1.0.3":
+ version: 1.0.3
+ resolution: "@radix-ui/react-toggle@npm:1.0.3"
+ dependencies:
+ "@babel/runtime": ^7.13.10
+ "@radix-ui/primitive": 1.0.1
+ "@radix-ui/react-primitive": 1.0.3
+ "@radix-ui/react-use-controllable-state": 1.0.1
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 9b487dad213ea7e70b0aa205e7c6f790a6f2bf394c39912e22dbe003403fd0d24a41c2efd31695fc31ab7bac286f28253dbb2fc5202cacd572ebf909f1fdc86c
+ languageName: node
+ linkType: hard
+
+"@radix-ui/react-toolbar@npm:^1.0.4":
+ version: 1.0.4
+ resolution: "@radix-ui/react-toolbar@npm:1.0.4"
+ dependencies:
+ "@babel/runtime": ^7.13.10
+ "@radix-ui/primitive": 1.0.1
+ "@radix-ui/react-context": 1.0.1
+ "@radix-ui/react-direction": 1.0.1
+ "@radix-ui/react-primitive": 1.0.3
+ "@radix-ui/react-roving-focus": 1.0.4
+ "@radix-ui/react-separator": 1.0.3
+ "@radix-ui/react-toggle-group": 1.0.4
+ peerDependencies:
+ "@types/react": "*"
+ "@types/react-dom": "*"
+ react: ^16.8 || ^17.0 || ^18.0
+ react-dom: ^16.8 || ^17.0 || ^18.0
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ "@types/react-dom":
+ optional: true
+ checksum: 3ed7ebe22ef2e8369e08bb59776671a7b8c413628249c338b8db86b4b9ac40127b4201d5bd4a9c23ea1fd21464769b4fa427d3ebcda3a7fcdbd45b256b5a753a
+ languageName: node
+ linkType: hard
+
"@radix-ui/react-use-callback-ref@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-use-callback-ref@npm:1.0.1"
@@ -6545,6 +6667,7 @@ __metadata:
dependencies:
"@popperjs/core": ^2.6.0
"@radix-ui/react-select": ^1.2.2
+ "@radix-ui/react-toolbar": ^1.0.4
"@storybook/client-logger": "workspace:*"
"@storybook/csf": ^0.1.0
"@storybook/global": ^5.0.0