Skip to main content
Back to ScopeForged

ScopeForged Documentation

Technical documentation, guides, and feature references for the ScopeForged client portal.

Dashboards & UI/UI Components

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

  1. Icon Component
  2. Content Width System
  3. Data Table Component
  4. Feedback Components
  5. Related Documentation

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

VariantViewBoxUsage
outline (default)0 0 24 24Standard icons with stroke
solid0 0 20 20Filled icons

Available Icons

Navigation:

  • arrow-left, arrow-right, arrow-up, arrow-down
  • chevron-left, chevron-right, chevron-up, chevron-down
  • arrow-top-right-on-square (external link)

Actions:

  • plus, x-mark, check, pencil, trash
  • download, upload, search, refresh
  • duplicate, save, archive

Documents:

  • document, document-text, document-download, document-arrow-down
  • clipboard, clipboard-document-check, clipboard-document-list
  • folder, folder-open

Status:

  • check-circle, x-circle, exclamation-circle, exclamation-triangle
  • info-circle, question-mark-circle

Objects:

  • mail, bell, clock, tag, chat, chat-bubble-left-right
  • eye, 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, underline
  • list-bullet, list-numbered
  • align-left, align-center, align-right
  • table-cells, undo, redo

Misc:

  • cog, adjustments, filter, calendar, currency-dollar
  • globe, server, database, code, terminal
  • paper-clip, musical-note, photograph
  • play, pause, stop
  • bars-2, bars-3, dots-vertical, dots-horizontal
  • star (solid), bookmark (solid), archive-box
  • pin (solid) - for pinned notes/items

Special Cases (Inline SVGs)

Some SVGs must remain inline due to technical requirements:

ReasonExamples
Animated spinnersComplex circle/path animations
JavaScript templatesBlade components don't work in JS template strings
Brand logosUnique designs (Slack, Discord, Zapier)
Data-driven graphicsCircular progress charts with dynamic values
Decorative illustrationsLarge, complex SVGs

Files with inline SVGs (by design):

  • alert-rules/create.blade.php, alert-rules/edit.blade.php - animated spinners
  • api-playground/index.blade.php - animated spinners
  • security/dashboard.blade.php - animated spinners
  • webhooks/templates.blade.php - brand logos (Slack, Discord, Zapier)
  • analytics-export-panel.blade.php - animated spinner
  • internal-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

AreaBeforeAfterReduction
Admin views308 SVGs12 special cases96%
Portal views15 SVGs0100%
Components~87 SVGs3 (spinners/toggles)97%
LayoutsMultiple0100%

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 TypeWidthNotes
Index/Table pagesmax-w-[1800px]Uses layout default
Dashboard pagesmax-w-[1800px]Uses layout default
Create/Edit formsmax-w-3xl or max-w-4xlInner wrapper
Show/Detail pagesmax-w-4xlInner 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

ClassWidthUse Case
max-w-[1800px]1800pxTables, dashboards, headers
max-w-7xl1280pxLegacy (prefer 1800px)
max-w-4xl896pxDetail/show pages
max-w-3xl768pxForms
max-w-2xl672pxModal 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

PropTypeRequiredDescription
endpointstringYesAJAX endpoint URL
filtersarrayYesFilter configuration array
dataPaginatorYesInitial paginated data
emptyMessagestringNoMessage when no results

Filter Configuration

Each filter is an array with:

KeyTypeRequiredDescription
namestringYesQuery parameter name
typestringYestext, select, or date
labelstringYesDisplay label
placeholderstringNoInput placeholder
optionsarrayFor selectKey-value options

Filter Types

TypeBehavior
textTriggers on Enter key press
selectTriggers immediately on change
dateTriggers 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

FeatureDescription
Hybrid FilteringServer-rendered initial load, AJAX for subsequent changes
URL SyncBrowser URL updates with filters for bookmarkable views
Per-Page Options10, 25, 50, 100 with default of 25
Loading StateVisual indicator during AJAX requests
Empty StateConfigurable empty message
Dark ModeFull dark mode support
Clear FiltersButton appears when filters are active

Pages Using Data Table

PageRoute
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

ComponentPurpose
data-table/filter-field.blade.phpRenders filter inputs
data-table/pagination.blade.phpPagination 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

PropertyTypeDefaultDescription
titlestring'Confirm Action'Modal title
messagestring'Are you sure?'Body text
confirmTextstring'Confirm'Confirm button label
cancelTextstring'Cancel'Cancel button label
variantstring'danger'Visual style: danger, warning, info, success
iconstring'exclamation-triangle'Icon: exclamation-triangle, trash, check, info
formHTMLElementnullForm element to submit on confirm
onConfirmfunctionnullCallback for non-form flows

Variants

VariantButton ColorIcon ColorUse For
dangerRedRedDelete, remove, destroy
warningYellowYellowArchive, deactivate, irreversible
infoBlueBlueNeutral confirmations
successGreenGreenPositive 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

PropertyTypeDefaultDescription
titlestring'Enter Value'Modal title
labelstring''Input label
placeholderstring''Input placeholder
inputTypestring'text'HTML input type (text, url, email)
onSubmitfunctionrequiredCallback 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

TypeColorUse For
successGreenSuccessful operations
errorRedFailed operations
warningYellowCaution messages
infoBlueNeutral information

Migration Summary (Plan 238)

All native browser dialogs have been replaced across the entire application:

DialogCountReplacement
confirm()~120 filesGlobal confirm modal (open-global-confirm event)
alert()~31 filesToast notifications (toast event)
prompt()1 file (2 calls)Prompt modal (open-global-prompt event)