diff --git a/TODO.md b/TODO.md index ed98123..b1602a1 100644 --- a/TODO.md +++ b/TODO.md @@ -1,43 +1,14 @@ # Core PHP Framework - TODO -No pending tasks! 🎉 - ---- - -## Completed (January 2026) - -### Security Fixes - -- [x] **MCP: Database Connection Fallback** - Fixed to throw exception instead of silently falling back to default connection - - See: `packages/core-mcp/changelog/2026/jan/security.md` - -- [x] **MCP: SQL Validator Regex** - Strengthened WHERE clause patterns to prevent SQL injection vectors - - See: `packages/core-mcp/changelog/2026/jan/security.md` - -### Features - -- [x] **MCP: EXPLAIN Plan** - Added query optimization analysis with human-readable performance insights - - See: `packages/core-mcp/changelog/2026/jan/features.md` - -- [x] **CDN: Integration Tests** - Comprehensive test suite for CDN operations and asset pipeline - - See: `packages/core-php/changelog/2026/jan/features.md` - -### Documentation & Code Quality - -- [x] **API docs** - Genericized vendor-specific content (removed Host UK branding, lt.hn references) - - See: `packages/core-api/changelog/2026/jan/features.md` - -- [x] **Admin: Route Audit** - Verified admin routes use Livewire modals instead of traditional controllers; #[Action] attributes not applicable - -- [x] **ServicesAdmin** - Reviewed stubbed bio service methods; intentionally stubbed pending module extraction (documented with TODO comments) +No pending tasks. --- ## Package Changelogs -For complete feature lists and implementation details: -- `packages/core-php/changelog/2026/jan/features.md` -- `packages/core-admin/changelog/2026/jan/features.md` -- `packages/core-api/changelog/2026/jan/features.md` -- `packages/core-mcp/changelog/2026/jan/features.md` -- `packages/core-mcp/changelog/2026/jan/security.md` ⚠️ Security fixes +For completed features and implementation details, see each package's changelog: + +- `packages/core-php/changelog/` +- `packages/core-admin/changelog/` +- `packages/core-api/changelog/` +- `packages/core-mcp/changelog/` diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 9aee0a3..11a8bf6 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -102,7 +102,9 @@ export default defineConfig({ { text: 'Activity Logging', link: '/packages/core/activity' }, { text: 'Media Processing', link: '/packages/core/media' }, { text: 'Search', link: '/packages/core/search' }, - { text: 'SEO Tools', link: '/packages/core/seo' } + { text: 'SEO Tools', link: '/packages/core/seo' }, + { text: 'Service Contracts', link: '/packages/core/service-contracts' }, + { text: 'Seeder System', link: '/packages/core/seeder-system' } ] } ], @@ -117,7 +119,10 @@ export default defineConfig({ { text: 'Global Search', link: '/packages/admin/search' }, { text: 'Admin Menus', link: '/packages/admin/menus' }, { text: 'Authorization', link: '/packages/admin/authorization' }, - { text: 'UI Components', link: '/packages/admin/components' } + { text: 'UI Components', link: '/packages/admin/components' }, + { text: 'Creating Admin Panels', link: '/packages/admin/creating-admin-panels' }, + { text: 'HLCRF Deep Dive', link: '/packages/admin/hlcrf-deep-dive' }, + { text: 'Components Reference', link: '/packages/admin/components-reference' } ] } ], @@ -131,7 +136,10 @@ export default defineConfig({ { text: 'Webhooks', link: '/packages/api/webhooks' }, { text: 'Rate Limiting', link: '/packages/api/rate-limiting' }, { text: 'Scopes', link: '/packages/api/scopes' }, - { text: 'Documentation', link: '/packages/api/documentation' } + { text: 'Documentation', link: '/packages/api/documentation' }, + { text: 'Building REST APIs', link: '/packages/api/building-rest-apis' }, + { text: 'Webhook Integration', link: '/packages/api/webhook-integration' }, + { text: 'Endpoints Reference', link: '/packages/api/endpoints-reference' } ] } ], @@ -146,7 +154,10 @@ export default defineConfig({ { text: 'Security', link: '/packages/mcp/security' }, { text: 'Workspace Context', link: '/packages/mcp/workspace' }, { text: 'Analytics', link: '/packages/mcp/analytics' }, - { text: 'Usage Quotas', link: '/packages/mcp/quotas' } + { text: 'Usage Quotas', link: '/packages/mcp/quotas' }, + { text: 'Creating MCP Tools', link: '/packages/mcp/creating-mcp-tools' }, + { text: 'SQL Security', link: '/packages/mcp/sql-security' }, + { text: 'Tools Reference', link: '/packages/mcp/tools-reference' } ] } ], diff --git a/docs/packages/admin/components-reference.md b/docs/packages/admin/components-reference.md new file mode 100644 index 0000000..1daaeb5 --- /dev/null +++ b/docs/packages/admin/components-reference.md @@ -0,0 +1,784 @@ +# Components Reference + +Complete API reference for all form components in the Admin package, including prop documentation, validation rules, authorization integration, and accessibility notes. + +## Overview + +All form components in Core PHP: +- Wrap Flux UI components with additional features +- Support authorization via `canGate` and `canResource` props +- Include ARIA accessibility attributes +- Work seamlessly with Livewire +- Follow consistent naming conventions + +## Input + +Text input with various types and authorization support. + +### Basic Usage + +```blade + +``` + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `id` | string | **required** | Unique identifier for the input | +| `label` | string | `null` | Label text displayed above input | +| `helper` | string | `null` | Helper text displayed below input | +| `canGate` | string | `null` | Gate/policy ability to check | +| `canResource` | mixed | `null` | Resource to check ability against | +| `instantSave` | bool | `false` | Use `wire:model.live.debounce.500ms` | +| `type` | string | `'text'` | Input type (text, email, password, number, etc.) | +| `placeholder` | string | `null` | Placeholder text | +| `disabled` | bool | `false` | Disable the input | +| `readonly` | bool | `false` | Make input read-only | +| `required` | bool | `false` | Mark as required | +| `min` | number | `null` | Minimum value (for number inputs) | +| `max` | number | `null` | Maximum value (for number inputs) | +| `maxlength` | number | `null` | Maximum character length | + +### Authorization Example + +```blade +{{-- Input disabled if user cannot update the post --}} + +``` + +### Type Variants + +```blade +{{-- Text input --}} + + +{{-- Email input --}} + + +{{-- Password input --}} + + +{{-- Number input --}} + + +{{-- Date input --}} + + +{{-- URL input --}} + +``` + +### Instant Save Mode + +```blade +{{-- Saves with 500ms debounce --}} + +``` + +### Accessibility + +The component automatically: +- Associates label with input via `id` +- Links error messages with `aria-describedby` +- Sets `aria-invalid="true"` when validation fails +- Includes helper text in accessible description + +--- + +## Textarea + +Multi-line text input with authorization support. + +### Basic Usage + +```blade + +``` + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `id` | string | **required** | Unique identifier | +| `label` | string | `null` | Label text | +| `helper` | string | `null` | Helper text | +| `canGate` | string | `null` | Gate/policy ability to check | +| `canResource` | mixed | `null` | Resource for ability check | +| `instantSave` | bool | `false` | Use live debounced binding | +| `rows` | number | `3` | Number of visible rows | +| `placeholder` | string | `null` | Placeholder text | +| `disabled` | bool | `false` | Disable the textarea | +| `maxlength` | number | `null` | Maximum character length | + +### Authorization Example + +```blade + +``` + +### With Character Limit + +```blade + +``` + +--- + +## Select + +Dropdown select with authorization support. + +### Basic Usage + +```blade + + Draft + Published + Archived + +``` + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `id` | string | **required** | Unique identifier | +| `label` | string | `null` | Label text | +| `helper` | string | `null` | Helper text | +| `canGate` | string | `null` | Gate/policy ability to check | +| `canResource` | mixed | `null` | Resource for ability check | +| `instantSave` | bool | `false` | Use live binding | +| `placeholder` | string | `null` | Placeholder option text | +| `disabled` | bool | `false` | Disable the select | +| `multiple` | bool | `false` | Allow multiple selections | + +### Authorization Example + +```blade + + @foreach($categories as $category) + + {{ $category->name }} + + @endforeach + +``` + +### With Placeholder + +```blade + + United States + United Kingdom + Canada + +``` + +### Multiple Selection + +```blade + + @foreach($tags as $tag) + + {{ $tag->name }} + + @endforeach + +``` + +--- + +## Checkbox + +Single checkbox with authorization support. + +### Basic Usage + +```blade + +``` + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `id` | string | **required** | Unique identifier | +| `label` | string | `null` | Label text (displayed inline) | +| `helper` | string | `null` | Helper text below checkbox | +| `canGate` | string | `null` | Gate/policy ability to check | +| `canResource` | mixed | `null` | Resource for ability check | +| `instantSave` | bool | `false` | Use live binding | +| `disabled` | bool | `false` | Disable the checkbox | +| `value` | string | `null` | Checkbox value (for arrays) | + +### Authorization Example + +```blade + +``` + +### With Helper Text + +```blade + +``` + +### Checkbox Group + +```blade +
+ Notifications + + + + + + +
+``` + +--- + +## Toggle + +Switch-style toggle with authorization support. + +### Basic Usage + +```blade + +``` + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `id` | string | **required** | Unique identifier | +| `label` | string | `null` | Label text (displayed to the left) | +| `helper` | string | `null` | Helper text below toggle | +| `canGate` | string | `null` | Gate/policy ability to check | +| `canResource` | mixed | `null` | Resource for ability check | +| `instantSave` | bool | `false` | Use live binding | +| `disabled` | bool | `false` | Disable the toggle | + +### Authorization Example + +```blade + +``` + +### Instant Save + +```blade +{{-- Toggle that saves immediately --}} + +``` + +### With Helper + +```blade + +``` + +--- + +## Button + +Action button with variants and authorization support. + +### Basic Usage + +```blade + + Save Changes + +``` + +### Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `variant` | string | `'primary'` | Button style variant | +| `type` | string | `'submit'` | Button type (submit, button, reset) | +| `canGate` | string | `null` | Gate/policy ability to check | +| `canResource` | mixed | `null` | Resource for ability check | +| `disabled` | bool | `false` | Disable the button | +| `loading` | bool | `false` | Show loading state | + +### Variants + +```blade +{{-- Primary (default) --}} +Primary + +{{-- Secondary --}} +Secondary + +{{-- Danger --}} +Delete + +{{-- Ghost --}} +Cancel +``` + +### Authorization Example + +```blade +{{-- Button disabled if user cannot delete --}} + + Delete Post + +``` + +### With Loading State + +```blade + + Save + Saving... + +``` + +### As Link + +```blade + + Cancel + +``` + +--- + +## Authorization Props Reference + +All form components support authorization through consistent props. + +### How Authorization Works + +When `canGate` and `canResource` are provided, the component checks if the authenticated user can perform the specified ability on the resource: + +```php +// Equivalent PHP check +auth()->user()?->can($canGate, $canResource) +``` + +If the check fails, the component is **disabled** (not hidden). + +### Props + +| Prop | Type | Description | +|------|------|-------------| +| `canGate` | string | The ability/gate name to check (e.g., `'update'`, `'delete'`, `'publish'`) | +| `canResource` | mixed | The resource to check the ability against (usually a model instance) | + +### Examples + +**Basic Policy Check:** +```blade + +``` + +**Multiple Components with Same Auth:** +```blade +@php $canEdit = auth()->user()?->can('update', $post); @endphp + + + +Save +``` + +**Combining with Blade Directives:** +```blade +@can('update', $post) + + Save +@else +

You do not have permission to edit this post.

+@endcan +``` + +### Defining Policies + +```php +id === $post->author_id + || $user->hasRole('editor'); + } + + public function delete(User $user, Post $post): bool + { + return $user->hasRole('admin'); + } + + public function publish(User $user, Post $post): bool + { + return $user->hasPermission('posts.publish') + && $post->status === 'draft'; + } +} +``` + +--- + +## Accessibility Notes + +### ARIA Attributes + +All components automatically include appropriate ARIA attributes: + +| Attribute | Usage | +|-----------|-------| +| `aria-labelledby` | Links to label element | +| `aria-describedby` | Links to helper text and error messages | +| `aria-invalid` | Set to `true` when validation fails | +| `aria-required` | Set when field is required | +| `aria-disabled` | Set when field is disabled | + +### Label Association + +Labels are automatically associated with inputs via the `id` prop: + +```blade + + +{{-- Renders as: --}} + + Email Address + + +``` + +### Error Announcements + +Validation errors are linked to inputs and announced to screen readers: + +```blade +{{-- Component renders error with aria-describedby link --}} + + +{{-- Screen readers announce: "Email is required" --}} +``` + +### Focus Management + +- Tab order follows visual order +- Focus states are clearly visible +- Error focus moves to first invalid field + +### Keyboard Support + +| Component | Keyboard Support | +|-----------|------------------| +| Input | Standard text input | +| Textarea | Standard multiline | +| Select | Arrow keys, Enter, Escape | +| Checkbox | Space to toggle | +| Toggle | Space to toggle, Arrow keys | +| Button | Enter/Space to activate | + +--- + +## Validation Integration + +### Server-Side Validation + +Components automatically display Laravel validation errors: + +```php +// In Livewire component +protected array $rules = [ + 'title' => 'required|max:255', + 'content' => 'required', + 'status' => 'required|in:draft,published', +]; + +public function save(): void +{ + $this->validate(); + // Errors automatically shown on components +} +``` + +### Real-Time Validation + +```php +public function updated($propertyName): void +{ + $this->validateOnly($propertyName); +} +``` + +```blade +{{-- Shows validation error as user types --}} + +``` + +### Custom Error Messages + +```php +protected array $messages = [ + 'title.required' => 'Please enter a post title.', + 'content.required' => 'Post content cannot be empty.', +]; +``` + +--- + +## Complete Form Example + +```blade +
+ {{-- Title --}} + + + {{-- Slug with instant save --}} + + + {{-- Content --}} + + + {{-- Category --}} + + @foreach($categories as $category) + + {{ $category->name }} + + @endforeach + + + {{-- Status --}} + + Draft + Published + Archived + + + {{-- Featured toggle --}} + + + {{-- Newsletter checkbox --}} + + + {{-- Actions --}} +
+ + Save Changes + + + + Cancel + + + @can('delete', $post) + + Delete + + @endcan +
+ +``` + +## Learn More + +- [Form Components Guide](/packages/admin/forms) +- [Authorization](/packages/admin/authorization) +- [Creating Admin Panels](/packages/admin/creating-admin-panels) +- [Livewire Modals](/packages/admin/modals) diff --git a/docs/packages/admin/creating-admin-panels.md b/docs/packages/admin/creating-admin-panels.md new file mode 100644 index 0000000..9fcf1d4 --- /dev/null +++ b/docs/packages/admin/creating-admin-panels.md @@ -0,0 +1,931 @@ +# Creating Admin Panels + +This guide covers the complete process of creating admin panels in the Core PHP Framework, including menu registration, modal creation, and authorization integration. + +## Overview + +Admin panels in Core PHP use: +- **AdminMenuProvider** - Interface for menu registration +- **Livewire Modals** - Full-page components for admin interfaces +- **Authorization Props** - Built-in permission checking on components +- **HLCRF Layouts** - Composable layout system + +## Menu Registration with AdminMenuProvider + +### Implementing AdminMenuProvider + +The `AdminMenuProvider` interface allows modules to contribute navigation items to the admin sidebar. + +```php + 'onAdminPanel', + ]; + + public function onAdminPanel(AdminPanelBooting $event): void + { + // Register views and routes + $event->views('blog', __DIR__.'/View/Blade'); + $event->routes(fn () => require __DIR__.'/Routes/admin.php'); + + // Register menu provider + app(AdminMenuRegistry::class)->register($this); + } + + public function adminMenuItems(): array + { + return [ + // Dashboard item in standalone group + [ + 'group' => 'dashboard', + 'priority' => self::PRIORITY_HIGH, + 'item' => fn () => [ + 'label' => 'Blog Dashboard', + 'icon' => 'newspaper', + 'href' => route('admin.blog.dashboard'), + 'active' => request()->routeIs('admin.blog.dashboard'), + ], + ], + + // Service item with entitlement + [ + 'group' => 'services', + 'priority' => self::PRIORITY_NORMAL, + 'entitlement' => 'core.srv.blog', + 'item' => fn () => [ + 'label' => 'Blog', + 'icon' => 'newspaper', + 'href' => route('admin.blog.posts'), + 'active' => request()->routeIs('admin.blog.*'), + 'color' => 'blue', + 'badge' => Post::draft()->count() ?: null, + 'children' => [ + ['label' => 'All Posts', 'href' => route('admin.blog.posts'), 'icon' => 'document-text'], + ['label' => 'Categories', 'href' => route('admin.blog.categories'), 'icon' => 'folder'], + ['label' => 'Tags', 'href' => route('admin.blog.tags'), 'icon' => 'tag'], + ], + ], + ], + + // Admin-only item + [ + 'group' => 'admin', + 'priority' => self::PRIORITY_LOW, + 'admin' => true, + 'item' => fn () => [ + 'label' => 'Blog Settings', + 'icon' => 'gear', + 'href' => route('admin.blog.settings'), + 'active' => request()->routeIs('admin.blog.settings'), + ], + ], + ]; + } +} +``` + +### Menu Item Structure + +Each item in `adminMenuItems()` follows this structure: + +| Property | Type | Description | +|----------|------|-------------| +| `group` | string | Menu group: `dashboard`, `workspaces`, `services`, `settings`, `admin` | +| `priority` | int | Order within group (use `PRIORITY_*` constants) | +| `entitlement` | string | Optional workspace feature code for access | +| `permissions` | array | Optional user permission keys required | +| `admin` | bool | Requires Hades/admin user | +| `item` | Closure | Lazy-evaluated item data | + +### Priority Constants + +```php +use Core\Front\Admin\Contracts\AdminMenuProvider; + +// Available priority constants +AdminMenuProvider::PRIORITY_FIRST // 0-9: System items +AdminMenuProvider::PRIORITY_HIGH // 10-19: Primary navigation +AdminMenuProvider::PRIORITY_ABOVE_NORMAL // 20-39: Important items +AdminMenuProvider::PRIORITY_NORMAL // 40-60: Standard items (default) +AdminMenuProvider::PRIORITY_BELOW_NORMAL // 61-79: Less important +AdminMenuProvider::PRIORITY_LOW // 80-89: Rarely used +AdminMenuProvider::PRIORITY_LAST // 90-99: End items +``` + +### Menu Groups + +| Group | Description | Rendering | +|-------|-------------|-----------| +| `dashboard` | Primary entry points | Standalone items | +| `workspaces` | Workspace management | Grouped dropdown | +| `services` | Application services | Standalone items | +| `settings` | User/account settings | Grouped dropdown | +| `admin` | Platform administration | Grouped dropdown (Hades only) | + +### Using MenuItemBuilder + +For complex menus, use the fluent `MenuItemBuilder`: + +```php +use Core\Front\Admin\Support\MenuItemBuilder; + +public function adminMenuItems(): array +{ + return [ + MenuItemBuilder::make('Commerce') + ->icon('shopping-cart') + ->route('admin.commerce.dashboard') + ->inServices() + ->priority(self::PRIORITY_NORMAL) + ->entitlement('core.srv.commerce') + ->color('green') + ->badge('New', 'green') + ->activeOnRoute('admin.commerce.*') + ->children([ + MenuItemBuilder::child('Products', route('admin.commerce.products')) + ->icon('cube'), + MenuItemBuilder::child('Orders', route('admin.commerce.orders')) + ->icon('receipt') + ->badge(fn () => Order::pending()->count()), + ['separator' => true], + MenuItemBuilder::child('Settings', route('admin.commerce.settings')) + ->icon('gear'), + ]) + ->build(), + + MenuItemBuilder::make('Analytics') + ->icon('chart-line') + ->route('admin.analytics.dashboard') + ->inServices() + ->entitlement('core.srv.analytics') + ->adminOnly() // Requires admin user + ->build(), + ]; +} +``` + +### Permission Checking + +The `HasMenuPermissions` trait provides default permission handling: + +```php +use Core\Front\Admin\Concerns\HasMenuPermissions; + +class BlogMenuProvider implements AdminMenuProvider +{ + use HasMenuPermissions; + + // Override for custom global permissions + public function menuPermissions(): array + { + return ['blog.view']; + } + + // Override for custom permission logic + public function canViewMenu(?object $user, ?object $workspace): bool + { + if ($user === null) { + return false; + } + + // Custom logic + return $user->hasRole('editor') || $user->isHades(); + } +} +``` + +## Creating Livewire Modals + +Livewire modals are full-page components that provide seamless admin interfaces. + +### Basic Modal Structure + +```php + 'required|string|max:255', + 'content' => 'required|string', + 'status' => 'required|in:draft,published,archived', + ]; + + public function mount(?Post $post = null): void + { + $this->post = $post; + + if ($post) { + $this->title = $post->title; + $this->content = $post->content; + $this->status = $post->status; + } + } + + public function save(): void + { + $validated = $this->validate(); + + if ($this->post) { + $this->post->update($validated); + $message = 'Post updated successfully.'; + } else { + Post::create($validated); + $message = 'Post created successfully.'; + } + + session()->flash('success', $message); + $this->redirect(route('admin.blog.posts')); + } + + public function render(): View + { + return view('blog::admin.post-editor'); + } +} +``` + +### Modal View with HLCRF + +```blade +{{-- resources/views/admin/post-editor.blade.php --}} + + +
+

+ {{ $post ? 'Edit Post' : 'Create Post' }} +

+ + + + +
+
+ + +
+ + + + + + Draft + Published + Archived + + +
+ + {{ $post ? 'Update' : 'Create' }} Post + + + + Cancel + +
+ +
+ + +
+

Publishing Tips

+
    +
  • Use descriptive titles
  • +
  • Save as draft first
  • +
  • Preview before publishing
  • +
+
+
+
+``` + +### Modal with Authorization + +```php +authorize('update', $post); + + $this->post = $post; + // ... load data + } + + public function save(): void + { + // Re-authorize on save + $this->authorize('update', $this->post); + + $this->post->update([...]); + } + + public function publish(): void + { + // Different authorization for publish + $this->authorize('publish', $this->post); + + $this->post->update(['status' => 'published']); + } + + public function delete(): void + { + $this->authorize('delete', $this->post); + + $this->post->delete(); + $this->redirect(route('admin.blog.posts')); + } +} +``` + +### Modal with File Uploads + +```php + 'required|image|max:5120', // 5MB max + 'altText' => 'required|string|max:255', + ]; + + public function upload(): void + { + $this->validate(); + + $path = $this->image->store('media', 'public'); + + Media::create([ + 'path' => $path, + 'alt_text' => $this->altText, + 'mime_type' => $this->image->getMimeType(), + ]); + + $this->dispatch('media-uploaded'); + $this->reset(['image', 'altText']); + } +} +``` + +## Authorization Integration + +### Form Component Authorization Props + +All form components support authorization via `canGate` and `canResource` props: + +```blade +{{-- Button disabled if user cannot update post --}} + + Save Changes + + +{{-- Input disabled if user cannot update --}} + + +{{-- Textarea with authorization --}} + + +{{-- Select with authorization --}} + + Draft + Published + + +{{-- Toggle with authorization --}} + +``` + +### Blade Conditional Rendering + +```blade +{{-- Show only if user can create --}} +@can('create', App\Models\Post::class) + New Post +@endcan + +{{-- Show if user can edit OR delete --}} +@canany(['update', 'delete'], $post) +
+ @can('update', $post) + Edit + @endcan + + @can('delete', $post) + + @endcan +
+@endcanany + +{{-- Show message if cannot edit --}} +@cannot('update', $post) +

You cannot edit this post.

+@endcannot +``` + +### Creating Policies + +```php +isHades()) { + return true; + } + + // Enforce workspace isolation + if ($model instanceof Post && $user->workspace_id !== $model->workspace_id) { + return false; + } + + return null; // Continue to specific method + } + + public function viewAny(User $user): bool + { + return $user->hasPermission('posts.view'); + } + + public function view(User $user, Post $post): bool + { + return $user->hasPermission('posts.view'); + } + + public function create(User $user): bool + { + return $user->hasPermission('posts.create'); + } + + public function update(User $user, Post $post): bool + { + return $user->hasPermission('posts.edit') + || $user->id === $post->author_id; + } + + public function delete(User $user, Post $post): bool + { + return $user->hasRole('admin') + || ($user->hasPermission('posts.delete') && $user->id === $post->author_id); + } + + public function publish(User $user, Post $post): bool + { + return $user->hasPermission('posts.publish') + && $post->status !== 'archived'; + } +} +``` + +## Complete Module Example + +Here is a complete example of an admin module with menus, modals, and authorization. + +### Directory Structure + +``` +Mod/Blog/ +├── Boot.php +├── Models/ +│ └── Post.php +├── Policies/ +│ └── PostPolicy.php +├── View/ +│ ├── Blade/ +│ │ └── admin/ +│ │ ├── posts-list.blade.php +│ │ └── post-editor.blade.php +│ └── Modal/ +│ └── Admin/ +│ ├── PostsList.php +│ └── PostEditor.php +└── Routes/ + └── admin.php +``` + +### Boot.php + +```php + 'onAdminPanel', + ]; + + public function boot(): void + { + // Register policy + Gate::policy(Post::class, PostPolicy::class); + } + + public function onAdminPanel(AdminPanelBooting $event): void + { + // Views + $event->views('blog', __DIR__.'/View/Blade'); + + // Routes + $event->routes(fn () => require __DIR__.'/Routes/admin.php'); + + // Menu + app(AdminMenuRegistry::class)->register($this); + + // Livewire components + $event->livewire('blog.admin.posts-list', View\Modal\Admin\PostsList::class); + $event->livewire('blog.admin.post-editor', View\Modal\Admin\PostEditor::class); + } + + public function adminMenuItems(): array + { + return [ + [ + 'group' => 'services', + 'priority' => self::PRIORITY_NORMAL, + 'entitlement' => 'core.srv.blog', + 'permissions' => ['posts.view'], + 'item' => fn () => [ + 'label' => 'Blog', + 'icon' => 'newspaper', + 'href' => route('admin.blog.posts'), + 'active' => request()->routeIs('admin.blog.*'), + 'color' => 'blue', + 'badge' => $this->getDraftCount(), + 'children' => [ + [ + 'label' => 'All Posts', + 'href' => route('admin.blog.posts'), + 'icon' => 'document-text', + 'active' => request()->routeIs('admin.blog.posts'), + ], + [ + 'label' => 'Create Post', + 'href' => route('admin.blog.posts.create'), + 'icon' => 'plus', + 'active' => request()->routeIs('admin.blog.posts.create'), + ], + ], + ], + ], + ]; + } + + protected function getDraftCount(): ?int + { + $count = Post::draft()->count(); + return $count > 0 ? $count : null; + } +} +``` + +### Routes/admin.php + +```php +prefix('admin/blog') + ->name('admin.blog.') + ->group(function () { + Route::get('/posts', PostsList::class)->name('posts'); + Route::get('/posts/create', PostEditor::class)->name('posts.create'); + Route::get('/posts/{post}/edit', PostEditor::class)->name('posts.edit'); + }); +``` + +### View/Modal/Admin/PostsList.php + +```php +resetPage(); + } + + #[Computed] + public function posts() + { + return Post::query() + ->when($this->search, fn ($q) => $q->where('title', 'like', "%{$this->search}%")) + ->when($this->status, fn ($q) => $q->where('status', $this->status)) + ->orderByDesc('created_at') + ->paginate(20); + } + + public function delete(int $postId): void + { + $post = Post::findOrFail($postId); + + $this->authorize('delete', $post); + + $post->delete(); + + session()->flash('success', 'Post deleted.'); + } + + public function render(): View + { + return view('blog::admin.posts-list'); + } +} +``` + +### View/Blade/admin/posts-list.blade.php + +```blade + + +
+

Blog Posts

+ + @can('create', \Mod\Blog\Models\Post::class) + + + New Post + + @endcan +
+
+ + + {{-- Filters --}} +
+ + + + All Statuses + Draft + Published + +
+ + {{-- Posts table --}} +
+ + + + + + + + + + + @forelse($this->posts as $post) + + + + + + + @empty + + + + @endforelse + +
TitleStatusDateActions
{{ $post->title }} + + {{ ucfirst($post->status) }} + + {{ $post->created_at->format('M d, Y') }} + @can('update', $post) + + Edit + + @endcan + + @can('delete', $post) + + @endcan +
+ No posts found. +
+
+ + {{-- Pagination --}} +
+ {{ $this->posts->links() }} +
+
+
+``` + +## Best Practices + +### 1. Always Use Entitlements for Services + +```php +// Menu item requires workspace entitlement +[ + 'group' => 'services', + 'entitlement' => 'core.srv.blog', // Required + 'item' => fn () => [...], +] +``` + +### 2. Authorize Early in Modals + +```php +public function mount(Post $post): void +{ + $this->authorize('update', $post); // Fail fast + $this->post = $post; +} +``` + +### 3. Use Form Component Authorization Props + +```blade +{{-- Declarative authorization --}} + + Save + + +{{-- Not manual checks --}} +@if(auth()->user()->can('update', $post)) + +@endif +``` + +### 4. Keep Menu Items Lazy + +```php +// Item closure is only evaluated when rendered +'item' => fn () => [ + 'label' => 'Posts', + 'badge' => Post::draft()->count(), // Computed at render time +], +``` + +### 5. Use HLCRF for Consistent Layouts + +```blade +{{-- Always use HLCRF for admin views --}} + + ... + ... + +``` + +## Learn More + +- [Admin Menus](/packages/admin/menus) +- [Livewire Modals](/packages/admin/modals) +- [Form Components](/packages/admin/forms) +- [Authorization](/packages/admin/authorization) +- [HLCRF Layouts](/packages/admin/hlcrf-deep-dive) diff --git a/docs/packages/admin/hlcrf-deep-dive.md b/docs/packages/admin/hlcrf-deep-dive.md new file mode 100644 index 0000000..f153cbe --- /dev/null +++ b/docs/packages/admin/hlcrf-deep-dive.md @@ -0,0 +1,843 @@ +# HLCRF Deep Dive + +This guide provides an in-depth look at the HLCRF (Header-Left-Content-Right-Footer) layout system, covering all layout combinations, the ID system, responsive patterns, and complex real-world examples. + +## Layout Combinations + +HLCRF supports any combination of its five regions. The variant name describes which regions are present. + +### All Possible Combinations + +| Variant | Regions | Use Case | +|---------|---------|----------| +| `C` | Content only | Simple content pages | +| `HC` | Header + Content | Landing pages | +| `CF` | Content + Footer | Article pages | +| `HCF` | Header + Content + Footer | Standard pages | +| `LC` | Left + Content | App with navigation | +| `CR` | Content + Right | Content with sidebar | +| `LCR` | Left + Content + Right | Three-column layout | +| `HLC` | Header + Left + Content | Admin dashboard | +| `HCR` | Header + Content + Right | Blog with widgets | +| `LCF` | Left + Content + Footer | App with footer | +| `CRF` | Content + Right + Footer | Blog layout | +| `HLCF` | Header + Left + Content + Footer | Standard admin | +| `HCRF` | Header + Content + Right + Footer | Blog layout | +| `HLCR` | Header + Left + Content + Right | Full admin | +| `LCRF` | Left + Content + Right + Footer | Complex app | +| `HLCRF` | All five regions | Complete layout | + +### Content-Only (C) + +Minimal layout for simple content: + +```php +use Core\Front\Components\Layout; + +$layout = Layout::make('C') + ->c('
Simple content without chrome
'); + +echo $layout->render(); +``` + +**Output:** +```html +
+
+
+
+
Simple content without chrome
+
+
+
+
+``` + +### Header + Content + Footer (HCF) + +Standard page layout: + +```php +$layout = Layout::make('HCF') + ->h('') + ->c('
Page Content
') + ->f('
Copyright 2026
'); +``` + +### Left + Content (LC) + +Application with navigation sidebar: + +```php +$layout = Layout::make('LC') + ->l('') + ->c('
App Content
'); +``` + +### Three-Column (LCR) + +Full three-column layout: + +```php +$layout = Layout::make('LCR') + ->l('') + ->c('
Content
') + ->r(''); +``` + +### Full Admin (HLCRF) + +Complete admin panel: + +```php +$layout = Layout::make('HLCRF') + ->h('
Admin Header
') + ->l('') + ->c('
Dashboard
') + ->r('') + ->f('
Status Bar
'); +``` + +## The ID System + +Every HLCRF element receives a unique, hierarchical ID that describes its position in the layout tree. + +### ID Format + +``` +{Region}-{Index}[-{NestedRegion}-{NestedIndex}]... +``` + +**Components:** +- **Region Letter** - `H`, `L`, `C`, `R`, or `F` +- **Index** - Zero-based position within that slot (0, 1, 2, ...) +- **Nesting** - Dash-separated chain for nested layouts + +### Region Letters + +| Letter | Region | Semantic Role | +|--------|--------|---------------| +| `H` | Header | Top navigation, branding | +| `L` | Left | Primary sidebar, navigation | +| `C` | Content | Main content area | +| `R` | Right | Secondary sidebar, widgets | +| `F` | Footer | Bottom links, copyright | + +### ID Examples + +**Simple layout:** +```html +
+
+
First header element
+
Second header element
+
+
+
First content element
+
+
+``` + +**Nested layout:** +```html +
+
+
+ +
+ +
+
Nested content
+
+
+
+
+
+``` + +### ID Interpretation + +| ID | Meaning | +|----|---------| +| `H-0` | First element in Header | +| `L-2` | Third element in Left sidebar | +| `C-0` | First element in Content | +| `C-L-0` | Content > Left > First element | +| `C-R-2` | Content > Right > Third element | +| `C-L-0-R-1` | Content > Left > First > Right > Second | +| `H-0-C-0-L-0` | Header > Content > Left (deeply nested) | + +### Using IDs for CSS + +The ID system enables precise CSS targeting: + +```css +/* Target first header element */ +[data-block="H-0"] { + background: #1a1a2e; +} + +/* Target all elements in left sidebar */ +[data-slot="L"] > [data-block] { + padding: 1rem; +} + +/* Target nested content areas */ +[data-block*="-C-"] { + margin: 2rem; +} + +/* Target second element in any right sidebar */ +[data-block$="-R-1"] { + border-top: 1px solid #e5e7eb; +} + +/* Target deeply nested layouts */ +[data-layout*="-"][data-layout*="-"] { + background: #f9fafb; +} +``` + +### Using IDs for Testing + +```php +// PHPUnit/Pest +$this->assertSee('[data-block="H-0"]'); +$this->assertSeeInOrder(['[data-slot="L"]', '[data-slot="C"]']); + +// Playwright/Cypress +await page.locator('[data-block="C-0"]').click(); +await expect(page.locator('[data-slot="R"]')).toBeVisible(); +``` + +### Using IDs for JavaScript + +```javascript +// Target specific elements +const header = document.querySelector('[data-block="H-0"]'); +const sidebar = document.querySelector('[data-slot="L"]'); + +// Dynamic targeting +function getContentBlock(index) { + return document.querySelector(`[data-block="C-${index}"]`); +} + +// Nested targeting +const nestedLeft = document.querySelector('[data-block="C-L-0"]'); +``` + +## Responsive Design Patterns + +### Mobile-First Stacking + +On mobile, stack regions vertically: + +```blade + + Navigation + Content + Widgets + +``` + +**Behavior:** +- **Mobile (< 768px):** Left -> Content -> Right (vertical) +- **Tablet (768px-1024px):** Left | Content (two columns) +- **Desktop (> 1024px):** Left | Content | Right (three columns) + +### Collapsible Sidebars + +```blade + + + +``` + +### Hidden Regions on Mobile + +```blade + +``` + +### Flexible Width Distribution + +```blade + + + Fixed-width sidebar + + + + Flexible content + + + + Percentage-width sidebar + + +``` + +### Responsive Grid Inside Content + +```blade + +
+ + + +
+
+``` + +## Complex Real-World Examples + +### Admin Dashboard + +A complete admin dashboard with nested layouts: + +```php +use Core\Front\Components\Layout; + +// Main admin layout +$admin = Layout::make('HLCF') + ->h( + '' + ) + ->l( + '' + ) + ->c( + // Nested layout inside content + Layout::make('HCR') + ->h('
+

Dashboard

+ +
') + ->c('
+
+
Stat 1
+
Stat 2
+
Stat 3
+
+
+

Recent Activity

+ ...
+
+
') + ->r('') + ) + ->f( + '
+ Version 1.0.0 | Last sync: 5 minutes ago +
' + ); + +echo $admin->render(); +``` + +**Generated IDs:** +- `H-0` - Admin header/navigation +- `L-0` - Sidebar navigation +- `C-0` - Nested layout container +- `C-0-H-0` - Content header (page title/actions) +- `C-0-C-0` - Content main area (stats/table) +- `C-0-R-0` - Content right sidebar (quick actions) +- `F-0` - Admin footer + +### E-Commerce Product Page + +Product page with nested sections: + +```php +$productPage = Layout::make('HCF') + ->h('
+ +
Search | Cart | Account
+
') + ->c( + Layout::make('LCR') + ->l('
+
+ Product +
+
+ + +
+
') + ->c( + // Empty - using left/right only + ) + ->r('
+

Product Name

+

$99.99

+

Product description...

+ +
+ + +
+ +
+

Shipping Info

+

Free delivery over $50

+
+
'), + // Reviews section + Layout::make('CR') + ->c('
+

Customer Reviews

+
+
Review 1...
+
Review 2...
+
+
') + ->r('') + ) + ->f('
+
+
About Us
+
Customer Service
+
Policies
+
Newsletter
+
+
'); +``` + +### Multi-Panel Settings Page + +Settings page with multiple nested panels: + +```php +$settings = Layout::make('HLC') + ->h('
+

Account Settings

+
') + ->l('') + ->c( + // Profile section + Layout::make('HCF') + ->h('
+

Profile Information

+

Update your account details

+
') + ->c('
+
+ + +
+
+ + +
+
+ + +
+
') + ->f('
+ + +
') + ); +``` + +### Documentation Site + +Documentation layout with table of contents: + +```php +$docs = Layout::make('HLCRF') + ->h('
+
+
+ + +
+
+ + GitHub +
+
+
') + ->l('') + ->c('
+

Introduction

+

Welcome to the documentation...

+ +

Key Features

+
    +
  • Feature 1
  • +
  • Feature 2
  • +
  • Feature 3
  • +
+ +

Next Steps

+

Continue to the installation guide...

+
') + ->r('') + ->f(''); +``` + +### Email Client Interface + +Complex email client with multiple nested panels: + +```php +$email = Layout::make('HLCR') + ->h('
+
+ + +
+
+ +
+
+ +
JD
+
+
') + ->l('') + ->c( + Layout::make('LC') + ->l('
+
+ +
+
+
+
+ John Smith + 10:30 AM +
+
Meeting Tomorrow
+
Hi, just wanted to confirm...
+
+
+
+ Jane Doe + Yesterday +
+
Project Update
+
Here is the latest update...
+
+
+
') + ->c('
+
+ + + + + + +
+
+
+

Meeting Tomorrow

+
+
JS
+
+
John Smith <john@example.com>
+
to me
+
+
Jan 15, 2026, 10:30 AM
+
+
+
+

Hi,

+

Just wanted to confirm our meeting tomorrow at 2pm.

+

Best regards,
John

+
+
+
') + ) + ->r(''); +``` + +## Performance Considerations + +### Lazy Content Loading + +For large layouts, defer non-critical content: + +```php +$layout = Layout::make('LCR') + ->l('') + ->c('
+
Loading...
+
@livewire("content-panel")
+
') + ->r(fn () => view('widgets.sidebar')); // Closure defers evaluation +``` + +### Conditional Region Rendering + +Only render regions when needed: + +```php +$layout = Layout::make('LCR'); + +$layout->l(''); +$layout->c('
Content
'); + +// Conditionally add right sidebar +if ($user->hasFeature('widgets')) { + $layout->r(''); +} +``` + +### Efficient CSS Targeting + +Use data attributes instead of deep selectors: + +```css +/* Efficient - uses data attribute */ +[data-block="C-0"] { padding: 1rem; } + +/* Less efficient - deep selector */ +.hlcrf-layout > .hlcrf-body > .hlcrf-content > div:first-child { padding: 1rem; } +``` + +## Testing HLCRF Layouts + +### Unit Testing + +```php +use Core\Front\Components\Layout; +use PHPUnit\Framework\TestCase; + +class LayoutTest extends TestCase +{ + public function test_generates_correct_ids(): void + { + $layout = Layout::make('LC') + ->l('Left') + ->c('Content'); + + $html = $layout->render(); + + $this->assertStringContainsString('data-slot="L"', $html); + $this->assertStringContainsString('data-slot="C"', $html); + $this->assertStringContainsString('data-block="L-0"', $html); + $this->assertStringContainsString('data-block="C-0"', $html); + } + + public function test_nested_layout_ids(): void + { + $nested = Layout::make('LR') + ->l('Nested Left') + ->r('Nested Right'); + + $outer = Layout::make('C') + ->c($nested); + + $html = $outer->render(); + + $this->assertStringContainsString('data-block="C-0-L-0"', $html); + $this->assertStringContainsString('data-block="C-0-R-0"', $html); + } +} +``` + +### Browser Testing + +```php +// Pest with Playwright +it('renders admin layout correctly', function () { + $this->browse(function ($browser) { + $browser->visit('/admin') + ->assertPresent('[data-layout="root"]') + ->assertPresent('[data-slot="H"]') + ->assertPresent('[data-slot="L"]') + ->assertPresent('[data-slot="C"]'); + }); +}); +``` + +## Best Practices + +### 1. Use Semantic Region Names + +```php +// Good - semantic use +->h('') +->l('') +->c('
Page content
') +->r('') +->f('
Site footer
') + +// Bad - misuse of regions +->h('') // Header for sidebar? +``` + +### 2. Leverage the ID System + +```css +/* Target specific elements precisely */ +[data-block="H-0"] { /* Header first element */ } +[data-block="C-L-0"] { /* Content > Left > First */ } + +/* Don't fight the system with complex selectors */ +``` + +### 3. Keep Nesting Shallow + +```php +// Good - 2-3 levels max +Layout::make('HCF') + ->c(Layout::make('LCR')->...); + +// Avoid - too deep +Layout::make('C') + ->c(Layout::make('C') + ->c(Layout::make('C') + ->c(Layout::make('C')...)))); +``` + +### 4. Use Consistent Widths + +```php +// Good - consistent sidebar widths across app +->l('