diff --git a/code/core/src/common/utils/framework-to-renderer.ts b/code/core/src/common/utils/framework-to-renderer.ts index a34ac765c2c7..6169237b12cd 100644 --- a/code/core/src/common/utils/framework-to-renderer.ts +++ b/code/core/src/common/utils/framework-to-renderer.ts @@ -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', diff --git a/code/core/src/common/versions.ts b/code/core/src/common/versions.ts index 192f2dbea6fc..c312baaa8b57 100644 --- a/code/core/src/common/versions.ts +++ b/code/core/src/common/versions.ts @@ -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', diff --git a/code/core/src/types/modules/frameworks.ts b/code/core/src/types/modules/frameworks.ts index 9ae2cc538b51..a8487124c6d6 100644 --- a/code/core/src/types/modules/frameworks.ts +++ b/code/core/src/types/modules/frameworks.ts @@ -5,6 +5,7 @@ export type SupportedFrameworks = | 'html-vite' | 'html-webpack5' | 'nextjs' + | 'nextjs-vite' | 'preact-vite' | 'preact-webpack5' | 'react-vite' diff --git a/code/frameworks/nextjs-vite/.eslintrc.json b/code/frameworks/nextjs-vite/.eslintrc.json new file mode 100644 index 000000000000..d76f64f6803d --- /dev/null +++ b/code/frameworks/nextjs-vite/.eslintrc.json @@ -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" + } + } + ] +} diff --git a/code/frameworks/nextjs-vite/README.md b/code/frameworks/nextjs-vite/README.md new file mode 100644 index 000000000000..2ed6aeca8721 --- /dev/null +++ b/code/frameworks/nextjs-vite/README.md @@ -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) diff --git a/code/frameworks/nextjs-vite/package.json b/code/frameworks/nextjs-vite/package.json new file mode 100644 index 000000000000..3818f5c40848 --- /dev/null +++ b/code/frameworks/nextjs-vite/package.json @@ -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" +} diff --git a/code/frameworks/nextjs-vite/preset.js b/code/frameworks/nextjs-vite/preset.js new file mode 100644 index 000000000000..a83f95279e7f --- /dev/null +++ b/code/frameworks/nextjs-vite/preset.js @@ -0,0 +1 @@ +module.exports = require('./dist/preset'); diff --git a/code/frameworks/nextjs-vite/project.json b/code/frameworks/nextjs-vite/project.json new file mode 100644 index 000000000000..049a16e3417f --- /dev/null +++ b/code/frameworks/nextjs-vite/project.json @@ -0,0 +1,8 @@ +{ + "name": "nextjs-vite", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "targets": { + "build": {} + } +} diff --git a/code/frameworks/nextjs-vite/src/config/preview.ts b/code/frameworks/nextjs-vite/src/config/preview.ts new file mode 100644 index 000000000000..4766f590bcaf --- /dev/null +++ b/code/frameworks/nextjs-vite/src/config/preview.ts @@ -0,0 +1,4 @@ +import { setConfig } from 'next/config'; + +// eslint-disable-next-line no-underscore-dangle +setConfig(process.env.__NEXT_RUNTIME_CONFIG); diff --git a/code/frameworks/nextjs-vite/src/export-mocks/cache/index.ts b/code/frameworks/nextjs-vite/src/export-mocks/cache/index.ts new file mode 100644 index 000000000000..35b74b8cb02f --- /dev/null +++ b/code/frameworks/nextjs-vite/src/export-mocks/cache/index.ts @@ -0,0 +1,23 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { fn } from '@storybook/test'; + +// biome-ignore lint/suspicious/noExplicitAny: +type Callback = (...args: any[]) => Promise; + +// 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 }; diff --git a/code/frameworks/nextjs-vite/src/export-mocks/headers/cookies.ts b/code/frameworks/nextjs-vite/src/export-mocks/headers/cookies.ts new file mode 100644 index 000000000000..3d84ecba3885 --- /dev/null +++ b/code/frameworks/nextjs-vite/src/export-mocks/headers/cookies.ts @@ -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()); +}; diff --git a/code/frameworks/nextjs-vite/src/export-mocks/headers/headers.ts b/code/frameworks/nextjs-vite/src/export-mocks/headers/headers.ts new file mode 100644 index 000000000000..d9eb5177b447 --- /dev/null +++ b/code/frameworks/nextjs-vite/src/export-mocks/headers/headers.ts @@ -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(); +}; diff --git a/code/frameworks/nextjs-vite/src/export-mocks/headers/index.ts b/code/frameworks/nextjs-vite/src/export-mocks/headers/index.ts new file mode 100644 index 000000000000..1ec2cbd2915f --- /dev/null +++ b/code/frameworks/nextjs-vite/src/export-mocks/headers/index.ts @@ -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 }; diff --git a/code/frameworks/nextjs-vite/src/export-mocks/navigation/index.ts b/code/frameworks/nextjs-vite/src/export-mocks/navigation/index.ts new file mode 100644 index 000000000000..09a013578250 --- /dev/null +++ b/code/frameworks/nextjs-vite/src/export-mocks/navigation/index.ts @@ -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'); diff --git a/code/frameworks/nextjs-vite/src/export-mocks/router/index.ts b/code/frameworks/nextjs-vite/src/export-mocks/router/index.ts new file mode 100644 index 000000000000..e776674c5cef --- /dev/null +++ b/code/frameworks/nextjs-vite/src/export-mocks/router/index.ts @@ -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) => { + const routerActions: Partial = { + 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'); diff --git a/code/frameworks/nextjs-vite/src/head-manager/decorator.tsx b/code/frameworks/nextjs-vite/src/head-manager/decorator.tsx new file mode 100644 index 000000000000..794ad9c77cb9 --- /dev/null +++ b/code/frameworks/nextjs-vite/src/head-manager/decorator.tsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import HeadManagerProvider from './head-manager-provider'; + +export const HeadManagerDecorator = (Story: React.FC): React.ReactNode => { + return ( + + + + ); +}; diff --git a/code/frameworks/nextjs-vite/src/head-manager/head-manager-provider.tsx b/code/frameworks/nextjs-vite/src/head-manager/head-manager-provider.tsx new file mode 100644 index 000000000000..ee62063351ea --- /dev/null +++ b/code/frameworks/nextjs-vite/src/head-manager/head-manager-provider.tsx @@ -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; + updateScripts?: ((state: any) => void) | undefined; + scripts?: any; + getIsSsr?: () => boolean; + appDir?: boolean | undefined; + nonce?: string | undefined; +}; + +const HeadManagerProvider: React.FC = ({ children }) => { + const headManager: HeadManagerValue = useMemo(initHeadManager, []); + headManager.getIsSsr = () => false; + + return {children}; +}; + +export default HeadManagerProvider; diff --git a/code/frameworks/nextjs-vite/src/images/decorator.tsx b/code/frameworks/nextjs-vite/src/images/decorator.tsx new file mode 100644 index 000000000000..2e0bc929d0f3 --- /dev/null +++ b/code/frameworks/nextjs-vite/src/images/decorator.tsx @@ -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 ; + } + + return ( + + + + ); +}; diff --git a/code/frameworks/nextjs-vite/src/index.ts b/code/frameworks/nextjs-vite/src/index.ts new file mode 100644 index 000000000000..a904f93ec89d --- /dev/null +++ b/code/frameworks/nextjs-vite/src/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './portable-stories'; diff --git a/code/frameworks/nextjs-vite/src/portable-stories.ts b/code/frameworks/nextjs-vite/src/portable-stories.ts new file mode 100644 index 000000000000..e682a103ef24 --- /dev/null +++ b/code/frameworks/nextjs-vite/src/portable-stories.ts @@ -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 + | NamedOrDefaultProjectAnnotations[] +): ProjectAnnotations { + return originalSetProjectAnnotations(projectAnnotations); +} + +// This will not be necessary once we have auto preset loading +const defaultProjectAnnotations: ProjectAnnotations = 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(Hello world); + * 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( + story: StoryAnnotationsOrFn, + componentAnnotations: Meta, + projectAnnotations?: ProjectAnnotations, + exportsName?: string +): ComposedStoryFn> { + return originalComposeStory( + story as StoryAnnotationsOrFn, + 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(Hello world); + * 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>( + csfExports: TModule, + projectAnnotations?: ProjectAnnotations +) { + // @ts-expect-error (Converted from ts-ignore) + const composedStories = originalComposeStories(csfExports, projectAnnotations, composeStory); + + return composedStories as unknown as Omit< + StoriesWithPartialProps, + keyof Store_CSFExports + >; +} diff --git a/code/frameworks/nextjs-vite/src/preset.ts b/code/frameworks/nextjs-vite/src/preset.ts new file mode 100644 index 000000000000..090970b664fd --- /dev/null +++ b/code/frameworks/nextjs-vite/src/preset.ts @@ -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; +}; diff --git a/code/frameworks/nextjs-vite/src/preview.tsx b/code/frameworks/nextjs-vite/src/preview.tsx new file mode 100644 index 000000000000..9b1207330728 --- /dev/null +++ b/code/frameworks/nextjs-vite/src/preview.tsx @@ -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[] = [ + 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); + }, + }, + }, +}; diff --git a/code/frameworks/nextjs-vite/src/routing/app-router-provider.tsx b/code/frameworks/nextjs-vite/src/routing/app-router-provider.tsx new file mode 100644 index 000000000000..8528bdbbf82c --- /dev/null +++ b/code/frameworks/nextjs-vite/src/routing/app-router-provider.tsx @@ -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): FlightRouterState => { + const segment = segmentsList.shift(); + + if (segment) { + return [segment, { children: getParallelRoutes(segmentsList) }]; + } + + return [] as any; +}; + +export const AppRouterProvider: React.FC> = ({ + 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 ( + + + + + + + {children} + + + + + + + ); +}; diff --git a/code/frameworks/nextjs-vite/src/routing/decorator.tsx b/code/frameworks/nextjs-vite/src/routing/decorator.tsx new file mode 100644 index 000000000000..2bb4211bd86c --- /dev/null +++ b/code/frameworks/nextjs-vite/src/routing/decorator.tsx @@ -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 ( + + {/* + 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. + */} + + + + + ); + } + + return ( + + + + ); +}; diff --git a/code/frameworks/nextjs-vite/src/routing/page-router-provider.tsx b/code/frameworks/nextjs-vite/src/routing/page-router-provider.tsx new file mode 100644 index 000000000000..91fa47cfc199 --- /dev/null +++ b/code/frameworks/nextjs-vite/src/routing/page-router-provider.tsx @@ -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 = ({ children }) => ( + {children} +); diff --git a/code/frameworks/nextjs-vite/src/routing/types.tsx b/code/frameworks/nextjs-vite/src/routing/types.tsx new file mode 100644 index 000000000000..e80b0413260f --- /dev/null +++ b/code/frameworks/nextjs-vite/src/routing/types.tsx @@ -0,0 +1,7 @@ +export type RouteParams = { + pathname: string; + query: Record; + [key: string]: any; +}; + +export type NextAppDirectory = boolean; diff --git a/code/frameworks/nextjs-vite/src/styledJsx/decorator.tsx b/code/frameworks/nextjs-vite/src/styledJsx/decorator.tsx new file mode 100644 index 000000000000..5e18664c9659 --- /dev/null +++ b/code/frameworks/nextjs-vite/src/styledJsx/decorator.tsx @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { StyleRegistry } from 'styled-jsx'; + +export const StyledJsxDecorator = (Story: React.FC): React.ReactNode => ( + + + +); diff --git a/code/frameworks/nextjs-vite/src/types.ts b/code/frameworks/nextjs-vite/src/types.ts new file mode 100644 index 000000000000..d33f91d1ccc2 --- /dev/null +++ b/code/frameworks/nextjs-vite/src/types.ts @@ -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 & {} diff --git a/code/frameworks/nextjs-vite/src/typings.d.ts b/code/frameworks/nextjs-vite/src/typings.d.ts new file mode 100644 index 000000000000..090a63a18725 --- /dev/null +++ b/code/frameworks/nextjs-vite/src/typings.d.ts @@ -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 & { + src: string | StaticImport; + } + > & + Omit + >; +} + +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 = React.Context; + export function createContext( + // If you thought this should be optional, see + // https://github.com/DefinitelyTyped/DefinitelyTyped/pull/24509#issuecomment-382213106 + defaultValue: T + ): Context; +} diff --git a/code/frameworks/nextjs-vite/template/cli/.eslintrc.json b/code/frameworks/nextjs-vite/template/cli/.eslintrc.json new file mode 100644 index 000000000000..2ce44cb74ab3 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/.eslintrc.json @@ -0,0 +1,7 @@ +{ + "rules": { + "import/no-extraneous-dependencies": "off", + "import/extensions": "off", + "react/no-unknown-property": "off" + } +} diff --git a/code/frameworks/nextjs-vite/template/cli/js/Button.jsx b/code/frameworks/nextjs-vite/template/cli/js/Button.jsx new file mode 100644 index 000000000000..7c613bbf2599 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/js/Button.jsx @@ -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.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, +}; diff --git a/code/frameworks/nextjs-vite/template/cli/js/Button.stories.js b/code/frameworks/nextjs-vite/template/cli/js/Button.stories.js new file mode 100644 index 000000000000..97b9ec0ed85d --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/js/Button.stories.js @@ -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', + }, +}; diff --git a/code/frameworks/nextjs-vite/template/cli/js/Configure.mdx b/code/frameworks/nextjs-vite/template/cli/js/Configure.mdx new file mode 100644 index 000000000000..cc3292373f73 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/js/Configure.mdx @@ -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 = () => + + + + + +
+
+ # 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. +
+
+
+ A wall of logos representing different styling technologies +

Add styling and CSS

+

Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.

+ Learn more +
+
+ An abstraction representing the composition of data for a component +

Provide context and mocking

+

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.

+ Learn more +
+
+ A representation of typography and image assets +
+

Load assets and resources

+

To link static files (like fonts) to your projects and stories, use the + `staticDirs` configuration option to specify folders to load when + starting Storybook.

+ Learn more +
+
+
+
+
+
+ # 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. +
+ +
+
+
+ A screenshot showing the autodocs tag being set, pointing a docs page being generated +

Autodocs

+

Auto-generate living, + interactive reference documentation from your components and stories.

+ Learn more +
+
+ A browser window showing a Storybook being published to a chromatic.com URL +

Publish to Chromatic

+

Publish your Storybook to review and collaborate with your entire team.

+ Learn more +
+
+ Windows showing the Storybook plugin in Figma +

Figma Plugin

+

Embed your stories into Figma to cross-reference the design and live + implementation in one place.

+ Learn more +
+
+ Screenshot of tests passing and failing +

Testing

+

Use stories to test a component in all its variations, no matter how + complex.

+ Learn more +
+
+ Screenshot of accessibility tests passing and failing +

Accessibility

+

Automatically test your components for a11y issues as you develop.

+ Learn more +
+
+ Screenshot of Storybook in light and dark mode +

Theming

+

Theme Storybook's UI to personalize it to your project.

+ Learn more +
+
+
+
+
+
+

Addons

+

Integrate your tools with Storybook to connect workflows.

+ Discover all addons +
+
+ Integrate your tools with Storybook to connect workflows. +
+
+ +
+
+ Github logo + Join our contributors building the future of UI development. + + Star on GitHub +
+
+ Discord logo +
+ Get support and chat with frontend developers. + + Join Discord server +
+
+
+ Youtube logo +
+ Watch tutorials, feature previews and interviews. + + Watch on YouTube +
+
+
+ A book +

Follow guided walkthroughs on for key workflows.

+ + Discover tutorials +
+
+ + diff --git a/code/frameworks/nextjs-vite/template/cli/js/Header.jsx b/code/frameworks/nextjs-vite/template/cli/js/Header.jsx new file mode 100644 index 000000000000..39e5226cffc1 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/js/Header.jsx @@ -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 }) => ( +
+
+
+ + + + + + + +

Acme

+
+
+ {user ? ( + <> + + Welcome, {user.name}! + +
+
+
+); + +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, +}; diff --git a/code/frameworks/nextjs-vite/template/cli/js/Header.stories.js b/code/frameworks/nextjs-vite/template/cli/js/Header.stories.js new file mode 100644 index 000000000000..982cd970fb5c --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/js/Header.stories.js @@ -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: {}, +}; diff --git a/code/frameworks/nextjs-vite/template/cli/js/Page.jsx b/code/frameworks/nextjs-vite/template/cli/js/Page.jsx new file mode 100644 index 000000000000..6db1e0ac3f36 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/js/Page.jsx @@ -0,0 +1,68 @@ +import React from 'react'; + +import { Header } from './Header'; +import './page.css'; + +export const Page = () => { + const [user, setUser] = React.useState(); + + return ( +
+
setUser({ name: 'Jane Doe' })} + onLogout={() => setUser(undefined)} + onCreateAccount={() => setUser({ name: 'Jane Doe' })} + /> +
+

Pages in Storybook

+

+ We recommend building UIs with a{' '} + + component-driven + {' '} + process starting with atomic components and ending with pages. +

+

+ 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: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at{' '} + + Storybook tutorials + + . Read more in the{' '} + + docs + + . +

+
+ Tip Adjust the width of the canvas with the{' '} + + + + + + Viewports addon in the toolbar +
+
+
+ ); +}; diff --git a/code/frameworks/nextjs-vite/template/cli/js/Page.stories.js b/code/frameworks/nextjs-vite/template/cli/js/Page.stories.js new file mode 100644 index 000000000000..53362f140e61 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/js/Page.stories.js @@ -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(); + }, +}; diff --git a/code/frameworks/nextjs-vite/template/cli/ts-3-8/Button.stories.ts b/code/frameworks/nextjs-vite/template/cli/ts-3-8/Button.stories.ts new file mode 100644 index 000000000000..2054fc59231e --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/ts-3-8/Button.stories.ts @@ -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 = { + 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; + +// 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', + }, +}; diff --git a/code/frameworks/nextjs-vite/template/cli/ts-3-8/Button.tsx b/code/frameworks/nextjs-vite/template/cli/ts-3-8/Button.tsx new file mode 100644 index 000000000000..30a6bcb96e49 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/ts-3-8/Button.tsx @@ -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 ( + + ); +}; diff --git a/code/frameworks/nextjs-vite/template/cli/ts-3-8/Configure.mdx b/code/frameworks/nextjs-vite/template/cli/ts-3-8/Configure.mdx new file mode 100644 index 000000000000..cc3292373f73 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/ts-3-8/Configure.mdx @@ -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 = () => + + + + + +
+
+ # 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. +
+
+
+ A wall of logos representing different styling technologies +

Add styling and CSS

+

Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.

+ Learn more +
+
+ An abstraction representing the composition of data for a component +

Provide context and mocking

+

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.

+ Learn more +
+
+ A representation of typography and image assets +
+

Load assets and resources

+

To link static files (like fonts) to your projects and stories, use the + `staticDirs` configuration option to specify folders to load when + starting Storybook.

+ Learn more +
+
+
+
+
+
+ # 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. +
+ +
+
+
+ A screenshot showing the autodocs tag being set, pointing a docs page being generated +

Autodocs

+

Auto-generate living, + interactive reference documentation from your components and stories.

+ Learn more +
+
+ A browser window showing a Storybook being published to a chromatic.com URL +

Publish to Chromatic

+

Publish your Storybook to review and collaborate with your entire team.

+ Learn more +
+
+ Windows showing the Storybook plugin in Figma +

Figma Plugin

+

Embed your stories into Figma to cross-reference the design and live + implementation in one place.

+ Learn more +
+
+ Screenshot of tests passing and failing +

Testing

+

Use stories to test a component in all its variations, no matter how + complex.

+ Learn more +
+
+ Screenshot of accessibility tests passing and failing +

Accessibility

+

Automatically test your components for a11y issues as you develop.

+ Learn more +
+
+ Screenshot of Storybook in light and dark mode +

Theming

+

Theme Storybook's UI to personalize it to your project.

+ Learn more +
+
+
+
+
+
+

Addons

+

Integrate your tools with Storybook to connect workflows.

+ Discover all addons +
+
+ Integrate your tools with Storybook to connect workflows. +
+
+ +
+
+ Github logo + Join our contributors building the future of UI development. + + Star on GitHub +
+
+ Discord logo +
+ Get support and chat with frontend developers. + + Join Discord server +
+
+
+ Youtube logo +
+ Watch tutorials, feature previews and interviews. + + Watch on YouTube +
+
+
+ A book +

Follow guided walkthroughs on for key workflows.

+ + Discover tutorials +
+
+ + diff --git a/code/frameworks/nextjs-vite/template/cli/ts-3-8/Header.stories.ts b/code/frameworks/nextjs-vite/template/cli/ts-3-8/Header.stories.ts new file mode 100644 index 000000000000..feddeae98faf --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/ts-3-8/Header.stories.ts @@ -0,0 +1,33 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { fn } from '@storybook/test'; + +import { Header } from './Header'; + +const meta: 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(), + }, +}; + +export default meta; +type Story = StoryObj; + +export const LoggedIn: Story = { + args: { + user: { + name: 'Jane Doe', + }, + }, +}; + +export const LoggedOut: Story = {}; diff --git a/code/frameworks/nextjs-vite/template/cli/ts-3-8/Header.tsx b/code/frameworks/nextjs-vite/template/cli/ts-3-8/Header.tsx new file mode 100644 index 000000000000..1bf981a4251f --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/ts-3-8/Header.tsx @@ -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) => ( +
+
+
+ + + + + + + +

Acme

+
+
+ {user ? ( + <> + + Welcome, {user.name}! + +
+
+
+); diff --git a/code/frameworks/nextjs-vite/template/cli/ts-3-8/Page.stories.ts b/code/frameworks/nextjs-vite/template/cli/ts-3-8/Page.stories.ts new file mode 100644 index 000000000000..4bcf593cafa7 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/ts-3-8/Page.stories.ts @@ -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 = { + 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; + +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(); + }, +}; diff --git a/code/frameworks/nextjs-vite/template/cli/ts-3-8/Page.tsx b/code/frameworks/nextjs-vite/template/cli/ts-3-8/Page.tsx new file mode 100644 index 000000000000..e11748301390 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/ts-3-8/Page.tsx @@ -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(); + + return ( +
+
setUser({ name: 'Jane Doe' })} + onLogout={() => setUser(undefined)} + onCreateAccount={() => setUser({ name: 'Jane Doe' })} + /> + +
+

Pages in Storybook

+

+ We recommend building UIs with a{' '} + + component-driven + {' '} + process starting with atomic components and ending with pages. +

+

+ 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: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at{' '} + + Storybook tutorials + + . Read more in the{' '} + + docs + + . +

+
+ Tip Adjust the width of the canvas with the{' '} + + + + + + Viewports addon in the toolbar +
+
+
+ ); +}; diff --git a/code/frameworks/nextjs-vite/template/cli/ts-4-9/Button.stories.ts b/code/frameworks/nextjs-vite/template/cli/ts-4-9/Button.stories.ts new file mode 100644 index 000000000000..455a9d8601c9 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/ts-4-9/Button.stories.ts @@ -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; + +export default meta; +type Story = StoryObj; + +// 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', + }, +}; diff --git a/code/frameworks/nextjs-vite/template/cli/ts-4-9/Button.tsx b/code/frameworks/nextjs-vite/template/cli/ts-4-9/Button.tsx new file mode 100644 index 000000000000..30a6bcb96e49 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/ts-4-9/Button.tsx @@ -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 ( + + ); +}; diff --git a/code/frameworks/nextjs-vite/template/cli/ts-4-9/Configure.mdx b/code/frameworks/nextjs-vite/template/cli/ts-4-9/Configure.mdx new file mode 100644 index 000000000000..cc3292373f73 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/ts-4-9/Configure.mdx @@ -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 = () => + + + + + +
+
+ # 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. +
+
+
+ A wall of logos representing different styling technologies +

Add styling and CSS

+

Like with web applications, there are many ways to include CSS within Storybook. Learn more about setting up styling within Storybook.

+ Learn more +
+
+ An abstraction representing the composition of data for a component +

Provide context and mocking

+

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.

+ Learn more +
+
+ A representation of typography and image assets +
+

Load assets and resources

+

To link static files (like fonts) to your projects and stories, use the + `staticDirs` configuration option to specify folders to load when + starting Storybook.

+ Learn more +
+
+
+
+
+
+ # 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. +
+ +
+
+
+ A screenshot showing the autodocs tag being set, pointing a docs page being generated +

Autodocs

+

Auto-generate living, + interactive reference documentation from your components and stories.

+ Learn more +
+
+ A browser window showing a Storybook being published to a chromatic.com URL +

Publish to Chromatic

+

Publish your Storybook to review and collaborate with your entire team.

+ Learn more +
+
+ Windows showing the Storybook plugin in Figma +

Figma Plugin

+

Embed your stories into Figma to cross-reference the design and live + implementation in one place.

+ Learn more +
+
+ Screenshot of tests passing and failing +

Testing

+

Use stories to test a component in all its variations, no matter how + complex.

+ Learn more +
+
+ Screenshot of accessibility tests passing and failing +

Accessibility

+

Automatically test your components for a11y issues as you develop.

+ Learn more +
+
+ Screenshot of Storybook in light and dark mode +

Theming

+

Theme Storybook's UI to personalize it to your project.

+ Learn more +
+
+
+
+
+
+

Addons

+

Integrate your tools with Storybook to connect workflows.

+ Discover all addons +
+
+ Integrate your tools with Storybook to connect workflows. +
+
+ +
+
+ Github logo + Join our contributors building the future of UI development. + + Star on GitHub +
+
+ Discord logo +
+ Get support and chat with frontend developers. + + Join Discord server +
+
+
+ Youtube logo +
+ Watch tutorials, feature previews and interviews. + + Watch on YouTube +
+
+
+ A book +

Follow guided walkthroughs on for key workflows.

+ + Discover tutorials +
+
+ + diff --git a/code/frameworks/nextjs-vite/template/cli/ts-4-9/Header.stories.ts b/code/frameworks/nextjs-vite/template/cli/ts-4-9/Header.stories.ts new file mode 100644 index 000000000000..39d15874f4c1 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/ts-4-9/Header.stories.ts @@ -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; + +export default meta; +type Story = StoryObj; + +export const LoggedIn: Story = { + args: { + user: { + name: 'Jane Doe', + }, + }, +}; + +export const LoggedOut: Story = {}; diff --git a/code/frameworks/nextjs-vite/template/cli/ts-4-9/Header.tsx b/code/frameworks/nextjs-vite/template/cli/ts-4-9/Header.tsx new file mode 100644 index 000000000000..1bf981a4251f --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/ts-4-9/Header.tsx @@ -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) => ( +
+
+
+ + + + + + + +

Acme

+
+
+ {user ? ( + <> + + Welcome, {user.name}! + +
+
+
+); diff --git a/code/frameworks/nextjs-vite/template/cli/ts-4-9/Page.stories.ts b/code/frameworks/nextjs-vite/template/cli/ts-4-9/Page.stories.ts new file mode 100644 index 000000000000..f7a06817f85a --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/ts-4-9/Page.stories.ts @@ -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; + +export default meta; +type Story = StoryObj; + +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(); + }, +}; diff --git a/code/frameworks/nextjs-vite/template/cli/ts-4-9/Page.tsx b/code/frameworks/nextjs-vite/template/cli/ts-4-9/Page.tsx new file mode 100644 index 000000000000..e11748301390 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/cli/ts-4-9/Page.tsx @@ -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(); + + return ( +
+
setUser({ name: 'Jane Doe' })} + onLogout={() => setUser(undefined)} + onCreateAccount={() => setUser({ name: 'Jane Doe' })} + /> + +
+

Pages in Storybook

+

+ We recommend building UIs with a{' '} + + component-driven + {' '} + process starting with atomic components and ending with pages. +

+

+ 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: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at{' '} + + Storybook tutorials + + . Read more in the{' '} + + docs + + . +

+
+ Tip Adjust the width of the canvas with the{' '} + + + + + + Viewports addon in the toolbar +
+
+
+ ); +}; diff --git a/code/frameworks/nextjs-vite/template/next-env.d.ts b/code/frameworks/nextjs-vite/template/next-env.d.ts new file mode 100644 index 000000000000..77e567dab0a2 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/next-env.d.ts @@ -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. + +/// +/// diff --git a/code/frameworks/nextjs-vite/template/stories/DynamicImport.stories.tsx b/code/frameworks/nextjs-vite/template/stories/DynamicImport.stories.tsx new file mode 100644 index 000000000000..47d1fd3e0af4 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/DynamicImport.stories.tsx @@ -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 ( + + + + ); +} + +const meta = { + component: Component, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/code/frameworks/nextjs-vite/template/stories/DynamicImport.tsx b/code/frameworks/nextjs-vite/template/stories/DynamicImport.tsx new file mode 100644 index 000000000000..4863633033f3 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/DynamicImport.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export default function DynamicComponent() { + return
I am a dynamically loaded component
; +} diff --git a/code/frameworks/nextjs-vite/template/stories/Font.stories.tsx b/code/frameworks/nextjs-vite/template/stories/Font.stories.tsx new file mode 100644 index 000000000000..b91247b1392b --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/Font.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Font from './Font'; + +const meta = { + component: Font, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const WithClassName: Story = { + args: { + variant: 'className', + }, +}; + +export const WithStyle: Story = { + args: { + variant: 'style', + }, +}; + +export const WithVariable: Story = { + args: { + variant: 'variable', + }, +}; diff --git a/code/frameworks/nextjs-vite/template/stories/Font.tsx b/code/frameworks/nextjs-vite/template/stories/Font.tsx new file mode 100644 index 000000000000..cdfde7def069 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/Font.tsx @@ -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 ( +
+

Google Rubik Puddles

+

Google Local Rubik Storm

+
+ ); + case 'style': + return ( +
+

Google Rubik Puddles

+

Google Local Rubik Storm

+
+ ); + case 'variable': + return ( +
+
+

+ Google Rubik Puddles +

+
+
+

+ Google Local Rubik Storm +

+
+
+ ); + default: + return null; + } +} diff --git a/code/frameworks/nextjs-vite/template/stories/GetImageProps.stories.tsx b/code/frameworks/nextjs-vite/template/stories/GetImageProps.stories.tsx new file mode 100644 index 000000000000..e955ed2e6b2b --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/GetImageProps.stories.tsx @@ -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) => { + 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 ( + + + + + + ); +}; + +const meta = { + component: Component, + args: { + alt: 'getImageProps Example', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/code/frameworks/nextjs-vite/template/stories/Head.stories.tsx b/code/frameworks/nextjs-vite/template/stories/Head.stories.tsx new file mode 100644 index 000000000000..23ae3526055a --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/Head.stories.tsx @@ -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 ( +
+ + Next.js Head Title + + + + + +

Hello world!

+
+ ); +} + +const meta = { + component: Component, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +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' + ); + }, +}; diff --git a/code/frameworks/nextjs-vite/template/stories/Image.stories.tsx b/code/frameworks/nextjs-vite/template/stories/Image.stories.tsx new file mode 100644 index 000000000000..ad18f125b523 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/Image.stories.tsx @@ -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; + +export default meta; + +type Story = StoryObj; + +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) =>
{Story()}
, + ], +}; + +export const Sized: Story = { + args: { + fill: true, + sizes: '(max-width: 600px) 100vw, 600px', + }, + decorators: [ + (Story) =>
{Story()}
, + ], +}; + +export const Lazy: Story = { + args: { + src: 'https://storybook.js.org/images/placeholders/50x50.png', + width: 50, + height: 50, + }, + decorators: [ + (Story) => ( + <> +
+ {Story()} + + ), + ], +}; + +export const Eager: Story = { + ...Lazy, + parameters: { + nextjs: { + image: { + loading: 'eager', + }, + }, + }, +}; + +export const WithRef: Story = { + render() { + const [ref, setRef] = useState(null); + + return ( +
+ Accessibility +

Alt attribute of image: {ref?.alt}

+
+ ); + }, +}; diff --git a/code/frameworks/nextjs-vite/template/stories/ImageLegacy.stories.tsx b/code/frameworks/nextjs-vite/template/stories/ImageLegacy.stories.tsx new file mode 100644 index 000000000000..b8a781499bd1 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/ImageLegacy.stories.tsx @@ -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 }, + }, +}; diff --git a/code/frameworks/nextjs-vite/template/stories/Link.stories.module.css b/code/frameworks/nextjs-vite/template/stories/Link.stories.module.css new file mode 100644 index 000000000000..9edb616226d0 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/Link.stories.module.css @@ -0,0 +1,3 @@ +.link { + color: green; +} diff --git a/code/frameworks/nextjs-vite/template/stories/Link.stories.tsx b/code/frameworks/nextjs-vite/template/stories/Link.stories.tsx new file mode 100644 index 000000000000..1bc99d187e3d --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/Link.stories.tsx @@ -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, HTMLAnchorElement> +>(function Button({ onClick, href, children }, ref) { + return ( + + {children} + + ); +}); + +const Component = () => ( +
    +
  • + Normal Link +
  • +
  • + + With URL Object + +
  • +
  • + + Replace the URL instead of push + +
  • +
  • + + Legacy behavior + +
  • +
  • + + child is a functional component + +
  • +
  • + + Disables scrolling to the top + +
  • +
  • + + No Prefetching + +
  • +
  • + + With style + +
  • +
  • + + With className + +
  • +
+); + +export default { + component: Component, +} as Meta; + +export const Default: StoryObj = {}; + +export const InAppDir: StoryObj = { + parameters: { + nextjs: { + appDirectory: true, + }, + }, +}; diff --git a/code/frameworks/nextjs-vite/template/stories/Navigation.stories.tsx b/code/frameworks/nextjs-vite/template/stories/Navigation.stories.tsx new file mode 100644 index 000000000000..ebedcf962a45 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/Navigation.stories.tsx @@ -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 ( +
+
pathname: {pathname}
+
segment: {segment}
+
segments: {segments.join(',')}
+
+ searchparams:{' '} +
    + {searchParamsList.map(([key, value]) => ( +
  • + {key}: {value} +
  • + ))} +
+
+
+ params:{' '} +
    + {Object.entries(params).map(([key, value]) => ( +
  • + {key}: {value} +
  • + ))} +
+
+ {routerActions.map(({ cb, name }) => ( +
+ +
+ ))} +
+ ); +} + +type Story = StoryObj; + +export default { + component: Component, + parameters: { + nextjs: { + appDirectory: true, + navigation: { + pathname: '/hello', + query: { + foo: 'bar', + }, + prefetch: () => { + console.log('custom prefetch'); + }, + }, + }, + }, +} as Meta; + +export const Default: StoryObj = { + 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'], + ], + }, + }, + }, +}; diff --git a/code/frameworks/nextjs-vite/template/stories/NextHeader.stories.tsx b/code/frameworks/nextjs-vite/template/stories/NextHeader.stories.tsx new file mode 100644 index 000000000000..8fbecc3fffc5 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/NextHeader.stories.tsx @@ -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; + +type Story = StoryObj; + +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', + }); + }); + }, +}; diff --git a/code/frameworks/nextjs-vite/template/stories/NextHeader.tsx b/code/frameworks/nextjs-vite/template/stories/NextHeader.tsx new file mode 100644 index 000000000000..b93c9611c774 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/NextHeader.tsx @@ -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 ( + <> +

Cookies:

+ {cookies() + .getAll() + .map(({ name, value }) => { + return ( +

+ Name: {name} + Value: {value} +

+ ); + })} + +

Headers:

+ {Array.from(headers().entries()).map(([name, value]: [string, string]) => { + return ( +

+ Name: {name} + Value: {value} +

+ ); + })} + +
+ +
+ + ); +} diff --git a/code/frameworks/nextjs-vite/template/stories/RSC.jsx b/code/frameworks/nextjs-vite/template/stories/RSC.jsx new file mode 100644 index 000000000000..9c894710b439 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/RSC.jsx @@ -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}; diff --git a/code/frameworks/nextjs-vite/template/stories/RSC.stories.jsx b/code/frameworks/nextjs-vite/template/stories/RSC.stories.jsx new file mode 100644 index 000000000000..505c6a3acab4 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/RSC.stories.jsx @@ -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) => ( + + + + ), +}; diff --git a/code/frameworks/nextjs-vite/template/stories/Redirect.stories.tsx b/code/frameworks/nextjs-vite/template/stories/Redirect.stories.tsx new file mode 100644 index 000000000000..05f4d5a68eab --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/Redirect.stories.tsx @@ -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 ( +
+
{state}
+
{ + state = 'State is invalidated successfully.'; + redirect('/'); + }} + > + +
+
+ ); + }, + 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')); + }, +}; diff --git a/code/frameworks/nextjs-vite/template/stories/Router.stories.tsx b/code/frameworks/nextjs-vite/template/stories/Router.stories.tsx new file mode 100644 index 000000000000..cfdb761b2210 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/Router.stories.tsx @@ -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 ( +
+
Router pathname: {Router.pathname}
+
pathname: {router.pathname}
+
+ searchparams:{' '} +
    + {Object.entries(searchParams).map(([key, value]) => ( +
  • + {key}: {value} +
  • + ))} +
+
+ {routerActions.map(({ cb, name }) => ( +
+ +
+ ))} +
+ ); +} + +export default { + component: Component, + parameters: { + nextjs: { + router: { + pathname: '/hello', + query: { + foo: 'bar', + }, + prefetch: () => { + console.log('custom prefetch'); + }, + }, + }, + }, +} as Meta; + +export const Default: StoryObj = { + 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'); + }); + }, +}; diff --git a/code/frameworks/nextjs-vite/template/stories/ServerActions.stories.tsx b/code/frameworks/nextjs-vite/template/stories/ServerActions.stories.tsx new file mode 100644 index 000000000000..893a0e2009f4 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/ServerActions.stories.tsx @@ -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 ( +
+
+ +
+
+ +
+
+ +
+
+ ); +} + +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; + +export const ProtectedWhileLoggedOut: StoryObj = { + 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 = { + 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 = { + 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 = { + 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()); + }, +}; diff --git a/code/frameworks/nextjs-vite/template/stories/ServerActions.tsx b/code/frameworks/nextjs-vite/template/stories/ServerActions.tsx new file mode 100644 index 000000000000..ffeba72ab848 --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/ServerActions.tsx @@ -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('/'); +} diff --git a/code/frameworks/nextjs-vite/template/stories/StyledJsx.stories.jsx b/code/frameworks/nextjs-vite/template/stories/StyledJsx.stories.jsx new file mode 100644 index 000000000000..5a0c586e232c --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/StyledJsx.stories.jsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const Component = () => ( +
+ +
+

This is styled using Styled JSX

+
+
+); + +export default { + component: Component, +}; + +export const Default = {}; diff --git a/code/frameworks/nextjs-vite/template/stories/fonts/OFL.txt b/code/frameworks/nextjs-vite/template/stories/fonts/OFL.txt new file mode 100644 index 000000000000..36d2f6f3febb --- /dev/null +++ b/code/frameworks/nextjs-vite/template/stories/fonts/OFL.txt @@ -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. diff --git a/code/frameworks/nextjs-vite/template/stories/fonts/RubikStorm-Regular.ttf b/code/frameworks/nextjs-vite/template/stories/fonts/RubikStorm-Regular.ttf new file mode 100644 index 000000000000..2304ee84a07b Binary files /dev/null and b/code/frameworks/nextjs-vite/template/stories/fonts/RubikStorm-Regular.ttf differ diff --git a/code/frameworks/nextjs-vite/tsconfig.json b/code/frameworks/nextjs-vite/tsconfig.json new file mode 100644 index 000000000000..a444d30c7af1 --- /dev/null +++ b/code/frameworks/nextjs-vite/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "Preserve", + "moduleResolution": "Bundler" + }, + "include": ["src/**/*", "template/**/*"] +} diff --git a/code/frameworks/nextjs-vite/vitest.config.ts b/code/frameworks/nextjs-vite/vitest.config.ts new file mode 100644 index 000000000000..ea8b4ef14350 --- /dev/null +++ b/code/frameworks/nextjs-vite/vitest.config.ts @@ -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 + }) +); diff --git a/code/lib/cli/src/sandbox-templates.ts b/code/lib/cli/src/sandbox-templates.ts index e7a55f4ba360..b93d859736e8 100644 --- a/code/lib/cli/src/sandbox-templates.ts +++ b/code/lib/cli/src/sandbox-templates.ts @@ -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'], }, diff --git a/code/yarn.lock b/code/yarn.lock index 2810b36b4760..78f36e3a9493 100644 --- a/code/yarn.lock +++ b/code/yarn.lock @@ -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"