# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

BlockRegistry is a Laravel module providing ACF-style block management for the MTC Pharmacy platform. It lets admins define reusable content blocks with configurable fields, assign them to page templates, and manage block instances on pages, products, and categories.

## Commands

Schema migrations live in `database/migrations/` and run via `make migrate` / `php artisan migrate`.

Block definitions are managed as JSON files in `blocks/` and synced via:
- `php artisan blocks:export {slug?} {--all}` — export DB definitions to JSON (blocked on production)
- `php artisan blocks:import {slug?} {--all} {--force}` — import JSON definitions into DB
- `php artisan blocks:diff {slug?}` — show differences between DB and JSON files

The admin sync page is at `/admin/blocks/sync`.

On deploy, `blocks:import --all --force` runs automatically after migrations (see `deploy.sh`).

## Architecture

### Bootstrap

The module boots via `BlockRegistryServiceProvider` (registered in `config/app.php`). It registers service singletons, loads routes, migrations, and artisan commands. Legacy hooks in `hooks.php` register Twig functions and add admin tabs.

### Two-Layer Data Model

**Definition layer** — what a block *is*:
- `BlockRegistry` — block definitions (name, slug, icon, optional Twig template path)
- `BlockRegistryField` — field definitions with self-referencing `parent_field_id` for repeater sub-fields
- `BlockRegistryCategory` — organizational grouping
- `BlockRegistryTemplateAssignment` — whitelist of which blocks are available on which page templates (with `is_default` flag for auto-add)

**Instance layer** — block data on a specific entity:
- `EntityBlock` — a block placed on a page/product/category/global, stores field values as JSON in `data` column
- `entity_type` is one of: `page`, `item`, `category`, `global`
- `region` is an optional `VARCHAR(64)` slot per row (e.g. `header`/`main`/`footer`) — `NULL` and `'main'` are treated equivalently in read scopes
- Global blocks (`entity_type=global`, `entity_id=0`) can be referenced from other entities via `source_block_id`

### JSON Sync System

Block definitions are stored as one JSON file per block in `blocks/{slug}.json`. Each file contains the block's metadata, fields (with nested sub-fields), category, and template assignments. The slug is the identity key.

**Workflow:**
- Edit blocks via admin UI on local or staging
- Export to JSON via admin sync page or `blocks:export`
- Commit JSON files to git branch
- Merge via normal git workflow
- Production auto-imports on deploy

**Key rules:**
- Export is blocked on production (`APP_ENV=production`)
- Import uses `updateOrCreate` by slug — safe to run repeatedly
- Fields are deleted and recreated on import (safe because `entity_blocks.data` keys by field name, not ID)
- Categories are auto-created on import if they don't exist
- Blocks in DB without JSON files are left untouched (never auto-deleted)

### Example Content (`example_data`)

Each block JSON may carry an optional top-level **`example_data`** key — one sample instance the block can be prefilled with. It drives two things from a single source:

1. **Previews** — `/component` and `/example-blocks` render a block's `example_data` through the real pipeline (`EntityBlockService::renderExample()`), so the preview is identical to a published instance. The Twig function `render_example_block(slug, themeOverride)` (in `hooks.php`) does this; it **falls back** to the template's inline `{% if show_dummy_data %}` block when no `example_data` is authored, so blocks migrate one at a time.
2. **Admin prefill** — the block data editor (`/admin/blocks/data/{id}`) shows a **"Fill with example content"** button for blocks that have `example_data`. It links to `?prefill=example`, and `editData()` renders the form populated from `example_data` **without saving** — the editor reviews the populated fields and clicks Save to persist, or navigates away to discard. Selector fields are blanked. Non-destructive by design: it never auto-writes, so existing field values are only replaced if the editor explicitly Saves.

**Authoring shape (`example_data` is authored NESTED / human-readable, NOT stringified):**
- simple (text/textarea/select/checkbox/colorpicker/datetime/icon) → scalar (`checkbox` → `"1"`/`"0"`; `Background` → `"white"`)
- `link` → `{ "url", "title", "target" }`; `image` → `{ "url", "alt", … }`; `file` → `{ "url" }`
  - `link` `url`s **must be relative and environment-neutral** (e.g. `/about-us`) — never a local absolute/category URL (`http://localhost…`, `/category/123`). `link` fields are free-form and are intentionally NOT blanked by `sanitizeExampleData()`, so an environment-specific URL authored here would survive a prefill.
- `repeater` → array of row objects (sub-field link/image as objects, text/icon as scalars)
- selector types (`page_selector`/`product_selector`) → **omit**; they store local IDs that don't map across environments. (Categories have no selector field type — they're referenced via `link` fields, so the rule above covers them.)

`EntityBlock::normalizeExampleData()` converts this nested shape to the true storage shape (JSON-encoding `link`/`image`/`file`/`repeater`) before render/save, so a prefilled block is byte-identical to an admin-saved one (the data editor loads it correctly). `EntityBlock::sanitizeExampleData()` blanks selector fields on prefill.

`example_data` is **file-only**: import ignores it, diff doesn't compare it, and export preserves it from the existing file. Blocks whose preview needs live entities or multiple variants (selector/product/category-driven blocks, `video_card`) keep their inline `show_dummy_data` and are intentionally NOT given `example_data`.

### Services

| Service | Responsibility |
|---------|---------------|
| `BlockRegistryService` | CRUD for block definitions, field management, duplication, validation |
| `BlockSyncService` | JSON export, import, diff, and sync status |
| `EntityBlockService` | Runtime block rendering, data access, Twig function implementations |
| `TemplateBlockService` | Template-to-block assignment management |
| `TemplateRegionRegistry` | Maps page templates to ordered region slugs for multi-region layouts |
| `ImageProcessingService` | Image cropping, srcset generation via Intervention Image v3 (Imagick) |
| `BlockTemplateService` | Block template (bundle) management |

### Rendering Pipeline

`EntityBlockService::renderBlock()` resolves the block's Twig template (from `BlockRegistry.template` field), prepares template data, and renders via `app('twig')`. The template receives: `block`, `block_name`, `block_slug`, `block_data` (field data with value, type, label, required), `fields` (simple values), `slides` (repeater data), and `p.pagedata` (legacy-compatible format).

Frontend Twig functions (registered in `hooks.php`):
- `render_block(entityBlockId)` — single block
- `render_entity_blocks(entityType, entityId, region = null)` — all published blocks for an entity, optionally scoped to a single region
- `render_blocks_by_type(entityType, entityId, blockSlug)` — filter by block type
- `get_entity_blocks(entityType, entityId)` — raw block data
- `render_global_block(globalBlockId)` — global block by ID
- `render_example_block(slug, themeOverride = null)` — render a block's `example_data` (preview only); falls back to inline `show_dummy_data`

### Admin Routes

All under `/admin/blocks` with `auth` middleware. The controller handles:
- Block definition CRUD (`/`, `/create`, `/{id}/edit`)
- Entity block data editing (`/data/{id}`)
- Global blocks (`/global`, `/global/create`)
- Categories (`/categories`)
- Sync page (`/sync`) — export, import, diff with status dashboard
- AJAX endpoints for pages (`/ajax/*`) and generic entities (`/ajax/entity/*`) — adding, reordering, toggling status

Admin integration files in `admin/` inject "Blocks" tabs into product, category, and page editors.

### Field Types

`text`, `textarea`, `checkbox`, `select`, `image`, `file`, `datetime`, `colorpicker`, `product_selector`, `page_selector`, `link`, `repeater`

Repeater fields support sub-fields (but not nested repeaters). Image fields support cropping with aspect ratio enforcement, and automatic srcset generation to `/uploads/cache/blocks/{entityBlockId}/`. Variants are encoded as WebP when the Imagick WebP delegate is available (lossless for sources with real transparency, lossy/quality-driven otherwise), falling back to PNG (transparent) or JPEG (opaque) when the delegate is missing — common on shared hosting. Transparency is decided by a pixel-level alpha inspection of the source, not its file extension. Icon fields provide a visual picker modal with searchable FA5 Pro icons and custom site icons (from `sites/default/css/base/icons.less`). The icon manifest is generated dynamically via `/admin/blocks/ajax/icon-manifest` — adding a new `&--iconName` entry to `icons.less` automatically makes it available in the picker.

### Conditional Field Visibility

Fields can be conditionally shown/hidden in the data editor based on a `select`-type field's value. This is configured in the block definition editor under each field's "Conditional Visibility" section, or directly in the JSON `settings` column:

```json
{"visibility": {"field": "Layout", "values": ["carousel"]}}
```

**Definition side** (`block-registry-edit.js`): `initVisibilitySelects()` populates the "depends on" dropdown with select-type fields, `updateVisibilityValues()` shows checkboxes for the controller's options. The controller's `parseFieldsFromRequest()` merges visibility into the `settings` JSON alongside any type-specific settings (image, page_selector).

**Data editor side** (`block-registry-edit-data.js`): `initFieldVisibility()` attaches change listeners to controller fields, `applyFieldVisibility()` evaluates `data-visibility-field` / `data-visibility-values` attributes on field wrappers and toggles `display: none`. Hidden fields keep their form values. Called on DOMContentLoaded and after `addRepeaterRow()` for dynamically added rows.

Works for both top-level fields and repeater sub-fields. The `settings` column is not cast on `BlockRegistryField` — it's stored as a JSON string and parsed in templates via `json_decode`.

### JavaScript

Vanilla JS files in `js/` — no build step, loaded directly in admin templates. Key files: `block-registry-edit.js` (block definition editor), `block-registry-edit-data.js` (data editor with image crop), `block-registry-entity-blocks.js` (AJAX block management on pages/entities), `block-registry-drag.js` (drag-and-drop reordering), `block-registry-sync.js` (sync page diff toggle and confirmations).

### Template Regions

`TemplateRegionRegistry::TEMPLATE_REGIONS` maps a template name to an ordered list of region slugs. Templates not listed implicitly have a single `main` region — existing block-based templates need no change. Multi-region templates (e.g. `content_sidebar.twig` → `['header', 'main', 'footer']`):

- Admin renders one sortable list per region inside the entity-blocks fieldset, each with its own "Add New Block" button (the click handler reads `data-region` and forwards it to the picker)
- The "Add" AJAX endpoints validate the incoming `region` against the entity template's declared regions via `BlockRegistryController::resolveRegionInput()` — invalid values fall back to `null` (default region) so a stale form can never write a rogue region
- Drag-reordering is per-region: `block-registry-entity-blocks.js` initialises one Sortable per `.entityBlocksSortable` and only sends rows from that region in the reorder POST. Order sequences are independent across regions because `EntityBlock::getMaxOrder()` is region-scoped
- Frontend renders each region with a separate `render_entity_blocks('page', p.id, 'region')` call. The `forRegion(null|'main')` scope matches both `region = 'main'` and legacy `NULL` rows, so existing pages keep rendering after the migration
- Cache keys include the region slug (or `'all'` when not scoped). `EntityBlockService::clearCache()` forgets the no-region key plus a key per region currently in DB **and** every region declared by the entity's template — necessary because the last row in a region might have just been deleted, leaving a stale cache entry
- The `2026_05_04_100000` migration adds the column and `(entity_type, entity_id, region)` index. The `100001` migration backfills `NULL` → `'main'` so legacy rows and new explicit `'main'` rows share an unambiguous ordering bucket

## Key Patterns

- Block definitions auto-generate UUID and slug on create, cascade-delete related data
- EntityBlock auto-sets `order` on create (scoped to `(entity, region)`) and initializes `data` with field defaults
- Published/draft status filtering — frontend rendering only shows published blocks
- Git history replaces in-app versioning — the `block_registry_versions` table exists but is unused
- Image paths are validated against directory traversal in `ImageProcessingService`. Paths resolve within the project root in production; on staging only, image-extension paths may additionally resolve against a `public_html` shared root (the standard shared-hosting layout), configurable via `BLOCKREGISTRY_STAGING_IMAGE_SOURCE` in `config/blockregistry.php`
