| @@ -1,53 +0,0 @@ | |||||
| import type { Meta, StoryObj } from '@storybook/react'; | |||||
| import { fn } from '@storybook/test'; | |||||
| import { Button } from './Button'; | |||||
| // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export | |||||
| const meta = { | |||||
| title: 'Example/Button', | |||||
| component: Button, | |||||
| parameters: { | |||||
| // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout | |||||
| layout: 'centered', | |||||
| }, | |||||
| // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs | |||||
| tags: ['autodocs'], | |||||
| // More on argTypes: https://storybook.js.org/docs/api/argtypes | |||||
| argTypes: { | |||||
| backgroundColor: { control: 'color' }, | |||||
| }, | |||||
| // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args | |||||
| args: { onClick: fn() }, | |||||
| } satisfies Meta<typeof Button>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args | |||||
| export const Primary: Story = { | |||||
| args: { | |||||
| primary: true, | |||||
| label: 'Button', | |||||
| }, | |||||
| }; | |||||
| export const Secondary: Story = { | |||||
| args: { | |||||
| label: 'Button', | |||||
| }, | |||||
| }; | |||||
| export const Large: Story = { | |||||
| args: { | |||||
| size: 'large', | |||||
| label: 'Button', | |||||
| }, | |||||
| }; | |||||
| export const Small: Story = { | |||||
| args: { | |||||
| size: 'small', | |||||
| label: 'Button', | |||||
| }, | |||||
| }; | |||||
| @@ -1,37 +0,0 @@ | |||||
| import React from 'react'; | |||||
| import './button.css'; | |||||
| export interface ButtonProps { | |||||
| /** Is this the principal call to action on the page? */ | |||||
| primary?: boolean; | |||||
| /** What background color to use */ | |||||
| backgroundColor?: string; | |||||
| /** How large should the button be? */ | |||||
| size?: 'small' | 'medium' | 'large'; | |||||
| /** Button contents */ | |||||
| label: string; | |||||
| /** Optional click handler */ | |||||
| onClick?: () => void; | |||||
| } | |||||
| /** Primary UI component for user interaction */ | |||||
| export const Button = ({ | |||||
| primary = false, | |||||
| size = 'medium', | |||||
| backgroundColor, | |||||
| label, | |||||
| ...props | |||||
| }: ButtonProps) => { | |||||
| const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; | |||||
| return ( | |||||
| <button | |||||
| type="button" | |||||
| className={['storybook-button', `storybook-button--${size}`, mode].join(' ')} | |||||
| style={{ backgroundColor }} | |||||
| {...props} | |||||
| > | |||||
| {label} | |||||
| </button> | |||||
| ); | |||||
| }; | |||||
| @@ -1,33 +0,0 @@ | |||||
| import type { Meta, StoryObj } from '@storybook/react'; | |||||
| import { fn } from '@storybook/test'; | |||||
| import { Header } from './Header'; | |||||
| const meta = { | |||||
| title: 'Example/Header', | |||||
| component: Header, | |||||
| // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs | |||||
| tags: ['autodocs'], | |||||
| parameters: { | |||||
| // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout | |||||
| layout: 'fullscreen', | |||||
| }, | |||||
| args: { | |||||
| onLogin: fn(), | |||||
| onLogout: fn(), | |||||
| onCreateAccount: fn(), | |||||
| }, | |||||
| } satisfies Meta<typeof Header>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| export const LoggedIn: Story = { | |||||
| args: { | |||||
| user: { | |||||
| name: 'Jane Doe', | |||||
| }, | |||||
| }, | |||||
| }; | |||||
| export const LoggedOut: Story = {}; | |||||
| @@ -1,56 +0,0 @@ | |||||
| import React from 'react'; | |||||
| import { Button } from './Button'; | |||||
| import './header.css'; | |||||
| type User = { | |||||
| name: string; | |||||
| }; | |||||
| export interface HeaderProps { | |||||
| user?: User; | |||||
| onLogin?: () => void; | |||||
| onLogout?: () => void; | |||||
| onCreateAccount?: () => void; | |||||
| } | |||||
| export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( | |||||
| <header> | |||||
| <div className="storybook-header"> | |||||
| <div> | |||||
| <svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"> | |||||
| <g fill="none" fillRule="evenodd"> | |||||
| <path | |||||
| d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z" | |||||
| fill="#FFF" | |||||
| /> | |||||
| <path | |||||
| d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z" | |||||
| fill="#555AB9" | |||||
| /> | |||||
| <path | |||||
| d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z" | |||||
| fill="#91BAF8" | |||||
| /> | |||||
| </g> | |||||
| </svg> | |||||
| <h1>Acme</h1> | |||||
| </div> | |||||
| <div> | |||||
| {user ? ( | |||||
| <> | |||||
| <span className="welcome"> | |||||
| Welcome, <b>{user.name}</b>! | |||||
| </span> | |||||
| <Button size="small" onClick={onLogout} label="Log out" /> | |||||
| </> | |||||
| ) : ( | |||||
| <> | |||||
| <Button size="small" onClick={onLogin} label="Log in" /> | |||||
| <Button primary size="small" onClick={onCreateAccount} label="Sign up" /> | |||||
| </> | |||||
| )} | |||||
| </div> | |||||
| </div> | |||||
| </header> | |||||
| ); | |||||
| @@ -1,32 +0,0 @@ | |||||
| import type { Meta, StoryObj } from '@storybook/react'; | |||||
| import { expect, userEvent, within } from '@storybook/test'; | |||||
| import { Page } from './Page'; | |||||
| const meta = { | |||||
| title: 'Example/Page', | |||||
| component: Page, | |||||
| parameters: { | |||||
| // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout | |||||
| layout: 'fullscreen', | |||||
| }, | |||||
| } satisfies Meta<typeof Page>; | |||||
| export default meta; | |||||
| type Story = StoryObj<typeof meta>; | |||||
| export const LoggedOut: Story = {}; | |||||
| // More on component testing: https://storybook.js.org/docs/writing-tests/component-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(); | |||||
| }, | |||||
| }; | |||||
| @@ -1,73 +0,0 @@ | |||||
| import React from 'react'; | |||||
| import { Header } from './Header'; | |||||
| import './page.css'; | |||||
| type User = { | |||||
| name: string; | |||||
| }; | |||||
| export const Page: React.FC = () => { | |||||
| const [user, setUser] = React.useState<User>(); | |||||
| return ( | |||||
| <article> | |||||
| <Header | |||||
| user={user} | |||||
| onLogin={() => setUser({ name: 'Jane Doe' })} | |||||
| onLogout={() => setUser(undefined)} | |||||
| onCreateAccount={() => setUser({ name: 'Jane Doe' })} | |||||
| /> | |||||
| <section className="storybook-page"> | |||||
| <h2>Pages in Storybook</h2> | |||||
| <p> | |||||
| We recommend building UIs with a{' '} | |||||
| <a href="https://componentdriven.org" target="_blank" rel="noopener noreferrer"> | |||||
| <strong>component-driven</strong> | |||||
| </a>{' '} | |||||
| process starting with atomic components and ending with pages. | |||||
| </p> | |||||
| <p> | |||||
| Render pages with mock data. This makes it easy to build and review page states without | |||||
| needing to navigate to them in your app. Here are some handy patterns for managing page | |||||
| data in Storybook: | |||||
| </p> | |||||
| <ul> | |||||
| <li> | |||||
| Use a higher-level connected component. Storybook helps you compose such data from the | |||||
| "args" of child component stories | |||||
| </li> | |||||
| <li> | |||||
| Assemble data in the page component from your services. You can mock these services out | |||||
| using Storybook. | |||||
| </li> | |||||
| </ul> | |||||
| <p> | |||||
| Get a guided tutorial on component-driven development at{' '} | |||||
| <a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer"> | |||||
| Storybook tutorials | |||||
| </a> | |||||
| . Read more in the{' '} | |||||
| <a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer"> | |||||
| docs | |||||
| </a> | |||||
| . | |||||
| </p> | |||||
| <div className="tip-wrapper"> | |||||
| <span className="tip">Tip</span> Adjust the width of the canvas with the{' '} | |||||
| <svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg"> | |||||
| <g fill="none" fillRule="evenodd"> | |||||
| <path | |||||
| d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z" | |||||
| id="a" | |||||
| fill="#999" | |||||
| /> | |||||
| </g> | |||||
| </svg> | |||||
| Viewports addon in the toolbar | |||||
| </div> | |||||
| </section> | |||||
| </article> | |||||
| ); | |||||
| }; | |||||
| @@ -1,30 +0,0 @@ | |||||
| .storybook-button { | |||||
| display: inline-block; | |||||
| cursor: pointer; | |||||
| border: 0; | |||||
| border-radius: 3em; | |||||
| font-weight: 700; | |||||
| line-height: 1; | |||||
| font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; | |||||
| } | |||||
| .storybook-button--primary { | |||||
| background-color: #555ab9; | |||||
| color: white; | |||||
| } | |||||
| .storybook-button--secondary { | |||||
| box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; | |||||
| background-color: transparent; | |||||
| color: #333; | |||||
| } | |||||
| .storybook-button--small { | |||||
| padding: 10px 16px; | |||||
| font-size: 12px; | |||||
| } | |||||
| .storybook-button--medium { | |||||
| padding: 11px 20px; | |||||
| font-size: 14px; | |||||
| } | |||||
| .storybook-button--large { | |||||
| padding: 12px 24px; | |||||
| font-size: 16px; | |||||
| } | |||||
| @@ -1,32 +0,0 @@ | |||||
| .storybook-header { | |||||
| display: flex; | |||||
| justify-content: space-between; | |||||
| align-items: center; | |||||
| border-bottom: 1px solid rgba(0, 0, 0, 0.1); | |||||
| padding: 15px 20px; | |||||
| font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; | |||||
| } | |||||
| .storybook-header svg { | |||||
| display: inline-block; | |||||
| vertical-align: top; | |||||
| } | |||||
| .storybook-header h1 { | |||||
| display: inline-block; | |||||
| vertical-align: top; | |||||
| margin: 6px 0 6px 10px; | |||||
| font-weight: 700; | |||||
| font-size: 20px; | |||||
| line-height: 1; | |||||
| } | |||||
| .storybook-header button + button { | |||||
| margin-left: 10px; | |||||
| } | |||||
| .storybook-header .welcome { | |||||
| margin-right: 10px; | |||||
| color: #333; | |||||
| font-size: 14px; | |||||
| } | |||||
| @@ -1,68 +0,0 @@ | |||||
| .storybook-page { | |||||
| margin: 0 auto; | |||||
| padding: 48px 20px; | |||||
| max-width: 600px; | |||||
| color: #333; | |||||
| font-size: 14px; | |||||
| line-height: 24px; | |||||
| font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; | |||||
| } | |||||
| .storybook-page h2 { | |||||
| display: inline-block; | |||||
| vertical-align: top; | |||||
| margin: 0 0 4px; | |||||
| font-weight: 700; | |||||
| font-size: 32px; | |||||
| line-height: 1; | |||||
| } | |||||
| .storybook-page p { | |||||
| margin: 1em 0; | |||||
| } | |||||
| .storybook-page a { | |||||
| color: inherit; | |||||
| } | |||||
| .storybook-page ul { | |||||
| margin: 1em 0; | |||||
| padding-left: 30px; | |||||
| } | |||||
| .storybook-page li { | |||||
| margin-bottom: 8px; | |||||
| } | |||||
| .storybook-page .tip { | |||||
| display: inline-block; | |||||
| vertical-align: top; | |||||
| margin-right: 10px; | |||||
| border-radius: 1em; | |||||
| background: #e7fdd8; | |||||
| padding: 4px 12px; | |||||
| color: #357a14; | |||||
| font-weight: 700; | |||||
| font-size: 11px; | |||||
| line-height: 12px; | |||||
| } | |||||
| .storybook-page .tip-wrapper { | |||||
| margin-top: 40px; | |||||
| margin-bottom: 40px; | |||||
| font-size: 13px; | |||||
| line-height: 20px; | |||||
| } | |||||
| .storybook-page .tip-wrapper svg { | |||||
| display: inline-block; | |||||
| vertical-align: top; | |||||
| margin-top: 3px; | |||||
| margin-right: 4px; | |||||
| width: 12px; | |||||
| height: 12px; | |||||
| } | |||||
| .storybook-page .tip-wrapper svg path { | |||||
| fill: #1ea7fd; | |||||
| } | |||||