# EXT:herobuilder — Hero Builder

Graphical, per-breakpoint **hero composition builder** for TYPO3.

Editors drag & drop FAL image layers, text and linked button layers onto a **stage**,
position/scale/rotate them **per breakpoint**, and manage multiple **collages** that render as a
Bootstrap 5 **carousel**. Layers stay individual, CSS-positioned elements (`%`-based) — no
server-side flattening for the frontend — with optional **AOS** entrance animations.

A **3-column canvas editor** (toolbar · layer panel · stage · property tabs, based on Moveable)
provides a full WYSIWYG workflow, including reusable **templates**, one-click **social / OG image
export** (ImageMagick), and a **live preview rendered with the real frontend CSS**.

- **Extension key:** `herobuilder`
- **Content element (CType):** `herobuilder`
- **TYPO3:** 12.4 · **PHP:** ≥ 8.1
- **State:** beta (0.1.0)

---

## Table of contents

- [Concepts](#concepts)
- [Feature list](#feature-list)
- [Editor GUI](#editor-gui)
- [Templates](#templates)
- [Social / OG image export](#social--og-image-export)
- [Data model](#data-model)
- [Installation](#installation)
- [Configuration](#configuration)
- [Keyboard shortcuts](#keyboard-shortcuts)
- [Architecture](#architecture)

---

## Concepts

- **Stage** — the hero canvas. Each breakpoint has a fixed **aspect ratio** (frontend) and a
  realistic **reference device width** (backend editor), so what you edit matches the device.
- **Breakpoints** — Bootstrap 5: `xs` (<576px), `sm` (576–767), `md` (768–991), `lg` (992–1199),
  `xl` (1200–1399), `xxl` (≥1400). Every layer is positioned independently per breakpoint.
- **Collage** — one slide. A content element holds *n* collages → rendered as a carousel.
- **Layer** — an **image** (FAL), a **text** block, or a **button** (text + TYPO3 link), freely
  placed within the stage.
- **Template** — a reusable composition (record in `tx_herobuilder_template` or a shipped preset)
  that can be applied to a collage or created from one.

---

## Feature list

### Content element & rendering
- Dedicated `herobuilder` content element with a New-Content-Element wizard entry.
- 1:n collages (`tx_herobuilder_collage`) rendered as a **Bootstrap 5 carousel**
  (autoplay, interval, loop, prev/next controls, indicators, pause-on-hover, slide/fade transition).
- Per-breakpoint **stage aspect ratios** (frontend) via scoped, media-query CSS — or a
  **fixed stage height** per breakpoint, switchable per content element.
- **Responsive images** — every image layer gets a WebP `srcset` (480–3200px, capped at the cropped
  source) and a `sizes` computed from its per-breakpoint geometry, including the overflow of
  `cover` (e.g. a wide background on a tall phone stage is painted much wider than the screen).
- **Accessible carousel** — localized labels (`locallang.xlf`), carousel/slide roles, a
  pause/play button whenever autoplay is on (WCAG 2.2.2), no autoplay and no AOS entrance
  animations under `prefers-reduced-motion`, autoplay holds while keyboard focus is inside,
  visible focus rings on the controls. Prev · indicators · next sit together in a dark pill at
  the bottom centre: never on the composition's text, >= 3:1 contrast on any artwork, 24 px
  targets.
- **CSP-ready frontend** — the scoped composition CSS goes into the `<head>` via the
  AssetCollector with `csp="true"` (hash/nonce), no inline `<style>` in the body.
- Backend page-module **preview renderer** (slide + layer thumbnails / text chips).

### Layers
- **Image layers** from FAL (native TYPO3 file browser via the sibling `assets` field → real
  `sys_file_reference`, clean reference index).
- **Text layers** — plain text, styled exclusively via predefined classes (safe, auto-escaped).
  The HTML element is selectable (`div`, `p`, `h1`–`h6`) for semantics only: hero.css resets the
  element's own styles, so a heading looks exactly like the same layer as a `div`.
- **Button layers** — text + **TYPO3 link** (page/url/file/mail/…) via the native Link Browser,
  styled with `btn` classes; rendered as a `<a>` typolink on the frontend.
- **Per-breakpoint geometry** — x / y / width / height (%), rotation, z-index, visibility.
- **Image fit** — `fill` / `cover` / `contain` (no distortion for cover/contain).
- **Focus point** — `object-position` picker (which part stays visible when `cover` crops).
- **Art direction** — "Crop & focus only for XS" gives a breakpoint its own crop and focus point.
  The frontend renders a `<picture>` whose `<source>` for that breakpoint carries physically
  cropped derivatives: a phone loads the phone framing (e.g. 588 px) instead of a desktop image
  it would mostly crop away.
- **Predefined CSS classes** — multi-select (chips) per layer from a page-TSconfig list
  (Bootstrap 5 set); validated against the same whitelist on the frontend (injection-safe).
- **Darkening overlay behind text** — adjustable scrim (0–100 %) for readability over busy images.
- **Per-layer TYPO3 link** on any layer (native Link Browser).
- **AOS entrance animation** per layer (effect, delay, duration).

### Templates & export
- **Templates gallery** (“Templates” button) — apply a reusable composition to the current
  collage; shows rendered thumbnails of shipped **presets** + user templates.
- **Save as template** — store the current composition as a `tx_herobuilder_template` record.
- **Social / OG image export** — render the composition to a downloadable image in OG, 16:9,
  Instagram 1:1 / 4:5 and Story 9:16 formats (ImageMagick, cached).
- See [Templates](#templates) and [Social / OG image export](#social--og-image-export).

### Backend editor (3-column, Moveable-based WYSIWYG)
- **Toolbar**: title, breakpoint pills (active = accent border), and actions
  (Add image/text/button, Templates, Save as template, Export image, Live preview toggle).
- **Left — Layer panel**: rows with color/thumbnail chip, name, eye (visibility) and lock;
  **drag-reorder** the z-stack; right-click for actions (duplicate, align, front/back, delete).
- **Center — Stage**: drag / resize / rotate with **snapping & guide lines** (stage edges/center
  + other layers); **zoom badge** (Ctrl+wheel cursor-anchored, click = fit ↔ 100 %); pan via
  scrollbars, middle-mouse or Space+drag.
- **Right — Property tabs**, fields per layer type: **Content** (text, HTML element, link — or for
  images crop/flip/auto-trim/own framing, fit, focus point for cover/contain, alternative text,
  link), **Design** (grouped class chips, text darkening), **Position** (numeric X/Y/W/H/rotation +
  arrow-key nudging, align icons, visible on this breakpoint, copy to all breakpoints),
  **Animation** (effect, delay, duration, replay). Delete via the layer list, context menu or Del.
- **Undo / redo** (100-step history) + keyboard shortcuts.

### Quality & i18n
- Fully **localized** UI (English source + German `de.` translation) — TCA, tabs, buttons,
  canvas panel, context menu, layer list, zoom, templates, export and preview.
- **CSP-safe live preview** — no inline `<script>` (external same-origin JS) and no inline
  `<style>` (dynamic CSS delivered as a JSON data island, injected via a constructable
  stylesheet). Works under a strict backend CSP (`script-src 'self'`, style-src without
  `'unsafe-inline'`).

---

## Editor GUI

A single field renders the whole editor in a 3-column grid under a toolbar:

```
[ Toolbar: title · breakpoint pills · Add image/text/button · Templates · Save as template · Export · Live preview ]
[ left ~230px: LAYERS | center: stage (zoom badge) | right ~340px: Transform · Style · Animation ]
```

- Selecting a layer (in the stage or the layer list) fills the right property panel.
- The Style tab shows only the controls relevant to the layer type (e.g. fit/focus for images,
  scrim for text, link for all).
- The live preview and the export both reuse the rendering paths described below.

## Templates

Reusable compositions applied to a collage or created from one.

- **Storage** — the `tx_herobuilder_template` table (records editable like any other TYPO3 record;
  put them in a sysfolder). Plus shipped, read-only **presets** as JSON under
  `Resources/Private/Templates/Presets/`.
- **Gallery** — the “Templates” toolbar button opens a modal with a card per template (rendered
  21:9 thumbnail + name). Select a card → “Apply template” replaces the current collage’s layers.
- **Save as template** — the “Save as template” button stores the current composition as a new
  `tx_herobuilder_template` record (via DataHandler) in the folder configured by
  `tx_herobuilder.templatePid` (page TSconfig; falls back to the record’s pid — a sysfolder is
  recommended, see [Configuration](#configuration)).
- Thumbnails are generated by `CompositeImageService` and cached in `typo3temp/assets/herobuilder/`.

## Social / OG image export

The “Export image” button renders the current composition into a downloadable raster image via
**ImageMagick** (`CompositeImageService`), cached by a hash of the inputs.

| Format | Size | Use |
|---|---|---|
| `og` | 1200×630 | Open Graph / Facebook / LinkedIn |
| `wide` | 1200×675 | 16:9 (Twitter/X, YouTube) |
| `square` | 1080×1080 | Instagram 1:1 |
| `portrait` | 1080×1350 | Instagram 4:5 |
| `story` | 1080×1920 | Story / Reel 9:16 |

For each format the breakpoint whose design ratio is closest is used, so the layout looks intended.
Image layers are composited with cover/contain/fill; text/button layers are approximated (font size
scales with canvas width, colors/alignment from classes, buttons as a filled box) — CSS-styled text
can’t be reproduced pixel-perfectly by ImageMagick, which is acceptable for share images/thumbnails.
Requires ImageMagick (`$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path']`).

## Data model

`tt_content` (CType `herobuilder`, carousel settings) → 1:n `tx_herobuilder_collage` (one row per
slide). Each collage stores its layer geometry in `composition` (JSON):

```json
{
  "layers": [
    {
      "id": "bg",
      "type": "image",
      "fileUid": 71877,
      "alt": "",
      "link": "",
      "fit": "cover",
      "focusX": 50,
      "focusY": 50,
      "overlay": 0,
      "locked": false,
      "anim": { "effect": "fade-up", "delay": 200, "duration": 600 },
      "placements": {
        "xs": { "x": 0, "y": 0, "w": 100, "h": 100, "rot": 0, "z": 1, "visible": true }
      }
    },
    {
      "id": "t1",
      "type": "text",
      "text": "DEV LAND 2026",
      "cssClass": "display-3 text-white fw-bold text-center",
      "overlay": 55,
      "placements": { "lg": { "x": 20, "y": 60, "w": 60, "h": 12, "rot": 0, "z": 3, "visible": true } }
    },
    {
      "id": "b1",
      "type": "button",
      "text": "Register now",
      "cssClass": "btn btn-primary",
      "link": "t3://page?uid=42",
      "placements": { "lg": { "x": 40, "y": 75, "w": 20, "h": 8, "rot": 0, "z": 4, "visible": true } }
    }
  ]
}
```

Field notes:
- `type` — `"image"` (default), `"text"` or `"button"`.
- `tag` — HTML element of a text layer: `p`, `h1`–`h6` (whitelisted; absent = `div`).
- `text` — label (text/button layers). `link` — TYPO3 link (`t3://…`), works on any layer;
  a button is rendered as an `<a>` typolink.
- `fit` — `fill` | `cover` | `contain` (image layers).
- `focusX` / `focusY` — 0–100 %, → `object-position` (image layers).
- `cssClass` — space-separated, **whitelisted** predefined classes.
- `overlay` — 0–100 %, dark scrim behind text.
- `locked` — layer locked in the editor (not draggable).
- `placements[<bp>]` — `x,y,w,h` (%), `rot` (deg), `z`, `visible`; optionally
  `image: {crop, focusX, focusY}` — the breakpoint's own framing (art direction), overriding the
  layer's `crop` / `focusX` / `focusY` there.

`tx_herobuilder_template` holds the same `composition` JSON plus a `title` and an indicator
`color`; presets are shipped as JSON (`{title, color, layers}`).

---

## Installation

```bash
composer require wapplersystems/herobuilder
vendor/bin/typo3 extension:setup   # or activate in the Extension Manager
```

Then add a **Hero Builder** content element on any page and open its *Collages* tab.

---

## Configuration

### Stage aspect ratios (TypoScript constants)

Frontend stage ratio per breakpoint (`W:H`):

```typoscript
plugin.tx_herobuilder.settings.stages {
    xs.ratio  = 9:16
    sm.ratio  = 3:4
    md.ratio  = 16:9
    lg.ratio  = 21:9
    xl.ratio  = 21:9
    xxl.ratio = 21:9
}
```

The backend editor additionally renders each breakpoint at a realistic **reference device width**
(defaults: xs 390, sm 576, md 768, lg 992, xl 1200, xxl 1400 px).

### Fixed stage height (page TSconfig + content element)

Per content element, **Stage height** (`tt_content.stage_mode`, tab *Collages*) switches between
*Aspect ratio* (default, above) and *Fixed height*. The heights are a site setting:

```tsconfig
tx_herobuilder.stages {
    xs.height = 43rem
    lg.height = 55rem
}
```

Heights are inherited upwards (here: xs–md 43rem, lg and above 55rem); a breakpoint without a
height keeps its ratio. Only `px` and `rem` are accepted (`rem` converted at 16px), so the editor
can show the stage exactly as the frontend renders it at the reference width. The width stays
fluid in the frontend — layers keep their `%` positions, so between reference widths their
proportions shift (as with any fixed-height slider). The canvas follows a change of the field
immediately, before the record is saved.

### Composition format version and links

The editor writes `"version": 1` into the composition JSON (`Composition::VERSION`). JSON without
a version is version 1. When the shape changes incompatibly, raise the constant and add the step
to `Composition::migrate()` — stored collages and templates are then brought forward on read.

Links live on layers only: one link per layer, rendered as an `<a>` around that layer — never
nested. A link on a full-stage background layer makes the whole slide clickable while the
buttons on top keep their own links. (A separate "slide link" field existed earlier but was never
rendered; it is gone from the form, its database column is left in place.)

Image layers take their alternative text from the file's metadata; the editor shows it as the
placeholder of the "Alternative text" field, and a text entered there overrides it for that
layer only. The panel warns about a button without a link (not clickable) and a linked image
without any alternative text (a link without a name for screen readers).

### Predefined layer classes (page TSconfig)

Selectable per layer in the editor **and** used as the frontend whitelist. One list per layer
type — the editor only offers a layer the classes of its type, and the frontend only renders a
class on a layer of that type. Override or extend per site.

```tsconfig
tx_herobuilder.layerClasses {
    text {
        display-1       = Display 1 (XXL)
        fw-bold         = Bold
        text-bg-primary = Box: primary
        # …
    }
    button {
        btn         = Button base
        btn-primary = Button: primary
        # …
    }
    image {
        rounded-3 = Rounded
        shadow    = Shadow
    }
    # Entries directly under layerClasses (the original flat format) apply to every type.
}
```

The shipped set covers Bootstrap text utilities, `btn`/`btn-*` for buttons and a few image
utilities. A class stored on a layer that is not (or no longer) offered for its type shows as
"not available" in the editor and is not rendered. Class names may use any utility syntax
(`md:text-lg`, `w-1/2`); whitespace, quotes and `<>` are rejected.

### Class groups in the editor (page TSconfig)

Optional presentation for the class chips — the whitelist stays `layerClasses`. Per layer type,
numbered groups with a `label`, a comma list of `classes`, `exclusive = 1` (selecting one class
deselects the others of the group) and `background = 1` (the classes set the layer background;
the text darkening is locked while one of them is selected). Offered classes in no group appear
under "More"; without any group configuration the chips stay one plain list.

```tsconfig
tx_herobuilder.layerClassGroups.text {
    10 {
        label = Size
        exclusive = 1
        classes = display-1, display-3, fs-1, fs-5
    }
    50 {
        label = Box
        exclusive = 1
        background = 1
        classes = text-bg-primary, text-bg-dark
    }
}
```

### Frame and spacing (TypoScript constant)

The hero renders inside the theme's `Default` content layout — frame with anchor id, spacing,
container, header, background — like any other content element. In a theme's full-bleed hero
area that frame would box the hero in, so list those columns in a constant; there the hero
renders bare (layout `HeroPlain`):

```typoscript
# T3Bootstrap's hero area is colPos 5. Empty (default): every hero uses the Default layout.
plugin.tx_herobuilder.settings.framelessColPos = 5
```

New hero elements start with a hidden header (`header_layout = 100`), so their title does not
appear above the stage unless an editor chooses so.

### Overriding the templates

The extension's template, partial and layout paths sit at index 1 of `lib.contentElement`'s
`templateRootPaths` / `partialRootPaths` / `layoutRootPaths` — below the theme (T3Bootstrap: 5)
and the site's own paths (10). A site overrides `Hero.html` or the `Slide` / `Layer` partials
by providing them in its own path; its own `Header/…` partials and `Default` layout apply to the
hero as well.

The site's own paths come from `styles.templates.templateRootPath` / `partialRootPath` /
`layoutRootPath`. With site sets (TYPO3 13+) these are **site settings** of the Fluid Styled
Content set: set them in the site's `settings.yaml`, not as TypoScript constants — a site setting
wins over a constant of the same name, so a constant has no effect.

### Template storage folder (page TSconfig)

Where “Save as template” stores `tx_herobuilder_template` records. Templates are global (listed
regardless of pid); point this at a sysfolder. Falls back to the record’s pid if unset.

```tsconfig
tx_herobuilder.templatePid = 123
```

### Image format (page TSconfig)

Image layer derivatives are written as **WebP** by default — several times smaller than PNG
cut-outs, with their transparency kept. SVG and GIF sources are never converted.

```tsconfig
tx_herobuilder.imageFormat = webp     # default; "original" keeps the source format
```

The format must be listed in `$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']` and writable
by the configured processor — otherwise the source format is used. (GraphicsMagick usually reads
AVIF but cannot write it.)

### Live-preview stylesheets (page TSconfig)

CSS pulled into the live-preview iframe so it looks like the frontend. The extension always adds
its own `hero.css`/`aos.css`; add the site's compiled theme CSS here (absolute URLs used as-is,
`EXT:`/site-relative paths resolved and skipped if missing).

```tsconfig
tx_herobuilder.previewCss {
    10 = /typo3temp/assets/css/mytheme.css
    20 = EXT:my_sitepackage/Resources/Public/Css/fonts.css
}
```

---

## Keyboard shortcuts

Active while the stage/editor is focused.

| Shortcut | Action |
|---|---|
| Arrow keys | Nudge selected layer 1 % (Shift = 5 %) |
| `Delete` / `Backspace` | Delete selected layer |
| `Ctrl/⌘ + Z` | Undo |
| `Ctrl/⌘ + Y` / `Ctrl/⌘ + Shift + Z` | Redo |
| `Ctrl/⌘ + D` | Duplicate selected layer |
| `Ctrl/⌘ + +` / `−` / `0` | Zoom in / out / reset |
| `Ctrl/⌘ + wheel` | Zoom (anchored at cursor) |
| Middle-mouse drag · `Space` + drag | Pan the stage |
| Right-click | Context menu (align, fill stage, z-order, …) |

---

## Architecture

**Backend**
- `Backend/Form/Element/CanvasElement` — custom FormEngine node `herobuilderCanvas`
  (`renderType`), builds the toolbar/stage/panel and hands localized labels + config to the JS.
- `Resources/Public/JavaScript/Backend/canvas.js` — the Moveable-based editor (breakpoint tabs,
  layers, panel, layer list, snap, zoom/pan, undo/redo, context menu, live-preview driver).
- AJAX routes: `herobuilder_fileinfo` (FAL uids → URL/dimensions), `herobuilder_preview`
  (composition → HTML document for the iframe), `herobuilder_template_list` / `herobuilder_template_save`
  (templates), `herobuilder_export` (composition → social/OG image).
- `Backend/Controller/PreviewController` — renders the unsaved composition via the shared
  `CompositionProcessor::expand()` + the `Preview.html` partial, CSP-safe (see above).
- `Backend/Controller/TemplateController` — lists presets + `tx_herobuilder_template` records
  (with thumbnails) and saves new templates via `DataHandler`.
- `Backend/Controller/ExportController` — renders a chosen social/OG format via `CompositeImageService`.
- `Service/CompositeImageService` — flattens a composition to a cached PNG via ImageMagick
  (used for template thumbnails and export).
- `Backend/Preview/HeroBuilderPreviewRenderer` — page-module preview.
- Records/config: `tx_herobuilder_template` (table + TCA, allowed on standard pages),
  presets in `Resources/Private/Templates/Presets/*.json`.
- The templates gallery ships its styles inline (`GALLERY_CSS` in `canvas.js`) because the TYPO3
  modal renders in the top document, where the field stylesheet is not loaded.

**Frontend**
- `DataProcessing/CompositionProcessor` — resolves FAL layers and generates the scoped
  per-breakpoint CSS (geometry, `object-fit`, `object-position`, text scrim, class whitelist)
  and the image layers' `srcset` / `sizes`.
- `Domain/Composition` — the `composition` value object (breakpoints, default stage ratios/widths,
  JSON parsing incl. text layers).
- Templates: `Templates/Hero.html` (carousel) → `Partials/Slide.html` → `Partials/Layer.html`;
  `Partials/Preview.html` (style-less variant for the CSP-safe live preview).
- Assets: `Css/hero.css`, `Css/aos.css`, `JavaScript/hero.js` (AOS init),
  `JavaScript/preview-frame.js` (in-iframe CSS injection + AOS replay).

**Localization** — `Resources/Private/Language/locallang_db.xlf` (en, source) and
`de.locallang_db.xlf` (German). JS strings are localized via a `labels` payload from PHP.
