# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with the BlockManager plugin.

## Overview

BlockManager is a Laravel + Vue 2 plugin that combines two complementary tools for authoring **BlockRegistry block definitions**:

1. **Composer view** — a Gutenberg-style drag-and-drop builder. Authors pick from a curated catalogue of HTML element primitives, choose a visual variant per element, compose them on a canvas, and ship the result as a BlockRegistry block.
2. **Designer view** — a constrained Figma-style visual editor that *creates the variants themselves*. Designers pick an element, edit its design properties (typography, box, border, background, effects, states, responsive) bound to design tokens from `variables.less`, and save the result as a new Twig + LESS variant file pair.

Together, the two views replace the Figma-round-tripped block-authoring loop for design work that lives within the platform's own design system. Figma stays useful for greenfield concepts, photo-realistic comps, or whole-site rebrands; BlockManager owns the productionising of variants and blocks once the design language is established.

It is a sibling of `plugins/GridManager`:
- **GridManager** → composes *layouts* (rows × blocks × elements) on a single page
- **BlockManager** → designs *variants* and composes *block definitions* that BlockRegistry then renders across pages

## Purpose & Motivation

Today, BlockRegistry blocks and their visual variants are authored either (a) manually in the admin UI, (b) via the Figma-to-Twig slash commands (`/mtc-extract`, `/mtc-rebrand`, `/mtc-block-registry`), or (c) by hand-editing JSON in `modules/BlockRegistry/blocks/` and Twig in `sites/default/templates/includes/`. None of these let a designer or non-developer **(i)** design a variant visually with live preview, **(ii)** assemble a block from existing variants without writing Twig, or **(iii)** keep both flows token-aware against `variables.less`.

BlockManager fills both gaps with two views:

**Composer view fills the assembly gap**:
1. A **curated element catalogue** (heading, paragraph, button, image, list, divider, icon, …)
2. **Multiple visual variants per element** (e.g. button → primary | secondary | ghost | pill)
3. A **drag-and-drop canvas** that mirrors the GridManager UX
4. A **Fields panel** that lets the author rename, reorder, require, group-as-repeater, and add custom fields
5. A **ship pipeline** that materialises the composed block into BlockRegistry as a first-class block

**Designer view fills the variant-authoring gap**:
1. A **token-bound property editor** — every colour/spacing/size input resolves to a `variables.less` variable, with raw values allowed as an escape hatch (flagged)
2. **Live preview** on a centre canvas with a breakpoint switcher (mobile / tablet / desktop)
3. **State editing** — default / hover / focus / active / disabled tabs, each emitting an `&:hover { … }` style block in the saved LESS
4. **Code generation** — saving produces a clean Twig partial + LESS file pair plus a manifest entry, ready for Composer to consume immediately via Vite HMR
5. **Round-trip editing** — variants authored in the Designer carry a `_designer_meta` JSON-in-comment header so the property panel can rebuild from the saved LESS losslessly

## Pipeline (mental model)

```
                ┌─────────────────────────── DESIGNER VIEW ───────────────────────────┐
                │                                                                       │
                │   [Element + state + breakpoint]   →   [Property panel]              │
                │   (heading / hover / mobile)            (typography, box, border, …) │
                │                                              │                        │
                │   token picker ──→ variables.less            ▼                        │
                │                                         [Live preview]                │
                │                                              │                        │
                │                                              ▼                        │
                │                                   [Save variant]                      │
                │   writes: elements/{el}/variants/{var}.twig                           │
                │           elements/{el}/styles/{var}.less                             │
                │           element.json (manifest entry)                               │
                └──────────────────────────────────┬────────────────────────────────────┘
                                                   │
                                                   ▼
                ┌──────────────────────────── COMPOSER VIEW ─────────────────────────────┐
                │   [Element catalogue]   heading, paragraph, button, image, …          │
                │           │       ⇣ N variants per element (from Designer or PR)      │
                │           ▼                                                            │
                │   [Drag onto canvas] + [variant picker] + [Fields panel]              │
                │                              │                                         │
                │                              ▼                                         │
                │                       [Save & Ship]                                    │
                │   writes: sites/default/templates/includes/page_blocks/{slug}.twig         │
                │           modules/BlockRegistry/blocks/{slug}.json                    │
                │   calls:  BlockSyncService::importBlock($json)                        │
                └──────────────────────────────────┬────────────────────────────────────┘
                                                   │
                                                   ▼
                                          [BlockRegistry]
                                                   │
                                                   ▼
                                  [Page renders via render_entity_blocks]
```

## Concepts

| Term | Meaning |
|---|---|
| **Element** | A semantic HTML primitive with a fixed set of input fields. E.g. `heading` has `{text, level}`; `button` has `{label, link, target}`; `image` has `{src, alt, crop_sizes}`. Defined by an `element.json` manifest in the catalogue |
| **Variant** | A visual design treatment for an element. E.g. `button` variants: `primary`, `secondary`, `ghost`, `pill`, `outline`. Each variant is a Twig partial under `plugins/BlockManager/elements/{element}/variants/{variant}.twig` plus a LESS stylesheet |
| **Element manifest** | A JSON file per element (`plugins/BlockManager/elements/{element}/element.json`) declaring its inputs, default variant, available variants, icon, category |
| **Composed block** | A user-saved arrangement of element-variant instances. Persisted as a `BlockManagerBlock` row, exported as a BlockRegistry block on ship |
| **Ship** | The act of materialising a composed block into BlockRegistry — writes Twig + JSON, registers the definition via `BlockSyncService::importBlock()` |
| **Property** | A single design attribute in the Designer (e.g. `font-size`, `padding-top`, `background-color`). Each property has a token-bound value and an optional raw-value override |
| **Token** | A LESS variable from `sites/default/css/variables.less` (e.g. `@primary_color`, `@gutter_width`, `@base_font_size`). Designer property values reference tokens by name |
| **State** | A pseudo-class variant of an element design — one of `default`, `hover`, `focus`, `active`, `disabled`. State styles emit `&:hover { … }` blocks in the saved LESS |
| **Breakpoint** | A responsive bracket — `desktop` (default), `tablet`, `mobile`. Non-default breakpoint property overrides emit `@media (max-width: …)` blocks |
| **Variant draft** | An in-progress Designer-view edit, persisted to `block_manager_variant_drafts` as a property tree. Promoted to a file pair on save |
| **Designer meta** | A `/* @designer-meta { … } */` JSON-in-comment header at the top of Designer-authored LESS files. Lets the Designer reopen and round-trip the variant without re-deriving from CSS |

## Directory layout

```
plugins/BlockManager/
├── loader.php                              # Twig fn registration, hook into admin tabs
├── claude.md                               # this file
├── readme.md
├── Providers/
│   └── BlockManagerServiceProvider.php
├── Http/Controllers/
│   ├── BlockManagerController.php          # CRUD for composed blocks (Composer)
│   ├── ElementCatalogueController.php      # GET /api/block-manager/elements
│   ├── VariantCatalogueController.php      # GET /api/block-manager/variants
│   ├── BlockShipController.php             # POST /api/block-manager/{id}/ship
│   ├── VariantDesignerController.php       # CRUD for variant drafts (Designer)
│   ├── VariantPublishController.php        # POST /api/block-manager/variants/{id}/publish
│   └── DesignTokenController.php           # GET /api/block-manager/tokens (read-only listing)
├── Models/
│   ├── BlockManagerBlock.php               # composed block (id, slug, name, structure_json, …)
│   └── BlockManagerVariantDraft.php        # in-progress Designer edit
├── Services/
│   ├── ElementCatalogueService.php         # scans elements/ dir and loads manifests
│   ├── BlockShipService.php                # orchestrates ship: Twig + JSON + DB import
│   ├── TwigGenerator.php                   # composes element-variant partials into a single block template
│   ├── VariantTwigGenerator.php            # emits a variant.twig from element structure + property tree
│   ├── VariantLessGenerator.php            # emits a variant.less from property tree + states + breakpoints (with @designer-meta header)
│   ├── VariantLessParser.php               # reverse direction — reads @designer-meta back into a property tree (round-trip)
│   └── DesignTokenService.php              # parses variables.less, lists tokens with type metadata (colour/space/size/font)
├── routes/
│   └── api.php                             # /api/block-manager/* (auth middleware)
├── database/migrations/
│   ├── {date}_create_block_manager_blocks.php
│   └── {date}_create_block_manager_variant_drafts.php
├── elements/                               # the element catalogue (Twig + LESS + manifest)
│   ├── heading/
│   │   ├── element.json                    # {type, label, inputs, variants, icon, category}
│   │   ├── variants/
│   │   │   ├── default.twig
│   │   │   ├── marketing.twig
│   │   │   └── overlay.twig
│   │   └── styles/
│   │       ├── default.less
│   │       └── marketing.less
│   ├── paragraph/ …
│   ├── button/ …
│   ├── image/ …
│   ├── list/ …
│   ├── divider/ …
│   └── icon/ …
├── admin/
│   ├── index.php                           # mounts <block-manager> Vue root with mode router
│   ├── js/
│   │   ├── components/
│   │   │   ├── BlockManagerMain.vue        # root SPA; routes to Composer or Designer
│   │   │   ├── ModeSwitcher.vue            # toolbar toggle: Composer ↔ Designer
│   │   │   │
│   │   │   ├── composer/                   # Composer view (block assembly)
│   │   │   │   ├── ComposerRoot.vue
│   │   │   │   ├── ElementPicker.vue       # left sidebar
│   │   │   │   ├── BlockCanvas.vue         # centre drop target
│   │   │   │   ├── ElementInstance.vue     # one composed element with variant switcher
│   │   │   │   ├── VariantPicker.vue       # popover/dropdown of variants
│   │   │   │   ├── StructureTree.vue       # right rail: Structure tab
│   │   │   │   ├── BlockSettings.vue       # right rail: Settings tab
│   │   │   │   ├── FieldsPanel.vue         # right rail: Fields tab (the mixed-model heart)
│   │   │   │   └── ShipButton.vue
│   │   │   │
│   │   │   └── designer/                   # Designer view (variant authoring — Figma-style)
│   │   │       ├── DesignerRoot.vue
│   │   │       ├── ElementStatePicker.vue  # left rail: element + state (default/hover/focus/active/disabled) + breakpoint
│   │   │       ├── PreviewCanvas.vue       # centre: live-rendered element preview with breakpoint frame
│   │   │       ├── BreakpointSwitcher.vue  # in canvas toolbar: desktop/tablet/mobile
│   │   │       ├── PropertyPanel.vue       # right rail container with grouped panels:
│   │   │       ├── TypographyPanel.vue     #   font, size, weight, line-height, letter-spacing, colour, transform, decoration, align
│   │   │       ├── BoxPanel.vue            #   width, height, padding, margin
│   │   │       ├── BorderPanel.vue         #   width, style, colour, radius
│   │   │       ├── BackgroundPanel.vue     #   colour, image, gradient
│   │   │       ├── LayoutPanel.vue         #   display, flex, grid, justify, align, gap
│   │   │       ├── EffectsPanel.vue        #   shadow, blur, opacity
│   │   │       ├── CustomCssPanel.vue      #   freeform LESS escape hatch
│   │   │       ├── TokenPicker.vue         # per-property popover: searchable variables.less token list
│   │   │       ├── TokenValueInput.vue     # input that swaps between token-bound and raw-value modes
│   │   │       ├── StateBadge.vue          # shows which state is being edited; visualises diff vs default
│   │   │       └── PublishButton.vue       # writes Twig + LESS + manifest entry
│   │   ├── helpers/
│   │   │   ├── element-registry.js         # mirrors GridManager pattern
│   │   │   ├── variant-registry.js
│   │   │   ├── token-resolver.js           # given a token name, looks up its computed value from variables.less for live preview
│   │   │   ├── property-schema.js          # canonical list of design properties, their CSS targets, and accepted token types
│   │   │   └── designer-less-codec.js      # serialise/deserialise property tree ↔ LESS string (mirrors VariantLessGenerator / VariantLessParser server-side)
│   │   └── store.js                        # Vuex (composer + designer modules)
│   └── styles/
│       └── block_manager_admin.less
└── templates/
    └── admin/
        ├── scripts.twig                    # vite('js/block_manager_admin.js')
        ├── styles.twig                     # vite('css/block_manager_admin.less')
        └── manager.twig                    # admin page shell
```

## Data model

| Table | Columns |
|---|---|
| `block_manager_blocks` | `id`, `uuid`, `slug` (unique), `name`, `description`, `category_id` (nullable), `icon`, `structure_json` (longtext), `last_shipped_at` (nullable), `shipped_block_registry_id` (nullable FK → `block_registry.id`), `created_at`, `updated_at` |
| `block_manager_variant_drafts` | `id`, `uuid`, `element_type` (e.g. `heading`), `variant_slug` (unique with `element_type`), `label`, `properties_json` (longtext — the full property tree across states + breakpoints), `last_published_at` (nullable), `published_twig_path` (nullable), `published_less_path` (nullable), `created_at`, `updated_at` |

`properties_json` schema (Designer view's working state):
```json
{
  "element_type": "button",
  "variant_slug": "primary-pill",
  "label": "Primary Pill",
  "base": {
    "default": {
      "desktop": {
        "typography": { "font_size": { "token": "@base_font_size" }, "font_weight": { "raw": 600 } },
        "box":        { "padding_x": { "token": "@gutter_width" }, "padding_y": { "raw": "12px" } },
        "border":     { "radius": { "token": "@border_radius" } },
        "background": { "color": { "token": "@primary_color" } }
      },
      "mobile":  { "typography": { "font_size": { "raw": "14px" } } }
    },
    "hover":  { "desktop": { "background": { "color": { "token": "@secondary_color" } } } },
    "focus":  { },
    "active": { },
    "disabled": { "background": { "color": { "raw": "#cccccc" } }, "effects": { "opacity": { "raw": 0.6 } } }
  },
  "custom_css": null
}
```

Only properties that *override the default* are stored per state/breakpoint — empty objects mean "inherit". This keeps the JSON compact and the LESS output minimal.

`structure_json` schema:
```json
{
  "elements": [
    {
      "instance_id": "el-001",
      "element_type": "heading",
      "variant": "marketing",
      "field_name": "title",
      "default_value": "Save big this winter"
    },
    {
      "instance_id": "el-002",
      "element_type": "paragraph",
      "variant": "default",
      "field_name": "body",
      "default_value": "…"
    },
    {
      "instance_id": "el-003",
      "element_type": "button",
      "variant": "primary",
      "field_name": "cta",
      "default_value": { "label": "Shop now", "url": "/shop" }
    }
  ],
  "fields_panel": {
    "order": ["title", "title_level", "body", "cta_label", "cta_url"],
    "renames": { },
    "required": { "title": true },
    "custom": [],
    "repeaters": []
  },
  "wrapper": { "css_classes": [], "layout": "stack" }
}
```

## Element manifest schema

`plugins/BlockManager/elements/{element}/element.json`:

```json
{
  "type": "heading",
  "label": "Heading",
  "category": "typography",
  "icon": "fa-heading",
  "inputs": [
    { "name": "text",  "label": "Text",  "type": "text",   "required": true },
    { "name": "level", "label": "Level", "type": "select", "options": ["h1","h2","h3","h4","h5","h6"], "default": "h2" }
  ],
  "variants": [
    { "id": "default",   "label": "Default",   "preview": "default.png" },
    { "id": "marketing", "label": "Marketing", "preview": "marketing.png" },
    { "id": "overlay",   "label": "Overlay",   "preview": "overlay.png" }
  ],
  "default_variant": "default"
}
```

`inputs[].type` MUST be a value from `BlockRegistryField::FIELD_TYPES` so it maps 1:1 to a BlockRegistry field at ship time.

## Element catalogue (initial set)

- **Typography**: `heading`, `paragraph`, `quote`, `list` (ordered/unordered)
- **Media**: `image`, `video`, `icon`
- **Actions**: `button`, `link`
- **Structural**: `divider`, `spacer`

Each element ships with at least one **default** variant. The library grows by adding Twig + LESS file pairs plus a manifest entry — no code changes.

## Ship pipeline (BlockManager → BlockRegistry)

Triggered by **Save & Ship** in the admin UI. `BlockShipService::ship(BlockManagerBlock $block)`:

1. **Generate the Twig template** (`TwigGenerator`)
   - Output path: `sites/default/templates/includes/page_blocks/{slug}.twig`
   - Body: a loop over the saved structure where each element instance includes its variant partial with user-named field bindings. Example output:
     ```twig
     {% include "heading/variants/marketing.twig"
        with { text: fields.title, level: fields.title_level } %}
     ```
   - For repeater-grouped elements, wraps in `{% for slide in slides %} … {% endfor %}` and binds to `slide.foo` instead of `fields.foo`

2. **Write the BlockRegistry JSON** to `modules/BlockRegistry/blocks/{slug}.json`
   - `template`: `"page_blocks/{slug}.twig"`
   - `name`, `slug`, `icon`, `category`, `description`, `template_assignments` — from `BlockManagerBlock` row
   - `fields`: the **final field list from the Fields panel**, not the raw element inputs — the panel is the source of truth, so renames, reorders, required-toggles, custom-additions, and repeater-grouping are honoured
   - JSON shape matches `BlockSyncService::blockToArray()` exactly so `blocks:import` accepts it without translation

3. **Register in BlockRegistry**
   - Call `BlockSyncService::importBlock($json)` — same code path as `php artisan blocks:import`, so DB import is identical to deploy-time import
   - Idempotent — `updateOrCreate` by slug; on subsequent ships, fields are wiped and recreated (safe per BlockRegistry's existing contract — `entity_blocks.data` keys by name, not field id)

4. **Update audit fields**: `last_shipped_at = now()`, `shipped_block_registry_id = $created->id`; `slug` becomes immutable from this point

The JSON file is the source of truth that flows through git → production `deploy.sh` → automatic `blocks:import --all --force`. Once shipped, the block is indistinguishable from any other BlockRegistry block — admins place it on pages via the existing entity-blocks UI, and `render_entity_blocks()` renders it.

## Admin UI — Composer view

Mirrors GridManager's three-pane SPA pattern.

- **Mount point**: `/admin/block-manager` route (registered via `BlockManagerServiceProvider`)
- **Stack**: Vue 2 SPA with three panes plus a tabbed right rail
  - **Left** (`ElementPicker.vue`) — category-grouped element tiles, drag source
  - **Centre** (`BlockCanvas.vue`) — drop target, vertical stack of `ElementInstance`s with reorder; each instance shows the chosen variant rendered live + a `VariantPicker` popover to swap
  - **Right rail** — tabbed:
    - **Structure** (`StructureTree.vue`) — hierarchical list of element instances, click-to-focus
    - **Settings** (`BlockSettings.vue`) — block name, slug, category, icon, template assignments
    - **Fields** (`FieldsPanel.vue`) — the heart of the mixed-model approach. Shows auto-derived fields from the canvas (marked with ◆) and lets the user:
      - Rename a field's machine name + label (with a warning if the block has already shipped, since rename = field destroy/recreate in BlockRegistry)
      - Reorder fields (drag handles)
      - Toggle `required`
      - Add **custom fields** that aren't bound to a canvas element (e.g. a section toggle, an admin-only note) — these appear in the BlockRegistry JSON but aren't referenced in the generated Twig
      - Group consecutive fields into a **repeater** (e.g. wrap three image+caption pairs into a `slides` repeater) — the Twig generator wraps the bound region in `{% for slide in slides %}` accordingly
      - Choose the canonical field type per input where multiple are valid (e.g. an `image` element input can map to BlockRegistry `image` *or* `file`)
- **State**: Vuex stores the working draft (canvas structure + fields panel state); saves go to `PUT /api/block-manager/{id}`
- **Rich-text inputs**: TipTap powers inline `text`/`textarea` previews on the canvas — reuse pattern from `GridManager/admin/js/components/items/TipTapEditor.vue`
- **Drag**: reuse `vue-draggable-plus` already pulled in by GridManager (for canvas reorder, fields-panel reorder, and element drag-in)

## Admin UI — Designer view

The Designer view is the platform's Figma alternative *bounded to the existing element catalogue and design tokens*. It is **not** a free-form vector tool; it is a constrained, code-generating visual editor whose output is production-ready Twig + LESS.

### Surface

Three panes mirroring the Composer's shape so authors can context-switch quickly:

- **Left rail** — `ElementStatePicker.vue`
  - Element selector (`heading`, `paragraph`, `button`, …) — uses the same element manifests Composer reads
  - State tabs: `Default`, `Hover`, `Focus`, `Active`, `Disabled` — only meaningful states for the element are shown (e.g. `paragraph` doesn't expose `Hover`)
  - Variant draft list — switch between in-progress and published variants for this element

- **Centre canvas** — `PreviewCanvas.vue`
  - Live preview of the element rendered with current property tree applied
  - Breakpoint frame switcher: **Desktop** (default, full width), **Tablet** (`@mobile_width` → tablet bracket), **Mobile** (`<= @mobile_width`)
  - Sample-content swap (for `heading`, toggle between short / long; for `image`, swap placeholder; etc.) so designers see the variant under realistic content
  - Zoom + ruler grid

- **Right rail** — `PropertyPanel.vue` (grouped accordion of sub-panels)
  - **Typography** — `font-family` (from a curated list bound to `@base_font_family` and siblings), `font-size`, `font-weight`, `line-height`, `letter-spacing`, `color`, `text-transform`, `text-decoration`, `text-align`
  - **Box** — `width`, `height`, `min/max-width`, `padding` (per side), `margin` (per side)
  - **Border** — `border-width`, `border-style`, `border-color`, `border-radius` (per corner or unified)
  - **Background** — `background-color`, optional `background-image`, optional gradient builder
  - **Layout** — `display`, `flex-direction`, `justify-content`, `align-items`, `gap`, or `grid-template-columns/rows`
  - **Effects** — `box-shadow` (preset chooser + custom), `opacity`, `filter: blur`
  - **Custom CSS** — escape-hatch textarea for arbitrary LESS appended to the variant block; lints for token references (warns on raw values)

### Token binding

Every colour, spacing, size, and font input has a **token picker** popover. The picker lists tokens from `variables.less` filtered by type (`color` tokens for colour inputs, `length` tokens for spacing/size, `font` tokens for typography). The selected token is stored as `{"token": "@primary_color"}`; raw values are stored as `{"raw": "#0f55c4"}`. Mixed usage in a single variant is allowed but a banner on save reports "N raw values not bound to tokens — design system audit may flag this".

`DesignTokenService` parses `sites/default/css/variables.less` at request time and exposes:
```php
[
  ['name' => '@primary_color',     'type' => 'color',  'value' => '#0f55c4'],
  ['name' => '@gutter_width',      'type' => 'length', 'value' => '32px'],
  ['name' => '@base_font_size',    'type' => 'length', 'value' => 16],
  ['name' => '@wrapper_width',     'type' => 'length', 'value' => '1380px'],
  …
]
```

The Designer **does not write** to `variables.less`. Token additions go through `/mtc-rebrand` or a dedicated future "Token Editor" view. This is a hard line — the Designer is a consumer of the design system, not its owner.

### States & breakpoints

Property overrides are stored per-`(state, breakpoint)` cell of the 5×3 matrix. The default cell (`default` × `desktop`) is the **base**; every other cell stores only the diff. Cell switching in the UI is reflected by a `StateBadge.vue` showing "Editing: hover @ mobile (3 overrides)".

LESS output organisation:

```less
/* @designer-meta {"version":1,"element":"button","variant":"primary-pill",...} */
.button--primary-pill {
  /* default @ desktop */
  font-size: @base_font_size;
  font-weight: 600;
  padding: 12px @gutter_width;
  border-radius: @border_radius;
  background-color: @primary_color;

  @media (max-width: @mobile_width) {
    font-size: 14px;
  }

  &:hover {
    background-color: @secondary_color;
  }

  &:disabled {
    background-color: #cccccc;
    opacity: 0.6;
  }
}
```

### Variant publish pipeline (Designer → catalogue)

Triggered by **Publish** in the Designer toolbar. `VariantPublishService::publish(BlockManagerVariantDraft $draft)`:

1. **Generate Twig** (`VariantTwigGenerator`)
   - Output: `plugins/BlockManager/elements/{element_type}/variants/{variant_slug}.twig`
   - Body uses the element's HTML shape from its manifest, with class names bound to the variant (`.button--primary-pill`) and parameter slots for element inputs (`{{ label }}`, `{{ link }}`, etc.)
2. **Generate LESS** (`VariantLessGenerator`)
   - Output: `plugins/BlockManager/elements/{element_type}/styles/{variant_slug}.less`
   - Prepends a `/* @designer-meta { … } */` JSON header for round-trip
   - Emits base properties, then state blocks (`&:hover`), then breakpoint blocks (`@media`), then custom CSS
3. **Update manifest** — appends the variant entry to `element.json`:
   ```json
   { "id": "primary-pill", "label": "Primary Pill", "preview": "primary-pill.png" }
   ```
4. **Preview screenshot** — optional: Puppeteer renders the variant at the desktop breakpoint and writes `{variant_slug}.png` next to the LESS file
5. **Update draft audit fields**: `last_published_at = now()`, `published_twig_path` and `published_less_path` populated
6. **Hot reload** — Vite HMR picks up the new files; Composer's variant picker shows the new variant on next refresh

### Round-tripping existing variants

When a Designer opens a published variant:
- `VariantLessParser` reads the `/* @designer-meta { … } */` header. If present, it deserialises into a property tree directly — no CSS parsing needed
- If the header is missing (e.g. a variant authored manually via `/mtc-extract` or hand-coded), the Designer shows a **"Read-only — designed outside BlockManager"** badge. Property panels are disabled; the only available actions are *Duplicate as new variant* (which seeds a fresh draft with empty overrides) and *Open in editor* (jumps to the file)
- This boundary keeps round-trip predictable: Designer never silently rewrites code it didn't generate

### Boundaries of Designer view (what it is not)

- **Not a vector tool** — no shapes, no pen tool, no arbitrary paths. The HTML structure of each element is fixed by its manifest
- **Not a layout tool** — page layout is GridManager's job; block composition is Composer's job. Designer designs *one element variant at a time*
- **Not a design-system editor** — `variables.less` is read-only from here. Use `/mtc-rebrand` or a dedicated future token editor
- **Not multi-user / real-time** — single-author drafts; concurrent edits to the same draft are last-write-wins (with `updated_at` conflict detection)
- **Not an arbitrary CSS editor** — the Custom CSS escape hatch exists for one-off needs but is flagged on save; large free-form CSS belongs in the file-system Twig+LESS workflow

## Vite integration

Add to `vite.config.js` rollup inputs (alongside the GridManager bundles):
```js
blockManagerAdminJs:    resolve(__dirname, 'sites/default/js/block_manager_admin.js'),
blockManagerAdminStyle: resolve(__dirname, 'sites/default/css/block_manager_admin.less'),
```
`block_manager_admin.js` imports the Vue components from `plugins/BlockManager/admin/js/`.

## API routes

`plugins/BlockManager/routes/api.php`, `auth` middleware:

**Composer view**
| Method | Path | Controller@Method |
|---|---|---|
| GET | `/api/block-manager` | `BlockManagerController@index` |
| GET | `/api/block-manager/elements` | `ElementCatalogueController@index` |
| GET | `/api/block-manager/{id}` | `BlockManagerController@show` |
| POST | `/api/block-manager` | `BlockManagerController@store` |
| PUT | `/api/block-manager/{id}` | `BlockManagerController@update` |
| DELETE | `/api/block-manager/{id}` | `BlockManagerController@destroy` |
| POST | `/api/block-manager/{id}/ship` | `BlockShipController@ship` |
| POST | `/api/block-manager/{id}/duplicate` | `BlockManagerController@duplicate` |

**Designer view**
| Method | Path | Controller@Method |
|---|---|---|
| GET | `/api/block-manager/variants` | `VariantDesignerController@index` (list drafts, filter by element) |
| GET | `/api/block-manager/variants/{id}` | `VariantDesignerController@show` |
| POST | `/api/block-manager/variants` | `VariantDesignerController@store` (create draft) |
| PUT | `/api/block-manager/variants/{id}` | `VariantDesignerController@update` (save properties_json) |
| DELETE | `/api/block-manager/variants/{id}` | `VariantDesignerController@destroy` |
| POST | `/api/block-manager/variants/{id}/publish` | `VariantPublishController@publish` |
| POST | `/api/block-manager/variants/{id}/preview` | `VariantDesignerController@preview` (renders HTML+CSS for live canvas without writing files) |
| POST | `/api/block-manager/variants/import-existing` | `VariantDesignerController@importExisting` (open a published variant — uses `@designer-meta` if present, else marks read-only) |
| GET | `/api/block-manager/tokens` | `DesignTokenController@index` (lists tokens from `variables.less` with type metadata) |

## Explicit design decisions (locked)

**Composer**
- **Variant storage** — Twig+LESS files on disk under `plugins/BlockManager/elements/{element}/variants/` plus a per-element `element.json` manifest. Adding a variant = drop in a new file pair + manifest entry. No DB, no code change.
- **Ship mechanism** — Write `sites/default/templates/includes/page_blocks/{slug}.twig` + `modules/BlockRegistry/blocks/{slug}.json` to disk, then call `BlockSyncService::importBlock()` to register in the DB. JSON is the source of truth for git/deploy; DB import is automatic on production deploy.
- **Fields configuration** — Mixed model: auto-derived from canvas elements *plus* a Fields panel that allows rename, reorder, required-toggle, custom-field-add, and repeater-grouping. The Fields panel is the source of truth at ship time.
- **Element inputs ↔ BlockRegistry field types** — manifest's `inputs[].type` MUST be a value in `BlockRegistryField::FIELD_TYPES` so it maps 1:1 at ship time.
- **Variant change after ship** — re-ships rewrite the Twig only; field names stable → existing `entity_blocks.data` preserved.
- **Renaming a field after first ship** — UI shows a warning ("this will destroy existing data on N placed instances") and requires confirmation.
- **Slug immutability** — once a block has been shipped, the slug is locked (to keep BlockRegistry sync stable).
- **Custom (element-less) fields** — appear in the BlockRegistry JSON but aren't referenced in the generated Twig. Authors who use these are expected to extend the generated Twig manually in a follow-up PR (or use them as admin-only flags).

**Designer**
- **Tokens are read-only** — Designer never writes to `variables.less`. Adding/changing tokens stays in `/mtc-rebrand` or a future Token Editor.
- **Property tree shape** — diff-from-base storage: only overrides per (state, breakpoint) are persisted; empty cells inherit. Avoids `properties_json` ballooning to a Cartesian product.
- **LESS round-trip via `@designer-meta`** — Designer-authored LESS gets a JSON-in-comment header (`/* @designer-meta { … } */`) at the top. Reopening reads the header directly; if missing, the variant is marked read-only.
- **Publish is idempotent by `(element_type, variant_slug)`** — repeated publishes overwrite the Twig + LESS in place, with the previous file backed up to `.bak` for one cycle.
- **No silent CSS edits** — if a user hand-edits a Designer-authored LESS file outside the UI, the next Publish detects the mismatch (parsed LESS ≠ `@designer-meta` body) and prompts: "Local edits detected — overwrite, merge, or open in editor?"
- **Token-binding is recommended, not required** — raw values are allowed but flagged on save (the badge "N raw values" appears on the published variant).
- **Variant slugs are namespaced by element** — `(element_type, variant_slug)` is the unique key, so `button.primary` and `heading.primary` can coexist.

## Boundaries & non-goals

**Plugin-wide**
- BlockManager **does not** render blocks at runtime — once shipped, the block lives in BlockRegistry and renders via the existing `render_entity_blocks` chain.
- BlockManager **does not** manage page layout — that is GridManager's job. The three plugins (BlockManager / BlockRegistry / GridManager) layer cleanly: BlockManager designs variants → BlockManager composes blocks → BlockRegistry registers them → GridManager (optionally) arranges them on a page → page renders.
- **No multi-language / no per-locale variants in v1.**

**Composer**
- Variants are **design-only** at the Composer level — they do not change the input schema of an element. (If a "card" element needs an image + title + body, all variants of "card" require the same inputs.)
- **No nested compositions in v1** — a composed block is a flat list of element instances. Nesting (e.g. a block inside a block) is future work.

**Designer (Figma-alternative honesty)**
- Designer is **not** a free-form vector or layout tool — it is a *constrained, code-generating visual editor* whose canvas is bound to the existing element catalogue and design tokens. If a need cannot be expressed within those bounds, the right answer is to (a) extend the element manifest in a PR, (b) add tokens via `/mtc-rebrand`, or (c) author the variant Twig+LESS by hand and accept the "read-only in Designer" badge.
- **No drawing primitives, no pen tool, no multi-page canvases, no plugin ecosystem, no comments/annotations, no real-time collaboration, no version branching.**
- **No bidirectional sync with Figma** — Designer is an alternative *path*, not a mirror of a Figma file. Round-trip is between the Designer property tree and the generated LESS only.
- **No arbitrary CSS authoring** — the Custom CSS panel is an explicit escape hatch with size limits and lint warnings, not a primary surface.

## Dependencies

Already in `package.json` via GridManager — no new npm installs expected for the Composer MVP:
- `vue-draggable-plus`
- `@tiptap/*` (rich-text inputs)
- `vue-sweetalert2` (confirm/save/dismiss dialogs)
- FontAwesome Pro (icon picker)

**Designer view may add** (re-evaluate at Track B kick-off — only if existing tooling can't cover it):
- A LESS parser usable in PHP (`oyejorge/less.php` is already in the project for the build pipeline — reuse it server-side for `VariantLessParser`)
- A small client-side colour utility for the colour picker (`@ckpack/vue-color` or similar) — only if FontAwesome's existing colour-input UX isn't sufficient
- Puppeteer/Playwright for publish-time preview screenshots — optional, can use Vite-rendered DOM snapshots instead

## Implementation phasing

**Track A — Composer MVP (ships first)**

1. **Scaffolding** — plugin skeleton, service provider, migrations, empty admin route
2. **Element catalogue** — manifest schema + 3 starter elements (heading, paragraph, button) each with 2 hand-authored variants
3. **Composer SPA** — read-only catalogue browser → drag-to-canvas → save draft
4. **Ship pipeline** — Twig generator + JSON writer + `BlockSyncService::importBlock()` call
5. **Composer right rail** — variant picker UI, structure tree, settings panel, fields panel
6. **Composer hardening** — slug lock, field-rename warnings, re-ship diff preview

**Track B — Designer MVP (starts after Composer phase 3)**

7. **Token reader** — `DesignTokenService` + `GET /api/block-manager/tokens` + frontend token resolver
8. **Designer skeleton** — `ModeSwitcher`, `DesignerRoot`, three-pane layout, draft CRUD
9. **Base property panels** — Typography, Box, Border, Background applied to the `default` state at the `desktop` breakpoint, no states/breakpoints yet
10. **Preview canvas** — live render with the property tree applied; sample content swap
11. **Publish pipeline** — `VariantTwigGenerator`, `VariantLessGenerator` (with `@designer-meta` header), manifest patcher
12. **States** — 5-tab state picker, diff-from-base property store, `&:hover` emission
13. **Breakpoints** — 3-frame switcher, `@media` emission, mobile-first vs desktop-first decision
14. **Round-trip** — `VariantLessParser` for `@designer-meta`-tagged files; read-only badge for foreign files
15. **Effects + Layout + Custom CSS panels** — round out the property panel set
16. **Designer hardening** — overwrite detection (local-edit mismatch), `.bak` retention, raw-value audit banner, publish-preview screenshot

## Related modules / plugins

- `modules/BlockRegistry` — downstream consumer of Composer ships. See `modules/BlockRegistry/CLAUDE.md`
- `plugins/GridManager` — sibling plugin (page layout authoring). UX patterns mirrored here
- `core/includes/twig.php` — plugin auto-discovery contract (`loader.php`, `hooks.php`, `templates/`)
- `sites/default/templates/includes/page_blocks/` — output directory for generated block Twig templates (Composer ship target)
- `modules/BlockRegistry/blocks/` — output directory for generated BlockRegistry JSON definitions (Composer ship target)
- `sites/default/css/variables.less` — design token source. Read-only from Designer view
- `.claude/commands/mtc-extract.md` — Figma-to-Twig slash command; complements Designer for greenfield concepts
- `.claude/commands/mtc-rebrand.md` — Figma-driven whole-site rebrand; owns token writes to `variables.less`
- `.claude/commands/mtc-block-registry.md` — generates BlockRegistry JSON from existing Twig; superseded by Composer ship for blocks built within BlockManager

## Acknowledgements

- Status: scope draft
- Created by: MTC Team
- Author: Nithika Senaweera — nithika.senaweera@mtc.co.uk
