# Storybook Guide

Practical guide for writing Storybook stories in UI-Bundle.

## How It Works

Storybook renders real Twig templates in the browser using [php-wasm](https://github.com/nicolo-ribaudo/php-wasm) (PHP 8.1 compiled to WebAssembly).

On boot, two ZIP archives are extracted into a virtual filesystem:

| Archive | Contents |
|---------|----------|
| `vendor.zip` | Twig engine, Symfony components, KnpMenu, mock extensions |
| `templates.zip` | All `.twig` files from `templates/` + wrapper templates from `stories/_stories/` |

The render pipeline:

1. Storybook calls `preview.tsx` with story args
2. `preview.tsx` passes args to `TwigWrapper.tsx` (React component)
3. `TwigWrapper` calls `php-engine.ts` which runs PHP/Twig in the virtual filesystem
4. The resulting HTML is injected into the DOM

During development, the `vite-twig-zipper` Vite plugin watches `templates/` and `stories/_stories/` for `.twig` changes and rebuilds `templates.zip` automatically, triggering a full page reload.

## Render Modes

Each story must specify how its template should be rendered via `parameters.twig.renderMode`:

| Mode | Use when | What it generates |
|------|----------|-------------------|
| `macro` | Template defines `{% macro %}` | `{% from '@Ui/...' import X %}{{ X(args) }}` |
| `include` | Template uses plain variables | `{% include '@Ui/...' with _context only %}` |
| `raw` | Needs `{% embed %}`, `{% extends %}`, `{% set %}`, or variable remapping | Renders a wrapper in `stories/_stories/` directly |

**Decision flowchart:**

```
Does the template define a macro?
  Yes -> macro mode
  No  -> Does it need embed, extends, set, or variable remapping?
    Yes -> raw mode (write a wrapper template)
    No  -> include mode
```

## Writing a Story

### Macro Mode

Use for templates that define `{% macro %}` (most components).

Example: `stories/component/button.stories.ts`

```ts
import type { Meta, StoryObj } from '@storybook/react';
import type { StoryParameters } from '../../.storybook/types';

interface ButtonArgs {
  text: string;
  size?: string;
  href?: string;
}

const meta: Meta<ButtonArgs> = {
  title: 'Components/Button',
  parameters: {
    twig: {
      template: 'component/button.html.twig',
      renderMode: 'macro',
      macroName: 'button',          // name of the macro to call
      macroParam: 'button_obj',     // name of the macro's parameter
      componentPath: 'button',      // for source viewer panel
      argsToContext: (args) => ({ button_obj: args }),
    },
  } as StoryParameters,
  argTypes: {
    text: {
      control: 'text',
      description: 'Button text label',
      table: { category: 'Core' },
    },
    // ...
  },
};

export default meta;
type Story = StoryObj<ButtonArgs>;

export const Primary: Story = {
  args: { text: 'Primary Button' },
};
```

Key points:
- `template` is the path relative to `templates/` (no leading `@Ui/`)
- `macroName` must match the macro defined in the Twig file
- `macroParam` is the variable name the macro expects
- `argsToContext` wraps args into the expected context shape

### Include Mode

Use for templates that consume plain variables (no macros).

Example: `stories/module/article/article_title.stories.ts`

```ts
const meta: Meta<ArticleTitleArgs> = {
  title: 'Modules/Article/Article Title',
  parameters: {
    twig: {
      template: 'module/article/article_title.html.twig',
      renderMode: 'include',
      componentPath: 'module/article/article_title',
      argsToContext: (args) => args,   // pass args directly as context
    },
  } as StoryParameters,
  // ...
};
```

Key points:
- `argsToContext: (args) => args` passes Storybook args directly as Twig variables
- No `macroName`/`macroParam` needed

### Raw Mode

Use when the template requires `{% embed %}`, `{% extends %}`, `{% set %}`, or variable remapping. This requires a wrapper template.

Example: `stories/module/roller/roller.stories.ts`

```ts
const meta: Meta<RollerArgs> = {
  title: 'Modules/Roller',
  parameters: {
    twig: {
      template: '_stories/roller-story.html.twig',  // points to wrapper
      componentPath: 'module/roller/roller',
      argsToContext: (args) => args,
      jsModules: ['roller'],   // initialize JS after render
    },
  } as StoryParameters,
  // ...
};
```

With a wrapper template at `stories/_stories/roller-story.html.twig`:

```twig
{# Story wrapper: renders roller module with args from Storybook #}
{% embed '@Ui/module/roller/roller.html.twig' with {
    offset: offset|default(15),
    no_nav: no_nav|default(false),
    icon_left: icon_left|default('chevron_left'),
    icon_right: icon_right|default('chevron_right')
} %}
    {% block slider %}
        {% for item in items|default([]) %}
            <div class="roller-item">{{ item.title }}</div>
        {% endfor %}
    {% endblock %}
{% endembed %}
```

Key points:
- `template` points to `_stories/...` wrapper, not the real template
- `componentPath` still points to the real template (for source viewer)
- Raw mode is the default when `renderMode` is omitted
- Always use `|default()` filters in wrappers

## Wrapper Templates

Located in `stories/_stories/`, these wrap templates that cannot be rendered via macro or include mode.

**Naming convention:** `{feature}-{component}-story.html.twig`

**When you need a wrapper:**
- Template uses `{% embed %}` with `{% block %}` overrides (roller, section-grid, dynamic_content)
- Template uses `{% extends %}` (section layouts)
- Template needs `{% set %}` to define defaults (consent-cookie_banner)
- Story args need remapping to different variable names (header-main-v2)

**Existing wrappers (22):**

| Wrapper | Reason |
|---------|--------|
| `roller-story` | `{% embed %}` with block override |
| `section-grid-story` | `{% embed %}` with block override |
| `section-roller-story` | `{% embed %}` with block override |
| `header-main-v2-story` | Variable remapping (`menu_items` -> `menu_obj`) |
| `breadcrumb-story` | `{% embed %}` with block override |
| `subnav-story` | Uses loop to generate nav items |
| `dropdown-story` | Uses loop to generate items |
| `directory-alpha-story` | Uses loop to generate entries |
| `pagination-bis-story` | Uses object construction |
| `author-biography-story` | Hardcodes `author_route_name` |
| `consent-cookie_banner-story` | Uses `{% set %}` defaults |
| `c2c-offer_card_base-story` | Variable construction |
| `ads-ad_bait-story` | Minimal wrapper |
| `anchor-mq_state_anchor-story` | Anchor module wrapper |
| `anchor-overlay_anchor-story` | Anchor module wrapper |
| `native_placements-banner_big-story` | Embed with block |
| `native_placements-banner_small-story` | Embed with block |
| `native_placements-player_footer-story` | Embed with block |
| `dynamic_content-base_layout-story` | Extends layout |
| `dynamic_content-layout_2_cols_ads-story` | Extends layout |
| `dynamic_content-layout_roller_big-story` | Extends layout |
| `dynamic_content-layout_single_item_left-story` | Extends layout |

## JS Module Integration

Some stories need JavaScript modules initialized after render (e.g., roller scrolling, search autocomplete).

Configure via `jsModules` in the twig parameters:

```ts
parameters: {
  twig: {
    template: '_stories/roller-story.html.twig',
    jsModules: ['roller'],
  },
},
```

**Available modules:** `roller`, `search`, `player`, `header`, `bottomBar`, `videoCard`, `darkMode`

**Adding a new module:**

1. Add the name to `JsModuleName` union in `.storybook/types.ts`
2. Import and register it in `.storybook/js-modules.ts`

## Source Viewer

The source viewer panel shows the Twig code needed to use the component in a project.

Configure via `componentPath`, `macroName`, and `macroParam`:

```ts
parameters: {
  twig: {
    componentPath: 'component/button',  // builds @Ui/component/button.html.twig
    macroName: 'button',                // shows {% from ... import button %}
    macroParam: 'button_obj',           // shows {{ button(button_obj) }}
  },
},
```

For include-mode components, only `componentPath` is needed:
```ts
parameters: {
  twig: {
    componentPath: 'module/article/article_title',
    // shows {% include '@Ui/module/article/article_title.html.twig' with {...} only %}
  },
},
```

## File Conventions

**Story location:** Separate `stories/` directory, mirroring the template hierarchy.
```
templates/                        # Production templates only
  component/
    button.html.twig
  module/
    roller/
      roller.html.twig

stories/                          # Storybook stories + wrapper templates
  _stories/                       # Raw mode wrapper .html.twig files
    roller-story.html.twig
  component/
    button.stories.ts
  module/
    roller/
      roller.stories.ts
```

**Title convention:**
- Components: `Components/ButtonName`
- Modules: `Modules/Feature/ComponentName`
- Sections: `Sections/Feature/ComponentName`

**argTypes categories:** Group related controls using `table.category`:
- `Core` - Essential props
- `Content` - Text, labels, items
- `Visual` - Styling, icons, classes
- `Meta` - Dates, flags, metadata
- `Navigation` - Links, routing
- `Social` - Share, social features
- `SEO` - Semantic HTML, structured data

## Reference

| File | Purpose |
|------|---------|
| `.storybook/preview.tsx` | Global render function, dispatches args to TwigWrapper |
| `.storybook/TwigWrapper.tsx` | React component, calls php-engine and handles loading/error states |
| `.storybook/src/php-engine.ts` | PHP-WASM bootstrap, extracts ZIPs, runs Twig render |
| `.storybook/types.ts` | Shared TypeScript types (`TwigStoryParams`, `JsModuleName`, etc.) |
| `.storybook/js-modules.ts` | JS module registry and initialization |
| `.storybook/main.ts` | Storybook config (story discovery, Vite config, static dirs) |
| `.storybook/plugins/vite-twig-zipper.ts` | Vite plugin for templates.zip hot-reload |
| `.storybook/mocks/MockTwigExtension.php` | Mock Symfony Twig functions for php-wasm |
