storybook

John Cardozo

John Cardozo

storybook

qué es storybook?

Storybook es un entorno frontend

Construir

Probar

Organizar

Documentar

Componentes UI

Páginas

Procedimiento Aislado

Múltiples frameworks

ventajas de storybook

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

problema que soluciona storybook

Componente

Estado 1

Estado 2

Estado 3

Estado 4

Chrome

Firefox

Safari

Opera

Smartphone

Laptop

Tablet

Desktop

Horizontal

Vertical

solución: desarrollo aislado de componentes

Componente

Estado 1

Estado 2

Estado 3

Estado 4

Historia 1

Historia 2

Historia 3

Historia 4

Variaciones

Ambiente aislado

alternativas de estilo para storybook

CSS

SASS

Tailwind

Styled Components

Bootstrap

Material UI

nodejs

Instalación

Verificación

node --version

npm --version

Node Package Manager

Node

instalación de storybook

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

Storybook es agregado al proyecto

React

React + Storybook

ejecución del proyecto y storybook

React

npm run dev

Storybook

npm run storybook

qué es un story?

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

story: hello world

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

hello world - entorno

Componente

Story

Ejemplos

Representación visual del Componente / Story

Argumentos interactivos

nuevos stories: primary + secondary

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

nuevos stories: tamaño

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

usar el componente en la aplicación

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

workshop: controles

zoom

ruler

size

outlines

settings

componente

full screen

Interacción con el Story seleccionado

Story

organizacion de componentes

export default {
  component: Boton,
  title: "Componentes/Boton",
};

Boton / Boton.stories.js

Valor por default

controles de los parámetros

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

argtypes: modificar el control por default

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

control de color: generado automáticamente

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

eventos: actions

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

componente usando otros componentes

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

naming y jerarquia: atom design

Atoms

Molecules

Organisms

Templates

Pages

Button

Search

Search

Inicio     Servicios      Contacto  

atom design en storybook

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

documentación

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

documentación: MDX2

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

viewport: devices


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

resoluciones comunes de pantalla

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

viewport: custom devices

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

ejemplo - componente responsive: pill - 1.0

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

ejemplo - componente responsive: pill - 2.1

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

ejemplo - componente responsive: pill - 2.2

.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;
  }
}

ejemplo - componente responsive: pill

Mobile

Tablet

Laptop

Desktop

Extra Large & TV

john cardozo

johncardozo@gmail.com

Made with Slides.com