UI Components Guide
Last Updated: 2026-02-14 Status: Implemented Plan Reference: 112-icon-library-consolidation.md, 121-content-width-standardization.md, 122-universal-data-table.md, 238-replace-browser-dialogs-with-ui-components.md
Overview
This guide documents the standardized UI components used throughout the client portal application, including the icon library, content width system, data table component, and feedback components (confirm modal, prompt modal, toast notifications).
Table of Contents
Icon Component
Overview
The application uses a consolidated icon library with 136+ icons available through a single Blade component, eliminating inline SVG duplication across views.
Location: resources/views/components/icon.blade.php
Basic Usage
{{-- Standard usage --}}
<x-icon name="check" />
{{-- With custom size --}}
<x-icon name="plus" class="w-4 h-4" />
{{-- With custom stroke width --}}
<x-icon name="arrow-left" stroke-width="2" />
{{-- Solid variant --}}
<x-icon name="check-circle" variant="solid" class="text-green-500" />
Icon Variants
| Variant | ViewBox | Usage |
|---|---|---|
outline (default) | 0 0 24 24 | Standard icons with stroke |
solid | 0 0 20 20 | Filled icons |
Available Icons
Navigation:
arrow-left,arrow-right,arrow-up,arrow-downchevron-left,chevron-right,chevron-up,chevron-downarrow-top-right-on-square(external link)
Actions:
plus,x-mark,check,pencil,trashdownload,upload,search,refreshduplicate,save,archive
Documents:
document,document-text,document-download,document-arrow-downclipboard,clipboard-document-check,clipboard-document-listfolder,folder-open
Status:
check-circle,x-circle,exclamation-circle,exclamation-triangleinfo-circle,question-mark-circle
Objects:
mail,bell,clock,tag,chat,chat-bubble-left-righteye,eye-off,shield-check,lock-closed,lock-open,key
Charts:
chart-bar,chart-pie,chart-line
Users:
user,users,user-circle,user-group
Rich Text:
bold,italic,underlinelist-bullet,list-numberedalign-left,align-center,align-righttable-cells,undo,redo
Misc:
cog,adjustments,filter,calendar,currency-dollarglobe,server,database,code,terminalpaper-clip,musical-note,photographplay,pause,stopbars-2,bars-3,dots-vertical,dots-horizontalstar(solid),bookmark(solid),archive-boxpin(solid) - for pinned notes/items
Special Cases (Inline SVGs)
Some SVGs must remain inline due to technical requirements:
| Reason | Examples |
|---|---|
| Animated spinners | Complex circle/path animations |
| JavaScript templates | Blade components don't work in JS template strings |
| Brand logos | Unique designs (Slack, Discord, Zapier) |
| Data-driven graphics | Circular progress charts with dynamic values |
| Decorative illustrations | Large, complex SVGs |
Files with inline SVGs (by design):
alert-rules/create.blade.php,alert-rules/edit.blade.php- animated spinnersapi-playground/index.blade.php- animated spinnerssecurity/dashboard.blade.php- animated spinnerswebhooks/templates.blade.php- brand logos (Slack, Discord, Zapier)analytics-export-panel.blade.php- animated spinnerinternal-note-item.blade.php- pin toggle (requires fill/stroke switching)- Marketing pages (
marketing/) - decorative illustrations
Adding New Icons
To add a new icon, edit resources/views/components/icon.blade.php:
$icons = [
// Add new icon
'new-icon-name' => '<path stroke-linecap="round" stroke-linejoin="round" d="M..." />',
];
// For solid icons
$solidIcons = [
'new-icon-name' => '<path fill="currentColor" d="M..." />',
];
Migration Results
| Area | Before | After | Reduction |
|---|---|---|---|
| Admin views | 308 SVGs | 12 special cases | 96% |
| Portal views | 15 SVGs | 0 | 100% |
| Components | ~87 SVGs | 3 (spinners/toggles) | 97% |
| Layouts | Multiple | 0 | 100% |
Latest consolidation (Plan 113): Converted 22 additional icons across 8 component files including batch-upload, analytics-export-panel, internal-notes-panel, activity-summary-widget, and others.
Content Width System
Overview
The application uses a standardized content width system where the layout provides a default max-w-[1800px] wrapper that matches the header/navigation width.
Layout Structure
Location: resources/views/layouts/app.blade.php
<!-- Page Content -->
<main id="main-content">
<div class="py-6">
<div class="max-w-[1800px] mx-auto sm:px-6 lg:px-8">
{{ $slot }}
</div>
</div>
</main>
Width Guidelines
| View Type | Width | Notes |
|---|---|---|
| Index/Table pages | max-w-[1800px] | Uses layout default |
| Dashboard pages | max-w-[1800px] | Uses layout default |
| Create/Edit forms | max-w-3xl or max-w-4xl | Inner wrapper |
| Show/Detail pages | max-w-4xl | Inner wrapper |
Wide Views (Use Layout Default)
For index/table views, remove outer wrapper since layout provides correct width:
{{-- Before (redundant wrapper) --}}
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
table content
</div>
</div>
{{-- After (uses layout default) --}}
table content
Narrow Views (Keep Inner Wrapper)
For forms and detail views, use inner wrapper for narrower content:
{{-- Form/detail view with narrow width --}}
<div class="max-w-3xl mx-auto">
form content
</div>
Width Constants
| Class | Width | Use Case |
|---|---|---|
max-w-[1800px] | 1800px | Tables, dashboards, headers |
max-w-7xl | 1280px | Legacy (prefer 1800px) |
max-w-4xl | 896px | Detail/show pages |
max-w-3xl | 768px | Forms |
max-w-2xl | 672px | Modal content |
Data Table Component
Overview
The <x-data-table> component provides a reusable, hybrid-filtering data table with AJAX updates and URL synchronization.
Location: resources/views/components/data-table.blade.php
Basic Usage
<x-data-table
endpoint="{{ route('admin.clients.index') }}"
:filters="[
['name' => 'search', 'type' => 'text', 'label' => 'Search', 'placeholder' => 'Name...'],
['name' => 'status', 'type' => 'select', 'label' => 'Status', 'options' => $statuses],
['name' => 'from', 'type' => 'date', 'label' => 'From'],
]"
:data="$clients"
emptyMessage="No clients found."
>
<x-slot name="header">
<th>Name</th>
<th>Email</th>
<th>Status</th>
</x-slot>
@include('admin.clients._rows')
</x-data-table>
Component Props
| Prop | Type | Required | Description |
|---|---|---|---|
endpoint | string | Yes | AJAX endpoint URL |
filters | array | Yes | Filter configuration array |
data | Paginator | Yes | Initial paginated data |
emptyMessage | string | No | Message when no results |
Filter Configuration
Each filter is an array with:
| Key | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Query parameter name |
type | string | Yes | text, select, or date |
label | string | Yes | Display label |
placeholder | string | No | Input placeholder |
options | array | For select | Key-value options |
Filter Types
| Type | Behavior |
|---|---|
text | Triggers on Enter key press |
select | Triggers immediately on change |
date | Triggers immediately on change |
Controller Setup
Controllers must use the HasPaginatedResponse trait:
use App\Http\Controllers\Traits\HasPaginatedResponse;
class ClientController extends Controller
{
use HasPaginatedResponse;
public function index(Request $request)
{
$query = Client::query();
// Apply filters
if ($search = $request->input('search')) {
$query->where('name', 'like', "%{$search}%");
}
$clients = $query->paginate($request->input('per_page', 25));
// Return AJAX or full page
if ($request->wantsJson() || $request->ajax()) {
return $this->paginatedResponse($clients, 'admin.clients._rows');
}
return view('admin.clients.index', compact('clients'));
}
}
AJAX Response Format
{
"data": [...],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 100,
"last_page": 4,
"from": 1,
"to": 25
},
"html": "<tr>...</tr>"
}
Row Partial Pattern
Create a _rows.blade.php partial for table body:
{{-- resources/views/admin/clients/_rows.blade.php --}}
@forelse($clients as $client)
<tr>
<td>{{ $client->name }}</td>
<td>{{ $client->email }}</td>
<td>{{ $client->status }}</td>
</tr>
@empty
<tr>
<td colspan="3" class="text-center py-8 text-gray-500">
No clients found.
</td>
</tr>
@endforelse
Features
| Feature | Description |
|---|---|
| Hybrid Filtering | Server-rendered initial load, AJAX for subsequent changes |
| URL Sync | Browser URL updates with filters for bookmarkable views |
| Per-Page Options | 10, 25, 50, 100 with default of 25 |
| Loading State | Visual indicator during AJAX requests |
| Empty State | Configurable empty message |
| Dark Mode | Full dark mode support |
| Clear Filters | Button appears when filters are active |
Pages Using Data Table
| Page | Route |
|---|---|
| Clients | /admin/clients |
| Leads | /admin/leads |
| Projects | /admin/projects |
| Invoices | /admin/invoices |
| Time Entries | /admin/time |
| Documents | /admin/collaboration/document-requests |
| Webhooks | /admin/webhooks |
| Audit Trail | /admin/audit |
| Activity | /admin/activity |
| Activity Comparison | /admin/activity/comparison |
Sub-Components
| Component | Purpose |
|---|---|
data-table/filter-field.blade.php | Renders filter inputs |
data-table/pagination.blade.php | Pagination with per-page dropdown |
Feedback Components
Overview
The application uses three feedback components for user interaction: a global confirm modal, a prompt modal, and toast notifications. All native browser confirm(), alert(), and prompt() calls have been replaced with these styled, accessible components (Plan 238).
Global Confirm Modal
Location: resources/views/components/feedback/global-confirm-modal.blade.php
Included in: resources/views/layouts/app.blade.php (automatically available on all pages)
A single Alpine.js-driven modal invoked dynamically via the open-global-confirm event. Supports four visual variants for different action types.
Usage from Blade (form submission)
<form method="POST" action="/items/1"
x-data
@submit.prevent="$dispatch('open-global-confirm', {
title: 'Delete Item',
message: 'Are you sure you want to delete this item? This cannot be undone.',
variant: 'danger',
confirmText: 'Delete',
icon: 'trash',
form: $el
})"
>
@csrf @method('DELETE')
<button type="submit">Delete</button>
</form>
Usage from Alpine.js (callback)
this.$dispatch('open-global-confirm', {
title: 'Archive Project',
message: 'Archive this project?',
variant: 'warning',
confirmText: 'Archive',
icon: 'exclamation-triangle',
onConfirm: async () => {
// perform action
}
});
Usage from standalone JavaScript
window.dispatchEvent(new CustomEvent('open-global-confirm', {
detail: {
title: 'Confirm',
message: 'Proceed?',
variant: 'info',
confirmText: 'Yes',
onConfirm: () => { /* action */ }
}
}));
Event Properties
| Property | Type | Default | Description |
|---|---|---|---|
title | string | 'Confirm Action' | Modal title |
message | string | 'Are you sure?' | Body text |
confirmText | string | 'Confirm' | Confirm button label |
cancelText | string | 'Cancel' | Cancel button label |
variant | string | 'danger' | Visual style: danger, warning, info, success |
icon | string | 'exclamation-triangle' | Icon: exclamation-triangle, trash, check, info |
form | HTMLElement | null | Form element to submit on confirm |
onConfirm | function | null | Callback for non-form flows |
Variants
| Variant | Button Color | Icon Color | Use For |
|---|---|---|---|
danger | Red | Red | Delete, remove, destroy |
warning | Yellow | Yellow | Archive, deactivate, irreversible |
info | Blue | Blue | Neutral confirmations |
success | Green | Green | Positive confirmations |
Prompt Modal
Location: resources/views/components/feedback/prompt-modal.blade.php
Included in: resources/views/layouts/app.blade.php
A styled text input modal for collecting single-value input from users, replacing native prompt().
Usage
this.$dispatch('open-global-prompt', {
title: 'Insert Link',
label: 'URL',
placeholder: 'https://example.com',
inputType: 'url',
onSubmit: (value) => {
// use the entered value
}
});
Event Properties
| Property | Type | Default | Description |
|---|---|---|---|
title | string | 'Enter Value' | Modal title |
label | string | '' | Input label |
placeholder | string | '' | Input placeholder |
inputType | string | 'text' | HTML input type (text, url, email) |
onSubmit | function | required | Callback receiving the entered value |
Toast Notifications
Location: resources/views/components/feedback/toast-notifications.blade.php
Included in: resources/views/layouts/app.blade.php
Non-blocking notifications that appear briefly and auto-dismiss. Used for success/error/warning/info feedback.
Usage from Blade/Alpine
$dispatch('toast', { message: 'Item saved successfully.', type: 'success' });
$dispatch('toast', { message: 'Failed to save item.', type: 'error' });
Usage from standalone JavaScript
window.dispatchEvent(new CustomEvent('toast', {
detail: { message: 'Operation complete.', type: 'info' }
}));
Toast Types
| Type | Color | Use For |
|---|---|---|
success | Green | Successful operations |
error | Red | Failed operations |
warning | Yellow | Caution messages |
info | Blue | Neutral information |
Migration Summary (Plan 238)
All native browser dialogs have been replaced across the entire application:
| Dialog | Count | Replacement |
|---|---|---|
confirm() | ~120 files | Global confirm modal (open-global-confirm event) |
alert() | ~31 files | Toast notifications (toast event) |
prompt() | 1 file (2 calls) | Prompt modal (open-global-prompt event) |
Related Documentation
- Blade Components Guide - Component patterns
- Accessibility Guide - ARIA attributes for components
- Portal UI Guide - Portal-specific components