Add @storybook/nextjs-vite package

This commit is contained in:
Valentin Palkovic 2024-08-04 00:25:58 +02:00
parent b94ead30bd
commit 2528064a2f
78 changed files with 4918 additions and 1 deletions

View File

@ -11,6 +11,7 @@ export const frameworkToRenderer: Record<
'html-vite': 'html',
'html-webpack5': 'html',
nextjs: 'react',
'nextjs-vite': 'react',
'preact-vite': 'preact',
'preact-webpack5': 'preact',
qwik: 'qwik',

View File

@ -44,6 +44,7 @@ export default {
'@storybook/html-vite': '8.3.0-alpha.3',
'@storybook/html-webpack5': '8.3.0-alpha.3',
'@storybook/nextjs': '8.3.0-alpha.3',
'@storybook/nextjs-vite': '8.3.0-alpha.3',
'@storybook/preact-vite': '8.3.0-alpha.3',
'@storybook/preact-webpack5': '8.3.0-alpha.3',
'@storybook/react-vite': '8.3.0-alpha.3',

View File

@ -5,6 +5,7 @@ export type SupportedFrameworks =
| 'html-vite'
| 'html-webpack5'
| 'nextjs'
| 'nextjs-vite'
| 'preact-vite'
| 'preact-webpack5'
| 'react-vite'

View File

@ -0,0 +1,23 @@
{
"rules": {
"global-require": "off",
"no-param-reassign": "off",
"import/no-dynamic-require": "off",
"import/no-unresolved": "off"
},
"overrides": [
{
"files": ["**/*.stories.@(jsx|tsx)"],
"rules": {
"react/no-unknown-property": "off",
"jsx-a11y/anchor-is-valid": "off"
}
},
{
"files": ["**/*.compat.@(tsx|ts)"],
"rules": {
"local-rules/no-uncategorized-errors": "off"
}
}
]
}

View File

@ -0,0 +1,10 @@
# Storybook for Next.js with Vite Builder
See [documentation](https://storybook.js.org/docs/get-started/frameworks/nextjs?renderer=react) for installation instructions, usage examples, APIs, and more.
## Acknowledgements
This framework borrows heavily from these Storybook addons:
- [storybook-addon-next](https://github.com/RyanClementsHax/storybook-addon-next) by [RyanClementsHax](https://github.com/RyanClementsHax/)
- [storybook-addon-next-router](https://github.com/lifeiscontent/storybook-addon-next-router) by [lifeiscontent](https://github.com/lifeiscontent)

View File

@ -0,0 +1,143 @@
{
"name": "@storybook/nextjs-vite",
"version": "8.3.0-alpha.3",
"description": "Storybook for Next.js and Vite",
"keywords": [
"storybook",
"nextjs",
"vite"
],
"homepage": "https://github.com/storybookjs/storybook/tree/next/code/frameworks/nextjs-vite",
"bugs": {
"url": "https://github.com/storybookjs/storybook/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/storybookjs/storybook.git",
"directory": "code/frameworks/nextjs"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"license": "MIT",
"exports": {
".": {
"types": "./dist/index.d.ts",
"node": "./dist/index.js",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
},
"./preset": {
"types": "./dist/preset.d.ts",
"require": "./dist/preset.js"
},
"./dist/preview.mjs": "./dist/preview.mjs",
"./cache.mock": {
"types": "./dist/export-mocks/cache/index.d.ts",
"import": "./dist/export-mocks/cache/index.mjs",
"require": "./dist/export-mocks/cache/index.js"
},
"./headers.mock": {
"types": "./dist/export-mocks/headers/index.d.ts",
"import": "./dist/export-mocks/headers/index.mjs",
"require": "./dist/export-mocks/headers/index.js"
},
"./navigation.mock": {
"types": "./dist/export-mocks/navigation/index.d.ts",
"import": "./dist/export-mocks/navigation/index.mjs",
"require": "./dist/export-mocks/navigation/index.js"
},
"./router.mock": {
"types": "./dist/export-mocks/router/index.d.ts",
"import": "./dist/export-mocks/router/index.mjs",
"require": "./dist/export-mocks/router/index.js"
},
"./package.json": "./package.json"
},
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"typesVersions": {
"*": {
"*": [
"dist/index.d.ts"
],
"cache.mock": [
"dist/export-mocks/cache/index.d.ts"
],
"headers.mock": [
"dist/export-mocks/headers/index.d.ts"
],
"router.mock": [
"dist/export-mocks/router/index.d.ts"
],
"navigation.mock": [
"dist/export-mocks/navigation/index.d.ts"
]
}
},
"files": [
"dist/**/*",
"template/cli/**/*",
"README.md",
"*.js",
"*.d.ts",
"!src/**/*"
],
"scripts": {
"check": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/check.ts",
"prep": "node --loader ../../../scripts/node_modules/esbuild-register/loader.js -r ../../../scripts/node_modules/esbuild-register/register.js ../../../scripts/prepare/bundle.ts"
},
"dependencies": {
"@storybook/builder-vite": "workspace:*",
"@storybook/react": "workspace:*",
"@storybook/test": "workspace:*",
"styled-jsx": "5.1.6"
},
"devDependencies": {
"@types/node": "^18.0.0",
"next": "^14.2.5",
"typescript": "^5.3.2",
"vite-plugin-storybook-nextjs": "^0.0.13"
},
"peerDependencies": {
"next": "^14.2.5",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta",
"storybook": "workspace:^",
"vite": "^5.0.0",
"vite-plugin-storybook-nextjs": "^0.0.13"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
},
"optionalDependencies": {
"sharp": "^0.33.3"
},
"engines": {
"node": ">=18.0.0"
},
"publishConfig": {
"access": "public"
},
"bundler": {
"entries": [
"./src/index.ts",
"./src/preset.ts",
"./src/preview.tsx",
"./src/export-mocks/cache/index.ts",
"./src/export-mocks/headers/index.ts",
"./src/export-mocks/router/index.ts",
"./src/export-mocks/navigation/index.ts",
"./src/images/decorator.tsx"
],
"externals": [
"sb-original/image-context"
],
"platform": "node"
},
"gitHead": "e6a7fd8a655c69780bc20b9749c2699e44beae16"
}

View File

@ -0,0 +1 @@
module.exports = require('./dist/preset');

View File

@ -0,0 +1,8 @@
{
"name": "nextjs-vite",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"targets": {
"build": {}
}
}

View File

@ -0,0 +1,4 @@
import { setConfig } from 'next/config';
// eslint-disable-next-line no-underscore-dangle
setConfig(process.env.__NEXT_RUNTIME_CONFIG);

View File

@ -0,0 +1,23 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { fn } from '@storybook/test';
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
type Callback = (...args: any[]) => Promise<any>;
// mock utilities/overrides (as of Next v14.2.0)
const revalidatePath = fn().mockName('next/cache::revalidatePath');
const revalidateTag = fn().mockName('next/cache::revalidateTag');
const unstable_cache = fn()
.mockName('next/cache::unstable_cache')
.mockImplementation((cb: Callback) => cb);
const unstable_noStore = fn().mockName('next/cache::unstable_noStore');
const cacheExports = {
unstable_cache,
revalidateTag,
revalidatePath,
unstable_noStore,
};
export default cacheExports;
export { unstable_cache, revalidateTag, revalidatePath, unstable_noStore };

View File

@ -0,0 +1,38 @@
import { fn } from '@storybook/test';
import { RequestCookies } from 'next/dist/compiled/@edge-runtime/cookies';
// We need this import to be a singleton, and because it's used in multiple entrypoints
// both in ESM and CJS, importing it via the package name instead of having a local import
// is the only way to achieve it actually being a singleton
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore we must ignore types here as during compilation they are not generated yet
import { headers } from '@storybook/nextjs/headers.mock';
class RequestCookiesMock extends RequestCookies {
get = fn(super.get.bind(this)).mockName('next/headers::cookies().get');
getAll = fn(super.getAll.bind(this)).mockName('next/headers::cookies().getAll');
has = fn(super.has.bind(this)).mockName('next/headers::cookies().has');
set = fn(super.set.bind(this)).mockName('next/headers::cookies().set');
delete = fn(super.delete.bind(this)).mockName('next/headers::cookies().delete');
}
let requestCookiesMock: RequestCookiesMock;
export const cookies = fn(() => {
if (!requestCookiesMock) {
requestCookiesMock = new RequestCookiesMock(headers());
}
return requestCookiesMock;
}).mockName('next/headers::cookies()');
const originalRestore = cookies.mockRestore.bind(null);
// will be called automatically by the test loader
cookies.mockRestore = () => {
originalRestore();
headers.mockRestore();
requestCookiesMock = new RequestCookiesMock(headers());
};

View File

@ -0,0 +1,39 @@
import { fn } from '@storybook/test';
import { HeadersAdapter } from 'next/dist/server/web/spec-extension/adapters/headers';
class HeadersAdapterMock extends HeadersAdapter {
constructor() {
super({});
}
append = fn(super.append.bind(this)).mockName('next/headers::headers().append');
delete = fn(super.delete.bind(this)).mockName('next/headers::headers().delete');
get = fn(super.get.bind(this)).mockName('next/headers::headers().get');
has = fn(super.has.bind(this)).mockName('next/headers::headers().has');
set = fn(super.set.bind(this)).mockName('next/headers::headers().set');
forEach = fn(super.forEach.bind(this)).mockName('next/headers::headers().forEach');
entries = fn(super.entries.bind(this)).mockName('next/headers::headers().entries');
keys = fn(super.keys.bind(this)).mockName('next/headers::headers().keys');
values = fn(super.values.bind(this)).mockName('next/headers::headers().values');
}
let headersAdapterMock: HeadersAdapterMock;
export const headers = () => {
if (!headersAdapterMock) headersAdapterMock = new HeadersAdapterMock();
return headersAdapterMock;
};
// This fn is called by ./cookies to restore the headers in the right order
headers.mockRestore = () => {
headersAdapterMock = new HeadersAdapterMock();
};

View File

@ -0,0 +1,13 @@
import { fn } from '@storybook/test';
import * as originalHeaders from 'next/dist/client/components/headers';
// re-exports of the actual module
export * from 'next/dist/client/components/headers';
// mock utilities/overrides (as of Next v14.2.0)
export { headers } from './headers';
export { cookies } from './cookies';
// passthrough mocks - keep original implementation but allow for spying
const draftMode = fn(originalHeaders.draftMode).mockName('draftMode');
export { draftMode };

View File

@ -0,0 +1,94 @@
import type { Mock } from '@storybook/test';
import { fn } from '@storybook/test';
import * as actual from 'next/dist/client/components/navigation';
import { NextjsRouterMocksNotAvailable } from 'storybook/internal/preview-errors';
import { RedirectStatusCode } from 'next/dist/client/components/redirect-status-code';
import { getRedirectError } from 'next/dist/client/components/redirect';
let navigationAPI: {
push: Mock;
replace: Mock;
forward: Mock;
back: Mock;
prefetch: Mock;
refresh: Mock;
};
/**
* Creates a next/navigation router API mock. Used internally.
* @ignore
* @internal
* */
export const createNavigation = (overrides: any) => {
const navigationActions = {
push: fn().mockName('next/navigation::useRouter().push'),
replace: fn().mockName('next/navigation::useRouter().replace'),
forward: fn().mockName('next/navigation::useRouter().forward'),
back: fn().mockName('next/navigation::useRouter().back'),
prefetch: fn().mockName('next/navigation::useRouter().prefetch'),
refresh: fn().mockName('next/navigation::useRouter().refresh'),
};
if (overrides) {
Object.keys(navigationActions).forEach((key) => {
if (key in overrides) {
(navigationActions as any)[key] = fn((...args: any[]) => {
return (overrides as any)[key](...args);
}).mockName(`useRouter().${key}`);
}
});
}
navigationAPI = navigationActions;
return navigationAPI;
};
export const getRouter = () => {
if (!navigationAPI) {
throw new NextjsRouterMocksNotAvailable({
importType: 'next/navigation',
});
}
return navigationAPI;
};
// re-exports of the actual module
export * from 'next/dist/client/components/navigation';
// mock utilities/overrides (as of Next v14.2.0)
export const redirect = fn(
(url: string, type: actual.RedirectType = actual.RedirectType.push): never => {
throw getRedirectError(url, type, RedirectStatusCode.SeeOther);
}
).mockName('next/navigation::redirect');
export const permanentRedirect = fn(
(url: string, type: actual.RedirectType = actual.RedirectType.push): never => {
throw getRedirectError(url, type, RedirectStatusCode.SeeOther);
}
).mockName('next/navigation::permanentRedirect');
// passthrough mocks - keep original implementation but allow for spying
export const useSearchParams = fn(actual.useSearchParams).mockName(
'next/navigation::useSearchParams'
);
export const usePathname = fn(actual.usePathname).mockName('next/navigation::usePathname');
export const useSelectedLayoutSegment = fn(actual.useSelectedLayoutSegment).mockName(
'next/navigation::useSelectedLayoutSegment'
);
export const useSelectedLayoutSegments = fn(actual.useSelectedLayoutSegments).mockName(
'next/navigation::useSelectedLayoutSegments'
);
export const useRouter = fn(actual.useRouter).mockName('next/navigation::useRouter');
export const useServerInsertedHTML = fn(actual.useServerInsertedHTML).mockName(
'next/navigation::useServerInsertedHTML'
);
export const notFound = fn(actual.notFound).mockName('next/navigation::notFound');
// Params, not exported by Next.js, is manually declared to avoid inference issues.
interface Params {
[key: string]: string | string[];
}
export const useParams = fn<[], Params>(actual.useParams).mockName('next/navigation::useParams');

View File

@ -0,0 +1,115 @@
import type { Mock } from '@storybook/test';
import { fn } from '@storybook/test';
import { NextjsRouterMocksNotAvailable } from 'storybook/internal/preview-errors';
import type { NextRouter, SingletonRouter } from 'next/router';
import singletonRouter, * as originalRouter from 'next/dist/client/router';
const defaultRouterState = {
route: '/',
asPath: '/',
basePath: '/',
pathname: '/',
query: {},
isFallback: false,
isLocaleDomain: false,
isReady: true,
isPreview: false,
};
let routerAPI: {
push: Mock;
replace: Mock;
reload: Mock;
back: Mock;
forward: Mock;
prefetch: Mock;
beforePopState: Mock;
events: {
on: Mock;
off: Mock;
emit: Mock;
};
} & typeof defaultRouterState;
/**
* Creates a next/router router API mock. Used internally.
* @ignore
* @internal
* */
export const createRouter = (overrides: Partial<NextRouter>) => {
const routerActions: Partial<NextRouter> = {
push: fn((..._args: any[]) => {
return Promise.resolve(true);
}).mockName('next/router::useRouter().push'),
replace: fn((..._args: any[]) => {
return Promise.resolve(true);
}).mockName('next/router::useRouter().replace'),
reload: fn((..._args: any[]) => {}).mockName('next/router::useRouter().reload'),
back: fn((..._args: any[]) => {}).mockName('next/router::useRouter().back'),
forward: fn(() => {}).mockName('next/router::useRouter().forward'),
prefetch: fn((..._args: any[]) => {
return Promise.resolve();
}).mockName('next/router::useRouter().prefetch'),
beforePopState: fn((..._args: any[]) => {}).mockName('next/router::useRouter().beforePopState'),
};
const routerEvents: NextRouter['events'] = {
on: fn((..._args: any[]) => {}).mockName('next/router::useRouter().events.on'),
off: fn((..._args: any[]) => {}).mockName('next/router::useRouter().events.off'),
emit: fn((..._args: any[]) => {}).mockName('next/router::useRouter().events.emit'),
};
if (overrides) {
Object.keys(routerActions).forEach((key) => {
if (key in overrides) {
(routerActions as any)[key] = fn((...args: any[]) => {
return (overrides as any)[key](...args);
}).mockName(`useRouter().${key}`);
}
});
}
if (overrides?.events) {
Object.keys(routerEvents).forEach((key) => {
if (key in routerEvents) {
(routerEvents as any)[key] = fn((...args: any[]) => {
return (overrides.events as any)[key](...args);
}).mockName(`useRouter().events.${key}`);
}
});
}
routerAPI = {
...defaultRouterState,
...overrides,
...routerActions,
// @ts-expect-error TODO improve typings
events: routerEvents,
};
// overwrite the singleton router from next/router
(singletonRouter as unknown as SingletonRouter).router = routerAPI as any;
(singletonRouter as unknown as SingletonRouter).readyCallbacks.forEach((cb) => cb());
(singletonRouter as unknown as SingletonRouter).readyCallbacks = [];
return routerAPI as unknown as NextRouter;
};
export const getRouter = () => {
if (!routerAPI) {
throw new NextjsRouterMocksNotAvailable({
importType: 'next/router',
});
}
return routerAPI;
};
// re-exports of the actual module
export * from 'next/dist/client/router';
export default singletonRouter;
// mock utilities/overrides (as of Next v14.2.0)
// passthrough mocks - keep original implementation but allow for spying
export const useRouter = fn(originalRouter.useRouter).mockName('next/router::useRouter');
export const withRouter = fn(originalRouter.withRouter).mockName('next/router::withRouter');

View File

@ -0,0 +1,10 @@
import * as React from 'react';
import HeadManagerProvider from './head-manager-provider';
export const HeadManagerDecorator = (Story: React.FC): React.ReactNode => {
return (
<HeadManagerProvider>
<Story />
</HeadManagerProvider>
);
};

View File

@ -0,0 +1,23 @@
import type { PropsWithChildren } from 'react';
import React, { useMemo } from 'react';
import { HeadManagerContext } from 'next/dist/shared/lib/head-manager-context.shared-runtime';
import initHeadManager from 'next/dist/client/head-manager';
type HeadManagerValue = {
updateHead?: ((state: JSX.Element[]) => void) | undefined;
mountedInstances?: Set<unknown>;
updateScripts?: ((state: any) => void) | undefined;
scripts?: any;
getIsSsr?: () => boolean;
appDir?: boolean | undefined;
nonce?: string | undefined;
};
const HeadManagerProvider: React.FC<PropsWithChildren> = ({ children }) => {
const headManager: HeadManagerValue = useMemo(initHeadManager, []);
headManager.getIsSsr = () => false;
return <HeadManagerContext.Provider value={headManager}>{children}</HeadManagerContext.Provider>;
};
export default HeadManagerProvider;

View File

@ -0,0 +1,19 @@
import * as React from 'react';
import type { Addon_StoryContext } from 'storybook/internal/types';
import { ImageContext } from 'sb-original/image-context';
export const ImageDecorator = (
Story: React.FC,
{ parameters }: Addon_StoryContext
): React.ReactNode => {
if (!parameters.nextjs?.image) {
return <Story />;
}
return (
<ImageContext.Provider value={parameters.nextjs.image}>
<Story />
</ImageContext.Provider>
);
};

View File

@ -0,0 +1,2 @@
export * from './types';
export * from './portable-stories';

View File

@ -0,0 +1,132 @@
import {
composeStory as originalComposeStory,
composeStories as originalComposeStories,
setProjectAnnotations as originalSetProjectAnnotations,
composeConfigs,
} from 'storybook/internal/preview-api';
import type {
Args,
ProjectAnnotations,
StoryAnnotationsOrFn,
Store_CSFExports,
StoriesWithPartialProps,
NamedOrDefaultProjectAnnotations,
ComposedStoryFn,
} from 'storybook/internal/types';
// ! ATTENTION: This needs to be a relative import so it gets prebundled. This is to avoid ESM issues in Nextjs + Jest setups
import { INTERNAL_DEFAULT_PROJECT_ANNOTATIONS as reactAnnotations } from '../../../renderers/react/src/portable-stories';
import * as rscAnnotations from '../../../renderers/react/src/entry-preview-rsc';
import * as nextJsAnnotations from './preview';
import type { ReactRenderer, Meta } from '@storybook/react';
/** Function that sets the globalConfig of your storybook. The global config is the preview module of your .storybook folder.
*
* It should be run a single time, so that your global config (e.g. decorators) is applied to your stories when using `composeStories` or `composeStory`.
*
* Example:
*```jsx
* // setup.js (for jest)
* import { setProjectAnnotations } from '@storybook/nextjs';
* import projectAnnotations from './.storybook/preview';
*
* setProjectAnnotations(projectAnnotations);
*```
*
* @param projectAnnotations - e.g. (import projectAnnotations from '../.storybook/preview')
*/
export function setProjectAnnotations(
projectAnnotations:
| NamedOrDefaultProjectAnnotations<ReactRenderer>
| NamedOrDefaultProjectAnnotations<ReactRenderer>[]
): ProjectAnnotations<ReactRenderer> {
return originalSetProjectAnnotations<ReactRenderer>(projectAnnotations);
}
// This will not be necessary once we have auto preset loading
const defaultProjectAnnotations: ProjectAnnotations<ReactRenderer> = composeConfigs([
reactAnnotations,
rscAnnotations,
nextJsAnnotations,
]);
/**
* Function that will receive a story along with meta (e.g. a default export from a .stories file)
* and optionally projectAnnotations e.g. (import * from '../.storybook/preview)
* and will return a composed component that has all args/parameters/decorators/etc combined and applied to it.
*
*
* It's very useful for reusing a story in scenarios outside of Storybook like unit testing.
*
* Example:
*```jsx
* import { render } from '@testing-library/react';
* import { composeStory } from '@storybook/nextjs';
* import Meta, { Primary as PrimaryStory } from './Button.stories';
*
* const Primary = composeStory(PrimaryStory, Meta);
*
* test('renders primary button with Hello World', () => {
* const { getByText } = render(<Primary>Hello world</Primary>);
* expect(getByText(/Hello world/i)).not.toBeNull();
* });
*```
*
* @param story
* @param componentAnnotations - e.g. (import Meta from './Button.stories')
* @param [projectAnnotations] - e.g. (import * as projectAnnotations from '../.storybook/preview') this can be applied automatically if you use `setProjectAnnotations` in your setup files.
* @param [exportsName] - in case your story does not contain a name and you want it to have a name.
*/
export function composeStory<TArgs extends Args = Args>(
story: StoryAnnotationsOrFn<ReactRenderer, TArgs>,
componentAnnotations: Meta<TArgs | any>,
projectAnnotations?: ProjectAnnotations<ReactRenderer>,
exportsName?: string
): ComposedStoryFn<ReactRenderer, Partial<TArgs>> {
return originalComposeStory<ReactRenderer, TArgs>(
story as StoryAnnotationsOrFn<ReactRenderer, Args>,
componentAnnotations,
projectAnnotations,
defaultProjectAnnotations,
exportsName
);
}
/**
* Function that will receive a stories import (e.g. `import * as stories from './Button.stories'`)
* and optionally projectAnnotations (e.g. `import * from '../.storybook/preview`)
* and will return an object containing all the stories passed, but now as a composed component that has all args/parameters/decorators/etc combined and applied to it.
*
*
* It's very useful for reusing stories in scenarios outside of Storybook like unit testing.
*
* Example:
*```jsx
* import { render } from '@testing-library/react';
* import { composeStories } from '@storybook/nextjs';
* import * as stories from './Button.stories';
*
* const { Primary, Secondary } = composeStories(stories);
*
* test('renders primary button with Hello World', () => {
* const { getByText } = render(<Primary>Hello world</Primary>);
* expect(getByText(/Hello world/i)).not.toBeNull();
* });
*```
*
* @param csfExports - e.g. (import * as stories from './Button.stories')
* @param [projectAnnotations] - e.g. (import * as projectAnnotations from '../.storybook/preview') this can be applied automatically if you use `setProjectAnnotations` in your setup files.
*/
export function composeStories<TModule extends Store_CSFExports<ReactRenderer, any>>(
csfExports: TModule,
projectAnnotations?: ProjectAnnotations<ReactRenderer>
) {
// @ts-expect-error (Converted from ts-ignore)
const composedStories = originalComposeStories(csfExports, projectAnnotations, composeStory);
return composedStories as unknown as Omit<
StoriesWithPartialProps<ReactRenderer, TModule>,
keyof Store_CSFExports
>;
}

View File

@ -0,0 +1,43 @@
// https://storybook.js.org/docs/react/addons/writing-presets
import type { StorybookConfigVite } from '@storybook/builder-vite';
import { dirname, join } from 'path';
import type { PresetProperty } from 'storybook/internal/types';
import vitePluginStorybookNextjs from 'vite-plugin-storybook-nextjs';
import type { StorybookConfig } from './types';
export const core: PresetProperty<'core'> = async (config, options) => {
const framework = await options.presets.apply('framework');
return {
...config,
builder: {
name: dirname(
require.resolve(join('@storybook/builder-vite', 'package.json'))
) as '@storybook/builder-vite',
options: {
...(typeof framework === 'string' ? {} : framework.options.builder || {}),
},
},
renderer: dirname(require.resolve(join('@storybook/react', 'package.json'))),
};
};
export const previewAnnotations: PresetProperty<'previewAnnotations'> = (entry = []) => {
const nextDir = dirname(require.resolve('@storybook/nextjs-vite/package.json'));
const result = [...entry, join(nextDir, 'dist/preview.mjs')];
return result;
};
export const viteFinal: StorybookConfigVite['viteFinal'] = async (config, options) => {
config.plugins = config.plugins || [];
const framework = (await options.presets.apply(
'framework',
{},
options
)) as StorybookConfig['framework'];
const nextAppDir = typeof framework !== 'string' ? framework.options.nextAppDir : undefined;
config.plugins.push(vitePluginStorybookNextjs({ dir: nextAppDir }));
return config;
};

View File

@ -0,0 +1,83 @@
import type { Addon_DecoratorFunction, Addon_LoaderFunction } from 'storybook/internal/types';
import './config/preview';
import { ImageDecorator } from './images/decorator';
import { RouterDecorator } from './routing/decorator';
import { StyledJsxDecorator } from './styledJsx/decorator';
import { HeadManagerDecorator } from './head-manager/decorator';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore we must ignore types here as during compilation they are not generated yet
import { createRouter } from '@storybook/nextjs-vite/router.mock';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore we must ignore types here as during compilation they are not generated yet
import { createNavigation } from '@storybook/nextjs-vite/navigation.mock';
import { isNextRouterError } from 'next/dist/client/components/is-next-router-error';
function addNextHeadCount() {
const meta = document.createElement('meta');
meta.name = 'next-head-count';
meta.content = '0';
document.head.appendChild(meta);
}
function isAsyncClientComponentError(error: unknown) {
return (
typeof error === 'string' &&
(error.includes('A component was suspended by an uncached promise.') ||
error.includes('async/await is not yet supported in Client Components'))
);
}
addNextHeadCount();
// Copying Next patch of console.error:
// https://github.com/vercel/next.js/blob/a74deb63e310df473583ab6f7c1783bc609ca236/packages/next/src/client/app-index.tsx#L15
const origConsoleError = globalThis.console.error;
globalThis.console.error = (...args: unknown[]) => {
const error = args[0];
if (isNextRouterError(error) || isAsyncClientComponentError(error)) {
return;
}
origConsoleError.apply(globalThis.console, args);
};
globalThis.addEventListener('error', (ev: WindowEventMap['error']): void => {
if (isNextRouterError(ev.error) || isAsyncClientComponentError(ev.error)) {
ev.preventDefault();
return;
}
});
export const decorators: Addon_DecoratorFunction<any>[] = [
StyledJsxDecorator,
ImageDecorator,
RouterDecorator,
HeadManagerDecorator,
];
export const loaders: Addon_LoaderFunction = async ({ globals, parameters }) => {
const { router, appDirectory } = parameters.nextjs ?? {};
if (appDirectory) {
createNavigation(router);
} else {
createRouter({
locale: globals.locale,
...router,
});
}
};
export const parameters = {
docs: {
source: {
excludeDecorators: true,
},
},
react: {
rootOptions: {
onCaughtError(error: unknown) {
if (isNextRouterError(error)) return;
console.error(error);
},
},
},
};

View File

@ -0,0 +1,112 @@
import React, { useMemo } from 'react';
import {
LayoutRouterContext,
AppRouterContext,
GlobalLayoutRouterContext,
} from 'next/dist/shared/lib/app-router-context.shared-runtime';
import {
PathnameContext,
SearchParamsContext,
PathParamsContext,
} from 'next/dist/shared/lib/hooks-client-context.shared-runtime';
import { type Params } from 'next/dist/shared/lib/router/utils/route-matcher';
import { PAGE_SEGMENT_KEY } from 'next/dist/shared/lib/segment';
import type { FlightRouterState } from 'next/dist/server/app-render/types';
import type { RouteParams } from './types';
// We need this import to be a singleton, and because it's used in multiple entrypoints
// both in ESM and CJS, importing it via the package name instead of having a local import
// is the only way to achieve it actually being a singleton
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore we must ignore types here as during compilation they are not generated yet
import { getRouter } from '@storybook/nextjs-vite/navigation.mock';
type AppRouterProviderProps = {
routeParams: RouteParams;
};
// Since Next 14.2.x
// https://github.com/vercel/next.js/pull/60708/files#diff-7b6239af735eba0c401e1a0db1a04dd4575c19a031934f02d128cf3ac813757bR106
function getSelectedParams(currentTree: FlightRouterState, params: Params = {}): Params {
const parallelRoutes = currentTree[1];
for (const parallelRoute of Object.values(parallelRoutes)) {
const segment = parallelRoute[0];
const isDynamicParameter = Array.isArray(segment);
const segmentValue = isDynamicParameter ? segment[1] : segment;
if (!segmentValue || segmentValue.startsWith(PAGE_SEGMENT_KEY)) continue;
// Ensure catchAll and optional catchall are turned into an array
const isCatchAll = isDynamicParameter && (segment[2] === 'c' || segment[2] === 'oc');
if (isCatchAll) {
params[segment[0]] = segment[1].split('/');
} else if (isDynamicParameter) {
params[segment[0]] = segment[1];
}
params = getSelectedParams(parallelRoute, params);
}
return params;
}
const getParallelRoutes = (segmentsList: Array<string>): FlightRouterState => {
const segment = segmentsList.shift();
if (segment) {
return [segment, { children: getParallelRoutes(segmentsList) }];
}
return [] as any;
};
export const AppRouterProvider: React.FC<React.PropsWithChildren<AppRouterProviderProps>> = ({
children,
routeParams,
}) => {
const { pathname, query, segments = [] } = routeParams;
const tree: FlightRouterState = [pathname, { children: getParallelRoutes([...segments]) }];
const pathParams = useMemo(() => {
return getSelectedParams(tree);
}, [tree]);
// https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/app-router.tsx#L436
return (
<PathParamsContext.Provider value={pathParams}>
<PathnameContext.Provider value={pathname}>
<SearchParamsContext.Provider value={new URLSearchParams(query)}>
<GlobalLayoutRouterContext.Provider
value={{
changeByServerResponse() {
// NOOP
},
buildId: 'storybook',
tree,
focusAndScrollRef: {
apply: false,
hashFragment: null,
segmentPaths: [tree],
onlyHashChange: false,
},
nextUrl: pathname,
}}
>
<AppRouterContext.Provider value={getRouter()}>
<LayoutRouterContext.Provider
value={{
childNodes: new Map(),
loading: null,
tree,
url: pathname,
}}
>
{children}
</LayoutRouterContext.Provider>
</AppRouterContext.Provider>
</GlobalLayoutRouterContext.Provider>
</SearchParamsContext.Provider>
</PathnameContext.Provider>
</PathParamsContext.Provider>
);
};

View File

@ -0,0 +1,48 @@
import * as React from 'react';
import type { Addon_StoryContext } from 'storybook/internal/types';
import { AppRouterProvider } from './app-router-provider';
import { PageRouterProvider } from './page-router-provider';
import type { RouteParams, NextAppDirectory } from './types';
import { RedirectBoundary } from 'next/dist/client/components/redirect-boundary';
const defaultRouterParams: RouteParams = {
pathname: '/',
query: {},
};
export const RouterDecorator = (
Story: React.FC,
{ parameters }: Addon_StoryContext
): React.ReactNode => {
const nextAppDirectory =
(parameters.nextjs?.appDirectory as NextAppDirectory | undefined) ?? false;
if (nextAppDirectory) {
if (!AppRouterProvider) {
return null;
}
return (
<AppRouterProvider
routeParams={{
...defaultRouterParams,
...parameters.nextjs?.navigation,
}}
>
{/*
The next.js RedirectBoundary causes flashing UI when used client side.
Possible use the implementation of the PR: https://github.com/vercel/next.js/pull/49439
Or wait for next to solve this on their side.
*/}
<RedirectBoundary>
<Story />
</RedirectBoundary>
</AppRouterProvider>
);
}
return (
<PageRouterProvider>
<Story />
</PageRouterProvider>
);
};

View File

@ -0,0 +1,13 @@
import { RouterContext } from 'next/dist/shared/lib/router-context.shared-runtime';
import type { PropsWithChildren } from 'react';
import React from 'react';
// We need this import to be a singleton, and because it's used in multiple entrypoints
// both in ESM and CJS, importing it via the package name instead of having a local import
// is the only way to achieve it actually being a singleton
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore we must ignore types here as during compilation they are not generated yet
import { getRouter } from '@storybook/nextjs-vite/router.mock';
export const PageRouterProvider: React.FC<PropsWithChildren> = ({ children }) => (
<RouterContext.Provider value={getRouter()}>{children}</RouterContext.Provider>
);

View File

@ -0,0 +1,7 @@
export type RouteParams = {
pathname: string;
query: Record<string, string>;
[key: string]: any;
};
export type NextAppDirectory = boolean;

View File

@ -0,0 +1,8 @@
import * as React from 'react';
import { StyleRegistry } from 'styled-jsx';
export const StyledJsxDecorator = (Story: React.FC): React.ReactNode => (
<StyleRegistry>
<Story />
</StyleRegistry>
);

View File

@ -0,0 +1,44 @@
import type {
CompatibleString,
StorybookConfig as StorybookConfigBase,
} from 'storybook/internal/types';
import type { StorybookConfigVite, BuilderOptions } from '@storybook/builder-vite';
type FrameworkName = CompatibleString<'@storybook/nextjs-vite'>;
type BuilderName = CompatibleString<'@storybook/builder-vite'>;
export type FrameworkOptions = {
/**
* The directory where the Next.js app is located.
* @default process.cwd()
*/
nextAppDir?: string;
builder?: BuilderOptions;
};
type StorybookConfigFramework = {
framework:
| FrameworkName
| {
name: FrameworkName;
options: FrameworkOptions;
};
core?: StorybookConfigBase['core'] & {
builder?:
| BuilderName
| {
name: BuilderName;
options: BuilderOptions;
};
};
}
/**
* The interface for Storybook configuration in `main.ts` files.
*/
export type StorybookConfig = Omit<
StorybookConfigBase,
keyof StorybookConfigVite | keyof StorybookConfigFramework
> &
StorybookConfigVite &
StorybookConfigFramework & {}

View File

@ -0,0 +1,32 @@
declare module 'sb-original/image-context' {
import type { StaticImport } from 'next/dist/shared/lib/get-img-props';
import type { Context } from 'next/dist/compiled/react';
import type { ImageProps } from 'next/image';
import type { ImageProps as LegacyImageProps } from 'next/legacy/image';
export const ImageContext: Context<
Partial<
Omit<ImageProps, 'src'> & {
src: string | StaticImport;
}
> &
Omit<LegacyImageProps, 'src'>
>;
}
declare module 'sb-original/default-loader' {
import type { ImageLoaderProps } from 'next/image';
export const defaultLoader: (props: ImageLoaderProps) => string;
}
declare module 'next/dist/compiled/react' {
import * as React from 'react';
export default React;
export type Context<T> = React.Context<T>;
export function createContext<T>(
// If you thought this should be optional, see
// https://github.com/DefinitelyTyped/DefinitelyTyped/pull/24509#issuecomment-382213106
defaultValue: T
): Context<T>;
}

View File

@ -0,0 +1,7 @@
{
"rules": {
"import/no-extraneous-dependencies": "off",
"import/extensions": "off",
"react/no-unknown-property": "off"
}
}

View File

@ -0,0 +1,54 @@
import React from 'react';
import PropTypes from 'prop-types';
import './button.css';
/**
* Primary UI component for user interaction
*/
export const Button = ({ primary, backgroundColor, size, label, ...props }) => {
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
return (
<button
type="button"
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
{...props}
>
{label}
<style jsx>{`
button {
background-color: ${backgroundColor};
}
`}</style>
</button>
);
};
Button.propTypes = {
/**
* Is this the principal call to action on the page?
*/
primary: PropTypes.bool,
/**
* What background color to use
*/
backgroundColor: PropTypes.string,
/**
* How large should the button be?
*/
size: PropTypes.oneOf(['small', 'medium', 'large']),
/**
* Button contents
*/
label: PropTypes.string.isRequired,
/**
* Optional click handler
*/
onClick: PropTypes.func,
};
Button.defaultProps = {
backgroundColor: null,
primary: false,
size: 'medium',
onClick: undefined,
};

View File

@ -0,0 +1,48 @@
import { fn } from '@storybook/test';
import { Button } from './Button';
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
export default {
title: 'Example/Button',
component: Button,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {
backgroundColor: { control: 'color' },
},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
args: { onClick: fn() },
};
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Primary = {
args: {
primary: true,
label: 'Button',
},
};
export const Secondary = {
args: {
label: 'Button',
},
};
export const Large = {
args: {
size: 'large',
label: 'Button',
},
};
export const Small = {
args: {
size: 'small',
label: 'Button',
},
};

View File

@ -0,0 +1,446 @@
import { Meta } from "@storybook/blocks";
import Image from "next/image";
import Github from "./assets/github.svg";
import Discord from "./assets/discord.svg";
import Youtube from "./assets/youtube.svg";
import Tutorials from "./assets/tutorials.svg";
import Styling from "./assets/styling.png";
import Context from "./assets/context.png";
import Assets from "./assets/assets.png";
import Docs from "./assets/docs.png";
import Share from "./assets/share.png";
import FigmaPlugin from "./assets/figma-plugin.png";
import Testing from "./assets/testing.png";
import Accessibility from "./assets/accessibility.png";
import Theming from "./assets/theming.png";
import AddonLibrary from "./assets/addon-library.png";
export const RightArrow = () => <svg
viewBox="0 0 14 14"
width="8px"
height="14px"
style={{
marginLeft: '4px',
display: 'inline-block',
shapeRendering: 'inherit',
verticalAlign: 'middle',
fill: 'currentColor',
'path fill': 'currentColor'
}}
>
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
</svg>
<Meta title="Configure your project" />
<div className="sb-container">
<div className='sb-section-title'>
# Configure your project
Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community.
</div>
<div className="sb-section">
<div className="sb-section-item">
<Image
src={Styling}
alt="A wall of logos representing different styling technologies"
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
/>
<h4 className="sb-section-item-heading">Add styling and CSS</h4>
<p className="sb-section-item-paragraph">Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.</p>
<a
href="https://storybook.js.org/docs/configure/styling-and-css/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-section-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Context}
alt="An abstraction representing the composition of data for a component"
/>
<h4 className="sb-section-item-heading">Provide context and mocking</h4>
<p className="sb-section-item-paragraph">Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.</p>
<a
href="https://storybook.js.org/docs/writing-stories/decorators/?renderer=react#context-for-mocking"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-section-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Assets}
alt="A representation of typography and image assets"
/>
<div>
<h4 className="sb-section-item-heading">Load assets and resources</h4>
<p className="sb-section-item-paragraph">To link static files (like fonts) to your projects and stories, use the
`staticDirs` configuration option to specify folders to load when
starting Storybook.</p>
<a
href="https://storybook.js.org/docs/configure/images-and-assets/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
</div>
</div>
</div>
<div className="sb-container">
<div className='sb-section-title'>
# Do more with Storybook
Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs.
</div>
<div className="sb-section">
<div className="sb-features-grid">
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Docs}
alt="A screenshot showing the autodocs tag being set, pointing a docs page being generated"
/>
<h4 className="sb-section-item-heading">Autodocs</h4>
<p className="sb-section-item-paragraph">Auto-generate living,
interactive reference documentation from your components and stories.</p>
<a
href="https://storybook.js.org/docs/writing-docs/autodocs/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Share}
alt="A browser window showing a Storybook being published to a chromatic.com URL"
/>
<h4 className="sb-section-item-heading">Publish to Chromatic</h4>
<p className="sb-section-item-paragraph">Publish your Storybook to review and collaborate with your entire team.</p>
<a
href="https://storybook.js.org/docs/sharing/publish-storybook/?renderer=react#publish-storybook-with-chromatic"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={FigmaPlugin}
alt="Windows showing the Storybook plugin in Figma"
/>
<h4 className="sb-section-item-heading">Figma Plugin</h4>
<p className="sb-section-item-paragraph">Embed your stories into Figma to cross-reference the design and live
implementation in one place.</p>
<a
href="https://storybook.js.org/docs/sharing/design-integrations/?renderer=react#embed-storybook-in-figma-with-the-plugin"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Testing}
alt="Screenshot of tests passing and failing"
/>
<h4 className="sb-section-item-heading">Testing</h4>
<p className="sb-section-item-paragraph">Use stories to test a component in all its variations, no matter how
complex.</p>
<a
href="https://storybook.js.org/docs/writing-tests/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Accessibility}
alt="Screenshot of accessibility tests passing and failing"
/>
<h4 className="sb-section-item-heading">Accessibility</h4>
<p className="sb-section-item-paragraph">Automatically test your components for a11y issues as you develop.</p>
<a
href="https://storybook.js.org/docs/writing-tests/accessibility-testing/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Theming}
alt="Screenshot of Storybook in light and dark mode"
/>
<h4 className="sb-section-item-heading">Theming</h4>
<p className="sb-section-item-paragraph">Theme Storybook's UI to personalize it to your project.</p>
<a
href="https://storybook.js.org/docs/configure/theming/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
</div>
</div>
</div>
<div className='sb-addon'>
<div className='sb-addon-text'>
<h4>Addons</h4>
<p className="sb-section-item-paragraph">Integrate your tools with Storybook to connect workflows.</p>
<a
href="https://storybook.js.org/addons/"
target="_blank"
>Discover all addons<RightArrow /></a>
</div>
<div className='sb-addon-img'>
<Image
width={650}
height={347}
src={AddonLibrary}
alt="Integrate your tools with Storybook to connect workflows."
/>
</div>
</div>
<div className="sb-section sb-socials">
<div className="sb-section-item">
<Image
width={32}
height={32}
layout="fixed"
src={Github}
alt="Github logo"
className="sb-explore-image"
/>
Join our contributors building the future of UI development.
<a
href="https://github.com/storybookjs/storybook"
target="_blank"
>Star on GitHub<RightArrow /></a>
</div>
<div className="sb-section-item">
<Image
width={33}
height={32}
layout="fixed"
src={Discord}
alt="Discord logo"
className="sb-explore-image"
/>
<div>
Get support and chat with frontend developers.
<a
href="https://discord.gg/storybook"
target="_blank"
>Join Discord server<RightArrow /></a>
</div>
</div>
<div className="sb-section-item">
<Image
width={32}
height={32}
layout="fixed"
src={Youtube}
alt="Youtube logo"
className="sb-explore-image"
/>
<div>
Watch tutorials, feature previews and interviews.
<a
href="https://www.youtube.com/@chromaticui"
target="_blank"
>Watch on YouTube<RightArrow /></a>
</div>
</div>
<div className="sb-section-item">
<Image
width={33}
height={32}
layout="fixed"
src={Tutorials}
alt="A book"
className="sb-explore-image"
/>
<p>Follow guided walkthroughs on for key workflows.</p>
<a
href="https://storybook.js.org/tutorials/"
target="_blank"
>Discover tutorials<RightArrow /></a>
</div>
</div>
<style>
{`
.sb-container {
margin-bottom: 48px;
}
.sb-section {
width: 100%;
display: flex;
flex-direction: row;
gap: 20px;
}
img {
object-fit: cover;
}
.sb-section-title {
margin-bottom: 32px;
}
.sb-section a:not(h1 a, h2 a, h3 a) {
font-size: 14px;
}
.sb-section-item, .sb-grid-item {
flex: 1;
display: flex;
flex-direction: column;
}
.sb-section-item-heading {
padding-top: 20px !important;
padding-bottom: 5px !important;
margin: 0 !important;
}
.sb-section-item-paragraph {
margin: 0;
padding-bottom: 10px;
}
.sb-chevron {
margin-left: 5px;
}
.sb-features-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 32px 20px;
}
.sb-socials {
display: grid;
grid-template-columns: repeat(4, 1fr);
}
.sb-socials p {
margin-bottom: 10px;
}
.sb-explore-image {
max-height: 32px;
align-self: flex-start;
}
.sb-addon {
width: 100%;
display: flex;
align-items: center;
position: relative;
background-color: #EEF3F8;
border-radius: 5px;
border: 1px solid rgba(0, 0, 0, 0.05);
background: #EEF3F8;
height: 180px;
margin-bottom: 48px;
overflow: hidden;
}
.sb-addon-text {
padding-left: 48px;
max-width: 240px;
}
.sb-addon-text h4 {
padding-top: 0px;
}
.sb-addon-img {
position: absolute;
left: 345px;
top: 0;
height: 100%;
width: 200%;
overflow: hidden;
}
.sb-addon-img img {
width: 650px;
transform: rotate(-15deg);
margin-left: 40px;
margin-top: -72px;
box-shadow: 0 0 1px rgba(255, 255, 255, 0);
backface-visibility: hidden;
}
@media screen and (max-width: 800px) {
.sb-addon-img {
left: 300px;
}
}
@media screen and (max-width: 600px) {
.sb-section {
flex-direction: column;
}
.sb-features-grid {
grid-template-columns: repeat(1, 1fr);
}
.sb-socials {
grid-template-columns: repeat(2, 1fr);
}
.sb-addon {
height: 280px;
align-items: flex-start;
padding-top: 32px;
overflow: hidden;
}
.sb-addon-text {
padding-left: 24px;
}
.sb-addon-img {
right: 0;
left: 0;
top: 130px;
bottom: 0;
overflow: hidden;
height: auto;
width: 124%;
}
.sb-addon-img img {
width: 1200px;
transform: rotate(-12deg);
margin-left: 0;
margin-top: 48px;
margin-bottom: -40px;
margin-left: -24px;
}
}
`}
</style>

View File

@ -0,0 +1,59 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from './Button';
import './header.css';
export const Header = ({ user, onLogin, onLogout, onCreateAccount }) => (
<header>
<div className="storybook-header">
<div>
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd">
<path
d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
fill="#FFF"
/>
<path
d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z"
fill="#555AB9"
/>
<path
d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z"
fill="#91BAF8"
/>
</g>
</svg>
<h1>Acme</h1>
</div>
<div>
{user ? (
<>
<span className="welcome">
Welcome, <b>{user.name}</b>!
</span>
<Button size="small" onClick={onLogout} label="Log out" />
</>
) : (
<>
<Button size="small" onClick={onLogin} label="Log in" />
<Button primary size="small" onClick={onCreateAccount} label="Sign up" />
</>
)}
</div>
</div>
</header>
);
Header.propTypes = {
user: PropTypes.shape({
name: PropTypes.string.isRequired,
}),
onLogin: PropTypes.func.isRequired,
onLogout: PropTypes.func.isRequired,
onCreateAccount: PropTypes.func.isRequired,
};
Header.defaultProps = {
user: null,
};

View File

@ -0,0 +1,29 @@
import { fn } from '@storybook/test';
import { Header } from './Header';
export default {
title: 'Example/Header',
component: Header,
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
layout: 'fullscreen',
},
args: {
onLogin: fn(),
onLogout: fn(),
onCreateAccount: fn(),
},
};
export const LoggedIn = {
args: {
user: {
name: 'Jane Doe',
},
},
};
export const LoggedOut = {
args: {},
};

View File

@ -0,0 +1,68 @@
import React from 'react';
import { Header } from './Header';
import './page.css';
export const Page = () => {
const [user, setUser] = React.useState();
return (
<article>
<Header
user={user}
onLogin={() => setUser({ name: 'Jane Doe' })}
onLogout={() => setUser(undefined)}
onCreateAccount={() => setUser({ name: 'Jane Doe' })}
/>
<section className="storybook-page">
<h2>Pages in Storybook</h2>
<p>
We recommend building UIs with a{' '}
<a href="https://componentdriven.org" target="_blank" rel="noopener noreferrer">
<strong>component-driven</strong>
</a>{' '}
process starting with atomic components and ending with pages.
</p>
<p>
Render pages with mock data. This makes it easy to build and review page states without
needing to navigate to them in your app. Here are some handy patterns for managing page
data in Storybook:
</p>
<ul>
<li>
Use a higher-level connected component. Storybook helps you compose such data from the
"args" of child component stories
</li>
<li>
Assemble data in the page component from your services. You can mock these services out
using Storybook.
</li>
</ul>
<p>
Get a guided tutorial on component-driven development at{' '}
<a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer">
Storybook tutorials
</a>
. Read more in the{' '}
<a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer">
docs
</a>
.
</p>
<div className="tip-wrapper">
<span className="tip">Tip</span> Adjust the width of the canvas with the{' '}
<svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd">
<path
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z"
id="a"
fill="#999"
/>
</g>
</svg>
Viewports addon in the toolbar
</div>
</section>
</article>
);
};

View File

@ -0,0 +1,27 @@
import { within, userEvent, expect } from '@storybook/test';
import { Page } from './Page';
export default {
title: 'Example/Page',
component: Page,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
layout: 'fullscreen',
},
};
export const LoggedOut = {};
// More on interaction testing: https://storybook.js.org/docs/writing-tests/interaction-testing
export const LoggedIn = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const loginButton = canvas.getByRole('button', { name: /Log in/i });
await expect(loginButton).toBeInTheDocument();
await userEvent.click(loginButton);
await expect(loginButton).not.toBeInTheDocument();
const logoutButton = canvas.getByRole('button', { name: /Log out/i });
await expect(logoutButton).toBeInTheDocument();
},
};

View File

@ -0,0 +1,52 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Button } from './Button';
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta: Meta<typeof Button> = {
title: 'Example/Button',
component: Button,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {
backgroundColor: { control: 'color' },
},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
args: { onClick: fn() },
};
export default meta;
type Story = StoryObj<typeof Button>;
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Primary: Story = {
args: {
primary: true,
label: 'Button',
},
};
export const Secondary: Story = {
args: {
label: 'Button',
},
};
export const Large: Story = {
args: {
size: 'large',
label: 'Button',
},
};
export const Small: Story = {
args: {
size: 'small',
label: 'Button',
},
};

View File

@ -0,0 +1,52 @@
import React from 'react';
import './button.css';
export interface ButtonProps {
/**
* Is this the principal call to action on the page?
*/
primary?: boolean;
/**
* What background color to use
*/
backgroundColor?: string;
/**
* How large should the button be?
*/
size?: 'small' | 'medium' | 'large';
/**
* Button contents
*/
label: string;
/**
* Optional click handler
*/
onClick?: () => void;
}
/**
* Primary UI component for user interaction
*/
export const Button = ({
primary = false,
size = 'medium',
backgroundColor,
label,
...props
}: ButtonProps) => {
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
return (
<button
type="button"
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
{...props}
>
{label}
<style jsx>{`
button {
background-color: ${backgroundColor};
}
`}</style>
</button>
);
};

View File

@ -0,0 +1,446 @@
import { Meta } from "@storybook/blocks";
import Image from "next/image";
import Github from "./assets/github.svg";
import Discord from "./assets/discord.svg";
import Youtube from "./assets/youtube.svg";
import Tutorials from "./assets/tutorials.svg";
import Styling from "./assets/styling.png";
import Context from "./assets/context.png";
import Assets from "./assets/assets.png";
import Docs from "./assets/docs.png";
import Share from "./assets/share.png";
import FigmaPlugin from "./assets/figma-plugin.png";
import Testing from "./assets/testing.png";
import Accessibility from "./assets/accessibility.png";
import Theming from "./assets/theming.png";
import AddonLibrary from "./assets/addon-library.png";
export const RightArrow = () => <svg
viewBox="0 0 14 14"
width="8px"
height="14px"
style={{
marginLeft: '4px',
display: 'inline-block',
shapeRendering: 'inherit',
verticalAlign: 'middle',
fill: 'currentColor',
'path fill': 'currentColor'
}}
>
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
</svg>
<Meta title="Configure your project" />
<div className="sb-container">
<div className='sb-section-title'>
# Configure your project
Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community.
</div>
<div className="sb-section">
<div className="sb-section-item">
<Image
src={Styling}
alt="A wall of logos representing different styling technologies"
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
/>
<h4 className="sb-section-item-heading">Add styling and CSS</h4>
<p className="sb-section-item-paragraph">Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.</p>
<a
href="https://storybook.js.org/docs/configure/styling-and-css/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-section-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Context}
alt="An abstraction representing the composition of data for a component"
/>
<h4 className="sb-section-item-heading">Provide context and mocking</h4>
<p className="sb-section-item-paragraph">Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.</p>
<a
href="https://storybook.js.org/docs/writing-stories/decorators/?renderer=react#context-for-mocking"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-section-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Assets}
alt="A representation of typography and image assets"
/>
<div>
<h4 className="sb-section-item-heading">Load assets and resources</h4>
<p className="sb-section-item-paragraph">To link static files (like fonts) to your projects and stories, use the
`staticDirs` configuration option to specify folders to load when
starting Storybook.</p>
<a
href="https://storybook.js.org/docs/configure/images-and-assets/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
</div>
</div>
</div>
<div className="sb-container">
<div className='sb-section-title'>
# Do more with Storybook
Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs.
</div>
<div className="sb-section">
<div className="sb-features-grid">
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Docs}
alt="A screenshot showing the autodocs tag being set, pointing a docs page being generated"
/>
<h4 className="sb-section-item-heading">Autodocs</h4>
<p className="sb-section-item-paragraph">Auto-generate living,
interactive reference documentation from your components and stories.</p>
<a
href="https://storybook.js.org/docs/writing-docs/autodocs/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Share}
alt="A browser window showing a Storybook being published to a chromatic.com URL"
/>
<h4 className="sb-section-item-heading">Publish to Chromatic</h4>
<p className="sb-section-item-paragraph">Publish your Storybook to review and collaborate with your entire team.</p>
<a
href="https://storybook.js.org/docs/sharing/publish-storybook/?renderer=react#publish-storybook-with-chromatic"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={FigmaPlugin}
alt="Windows showing the Storybook plugin in Figma"
/>
<h4 className="sb-section-item-heading">Figma Plugin</h4>
<p className="sb-section-item-paragraph">Embed your stories into Figma to cross-reference the design and live
implementation in one place.</p>
<a
href="https://storybook.js.org/docs/sharing/design-integrations/?renderer=react#embed-storybook-in-figma-with-the-plugin"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Testing}
alt="Screenshot of tests passing and failing"
/>
<h4 className="sb-section-item-heading">Testing</h4>
<p className="sb-section-item-paragraph">Use stories to test a component in all its variations, no matter how
complex.</p>
<a
href="https://storybook.js.org/docs/writing-tests/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Accessibility}
alt="Screenshot of accessibility tests passing and failing"
/>
<h4 className="sb-section-item-heading">Accessibility</h4>
<p className="sb-section-item-paragraph">Automatically test your components for a11y issues as you develop.</p>
<a
href="https://storybook.js.org/docs/writing-tests/accessibility-testing/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Theming}
alt="Screenshot of Storybook in light and dark mode"
/>
<h4 className="sb-section-item-heading">Theming</h4>
<p className="sb-section-item-paragraph">Theme Storybook's UI to personalize it to your project.</p>
<a
href="https://storybook.js.org/docs/configure/theming/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
</div>
</div>
</div>
<div className='sb-addon'>
<div className='sb-addon-text'>
<h4>Addons</h4>
<p className="sb-section-item-paragraph">Integrate your tools with Storybook to connect workflows.</p>
<a
href="https://storybook.js.org/addons/"
target="_blank"
>Discover all addons<RightArrow /></a>
</div>
<div className='sb-addon-img'>
<Image
width={650}
height={347}
src={AddonLibrary}
alt="Integrate your tools with Storybook to connect workflows."
/>
</div>
</div>
<div className="sb-section sb-socials">
<div className="sb-section-item">
<Image
width={32}
height={32}
layout="fixed"
src={Github}
alt="Github logo"
className="sb-explore-image"
/>
Join our contributors building the future of UI development.
<a
href="https://github.com/storybookjs/storybook"
target="_blank"
>Star on GitHub<RightArrow /></a>
</div>
<div className="sb-section-item">
<Image
width={33}
height={32}
layout="fixed"
src={Discord}
alt="Discord logo"
className="sb-explore-image"
/>
<div>
Get support and chat with frontend developers.
<a
href="https://discord.gg/storybook"
target="_blank"
>Join Discord server<RightArrow /></a>
</div>
</div>
<div className="sb-section-item">
<Image
width={32}
height={32}
layout="fixed"
src={Youtube}
alt="Youtube logo"
className="sb-explore-image"
/>
<div>
Watch tutorials, feature previews and interviews.
<a
href="https://www.youtube.com/@chromaticui"
target="_blank"
>Watch on YouTube<RightArrow /></a>
</div>
</div>
<div className="sb-section-item">
<Image
width={33}
height={32}
layout="fixed"
src={Tutorials}
alt="A book"
className="sb-explore-image"
/>
<p>Follow guided walkthroughs on for key workflows.</p>
<a
href="https://storybook.js.org/tutorials/"
target="_blank"
>Discover tutorials<RightArrow /></a>
</div>
</div>
<style>
{`
.sb-container {
margin-bottom: 48px;
}
.sb-section {
width: 100%;
display: flex;
flex-direction: row;
gap: 20px;
}
img {
object-fit: cover;
}
.sb-section-title {
margin-bottom: 32px;
}
.sb-section a:not(h1 a, h2 a, h3 a) {
font-size: 14px;
}
.sb-section-item, .sb-grid-item {
flex: 1;
display: flex;
flex-direction: column;
}
.sb-section-item-heading {
padding-top: 20px !important;
padding-bottom: 5px !important;
margin: 0 !important;
}
.sb-section-item-paragraph {
margin: 0;
padding-bottom: 10px;
}
.sb-chevron {
margin-left: 5px;
}
.sb-features-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 32px 20px;
}
.sb-socials {
display: grid;
grid-template-columns: repeat(4, 1fr);
}
.sb-socials p {
margin-bottom: 10px;
}
.sb-explore-image {
max-height: 32px;
align-self: flex-start;
}
.sb-addon {
width: 100%;
display: flex;
align-items: center;
position: relative;
background-color: #EEF3F8;
border-radius: 5px;
border: 1px solid rgba(0, 0, 0, 0.05);
background: #EEF3F8;
height: 180px;
margin-bottom: 48px;
overflow: hidden;
}
.sb-addon-text {
padding-left: 48px;
max-width: 240px;
}
.sb-addon-text h4 {
padding-top: 0px;
}
.sb-addon-img {
position: absolute;
left: 345px;
top: 0;
height: 100%;
width: 200%;
overflow: hidden;
}
.sb-addon-img img {
width: 650px;
transform: rotate(-15deg);
margin-left: 40px;
margin-top: -72px;
box-shadow: 0 0 1px rgba(255, 255, 255, 0);
backface-visibility: hidden;
}
@media screen and (max-width: 800px) {
.sb-addon-img {
left: 300px;
}
}
@media screen and (max-width: 600px) {
.sb-section {
flex-direction: column;
}
.sb-features-grid {
grid-template-columns: repeat(1, 1fr);
}
.sb-socials {
grid-template-columns: repeat(2, 1fr);
}
.sb-addon {
height: 280px;
align-items: flex-start;
padding-top: 32px;
overflow: hidden;
}
.sb-addon-text {
padding-left: 24px;
}
.sb-addon-img {
right: 0;
left: 0;
top: 130px;
bottom: 0;
overflow: hidden;
height: auto;
width: 124%;
}
.sb-addon-img img {
width: 1200px;
transform: rotate(-12deg);
margin-left: 0;
margin-top: 48px;
margin-bottom: -40px;
margin-left: -24px;
}
}
`}
</style>

View File

@ -0,0 +1,33 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Header } from './Header';
const meta: Meta<typeof Header> = {
title: 'Example/Header',
component: Header,
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
layout: 'fullscreen',
},
args: {
onLogin: fn(),
onLogout: fn(),
onCreateAccount: fn(),
},
};
export default meta;
type Story = StoryObj<typeof Header>;
export const LoggedIn: Story = {
args: {
user: {
name: 'Jane Doe',
},
},
};
export const LoggedOut: Story = {};

View File

@ -0,0 +1,56 @@
import React from 'react';
import { Button } from './Button';
import './header.css';
type User = {
name: string;
};
export interface HeaderProps {
user?: User;
onLogin?: () => void;
onLogout?: () => void;
onCreateAccount?: () => void;
}
export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => (
<header>
<div className="storybook-header">
<div>
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd">
<path
d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
fill="#FFF"
/>
<path
d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z"
fill="#555AB9"
/>
<path
d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z"
fill="#91BAF8"
/>
</g>
</svg>
<h1>Acme</h1>
</div>
<div>
{user ? (
<>
<span className="welcome">
Welcome, <b>{user.name}</b>!
</span>
<Button size="small" onClick={onLogout} label="Log out" />
</>
) : (
<>
<Button size="small" onClick={onLogin} label="Log in" />
<Button primary size="small" onClick={onCreateAccount} label="Sign up" />
</>
)}
</div>
</div>
</header>
);

View File

@ -0,0 +1,32 @@
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent, expect } from '@storybook/test';
import { Page } from './Page';
const meta: Meta<typeof Page> = {
title: 'Example/Page',
component: Page,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
layout: 'fullscreen',
},
};
export default meta;
type Story = StoryObj<typeof Page>;
export const LoggedOut: Story = {};
// More on interaction testing: https://storybook.js.org/docs/writing-tests/interaction-testing
export const LoggedIn: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const loginButton = canvas.getByRole('button', { name: /Log in/i });
await expect(loginButton).toBeInTheDocument();
await userEvent.click(loginButton);
await expect(loginButton).not.toBeInTheDocument();
const logoutButton = canvas.getByRole('button', { name: /Log out/i });
await expect(logoutButton).toBeInTheDocument();
},
};

View File

@ -0,0 +1,73 @@
import React from 'react';
import { Header } from './Header';
import './page.css';
type User = {
name: string;
};
export const Page: React.FC = () => {
const [user, setUser] = React.useState<User>();
return (
<article>
<Header
user={user}
onLogin={() => setUser({ name: 'Jane Doe' })}
onLogout={() => setUser(undefined)}
onCreateAccount={() => setUser({ name: 'Jane Doe' })}
/>
<section className="storybook-page">
<h2>Pages in Storybook</h2>
<p>
We recommend building UIs with a{' '}
<a href="https://componentdriven.org" target="_blank" rel="noopener noreferrer">
<strong>component-driven</strong>
</a>{' '}
process starting with atomic components and ending with pages.
</p>
<p>
Render pages with mock data. This makes it easy to build and review page states without
needing to navigate to them in your app. Here are some handy patterns for managing page
data in Storybook:
</p>
<ul>
<li>
Use a higher-level connected component. Storybook helps you compose such data from the
"args" of child component stories
</li>
<li>
Assemble data in the page component from your services. You can mock these services out
using Storybook.
</li>
</ul>
<p>
Get a guided tutorial on component-driven development at{' '}
<a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer">
Storybook tutorials
</a>
. Read more in the{' '}
<a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer">
docs
</a>
.
</p>
<div className="tip-wrapper">
<span className="tip">Tip</span> Adjust the width of the canvas with the{' '}
<svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd">
<path
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z"
id="a"
fill="#999"
/>
</g>
</svg>
Viewports addon in the toolbar
</div>
</section>
</article>
);
};

View File

@ -0,0 +1,52 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Button } from './Button';
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
title: 'Example/Button',
component: Button,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {
backgroundColor: { control: 'color' },
},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
args: { onClick: fn() },
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Primary: Story = {
args: {
primary: true,
label: 'Button',
},
};
export const Secondary: Story = {
args: {
label: 'Button',
},
};
export const Large: Story = {
args: {
size: 'large',
label: 'Button',
},
};
export const Small: Story = {
args: {
size: 'small',
label: 'Button',
},
};

View File

@ -0,0 +1,52 @@
import React from 'react';
import './button.css';
export interface ButtonProps {
/**
* Is this the principal call to action on the page?
*/
primary?: boolean;
/**
* What background color to use
*/
backgroundColor?: string;
/**
* How large should the button be?
*/
size?: 'small' | 'medium' | 'large';
/**
* Button contents
*/
label: string;
/**
* Optional click handler
*/
onClick?: () => void;
}
/**
* Primary UI component for user interaction
*/
export const Button = ({
primary = false,
size = 'medium',
backgroundColor,
label,
...props
}: ButtonProps) => {
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
return (
<button
type="button"
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
{...props}
>
{label}
<style jsx>{`
button {
background-color: ${backgroundColor};
}
`}</style>
</button>
);
};

View File

@ -0,0 +1,446 @@
import { Meta } from "@storybook/blocks";
import Image from "next/image";
import Github from "./assets/github.svg";
import Discord from "./assets/discord.svg";
import Youtube from "./assets/youtube.svg";
import Tutorials from "./assets/tutorials.svg";
import Styling from "./assets/styling.png";
import Context from "./assets/context.png";
import Assets from "./assets/assets.png";
import Docs from "./assets/docs.png";
import Share from "./assets/share.png";
import FigmaPlugin from "./assets/figma-plugin.png";
import Testing from "./assets/testing.png";
import Accessibility from "./assets/accessibility.png";
import Theming from "./assets/theming.png";
import AddonLibrary from "./assets/addon-library.png";
export const RightArrow = () => <svg
viewBox="0 0 14 14"
width="8px"
height="14px"
style={{
marginLeft: '4px',
display: 'inline-block',
shapeRendering: 'inherit',
verticalAlign: 'middle',
fill: 'currentColor',
'path fill': 'currentColor'
}}
>
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
</svg>
<Meta title="Configure your project" />
<div className="sb-container">
<div className='sb-section-title'>
# Configure your project
Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community.
</div>
<div className="sb-section">
<div className="sb-section-item">
<Image
src={Styling}
alt="A wall of logos representing different styling technologies"
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
/>
<h4 className="sb-section-item-heading">Add styling and CSS</h4>
<p className="sb-section-item-paragraph">Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.</p>
<a
href="https://storybook.js.org/docs/configure/styling-and-css/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-section-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Context}
alt="An abstraction representing the composition of data for a component"
/>
<h4 className="sb-section-item-heading">Provide context and mocking</h4>
<p className="sb-section-item-paragraph">Often when a story doesn't render, it's because your component is expecting a specific environment or context (like a theme provider) to be available.</p>
<a
href="https://storybook.js.org/docs/writing-stories/decorators/?renderer=react#context-for-mocking"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-section-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Assets}
alt="A representation of typography and image assets"
/>
<div>
<h4 className="sb-section-item-heading">Load assets and resources</h4>
<p className="sb-section-item-paragraph">To link static files (like fonts) to your projects and stories, use the
`staticDirs` configuration option to specify folders to load when
starting Storybook.</p>
<a
href="https://storybook.js.org/docs/configure/images-and-assets/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
</div>
</div>
</div>
<div className="sb-container">
<div className='sb-section-title'>
# Do more with Storybook
Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs.
</div>
<div className="sb-section">
<div className="sb-features-grid">
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Docs}
alt="A screenshot showing the autodocs tag being set, pointing a docs page being generated"
/>
<h4 className="sb-section-item-heading">Autodocs</h4>
<p className="sb-section-item-paragraph">Auto-generate living,
interactive reference documentation from your components and stories.</p>
<a
href="https://storybook.js.org/docs/writing-docs/autodocs/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Share}
alt="A browser window showing a Storybook being published to a chromatic.com URL"
/>
<h4 className="sb-section-item-heading">Publish to Chromatic</h4>
<p className="sb-section-item-paragraph">Publish your Storybook to review and collaborate with your entire team.</p>
<a
href="https://storybook.js.org/docs/sharing/publish-storybook/?renderer=react#publish-storybook-with-chromatic"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={FigmaPlugin}
alt="Windows showing the Storybook plugin in Figma"
/>
<h4 className="sb-section-item-heading">Figma Plugin</h4>
<p className="sb-section-item-paragraph">Embed your stories into Figma to cross-reference the design and live
implementation in one place.</p>
<a
href="https://storybook.js.org/docs/sharing/design-integrations/?renderer=react#embed-storybook-in-figma-with-the-plugin"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Testing}
alt="Screenshot of tests passing and failing"
/>
<h4 className="sb-section-item-heading">Testing</h4>
<p className="sb-section-item-paragraph">Use stories to test a component in all its variations, no matter how
complex.</p>
<a
href="https://storybook.js.org/docs/writing-tests/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Accessibility}
alt="Screenshot of accessibility tests passing and failing"
/>
<h4 className="sb-section-item-heading">Accessibility</h4>
<p className="sb-section-item-paragraph">Automatically test your components for a11y issues as you develop.</p>
<a
href="https://storybook.js.org/docs/writing-tests/accessibility-testing/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
<div className="sb-grid-item">
<Image
width={0}
height={0}
style={{ width: '100%', height: 'auto' }}
src={Theming}
alt="Screenshot of Storybook in light and dark mode"
/>
<h4 className="sb-section-item-heading">Theming</h4>
<p className="sb-section-item-paragraph">Theme Storybook's UI to personalize it to your project.</p>
<a
href="https://storybook.js.org/docs/configure/theming/?renderer=react"
target="_blank"
>Learn more<RightArrow /></a>
</div>
</div>
</div>
</div>
<div className='sb-addon'>
<div className='sb-addon-text'>
<h4>Addons</h4>
<p className="sb-section-item-paragraph">Integrate your tools with Storybook to connect workflows.</p>
<a
href="https://storybook.js.org/addons/"
target="_blank"
>Discover all addons<RightArrow /></a>
</div>
<div className='sb-addon-img'>
<Image
width={650}
height={347}
src={AddonLibrary}
alt="Integrate your tools with Storybook to connect workflows."
/>
</div>
</div>
<div className="sb-section sb-socials">
<div className="sb-section-item">
<Image
width={32}
height={32}
layout="fixed"
src={Github}
alt="Github logo"
className="sb-explore-image"
/>
Join our contributors building the future of UI development.
<a
href="https://github.com/storybookjs/storybook"
target="_blank"
>Star on GitHub<RightArrow /></a>
</div>
<div className="sb-section-item">
<Image
width={33}
height={32}
layout="fixed"
src={Discord}
alt="Discord logo"
className="sb-explore-image"
/>
<div>
Get support and chat with frontend developers.
<a
href="https://discord.gg/storybook"
target="_blank"
>Join Discord server<RightArrow /></a>
</div>
</div>
<div className="sb-section-item">
<Image
width={32}
height={32}
layout="fixed"
src={Youtube}
alt="Youtube logo"
className="sb-explore-image"
/>
<div>
Watch tutorials, feature previews and interviews.
<a
href="https://www.youtube.com/@chromaticui"
target="_blank"
>Watch on YouTube<RightArrow /></a>
</div>
</div>
<div className="sb-section-item">
<Image
width={33}
height={32}
layout="fixed"
src={Tutorials}
alt="A book"
className="sb-explore-image"
/>
<p>Follow guided walkthroughs on for key workflows.</p>
<a
href="https://storybook.js.org/tutorials/"
target="_blank"
>Discover tutorials<RightArrow /></a>
</div>
</div>
<style>
{`
.sb-container {
margin-bottom: 48px;
}
.sb-section {
width: 100%;
display: flex;
flex-direction: row;
gap: 20px;
}
img {
object-fit: cover;
}
.sb-section-title {
margin-bottom: 32px;
}
.sb-section a:not(h1 a, h2 a, h3 a) {
font-size: 14px;
}
.sb-section-item, .sb-grid-item {
flex: 1;
display: flex;
flex-direction: column;
}
.sb-section-item-heading {
padding-top: 20px !important;
padding-bottom: 5px !important;
margin: 0 !important;
}
.sb-section-item-paragraph {
margin: 0;
padding-bottom: 10px;
}
.sb-chevron {
margin-left: 5px;
}
.sb-features-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 32px 20px;
}
.sb-socials {
display: grid;
grid-template-columns: repeat(4, 1fr);
}
.sb-socials p {
margin-bottom: 10px;
}
.sb-explore-image {
max-height: 32px;
align-self: flex-start;
}
.sb-addon {
width: 100%;
display: flex;
align-items: center;
position: relative;
background-color: #EEF3F8;
border-radius: 5px;
border: 1px solid rgba(0, 0, 0, 0.05);
background: #EEF3F8;
height: 180px;
margin-bottom: 48px;
overflow: hidden;
}
.sb-addon-text {
padding-left: 48px;
max-width: 240px;
}
.sb-addon-text h4 {
padding-top: 0px;
}
.sb-addon-img {
position: absolute;
left: 345px;
top: 0;
height: 100%;
width: 200%;
overflow: hidden;
}
.sb-addon-img img {
width: 650px;
transform: rotate(-15deg);
margin-left: 40px;
margin-top: -72px;
box-shadow: 0 0 1px rgba(255, 255, 255, 0);
backface-visibility: hidden;
}
@media screen and (max-width: 800px) {
.sb-addon-img {
left: 300px;
}
}
@media screen and (max-width: 600px) {
.sb-section {
flex-direction: column;
}
.sb-features-grid {
grid-template-columns: repeat(1, 1fr);
}
.sb-socials {
grid-template-columns: repeat(2, 1fr);
}
.sb-addon {
height: 280px;
align-items: flex-start;
padding-top: 32px;
overflow: hidden;
}
.sb-addon-text {
padding-left: 24px;
}
.sb-addon-img {
right: 0;
left: 0;
top: 130px;
bottom: 0;
overflow: hidden;
height: auto;
width: 124%;
}
.sb-addon-img img {
width: 1200px;
transform: rotate(-12deg);
margin-left: 0;
margin-top: 48px;
margin-bottom: -40px;
margin-left: -24px;
}
}
`}
</style>

View File

@ -0,0 +1,32 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Header } from './Header';
const meta = {
title: 'Example/Header',
component: Header,
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
layout: 'fullscreen',
},
args: {
onLogin: fn(),
onLogout: fn(),
onCreateAccount: fn(),
},
} satisfies Meta<typeof Header>;
export default meta;
type Story = StoryObj<typeof meta>;
export const LoggedIn: Story = {
args: {
user: {
name: 'Jane Doe',
},
},
};
export const LoggedOut: Story = {};

View File

@ -0,0 +1,56 @@
import React from 'react';
import { Button } from './Button';
import './header.css';
type User = {
name: string;
};
export interface HeaderProps {
user?: User;
onLogin?: () => void;
onLogout?: () => void;
onCreateAccount?: () => void;
}
export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => (
<header>
<div className="storybook-header">
<div>
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd">
<path
d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
fill="#FFF"
/>
<path
d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z"
fill="#555AB9"
/>
<path
d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z"
fill="#91BAF8"
/>
</g>
</svg>
<h1>Acme</h1>
</div>
<div>
{user ? (
<>
<span className="welcome">
Welcome, <b>{user.name}</b>!
</span>
<Button size="small" onClick={onLogout} label="Log out" />
</>
) : (
<>
<Button size="small" onClick={onLogin} label="Log in" />
<Button primary size="small" onClick={onCreateAccount} label="Sign up" />
</>
)}
</div>
</div>
</header>
);

View File

@ -0,0 +1,32 @@
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent, expect } from '@storybook/test';
import { Page } from './Page';
const meta = {
title: 'Example/Page',
component: Page,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
layout: 'fullscreen',
},
} satisfies Meta<typeof Page>;
export default meta;
type Story = StoryObj<typeof meta>;
export const LoggedOut: Story = {};
// More on interaction testing: https://storybook.js.org/docs/writing-tests/interaction-testing
export const LoggedIn: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const loginButton = canvas.getByRole('button', { name: /Log in/i });
await expect(loginButton).toBeInTheDocument();
await userEvent.click(loginButton);
await expect(loginButton).not.toBeInTheDocument();
const logoutButton = canvas.getByRole('button', { name: /Log out/i });
await expect(logoutButton).toBeInTheDocument();
},
};

View File

@ -0,0 +1,73 @@
import React from 'react';
import { Header } from './Header';
import './page.css';
type User = {
name: string;
};
export const Page: React.FC = () => {
const [user, setUser] = React.useState<User>();
return (
<article>
<Header
user={user}
onLogin={() => setUser({ name: 'Jane Doe' })}
onLogout={() => setUser(undefined)}
onCreateAccount={() => setUser({ name: 'Jane Doe' })}
/>
<section className="storybook-page">
<h2>Pages in Storybook</h2>
<p>
We recommend building UIs with a{' '}
<a href="https://componentdriven.org" target="_blank" rel="noopener noreferrer">
<strong>component-driven</strong>
</a>{' '}
process starting with atomic components and ending with pages.
</p>
<p>
Render pages with mock data. This makes it easy to build and review page states without
needing to navigate to them in your app. Here are some handy patterns for managing page
data in Storybook:
</p>
<ul>
<li>
Use a higher-level connected component. Storybook helps you compose such data from the
"args" of child component stories
</li>
<li>
Assemble data in the page component from your services. You can mock these services out
using Storybook.
</li>
</ul>
<p>
Get a guided tutorial on component-driven development at{' '}
<a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer">
Storybook tutorials
</a>
. Read more in the{' '}
<a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer">
docs
</a>
.
</p>
<div className="tip-wrapper">
<span className="tip">Tip</span> Adjust the width of the canvas with the{' '}
<svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd">
<path
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z"
id="a"
fill="#999"
/>
</g>
</svg>
Viewports addon in the toolbar
</div>
</section>
</article>
);
};

View File

@ -0,0 +1,7 @@
// Reference necessary since Next.js 13.2.0, because types in `next/navigation` are not exported per default, but
// type references are dynamically created during Next.js start up.
// See https://github.com/vercel/next.js/commit/cdf1d52d9aed42d01a46539886a4bda14cb77a99
// for more insights.
/// <reference types="next" />
/// <reference types="next/navigation-types/navigation" />

View File

@ -0,0 +1,25 @@
import dynamic from 'next/dynamic';
import React, { Suspense } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
const DynamicComponent = dynamic(() => import('./DynamicImport'), {
ssr: false,
});
function Component() {
return (
<Suspense fallback="Loading...">
<DynamicComponent />
</Suspense>
);
}
const meta = {
component: Component,
} satisfies Meta<typeof DynamicComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};

View File

@ -0,0 +1,5 @@
import React from 'react';
export default function DynamicComponent() {
return <div>I am a dynamically loaded component</div>;
}

View File

@ -0,0 +1,28 @@
import type { Meta, StoryObj } from '@storybook/react';
import Font from './Font';
const meta = {
component: Font,
} satisfies Meta<typeof Font>;
export default meta;
type Story = StoryObj<typeof meta>;
export const WithClassName: Story = {
args: {
variant: 'className',
},
};
export const WithStyle: Story = {
args: {
variant: 'style',
},
};
export const WithVariable: Story = {
args: {
variant: 'variable',
},
};

View File

@ -0,0 +1,67 @@
import { Rubik_Puddles } from 'next/font/google';
import localFont from 'next/font/local';
import React from 'react';
const rubik = Rubik_Puddles({
subsets: ['latin'],
variable: '--font-latin-rubik',
weight: '400',
});
export const localRubikStorm = localFont({
src: '/fonts/RubikStorm-Regular.ttf',
variable: '--font-rubik-storm',
});
type FontProps = {
variant: 'className' | 'style' | 'variable';
};
export default function Font({ variant }: FontProps) {
switch (variant) {
case 'className':
return (
<div>
<h1 className={rubik.className}>Google Rubik Puddles</h1>
<h1 className={localRubikStorm.className}>Google Local Rubik Storm</h1>
</div>
);
case 'style':
return (
<div>
<h1 style={rubik.style}>Google Rubik Puddles</h1>
<h1 style={localRubikStorm.style}>Google Local Rubik Storm</h1>
</div>
);
case 'variable':
return (
<div>
<div className={rubik.variable}>
<h1
style={{
fontFamily: 'var(--font-latin-rubik)',
fontStyle: rubik.style.fontStyle,
fontWeight: rubik.style.fontWeight,
}}
>
Google Rubik Puddles
</h1>
</div>
<div className={localRubikStorm.variable}>
<h1
style={{
fontFamily: 'var(--font-rubik-storm)',
fontStyle: localRubikStorm.style.fontStyle,
fontWeight: localRubikStorm.style.fontWeight,
}}
>
Google Local Rubik Storm
</h1>
</div>
</div>
);
default:
return null;
}
}

View File

@ -0,0 +1,38 @@
import { type ImageProps, getImageProps } from 'next/image';
import React from 'react';
import type { Meta, StoryObj } from '@storybook/nextjs-vite';
import Accessibility from '../../assets/accessibility.svg';
import Testing from '../../assets/testing.png';
// referenced from https://nextjs.org/docs/pages/api-reference/components/image#theme-detection-picture
const Component = (props: Omit<ImageProps, 'src'>) => {
const {
props: { srcSet: dark },
} = getImageProps({ src: Accessibility, ...props });
const {
// capture rest on one to spread to img as default; it doesn't matter which barring art direction
props: { srcSet: light, ...rest },
} = getImageProps({ src: Testing, ...props });
return (
<picture>
<source media="(prefers-color-scheme: dark)" srcSet={dark} />
<source media="(prefers-color-scheme: light)" srcSet={light} />
<img {...rest} />
</picture>
);
};
const meta = {
component: Component,
args: {
alt: 'getImageProps Example',
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};

View File

@ -0,0 +1,38 @@
import Head from 'next/head';
import React from 'react';
import { waitFor, expect } from '@storybook/test';
import type { Meta } from '@storybook/react';
import type { StoryObj } from '@storybook/react';
function Component() {
return (
<div>
<Head>
<title>Next.js Head Title</title>
<meta property="og:title" content="My page title" key="title" />
</Head>
<Head>
<meta property="og:title" content="My new title" key="title" />
</Head>
<p>Hello world!</p>
</div>
);
}
const meta = {
component: Component,
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
play: async () => {
await waitFor(() => expect(document.title).toEqual('Next.js Head Title'));
await expect(document.querySelectorAll('meta[property="og:title"]')).toHaveLength(1);
await expect(document.querySelector('meta[property="og:title"]').content).toEqual(
'My new title'
);
},
};

View File

@ -0,0 +1,108 @@
import React, { useRef, useState } from 'react';
import Image from 'next/image';
import Accessibility from '../../assets/accessibility.svg';
import AvifImage from '../../assets/avif-test-image.avif';
import type { Meta, StoryObj } from '@storybook/react';
const meta = {
component: Image,
args: {
src: Accessibility,
alt: 'Accessibility',
},
} satisfies Meta<typeof Image>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Avif: Story = {
args: {
src: AvifImage,
alt: 'Avif Test Image',
},
};
export const BlurredPlaceholder: Story = {
args: {
placeholder: 'blur',
},
};
export const BlurredAbsolutePlaceholder: Story = {
args: {
src: 'https://storybook.js.org/images/placeholders/50x50.png',
width: 50,
height: 50,
blurDataURL:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAABP5JREFUWEeNlwtz2zgMhEGKsv/k9XFp82xe11763yOJVGcXC4m2czM3GYa0E2s/LACSTi+vv1czM/7CvPpqxY/ejPeS3khmFiPHOiVLHKaZi4ux4j18GpMlS6cALupEQBCKQM4BdnGzjIcJgs//QBxAPQAem55f3yL4PWJIdyCyhlMfPdYZot0cwj3Ayg/5JwHA13paen7pADphxr/n5VI8JQsHCCGQ3gVGLLsxQ3h/LYSn5383B05rwNOws3Z576LOTOduvwfrOd5FtVat4akx0uPTrw8BNuUz23vLsY7hmg7i4ipqM2saiAdruNuirh4ff0bNdb3Qy3vkvfAQwrkHoDxTTZtDCOKrC1bMEkdnsQh/PLyetOGHkXeRAgQAQ84efQcBdUhvFofoulpdm9WGGTA+AJEe7l+i37a2c371tCFKs5zzJjxQNBMi1im7OCudP2aNghJuzZaGdSMEZjpwf/t0UgNdg9DOyLGLnY0BUHlzwVtNDkgEQhBeKkb1tUDgQrq7frwAiIJi5BKAeIFgHk5mOpPzvgltOfcoK0Rrs7lWHwsgqtXarK3N0u23h5Ne8+3Cqxn5RYSMfHCAMgDAx4CBWlA9RAGw0GA/ol0gvFB4WjAvBAFUa83SzdUdAbYMqp28uHpxCRefxwAYhksAFBlthxCiXig+zT4TYqkC+Hq7OdAfJv8lPpZiZShWBBIuRP+jspDb2lwcDkzz7OLzbO/zvAHAoXTz5eYMQL0t2yHAiCFcfPY1QDwNFylA5bPoFpsV9fsEiMl8dhcc4PP1CYD3drYcBYdIKQrx0cbRxd2JHSDcQ297/vvoZ5smRC+AyV2AQ+nm03evge08Tyy4jGqXzWWEoIvTgXHU38pWiNgH4ixB/ukAcy/xycXfp4kwdAAAt399W+OCgMjxILQacxvRQ3gEwHgKUIr/rz53CuDFNyP/Eob4+/vEWkBq6AAA/HIi62n/Lk67Q7wDYQ0UpQB7hc54T4E6gACLTYxeAwB0YKZL6U4ATEGIBwCs7qPfQJCCHkCnoK50noJKcXcAojsEAJZZKXhgCoziGKxqWV8IMNp4kP2aC+oB0TMFvhGxDQHQfIPhDrilwKOm/YCZASAHfgBABQjr3f7CyAkA0cPB03AQULRhKd4xAIjzHymo2Gp7gN0FAMAVOoA2fPz03a9ssh/RM7Iz8QKIzYF9HyB0XEZ1xJ4DzNoDOAfAslhDDTyjDfv8A2AcBeCiu/jBHQEgxnYW6Kp6BlCVAkQM8VnieF2Xyr0ivXy+XvsCzKOihwNHCCryw8HrQXVB8dgFeRfAVQiXjMbIIgXINQYB2H7Kf5wF/2Ar7h0AgKKGuAP4zOjhzlkLbpcRXKRZhNUjxG6HIQDOjN47gCn4+fWW3xVS9urPESEEwwHMo9IhAGxS2ISiA1iEnQOoA4hXRAwItp7WzL9Ow18ESJaw/ar4NgeOR49cAHCAnaH8swBhv+6CBGjeBSxEOUAI7HyKHkD4O9xKb3/feQouAI4uLBciHRRHmgbfA7h/xFc9AngNBADthvii1sMOiPwDAFeyt6s7FSFS4PmnA1v0vQvqDqQKAAPE/weAUuEgsj8c+H11Twdw/AKANXA82EDr5cJBEEzB3oI4Mb0AdR3nNw8vQnegWuvqAABwJFJEBwDgNdA7IOs3gL0LhuJdwBY8c4BfNnDdVgooHiOqn/b7JoSW/QODjTHXhU7hMQAAAABJRU5ErkJggg==',
placeholder: 'blur',
},
parameters: {
// ignoring in Chromatic to avoid inconsistent snapshots
// given that the switch from blur to image is quite fast
chromatic: { disableSnapshot: true },
},
};
export const FilledParent: Story = {
args: {
fill: true,
},
decorators: [
(Story) => <div style={{ width: 500, height: 500, position: 'relative' }}>{Story()}</div>,
],
};
export const Sized: Story = {
args: {
fill: true,
sizes: '(max-width: 600px) 100vw, 600px',
},
decorators: [
(Story) => <div style={{ width: 800, height: 800, position: 'relative' }}>{Story()}</div>,
],
};
export const Lazy: Story = {
args: {
src: 'https://storybook.js.org/images/placeholders/50x50.png',
width: 50,
height: 50,
},
decorators: [
(Story) => (
<>
<div style={{ height: '200vh' }} />
{Story()}
</>
),
],
};
export const Eager: Story = {
...Lazy,
parameters: {
nextjs: {
image: {
loading: 'eager',
},
},
},
};
export const WithRef: Story = {
render() {
const [ref, setRef] = useState<HTMLImageElement | null>(null);
return (
<div>
<Image src={Accessibility} alt="Accessibility" ref={setRef} />
<p>Alt attribute of image: {ref?.alt}</p>
</div>
);
},
};

View File

@ -0,0 +1,36 @@
import React from 'react';
import Image from 'next/legacy/image';
import Accessibility from '../../assets/accessibility.svg';
export default {
component: Image,
args: {
src: Accessibility,
alt: 'Accessibility',
},
};
export const Default = {};
export const BlurredPlaceholder = {
args: {
placeholder: 'blur',
},
};
export const BlurredAbsolutePlaceholder = {
args: {
src: 'https://storybook.js.org/images/placeholders/50x50.png',
width: 50,
height: 50,
blurDataURL:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAABP5JREFUWEeNlwtz2zgMhEGKsv/k9XFp82xe11763yOJVGcXC4m2czM3GYa0E2s/LACSTi+vv1czM/7CvPpqxY/ejPeS3khmFiPHOiVLHKaZi4ux4j18GpMlS6cALupEQBCKQM4BdnGzjIcJgs//QBxAPQAem55f3yL4PWJIdyCyhlMfPdYZot0cwj3Ayg/5JwHA13paen7pADphxr/n5VI8JQsHCCGQ3gVGLLsxQ3h/LYSn5383B05rwNOws3Z576LOTOduvwfrOd5FtVat4akx0uPTrw8BNuUz23vLsY7hmg7i4ipqM2saiAdruNuirh4ff0bNdb3Qy3vkvfAQwrkHoDxTTZtDCOKrC1bMEkdnsQh/PLyetOGHkXeRAgQAQ84efQcBdUhvFofoulpdm9WGGTA+AJEe7l+i37a2c371tCFKs5zzJjxQNBMi1im7OCudP2aNghJuzZaGdSMEZjpwf/t0UgNdg9DOyLGLnY0BUHlzwVtNDkgEQhBeKkb1tUDgQrq7frwAiIJi5BKAeIFgHk5mOpPzvgltOfcoK0Rrs7lWHwsgqtXarK3N0u23h5Ne8+3Cqxn5RYSMfHCAMgDAx4CBWlA9RAGw0GA/ol0gvFB4WjAvBAFUa83SzdUdAbYMqp28uHpxCRefxwAYhksAFBlthxCiXig+zT4TYqkC+Hq7OdAfJv8lPpZiZShWBBIuRP+jspDb2lwcDkzz7OLzbO/zvAHAoXTz5eYMQL0t2yHAiCFcfPY1QDwNFylA5bPoFpsV9fsEiMl8dhcc4PP1CYD3drYcBYdIKQrx0cbRxd2JHSDcQ297/vvoZ5smRC+AyV2AQ+nm03evge08Tyy4jGqXzWWEoIvTgXHU38pWiNgH4ixB/ukAcy/xycXfp4kwdAAAt399W+OCgMjxILQacxvRQ3gEwHgKUIr/rz53CuDFNyP/Eob4+/vEWkBq6AAA/HIi62n/Lk67Q7wDYQ0UpQB7hc54T4E6gACLTYxeAwB0YKZL6U4ATEGIBwCs7qPfQJCCHkCnoK50noJKcXcAojsEAJZZKXhgCoziGKxqWV8IMNp4kP2aC+oB0TMFvhGxDQHQfIPhDrilwKOm/YCZASAHfgBABQjr3f7CyAkA0cPB03AQULRhKd4xAIjzHymo2Gp7gN0FAMAVOoA2fPz03a9ssh/RM7Iz8QKIzYF9HyB0XEZ1xJ4DzNoDOAfAslhDDTyjDfv8A2AcBeCiu/jBHQEgxnYW6Kp6BlCVAkQM8VnieF2Xyr0ivXy+XvsCzKOihwNHCCryw8HrQXVB8dgFeRfAVQiXjMbIIgXINQYB2H7Kf5wF/2Ar7h0AgKKGuAP4zOjhzlkLbpcRXKRZhNUjxG6HIQDOjN47gCn4+fWW3xVS9urPESEEwwHMo9IhAGxS2ISiA1iEnQOoA4hXRAwItp7WzL9Ow18ESJaw/ar4NgeOR49cAHCAnaH8swBhv+6CBGjeBSxEOUAI7HyKHkD4O9xKb3/feQouAI4uLBciHRRHmgbfA7h/xFc9AngNBADthvii1sMOiPwDAFeyt6s7FSFS4PmnA1v0vQvqDqQKAAPE/weAUuEgsj8c+H11Twdw/AKANXA82EDr5cJBEEzB3oI4Mb0AdR3nNw8vQnegWuvqAABwJFJEBwDgNdA7IOs3gL0LhuJdwBY8c4BfNnDdVgooHiOqn/b7JoSW/QODjTHXhU7hMQAAAABJRU5ErkJggg==',
placeholder: 'blur',
},
parameters: {
// ignoring in Chromatic to avoid inconsistent snapshots
// given that the switch from blur to image is quite fast
chromatic: { disableSnapshot: true },
},
};

View File

@ -0,0 +1,3 @@
.link {
color: green;
}

View File

@ -0,0 +1,85 @@
import React from 'react';
import Link from 'next/link';
import type { Meta, StoryObj } from '@storybook/react';
import style from './Link.stories.module.css';
// `onClick`, `href`, and `ref` need to be passed to the DOM element
// for proper handling
const MyButton = React.forwardRef<
HTMLAnchorElement,
React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>
>(function Button({ onClick, href, children }, ref) {
return (
<a href={href} onClick={onClick} ref={ref}>
{children}
</a>
);
});
const Component = () => (
<ul>
<li>
<Link href="/">Normal Link</Link>
</li>
<li>
<Link
href={{
pathname: '/with-url-object',
query: { name: 'test' },
}}
>
With URL Object
</Link>
</li>
<li>
<Link href="/replace-url" replace>
Replace the URL instead of push
</Link>
</li>
<li>
<Link href="/legacy-behaviour" legacyBehavior>
<a>Legacy behavior</a>
</Link>
</li>
<li>
<Link href="/child-is-functional-component" passHref legacyBehavior>
<MyButton>child is a functional component</MyButton>
</Link>
</li>
<li>
<Link href="/#hashid" scroll={false}>
Disables scrolling to the top
</Link>
</li>
<li>
<Link href="/no-prefetch" prefetch={false}>
No Prefetching
</Link>
</li>
<li>
<Link style={{ color: 'red' }} href="/with-style">
With style
</Link>
</li>
<li>
<Link className={style.link} href="/with-classname">
With className
</Link>
</li>
</ul>
);
export default {
component: Component,
} as Meta<typeof Component>;
export const Default: StoryObj<typeof Component> = {};
export const InAppDir: StoryObj<typeof Component> = {
parameters: {
nextjs: {
appDirectory: true,
},
},
};

View File

@ -0,0 +1,151 @@
import {
useRouter,
usePathname,
useSearchParams,
useParams,
useSelectedLayoutSegment,
useSelectedLayoutSegments,
} from 'next/navigation';
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
import { getRouter } from '@storybook/nextjs-vite/navigation.mock';
function Component() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const params = useParams();
const segment = useSelectedLayoutSegment();
const segments = useSelectedLayoutSegments();
const searchParamsList = searchParams ? Array.from(searchParams.entries()) : [];
const routerActions = [
{
cb: () => router.back(),
name: 'Go back',
},
{
cb: () => router.forward(),
name: 'Go forward',
},
{
cb: () => router.prefetch('/prefetched-html'),
name: 'Prefetch',
},
{
// @ts-expect-error (old API)
cb: () => router.push('/push-html', { forceOptimisticNavigation: true }),
name: 'Push HTML',
},
{
cb: () => router.refresh(),
name: 'Refresh',
},
{
// @ts-expect-error (old API)
cb: () => router.replace('/replaced-html', { forceOptimisticNavigation: true }),
name: 'Replace',
},
];
return (
<div>
<div>pathname: {pathname}</div>
<div>segment: {segment}</div>
<div>segments: {segments.join(',')}</div>
<div>
searchparams:{' '}
<ul>
{searchParamsList.map(([key, value]) => (
<li key={key}>
{key}: {value}
</li>
))}
</ul>
</div>
<div>
params:{' '}
<ul>
{Object.entries(params).map(([key, value]) => (
<li key={key}>
{key}: {value}
</li>
))}
</ul>
</div>
{routerActions.map(({ cb, name }) => (
<div key={name} style={{ marginBottom: '1em' }}>
<button type="button" onClick={cb}>
{name}
</button>
</div>
))}
</div>
);
}
type Story = StoryObj<typeof Component>;
export default {
component: Component,
parameters: {
nextjs: {
appDirectory: true,
navigation: {
pathname: '/hello',
query: {
foo: 'bar',
},
prefetch: () => {
console.log('custom prefetch');
},
},
},
},
} as Meta<typeof Component>;
export const Default: StoryObj<typeof Component> = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
const routerMock = getRouter();
await step('Asserts whether forward hook is called', async () => {
const forwardBtn = await canvas.findByText('Go forward');
await userEvent.click(forwardBtn);
await expect(routerMock.forward).toHaveBeenCalled();
});
await step('Asserts whether custom prefetch hook is called', async () => {
const prefetchBtn = await canvas.findByText('Prefetch');
await userEvent.click(prefetchBtn);
await expect(routerMock.prefetch).toHaveBeenCalledWith('/prefetched-html');
});
},
};
export const WithSegmentDefined: Story = {
parameters: {
nextjs: {
appDirectory: true,
navigation: {
segments: ['dashboard', 'settings'],
},
},
},
};
export const WithSegmentDefinedForParams: Story = {
parameters: {
nextjs: {
appDirectory: true,
navigation: {
segments: [
['slug', 'hello'],
['framework', 'nextjs'],
],
},
},
},
};

View File

@ -0,0 +1,46 @@
import NextHeader from './NextHeader';
import type { Meta } from '@storybook/react';
import type { StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
import { cookies, headers } from '@storybook/nextjs-vite/headers.mock';
export default {
component: NextHeader,
} as Meta<typeof NextHeader>;
type Story = StoryObj<typeof NextHeader>;
export const Default: Story = {
loaders: async () => {
cookies().set('firstName', 'Jane');
cookies().set({
name: 'lastName',
value: 'Doe',
});
headers().set('timezone', 'Central European Summer Time');
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
const headersMock = headers();
const cookiesMock = cookies();
await step('Cookie and header store apis are called upon rendering', async () => {
await expect(cookiesMock.getAll).toHaveBeenCalled();
await expect(headersMock.entries).toHaveBeenCalled();
});
await step('Upon clicking on submit, the user-id cookie is set', async () => {
const submitButton = await canvas.findByRole('button');
await userEvent.click(submitButton);
await expect(cookiesMock.set).toHaveBeenCalledWith('user-id', 'encrypted-id');
});
await step('The user-id cookie is available in cookie and header stores', async () => {
await expect(headersMock.get('cookie')).toContain('user-id=encrypted-id');
await expect(cookiesMock.get('user-id')).toEqual({
name: 'user-id',
value: 'encrypted-id',
});
});
},
};

View File

@ -0,0 +1,39 @@
import React from 'react';
import { cookies, headers } from 'next/headers';
export default async function Component() {
async function handleClick() {
'use server';
cookies().set('user-id', 'encrypted-id');
}
return (
<>
<h3>Cookies:</h3>
{cookies()
.getAll()
.map(({ name, value }) => {
return (
<p key={name} style={{ display: 'flex', flexDirection: 'row', gap: 8 }}>
<strong>Name:</strong> <span>{name}</span>
<strong>Value:</strong> <span>{value}</span>
</p>
);
})}
<h3>Headers:</h3>
{Array.from(headers().entries()).map(([name, value]: [string, string]) => {
return (
<p key={name} style={{ display: 'flex', flexDirection: 'row', gap: 8 }}>
<strong>Name:</strong> <span>{name}</span>
<strong>Value:</strong> <span>{value}</span>
</p>
);
})}
<form action={handleClick}>
<button>add user-id cookie</button>
</form>
</>
);
}

View File

@ -0,0 +1,6 @@
import React from 'react';
import 'server-only';
export const RSC = async ({ label }) => <>RSC {label}</>;
export const Nested = async ({ children }) => <>Nested {children}</>;

View File

@ -0,0 +1,35 @@
import React from 'react';
import { RSC, Nested } from './RSC';
export default {
component: RSC,
args: { label: 'label' },
};
export const Default = {};
export const DisableRSC = {
tags: ['!test'],
parameters: {
chromatic: { disable: true },
nextjs: { rsc: false },
},
};
export const Error = {
tags: ['!test'],
parameters: {
chromatic: { disable: true },
},
render: () => {
throw new Error('RSC Error');
},
};
export const NestedRSC = {
render: (args) => (
<Nested>
<RSC {...args} />
</Nested>
),
};

View File

@ -0,0 +1,55 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/test';
import { redirect } from 'next/navigation';
let state = 'Bug! Not invalidated';
export default {
render() {
return (
<div>
<div>{state}</div>
<form
action={() => {
state = 'State is invalidated successfully.';
redirect('/');
}}
>
<button>Submit</button>
</form>
</div>
);
},
parameters: {
test: {
// This is needed until Next will update to the React 19 beta: https://github.com/vercel/next.js/pull/65058
// In the React 19 beta ErrorBoundary errors (such as redirect) are only logged, and not thrown.
// We will also suspress console.error logs for re the console.error logs for redirect in the next framework.
// Using the onCaughtError react root option:
// react: {
// rootOptions: {
// onCaughtError(error: unknown) {
// if (isNextRouterError(error)) return;
// console.error(error);
// },
// },
// See: code/frameworks/nextjs/src/preview.tsx
dangerouslyIgnoreUnhandledErrors: true,
},
nextjs: {
appDirectory: true,
navigation: {
pathname: '/',
},
},
},
tags: ['!test'],
} as Meta;
export const SingletonStateGetsInvalidatedAfterRedirecting: StoryObj = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button'));
},
};

View File

@ -0,0 +1,108 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { expect, within, userEvent } from '@storybook/test';
import { getRouter } from '@storybook/nextjs-vite/router.mock';
import Router, { useRouter } from 'next/router';
function Component() {
const router = useRouter();
const searchParams = router.query;
const routerActions = [
{
cb: () => router.back(),
name: 'Go back',
},
{
cb: () => router.forward(),
name: 'Go forward',
},
{
cb: () => router.prefetch('/prefetched-html'),
name: 'Prefetch',
},
{
// @ts-expect-error (old API)
cb: () => router.push('/push-html', { forceOptimisticNavigation: true }),
name: 'Push HTML',
},
{
// @ts-expect-error (old API)
cb: () => router.replace('/replaced-html', { forceOptimisticNavigation: true }),
name: 'Replace',
},
];
return (
<div>
<div>Router pathname: {Router.pathname}</div>
<div>pathname: {router.pathname}</div>
<div>
searchparams:{' '}
<ul>
{Object.entries(searchParams).map(([key, value]) => (
<li key={key}>
{key}: {value}
</li>
))}
</ul>
</div>
{routerActions.map(({ cb, name }) => (
<div key={name} style={{ marginBottom: '1em' }}>
<button type="button" onClick={cb}>
{name}
</button>
</div>
))}
</div>
);
}
export default {
component: Component,
parameters: {
nextjs: {
router: {
pathname: '/hello',
query: {
foo: 'bar',
},
prefetch: () => {
console.log('custom prefetch');
},
},
},
},
} as Meta<typeof Component>;
export const Default: StoryObj<typeof Component> = {
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
const routerMock = getRouter();
await step('Router property overrides should be available in useRouter fn', async () => {
await expect(Router.pathname).toBe('/hello');
await expect(Router.query).toEqual({ foo: 'bar' });
});
await step(
'Router property overrides should be available in default export from next/router',
async () => {
await expect(Router.pathname).toBe('/hello');
await expect(Router.query).toEqual({ foo: 'bar' });
}
);
await step('Asserts whether forward hook is called', async () => {
const forwardBtn = await canvas.findByText('Go forward');
await userEvent.click(forwardBtn);
await expect(routerMock.forward).toHaveBeenCalled();
});
await step('Asserts whether custom prefetch hook is called', async () => {
const prefetchBtn = await canvas.findByText('Prefetch');
await userEvent.click(prefetchBtn);
await expect(routerMock.prefetch).toHaveBeenCalledWith('/prefetched-html');
});
},
};

View File

@ -0,0 +1,115 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { expect, within, userEvent, waitFor } from '@storybook/test';
import { cookies } from '@storybook/nextjs-vite/headers.mock';
import { revalidatePath } from '@storybook/nextjs-vite/cache.mock';
import { redirect, getRouter } from '@storybook/nextjs-vite/navigation.mock';
import { accessRoute, login, logout } from './ServerActions';
function Component() {
return (
<div style={{ display: 'flex', gap: 8 }}>
<form>
<button type="submit" formAction={login}>
Login
</button>
</form>
<form>
<button type="submit" formAction={logout}>
Logout
</button>
</form>
<form>
<button type="submit" formAction={accessRoute}>
Access protected route
</button>
</form>
</div>
);
}
export default {
component: Component,
tags: ['!test'],
parameters: {
nextjs: {
appDirectory: true,
navigation: {
pathname: '/',
},
},
test: {
// This is needed until Next will update to the React 19 beta: https://github.com/vercel/next.js/pull/65058
// In the React 19 beta ErrorBoundary errors (such as redirect) are only logged, and not thrown.
// We will also suspress console.error logs for re the console.error logs for redirect in the next framework.
// Using the onCaughtError react root option:
// react: {
// rootOptions: {
// onCaughtError(error: unknown) {
// if (isNextRouterError(error)) return;
// console.error(error);
// },
// },
// See: code/frameworks/nextjs/src/preview.tsx
dangerouslyIgnoreUnhandledErrors: true,
},
},
} as Meta<typeof Component>;
export const ProtectedWhileLoggedOut: StoryObj<typeof Component> = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByText('Access protected route'));
await expect(cookies().get).toHaveBeenCalledWith('user');
await expect(redirect).toHaveBeenCalledWith('/');
await waitFor(() => expect(getRouter().push).toHaveBeenCalled());
},
};
export const ProtectedWhileLoggedIn: StoryObj<typeof Component> = {
beforeEach() {
cookies().set('user', 'storybookjs');
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByText('Access protected route'));
await expect(cookies().get).toHaveBeenLastCalledWith('user');
await expect(revalidatePath).toHaveBeenLastCalledWith('/');
await expect(redirect).toHaveBeenLastCalledWith('/protected');
await waitFor(() => expect(getRouter().push).toHaveBeenCalled());
},
};
export const Logout: StoryObj<typeof Component> = {
beforeEach() {
cookies().set('user', 'storybookjs');
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByText('Logout'));
await expect(cookies().delete).toHaveBeenCalled();
await expect(revalidatePath).toHaveBeenCalledWith('/');
await expect(redirect).toHaveBeenCalledWith('/');
await waitFor(() => expect(getRouter().push).toHaveBeenCalled());
},
};
export const Login: StoryObj<typeof Component> = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByText('Login'));
await expect(cookies().set).toHaveBeenCalledWith('user', 'storybookjs');
await expect(revalidatePath).toHaveBeenCalledWith('/');
await expect(redirect).toHaveBeenCalledWith('/');
await waitFor(() => expect(getRouter().push).toHaveBeenCalled());
},
};

View File

@ -0,0 +1,28 @@
'use server';
import { cookies } from 'next/headers';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function accessRoute() {
const user = cookies().get('user');
if (!user) {
redirect('/');
}
revalidatePath('/');
redirect(`/protected`);
}
export async function logout() {
cookies().delete('user');
revalidatePath('/');
redirect('/');
}
export async function login() {
cookies().set('user', 'storybookjs');
revalidatePath('/');
redirect('/');
}

View File

@ -0,0 +1,20 @@
import React from 'react';
const Component = () => (
<div>
<style jsx>{`
.main p {
color: #ff4785;
}
`}</style>
<main className="main">
<p>This is styled using Styled JSX</p>
</main>
</div>
);
export default {
component: Component,
};
export const Default = {};

View File

@ -0,0 +1,93 @@
Copyright 2020 The Rubik Filtered Project Authors (https://https://github.com/NaN-xyz/Rubik-Filtered)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "Preserve",
"moduleResolution": "Bundler"
},
"include": ["src/**/*", "template/**/*"]
}

View File

@ -0,0 +1,10 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { defineConfig, mergeConfig } from 'vitest/config';
import { vitestCommonConfig } from '../../vitest.workspace';
export default mergeConfig(
vitestCommonConfig,
defineConfig({
// Add custom config here
})
);

View File

@ -196,11 +196,33 @@ const baseTemplates = {
renderer: '@storybook/react',
builder: '@storybook/builder-webpack5',
},
modifications: {
extraDependencies: ['server-only'],
},
skipTasks: ['e2e-tests-dev', 'bench'],
},
'nextjs-vite/default-ts': {
name: 'Next.js Latest (Vite | TypeScript)',
script:
'yarn create next-app {{beforeDir}} --typescript --eslint --tailwind --app --import-alias="@/*" --src-dir',
inDevelopment: true,
expected: {
framework: '@storybook/nextjs-vite',
renderer: '@storybook/react',
builder: '@storybook/builder-vite',
},
modifications: {
mainConfig: {
framework: '@storybook/nextjs-vite',
features: { experimentalRSC: true },
},
extraDependencies: ['server-only'],
extraDependencies: [
'server-only',
'vite-plugin-storybook-nextjs',
'@storybook/nextjs-vite',
'vite',
],
},
skipTasks: ['e2e-tests-dev', 'bench'],
},

View File

@ -2437,6 +2437,15 @@ __metadata:
languageName: node
linkType: hard
"@emnapi/runtime@npm:^1.1.1":
version: 1.2.0
resolution: "@emnapi/runtime@npm:1.2.0"
dependencies:
tslib: "npm:^2.4.0"
checksum: 10c0/7005ff8b67724c9e61b6cd79a3decbdb2ce25d24abd4d3d187472f200ee6e573329c30264335125fb136bd813aa9cf9f4f7c9391d04b07dd1e63ce0a3427be57
languageName: node
linkType: hard
"@emotion/babel-plugin@npm:^11.11.0":
version: 11.11.0
resolution: "@emotion/babel-plugin@npm:11.11.0"
@ -3369,6 +3378,18 @@ __metadata:
languageName: node
linkType: hard
"@img/sharp-darwin-arm64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-darwin-arm64@npm:0.33.4"
dependencies:
"@img/sharp-libvips-darwin-arm64": "npm:1.0.2"
dependenciesMeta:
"@img/sharp-libvips-darwin-arm64":
optional: true
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@img/sharp-darwin-x64@npm:0.33.3":
version: 0.33.3
resolution: "@img/sharp-darwin-x64@npm:0.33.3"
@ -3381,6 +3402,18 @@ __metadata:
languageName: node
linkType: hard
"@img/sharp-darwin-x64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-darwin-x64@npm:0.33.4"
dependencies:
"@img/sharp-libvips-darwin-x64": "npm:1.0.2"
dependenciesMeta:
"@img/sharp-libvips-darwin-x64":
optional: true
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@img/sharp-libvips-darwin-arm64@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-darwin-arm64@npm:1.0.2"
@ -3449,6 +3482,18 @@ __metadata:
languageName: node
linkType: hard
"@img/sharp-linux-arm64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linux-arm64@npm:0.33.4"
dependencies:
"@img/sharp-libvips-linux-arm64": "npm:1.0.2"
dependenciesMeta:
"@img/sharp-libvips-linux-arm64":
optional: true
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@img/sharp-linux-arm@npm:0.33.3":
version: 0.33.3
resolution: "@img/sharp-linux-arm@npm:0.33.3"
@ -3461,6 +3506,18 @@ __metadata:
languageName: node
linkType: hard
"@img/sharp-linux-arm@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linux-arm@npm:0.33.4"
dependencies:
"@img/sharp-libvips-linux-arm": "npm:1.0.2"
dependenciesMeta:
"@img/sharp-libvips-linux-arm":
optional: true
conditions: os=linux & cpu=arm & libc=glibc
languageName: node
linkType: hard
"@img/sharp-linux-s390x@npm:0.33.3":
version: 0.33.3
resolution: "@img/sharp-linux-s390x@npm:0.33.3"
@ -3473,6 +3530,18 @@ __metadata:
languageName: node
linkType: hard
"@img/sharp-linux-s390x@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linux-s390x@npm:0.33.4"
dependencies:
"@img/sharp-libvips-linux-s390x": "npm:1.0.2"
dependenciesMeta:
"@img/sharp-libvips-linux-s390x":
optional: true
conditions: os=linux & cpu=s390x & libc=glibc
languageName: node
linkType: hard
"@img/sharp-linux-x64@npm:0.33.3":
version: 0.33.3
resolution: "@img/sharp-linux-x64@npm:0.33.3"
@ -3485,6 +3554,18 @@ __metadata:
languageName: node
linkType: hard
"@img/sharp-linux-x64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linux-x64@npm:0.33.4"
dependencies:
"@img/sharp-libvips-linux-x64": "npm:1.0.2"
dependenciesMeta:
"@img/sharp-libvips-linux-x64":
optional: true
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@img/sharp-linuxmusl-arm64@npm:0.33.3":
version: 0.33.3
resolution: "@img/sharp-linuxmusl-arm64@npm:0.33.3"
@ -3497,6 +3578,18 @@ __metadata:
languageName: node
linkType: hard
"@img/sharp-linuxmusl-arm64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linuxmusl-arm64@npm:0.33.4"
dependencies:
"@img/sharp-libvips-linuxmusl-arm64": "npm:1.0.2"
dependenciesMeta:
"@img/sharp-libvips-linuxmusl-arm64":
optional: true
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@img/sharp-linuxmusl-x64@npm:0.33.3":
version: 0.33.3
resolution: "@img/sharp-linuxmusl-x64@npm:0.33.3"
@ -3509,6 +3602,18 @@ __metadata:
languageName: node
linkType: hard
"@img/sharp-linuxmusl-x64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linuxmusl-x64@npm:0.33.4"
dependencies:
"@img/sharp-libvips-linuxmusl-x64": "npm:1.0.2"
dependenciesMeta:
"@img/sharp-libvips-linuxmusl-x64":
optional: true
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@img/sharp-wasm32@npm:0.33.3":
version: 0.33.3
resolution: "@img/sharp-wasm32@npm:0.33.3"
@ -3518,6 +3623,15 @@ __metadata:
languageName: node
linkType: hard
"@img/sharp-wasm32@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-wasm32@npm:0.33.4"
dependencies:
"@emnapi/runtime": "npm:^1.1.1"
conditions: cpu=wasm32
languageName: node
linkType: hard
"@img/sharp-win32-ia32@npm:0.33.3":
version: 0.33.3
resolution: "@img/sharp-win32-ia32@npm:0.33.3"
@ -3525,6 +3639,13 @@ __metadata:
languageName: node
linkType: hard
"@img/sharp-win32-ia32@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-win32-ia32@npm:0.33.4"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@img/sharp-win32-x64@npm:0.33.3":
version: 0.33.3
resolution: "@img/sharp-win32-x64@npm:0.33.3"
@ -3532,6 +3653,13 @@ __metadata:
languageName: node
linkType: hard
"@img/sharp-win32-x64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-win32-x64@npm:0.33.4"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@isaacs/cliui@npm:^8.0.2":
version: 8.0.2
resolution: "@isaacs/cliui@npm:8.0.2"
@ -3788,6 +3916,13 @@ __metadata:
languageName: node
linkType: hard
"@next/env@npm:14.2.5, @next/env@npm:^14.2.5":
version: 14.2.5
resolution: "@next/env@npm:14.2.5"
checksum: 10c0/63d8b88ac450b3c37940a9e2119a63a1074aca89908574ade6157a8aa295275dcb3ac5f69e00883fc55d0f12963b73b74e87ba32a5768a489f9609c6be57b699
languageName: node
linkType: hard
"@next/swc-darwin-arm64@npm:14.1.0":
version: 14.1.0
resolution: "@next/swc-darwin-arm64@npm:14.1.0"
@ -3795,6 +3930,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-darwin-arm64@npm:14.2.5":
version: 14.2.5
resolution: "@next/swc-darwin-arm64@npm:14.2.5"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:14.1.0":
version: 14.1.0
resolution: "@next/swc-darwin-x64@npm:14.1.0"
@ -3802,6 +3944,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:14.2.5":
version: 14.2.5
resolution: "@next/swc-darwin-x64@npm:14.2.5"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:14.1.0":
version: 14.1.0
resolution: "@next/swc-linux-arm64-gnu@npm:14.1.0"
@ -3809,6 +3958,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:14.2.5":
version: 14.2.5
resolution: "@next/swc-linux-arm64-gnu@npm:14.2.5"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:14.1.0":
version: 14.1.0
resolution: "@next/swc-linux-arm64-musl@npm:14.1.0"
@ -3816,6 +3972,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:14.2.5":
version: 14.2.5
resolution: "@next/swc-linux-arm64-musl@npm:14.2.5"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:14.1.0":
version: 14.1.0
resolution: "@next/swc-linux-x64-gnu@npm:14.1.0"
@ -3823,6 +3986,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:14.2.5":
version: 14.2.5
resolution: "@next/swc-linux-x64-gnu@npm:14.2.5"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:14.1.0":
version: 14.1.0
resolution: "@next/swc-linux-x64-musl@npm:14.1.0"
@ -3830,6 +4000,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:14.2.5":
version: 14.2.5
resolution: "@next/swc-linux-x64-musl@npm:14.2.5"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:14.1.0":
version: 14.1.0
resolution: "@next/swc-win32-arm64-msvc@npm:14.1.0"
@ -3837,6 +4014,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:14.2.5":
version: 14.2.5
resolution: "@next/swc-win32-arm64-msvc@npm:14.2.5"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@next/swc-win32-ia32-msvc@npm:14.1.0":
version: 14.1.0
resolution: "@next/swc-win32-ia32-msvc@npm:14.1.0"
@ -3844,6 +4028,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-win32-ia32-msvc@npm:14.2.5":
version: 14.2.5
resolution: "@next/swc-win32-ia32-msvc@npm:14.2.5"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:14.1.0":
version: 14.1.0
resolution: "@next/swc-win32-x64-msvc@npm:14.1.0"
@ -3851,6 +4042,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:14.2.5":
version: 14.2.5
resolution: "@next/swc-win32-x64-msvc@npm:14.2.5"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@ngtools/webpack@npm:17.3.0":
version: 17.3.0
resolution: "@ngtools/webpack@npm:17.3.0"
@ -6084,6 +6282,35 @@ __metadata:
languageName: unknown
linkType: soft
"@storybook/nextjs-vite@workspace:frameworks/nextjs-vite":
version: 0.0.0-use.local
resolution: "@storybook/nextjs-vite@workspace:frameworks/nextjs-vite"
dependencies:
"@storybook/builder-vite": "workspace:*"
"@storybook/react": "workspace:*"
"@storybook/test": "workspace:*"
"@types/node": "npm:^18.0.0"
next: "npm:^14.2.5"
sharp: "npm:^0.33.3"
styled-jsx: "npm:5.1.6"
typescript: "npm:^5.3.2"
vite-plugin-storybook-nextjs: "npm:^0.0.13"
peerDependencies:
next: ^14.2.5
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
storybook: "workspace:^"
vite: ^5.0.0
vite-plugin-storybook-nextjs: ^0.0.13
dependenciesMeta:
sharp:
optional: true
peerDependenciesMeta:
typescript:
optional: true
languageName: unknown
linkType: soft
"@storybook/nextjs@workspace:*, @storybook/nextjs@workspace:frameworks/nextjs":
version: 0.0.0-use.local
resolution: "@storybook/nextjs@workspace:frameworks/nextjs"
@ -6964,6 +7191,13 @@ __metadata:
languageName: node
linkType: hard
"@swc/counter@npm:^0.1.3":
version: 0.1.3
resolution: "@swc/counter@npm:0.1.3"
checksum: 10c0/8424f60f6bf8694cfd2a9bca45845bce29f26105cda8cf19cdb9fd3e78dc6338699e4db77a89ae449260bafa1cc6bec307e81e7fb96dbf7dcfce0eea55151356
languageName: node
linkType: hard
"@swc/helpers@npm:0.5.2":
version: 0.5.2
resolution: "@swc/helpers@npm:0.5.2"
@ -6973,6 +7207,16 @@ __metadata:
languageName: node
linkType: hard
"@swc/helpers@npm:0.5.5":
version: 0.5.5
resolution: "@swc/helpers@npm:0.5.5"
dependencies:
"@swc/counter": "npm:^0.1.3"
tslib: "npm:^2.4.0"
checksum: 10c0/21a9b9cfe7e00865f9c9f3eb4c1cc5b397143464f7abee76a2c5366e591e06b0155b5aac93fe8269ef8d548df253f6fd931e9ddfc0fd12efd405f90f45506e7d
languageName: node
linkType: hard
"@swc/helpers@npm:~0.5.0":
version: 0.5.6
resolution: "@swc/helpers@npm:0.5.6"
@ -16741,6 +16985,17 @@ __metadata:
languageName: node
linkType: hard
"image-size@npm:^1.1.1":
version: 1.1.1
resolution: "image-size@npm:1.1.1"
dependencies:
queue: "npm:6.0.2"
bin:
image-size: bin/image-size.js
checksum: 10c0/2660470096d12be82195f7e80fe03274689fbd14184afb78eaf66ade7cd06352518325814f88af4bde4b26647889fe49e573129f6e7ba8f5ff5b85cc7f559000
languageName: node
linkType: hard
"image-size@npm:~0.5.0":
version: 0.5.5
resolution: "image-size@npm:0.5.5"
@ -20489,6 +20744,13 @@ __metadata:
languageName: node
linkType: hard
"module-alias@npm:^2.2.3":
version: 2.2.3
resolution: "module-alias@npm:2.2.3"
checksum: 10c0/47dc5b6d04f6e7df0ff330ca9b2a37c688a682ed661e9432b0b327e1e6c43eedad052151b8d50d6beea8b924828d2a92fa4625c18d651bf2d93d8f03aa0172fa
languageName: node
linkType: hard
"mri@npm:^1.1.0, mri@npm:^1.2.0":
version: 1.2.0
resolution: "mri@npm:1.2.0"
@ -20683,6 +20945,64 @@ __metadata:
languageName: node
linkType: hard
"next@npm:^14.2.5":
version: 14.2.5
resolution: "next@npm:14.2.5"
dependencies:
"@next/env": "npm:14.2.5"
"@next/swc-darwin-arm64": "npm:14.2.5"
"@next/swc-darwin-x64": "npm:14.2.5"
"@next/swc-linux-arm64-gnu": "npm:14.2.5"
"@next/swc-linux-arm64-musl": "npm:14.2.5"
"@next/swc-linux-x64-gnu": "npm:14.2.5"
"@next/swc-linux-x64-musl": "npm:14.2.5"
"@next/swc-win32-arm64-msvc": "npm:14.2.5"
"@next/swc-win32-ia32-msvc": "npm:14.2.5"
"@next/swc-win32-x64-msvc": "npm:14.2.5"
"@swc/helpers": "npm:0.5.5"
busboy: "npm:1.6.0"
caniuse-lite: "npm:^1.0.30001579"
graceful-fs: "npm:^4.2.11"
postcss: "npm:8.4.31"
styled-jsx: "npm:5.1.1"
peerDependencies:
"@opentelemetry/api": ^1.1.0
"@playwright/test": ^1.41.2
react: ^18.2.0
react-dom: ^18.2.0
sass: ^1.3.0
dependenciesMeta:
"@next/swc-darwin-arm64":
optional: true
"@next/swc-darwin-x64":
optional: true
"@next/swc-linux-arm64-gnu":
optional: true
"@next/swc-linux-arm64-musl":
optional: true
"@next/swc-linux-x64-gnu":
optional: true
"@next/swc-linux-x64-musl":
optional: true
"@next/swc-win32-arm64-msvc":
optional: true
"@next/swc-win32-ia32-msvc":
optional: true
"@next/swc-win32-x64-msvc":
optional: true
peerDependenciesMeta:
"@opentelemetry/api":
optional: true
"@playwright/test":
optional: true
sass:
optional: true
bin:
next: dist/bin/next
checksum: 10c0/8df7d8ccc1a5bab03fa50dd6656c8a6f3750e81ef0b087dc329fea9346847c3094a933a890a8e87151dc32f0bc55020b8f6386d4565856d83bcc10895d29ec08
languageName: node
linkType: hard
"nice-napi@npm:^1.0.2":
version: 1.0.2
resolution: "nice-napi@npm:1.0.2"
@ -24953,6 +25273,75 @@ __metadata:
languageName: node
linkType: hard
"sharp@npm:^0.33.4":
version: 0.33.4
resolution: "sharp@npm:0.33.4"
dependencies:
"@img/sharp-darwin-arm64": "npm:0.33.4"
"@img/sharp-darwin-x64": "npm:0.33.4"
"@img/sharp-libvips-darwin-arm64": "npm:1.0.2"
"@img/sharp-libvips-darwin-x64": "npm:1.0.2"
"@img/sharp-libvips-linux-arm": "npm:1.0.2"
"@img/sharp-libvips-linux-arm64": "npm:1.0.2"
"@img/sharp-libvips-linux-s390x": "npm:1.0.2"
"@img/sharp-libvips-linux-x64": "npm:1.0.2"
"@img/sharp-libvips-linuxmusl-arm64": "npm:1.0.2"
"@img/sharp-libvips-linuxmusl-x64": "npm:1.0.2"
"@img/sharp-linux-arm": "npm:0.33.4"
"@img/sharp-linux-arm64": "npm:0.33.4"
"@img/sharp-linux-s390x": "npm:0.33.4"
"@img/sharp-linux-x64": "npm:0.33.4"
"@img/sharp-linuxmusl-arm64": "npm:0.33.4"
"@img/sharp-linuxmusl-x64": "npm:0.33.4"
"@img/sharp-wasm32": "npm:0.33.4"
"@img/sharp-win32-ia32": "npm:0.33.4"
"@img/sharp-win32-x64": "npm:0.33.4"
color: "npm:^4.2.3"
detect-libc: "npm:^2.0.3"
semver: "npm:^7.6.0"
dependenciesMeta:
"@img/sharp-darwin-arm64":
optional: true
"@img/sharp-darwin-x64":
optional: true
"@img/sharp-libvips-darwin-arm64":
optional: true
"@img/sharp-libvips-darwin-x64":
optional: true
"@img/sharp-libvips-linux-arm":
optional: true
"@img/sharp-libvips-linux-arm64":
optional: true
"@img/sharp-libvips-linux-s390x":
optional: true
"@img/sharp-libvips-linux-x64":
optional: true
"@img/sharp-libvips-linuxmusl-arm64":
optional: true
"@img/sharp-libvips-linuxmusl-x64":
optional: true
"@img/sharp-linux-arm":
optional: true
"@img/sharp-linux-arm64":
optional: true
"@img/sharp-linux-s390x":
optional: true
"@img/sharp-linux-x64":
optional: true
"@img/sharp-linuxmusl-arm64":
optional: true
"@img/sharp-linuxmusl-x64":
optional: true
"@img/sharp-wasm32":
optional: true
"@img/sharp-win32-ia32":
optional: true
"@img/sharp-win32-x64":
optional: true
checksum: 10c0/428c5c6a84ff8968effe50c2de931002f5f30b9f263e1c026d0384e581673c13088a49322f7748114d3d9be4ae9476a74bf003a3af34743e97ef2f880d1cfe45
languageName: node
linkType: hard
"shebang-command@npm:^1.2.0":
version: 1.2.0
resolution: "shebang-command@npm:1.2.0"
@ -25884,6 +26273,22 @@ __metadata:
languageName: node
linkType: hard
"styled-jsx@npm:5.1.6":
version: 5.1.6
resolution: "styled-jsx@npm:5.1.6"
dependencies:
client-only: "npm:0.0.1"
peerDependencies:
react: ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
peerDependenciesMeta:
"@babel/core":
optional: true
babel-plugin-macros:
optional: true
checksum: 10c0/ace50e7ea5ae5ae6a3b65a50994c51fca6ae7df9c7ecfd0104c36be0b4b3a9c5c1a2374d16e2a11e256d0b20be6d47256d768ecb4f91ab390f60752a075780f5
languageName: node
linkType: hard
"stylis@npm:4.2.0":
version: 4.2.0
resolution: "stylis@npm:4.2.0"
@ -27742,6 +28147,27 @@ __metadata:
languageName: node
linkType: hard
"vite-plugin-storybook-nextjs@npm:^0.0.13":
version: 0.0.13
resolution: "vite-plugin-storybook-nextjs@npm:0.0.13"
dependencies:
"@next/env": "npm:^14.2.5"
image-size: "npm:^1.1.1"
module-alias: "npm:^2.2.3"
sharp: "npm:^0.33.4"
ts-dedent: "npm:^2.2.0"
peerDependencies:
"@storybook/test": ^8.3.0-alpha.3
next: ^14.2.5
storybook: ^8.3.0-alpha.3
vite: ^5.0.0
dependenciesMeta:
sharp:
optional: true
checksum: 10c0/331e43c91f6c395e3ed87b6a2f372f1372fa7dfc00d0cb8e0a9daad129d779f6f56b5d01fb4b1ab6c231917c458189c02535ecbe2d9d447bc8bf4c3939cc42aa
languageName: node
linkType: hard
"vite@npm:5.1.5, vite@npm:^5.0.0":
version: 5.1.5
resolution: "vite@npm:5.1.5"