feat(ui): create the first branded Blade component set#32
Conversation
There was a problem hiding this comment.
Pull request overview
Introduces an initial set of branded, reusable Blade UI primitives and refactors the shell/home view to consume them, with initial Pest render coverage for key components.
Changes:
- Added anonymous Blade UI components (button, input/select/textarea, badge/avatar, panel/page-header, dropdown/modal).
- Refactored the app shell navigation items into dedicated
x-app.*components and updated the home screen to usex-ui.*primitives. - Added a feature test to render and assert basic output for several shared UI components.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Feature/Ui/UiComponentsTest.php | Adds Blade render assertions for shared UI components. |
| resources/views/home.blade.php | Refactors home screen markup to use new x-ui.page-header, x-ui.panel, and x-ui.badge. |
| resources/views/components/ui/button.blade.php | Adds a reusable button primitive with variants and default type. |
| resources/views/components/ui/input.blade.php | Adds a labeled input primitive with optional error rendering. |
| resources/views/components/ui/textarea.blade.php | Adds a labeled textarea primitive with optional error rendering. |
| resources/views/components/ui/select.blade.php | Adds a labeled select primitive with optional error rendering and slot options. |
| resources/views/components/ui/badge.blade.php | Adds a badge primitive with tone-based styling. |
| resources/views/components/ui/avatar.blade.php | Adds an avatar primitive with initials/name fallback and size variants. |
| resources/views/components/ui/panel.blade.php | Adds a panel/container primitive with optional title/subtitle header. |
| resources/views/components/ui/page-header.blade.php | Adds a page header primitive with eyebrow/title/description and actions region. |
| resources/views/components/ui/dropdown.blade.php | Adds a details/summary-based dropdown primitive. |
| resources/views/components/ui/modal.blade.php | Adds a “modal” container primitive with optional title. |
| resources/views/components/layouts/app-shell.blade.php | Switches inline toolbar/context nav markup to new x-app.* components and minor class tweaks. |
| resources/views/components/app/toolbar-item.blade.php | Extracts toolbar item link rendering into a reusable component. |
| resources/views/components/app/context-nav-item.blade.php | Extracts context nav item link rendering into a reusable component. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <input {{ $attributes->class([$fieldClasses]) }}> | ||
|
|
||
| @if ($error) | ||
| <span class="text-sm text-red-500">{{ $error }}</span> | ||
| @endif |
There was a problem hiding this comment.
When $error is present, the input isn’t marked/linked for assistive tech: consider setting aria-invalid="true" and associating the error text via aria-describedby (using a stable id from $attributes->get('id') or derived from name). This makes validation errors discoverable to screen readers.
| <select {{ $attributes->class([$fieldClasses]) }}> | ||
| {{ $slot }} | ||
| </select> | ||
|
|
||
| @if ($error) |
There was a problem hiding this comment.
When $error is present, the select isn’t marked/linked for assistive tech: consider setting aria-invalid="true" and associating the error text via aria-describedby (using a stable id from $attributes->get('id') or derived from name).
| <textarea {{ $attributes->class([$fieldClasses]) }}></textarea> | ||
|
|
||
| @if ($error) | ||
| <span class="text-sm text-red-500">{{ $error }}</span> | ||
| @endif |
There was a problem hiding this comment.
When $error is present, the textarea isn’t marked/linked for assistive tech: consider setting aria-invalid="true" and associating the error text via aria-describedby (using a stable id from $attributes->get('id') or derived from name).
| <div {{ $attributes->class(['rounded-md bg-panel p-5']) }}> | ||
| @if ($title) | ||
| <h2 class="text-base font-semibold text-copy">{{ $title }}</h2> |
There was a problem hiding this comment.
This component is named modal, but the markup is only a styled container and doesn’t provide dialog/modal semantics (e.g., role="dialog", aria-modal, labeling via aria-labelledby, backdrop/focus management). Either rename it to reflect that it’s a non-modal surface, or implement the expected modal semantics to avoid consumers shipping an inaccessible/non-modal “modal”.
| <div {{ $attributes->class(['rounded-md bg-panel p-5']) }}> | |
| @if ($title) | |
| <h2 class="text-base font-semibold text-copy">{{ $title }}</h2> | |
| @php | |
| $titleId = $title ? 'modal-title-' . uniqid() : null; | |
| @endphp | |
| <div | |
| role="dialog" | |
| aria-modal="true" | |
| @if ($titleId) aria-labelledby="{{ $titleId }}" @endif | |
| tabindex="-1" | |
| {{ $attributes->class(['rounded-md bg-panel p-5']) }} | |
| > | |
| @if ($title) | |
| <h2 id="{{ $titleId }}" class="text-base font-semibold text-copy">{{ $title }}</h2> |
| ]) | ||
|
|
||
| <details {{ $attributes->class(['group relative']) }}> | ||
| <summary class="list-none rounded-md bg-main px-3 py-2 text-sm font-medium text-copy marker:hidden"> |
There was a problem hiding this comment.
marker:hidden is unlikely to affect the <summary> disclosure marker (::marker doesn’t support display: none). Since list-none is already present, consider removing marker:hidden, or hide the marker via a targeted CSS rule (e.g., summary::-webkit-details-marker { display: none; }) if needed.
| <summary class="list-none rounded-md bg-main px-3 py-2 text-sm font-medium text-copy marker:hidden"> | |
| <summary class="list-none rounded-md bg-main px-3 py-2 text-sm font-medium text-copy"> |
| it('renders the shared form controls', function (): void { | ||
| $rendered = Blade::render(<<<'BLADE' | ||
| <x-ui.input label="Name" /> | ||
| <x-ui.textarea label="Summary"></x-ui.textarea> | ||
| <x-ui.select label="State"> |
There was a problem hiding this comment.
The PR description mentions Pest coverage for shared component outputs, but this file doesn’t currently exercise some newly added components (e.g., x-ui.panel, x-ui.page-header, x-ui.dropdown, x-ui.modal). Adding minimal render assertions for them would help catch markup regressions and better match the stated coverage goals.
| ]) | ||
|
|
||
| <a | ||
| href="{{ $href }}" |
There was a problem hiding this comment.
Consider adding aria-current when $current is true (e.g., aria-current="page") so the active toolbar item is announced correctly to screen readers, not just styled visually.
| href="{{ $href }}" | |
| href="{{ $href }}" | |
| @if ($current) aria-current="page" @endif |
| ]) | ||
|
|
||
| <a | ||
| href="{{ $href }}" |
There was a problem hiding this comment.
Consider adding aria-current when $current is true (e.g., aria-current="page") so the active context nav item is announced correctly to screen readers, not just styled visually.
| href="{{ $href }}" | |
| href="{{ $href }}" | |
| @if($current) aria-current="page" @endif |
Closes #6\n\nSummary:\n- Adds reusable Blade primitives for buttons, inputs, selects, textareas, badges, avatars, panels, dropdowns, modals, headers, and shell nav items\n- Refactors the app shell and home screen to consume shared components\n- Adds Pest coverage for the shared component outputs\n\nValidation:\n- vendor/bin/pint --dirty --format agent\n- php artisan test --compact\n- npm run build