diff --git a/.prettierignore b/.prettierignore
index fe38de2e01a..2d761e81124 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -27,5 +27,4 @@ GH2SG.bookmarklet.js
docker-images/grafana/config/provisioning/dashboards/sourcegraph/
storybook-static/
browser/code-intel-extensions/
-!/.storybook/**
monitoring/monitoring/README.md
diff --git a/.storybook/main.js b/.storybook/main.js
deleted file mode 100644
index 05042ea825d..00000000000
--- a/.storybook/main.js
+++ /dev/null
@@ -1,147 +0,0 @@
-const path = require('path')
-const { remove } = require('lodash')
-const { DefinePlugin, ProgressPlugin } = require('webpack')
-const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
-const TerserPlugin = require('terser-webpack-plugin')
-
-const monacoEditorPaths = [path.resolve(__dirname, '..', 'node_modules', 'monaco-editor')]
-
-const shouldMinify = !!process.env.MINIFY
-
-const config = {
- stories: ['../client/**/*.story.tsx'],
- addons: [
- '@storybook/addon-knobs',
- '@storybook/addon-actions',
- 'storybook-addon-designs',
- 'storybook-dark-mode',
- '@storybook/addon-a11y',
- '@storybook/addon-toolbars',
- './redesign-toggle-toolbar/preset.js',
- ],
-
- /**
- * @param config {import('webpack').Configuration}
- * @returns {import('webpack').Configuration}
- */
- webpackFinal: config => {
- // Include sourcemaps
- config.mode = shouldMinify ? 'production' : 'development'
- config.devtool = shouldMinify ? 'source-map' : 'cheap-module-eval-source-map'
- const definePlugin = config.plugins.find(plugin => plugin instanceof DefinePlugin)
- // @ts-ignore
- definePlugin.definitions.NODE_ENV = JSON.stringify(config.mode)
- // @ts-ignore
- definePlugin.definitions['process.env'].NODE_ENV = JSON.stringify(config.mode)
-
- if (shouldMinify) {
- config.optimization = {
- minimize: true,
- minimizer: [
- new TerserPlugin({
- sourceMap: true,
- terserOptions: {
- compress: {
- // // Don't inline functions, which causes name collisions with uglify-es:
- // https://github.com/mishoo/UglifyJS2/issues/2842
- inline: 1,
- },
- },
- }),
- ],
- namedModules: false,
- }
- }
-
- // We don't use Storybook's default Babel config for our repo, it doesn't include everything we need.
- config.module.rules.splice(0, 1)
-
- if (process.env.CI) {
- remove(config.plugins, plugin => plugin instanceof ProgressPlugin)
- }
-
- config.module.rules.push({
- test: /\.tsx?$/,
- loader: require.resolve('babel-loader'),
- options: {
- configFile: path.resolve(__dirname, '..', 'babel.config.js'),
- },
- })
-
- config.plugins.push(
- new MonacoWebpackPlugin({
- languages: ['json'],
- features: [
- 'bracketMatching',
- 'clipboard',
- 'coreCommands',
- 'cursorUndo',
- 'find',
- 'format',
- 'hover',
- 'inPlaceReplace',
- 'iPadShowKeyboard',
- 'links',
- 'suggest',
- ],
- })
- )
-
- const storybookDirectory = path.resolve(__dirname, '../node_modules/@storybook')
-
- // Put our style rules at the beginning so they're processed by the time it
- // gets to storybook's style rules.
- config.module.rules.unshift({
- test: /\.(sass|scss)$/,
- use: [
- 'to-string-loader',
- 'css-loader',
- {
- loader: 'postcss-loader',
- },
- {
- loader: 'sass-loader',
- options: {
- sassOptions: {
- includePaths: [path.resolve(__dirname, '..', 'node_modules')],
- },
- },
- },
- ],
- // Make sure Storybook styles get handled by the Storybook config
- exclude: storybookDirectory,
- })
-
- // Make sure Storybook style loaders are only evaluated for Storybook styles.
- config.module.rules.find(rule => rule.test?.toString() === /\.css$/.toString()).include = storybookDirectory
-
- config.module.rules.unshift({
- // CSS rule for external plain CSS (skip SASS and PostCSS for build perf)
- test: /\.css$/,
- // Make sure Storybook styles get handled by the Storybook config
- exclude: [storybookDirectory, ...monacoEditorPaths],
- use: ['to-string-loader', 'css-loader'],
- })
-
- config.module.rules.unshift({
- // CSS rule for monaco-editor, it expects styles to be loaded with `style-loader`.
- test: /\.css$/,
- include: monacoEditorPaths,
- // Make sure Storybook styles get handled by the Storybook config
- exclude: [storybookDirectory],
- use: ['style-loader', 'css-loader'],
- })
- config.module.rules.unshift({
- test: /\.ya?ml$/,
- use: ['raw-loader'],
- })
-
- Object.assign(config.entry, {
- 'editor.worker': 'monaco-editor/esm/vs/editor/editor.worker.js',
- 'json.worker': 'monaco-editor/esm/vs/language/json/json.worker',
- })
-
- return config
- },
-}
-module.exports = config
diff --git a/.storybook/preview.js b/.storybook/preview.js
deleted file mode 100644
index e4528023c19..00000000000
--- a/.storybook/preview.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import 'focus-visible'
-
-import { configureActions } from '@storybook/addon-actions'
-import { withConsole } from '@storybook/addon-console'
-import { setLinkComponent, AnchorLink } from '../client/shared/src/components/Link'
-import { withDesign } from 'storybook-addon-designs'
-import isChromatic from 'chromatic/isChromatic'
-import * as themes from './themes'
-
-export const decorators = [withDesign, (storyFn, context) => withConsole()(storyFn)(context)]
-
-export const parameters = {
- darkMode: {
- stylePreview: true,
- darkClass: 'theme-dark',
- lightClass: 'theme-light',
- light: themes.light,
- dark: themes.dark,
- },
-}
-
-configureActions({ depth: 100, limit: 20 })
-
-setLinkComponent(AnchorLink)
-
-// Default to light theme for Chromatic and "Open canvas in new tab" button.
-// addon-dark-mode will override this if it's running.
-if (!document.body.classList.contains('theme-dark')) {
- document.body.classList.add('theme-light')
-}
-
-if (isChromatic()) {
- const style = document.createElement('style')
- style.innerHTML = `
- .monaco-editor .cursor {
- visibility: hidden !important;
- }
- `
- document.head.append(style)
-}
-
-// @ts-ignore
-window.MonacoEnvironment = {
- getWorkerUrl(_, label) {
- if (label === 'json') {
- return '/json.worker.bundle.js'
- }
- return '/editor.worker.bundle.js'
- },
-}
diff --git a/.storybook/redesign-toggle-toolbar/preset.js b/.storybook/redesign-toggle-toolbar/preset.js
deleted file mode 100644
index 228842d65fa..00000000000
--- a/.storybook/redesign-toggle-toolbar/preset.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = {
- managerEntries: (entry = []) => {
- return [...entry, require.resolve('./register')]
- },
-}
diff --git a/.storybook/redesign-toggle-toolbar/register.js b/.storybook/redesign-toggle-toolbar/register.js
deleted file mode 100644
index 73100ba4487..00000000000
--- a/.storybook/redesign-toggle-toolbar/register.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import React from 'react'
-import addons, { types } from '@storybook/addons'
-import { Icons, IconButton } from '@storybook/components'
-import { useRedesignToggle, REDESIGN_CLASS_NAME } from '../../client/shared/src/util/useRedesignToggle'
-
-const toggleRedesignClass = (element, isRedesignEnabled) => {
- element.classList.toggle(REDESIGN_CLASS_NAME, !isRedesignEnabled)
-}
-
-const updatePreview = isRedesignEnabled => {
- const iframe = document.getElementById('storybook-preview-iframe')
-
- if (!iframe) {
- return
- }
-
- const iframeDocument = iframe.contentDocument || iframe.contentWindow?.document
- const body = iframeDocument?.body
-
- toggleRedesignClass(body, isRedesignEnabled)
-}
-
-const updateManager = isRedesignEnabled => {
- const manager = document.querySelector('body')
-
- if (!manager) {
- return
- }
-
- toggleRedesignClass(manager, isRedesignEnabled)
-}
-
-const RedesignToggleStorybook = () => {
- const { isRedesignEnabled, setIsRedesignEnabled } = useRedesignToggle()
-
- const handleRedesignToggle = () => {
- setIsRedesignEnabled(!isRedesignEnabled)
- updatePreview(isRedesignEnabled)
- updateManager(isRedesignEnabled)
- }
-
- return (
-
-
-
- )
-}
-
-/**
- * Custom toolbar which renders button to toggle redesign theme global CSS class.
- */
-addons.register('sourcegraph/redesign-toggle-toolbar', () => {
- addons.add('sourcegraph/redesign-toggle-toolbar', {
- title: 'Redesign toggle toolbar',
- type: types.TOOL,
- match: ({ viewMode }) => viewMode === 'story' || viewMode === 'docs',
- render: RedesignToggleStorybook,
- })
-})
diff --git a/.storybook/themes.js b/.storybook/themes.js
deleted file mode 100644
index 55666aeb9a3..00000000000
--- a/.storybook/themes.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { themes } from '@storybook/theming'
-import openColor from 'open-color'
-// @ts-ignore
-import brandImage from '../ui/assets/img/wildcard-design-system.svg'
-
-// Themes use the colors from our webapp.
-
-/** @type {Partial} */
-const common = {
- colorPrimary: openColor.blue[6],
- colorSecondary: openColor.blue[6],
- brandImage,
- brandTitle: 'Sourcegraph Wildcard design system',
- fontBase:
- '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
- fontCode: 'sfmono-regular, consolas, menlo, dejavu sans mono, monospace',
-}
-
-/** @type {import('@storybook/theming').ThemeVars} */
-export const dark = {
- ...themes.dark,
- ...common,
- appBg: '#1c2736',
- appContentBg: '#151c28',
- appBorderColor: '#2b3750',
- barBg: '#0e121b',
- barTextColor: '#a2b0cd',
- textColor: '#f2f4f8',
- inputTextColor: '#ffffff',
-}
-
-/** @type {import('@storybook/theming').ThemeVars} */
-export const light = {
- ...themes.light,
- ...common,
- appBg: '#fbfdff',
- textColor: '#2b3750',
- barTextColor: '#566e9f',
- inputTextColor: '#2b3750',
-}
diff --git a/babel.config.js b/babel.config.js
index ec4fd579b71..e55a2f7d126 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -1,6 +1,7 @@
// @ts-check
const logger = require('gulplog')
const semver = require('semver')
+const path = require('path')
/** @type {import('@babel/core').ConfigFunction} */
module.exports = api => {
@@ -19,7 +20,7 @@ module.exports = api => {
return {
presets: [
// Can't put this in plugins because it needs to run as the last plugin.
- ...(instrument ? [{ plugins: [['babel-plugin-istanbul', { exclude: ['node_modules/**'] }]] }] : []),
+ ...(instrument ? [{ plugins: [['babel-plugin-istanbul', { cwd: path.resolve(__dirname) }]] }] : []),
[
'@babel/preset-env',
{
diff --git a/client/README.md b/client/README.md
index 297e0cbcfd4..da611af749c 100644
--- a/client/README.md
+++ b/client/README.md
@@ -10,6 +10,7 @@
- **shared**: Contains common TypeScript/React/SCSS client code shared between the browser extension and the web app. Everything in this package is code-host agnostic.
- **branded**: Contains React components and implements the visual design language we use across our web app and e.g. in the options menu of the browser extension. Over time, components from `shared` and `branded` packages should be moved into the `wildcard` package.
- **wildcard**: Package that encapsulates storybook configuration and contains our Wildcard design system components. If we're using a component in two or more different areas (e.g. `web-app` and `browser-extension`) then it should live in the `wildcard` package. Otherwise the components should be better colocated with the code where they're actually used.
+- **storybook**: Storybook configuration.
## Further migration plan
diff --git a/client/storybook/.eslintignore b/client/storybook/.eslintignore
new file mode 100644
index 00000000000..52f673c41d7
--- /dev/null
+++ b/client/storybook/.eslintignore
@@ -0,0 +1 @@
+out/
diff --git a/client/storybook/.eslintrc.js b/client/storybook/.eslintrc.js
new file mode 100644
index 00000000000..3747f40c469
--- /dev/null
+++ b/client/storybook/.eslintrc.js
@@ -0,0 +1,13 @@
+// @ts-check
+
+const baseConfig = require('../../.eslintrc.js')
+
+module.exports = {
+ extends: '../../.eslintrc.js',
+ parserOptions: {
+ ...baseConfig.parserOptions,
+ project: [__dirname + '/tsconfig.json'],
+ },
+ rules: {},
+ overrides: baseConfig.overrides,
+}
diff --git a/client/storybook/README.md b/client/storybook/README.md
new file mode 100644
index 00000000000..ab2258df5b8
--- /dev/null
+++ b/client/storybook/README.md
@@ -0,0 +1,12 @@
+# Storybook configuration
+
+## Usage
+
+Storybook configuration is setup as a `yarn workspace` symlink.
+
+Important commands are expose via root `package.json`:
+
+```sh
+yarn storybook
+yarn build-storybook
+```
diff --git a/ui/assets/img/wildcard-design-system.svg b/client/storybook/assets/img/wildcard-design-system.svg
similarity index 100%
rename from ui/assets/img/wildcard-design-system.svg
rename to client/storybook/assets/img/wildcard-design-system.svg
diff --git a/client/storybook/babel.config.js b/client/storybook/babel.config.js
new file mode 100644
index 00000000000..73a95d310cd
--- /dev/null
+++ b/client/storybook/babel.config.js
@@ -0,0 +1,5 @@
+// @ts-check
+
+module.exports = {
+ extends: '../../babel.config.js',
+}
diff --git a/client/storybook/globals.d.ts b/client/storybook/globals.d.ts
new file mode 100644
index 00000000000..0242f33e16d
--- /dev/null
+++ b/client/storybook/globals.d.ts
@@ -0,0 +1,9 @@
+declare module '@storybook/addon-console' {
+ export declare const withConsole: () => (storyFn: any) => (context: StoryContext) => React.ReactElement
+}
+
+declare interface Window {
+ MonacoEnvironment: {
+ getWorkerUrl(moduleId: string, label: string): string
+ }
+}
diff --git a/.storybook/jest.config.js b/client/storybook/jest.config.js
similarity index 82%
rename from .storybook/jest.config.js
rename to client/storybook/jest.config.js
index 51edb1bf556..7284291c2c1 100644
--- a/.storybook/jest.config.js
+++ b/client/storybook/jest.config.js
@@ -1,6 +1,6 @@
// @ts-check
-const config = require('../jest.config.base')
+const config = require('../../jest.config.base')
const exportedConfig = {
...config,
diff --git a/client/storybook/package.json b/client/storybook/package.json
new file mode 100644
index 00000000000..a4e5ee5dcfe
--- /dev/null
+++ b/client/storybook/package.json
@@ -0,0 +1,14 @@
+{
+ "private": true,
+ "name": "@sourcegraph/storybook",
+ "version": "0.0.1",
+ "description": "Sourcegraph Storybook configuration",
+ "sideEffects": false,
+ "license": "Apache-2.0",
+ "scripts": {
+ "eslint": "eslint --cache 'src/**/*.[jt]s?(x)'",
+ "start": "start-storybook -p 9001 -c ./src -s ./assets",
+ "build": "build-storybook -c ./src -s ./assets",
+ "test": "jest"
+ }
+}
diff --git a/.storybook/coverage.test.ts b/client/storybook/src/coverage.test.ts
similarity index 86%
rename from .storybook/coverage.test.ts
rename to client/storybook/src/coverage.test.ts
index 5e8c5a9497e..11e8c9afaf6 100644
--- a/.storybook/coverage.test.ts
+++ b/client/storybook/src/coverage.test.ts
@@ -1,8 +1,8 @@
+import path from 'path'
+import { pathToFileURL } from 'url'
import initStoryshots from '@storybook/addon-storyshots'
import { puppeteerTest } from '@storybook/addon-storyshots-puppeteer'
-import * as path from 'path'
-import { pathToFileURL } from 'url'
-import { recordCoverage } from '../client/shared/src/testing/coverage'
+import { recordCoverage } from '@sourcegraph/shared/src/testing/coverage'
// This test suite does not actually test anything.
// It just loads up the storybook in Puppeteer and records its coverage,
diff --git a/client/storybook/src/main.ts b/client/storybook/src/main.ts
new file mode 100644
index 00000000000..5464e1fb288
--- /dev/null
+++ b/client/storybook/src/main.ts
@@ -0,0 +1,151 @@
+import path from 'path'
+import { remove } from 'lodash'
+import TerserPlugin from 'terser-webpack-plugin'
+import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin'
+import { Configuration, DefinePlugin, ProgressPlugin, RuleSetRule } from 'webpack'
+
+const rootPath = path.resolve(__dirname, '../../../')
+const monacoEditorPaths = [path.resolve(rootPath, 'node_modules', 'monaco-editor')]
+const storiesGlob = path.resolve(rootPath, 'client/**/*.story.tsx')
+
+const shouldMinify = !!process.env.MINIFY
+
+const config = {
+ stories: [storiesGlob],
+ addons: [
+ '@storybook/addon-knobs',
+ '@storybook/addon-actions',
+ 'storybook-addon-designs',
+ 'storybook-dark-mode',
+ '@storybook/addon-a11y',
+ '@storybook/addon-toolbars',
+ './redesign-toggle-toolbar/register.ts',
+ ],
+
+ webpackFinal: (config: Configuration) => {
+ // Include sourcemaps
+ config.mode = shouldMinify ? 'production' : 'development'
+ config.devtool = shouldMinify ? 'source-map' : 'cheap-module-eval-source-map'
+
+ config.plugins?.push(
+ new DefinePlugin({
+ NODE_ENV: JSON.stringify(config.mode),
+ 'process.env.NODE_ENV': JSON.stringify(config.mode),
+ })
+ )
+
+ if (shouldMinify) {
+ config.optimization = {
+ namedModules: false,
+ minimize: true,
+ minimizer: [
+ new TerserPlugin({
+ terserOptions: {
+ sourceMap: true,
+ compress: {
+ // Don't inline functions, which causes name collisions with uglify-es:
+ // https://github.com/mishoo/UglifyJS2/issues/2842
+ inline: 1,
+ },
+ },
+ }),
+ ],
+ }
+ }
+
+ // We don't use Storybook's default Babel config for our repo, it doesn't include everything we need.
+ config.module?.rules.splice(0, 1)
+
+ if (process.env.CI) {
+ remove(config.plugins || [], plugin => plugin instanceof ProgressPlugin)
+ }
+
+ config.module?.rules.push({
+ test: /\.tsx?$/,
+ loader: require.resolve('babel-loader'),
+ options: {
+ configFile: path.resolve(rootPath, 'babel.config.js'),
+ },
+ })
+
+ config.plugins?.push(
+ new MonacoWebpackPlugin({
+ languages: ['json'],
+ features: [
+ 'bracketMatching',
+ 'clipboard',
+ 'coreCommands',
+ 'cursorUndo',
+ 'find',
+ 'format',
+ 'hover',
+ 'inPlaceReplace',
+ 'iPadShowKeyboard',
+ 'links',
+ 'suggest',
+ ],
+ })
+ )
+
+ const storybookDirectory = path.resolve(rootPath, 'node_modules/@storybook')
+ config.resolve?.modules?.push('src')
+
+ // Put our style rules at the beginning so they're processed by the time it
+ // gets to storybook's style rules.
+ config.module?.rules.unshift({
+ test: /\.(sass|scss)$/,
+ use: [
+ 'to-string-loader',
+ 'css-loader',
+ {
+ loader: 'postcss-loader',
+ },
+ {
+ loader: 'sass-loader',
+ options: {
+ sassOptions: {
+ includePaths: [path.resolve(rootPath, 'node_modules')],
+ },
+ },
+ },
+ ],
+ // Make sure Storybook styles get handled by the Storybook config
+ exclude: storybookDirectory,
+ })
+
+ // Make sure Storybook style loaders are only evaluated for Storybook styles.
+ const cssRule = config.module?.rules.find(rule => rule.test?.toString() === /\.css$/.toString()) as RuleSetRule
+ cssRule.include = storybookDirectory
+
+ config.module?.rules.unshift({
+ // CSS rule for external plain CSS (skip SASS and PostCSS for build perf)
+ test: /\.css$/,
+ // Make sure Storybook styles get handled by the Storybook config
+ exclude: [storybookDirectory, ...monacoEditorPaths],
+ use: ['to-string-loader', 'css-loader'],
+ })
+
+ config.module?.rules.unshift({
+ // CSS rule for monaco-editor, it expects styles to be loaded with `style-loader`.
+ test: /\.css$/,
+ include: monacoEditorPaths,
+ // Make sure Storybook styles get handled by the Storybook config
+ exclude: [storybookDirectory],
+ use: ['style-loader', 'css-loader'],
+ })
+
+ config.module?.rules.unshift({
+ test: /\.ya?ml$/,
+ use: ['raw-loader'],
+ })
+
+ Object.assign(config.entry, {
+ 'editor.worker': 'monaco-editor/esm/vs/editor/editor.worker.js',
+ 'json.worker': 'monaco-editor/esm/vs/language/json/json.worker',
+ })
+
+ return config
+ },
+}
+
+module.exports = config
diff --git a/.storybook/manager-head.html b/client/storybook/src/manager-head.html
similarity index 100%
rename from .storybook/manager-head.html
rename to client/storybook/src/manager-head.html
diff --git a/client/storybook/src/preview.ts b/client/storybook/src/preview.ts
new file mode 100644
index 00000000000..3419f1f0f5c
--- /dev/null
+++ b/client/storybook/src/preview.ts
@@ -0,0 +1,54 @@
+import 'focus-visible'
+import { ReactElement } from 'react'
+import isChromatic from 'chromatic/isChromatic'
+import { withDesign } from 'storybook-addon-designs'
+import { DecoratorFunction } from '@storybook/addons'
+import { configureActions } from '@storybook/addon-actions'
+import { withConsole } from '@storybook/addon-console'
+import { setLinkComponent, AnchorLink } from '@sourcegraph/shared/src/components/Link'
+import * as themes from './themes'
+
+const withConsoleDecorator: DecoratorFunction = (storyFn, context): ReactElement =>
+ withConsole()(storyFn)(context)
+
+export const decorators = [withDesign, withConsoleDecorator]
+
+export const parameters = {
+ darkMode: {
+ stylePreview: true,
+ darkClass: 'theme-dark',
+ lightClass: 'theme-light',
+ light: themes.light,
+ dark: themes.dark,
+ },
+}
+
+configureActions({ depth: 100, limit: 20 })
+
+setLinkComponent(AnchorLink)
+
+// Default to light theme for Chromatic and "Open canvas in new tab" button.
+// addon-dark-mode will override this if it's running.
+if (!document.body.classList.contains('theme-dark')) {
+ document.body.classList.add('theme-light')
+}
+
+if (isChromatic()) {
+ const style = document.createElement('style')
+ style.innerHTML = `
+ .monaco-editor .cursor {
+ visibility: hidden !important;
+ }
+ `
+ document.head.append(style)
+}
+
+window.MonacoEnvironment = {
+ getWorkerUrl(moduleId: string, label: string) {
+ if (label === 'json') {
+ return '/json.worker.bundle.js'
+ }
+
+ return '/editor.worker.bundle.js'
+ },
+}
diff --git a/client/storybook/src/redesign-toggle-toolbar/RedesignToggleStorybook.tsx b/client/storybook/src/redesign-toggle-toolbar/RedesignToggleStorybook.tsx
new file mode 100644
index 00000000000..37628309a12
--- /dev/null
+++ b/client/storybook/src/redesign-toggle-toolbar/RedesignToggleStorybook.tsx
@@ -0,0 +1,48 @@
+import React, { ReactElement } from 'react'
+import { Icons, IconButton } from '@storybook/components'
+import { useRedesignToggle, REDESIGN_CLASS_NAME } from '@sourcegraph/shared/src/util/useRedesignToggle'
+
+const toggleRedesignClass = (element: HTMLElement, isRedesignEnabled: boolean): void => {
+ element.classList.toggle(REDESIGN_CLASS_NAME, !isRedesignEnabled)
+}
+
+const updatePreview = (isRedesignEnabled: boolean): void => {
+ const iframe = document.querySelector('#storybook-preview-iframe') as HTMLIFrameElement | undefined
+
+ const iframeDocument = iframe?.contentDocument || iframe?.contentWindow?.document
+ const body = iframeDocument?.body
+
+ if (body) {
+ toggleRedesignClass(body, isRedesignEnabled)
+ }
+}
+
+const updateManager = (isRedesignEnabled: boolean): void => {
+ const manager = document.querySelector('body')
+
+ if (manager) {
+ toggleRedesignClass(manager, isRedesignEnabled)
+ }
+}
+
+export const RedesignToggleStorybook = (): ReactElement => {
+ const { isRedesignEnabled, setIsRedesignEnabled } = useRedesignToggle()
+
+ const handleRedesignToggle = (): void => {
+ setIsRedesignEnabled(!isRedesignEnabled)
+ updatePreview(isRedesignEnabled)
+ updateManager(isRedesignEnabled)
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/client/storybook/src/redesign-toggle-toolbar/register.ts b/client/storybook/src/redesign-toggle-toolbar/register.ts
new file mode 100644
index 00000000000..9c458366f9a
--- /dev/null
+++ b/client/storybook/src/redesign-toggle-toolbar/register.ts
@@ -0,0 +1,14 @@
+import addons, { types } from '@storybook/addons'
+import { RedesignToggleStorybook } from './RedesignToggleStorybook'
+
+/**
+ * Custom toolbar which renders button to toggle redesign theme global CSS class.
+ */
+addons.register('sourcegraph/redesign-toggle-toolbar', () => {
+ addons.add('sourcegraph/redesign-toggle-toolbar', {
+ title: 'Redesign toggle toolbar',
+ type: types.TOOL,
+ match: ({ viewMode }) => viewMode === 'story' || viewMode === 'docs',
+ render: RedesignToggleStorybook,
+ })
+})
diff --git a/client/storybook/src/themes.ts b/client/storybook/src/themes.ts
new file mode 100644
index 00000000000..d76e37be71d
--- /dev/null
+++ b/client/storybook/src/themes.ts
@@ -0,0 +1,35 @@
+import { ThemeVars, themes } from '@storybook/theming'
+import openColor from 'open-color'
+
+// Themes use the colors from our webapp.
+const common: Omit = {
+ colorPrimary: openColor.blue[6],
+ colorSecondary: openColor.blue[6],
+ brandTitle: 'Sourcegraph Wildcard design system',
+ brandUrl: 'https://sourcegraph.com',
+ brandImage: '/img/wildcard-design-system.svg',
+ fontBase:
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
+ fontCode: 'sfmono-regular, consolas, menlo, dejavu sans mono, monospace',
+}
+
+export const dark: ThemeVars = {
+ ...themes.dark,
+ ...common,
+ appBg: '#1c2736',
+ appContentBg: '#151c28',
+ appBorderColor: '#2b3750',
+ barBg: '#0e121b',
+ barTextColor: '#a2b0cd',
+ textColor: '#f2f4f8',
+ inputTextColor: '#ffffff',
+}
+
+export const light: ThemeVars = {
+ ...themes.light,
+ ...common,
+ appBg: '#fbfdff',
+ textColor: '#2b3750',
+ barTextColor: '#566e9f',
+ inputTextColor: '#2b3750',
+}
diff --git a/client/storybook/tsconfig.json b/client/storybook/tsconfig.json
new file mode 100644
index 00000000000..cdb52c96b58
--- /dev/null
+++ b/client/storybook/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "module": "commonjs",
+ "sourceRoot": "src",
+ "rootDir": ".",
+ "outDir": "./out",
+ "baseUrl": "./src",
+ "jsx": "react",
+ },
+ "references": [{ "path": "../shared" }],
+ "include": ["./src/**/*", "./*.ts"],
+}
diff --git a/dev/foreach-ts-project.sh b/dev/foreach-ts-project.sh
index 47cdb8acf81..a3d45577b65 100755
--- a/dev/foreach-ts-project.sh
+++ b/dev/foreach-ts-project.sh
@@ -19,6 +19,7 @@ DIRS=(
client/extension-api
client/eslint-plugin-sourcegraph
client/extension-api-types
+ client/storybook
dev/release
dev/ts-morph
)
diff --git a/jest.config.js b/jest.config.js
index dfc3937861b..ec2f3eb23da 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -11,6 +11,6 @@ module.exports = {
'client/branded/jest.config.js',
'client/web/jest.config.js',
'client/wildcard/jest.config.js',
- '.storybook/jest.config.js',
+ 'client/storybook/jest.config.js',
],
}
diff --git a/package.json b/package.json
index 11222ff0d38..069a974d4b2 100644
--- a/package.json
+++ b/package.json
@@ -27,9 +27,9 @@
"cover-integration": "nyc --hook-require=false yarn test-integration",
"test-e2e": "TS_NODE_PROJECT=client/web/src/end-to-end/tsconfig.json mocha ./client/web/src/end-to-end/end-to-end.test.ts",
"cover-e2e": "nyc --hook-require=false --silent=true yarn test-e2e",
- "storybook": "start-storybook -p 9001 -c .storybook -s ui/assets",
- "build-storybook": "build-storybook -c .storybook -s ui/assets",
- "cover-storybook": "nyc --hook-require=false yarn jest .storybook/coverage",
+ "storybook": "yarn workspace @sourcegraph/storybook run start",
+ "build-storybook": "yarn workspace @sourcegraph/storybook run build",
+ "cover-storybook": "nyc --hook-require=false yarn jest client/storybook/src/coverage",
"deduplicate": "yarn-deduplicate -s fewer",
"release": "cd dev/release && yarn run release",
"docsite:serve": "./dev/docsite.sh -config doc/docsite.json serve -http=localhost:5080",
@@ -58,7 +58,8 @@
"exclude": [
"node_modules",
"**/*.d.ts",
- "**/*.@(test|story).ts?(x)"
+ "**/*.@(test|story).ts?(x)",
+ "client/storybook"
]
},
"jscpd": {
@@ -175,6 +176,7 @@
"@types/sinon": "9.0.4",
"@types/socket.io": "2.1.10",
"@types/socket.io-client": "1.4.33",
+ "@types/terser-webpack-plugin": "^4.2.0",
"@types/testing-library__jest-dom": "^5.9.5",
"@types/textarea-caret": "3.0.0",
"@types/uuid": "8.0.1",
diff --git a/tsconfig.all.json b/tsconfig.all.json
index a7d583987e6..125744fdc08 100644
--- a/tsconfig.all.json
+++ b/tsconfig.all.json
@@ -13,6 +13,7 @@
{ "path": "client/browser/src/end-to-end" },
{ "path": "client/extension-api" },
{ "path": "client/extension-api-types" },
+ { "path": "client/storybook" },
{ "path": "dev/release" },
{ "path": "schema" },
],
diff --git a/yarn.lock b/yarn.lock
index 14d1eb02d31..39d62fd84d1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4373,6 +4373,14 @@
resolved "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.6.tgz#a9ca4b70a18b270ccb2bc0aaafefd1d486b7ea74"
integrity sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA==
+"@types/terser-webpack-plugin@^4.2.0":
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/@types/terser-webpack-plugin/-/terser-webpack-plugin-4.2.0.tgz#fe39917d334287c5cf25abcf370867a31ed59cd6"
+ integrity sha512-oGfGZzjwKY7s8gAYLZJuVuu9GXuc/ACo7bL/DQg7ROFkEMFQULB1W7qZjQrTXf2SkTfQx7/zcerfuLkUCVFGhg==
+ dependencies:
+ "@types/webpack" "*"
+ terser "^4.6.13"
+
"@types/testing-library__jest-dom@^5.9.1", "@types/testing-library__jest-dom@^5.9.5":
version "5.9.5"
resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.5.tgz#5bf25c91ad2d7b38f264b12275e5c92a66d849b0"
@@ -21129,7 +21137,7 @@ terser-webpack-plugin@^4.2.3:
terser "^5.3.4"
webpack-sources "^1.4.3"
-terser@^4.1.2, terser@^4.6.3, terser@^4.8.0:
+terser@^4.1.2, terser@^4.6.13, terser@^4.6.3, terser@^4.8.0:
version "4.8.0"
resolved "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17"
integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==