+
),
diff --git a/code/core/src/manager/components/notifications/NotificationItem.tsx b/code/core/src/manager/components/notifications/NotificationItem.tsx
index 75ae36129b76..159fa0da137f 100644
--- a/code/core/src/manager/components/notifications/NotificationItem.tsx
+++ b/code/core/src/manager/components/notifications/NotificationItem.tsx
@@ -11,6 +11,8 @@ import { type State } from '@storybook/core/manager-api';
import { transparentize } from 'polished';
+import { MEDIA_DESKTOP_BREAKPOINT } from '../../constants';
+
const slideIn = keyframes({
'0%': {
opacity: 0,
@@ -35,17 +37,21 @@ const Notification = styled.div<{ duration?: number }>(
({ theme }) => ({
position: 'relative',
display: 'flex',
- padding: 15,
- width: 280,
- borderRadius: 4,
+ border: `1px solid ${theme.appBorderColor}`,
+ padding: '12px 6px 12px 12px',
+ borderRadius: theme.appBorderRadius + 1,
alignItems: 'center',
animation: `${slideIn} 500ms`,
background: theme.base === 'light' ? 'hsla(203, 50%, 20%, .97)' : 'hsla(203, 30%, 95%, .97)',
- boxShadow: `0 2px 5px 0 rgba(0,0,0,0.05), 0 5px 15px 0 rgba(0,0,0,0.1)`,
+ boxShadow: `0 2px 5px 0 rgba(0, 0, 0, 0.05), 0 5px 15px 0 rgba(0, 0, 0, 0.1)`,
color: theme.color.inverseText,
textDecoration: 'none',
overflow: 'hidden',
+
+ [MEDIA_DESKTOP_BREAKPOINT]: {
+ boxShadow: `0 1px 2px 0 rgba(0, 0, 0, 0.05), 0px -5px 20px 10px ${theme.background.app}`,
+ },
}),
({ duration, theme }) =>
duration && {
@@ -107,9 +113,8 @@ const NotificationTextWrapper = styled.div(({ theme }) => ({
const Headline = styled.div<{ hasIcon: boolean }>(({ theme, hasIcon }) => ({
height: '100%',
- width: hasIcon ? 205 : 230,
alignItems: 'center',
- whiteSpace: 'nowrap',
+ whiteSpace: 'balance',
overflow: 'hidden',
textOverflow: 'ellipsis',
fontSize: theme.typography.size.s1,
@@ -122,6 +127,7 @@ const SubHeadline = styled.div(({ theme }) => ({
fontSize: theme.typography.size.s1 - 1,
lineHeight: '14px',
marginTop: 2,
+ whiteSpace: 'balance',
}));
const ItemContent: FC
> = ({
@@ -154,6 +160,7 @@ const ItemContent: FC> = ({
};
const DismissButtonWrapper = styled(IconButton)(({ theme }) => ({
+ width: 28,
alignSelf: 'center',
marginTop: 0,
color: theme.base === 'light' ? 'rgba(255,255,255,0.7)' : ' #999999',
@@ -181,9 +188,11 @@ export const NotificationItemSpacer = styled.div({
const NotificationItem: FC<{
notification: State['notifications'][0];
onDismissNotification: (id: string) => void;
+ zIndex?: number;
}> = ({
notification: { content, duration, link, onClear, onClick, id, icon },
onDismissNotification,
+ zIndex,
}) => {
const onTimeout = useCallback(() => {
onDismissNotification(id);
@@ -191,7 +200,7 @@ const NotificationItem: FC<{
if (onClear) {
onClear({ dismissed: false, timeout: true });
}
- }, [onDismissNotification, onClear]);
+ }, [id, onDismissNotification, onClear]);
const timer = useRef | null>(null);
useEffect(() => {
@@ -211,11 +220,11 @@ const NotificationItem: FC<{
if (onClear) {
onClear({ dismissed: true, timeout: false });
}
- }, [onDismissNotification, onClear]);
+ }, [id, onDismissNotification, onClear]);
if (link) {
return (
-
+
@@ -224,7 +233,11 @@ const NotificationItem: FC<{
if (onClick) {
return (
- onClick({ onDismiss })}>
+ onClick({ onDismiss })}
+ style={{ zIndex }}
+ >
@@ -232,7 +245,7 @@ const NotificationItem: FC<{
}
return (
-
+
diff --git a/code/core/src/manager/components/notifications/NotificationList.tsx b/code/core/src/manager/components/notifications/NotificationList.tsx
index a0c8fabd13c6..0e4789a28bf5 100644
--- a/code/core/src/manager/components/notifications/NotificationList.tsx
+++ b/code/core/src/manager/components/notifications/NotificationList.tsx
@@ -2,11 +2,10 @@ import type { FC } from 'react';
import React from 'react';
import { styled } from '@storybook/core/theming';
-import type { CSSObject } from '@storybook/core/theming';
import type { State } from '@storybook/core/manager-api';
-import { MEDIA_DESKTOP_BREAKPOINT } from '../../constants';
+import { useLayout } from '../layout/LayoutProvider';
import NotificationItem from './NotificationItem';
interface NotificationListProps {
@@ -18,35 +17,36 @@ export const NotificationList: FC = ({
notifications,
clearNotification,
}) => {
+ const { isMobile } = useLayout();
return (
-
+
{notifications &&
- notifications.map((notification) => (
+ notifications.map((notification, index) => (
clearNotification(id)}
notification={notification}
+ zIndex={notifications.length - index}
/>
))}
);
};
-const List = styled.div<{ placement?: CSSObject }>({
- zIndex: 200,
- position: 'fixed',
- left: 20,
- bottom: 60,
-
- [MEDIA_DESKTOP_BREAKPOINT]: {
- bottom: 20,
- },
-
- '> * + *': {
- marginTop: 10,
- },
-
- '&:empty': {
- display: 'none',
+const List = styled.div<{ isMobile?: boolean }>(
+ {
+ zIndex: 200,
+ '> * + *': {
+ marginTop: 12,
+ },
+ '&:empty': {
+ display: 'none',
+ },
},
-});
+ ({ isMobile }) =>
+ isMobile && {
+ position: 'fixed',
+ bottom: 40,
+ margin: 20,
+ }
+);
diff --git a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx
index 30be9d3e9bf5..410403971002 100644
--- a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx
+++ b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx
@@ -42,6 +42,7 @@ const managerContext: any = {
'api::getShortcutKeys'
),
getChannel: fn().mockName('api::getChannel'),
+ getElements: fn(() => ({})),
selectStory: fn().mockName('api::selectStory'),
experimental_setFilter: fn().mockName('api::experimental_setFilter'),
},
diff --git a/code/core/src/manager/components/sidebar/Sidebar.tsx b/code/core/src/manager/components/sidebar/Sidebar.tsx
index f658f00e4bf8..c752e76586d5 100644
--- a/code/core/src/manager/components/sidebar/Sidebar.tsx
+++ b/code/core/src/manager/components/sidebar/Sidebar.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useMemo } from 'react';
import { Button, ScrollArea, Spaced } from '@storybook/core/components';
import { styled } from '@storybook/core/theming';
@@ -6,13 +6,12 @@ import type { API_LoadedRefData, Addon_SidebarTopType } from '@storybook/core/ty
import {
TESTING_MODULE_RUN_ALL_REQUEST,
- TESTING_MODULE_WATCH_MODE_REQUEST,
type TestingModuleRunAllRequestPayload,
- type TestingModuleWatchModeRequestPayload,
} from '@storybook/core/core-events';
import { type State, useStorybookApi } from '@storybook/core/manager-api';
import { MEDIA_DESKTOP_BREAKPOINT } from '../../constants';
+import { useLayout } from '../layout/LayoutProvider';
import { Explorer } from './Explorer';
import type { HeadingProps } from './Heading';
import { Heading } from './Heading';
@@ -51,19 +50,6 @@ const Top = styled(Spaced)({
flex: 1,
});
-const Bottom = styled.div(({ theme }) => ({
- borderTop: `1px solid ${theme.appBorderColor}`,
- padding: theme.layoutMargin / 2,
- display: 'flex',
- flexWrap: 'wrap',
- gap: theme.layoutMargin / 2,
- backgroundColor: theme.barBg,
-
- '&:empty': {
- display: 'none',
- },
-}));
-
const Swap = React.memo(function Swap({
children,
condition,
@@ -140,15 +126,7 @@ export const Sidebar = React.memo(function Sidebar({
const dataset = useCombination(index, indexError, previewInitialized, status, refs);
const isLoading = !index && !indexError;
const lastViewedProps = useLastViewed(selected);
- const api = useStorybookApi();
- const [watchMode, setWatchMode] = useState(false);
-
- useEffect(() => {
- api.emit(TESTING_MODULE_WATCH_MODE_REQUEST, {
- providerId: TEST_PROVIDER_ID,
- watchMode,
- } as TestingModuleWatchModeRequestPayload);
- }, [api, watchMode]);
+ const { isMobile } = useLayout();
return (
@@ -200,25 +178,8 @@ export const Sidebar = React.memo(function Sidebar({
)}
+ {isMobile || isLoading ? null : }
- {isLoading ? null : (
-
-
-
-
-
- )}
);
});
diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx
index 498750fd82e0..466222b2723c 100644
--- a/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx
+++ b/code/core/src/manager/components/sidebar/SidebarBottom.stories.tsx
@@ -1,3 +1,7 @@
+import React from 'react';
+
+import { Addon_TypesEnum } from '@storybook/core/types';
+import { ContrastIcon, PointerHandIcon } from '@storybook/icons';
import { fn } from '@storybook/test';
import { SidebarBottomBase } from './SidebarBottom';
@@ -6,8 +10,28 @@ export default {
component: SidebarBottomBase,
args: {
api: {
- experimental_setFilter: fn(),
+ clearNotification: fn(),
emit: fn(),
+ experimental_setFilter: fn(),
+ getElements: fn(() => ({
+ 'component-tests': {
+ type: Addon_TypesEnum.experimental_TEST_PROVIDER,
+ id: 'component-tests',
+ title: 'Component tests',
+ description: () => 'Ran 2 seconds ago',
+ icon: ,
+ runnable: true,
+ watchable: true,
+ },
+ 'visual-tests': {
+ type: Addon_TypesEnum.experimental_TEST_PROVIDER,
+ id: 'visual-tests',
+ title: 'Visual tests',
+ description: () => 'Not run',
+ icon: ,
+ runnable: true,
+ },
+ })),
},
},
};
diff --git a/code/core/src/manager/components/sidebar/SidebarBottom.tsx b/code/core/src/manager/components/sidebar/SidebarBottom.tsx
index 290e44ac8f6a..fb62aead6d4d 100644
--- a/code/core/src/manager/components/sidebar/SidebarBottom.tsx
+++ b/code/core/src/manager/components/sidebar/SidebarBottom.tsx
@@ -1,11 +1,17 @@
-import React, { useCallback, useEffect } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { styled } from '@storybook/core/theming';
-import type { API_FilterFunction, API_StatusUpdate, API_StatusValue } from '@storybook/types';
+import {
+ type API_FilterFunction,
+ type API_StatusUpdate,
+ type API_StatusValue,
+ Addon_TypesEnum,
+} from '@storybook/core/types';
import {
+ TESTING_MODULE_RUN_ALL_REQUEST,
TESTING_MODULE_RUN_PROGRESS_RESPONSE,
- type TestingModuleRunProgressPayload,
+ TESTING_MODULE_WATCH_MODE_REQUEST,
type TestingModuleRunResponsePayload,
} from '@storybook/core/core-events';
import {
@@ -14,9 +20,11 @@ import {
useStorybookApi,
useStorybookState,
} from '@storybook/core/manager-api';
-import { useChannel } from '@storybook/core/preview-api';
-import { FilterToggle } from './FilterToggle';
+import { throttle } from 'es-toolkit';
+
+import { NotificationList } from '../notifications/NotificationList';
+import { TestingModule } from './TestingModule';
const filterNone: API_FilterFunction = () => true;
const filterWarn: API_FilterFunction = ({ status = {} }) =>
@@ -26,30 +34,47 @@ const filterError: API_FilterFunction = ({ status = {} }) =>
const filterBoth: API_FilterFunction = ({ status = {} }) =>
Object.values(status).some((value) => value?.status === 'warn' || value?.status === 'error');
-const getFilter = (showWarnings = false, showErrors = false) => {
- if (showWarnings && showErrors) {
+const getFilter = (warningsActive = false, errorsActive = false) => {
+ if (warningsActive && errorsActive) {
return filterBoth;
}
- if (showWarnings) {
+ if (warningsActive) {
return filterWarn;
}
- if (showErrors) {
+ if (errorsActive) {
return filterError;
}
return filterNone;
};
const Wrapper = styled.div({
- display: 'flex',
- gap: 5,
+ transition: 'height 250ms',
});
-interface SidebarBottomProps {
- api: API;
- status: State['status'];
-}
+const Content = styled.div(({ theme }) => ({
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ padding: 12,
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 12,
+ color: theme.color.defaultText,
+ fontSize: theme.typography.size.s1,
+
+ '&:empty': {
+ display: 'none',
+ },
+
+ // Integrators can use these to style their custom additions
+ '--sb-sidebar-bottom-card-background': theme.background.content,
+ '--sb-sidebar-bottom-card-border': `1px solid ${theme.appBorderColor}`,
+ '--sb-sidebar-bottom-card-border-radius': `${theme.appBorderRadius + 1}px`,
+ '--sb-sidebar-bottom-card-box-shadow': `0 1px 2px 0 rgba(0, 0, 0, 0.05), 0px -5px 20px 10px ${theme.background.app}`,
+}));
const statusMap: Record = {
failed: 'error',
@@ -75,9 +100,30 @@ function processTestReport(payload: TestingModuleRunResponsePayload) {
return result;
}
-export const SidebarBottomBase = ({ api, status = {} }: SidebarBottomProps) => {
- const [showWarnings, setShowWarnings] = React.useState(false);
- const [showErrors, setShowErrors] = React.useState(false);
+interface SidebarBottomProps {
+ api: API;
+ notifications: State['notifications'];
+ status: State['status'];
+}
+
+export const SidebarBottomBase = ({ api, notifications = [], status = {} }: SidebarBottomProps) => {
+ const [warningsActive, setWarningsActive] = useState(false);
+ const [errorsActive, setErrorsActive] = useState(false);
+ const [contentHeight, setContentHeight] = useState(0);
+
+ const resizeObserverCallback = useMemo(
+ () => throttle((element) => setContentHeight(element.clientHeight || 0), 250),
+ []
+ );
+
+ useEffect(() => {
+ const wrapper = document.getElementById('sidebar-bottom');
+ if (wrapper) {
+ const resizeObserver = new ResizeObserver(() => resizeObserverCallback(wrapper));
+ resizeObserver.observe(wrapper);
+ return () => resizeObserver.disconnect();
+ }
+ }, [resizeObserverCallback]);
const warnings = Object.values(status).filter((statusByAddonId) =>
Object.values(statusByAddonId).some((value) => value?.status === 'warn')
@@ -88,47 +134,55 @@ export const SidebarBottomBase = ({ api, status = {} }: SidebarBottomProps) => {
const hasWarnings = warnings.length > 0;
const hasErrors = errors.length > 0;
- const toggleWarnings = useCallback(() => setShowWarnings((shown) => !shown), []);
- const toggleErrors = useCallback(() => setShowErrors((shown) => !shown), []);
+ const onRunTests = useCallback(
+ (providerId?: string) => {
+ api.emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId });
+ },
+ [api]
+ );
+ const onSetWatchMode = useCallback(
+ (providerId: string, watchMode: boolean) => {
+ api.emit(TESTING_MODULE_WATCH_MODE_REQUEST, { providerId, watchMode });
+ },
+ [api]
+ );
useEffect(() => {
- const filter = getFilter(hasWarnings && showWarnings, hasErrors && showErrors);
+ const filter = getFilter(hasWarnings && warningsActive, hasErrors && errorsActive);
api.experimental_setFilter('sidebar-bottom-filter', filter);
- }, [api, hasWarnings, hasErrors, showWarnings, showErrors]);
+ }, [api, hasWarnings, hasErrors, warningsActive, errorsActive]);
- if (!hasWarnings && !hasErrors) {
+ const testProviders = Object.values(api.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER));
+
+ if (!hasWarnings && !hasErrors && !testProviders.length) {
return null;
}
return (
-
);
};
export const SidebarBottom = () => {
const api = useStorybookApi();
- const { status } = useStorybookState();
+ const { notifications, status } = useStorybookState();
useEffect(() => {
api.getChannel()?.on(TESTING_MODULE_RUN_PROGRESS_RESPONSE, (data) => {
@@ -142,5 +196,5 @@ export const SidebarBottom = () => {
});
}, [api]);
- return ;
+ return ;
};
diff --git a/code/core/src/manager/components/sidebar/TestingModule.stories.tsx b/code/core/src/manager/components/sidebar/TestingModule.stories.tsx
new file mode 100644
index 000000000000..d3a151f003cf
--- /dev/null
+++ b/code/core/src/manager/components/sidebar/TestingModule.stories.tsx
@@ -0,0 +1,115 @@
+import React from 'react';
+
+import { Addon_TypesEnum } from '@storybook/core/types';
+import { ContrastIcon, MarkupIcon, PointerHandIcon } from '@storybook/icons';
+import type { Meta, StoryObj } from '@storybook/react';
+import { fn, userEvent } from '@storybook/test';
+
+import { TestingModule } from './TestingModule';
+
+const testProviders = [
+ {
+ type: Addon_TypesEnum.experimental_TEST_PROVIDER,
+ id: 'component-tests',
+ title: 'Component tests',
+ description: () => 'Ran 2 seconds ago',
+ icon: ,
+ runnable: true,
+ watchable: true,
+ },
+ {
+ type: Addon_TypesEnum.experimental_TEST_PROVIDER,
+ id: 'visual-tests',
+ title: 'Visual tests',
+ description: () => 'Not run',
+ icon: ,
+ runnable: true,
+ },
+ {
+ type: Addon_TypesEnum.experimental_TEST_PROVIDER,
+ id: 'linting',
+ title: 'Linting',
+ description: () => 'Watching for changes',
+ icon: ,
+ watching: true,
+ },
+];
+
+const meta = {
+ component: TestingModule,
+ args: {
+ testProviders,
+ errorCount: 0,
+ errorsActive: false,
+ setErrorsActive: fn(),
+ warningCount: 0,
+ warningsActive: false,
+ setWarningsActive: fn(),
+ onRunTests: fn(),
+ onSetWatchMode: fn(),
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Default: Story = {};
+
+export const Collapsed: Story = {
+ play: async ({ canvas }) => {
+ const button = await canvas.findByRole('button', { name: /Collapse/ });
+ await userEvent.click(button);
+ },
+};
+
+export const Statuses: Story = {
+ args: {
+ errorCount: 14,
+ warningCount: 42,
+ },
+};
+
+export const ErrorsActive: Story = {
+ args: {
+ ...Statuses.args,
+ errorsActive: true,
+ },
+};
+
+export const WarningsActive: Story = {
+ args: {
+ ...Statuses.args,
+ warningsActive: true,
+ },
+};
+
+export const BothActive: Story = {
+ args: {
+ ...Statuses.args,
+ errorsActive: true,
+ warningsActive: true,
+ },
+};
+
+export const CollapsedStatuses: Story = {
+ args: Statuses.args,
+ play: Collapsed.play,
+};
+
+export const Running: Story = {
+ args: {
+ testProviders: testProviders.map((tp) => ({ ...tp, running: true })),
+ },
+};
+
+export const CollapsedRunning: Story = {
+ args: Running.args,
+ play: Collapsed.play,
+};
+
+export const Watching: Story = {
+ args: {
+ testProviders: testProviders.map((tp) => ({ ...tp, watching: true })),
+ },
+};
diff --git a/code/core/src/manager/components/sidebar/TestingModule.tsx b/code/core/src/manager/components/sidebar/TestingModule.tsx
new file mode 100644
index 000000000000..48964c6b5101
--- /dev/null
+++ b/code/core/src/manager/components/sidebar/TestingModule.tsx
@@ -0,0 +1,320 @@
+import React, { type SyntheticEvent, useCallback, useEffect, useRef, useState } from 'react';
+
+import { Button } from '@storybook/core/components';
+import { keyframes, styled } from '@storybook/core/theming';
+import {
+ ChevronSmallUpIcon,
+ EyeIcon,
+ PlayAllHollowIcon,
+ PlayHollowIcon,
+ StopAltHollowIcon,
+} from '@storybook/icons';
+import type { Addon_TestProviderType } from '@storybook/types';
+
+const DEFAULT_HEIGHT = 500;
+
+const spin = keyframes({
+ from: { transform: 'rotate(0deg)' },
+ to: { transform: 'rotate(360deg)' },
+});
+
+const Outline = styled.div<{ active: boolean }>(({ theme, active }) => ({
+ position: 'relative',
+ lineHeight: '20px',
+ width: '100%',
+ padding: 1,
+ overflow: 'hidden',
+ background: 'var(--sb-sidebar-bottom-card-background)',
+ border: 'var(--sb-sidebar-bottom-card-border)',
+ borderRadius: 'var(--sb-sidebar-bottom-card-border-radius)' as any,
+ boxShadow: 'var(--sb-sidebar-bottom-card-box-shadow)',
+ transitionProperty: 'color, background-color, border-color, text-decoration-color, fill, stroke',
+ transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
+ transitionDuration: '0.15s',
+
+ '&:after': {
+ content: '""',
+ display: active ? 'block' : 'none',
+ position: 'absolute',
+ left: '50%',
+ top: '50%',
+ marginLeft: 'calc(max(100vw, 100vh) * -0.5)',
+ marginTop: 'calc(max(100vw, 100vh) * -0.5)',
+ height: 'max(100vw, 100vh)',
+ width: 'max(100vw, 100vh)',
+ animation: `${spin} 3s linear infinite`,
+ background:
+ 'conic-gradient(rgba(255, 71, 133, 0.2) 0deg, rgb(255, 71, 133) 0deg, transparent 160deg)',
+ opacity: 1,
+ willChange: 'auto',
+ },
+}));
+
+const Card = styled.div(({ theme }) => ({
+ position: 'relative',
+ zIndex: 1,
+ borderRadius: theme.appBorderRadius,
+ backgroundColor: theme.background.content,
+
+ '&:hover #testing-module-collapse-toggle': {
+ opacity: 1,
+ },
+}));
+
+const Collapsible = styled.div(({ theme }) => ({
+ overflow: 'hidden',
+ transition: 'max-height 250ms',
+ willChange: 'auto',
+ boxShadow: `inset 0 -1px 0 ${theme.appBorderColor}`,
+}));
+
+const Content = styled.div({
+ padding: '12px 6px',
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '12px',
+});
+
+const Bar = styled.div<{ onClick?: (e: SyntheticEvent) => void }>(({ onClick }) => ({
+ display: 'flex',
+ width: '100%',
+ cursor: onClick ? 'pointer' : 'default',
+ userSelect: 'none',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ overflow: 'hidden',
+ padding: '6px',
+}));
+
+const Filters = styled.div({
+ display: 'flex',
+ flexBasis: '100%',
+ justifyContent: 'flex-end',
+ gap: 6,
+});
+
+const CollapseToggle = styled(Button)({
+ opacity: 0,
+ transition: 'opacity 250ms',
+ willChange: 'auto',
+ '&:focus, &:hover': {
+ opacity: 1,
+ },
+});
+
+const StatusButton = styled(Button)<{ status: 'negative' | 'warning' }>(
+ { minWidth: 28 },
+ ({ active, status, theme }) =>
+ !active &&
+ (theme.base === 'light'
+ ? {
+ background: {
+ negative: theme.background.negative,
+ warning: theme.background.warning,
+ }[status],
+ color: {
+ negative: theme.color.negativeText,
+ warning: theme.color.warningText,
+ }[status],
+ }
+ : {
+ background: {
+ negative: `${theme.color.negative}22`,
+ warning: `${theme.color.warning}22`,
+ }[status],
+ color: {
+ negative: theme.color.negative,
+ warning: theme.color.warning,
+ }[status],
+ })
+);
+
+const TestProvider = styled.div({
+ display: 'flex',
+ justifyContent: 'space-between',
+ gap: 6,
+});
+
+const Info = styled.div({
+ display: 'flex',
+ gap: 6,
+});
+
+const Actions = styled.div({
+ display: 'flex',
+ gap: 6,
+});
+
+const Details = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+});
+
+const Title = styled.div(({ theme }) => ({
+ fontSize: theme.typography.size.s2,
+}));
+
+const Description = styled.div(({ theme }) => ({
+ fontSize: theme.typography.size.s1,
+ color: theme.barTextColor,
+}));
+
+const Icon = styled.div(({ theme }) => ({
+ color: theme.barTextColor,
+ padding: '2px 6px',
+}));
+
+interface TestingModuleProps {
+ testProviders: (Addon_TestProviderType & { running?: boolean; watching?: boolean })[];
+ errorCount: number;
+ errorsActive: boolean;
+ setErrorsActive: (active: boolean) => void;
+ warningCount: number;
+ warningsActive: boolean;
+ setWarningsActive: (active: boolean) => void;
+ onRunTests: (providerId?: string) => void;
+ onSetWatchMode: (providerId: string, watchMode: boolean) => void;
+}
+
+export const TestingModule = ({
+ testProviders,
+ errorCount,
+ errorsActive,
+ setErrorsActive,
+ warningCount,
+ warningsActive,
+ setWarningsActive,
+ onRunTests,
+ onSetWatchMode,
+}: TestingModuleProps) => {
+ const contentRef = useRef(null);
+ const [collapsed, setCollapsed] = useState(false);
+ const [maxHeight, setMaxHeight] = useState(DEFAULT_HEIGHT);
+
+ useEffect(() => {
+ setMaxHeight(contentRef.current?.offsetHeight || DEFAULT_HEIGHT);
+ }, []);
+
+ const toggleCollapsed = () => {
+ setMaxHeight(contentRef.current?.offsetHeight || DEFAULT_HEIGHT);
+ setCollapsed(!collapsed);
+ };
+
+ const active = testProviders.some((tp) => tp.running);
+ const testing = testProviders.length > 0;
+
+ return (
+
+
+
+
+ {testProviders.map(
+ ({ id, icon, title, description, runnable, running, watchable, watching }) => (
+
+
+ {icon}
+
+ {title}
+ {description({})}
+
+
+
+ {watchable && (
+
+ )}
+ {runnable && (
+
+ )}
+
+
+ )
+ )}
+
+
+
+
+ {testing && (
+
+ )}
+
+ {testing && (
+
+
+
+ )}
+
+ {errorCount > 0 && (
+ {
+ e.stopPropagation();
+ setErrorsActive(!errorsActive);
+ }}
+ aria-label="Show errors"
+ >
+ {errorCount < 100 ? errorCount : '99+'}
+
+ )}
+ {warningCount > 0 && (
+ {
+ e.stopPropagation();
+ setWarningsActive(!warningsActive);
+ }}
+ aria-label="Show warnings"
+ >
+ {warningCount < 100 ? warningCount : '99+'}
+
+ )}
+
+
+
+
+ );
+};
diff --git a/code/core/src/manager/globals/exports.ts b/code/core/src/manager/globals/exports.ts
index b7cac2e8a3c6..c01fdf8415a9 100644
--- a/code/core/src/manager/globals/exports.ts
+++ b/code/core/src/manager/globals/exports.ts
@@ -214,7 +214,9 @@ export default {
'PhotoIcon',
'PinAltIcon',
'PinIcon',
+ 'PlayAllHollowIcon',
'PlayBackIcon',
+ 'PlayHollowIcon',
'PlayIcon',
'PlayNextIcon',
'PlusIcon',
@@ -235,6 +237,7 @@ export default {
'RequestChangeIcon',
'RewindIcon',
'RulerIcon',
+ 'SaveIcon',
'SearchIcon',
'ShareAltIcon',
'ShareIcon',
@@ -252,6 +255,7 @@ export default {
'StatusPassIcon',
'StatusWarnIcon',
'StickerIcon',
+ 'StopAltHollowIcon',
'StopAltIcon',
'StopIcon',
'StorybookIcon',
diff --git a/code/core/src/types/modules/addons.ts b/code/core/src/types/modules/addons.ts
index 5aa530064699..8099f88b941e 100644
--- a/code/core/src/types/modules/addons.ts
+++ b/code/core/src/types/modules/addons.ts
@@ -468,6 +468,8 @@ export interface Addon_TestProviderType {
icon: ReactNode;
title: string;
description: FC;
+ runnable?: boolean;
+ watchable?: boolean;
}
type Addon_TypeBaseNames = Exclude<
diff --git a/code/lib/blocks/package.json b/code/lib/blocks/package.json
index f14f71319ded..54b5c0e7d421 100644
--- a/code/lib/blocks/package.json
+++ b/code/lib/blocks/package.json
@@ -45,10 +45,10 @@
"dependencies": {
"@storybook/csf": "^0.1.11",
"@storybook/global": "^5.0.0",
- "@storybook/icons": "^1.2.10",
+ "@storybook/icons": "^1.2.12",
"color-convert": "^2.0.1",
"dequal": "^2.0.2",
- "es-toolkit": "^1.21.0",
+ "es-toolkit": "^1.22.0",
"markdown-to-jsx": "^7.4.5",
"memoizerific": "^1.11.3",
"polished": "^4.2.2",
diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json
index 0f3ab191752e..2c6ba312e285 100644
--- a/code/lib/codemod/package.json
+++ b/code/lib/codemod/package.json
@@ -61,7 +61,7 @@
"@storybook/csf": "^0.1.11",
"@types/cross-spawn": "^6.0.2",
"cross-spawn": "^7.0.3",
- "es-toolkit": "^1.21.0",
+ "es-toolkit": "^1.22.0",
"globby": "^14.0.1",
"jscodeshift": "^0.15.1",
"prettier": "^3.1.1",
diff --git a/code/lib/source-loader/package.json b/code/lib/source-loader/package.json
index b21a1a2fec33..d85849b7ea4e 100644
--- a/code/lib/source-loader/package.json
+++ b/code/lib/source-loader/package.json
@@ -45,7 +45,7 @@
},
"dependencies": {
"@storybook/csf": "^0.1.11",
- "es-toolkit": "^1.21.0",
+ "es-toolkit": "^1.22.0",
"estraverse": "^5.2.0",
"prettier": "^3.1.1"
},
diff --git a/code/package.json b/code/package.json
index 240dc465751a..8bed0733fa94 100644
--- a/code/package.json
+++ b/code/package.json
@@ -185,7 +185,7 @@
"create-storybook": "workspace:*",
"cross-env": "^7.0.3",
"danger": "^12.3.3",
- "es-toolkit": "^1.21.0",
+ "es-toolkit": "^1.22.0",
"esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0",
"esbuild-loader": "^4.2.0",
"esbuild-plugin-alias": "^0.2.1",
diff --git a/code/renderers/react/package.json b/code/renderers/react/package.json
index 97ba5f8b0c7c..1a71e6fd4315 100644
--- a/code/renderers/react/package.json
+++ b/code/renderers/react/package.json
@@ -89,7 +89,7 @@
"@types/semver": "^7.3.4",
"@types/util-deprecate": "^1.0.0",
"babel-plugin-react-docgen": "^4.2.1",
- "es-toolkit": "^1.21.0",
+ "es-toolkit": "^1.22.0",
"expect-type": "^0.15.0",
"require-from-string": "^2.0.2"
},
diff --git a/code/yarn.lock b/code/yarn.lock
index 77bd47b36b2f..ff12b2b6fca1 100644
--- a/code/yarn.lock
+++ b/code/yarn.lock
@@ -5335,7 +5335,7 @@ __metadata:
dependencies:
"@storybook/addon-highlight": "workspace:*"
"@storybook/global": "npm:^5.0.0"
- "@storybook/icons": "npm:^1.2.10"
+ "@storybook/icons": "npm:^1.2.12"
"@testing-library/react": "npm:^14.0.0"
axe-core: "npm:^4.2.0"
react: "npm:^18.2.0"
@@ -5373,7 +5373,7 @@ __metadata:
resolution: "@storybook/addon-backgrounds@workspace:addons/backgrounds"
dependencies:
"@storybook/global": "npm:^5.0.0"
- "@storybook/icons": "npm:^1.2.10"
+ "@storybook/icons": "npm:^1.2.12"
memoizerific: "npm:^1.11.3"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
@@ -5390,7 +5390,7 @@ __metadata:
dependencies:
"@storybook/blocks": "workspace:*"
"@storybook/global": "npm:^5.0.0"
- "@storybook/icons": "npm:^1.2.10"
+ "@storybook/icons": "npm:^1.2.12"
dequal: "npm:^2.0.2"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
@@ -5506,7 +5506,7 @@ __metadata:
resolution: "@storybook/addon-jest@workspace:addons/jest"
dependencies:
"@storybook/global": "npm:^5.0.0"
- "@storybook/icons": "npm:^1.2.10"
+ "@storybook/icons": "npm:^1.2.12"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
react-resize-detector: "npm:^7.1.2"
@@ -5553,7 +5553,7 @@ __metadata:
resolution: "@storybook/addon-measure@workspace:addons/measure"
dependencies:
"@storybook/global": "npm:^5.0.0"
- "@storybook/icons": "npm:^1.2.10"
+ "@storybook/icons": "npm:^1.2.12"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
tiny-invariant: "npm:^1.3.1"
@@ -5568,7 +5568,7 @@ __metadata:
resolution: "@storybook/addon-onboarding@workspace:addons/onboarding"
dependencies:
"@radix-ui/react-dialog": "npm:^1.0.5"
- "@storybook/icons": "npm:^1.2.10"
+ "@storybook/icons": "npm:^1.2.12"
"@storybook/react": "workspace:*"
framer-motion: "npm:^11.0.3"
react: "npm:^18.2.0"
@@ -5587,7 +5587,7 @@ __metadata:
resolution: "@storybook/addon-outline@workspace:addons/outline"
dependencies:
"@storybook/global": "npm:^5.0.0"
- "@storybook/icons": "npm:^1.2.10"
+ "@storybook/icons": "npm:^1.2.12"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
ts-dedent: "npm:^2.0.0"
@@ -5619,7 +5619,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@storybook/addon-themes@workspace:addons/themes"
dependencies:
- "@storybook/icons": "npm:^1.2.10"
+ "@storybook/icons": "npm:^1.2.12"
ts-dedent: "npm:^2.0.0"
typescript: "npm:^5.3.2"
peerDependencies:
@@ -5644,7 +5644,7 @@ __metadata:
resolution: "@storybook/addon-viewport@workspace:addons/viewport"
dependencies:
"@storybook/global": "npm:^5.0.0"
- "@storybook/icons": "npm:^1.2.10"
+ "@storybook/icons": "npm:^1.2.12"
memoizerific: "npm:^1.11.3"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
@@ -5751,13 +5751,13 @@ __metadata:
"@storybook/addon-actions": "workspace:*"
"@storybook/csf": "npm:^0.1.11"
"@storybook/global": "npm:^5.0.0"
- "@storybook/icons": "npm:^1.2.10"
+ "@storybook/icons": "npm:^1.2.12"
"@storybook/react": "workspace:*"
"@storybook/test": "workspace:*"
"@types/color-convert": "npm:^2.0.0"
color-convert: "npm:^2.0.1"
dequal: "npm:^2.0.2"
- es-toolkit: "npm:^1.21.0"
+ es-toolkit: "npm:^1.22.0"
markdown-to-jsx: "npm:^7.4.5"
memoizerific: "npm:^1.11.3"
polished: "npm:^4.2.2"
@@ -5930,7 +5930,7 @@ __metadata:
ansi-regex: "npm:^6.0.1"
camelcase: "npm:^8.0.0"
cross-spawn: "npm:^7.0.3"
- es-toolkit: "npm:^1.21.0"
+ es-toolkit: "npm:^1.22.0"
globby: "npm:^14.0.1"
jscodeshift: "npm:^0.15.1"
mdast-util-mdx-jsx: "npm:^3.0.0"
@@ -6022,7 +6022,7 @@ __metadata:
"@storybook/csf": "npm:^0.1.11"
"@storybook/docs-mdx": "npm:4.0.0-next.1"
"@storybook/global": "npm:^5.0.0"
- "@storybook/icons": "npm:^1.2.10"
+ "@storybook/icons": "npm:^1.2.12"
"@tanstack/react-virtual": "npm:^3.3.0"
"@testing-library/react": "npm:^14.0.0"
"@types/compression": "npm:^1.7.0"
@@ -6072,7 +6072,7 @@ __metadata:
diff: "npm:^5.2.0"
downshift: "npm:^9.0.4"
ejs: "npm:^3.1.10"
- es-toolkit: "npm:^1.21.0"
+ es-toolkit: "npm:^1.22.0"
esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0"
esbuild-plugin-alias: "npm:^0.2.1"
esbuild-register: "npm:^3.5.0"
@@ -6237,7 +6237,7 @@ __metadata:
"@devtools-ds/object-inspector": "npm:^1.1.2"
"@storybook/csf": "npm:^0.1.11"
"@storybook/global": "npm:^5.0.0"
- "@storybook/icons": "npm:^1.2.5"
+ "@storybook/icons": "npm:^1.2.12"
"@storybook/instrumenter": "workspace:*"
"@storybook/test": "workspace:*"
"@types/node": "npm:^22.0.0"
@@ -6347,7 +6347,17 @@ __metadata:
languageName: unknown
linkType: soft
-"@storybook/icons@npm:^1.2.10, @storybook/icons@npm:^1.2.5":
+"@storybook/icons@npm:^1.2.12":
+ version: 1.2.12
+ resolution: "@storybook/icons@npm:1.2.12"
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+ checksum: 10c0/97f6a7b7841fb5a0d1c8a30c36173469e7b0814a674c8103c7c0fd8803f0f7c2a778545af864012d40883195a533534dbc98541deac2bafe31e6a3fe37fdfc66
+ languageName: node
+ linkType: hard
+
+"@storybook/icons@npm:^1.2.5":
version: 1.2.10
resolution: "@storybook/icons@npm:1.2.10"
peerDependencies:
@@ -6777,7 +6787,7 @@ __metadata:
acorn-jsx: "npm:^5.3.1"
acorn-walk: "npm:^7.2.0"
babel-plugin-react-docgen: "npm:^4.2.1"
- es-toolkit: "npm:^1.21.0"
+ es-toolkit: "npm:^1.22.0"
escodegen: "npm:^2.1.0"
expect-type: "npm:^0.15.0"
html-tags: "npm:^3.1.0"
@@ -6901,7 +6911,7 @@ __metadata:
create-storybook: "workspace:*"
cross-env: "npm:^7.0.3"
danger: "npm:^12.3.3"
- es-toolkit: "npm:^1.21.0"
+ es-toolkit: "npm:^1.22.0"
esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0"
esbuild-loader: "npm:^4.2.0"
esbuild-plugin-alias: "npm:^0.2.1"
@@ -7003,7 +7013,7 @@ __metadata:
resolution: "@storybook/source-loader@workspace:lib/source-loader"
dependencies:
"@storybook/csf": "npm:^0.1.11"
- es-toolkit: "npm:^1.21.0"
+ es-toolkit: "npm:^1.22.0"
estraverse: "npm:^5.2.0"
prettier: "npm:^3.1.1"
typescript: "npm:^5.3.2"
@@ -14159,10 +14169,10 @@ __metadata:
languageName: node
linkType: hard
-"es-toolkit@npm:^1.21.0":
- version: 1.21.0
- resolution: "es-toolkit@npm:1.21.0"
- checksum: 10c0/894a63f8ce5b2e5c1be242c8e8eace6364ea1212d01cdf89594d2cc582c5e1574114ad2ee7022ad5206561c4d5170511d83b38853257249860e56178768854ea
+"es-toolkit@npm:^1.22.0":
+ version: 1.22.0
+ resolution: "es-toolkit@npm:1.22.0"
+ checksum: 10c0/a167789f727437d435071af74e22c0c4a5a557aa61a5013a1656d24b1c8636c88d6b74f12ad0c3966b74d3f56e432d8e8d1989e5c10c10fda8eba5752783af18
languageName: node
linkType: hard
diff --git a/scripts/package.json b/scripts/package.json
index 456ba6c06038..ff66a0feab87 100644
--- a/scripts/package.json
+++ b/scripts/package.json
@@ -107,7 +107,7 @@
"detect-port": "^1.6.1",
"ejs": "^3.1.10",
"ejs-lint": "^2.0.0",
- "es-toolkit": "^1.21.0",
+ "es-toolkit": "^1.22.0",
"esbuild": "^0.23.0",
"esbuild-plugin-alias": "^0.2.1",
"eslint": "^8.57.0",
diff --git a/scripts/yarn.lock b/scripts/yarn.lock
index a0e46537c940..d6e69cea50b0 100644
--- a/scripts/yarn.lock
+++ b/scripts/yarn.lock
@@ -1571,7 +1571,7 @@ __metadata:
detect-port: "npm:^1.6.1"
ejs: "npm:^3.1.10"
ejs-lint: "npm:^2.0.0"
- es-toolkit: "npm:^1.21.0"
+ es-toolkit: "npm:^1.22.0"
esbuild: "npm:^0.23.0"
esbuild-plugin-alias: "npm:^0.2.1"
eslint: "npm:^8.57.0"
@@ -5308,10 +5308,10 @@ __metadata:
languageName: node
linkType: hard
-"es-toolkit@npm:^1.21.0":
- version: 1.21.0
- resolution: "es-toolkit@npm:1.21.0"
- checksum: 10c0/894a63f8ce5b2e5c1be242c8e8eace6364ea1212d01cdf89594d2cc582c5e1574114ad2ee7022ad5206561c4d5170511d83b38853257249860e56178768854ea
+"es-toolkit@npm:^1.22.0":
+ version: 1.22.0
+ resolution: "es-toolkit@npm:1.22.0"
+ checksum: 10c0/a167789f727437d435071af74e22c0c4a5a557aa61a5013a1656d24b1c8636c88d6b74f12ad0c3966b74d3f56e432d8e8d1989e5c10c10fda8eba5752783af18
languageName: node
linkType: hard