---
name: storybook
description: 'Storybook configuration and component story authoring for s360-base-theme. Use when: writing component stories, modifying .storybook/main.js or preview.js, configuring Twig extensions in setupTwig.js, troubleshooting Storybook build issues, adding a new story for a node/paragraph/component, understanding story file format, running Storybook dev server.'
---

# Storybook — s360-base-theme

## Overview

Storybook lives inside the `ui/` Yarn workspace. It uses `@storybook/html-webpack5` and renders components via the `twig` npm package with Drupal-compatible Twig extensions. All story files live next to their component source files.

**Source:** `https://github.com/Square360/s360-base-theme`

## Workspace Structure

Storybook runs from the `ui/` Yarn workspace inside the theme:

```
{theme}/
├── ui/
│   ├── .storybook/
│   │   ├── main.js           ← framework, stories glob, addons, staticDirs
│   │   ├── preview.js        ← decorators, parameters, Twig setup, global imports
│   │   ├── setupTwig.js      ← registers Drupal Twig extensions
│   │   ├── drupal.js         ← Drupal global mock
│   │   ├── once.global.js    ← core/once mock for behaviors
│   │   └── webpack.config.js ← Storybook-specific webpack overrides
│   ├── src/
│   │   ├── base/             ← global styles (imported in preview.js)
│   │   ├── component/        ← isolated components (pager, etc.)
│   │   ├── templates/
│   │   │   ├── node/{name}/{view-mode}/component/
│   │   │   ├── paragraph/{name}/component/
│   │   │   ├── navigation/{name}/
│   │   │   ├── field/
│   │   │   ├── form/
│   │   │   ├── media/
│   │   │   ├── taxonomy/
│   │   │   └── views/
│   │   └── images/
│   ├── dist/                 ← webpack compiled output (referenced by libraries.yml)
│   ├── package.json
│   ├── webpack.config.js
│   └── plopfile.mjs          ← yarn generate scaffolding
└── ui-core/                  ← SCSS-only workspace (variables/mixins, no output)
```

## Running Storybook

Always run from the `ui/` directory:

```bash
cd web/themes/custom/{theme}/ui
yarn develop        # webpack watch + storybook dev server (concurrently)
yarn storybook      # storybook dev server only (port 6006)
yarn build          # production webpack + storybook static build
yarn storybook-build # storybook only → ../.storybook-out/
```

## Configuration Files

### ui/.storybook/main.js

```js
module.exports = {
  framework: {
    name: '@storybook/html-webpack5',
    options: { fastRefresh: true },
  },
  stories: [
    '../src/**/*.stories.@(js|ts)',
    // Uncomment for multiple themes:
    // '../../../[THEME_NAME]/src/**/*.stories.@(js|ts)'
  ],
  addons: [
    '@storybook/addon-a11y',
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    'storybook-addon-render-modes',
    // 'storybook-addon-data-theme-switcher'  ← for [data-theme] switching
  ],
  staticDirs: [
    '../dist/',       ← compiled webpack assets served statically
    './fontawesome',
  ],
};
```

### ui/.storybook/preview.js

```js
import { useEffect } from '@storybook/preview-api';
import Twig from 'twig';
import { setupTwig } from './setupTwig';

// Drupal globals
import './drupal.js';
import './once.global.js';

// Global styles (compiled by webpack)
import '../src/base/base.js';
import '../src/templates/site-layout/component/site-layout.js';
import '../src/templates/field/ckeditor/component/ckeditor.js';

setupTwig(Twig);

export const parameters = {
  backgrounds: { disable: true },
  layout: 'none',
  options: {
    storySort: {
      method: 'alphabetical',
      order: [
        'Foundation', 'Layouts', 'Components', 'Content Types',
        'Layout Components', 'Media', 'Menus', 'Taxonomy', 'Site Layout',
      ],
    },
  },
};

export const decorators = [
  (storyFn) => {
    useEffect(() => Drupal.attachBehaviors(), []);
    return storyFn();
  },
];
```

### ui/.storybook/setupTwig.js

```js
const twigDrupal = require('twig-drupal-filters');
const twigAddAttributes = require('add-attributes-twig-extension');
const { addDrupalExtensions } = require('drupal-twig-extensions/twig');

module.exports.setupTwig = function setupTwig(twig) {
  twig.cache();
  twigDrupal(twig);
  twigAddAttributes(twig);
  addDrupalExtensions(twig, {});

  // Mock custom Drupal module functions as needed:
  twig.extendFunction('menus_attribute', function() { return true; });

  return twig;
};
```

Add additional `twig.extendFunction()` or `twig.extendFilter()` calls here for any site-specific Twig functions that aren't covered by the extensions above.

## Story File Format

Generated by `yarn generate` (plop). The pattern for all three entity types:

```js
// src/templates/paragraph/card/component/paragraph.card.stories.js
import paragraphCardTwig from './paragraph.card.twig';
import paragraphCardData from './paragraph.card.yml';
import './paragraph.card.js';   // ← pulls in SCSS via webpack entry

/**
 * Storybook Definition.
 */
export default {
  title: 'Layout Components/Card',   // node → "Content Types", component → "Components"
};

export const card = (args) => {
  let data = Object.assign(structuredClone(paragraphCardData ?? {}), args);
  return paragraphCardTwig({ ...data });
};
card.args = paragraphCardData;
```

### Story title categories

| Entity type | Storybook category |
|-------------|--------------------|
| Node (full) | `Content Types/{Name}/Full` |
| Node (teaser) | `Content Types/{Name}/Teaser` |
| Paragraph | `Layout Components/{Name}` |
| Isolated component | `Components/{Name}` |

## Component File Set

Each component (generated by `yarn generate`) produces:

| File | Purpose |
|------|---------|
| `component-name.twig` | Component Twig template (for Storybook) |
| `component-name.scss` | BEM styles — one top-level selector |
| `component-name.js` | Webpack entry — imports SCSS + any behavior |
| `component-name.stories.js` | Storybook story |
| `component-name.yml` | Story default data (mapped to args) |

The Drupal template (`paragraph--bundle-name.html.twig`) is a separate file that extends the component twig via `{% extends %}`.

## Generating a New Component

```bash
cd web/themes/custom/{theme}/ui
yarn generate
```

Prompts:
1. Entity type: `Content Type` / `Paragraph` / `Isolated Component`
2. Name: human-readable (converted to kebab-case automatically)
3. (Node only) View mode: `Full` / `Teaser`

This creates all five component files in the correct location. Always use `yarn generate` — do not create component files manually.

## Drupal Twig vs Component Twig

Each entity has **two** Twig files with different roles:

| File | Location | Role |
|------|----------|------|
| `node--{name}--{mode}.html.twig` | `src/templates/node/{name}/{mode}/` | Drupal template — extends component twig |
| `node.{name}.{mode}.twig` | `src/templates/node/{name}/{mode}/component/` | Component template — used by Storybook |

The Drupal template uses `{% extends "@ui-node/..." %}` to delegate rendering to the component twig. In Storybook, only the component twig is used directly.

## Adding a Global CSS/JS Import to Storybook

If a new base style or site-layout component is added, import its `.js` entry in `preview.js`:

```js
import '../src/base/your-new-base.js';
```

## Accessibility Checks

The `@storybook/addon-a11y` addon runs axe-core on every rendered story. Check the **Accessibility** panel in Storybook before considering a component done. Coordinate with Aria for any violations.

## Common Issues

| Issue | Fix |
|-------|-----|
| `Cannot find module 'twigjs-loader'` | Run `yarn install` from `ui/` — note it is `twigjs-loader` not `twig-loader` |
| Twig function not found | Add `twig.extendFunction()` mock in `setupTwig.js` |
| Story renders blank | Check `.yml` data file exists and is valid YAML |
| `Drupal is not defined` | Ensure `drupal.js` is imported in `preview.js` before other imports |
| `once is not a function` | Ensure `once.global.js` is imported in `preview.js` |
| SCSS `@import` error | Replace with `@use` — `@import` is banned |
| Story not visible in sidebar | Check the `title:` category matches the sort order in `preview.js` |
