h('') * ->l(Sidebar::make()->items(['Dashboard', 'Settings'])) * ->c('
Content
') * ->f('') * ->render(); */ class Layout implements Htmlable, Renderable { protected string $variant; protected array $attributes = []; protected string $path = ''; // Hierarchical path (e.g., "L-" for nested in Left) protected array $header = []; protected array $left = []; protected array $content = []; protected array $right = []; protected array $footer = []; public function __construct(string $variant = 'HCF', string $path = '') { $this->variant = strtoupper($variant); $this->path = $path; } /** * Create a new layout instance */ public static function make(string $variant = 'HCF', string $path = ''): static { return new static($variant, $path); } /** * Get the slot ID for a given slot letter */ protected function slotId(string $slot): string { return $this->path.$slot; } /** * Add to the Header slot */ public function h(mixed ...$items): static { foreach ($items as $item) { $this->header[] = $item; } return $this; } /** * Add to the Left slot */ public function l(mixed ...$items): static { foreach ($items as $item) { $this->left[] = $item; } return $this; } /** * Add to the Content slot */ public function c(mixed ...$items): static { foreach ($items as $item) { $this->content[] = $item; } return $this; } /** * Add to the Right slot */ public function r(mixed ...$items): static { foreach ($items as $item) { $this->right[] = $item; } return $this; } /** * Add to the Footer slot */ public function f(mixed ...$items): static { foreach ($items as $item) { $this->footer[] = $item; } return $this; } /** * Alias methods for readability (variadic) */ public function addHeader(mixed ...$items): static { return $this->h(...$items); } public function addLeft(mixed ...$items): static { return $this->l(...$items); } public function addContent(mixed ...$items): static { return $this->c(...$items); } public function addRight(mixed ...$items): static { return $this->r(...$items); } public function addFooter(mixed ...$items): static { return $this->f(...$items); } /** * Set HTML attributes on the layout container */ public function attributes(array $attributes): static { $this->attributes = array_merge($this->attributes, $attributes); return $this; } /** * Add a CSS class */ public function class(string $class): static { $existing = $this->attributes['class'] ?? ''; $this->attributes['class'] = trim($existing.' '.$class); return $this; } /** * Check if variant includes a slot */ protected function has(string $slot): bool { return str_contains($this->variant, strtoupper($slot)); } /** * Render all items in a slot with indexed data attributes */ protected function renderSlot(array $items, string $slot): string { $html = ''; foreach ($items as $index => $item) { $itemId = $this->slotId($slot).'-'.$index; $resolved = $this->resolveItem($item, $slot); $html .= '
'.$resolved.'
'; } return $html; } /** * Resolve a single item to string, passing path context to nested layouts */ protected function resolveItem(mixed $content, string $slot): string { if ($content === null) { return ''; } // Nested Layout - inject the path context if ($content instanceof Layout) { $content->path = $this->slotId($slot).'-'; return $content->render(); } if ($content instanceof Htmlable) { return $content->toHtml(); } if ($content instanceof Renderable) { return $content->render(); } if ($content instanceof View) { return $content->render(); } if (is_callable($content)) { return $this->resolveItem($content(), $slot); } return (string) $content; } /** * Build attributes string */ protected function buildAttributes(): string { $attrs = $this->attributes; $attrs['class'] = trim('hlcrf-layout '.($attrs['class'] ?? '')); $parts = []; foreach ($attrs as $key => $value) { if ($value === true) { $parts[] = $key; } elseif ($value !== false && $value !== null) { $parts[] = $key.'="'.e($value).'"'; } } return implode(' ', $parts); } /** * Render the layout to HTML */ public function render(): string { $layoutId = $this->path ? rtrim($this->path, '-') : 'root'; $html = '
buildAttributes().' data-layout="'.e($layoutId).'">'; // Header if ($this->has('H') && ! empty($this->header)) { $id = $this->slotId('H'); $html .= '
'.$this->renderSlot($this->header, 'H').'
'; } // Body (L, C, R) if ($this->has('L') || $this->has('C') || $this->has('R')) { $html .= '
'; if ($this->has('L') && ! empty($this->left)) { $id = $this->slotId('L'); $html .= ''; } if ($this->has('C')) { $id = $this->slotId('C'); $html .= '
'.$this->renderSlot($this->content, 'C').'
'; } if ($this->has('R') && ! empty($this->right)) { $id = $this->slotId('R'); $html .= ''; } $html .= '
'; } // Footer if ($this->has('F') && ! empty($this->footer)) { $id = $this->slotId('F'); $html .= ''; } $html .= '
'; return $html; } /** * Get the HTML string */ public function toHtml(): string { return $this->render(); } /** * Cast to string */ public function __toString(): string { return $this->render(); } }