# Layout

[![Commercial](https://img.shields.io/badge/License-Commercial-red.svg)](LICENSE.md)
[![Cockpit CMS](https://img.shields.io/badge/Cockpit%20CMS-Addon-blue.svg)](https://getcockpit.com)
[![Components](https://img.shields.io/badge/Feature-Visual%20Builder-green.svg)](#features)

> **Visual component system and layout builder for Cockpit CMS**

Layout provides a powerful drag-and-drop component system that enables visual content creation. Build complex layouts using pre-defined or custom components, create reusable shared components, and deliver dynamic content with an intuitive interface.

## ✨ Features

### 🎨 **Component System**
- **Built-in Components**: Grid, Row, Section, Button, Heading, Text, Image, Video, and more
- **Custom Components**: Create your own components with custom fields and logic
- **Shared Components**: Build once, reuse everywhere with shared component library
- **Component Groups**: Organize components into logical categories
- **Visual Preview**: Live preview of components while editing

### 🔧 **Developer-Friendly**
- **Field Types**: Support for all Cockpit field types (text, select, asset, wysiwyg, etc.)
- **i18n Support**: Built-in internationalization for multilingual content
- **Event Hooks**: Extend component behavior with custom processing
- **API Access**: Full programmatic access to component system
- **Pages Integration**: Seamless integration with Pages addon for URL handling

### 📊 **Layout Capabilities**
- **Nested Structures**: Create complex layouts with Grid, Row, and Section containers
- **Responsive Design**: Mobile-first component architecture
- **Asset Management**: Direct integration with Cockpit's asset system
- **Dynamic Content**: Process components with context-aware data
- **Flexible Configuration**: Extensive customization options per component

## 🏗️ Built-in Components

### Layout Components

| Component | Description | Container |
|-----------|-------------|-----------|
| **Grid** | Multi-column layout with configurable column widths | ✅ |
| **Row** | Horizontal container for organizing components | ✅ |
| **Section** | Semantic container for grouping related content | ✅ |
| **Spacer** | Add vertical spacing between components | ❌ |
| **Divider** | Visual separator line | ❌ |

### Content Components

| Component | Description | Fields |
|-----------|-------------|--------|
| **Heading** | H1-H6 headings with i18n support | `text`, `level` |
| **Richtext** | WYSIWYG editor for formatted content | `html` |
| **Markdown** | Markdown editor with preview | `markdown` |
| **HTML** | Raw HTML code editor | `html` |
| **Button** | Call-to-action button | `url`, `caption`, `target` |
| **Link** | Text link component | `url`, `caption`, `target` |

### Media Components

| Component | Description | Features |
|-----------|-------------|----------|
| **Image** | Image with optional link | Asset picker, URL field |
| **Video** | Video player with controls | Asset picker, poster image, attributes |

## 🔨 Creating Custom Components

### Basic Component Structure

Custom components are defined as PHP arrays with specific properties:

```php
<?php
// In your addon's bootstrap.php or module initialization

$this->on('layout.components.collect', function($components) {
    
    $components['testimonial'] = [
        'label' => 'Testimonial',
        'group' => 'Custom',
        'fields' => [
            [
                'name' => 'quote',
                'type' => 'textarea',
                'label' => 'Quote',
                'i18n' => true,
                'opts' => ['rows' => 4]
            ],
            [
                'name' => 'author',
                'type' => 'text',
                'label' => 'Author Name',
                'i18n' => true
            ],
            [
                'name' => 'role',
                'type' => 'text',
                'label' => 'Author Role',
                'i18n' => true
            ],
            [
                'name' => 'image',
                'type' => 'asset',
                'label' => 'Author Photo',
                'opts' => ['filter' => ['type' => 'image']]
            ]
        ],
        'preview' => '<div class="kiss-padding">
            <div v-if="data.quote" class="kiss-color-muted">
                <i>"{{ truncate(data.quote, 100) }}"</i>
            </div>
            <div v-if="data.author" class="kiss-text-bold kiss-margin-small-top">
                - {{ data.author }}
            </div>
        </div>',
        'children' => false
    ];
});
```

### Advanced Component with Options

```php
$components['feature-box'] = [
    'label' => 'Feature Box',
    'group' => 'Custom',
    'fields' => [
        [
            'name' => 'icon',
            'type' => 'text',
            'label' => 'Icon Name',
            'info' => 'Material icon name'
        ],
        [
            'name' => 'title',
            'type' => 'text',
            'label' => 'Title',
            'i18n' => true
        ],
        [
            'name' => 'description',
            'type' => 'textarea',
            'label' => 'Description',
            'i18n' => true
        ],
        [
            'name' => 'style',
            'type' => 'select',
            'label' => 'Box Style',
            'opts' => [
                'options' => [
                    ['value' => 'default', 'label' => 'Default'],
                    ['value' => 'bordered', 'label' => 'Bordered'],
                    ['value' => 'shadowed', 'label' => 'With Shadow']
                ],
                'default' => 'default'
            ]
        ]
    ],
    'preview' => '<div class="kiss-padding kiss-bgcolor-contrast kiss-color-muted">
        <icon v-if="data.icon">{{ data.icon }}</icon>
        <div v-if="data.title" class="kiss-text-bold">{{ data.title }}</div>
        <div v-if="data.description" class="kiss-size-small">{{ truncate(data.description, 50) }}</div>
    </div>',
    'children' => false,
    'type' => null,                    // Layout behavior: null|'grid'|'row'
    'opts' => [
        'dialog.size' => 'large',        // Edit dialog size: small, normal, large, xlarge
        'previewComponent' => null       // Custom Vue component for preview
    ]
];
```

### Container Component

Container components can hold other components:

```php
$components['accordion'] = [
    'label' => 'Accordion',
    'group' => 'Layout',
    'fields' => [
        [
            'name' => 'title',
            'type' => 'text',
            'label' => 'Accordion Title',
            'i18n' => true
        ],
        [
            'name' => 'expanded',
            'type' => 'boolean',
            'label' => 'Initially Expanded',
            'default' => false
        ]
    ],
    'preview' => '<div class="kiss-padding kiss-bgcolor-contrast">
        <div class="kiss-flex kiss-flex-middle">
            <icon>{{ data.expanded ? "expand_less" : "expand_more" }}</icon>
            <span class="kiss-margin-small-start">{{ data.title || "Accordion" }}</span>
        </div>
        <div class="kiss-margin-top kiss-color-muted" v-if="children && children.length">
            {{ children.length }} item(s)
        </div>
    </div>',
    'children' => true    // This component can contain other components
];
```

## 🎯 Component API

### Component Properties

| Property | Type | Description | Required |
|----------|------|-------------|----------|
| `label` | string | Display name in component picker | ✅ |
| `group` | string | Category for organization | ✅ |
| `fields` | array | Configuration fields | ✅ |
| `preview` | string | Vue template for preview | ❌ |
| `children` | boolean | Can contain other components | ✅ |
| `opts` | array | Additional options | ❌ |

### Field Definition

```php
[
    'name' => 'fieldName',           // Required: Field identifier
    'type' => 'text',                // Required: Field type
    'label' => 'Field Label',        // Optional: Display label
    'info' => 'Help text',           // Optional: Helper text
    'i18n' => true,                  // Optional: Enable localization
    'default' => 'value',            // Optional: Default value
    'opts' => [                      // Optional: Field-specific options
        'placeholder' => 'Enter text',
        'required' => true
    ]
]
```

### Per-Column Settings for Grid/Row

Grid and Row support custom settings per column. Define column fields in the component meta under `opts.colFields`. These render inside each column editor and bind to `column.data`. You can also turn any custom component into a grid or row by setting `meta.type` to `grid` or `row`.

```php
// Example: add per-column CSS class for Grid
$this->on('layout.components.collect', function($components) {
    $components['grid']['opts']['colFields'] = [
        [
            'name' => 'class',
            'type' => 'text',
            'label' => 'CSS Class'
        ],
    ];

    // Example: move Row width selection into a field (hides the built-in width control)
    $components['row']['opts']['colFields'] = [
        [
            'name' => 'width',
            'type' => 'select',
            'label' => 'Width',
            'opts' => [
                'options' => ['1-2','1-3','1-4','2-3','3-4']
            ]
        ],
        [
            'name' => 'class',
            'type' => 'text',
            'label' => 'CSS Class'
        ]
    ];
});
```

Notes:
- Row keeps its built-in width selector unless a `colFields` entry named `width` is provided — if present, the inline width control is hidden to avoid duplication.
- Field values are stored at `column.data.<fieldName>`.
- Components with `meta.type = 'grid'` or `'row'` render the respective editors; all other components behave as standard. Standard containers still use `meta.children = true`.

### Column Limits

Grid and Row can define column limits in `opts`:

```php
$components['grid']['opts']['minCols'] = 2;   // ensure at least 2 columns on init
$components['grid']['opts']['maxCols'] = 6;   // prevent adding beyond 6

// Row defaults to maxColumns = 4 (backward compatible)
$components['row']['opts']['minCols'] = 1;
$components['row']['opts']['maxCols'] = 4;   // override to a different max if desired
```

- On editor init, the UI auto-creates columns up to `minColumns` if fewer exist.
- Add column is blocked when reaching `maxColumns` with a notification.
- Removing columns is disabled when the current count equals `minColumns`.

### Supported Field Types

All Cockpit field types are supported:
- `text`, `textarea`, `select`, `boolean`
- `asset`, `color`, `date`, `datetime`
- `code`, `wysiwyg`, `markdown`
- `number`, `rating`, `tags`
- `pageUrl` (when Pages addon is available)

## 🔧 Component Processing

### Custom Component Processing

Hook into component processing to add custom logic:

```php
$this->on('layout.component.process', function($componentName, &$data, $context, $meta) {
    
    // Example: Process custom video embed component
    if ($componentName === 'video-embed' && isset($data['url'])) {
        
        // Extract video ID from YouTube URL
        if (preg_match('/youtube\.com\/watch\?v=([^&]+)/', $data['url'], $matches)) {
            $data['youtube_id'] = $matches[1];
            $data['embed_url'] = "https://www.youtube.com/embed/{$matches[1]}";
        }
        
        // Add responsive wrapper class based on aspect ratio
        $data['wrapper_class'] = $data['aspect_ratio'] === '16:9' ? 'video-16-9' : 'video-4-3';
    }
    
    // Example: Add computed properties
    if ($componentName === 'feature-box' && isset($data['style'])) {
        $data['css_classes'] = match($data['style']) {
            'bordered' => 'feature-box feature-box-bordered',
            'shadowed' => 'feature-box feature-box-shadow',
            default => 'feature-box'
        };
    }
});
```

### Context-Aware Processing

Components receive context during processing:

```php
// Context includes locale information
$context = [
    'locale' => 'en'    // Current locale for i18n fields
];

// Process components with context
$processedData = $this->helper('layoutComponents')->process($layoutData, $context);
```

## 📦 Shared Components

### Creating Shared Components

Shared components are reusable component instances that can be referenced throughout your layouts:

1. **Create a shared component** through the admin UI at `/layout-components/shared`
2. **Configure the component** with your desired settings
3. **Reference in layouts** using the shared component ID

### Using Shared Components in Code

```php
// Reference a shared component in layout data
$layoutData = [
    [
        'component' => 'section',
        'children' => [
            [
                'shared' => '507f1f77bcf86cd799439011'  // Shared component ID
            ],
            [
                'component' => 'heading',
                'text' => 'Additional content after shared component'
            ]
        ]
    ]
];

// The layout processor will automatically resolve shared components
$processed = $this->helper('layoutComponents')->process($layoutData);
```

### Managing Shared Components

```php
// Get all shared components
$sharedComponents = $this->app->dataStorage->find('layout/components_shared')->toArray();

// Remove shared component references from layout data
$cleanedData = $this->helper('layoutComponents')->removeSharedComponents(
    $layoutData, 
    $sharedComponentId
);
```

## 🔌 Integration with Other Addons

### Pages Addon Integration

When the Pages addon is available, Layout components gain additional features:

```php
// URL fields automatically become page selectors
[
    'name' => 'link',
    'type' => 'pageUrl',     // Special field type when Pages is available
    'label' => 'Link to Page'
]

// In component definition
'fields' => [
    [
        'name' => 'url', 
        'type' => $isPagesAddonAvailable ? 'pageUrl' : 'text'
    ]
]
```

### Content Models Integration

Use Layout fields in your content models:

```php
// In a collection/singleton model
'fields' => [
    [
        'name' => 'content',
        'type' => 'layout',
        'label' => 'Page Content',
        'opts' => [
            'components' => ['heading', 'text', 'image'],  // Limit available components
            'exclude' => ['html', 'code'],                 // Exclude specific components
        ]
    ]
]
```


## 🐛 Troubleshooting

### Common Issues

**❌ Component not appearing in picker**
- Ensure component is registered in `layout.components.collect` event
- Check component has required properties: `label`, `group`, `fields`, `children`
- Clear cache if using persistent caching

**❌ i18n fields not showing translations**
- Verify field has `'i18n' => true` property
- Check locale is passed in processing context
- Ensure locale exists in system configuration

**❌ Preview not updating**
- Vue template syntax must be valid
- Use `v-if` for conditional rendering
- Check browser console for Vue errors

**❌ Asset fields not working**
- Verify Assets addon is enabled
- Check user has asset permissions
- Ensure filter options are valid

### Debug Mode

Enable debug mode to bypass component caching:

```php
// In config/config.php
'debug' => true
```

This forces fresh component loading on each request.

## 📄 License

This is a commercial addon for Cockpit CMS. See [LICENSE.md](LICENSE.md) for full terms.

## 🙏 Credits

Layout is developed by [Agentejo](https://agentejo.com) as part of the Cockpit CMS Pro ecosystem.

---

**Ready to build visually?** Start creating custom components and build amazing layouts with the power of Cockpit CMS!
