diff --git a/code/ui/components/src/components/placeholder/placeholder.stories.tsx b/code/ui/components/src/components/placeholder/placeholder.stories.tsx index 4e7c53172449..a4c6ea5c32b0 100644 --- a/code/ui/components/src/components/placeholder/placeholder.stories.tsx +++ b/code/ui/components/src/components/placeholder/placeholder.stories.tsx @@ -1,16 +1,20 @@ import React, { Fragment } from 'react'; +import type { Meta, StoryFn } from '@storybook/react'; import { Placeholder } from './placeholder'; import { Link } from '../typography/link/link'; export default { component: Placeholder, -}; +} as Meta; -export const SingleChild = () => ( +type Story = StoryFn; + +export const SingleChild: Story = () => ( This is a placeholder with single child, it's bolded ); -export const TwoChildren = () => ( + +export const TwoChildren: Story = () => ( This has two children, the first bold diff --git a/code/ui/components/src/components/placeholder/placeholder.tsx b/code/ui/components/src/components/placeholder/placeholder.tsx index ad2367a46e01..9cfff19fb449 100644 --- a/code/ui/components/src/components/placeholder/placeholder.tsx +++ b/code/ui/components/src/components/placeholder/placeholder.tsx @@ -9,7 +9,11 @@ const Desc = styled.div(); const Message = styled.div(({ theme }) => ({ padding: 30, - textAlign: 'center', + height: '100%', + justifyContent: 'center', + alignItems: 'center', + display: 'flex', + flexDirection: 'column', color: theme.color.defaultText, fontSize: theme.typography.size.s2 - 1, })); diff --git a/code/ui/components/src/components/tabs/tabs.stories.tsx b/code/ui/components/src/components/tabs/tabs.stories.tsx index 46a332a87f1f..efae04e56eda 100644 --- a/code/ui/components/src/components/tabs/tabs.stories.tsx +++ b/code/ui/components/src/components/tabs/tabs.stories.tsx @@ -1,9 +1,9 @@ import { expect } from '@storybook/test'; -import React, { Fragment } from 'react'; +import React from 'react'; import { action } from '@storybook/addon-actions'; import type { Meta, StoryObj } from '@storybook/react'; import { within, fireEvent, waitFor, screen, userEvent, findByText } from '@storybook/test'; -import { CPUIcon, MemoryIcon } from '@storybook/icons'; +import { BottomBarIcon, CloseIcon } from '@storybook/icons'; import { Tabs, TabsState, TabWrapper } from './tabs'; import type { ChildrenList } from './tabs.helpers'; import { IconButton } from '../IconButton/IconButton'; @@ -260,7 +260,27 @@ export const StatelessBordered = { ), } satisfies Story; +const AddonTools = () => ( +
+ + + + + + +
+); + export const StatelessWithTools = { + args: { + tools: , + }, render: (args) => ( - - - - - - -
- } {...args} > {content} ), -} satisfies Story; +} satisfies StoryObj; export const StatelessAbsolute = { parameters: { @@ -303,7 +313,7 @@ export const StatelessAbsolute = { {content} ), -} satisfies Story; +} satisfies StoryObj; export const StatelessAbsoluteBordered = { parameters: { @@ -323,9 +333,13 @@ export const StatelessAbsoluteBordered = { {content} ), -} satisfies Story; +} satisfies StoryObj; -export const StatelessEmpty = { +export const StatelessEmptyWithTools = { + args: { + ...StatelessWithTools.args, + showToolsWhenEmpty: true, + }, parameters: { layout: 'fullscreen', }, @@ -340,4 +354,25 @@ export const StatelessEmpty = { {...args} /> ), -} satisfies Story; +} satisfies StoryObj; + +export const StatelessWithCustomEmpty = { + args: { + ...StatelessEmptyWithTools.args, + customEmptyContent:
I am custom!
, + }, + parameters: { + layout: 'fullscreen', + }, + render: (args) => ( + + ), +} satisfies StoryObj; diff --git a/code/ui/components/src/components/tabs/tabs.tsx b/code/ui/components/src/components/tabs/tabs.tsx index 0e3484eab4ba..afa542e2ee99 100644 --- a/code/ui/components/src/components/tabs/tabs.tsx +++ b/code/ui/components/src/components/tabs/tabs.tsx @@ -119,6 +119,8 @@ export interface TabsProps { }>[]; id?: string; tools?: ReactNode; + showToolsWhenEmpty?: boolean; + customEmptyContent?: ReactNode; selected?: string; actions?: { onSelect: (id: string) => void; @@ -140,6 +142,8 @@ export const Tabs: FC = memo( backgroundColor, id: htmlId, menuName, + customEmptyContent, + showToolsWhenEmpty, }) => { const idList = childrenToList(children) .map((i) => i.id) @@ -157,7 +161,17 @@ export const Tabs: FC = memo( const { visibleList, tabBarRef, tabRefs, AddonTab } = useList(list); - return list.length ? ( + const EmptyState = customEmptyContent ?? ( + + Nothing found + + ); + + if (!showToolsWhenEmpty && list.length === 0) { + return EmptyState; + } + + return ( @@ -190,15 +204,13 @@ export const Tabs: FC = memo( {tools} - {list.map(({ id, active, render }) => { - return React.createElement(render, { key: id, active }, null); - })} + {list.length + ? list.map(({ id, active, render }) => { + return React.createElement(render, { key: id, active }, null); + }) + : EmptyState} - ) : ( - - Nothing found - ); } ); diff --git a/code/ui/manager/src/components/panel/Panel.tsx b/code/ui/manager/src/components/panel/Panel.tsx index e37339445c82..56cc32e7a161 100644 --- a/code/ui/manager/src/components/panel/Panel.tsx +++ b/code/ui/manager/src/components/panel/Panel.tsx @@ -1,10 +1,10 @@ -import React, { Component } from 'react'; -import { Tabs, IconButton } from '@storybook/components'; +import React, { Component, Fragment } from 'react'; +import { Tabs, IconButton, Placeholder, P, Link } from '@storybook/components'; import type { State } from '@storybook/manager-api'; import { shortcutToHumanString } from '@storybook/manager-api'; import type { Addon_BaseType } from '@storybook/types'; import { styled } from '@storybook/theming'; -import { BottomBarIcon, CloseIcon, SidebarAltIcon } from '@storybook/icons'; +import { BottomBarIcon, CloseIcon, DocumentIcon, SidebarAltIcon } from '@storybook/icons'; import { useLayout } from '../layout/LayoutProvider'; export interface SafeTabProps { @@ -60,6 +60,23 @@ export const AddonPanel = React.memo<{ {...(selectedPanel ? { selected: selectedPanel } : {})} menuName="Addons" actions={actions} + showToolsWhenEmpty + customEmptyContent={ + + +

Storybook add-ons

+
+ +

+ Integrate your tools with Storybook to connect workflows and unlock advanced + features. +

+ + Explore integrations catalog + +
+
+ } tools={ {isDesktop ? ( @@ -108,3 +125,9 @@ const Actions = styled.div({ alignItems: 'center', gap: 6, }); + +const EmptyStateDescription = styled.div({ + display: 'flex', + alignItems: 'center', + flexDirection: 'column', +});