diff --git a/TODO.md b/TODO.md
index 0e7b606..64d2aa8 100644
--- a/TODO.md
+++ b/TODO.md
@@ -41,12 +41,13 @@
- **Completed:** January 2026
- **File:** `tests/Feature/Menu/AdminMenuSystemTest.php`
-- [ ] **Test Coverage: HLCRF Components** - Test layout system
- - [ ] Test HierarchicalLayoutBuilder parsing
- - [ ] Test nested layout rendering
- - [ ] Test self-documenting IDs (H-0, C-R-2, etc.)
- - [ ] Test responsive breakpoints
- - **Estimated effort:** 4-5 hours
+- [x] **Test Coverage: HLCRF Components** - Test layout system
+ - [x] Test HierarchicalLayoutBuilder parsing
+ - [x] Test nested layout rendering
+ - [x] Test self-documenting IDs (H-0, C-R-2, etc.)
+ - [x] Test responsive breakpoints
+ - **Completed:** January 2026
+ - **File:** `tests/Feature/Layout/HlcrfLayoutTest.php`
### Low Priority
@@ -237,5 +238,6 @@
- [x] **Test Coverage: Teapot/Honeypot** - Bot detection, severity classification, rate limiting, header sanitization, model scopes (40+ tests)
- [x] **Test Coverage: Search System** - SearchProviderRegistry, search execution, result aggregation, fuzzy matching, relevance scoring, SearchResult tests (60+ tests)
- [x] **Test Coverage: Livewire Modals** - Modal opening/closing, events, data passing, validation, nested modals, lifecycle (50+ tests)
+- [x] **Test Coverage: HLCRF Components** - Layout variant parsing, nested rendering, self-documenting IDs, slot rendering, CSS structure (80+ tests)
*See `changelog/2026/jan/` for completed features.*
diff --git a/tests/Feature/Layout/HlcrfLayoutTest.php b/tests/Feature/Layout/HlcrfLayoutTest.php
new file mode 100644
index 0000000..e59d3cf
--- /dev/null
+++ b/tests/Feature/Layout/HlcrfLayoutTest.php
@@ -0,0 +1,997 @@
+c('Content');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('data-layout="root"')
+ ->and($html)->toContain('data-slot="C"')
+ ->and($html)->toContain('hlcrf-content');
+ });
+
+ it('parses header-content variant (HC)', function () {
+ $layout = Layout::make('HC')
+ ->h('')
+ ->c('Content');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('data-slot="H"')
+ ->and($html)->toContain('data-slot="C"')
+ ->and($html)->not->toContain('data-slot="L"')
+ ->and($html)->not->toContain('data-slot="R"')
+ ->and($html)->not->toContain('data-slot="F"');
+ });
+
+ it('parses header-content-footer variant (HCF)', function () {
+ $layout = Layout::make('HCF')
+ ->h('')
+ ->c('Content')
+ ->f('');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('data-slot="H"')
+ ->and($html)->toContain('data-slot="C"')
+ ->and($html)->toContain('data-slot="F"')
+ ->and($html)->not->toContain('data-slot="L"')
+ ->and($html)->not->toContain('data-slot="R"');
+ });
+
+ it('parses left-content variant (LC)', function () {
+ $layout = Layout::make('LC')
+ ->l('')
+ ->c('Content');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('data-slot="L"')
+ ->and($html)->toContain('data-slot="C"')
+ ->and($html)->not->toContain('data-slot="H"')
+ ->and($html)->not->toContain('data-slot="R"')
+ ->and($html)->not->toContain('data-slot="F"');
+ });
+
+ it('parses content-right variant (CR)', function () {
+ $layout = Layout::make('CR')
+ ->c('Content')
+ ->r('');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('data-slot="C"')
+ ->and($html)->toContain('data-slot="R"')
+ ->and($html)->not->toContain('data-slot="H"')
+ ->and($html)->not->toContain('data-slot="L"')
+ ->and($html)->not->toContain('data-slot="F"');
+ });
+
+ it('parses three-column variant (LCR)', function () {
+ $layout = Layout::make('LCR')
+ ->l('')
+ ->c('Content')
+ ->r('');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('data-slot="L"')
+ ->and($html)->toContain('data-slot="C"')
+ ->and($html)->toContain('data-slot="R"')
+ ->and($html)->not->toContain('data-slot="H"')
+ ->and($html)->not->toContain('data-slot="F"');
+ });
+
+ it('parses full variant (HLCRF)', function () {
+ $layout = Layout::make('HLCRF')
+ ->h('')
+ ->l('')
+ ->c('Content')
+ ->r('')
+ ->f('');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('data-slot="H"')
+ ->and($html)->toContain('data-slot="L"')
+ ->and($html)->toContain('data-slot="C"')
+ ->and($html)->toContain('data-slot="R"')
+ ->and($html)->toContain('data-slot="F"');
+ });
+
+ it('normalises lowercase variant to uppercase', function () {
+ $layout = Layout::make('hlcrf')
+ ->h('')
+ ->l('')
+ ->c('Content')
+ ->r('')
+ ->f('');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('data-slot="H"')
+ ->and($html)->toContain('data-slot="L"')
+ ->and($html)->toContain('data-slot="C"')
+ ->and($html)->toContain('data-slot="R"')
+ ->and($html)->toContain('data-slot="F"');
+ });
+
+ it('uses HCF as default variant', function () {
+ $layout = Layout::make()
+ ->h('')
+ ->c('Content')
+ ->f('');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('data-slot="H"')
+ ->and($html)->toContain('data-slot="C"')
+ ->and($html)->toContain('data-slot="F"');
+ });
+});
+
+// =============================================================================
+// Self-Documenting ID System Tests
+// =============================================================================
+
+describe('Self-documenting ID system', function () {
+ it('generates correct block IDs for single items', function () {
+ $layout = Layout::make('HLCRF')
+ ->h('')
+ ->l('')
+ ->c('Content')
+ ->r('')
+ ->f('');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('data-block="H-0"')
+ ->and($html)->toContain('data-block="L-0"')
+ ->and($html)->toContain('data-block="C-0"')
+ ->and($html)->toContain('data-block="R-0"')
+ ->and($html)->toContain('data-block="F-0"');
+ });
+
+ it('generates sequential block IDs for multiple items', function () {
+ $layout = Layout::make('C')
+ ->c('')
+ ->c('')
+ ->c('');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('data-block="C-0"')
+ ->and($html)->toContain('data-block="C-1"')
+ ->and($html)->toContain('data-block="C-2"');
+ });
+
+ it('generates sequential IDs for header items', function () {
+ $layout = Layout::make('HC')
+ ->h('')
+ ->h('')
+ ->h('')
+ ->c('Content');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('data-block="H-0"')
+ ->and($html)->toContain('data-block="H-1"')
+ ->and($html)->toContain('data-block="H-2"');
+ });
+
+ it('generates correct nested IDs for nested layouts', function () {
+ $nested = Layout::make('LR')
+ ->l('Nested Left')
+ ->r('Nested Right');
+
+ $outer = Layout::make('C')
+ ->c($nested);
+
+ $html = $outer->render();
+
+ // The nested layout should have IDs prefixed with parent context
+ expect($html)->toContain('data-block="C-0-L-0"')
+ ->and($html)->toContain('data-block="C-0-R-0"');
+ });
+
+ it('generates correct deeply nested IDs', function () {
+ $deepNested = Layout::make('C')
+ ->c('Deep content');
+
+ $nested = Layout::make('LR')
+ ->l($deepNested)
+ ->r('Nested Right');
+
+ $outer = Layout::make('C')
+ ->c($nested);
+
+ $html = $outer->render();
+
+ expect($html)->toContain('data-block="C-0-L-0-C-0"');
+ });
+
+ it('sets correct data-layout attribute on root', function () {
+ $layout = Layout::make('C')
+ ->c('Content');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('data-layout="root"');
+ });
+
+ it('sets correct data-layout attribute on nested layouts', function () {
+ $nested = Layout::make('C')
+ ->c('Nested content');
+
+ $outer = Layout::make('C')
+ ->c($nested);
+
+ $html = $outer->render();
+
+ // Nested layout should have a path-based layout ID
+ expect($html)->toContain('data-layout="C-0"');
+ });
+});
+
+// =============================================================================
+// Nested Layout Rendering Tests
+// =============================================================================
+
+describe('Nested layout rendering', function () {
+ it('renders nested layout within content', function () {
+ $nested = Layout::make('LCR')
+ ->l('')
+ ->c('Nested Content')
+ ->r('');
+
+ $outer = Layout::make('HCF')
+ ->h('')
+ ->c($nested)
+ ->f('');
+
+ $html = $outer->render();
+
+ expect($html)->toContain('Nested Nav')
+ ->and($html)->toContain('Nested Content')
+ ->and($html)->toContain('Nested Aside')
+ ->and($html)->toContain('Header')
+ ->and($html)->toContain('Footer');
+ });
+
+ it('renders multiple levels of nesting', function () {
+ $level3 = Layout::make('C')
+ ->c('Level 3 Content');
+
+ $level2 = Layout::make('LC')
+ ->l('Level 2 Sidebar')
+ ->c($level3);
+
+ $level1 = Layout::make('HCF')
+ ->h('Level 1 Header')
+ ->c($level2)
+ ->f('Level 1 Footer');
+
+ $html = $level1->render();
+
+ expect($html)->toContain('Level 3 Content')
+ ->and($html)->toContain('Level 2 Sidebar')
+ ->and($html)->toContain('Level 1 Header')
+ ->and($html)->toContain('Level 1 Footer');
+ });
+
+ it('renders nested layouts in different slots', function () {
+ $leftNested = Layout::make('C')
+ ->c('Left nested content');
+
+ $rightNested = Layout::make('C')
+ ->c('Right nested content');
+
+ $outer = Layout::make('LCR')
+ ->l($leftNested)
+ ->c('Main content')
+ ->r($rightNested);
+
+ $html = $outer->render();
+
+ expect($html)->toContain('Left nested content')
+ ->and($html)->toContain('Main content')
+ ->and($html)->toContain('Right nested content')
+ ->and($html)->toContain('data-block="L-0-C-0"')
+ ->and($html)->toContain('data-block="R-0-C-0"');
+ });
+
+ it('preserves correct path context through multiple nested items', function () {
+ $nested1 = Layout::make('C')->c('First nested');
+ $nested2 = Layout::make('C')->c('Second nested');
+
+ $outer = Layout::make('C')
+ ->c($nested1)
+ ->c($nested2);
+
+ $html = $outer->render();
+
+ expect($html)->toContain('data-layout="C-0"')
+ ->and($html)->toContain('data-layout="C-1"')
+ ->and($html)->toContain('data-block="C-0-C-0"')
+ ->and($html)->toContain('data-block="C-1-C-0"');
+ });
+});
+
+// =============================================================================
+// Slot Rendering Tests
+// =============================================================================
+
+describe('Slot rendering', function () {
+ it('renders string content directly', function () {
+ $layout = Layout::make('C')
+ ->c('Simple string content');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('Simple string content');
+ });
+
+ it('renders HTML content correctly', function () {
+ $layout = Layout::make('C')
+ ->c('
');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('');
+ });
+
+ it('renders Htmlable objects', function () {
+ $htmlable = new class implements Htmlable
+ {
+ public function toHtml(): string
+ {
+ return 'Htmlable Content';
+ }
+ };
+
+ $layout = Layout::make('C')
+ ->c($htmlable);
+
+ $html = $layout->render();
+
+ expect($html)->toContain('Htmlable Content');
+ });
+
+ it('renders callable/closure content', function () {
+ $layout = Layout::make('C')
+ ->c(fn () => 'Closure Content
');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('Closure Content
');
+ });
+
+ it('handles null content gracefully', function () {
+ $layout = Layout::make('C')
+ ->c(null)
+ ->c('Valid content');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('Valid content')
+ ->and($html)->toContain('data-block="C-0"')
+ ->and($html)->toContain('data-block="C-1"');
+ });
+
+ it('does not render empty slots', function () {
+ $layout = Layout::make('LCR')
+ ->c('Content only');
+
+ $html = $layout->render();
+
+ // L and R slots should not appear since they have no content
+ expect($html)->toContain('data-slot="C"')
+ ->and($html)->not->toContain('data-slot="L"')
+ ->and($html)->not->toContain('data-slot="R"');
+ });
+
+ it('supports variadic item addition', function () {
+ $layout = Layout::make('C')
+ ->c('First', 'Second', 'Third');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('First')
+ ->and($html)->toContain('Second')
+ ->and($html)->toContain('Third')
+ ->and($html)->toContain('data-block="C-0"')
+ ->and($html)->toContain('data-block="C-1"')
+ ->and($html)->toContain('data-block="C-2"');
+ });
+});
+
+// =============================================================================
+// Alias Method Tests
+// =============================================================================
+
+describe('Alias methods', function () {
+ it('addHeader works like h()', function () {
+ $layout = Layout::make('HC')
+ ->addHeader('')
+ ->c('Content');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('Header Content')
+ ->and($html)->toContain('data-block="H-0"');
+ });
+
+ it('addLeft works like l()', function () {
+ $layout = Layout::make('LC')
+ ->addLeft('')
+ ->c('Content');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('Left Content')
+ ->and($html)->toContain('data-block="L-0"');
+ });
+
+ it('addContent works like c()', function () {
+ $layout = Layout::make('C')
+ ->addContent('Main Content');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('Main Content')
+ ->and($html)->toContain('data-block="C-0"');
+ });
+
+ it('addRight works like r()', function () {
+ $layout = Layout::make('CR')
+ ->c('Content')
+ ->addRight('');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('Right Content')
+ ->and($html)->toContain('data-block="R-0"');
+ });
+
+ it('addFooter works like f()', function () {
+ $layout = Layout::make('CF')
+ ->c('Content')
+ ->addFooter('');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('Footer Content')
+ ->and($html)->toContain('data-block="F-0"');
+ });
+
+ it('alias methods support variadic arguments', function () {
+ $layout = Layout::make('C')
+ ->addContent('First', 'Second', 'Third');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('First')
+ ->and($html)->toContain('Second')
+ ->and($html)->toContain('Third');
+ });
+});
+
+// =============================================================================
+// Attributes and CSS Classes Tests
+// =============================================================================
+
+describe('Attributes and CSS classes', function () {
+ it('includes default hlcrf-layout class', function () {
+ $layout = Layout::make('C')
+ ->c('Content');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('class="hlcrf-layout"');
+ });
+
+ it('adds custom class with class() method', function () {
+ $layout = Layout::make('C')
+ ->class('custom-class')
+ ->c('Content');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('class="hlcrf-layout custom-class"');
+ });
+
+ it('accumulates multiple classes', function () {
+ $layout = Layout::make('C')
+ ->class('first-class')
+ ->class('second-class')
+ ->c('Content');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('hlcrf-layout')
+ ->and($html)->toContain('first-class')
+ ->and($html)->toContain('second-class');
+ });
+
+ it('sets custom attributes with attributes() method', function () {
+ $layout = Layout::make('C')
+ ->attributes(['id' => 'main-layout', 'data-theme' => 'dark'])
+ ->c('Content');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('id="main-layout"')
+ ->and($html)->toContain('data-theme="dark"');
+ });
+
+ it('merges attributes correctly', function () {
+ $layout = Layout::make('C')
+ ->attributes(['data-first' => 'one'])
+ ->attributes(['data-second' => 'two'])
+ ->c('Content');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('data-first="one"')
+ ->and($html)->toContain('data-second="two"');
+ });
+
+ it('handles boolean true attributes', function () {
+ $layout = Layout::make('C')
+ ->attributes(['data-visible' => true])
+ ->c('Content');
+
+ $html = $layout->render();
+
+ expect($html)->toContain('data-visible');
+ });
+
+ it('excludes boolean false attributes', function () {
+ $layout = Layout::make('C')
+ ->attributes(['data-hidden' => false])
+ ->c('Content');
+
+ $html = $layout->render();
+
+ expect($html)->not->toContain('data-hidden');
+ });
+
+ it('excludes null attributes', function () {
+ $layout = Layout::make('C')
+ ->attributes(['data-maybe' => null])
+ ->c('Content');
+
+ $html = $layout->render();
+
+ expect($html)->not->toContain('data-maybe');
+ });
+
+ it('escapes attribute values', function () {
+ $layout = Layout::make('C')
+ ->attributes(['data-value' => ''])
+ ->c('Content');
+
+ $html = $layout->render();
+
+ expect($html)->not->toContain('