Move Storybook to the root

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-08-13 22:18:04 +02:00
parent d9fb18dc9a
commit 854351e088
13 changed files with 982 additions and 5251 deletions
+49
View File
@@ -0,0 +1,49 @@
import type { StorybookConfig } from '@storybook/react-vite';
import { join, dirname, posix } from 'path';
// This set of stories are the ones that we publish to backstage.io.
const backstageCoreStories = [
'packages/ui',
'packages/core-components',
'packages/app',
'plugins/org',
'plugins/search',
'plugins/search-react',
'plugins/home',
'plugins/catalog-react',
];
const rootPath = '../';
const storiesSrcMdx = 'src/**/*.mdx';
const storiesSrcGlob = 'src/**/*.stories.@(js|jsx|mjs|ts|tsx)';
const getStoriesPath = (element: string, pattern: string) =>
posix.join(rootPath, element, pattern);
const stories = backstageCoreStories.flatMap(element => [
getStoriesPath(element, storiesSrcMdx),
getStoriesPath(element, storiesSrcGlob),
]);
// Resolve absolute path of a package. Needed in monorepos.
function getAbsolutePath(value: string): any {
return dirname(require.resolve(join(value, 'package.json')));
}
const config: StorybookConfig = {
stories,
addons: [
getAbsolutePath('@storybook/addon-links'),
getAbsolutePath('@storybook/addon-essentials'),
getAbsolutePath('@storybook/addon-interactions'),
getAbsolutePath('@storybook/addon-themes'),
getAbsolutePath('@storybook/addon-storysource'),
],
framework: {
name: getAbsolutePath('@storybook/react-vite'),
options: {},
},
};
export default config;
+126
View File
@@ -0,0 +1,126 @@
import React, { useEffect } from 'react';
import { TestApiProvider } from '@backstage/test-utils';
import { Content, AlertDisplay } from '@backstage/core-components';
import { apis } from './support/apis';
import type { Decorator, Preview } from '@storybook/react';
import { useGlobals } from '@storybook/preview-api';
import { UnifiedThemeProvider, themes } from '@backstage/theme';
// Default Backstage theme CSS (from packages/ui)
import '../packages/ui/src/css/styles.css';
// Custom Storybook chrome/styles
import './storybook.css';
// Custom themes
import './themes/spotify.css';
const preview: Preview = {
globalTypes: {
themeMode: {
name: 'Theme Mode',
description: 'Global theme mode for components',
defaultValue: 'light',
toolbar: {
icon: 'circlehollow',
items: [
{ value: 'light', icon: 'circlehollow', title: 'Light' },
{ value: 'dark', icon: 'circle', title: 'Dark' },
],
showName: true,
dynamicTitle: true,
},
},
themeName: {
name: 'Theme Name',
description: 'Global theme name for components',
defaultValue: 'backstage',
toolbar: {
icon: 'paintbrush',
items: [
{ value: 'backstage', title: 'Backstage' },
{ value: 'spotify', title: 'Spotify' },
],
showName: true,
dynamicTitle: true,
},
},
},
initialGlobals: {
themeMode: 'light',
themeName: 'backstage',
},
parameters: {
layout: 'fullscreen',
backgrounds: {
disable: true,
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
options: {
storySort: {
order: ['Backstage UI', 'Plugins', 'Layout', 'Navigation'],
},
},
viewport: {
viewports: {
initial: {
name: 'Initial',
styles: { width: '320px', height: '100%' },
},
xs: { name: 'Extra Small', styles: { width: '640px', height: '100%' } },
sm: { name: 'Small', styles: { width: '768px', height: '100%' } },
md: { name: 'Medium', styles: { width: '1024px', height: '100%' } },
lg: { name: 'Large', styles: { width: '1280px', height: '100%' } },
xl: {
name: 'Extra Large',
styles: { width: '1536px', height: '100%' },
},
},
},
},
decorators: [
Story => {
const [globals] = useGlobals();
const selectedTheme =
globals.themeMode === 'light' ? themes.light : themes.dark;
const selectedThemeMode = globals.themeMode || 'light';
const selectedThemeName = globals.themeName || 'backstage';
useEffect(() => {
document.body.removeAttribute('data-theme-mode');
document.body.removeAttribute('data-theme-name');
document.body.setAttribute('data-theme-mode', selectedThemeMode);
document.body.setAttribute('data-theme-name', selectedThemeName);
return () => {
document.body.removeAttribute('data-theme-mode');
document.body.removeAttribute('data-theme-name');
};
}, [selectedTheme, selectedThemeName]);
document.body.style.backgroundColor = 'var(--bui-bg)';
const docsStoryElements = document.getElementsByClassName('docs-story');
Array.from(docsStoryElements).forEach(element => {
(element as HTMLElement).style.backgroundColor = 'var(--bui-bg)';
});
return (
<UnifiedThemeProvider theme={selectedTheme}>
{/* @ts-ignore */}
<TestApiProvider apis={apis}>
<AlertDisplay />
<Content>
<Story />
</Content>
</TestApiProvider>
</UnifiedThemeProvider>
);
},
],
};
export default preview;
+38
View File
@@ -0,0 +1,38 @@
:root {
--sb-panel-radius: 0;
--sb-panel-top: 0;
--sb-panel-bottom: 0;
--sb-panel-left: 0;
--sb-panel-right: 0;
--sb-sidebar-border: none;
--sb-sidebar-border-right: 1px solid var(--bui-border);
--sb-sidebar-bg: #000;
--sb-options-border: none;
--sb-options-border-left: 1px solid var(--bui-border);
--sb-content-padding-inline: 250px;
}
[data-theme-mode='dark'] {
--sb-sidebar-bg: var(--bui-bg-surface-1);
}
[data-theme-name='spotify'] {
--sb-panel-radius: var(--bui-radius-3);
--sb-panel-top: 8px;
--sb-panel-bottom: 8px;
--sb-panel-left: 8px;
--sb-panel-right: 8px;
--sb-sidebar-border: none;
--sb-sidebar-border-right: none;
--sb-sidebar-bg: var(--bui-bg-surface-1);
--sb-options-border: none;
--sb-options-border-left: none;
--sb-content-padding-inline: 258px;
}
[data-theme-name='spotify'][data-theme-mode='light'] {
--sb-sidebar-border: 1px solid var(--bui-border);
--sb-sidebar-border-right: 1px solid var(--bui-border);
--sb-options-border: 1px solid var(--bui-border);
--sb-options-border-left: 1px solid var(--bui-border);
}
+75
View File
@@ -0,0 +1,75 @@
import {
AlertApiForwarder,
ErrorAlerter,
ErrorApiForwarder,
GithubAuth,
GitlabAuth,
GoogleAuth,
OAuthRequestManager,
OktaAuth,
ConfigReader,
LocalStorageFeatureFlags,
} from '@backstage/core-app-api';
import {
alertApiRef,
errorApiRef,
githubAuthApiRef,
gitlabAuthApiRef,
googleAuthApiRef,
identityApiRef,
oauthRequestApiRef,
oktaAuthApiRef,
configApiRef,
featureFlagsApiRef,
} from '@backstage/core-plugin-api';
import { translationApiRef } from '@backstage/core-plugin-api/alpha';
import { MockTranslationApi } from '@backstage/test-utils/alpha';
const configApi = new ConfigReader({});
const featureFlagsApi = new LocalStorageFeatureFlags();
const alertApi = new AlertApiForwarder();
const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder());
const identityApi = {
getUserId: () => 'guest',
getProfile: () => ({ email: 'guest@example.com' }),
getIdToken: () => undefined,
signOut: async () => {},
};
const oauthRequestApi = new OAuthRequestManager();
const googleAuthApi = GoogleAuth.create({
apiOrigin: 'http://localhost:7007',
basePath: '/auth/',
oauthRequestApi,
});
const githubAuthApi = GithubAuth.create({
apiOrigin: 'http://localhost:7007',
basePath: '/auth/',
oauthRequestApi,
});
const gitlabAuthApi = GitlabAuth.create({
apiOrigin: 'http://localhost:7007',
basePath: '/auth/',
oauthRequestApi,
});
const oktaAuthApi = OktaAuth.create({
apiOrigin: 'http://localhost:7007',
basePath: '/auth/',
oauthRequestApi,
});
const translationApi = MockTranslationApi.create();
export const apis = [
[configApiRef, configApi],
[featureFlagsApiRef, featureFlagsApi],
[alertApiRef, alertApi],
[errorApiRef, errorApi],
[identityApiRef, identityApi],
[oauthRequestApiRef, oauthRequestApi],
[googleAuthApiRef, googleAuthApi],
[githubAuthApiRef, githubAuthApi],
[gitlabAuthApiRef, gitlabAuthApi],
[oktaAuthApiRef, oktaAuthApi],
[translationApiRef, translationApi],
];
+1
View File
@@ -0,0 +1 @@
/* Placeholder for custom Spotify-like theme tweaks applied to the Storybook chrome */