John Cardozo
John Cardozo
Storybook es un entorno frontend
Construir
Probar
Organizar
Documentar
Componentes UI
Páginas
Procedimiento Aislado
Múltiples frameworks
No mezcla lógica de negocio
Aislamiento de desarrollo
Orientado a reutilización
Facilidad de pruebas
Integración con terceros + Addons
Mejora la calidad del código
Facilita la comunicación entre el equipo
Mejora la documentación
Componente
Estado 1
Estado 2
Estado 3
Estado 4
Chrome
Firefox
Safari
Opera
Smartphone
Laptop
Tablet
Desktop
Horizontal
Vertical
Componente
Estado 1
Estado 2
Estado 3
Estado 4
Historia 1
Historia 2
Historia 3
Historia 4
Variaciones
Ambiente aislado
CSS
SASS
Tailwind
Styled Components
Bootstrap
Material UI
Instalación
Verificación
node --version
npm --version
Node Package Manager
Node
Storybook se agrega a un proyecto existente
Ejemplo de creación de proyecto de React con Vite
npm init vite@latest nombre-proyecto -- --template react
Agregar Storybook al proyecto de React
npx storybook@latest init
Storybook no está hecho para proyectos vacíos
Instala las dependencias requeridas
Configura los Scripts de ejecución
Agrega la configuración de Storybook
Agrega algunos Stories de ejemplo
Cambios en el ambiente local
React
React + Storybook
React
npm run dev
Storybook
npm run storybook
Un Story representa el estado de un componente UI
Componente
Estado 1
Estado 2
Estado n
...
Story
Archivo .stories.js o stories.ts
Component Story Format (CSF)
Componente
Stories
jsx
stories.js
Estilos
css
Estructura básica del componente
import PropTypes from "prop-types";
export const Boton = ({ label }) => {
return <button type="button">{label}</button>;
};
Boton.propTypes = {
label: PropTypes.string.isRequired,
};
Boton.defaultProps = {
label: "Botón",
};
Boton / Boton.jsx
import { Boton } from "./Boton";
export default {
component: Boton,
};
export const Basic = {
args: {
label: "Hello world",
},
};
Boton / Boton.stories.js
Nombre del Story
Parámetros del Story
El archivo de Stories importa el componente
Componente
Story
Ejemplos
Representación visual del Componente / Story
Argumentos interactivos
import PropTypes from "prop-types";
import "./Boton.css";
export const Boton = ({ label, primary }) => {
const botonType = primary ? "btn-primary" : "btn-secondary";
return (
<button type="button" className={`btn ${botonType}`}>
{label}
</button>
);
};
Boton.propTypes = {
label: PropTypes.string.isRequired,
primary: PropTypes.bool,
};
Boton.defaultProps = {
label: "Botón",
primary: true,
};
Boton / Boton.jsx
.btn {
font-family: Arial;
border: 0;
font-weight: 800;
padding: 0.8rem 1.5rem;
border-radius: 9999px;
cursor: pointer;
}
.btn-primary {
background-color: #097bed;
color: #fff;
}
.btn-secondary {
background-color: #f0f0f0;
color: #404040;
border: 1px solid #404040;
}
Boton / Boton.css
export default {
component: Boton,
};
export const Primary = {
args: {
label: "Primary",
primary: true,
},
};
export const Secondary = {
args: {
label: "Secondary",
primary: false,
},
};
Boton / Boton.stories.js
El botón se visualiza diferente dependiendo de los parámetros
2 Stories mapean los nuevos argumentos
Nuevos controles
import PropTypes from "prop-types";
import "./Boton.css";
export const Boton = ({ label, primary, size }) => {
const botonType = primary ? "btn-primary" : "btn-secondary";
return (
<button type="button"
className={`btn ${botonType} btn-${size}`}>
{label}
</button>
);
};
Boton.propTypes = {
label: PropTypes.string.isRequired,
primary: PropTypes.bool,
size: PropTypes.oneOf(["small", "medium", "large"]),
};
Boton.defaultProps = {
label: "Button",
primary: true,
size: "medium",
};
Boton / Boton.jsx
.btn-small {
font-size: 0.8rem;
padding: 0.4rem 1rem;
}
.btn-medium {
font-size: 0.9rem;
padding: 0.5rem 1.2rem;
}
.btn-large {
font-size: 1.2rem;
padding: 0.8rem 1.5rem;
}
Boton / Boton.css
export const Large = {
args: {
label: "Large",
primary: true,
size: "large",
},
};
export const Medium = {
args: {
label: "Medium",
primary: true,
size: "medium",
},
};
export const Small = {
args: {
label: "Small",
primary: true,
size: "small",
},
};
Boton / Boton.stories.js
3 nuevos Stories
Nuevo control para el tamaño
Provee valores para los argumentos
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";
import { Boton } from "./stories/Boton/Boton";
function App() {
const [count, setCount] = useState(0);
return (
<>
<div>
<a href="https://vitejs.dev" target="_blank" rel="noreferrer">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank" rel="noreferrer">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.jsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
);
}
export default App;
App.jsx
<Boton onClick={() => setCount((count) => count + 1)}>
count is {count}
</Boton>
El componente puede ser utilizado en cualquier parte de la aplicación
Aún no funciona el onClick
zoom
ruler
size
outlines
settings
componente
full screen
Interacción con el Story seleccionado
Story
export default {
component: Boton,
title: "Componentes/Boton",
};
Boton / Boton.stories.js
Valor por default
import { Titulo } from "./Titulo";
export default {
component: Titulo,
title: "Componentes/Titulo",
};
export const Primary = {
args: {
texto: "Titulo",
nivel: 1,
},
};
Titulo / Titulo.stories.js
import PropTypes from "prop-types";
export const Titulo = ({ texto, nivel }) => {
if (nivel === 1) return <h1>{texto}</h1>;
if (nivel === 2) return <h2>{texto}</h2>;
if (nivel === 3) return <h3>{texto}</h3>;
if (nivel === 4) return <h4>{texto}</h4>;
if (nivel === 5) return <h5>{texto}</h5>;
if (nivel === 6) return <h6>{texto}</h6>;
};
Titulo.propTypes = {
texto: PropTypes.string.isRequired,
nivel: PropTypes.oneOf([1, 2, 3, 4, 5, 6]),
};
Titulo.defaultProps = {
texto: "Titulo",
nivel: 1
};
Titulo / Titulo.jsx
Storybook crea el control por defecto para el argumento
import { Titulo } from "./Titulo";
export default {
component: Titulo,
title: "Componentes/Titulo",
argTypes: {
nivel: { control: "radio" },
},
};
Titulo / Titulo.stories.js
Establece el tipo de control para los argumentos
radio
inline-radio
select
Tipos de control para conjuntos de datos
export default {
component: Titulo,
title: "Componentes/Titulo",
argTypes: {
nivel: { control: "radio" },
},
};
export const DHL = {
args: {
texto: "DHL",
nivel: 1,
textColor: "red",
backgroundColor: "orange",
},
};
export const Coke = {
args: {
texto: "Coca-Cola",
nivel: 3,
textColor: "white",
backgroundColor: "red",
},
};
Titulo / Titulo.stories.js
export const Titulo = (
{ texto,
nivel,
backgroundColor,
textColor }) => {
const estilo = { backgroundColor, color: textColor };
if (nivel === 1) return <h1 style={estilo}>{texto}</h1>;
if (nivel === 2) return <h2 style={estilo}>{texto}</h2>;
if (nivel === 3) return <h3 style={estilo}>{texto}</h3>;
if (nivel === 4) return <h4 style={estilo}>{texto}</h4>;
if (nivel === 5) return <h5 style={estilo}>{texto}</h5>;
if (nivel === 6) return <h6 style={estilo}>{texto}</h6>;
};
Titulo.propTypes = {
texto: PropTypes.string.isRequired,
nivel: PropTypes.oneOf([1, 2, 3, 4, 5, 6]),
backgroundColor: PropTypes.string,
textColor: PropTypes.string,
};
Titulo.defaultProps = {
texto: "Título",
nivel: 1,
backgroundColor: "white",
textColor: "black",
};
Titulo / Titulo.jsx
Storybook automáticamente genera el tipo de control
export const Boton = ({ label, primary, size, onClick }) => {
const botonType = primary ? "btn-primary" : "btn-secondary";
return (
<button
type="button"
className={`btn ${botonType} btn-${size}`}
onClick={onClick}
>
{label}
</button>
);
};
Boton.propTypes = {
label: PropTypes.string.isRequired,
primary: PropTypes.bool,
size: PropTypes.oneOf(["small", "medium", "large"]),
onClick: PropTypes.func,
};
Boton.defaultProps = {
label: "Button",
primary: true,
size: "medium",
onClick: undefined,
};
Boton / Boton.jsx
parametro onClick
Se asocia el parámetro con el evento en el componente
Tipo del parámetro: func
Valor por default
Detecta el evento onClick en el workshop
import { Boton } from "../Boton/Boton";
import { Titulo } from "../Titulo/Titulo";
import "./Toolbar.css";
export const Toolbar = () => {
return (
<div className="toolbar">
<div>
<Titulo texto="Frontend" />
</div>
<div className="toolbar-btns">
<Boton label="inicio" />
<Boton label="servicios" />
<Boton label="contacto" primary={false} />
</div>
</div>
);
};
export default Toolbar;
Toolbar / Toolbar.jsx
import { Toolbar } from "./Toolbar";
export default {
component: Toolbar,
title: "Componentes/Toolbar",
};
export const Primary = { };
Toolbar / Toolbar.stories.js
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #606060;
margin: 0;
}
.toolbar-btns {
display: flex;
gap: 5px;
}
Toolbar / Toolbar.css
El componente usa otros componentes
Atoms
Molecules
Organisms
Templates
Pages
Button
Search
Search
Inicio Servicios Contacto
Categoría
Componente
Documentación
Story
.storybook / preview.js
const preview = {
parameters: {
options: {
storySort: {
order: ["pages", "atoms", "examples"]
}
}
...
};
Atoms/Esentials/Import/Boton
Se pueden agregar varios niveles de folders
Automática
export default {
component: Boton,
tags: ["autodocs"],
title: "Atoms/Boton",
};
Boton / Boton.stories.js
Comentarios
Boton.propTypes = {
/**
* Texto a mostrar en el botón
*/
label: PropTypes.string.isRequired,
/**
* Este botón es primario?
*/
primary: PropTypes.bool,
/**
* Qué tan grande será el botón?
*/
size: PropTypes.oneOf(["small", "medium", "large"]),
/**
* Handler opcional para el evento onClick
*/
onClick: PropTypes.func,
};
Boton / Boton.jsx
import { Meta, Description, Canvas, Controls }
from "@storybook/blocks";
import * as BotonStories from "./Boton.stories";
import Code from "../assets/code-brackets.svg";
<Meta of={BotonStories} name="Docs" />
<Description of={BotonStories} />
<style>
{`
.warning {
border: 1px solid orange;
padding: 1rem;
color: orange;
font-weight: bold;
margin-bottom: 2rem;
}
`}
</style>
# Componente Botón
El componente botón está diseñado para funcionar
dentro de cualquier componente de la aplicación.
<div class="warning">Esta es una alerta</div>
## Subtitulo
lorem ipsum dolor sit amet, consectetur
- Item 1
- Item 2
- Item 3
Boton / Boton.mdx
<img src={Code} alt="Code" />
{/* Este es un comentario */}
### Código fuente
Este es un ejemplo de una `variable`.
Se pueden usar expresiones como en JSX: {1 + 2}
```js
console.log("Hello world");
```
<Canvas
of={BotonStories.Advanced}
sourceState="shown"
source={{ type: "code" }}
/>
<Controls of={BotonStories.Basic} />
Meta
Markdown
HTML
CSS
Stories
Canvas
Controls
Description
import { INITIAL_VIEWPORTS } from "@storybook/addon-viewport";
const preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
viewport: {
viewports: INITIAL_VIEWPORTS,
},
},
};
.storybook / preview.js
Dispositivo | Mínima | Máxima |
---|---|---|
Mobile | 320px | 480px |
iPads, tablets | 481px | 768px |
Pantallas pequeñas y laptops | 769px | 1024px |
Pantallas grandes y desktops | 1025px | 1200px |
Pantallas extra-grandes y TV | 1201px |
import { INITIAL_VIEWPORTS }
from "@storybook/addon-viewport";
const customViewports = {
laptop: {
name: "Laptop",
styles: {
width: "1024px",
height: "800px",
},
},
desktop: {
name: "Desktop",
styles: {
width: "1025px",
height: "800px",
},
},
};
const preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
viewport: {
viewports: {
...INITIAL_VIEWPORTS,
...customViewports,
},
},
},
};
.storybook / preview.js
import PropTypes from "prop-types";
import "./Pill.css";
export const Pill = ({
label,
backgroundColor, textColor }) => {
const estilo = { backgroundColor,
color: textColor };
return (
<a className="pill" style={estilo} href="#">
{label}
</a>
);
};
Pill.propTypes = {
label: PropTypes.string.isRequired,
backgroundColor: PropTypes.string,
textColor: PropTypes.string,
};
Pill.defaultProps = {
label: "Pill",
backgroundColor: "blue",
textColor: "white",
};
export default Pill;
Pill / Pill.jsx
import { Pill } from "./Pill";
export default {
component: Pill,
tags: ["autodocs"],
title: "Atoms/Pill",
};
export const Primary = {
args: {
label: "Primary",
},
};
Pill / Pill.stories.js
.pill {
font-family: "Helvetica Neue";
text-decoration: none;
padding: 0.3rem 1rem;
border-radius: 9999px;
}
@media only screen and (min-width: 320px) and (max-width: 480px) {
.pill {
border: 4px solid red;
}
}
@media only screen and (min-width: 481px) and (max-width: 768px) {
.pill {
border: 4px solid green;
}
}
@media only screen and (min-width: 769px) and (max-width: 1024px) {
.pill {
border: 4px solid magenta;
}
}
@media only screen and (min-width: 1025px) and (max-width: 1200px) {
.pill {
border: 4px solid orange;
}
}
@media only screen and (min-width: 1201px) {
.pill {
border: 4px solid cyan;
}
}
Pill / Pill.css
Versión inicial para revisar que funcionan los Media Queries
import PropTypes from "prop-types";
import "./Pill.css";
export const Pill = ({ icon,
label,
backgroundColor,
textColor }) => {
const estilo = { backgroundColor, color: textColor };
return (
<button className="pill" style={estilo} href="#">
<div className="icon">{icon}</div>
<div className="label">{label}</div>
</button>
);
};
Pill.propTypes = {
icon: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
backgroundColor: PropTypes.string,
textColor: PropTypes.string,
};
Pill.defaultProps = {
icon: "",
label: "pill",
backgroundColor: "blue",
textColor: "white",
};
export default Pill;
Pill / Pill.jsx
import { Pill } from "./Pill";
export default {
component: Pill,
tags: ["autodocs"],
title: "Atoms/Pill",
};
export const France = {
args: {
icon: "🇫🇷",
label: "France",
backgroundColor: "#1c1c9f",
textColor: "white",
},
};
export const Spain = {
args: {
icon: "🇪🇸",
label: "Spain",
backgroundColor: "#f5a60c",
textColor: "white",
},
};
Pill / Pill.stories.js
El estilo se encuentra en la siguiente diapositiva
.pill {
font-family: "Helvetica Neue";
text-decoration: none;
padding: 0.3rem 1rem;
border: 0;
display: flex;
align-items: center;
gap: 5px;
}
.pill .icon {
font-size: 1rem;
}
.pill .label {
font-size: 1.1rem;
}
/* Mobile */
@media only screen and (min-width: 320px) and (max-width: 480px) {
.pill {
border-radius: 50%;
width: 50px;
height: 50px;
gap: 0;
justify-content: center;
}
.pill .icon {
font-size: 1.5rem;
}
.pill .label {
display: none;
}
}
Pill / Pill.stories.js
/* Tablet */
@media only screen and (min-width: 481px) and (max-width: 768px) {
.pill {
border-radius: 5px;
padding: 0.4rem 2rem;
}
.pill .icon {
font-size: 1.5rem;
}
.pill .label {
font-size: 0.9rem;
}
}
/* Laptop */
@media only screen and (min-width: 769px) and (max-width: 1024px) {
.pill {
padding: 0.5rem 2rem;
border-radius: 9999px;
}
}
/* Desktop */
@media only screen and (min-width: 1025px) and (max-width: 1200px) {
.pill {
border-radius: 9999px;
flex-direction: column;
gap: 0;
padding-bottom: 1.2rem;
}
.pill .icon {
font-size: 2.5rem;
padding: 0;
}
.pill .label {
margin: -10px;
font-size: 0.8rem;
}
}
/* Large screens & TV */
@media only screen and (min-width: 1201px) {
.pill {
padding: 0.5rem 1.5rem;
}
}
Mobile
Tablet
Laptop
Desktop
Extra Large & TV
johncardozo@gmail.com