# 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)

### 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

### 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}/`. 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`
