diff --git a/addons/toolbars/README.md b/addons/toolbars/README.md
new file mode 100644
index 000000000000..2583c2b721d6
--- /dev/null
+++ b/addons/toolbars/README.md
@@ -0,0 +1,177 @@
+
+
+
+
+Storybook Addon Toolbars
+
+The Toolbars addon controls global story rendering options from Storybook's toolbar UI. It's a general purpose addon that can be used to:
+
+- set a theme for your components
+- set your components' internationalization (i18n) locale
+- configure just about anything in Storybook that makes use of a global variable
+
+Toolbars is implemented using Storybook Args (SB6.0+): dynamic variables that trigger a story re-render when they are set.
+
+- [Get started](#get-started)
+ - [Installation](#installation)
+ - [Configure menu UI](#configure-menu-ui)
+ - [Create a decorator](#create-a-decorator)
+- [Advanced usage](#advanced-usage)
+ - [Advanced menu configuration](#advanced-menu-configuration)
+ - [Consuming global args from within a story](#consuming-global-args-from-within-a-story)
+- [FAQs](#faqs)
+ - [How does this compare to `addon-contexts`?](#how-does-this-compare-to-addon-contexts)
+
+## Get started
+
+To get started with `addon-toolbars`: (1) [install the addon](#installation), (2) [configure the menu UI](#configure-menu-ui), and (3) [Create a decorator to implement custom logic](#create-a-decorator).
+
+### Installation
+
+First, install the package:
+
+```sh
+npm install @storybook/addon-toolbars -D # or yarn
+```
+
+Then add it to your `.storybook/main.js` config:
+
+```js
+module.exports = {
+ addons: ['@storybook/addon-toolbars'],
+};
+```
+
+### Configure menu UI
+
+Addon-toolbars has a simple, declarative syntax for configuring toolbar menus. You can add toolbars by adding `globalArgTypes` with a `toolbar` annotation, in `.storybook/preview.js`:
+
+```js
+export const globalArgTypes = {
+ theme: {
+ name: 'Theme'
+ description: 'Global theme for components',
+ defaultValue: 'light',
+ toolbar: { icon: 'box', options: ['light','dark', 'medium'] },
+ }
+}
+```
+
+You should see a dropdown in your toolbar with options `light`, `dark`, and `medium`.
+
+### Create a decorator
+
+Now, let's wire it up! We can consume our new `theme` global arg in a decorator using the `context.globalArgs.theme` value.
+
+For example, suppose you are using`styled-components`. You can add a theme provider decorator to your `.storybook/preview.js` config:
+
+```js
+const styledComponentsThemeDecorator = (storyFn, { globalArgs: { theme } }) => (
+ {storyFn()}
+);
+
+export const decorators = [styledComponentsThemeDecorator];
+```
+
+## Advanced usage
+
+The previous section shows the common case. There are two advanced use cases: (1) [advanced menu configurations](#advanced-menu-configuration), (2) [consuming global args inside a story](#consuming-global-args-from-within-a-story).
+
+### Advanced menu configuration
+
+The default menu configuration is simple: everything's a string! However, the Toolbars addon also support configuration options to tweak the appearance of the menu:
+
+```ts
+type MenuItem {
+ /**
+ * The string value of the menu that gets set in the global args
+ */
+ value: string,
+ /**
+ * The main text of the title
+ */
+ title: string,
+ /**
+ * A string that gets shown in left side of the menu, if set
+ */
+ left?: string,
+ /**
+ * A string that gets shown in right side of the menu, if set
+ */
+ right?: string,
+ /**
+ * An icon that gets shown in the toolbar if this item is selected
+ */
+ icon?: icon,
+}
+```
+
+Thus if you want to show right-justified flags for an internationalization locale, you might set up the following configuration in `.storybook/preview.js`:
+
+```js
+export const globalArgTypes = {
+ locale: {
+ name: 'Locale',
+ description: 'Internationalization locale',
+ defaultValue: 'en',
+ toolbar: {
+ icon: 'globe',
+ items: [
+ { value: 'en', right: '🇺🇸', title: 'English' },
+ { value: 'fr', right: '🇫🇷', title: 'Français' },
+ { value: 'es', right: '🇪🇸', title: 'Español' },
+ { value: 'zh', right: '🇨🇳', title: 'ä¸æ–‡' },
+ { value: 'kr', right: '🇰🇷', title: 'í•œêµì–´' },
+ ],
+ }
+ },
+ },
+};
+```
+
+### Consuming global args from within a story
+
+The recommended usage, as shown in the examples above, is to consume global args from within a decorator and implement a global setting that applies to all stories. But sometimes it's useful to use toolbar options inside individual stories.
+
+Storybook's `globalArgs` are available via the story context:
+
+```js
+const getCaptionForLocale = (locale) => {
+ switch(locale) {
+ case 'es': return 'Hola!';
+ case 'fr': return 'Bonjour!';
+ case 'kr': return '안녕하세요!';
+ case 'zh': return 'ä½ å¥½!';
+ default:
+ return 'Hello!',
+ }
+}
+
+export const StoryWithLocale = ({ globalArgs: { locale } }) => {
+ const caption = getCaptionForLocale(locale);
+ return <>{caption}>
+};
+```
+
+**NOTE:** In Storybook 6.0, if you set the global option `passArgsFirst`, the story context is passes as the second argument:
+
+```js
+export const StoryWithLocale = (args, { globalArgs: { locale } }) => {
+ const caption = getCaptionForLocale(locale);
+ return <>{caption}>;
+};
+```
+
+## FAQs
+
+### How does this compare to `addon-contexts`?
+
+`Addon-toolbars` is the successor to `addon-contexts`, which provided convenient global toolbars in Storybook's toolbar.
+
+The primary difference between the two packages is that `addon-toolbars` makes use of Storybook's new **Story Args** feature, which has the following advantages:
+
+- **Standardization**. Args are built into Storybook in 6.x. Since `addon-toolbars` is based on args, you don't need to learn any addon-specific APIs to use it.
+
+- **Ergonomics**. Global args are easy to consume [in stories](#consuming-global-args-from-within-a-story), in [Storybook Docs](https://github.com/storybookjs/storybook/tree/master/addons/docs), or even in other addons.
+
+* **Framework compatibility**. Args are completely framework-independent, so `addon-toolbars` is compatible with React, Vue, Angular, etc. out of the box with no framework logic needed in the addon.
diff --git a/addons/toolbars/docs/hero.gif b/addons/toolbars/docs/hero.gif
new file mode 100644
index 000000000000..80ca15be907c
Binary files /dev/null and b/addons/toolbars/docs/hero.gif differ
diff --git a/addons/toolbars/package.json b/addons/toolbars/package.json
new file mode 100644
index 000000000000..557d0caef5db
--- /dev/null
+++ b/addons/toolbars/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "@storybook/addon-toolbars",
+ "version": "6.0.0-alpha.26",
+ "description": "Storybook Addon Controls",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/storybookjs/storybook.git",
+ "directory": "addons/toolbars"
+ },
+ "license": "MIT",
+ "main": "dist/register.js",
+ "files": [
+ "dist/**/*",
+ "README.md",
+ "*.js",
+ "*.d.ts"
+ ],
+ "scripts": {
+ "prepare": "node ../../scripts/prepare.js"
+ },
+ "dependencies": {
+ "@storybook/addons": "6.0.0-alpha.26",
+ "@storybook/api": "6.0.0-alpha.26",
+ "@storybook/client-api": "6.0.0-alpha.26",
+ "@storybook/components": "6.0.0-alpha.26"
+ },
+ "peerDependencies": {
+ "react": "*"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/addons/toolbars/preset.js b/addons/toolbars/preset.js
new file mode 100644
index 000000000000..a83f95279e7f
--- /dev/null
+++ b/addons/toolbars/preset.js
@@ -0,0 +1 @@
+module.exports = require('./dist/preset');
diff --git a/addons/toolbars/register.js b/addons/toolbars/register.js
new file mode 100644
index 000000000000..06b5a5887266
--- /dev/null
+++ b/addons/toolbars/register.js
@@ -0,0 +1 @@
+export * from './dist/register';
diff --git a/addons/toolbars/src/components/MenuToolbar.tsx b/addons/toolbars/src/components/MenuToolbar.tsx
new file mode 100644
index 000000000000..45ac06525739
--- /dev/null
+++ b/addons/toolbars/src/components/MenuToolbar.tsx
@@ -0,0 +1,47 @@
+import React, { FC } from 'react';
+import { useGlobalArgs } from '@storybook/api';
+import { Icons, IconButton, WithTooltip, TooltipLinkList } from '@storybook/components';
+import { NormalizedToolbarArgType } from '../types';
+
+export type MenuToolbarProps = NormalizedToolbarArgType & { id: string };
+
+export const MenuToolbar: FC = ({
+ id,
+ name,
+ description,
+ toolbar: { icon, items },
+}) => {
+ const [globalArgs, updateGlobalArgs] = useGlobalArgs();
+ const selectedValue = globalArgs[id];
+ const active = selectedValue != null;
+ const selectedItem = active && items.find(item => item.value === selectedValue);
+
+ return (
+ {
+ const links = items.map(item => {
+ const { value, left, title, right } = item;
+ return {
+ id: value,
+ left,
+ title,
+ right,
+ active: selectedValue === value,
+ onClick: () => {
+ updateGlobalArgs({ [id]: value });
+ onHide();
+ },
+ };
+ });
+ return ;
+ }}
+ closeOnClick
+ >
+
+
+
+
+ );
+};
diff --git a/addons/toolbars/src/components/ToolbarManager.tsx b/addons/toolbars/src/components/ToolbarManager.tsx
new file mode 100644
index 000000000000..b55857f6e11a
--- /dev/null
+++ b/addons/toolbars/src/components/ToolbarManager.tsx
@@ -0,0 +1,37 @@
+import React, { FC } from 'react';
+import { useGlobalArgTypes } from '@storybook/api';
+import { Separator } from '@storybook/components';
+
+import { ToolbarArgType } from '../types';
+import { MenuToolbar } from './MenuToolbar';
+
+const normalize = (key: string, argType: ToolbarArgType) => ({
+ ...argType,
+ name: argType.name || key,
+ description: argType.description || key,
+ toolbar: {
+ ...argType.toolbar,
+ items: argType.toolbar.items.map(item =>
+ typeof item === 'string' ? { value: item, title: item } : item
+ ),
+ },
+});
+
+/**
+ * A smart component for handling manager-preview interactions.
+ */
+export const ToolbarManager: FC = () => {
+ const globalArgTypes = useGlobalArgTypes();
+ const keys = Object.keys(globalArgTypes).filter(key => !!globalArgTypes[key].toolbar);
+ if (!keys.length) return null;
+
+ return (
+ <>
+
+ {keys.map(key => {
+ const normalizedConfig = normalize(key, globalArgTypes[key] as ToolbarArgType);
+ return ;
+ })}
+ >
+ );
+};
diff --git a/addons/toolbars/src/constants.ts b/addons/toolbars/src/constants.ts
new file mode 100644
index 000000000000..1be8012f12e3
--- /dev/null
+++ b/addons/toolbars/src/constants.ts
@@ -0,0 +1,2 @@
+export const ID = 'addon-toolbars' as const;
+export const PARAM = 'toolbars' as const;
diff --git a/addons/toolbars/src/preset/index.ts b/addons/toolbars/src/preset/index.ts
new file mode 100644
index 000000000000..0e20177db06c
--- /dev/null
+++ b/addons/toolbars/src/preset/index.ts
@@ -0,0 +1,3 @@
+export function managerEntries(entry: any[] = []) {
+ return [...entry, require.resolve('../register')];
+}
diff --git a/addons/toolbars/src/register.tsx b/addons/toolbars/src/register.tsx
new file mode 100644
index 000000000000..739214fb5b50
--- /dev/null
+++ b/addons/toolbars/src/register.tsx
@@ -0,0 +1,13 @@
+import React from 'react';
+import addons, { types } from '@storybook/addons';
+import { ToolbarManager } from './components/ToolbarManager';
+import { ID } from './constants';
+
+addons.register(ID, api =>
+ addons.add(ID, {
+ title: ID,
+ type: types.TOOL,
+ match: ({ viewMode }) => viewMode === 'story',
+ render: () => ,
+ })
+);
diff --git a/addons/toolbars/src/types.ts b/addons/toolbars/src/types.ts
new file mode 100644
index 000000000000..6e9b38ddf9d0
--- /dev/null
+++ b/addons/toolbars/src/types.ts
@@ -0,0 +1,27 @@
+import { IconsProps } from '@storybook/components';
+import { ArgType } from '@storybook/api';
+
+export interface ToolbarItem {
+ value: string;
+ icon?: IconsProps['icon'];
+ left?: string;
+ right?: string;
+ title?: string;
+}
+
+export interface NormalizedToolbarConfig {
+ icon?: IconsProps['icon'];
+ items: ToolbarItem[];
+}
+
+export type NormalizedToolbarArgType = ArgType & {
+ toolbar: NormalizedToolbarConfig;
+};
+
+export type ToolbarConfig = NormalizedToolbarConfig & {
+ items: string[] | ToolbarItem[];
+};
+
+export type ToolbarArgType = ArgType & {
+ toolbar: ToolbarConfig;
+};
diff --git a/addons/toolbars/tsconfig.json b/addons/toolbars/tsconfig.json
new file mode 100644
index 000000000000..eac4a67bed71
--- /dev/null
+++ b/addons/toolbars/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "types": ["webpack-env", "jest"]
+ },
+ "include": ["src/**/*"],
+ "exclude": ["src/**.test.ts"]
+}
diff --git a/examples/official-storybook/main.js b/examples/official-storybook/main.js
index f48040c27589..908363f9c7fa 100644
--- a/examples/official-storybook/main.js
+++ b/examples/official-storybook/main.js
@@ -22,6 +22,7 @@ module.exports = {
'@storybook/addon-viewport',
'@storybook/addon-graphql',
'@storybook/addon-contexts',
+ '@storybook/addon-toolbars',
'@storybook/addon-queryparams',
],
webpackFinal: async (config, { configType }) => ({
diff --git a/examples/official-storybook/package.json b/examples/official-storybook/package.json
index ab54f5f3a421..bd2d628abf33 100644
--- a/examples/official-storybook/package.json
+++ b/examples/official-storybook/package.json
@@ -32,6 +32,7 @@
"@storybook/addon-storyshots": "6.0.0-alpha.26",
"@storybook/addon-storyshots-puppeteer": "6.0.0-alpha.26",
"@storybook/addon-storysource": "6.0.0-alpha.26",
+ "@storybook/addon-toolbars": "6.0.0-alpha.26",
"@storybook/addon-viewport": "6.0.0-alpha.26",
"@storybook/addons": "6.0.0-alpha.26",
"@storybook/components": "6.0.0-alpha.26",
diff --git a/examples/official-storybook/preview.js b/examples/official-storybook/preview.js
index abd907d304c3..e4abcf92b22d 100644
--- a/examples/official-storybook/preview.js
+++ b/examples/official-storybook/preview.js
@@ -25,6 +25,18 @@ addHeadWarning('dotenv-file-not-loaded', 'Dotenv file not loaded');
addDecorator(withCssResources);
+const themeDecorator = (storyFn, { globalArgs: { theme } }) => {
+ const selectedTheme = theme === 'dark' ? themes.dark : themes.light;
+ return (
+
+
+ {storyFn()}
+
+ );
+};
+
+addDecorator(themeDecorator);
+
addDecorator(storyFn => (
@@ -69,4 +81,31 @@ export const globalArgs = {
export const globalArgTypes = {
foo: { defaultValue: 'fooDefaultValue' },
bar: { defaultValue: 'barDefaultValue' },
+ theme: {
+ name: 'Theme',
+ description: 'Global theme for components',
+ defaultValue: null,
+ toolbar: {
+ icon: 'circlehollow',
+ // items: ['light', 'dark'],
+ items: [
+ { value: 'light', icon: 'circlehollow', title: 'light' },
+ { value: 'dark', icon: 'circle', title: 'dark' },
+ ],
+ },
+ },
+ locale: {
+ name: 'Locale',
+ description: 'Internationalization locale',
+ defaultValue: 'en',
+ toolbar: {
+ icon: 'globe',
+ items: [
+ { value: 'en', right: '🇺🇸', title: 'English' },
+ { value: 'es', right: '🇪🇸', title: 'Español' },
+ { value: 'zh', right: '🇨🇳', title: 'ä¸æ–‡' },
+ { value: 'kr', right: '🇰🇷', title: 'í•œêµì–´' },
+ ],
+ },
+ },
};
diff --git a/examples/official-storybook/stories/addon-toolbars.stories.js b/examples/official-storybook/stories/addon-toolbars.stories.js
new file mode 100644
index 000000000000..a09f5434ca9a
--- /dev/null
+++ b/examples/official-storybook/stories/addon-toolbars.stories.js
@@ -0,0 +1,38 @@
+import React from 'react';
+import { styled } from '@storybook/theming';
+
+export default {
+ title: 'Addons/Toolbars',
+};
+
+const getCaptionForLocale = locale => {
+ switch (locale) {
+ case 'es':
+ return 'Hola!';
+ case 'fr':
+ return 'Bonjour!';
+ case 'zh':
+ return 'ä½ å¥½!';
+ case 'kr':
+ return '안녕하세요!';
+ case 'en':
+ default:
+ return 'Hello';
+ }
+};
+
+const Themed = styled.div(({ theme }) => ({
+ color: theme.color.defaultText,
+ background: theme.background.content,
+}));
+
+// eslint-disable-next-line react/prop-types
+export const Locale = ({ globalArgs: { locale } }) => {
+ return (
+
+ Your locale is '{locale}', so I say:
+
+ {getCaptionForLocale(locale)}
+
+ );
+};
diff --git a/lib/api/src/index.tsx b/lib/api/src/index.tsx
index 72b4fa773740..9e599bdfd2d2 100644
--- a/lib/api/src/index.tsx
+++ b/lib/api/src/index.tsx
@@ -119,6 +119,17 @@ export interface Args {
[key: string]: any;
}
+export interface ArgType {
+ name?: string;
+ description?: string;
+ defaultValue?: any;
+ [key: string]: any;
+}
+
+export interface ArgTypes {
+ [key: string]: ArgType;
+}
+
type StatePartial = Partial;
export type ManagerProviderProps = Children & RouterData & ProviderData & DocsModeData;
@@ -421,3 +432,11 @@ export function useGlobalArgs(): [Args, (newGlobalArgs: Args) => void] {
return [globalArgs, updateGlobalArgs];
}
+
+export function useArgTypes(): ArgTypes {
+ return useParameter('argTypes', {});
+}
+
+export function useGlobalArgTypes(): ArgTypes {
+ return useParameter('globalArgTypes', {});
+}
diff --git a/lib/components/src/index.ts b/lib/components/src/index.ts
index 96b5c0307fa9..42bd811e7e02 100644
--- a/lib/components/src/index.ts
+++ b/lib/components/src/index.ts
@@ -34,7 +34,7 @@ export { Bar, FlexBar } from './bar/bar';
export { AddonPanel } from './addon-panel/addon-panel';
// Graphics
-export { Icons } from './icon/icon';
+export { Icons, IconsProps } from './icon/icon';
export { StorybookLogo } from './brand/StorybookLogo';
export { StorybookIcon } from './brand/StorybookIcon';