Documentation

Widgets

Widgets

UltraViolet Pro includes a comprehensive widget system for creating dynamic, reusable dashboard components. Widgets can display statistics, charts, lists, and interactive elements.

Widget Types

UltraViolet Pro includes three distinct widget types, each designed for specific use cases and data display patterns.

1. Basic Widgets

Basic widgets display simple statistics and metrics with optional icons and trend indicators. These are perfect for displaying key performance indicators (KPIs) and summary data.

Route: /admin/widgets?page=basic

Characteristics:

  • Single data point display
  • Icon and trend indicators
  • Color-coded status
  • Responsive design
  • Loading states support

Statistics Widget

<div class="card widget-stats">
    <div class="card-body">
        <div class="d-flex justify-content-between align-items-center">
            <div>
                <h6 class="text-muted mb-1">Total Revenue</h6>
                <h2 class="mb-0">$125,430</h2>
                <span class="badge bg-success">
                    <i class="material-symbols-rounded">trending_up</i>
                    +12.5%
                </span>
            </div>
            <div class="widget-icon">
                <i class="material-symbols-rounded text-primary fs-1">payments</i>
            </div>
        </div>
    </div>
</div>

Features

  • Icon Support: Material Icons and Bootstrap Icons
  • Trend Indicators: Up/down arrows with percentages
  • Color Variants: Primary, success, danger, warning, info
  • Responsive: Mobile-optimized layouts
  • Loading States: Skeleton loaders for async data

Example: User Widget

// Controller
public function getUserStats()
{
    return [
        'total' => User::count(),
        'active' => User::where('status', 'active')->count(),
        'new_today' => User::whereDate('created_at', today())->count(),
        'trend' => 8.3
    ];
}
<!-- View -->
<div class="card">
    <div class="card-body">
        <h6 class="text-muted">Total Users</h6>
        <h2>{{ $stats['total'] }}</h2>
        <small class="text-success">+{{ $stats['trend'] }}% from last month</small>
    </div>
</div>

2. Multi-Part Widgets

Multi-part widgets combine multiple data points or sections within a single widget. These are ideal for displaying related information in a compact format.

Route: /admin/widgets?page=multi-part

Characteristics:

  • Multiple data sections
  • Split layouts
  • Progress indicators
  • Timeline displays
  • Complex data relationships

Split Statistics Widget

<div class="card widget-multi-part">
    <div class="card-header">
        <h5 class="card-title mb-0">Sales Overview</h5>
    </div>
    <div class="card-body">
        <div class="row">
            <div class="col-md-6 border-end">
                <div class="text-center">
                    <h6 class="text-muted">Today's Sales</h6>
                    <h3 class="text-primary">$12,430</h3>
                    <span class="badge bg-success">+15%</span>
                </div>
            </div>
            <div class="col-md-6">
                <div class="text-center">
                    <h6 class="text-muted">This Month</h6>
                    <h3 class="text-info">$325,790</h3>
                    <span class="badge bg-success">+8%</span>
                </div>
            </div>
        </div>
    </div>
    <div class="card-footer">
        <div class="row text-center">
            <div class="col-4">
                <small class="text-muted">Orders</small>
                <div class="fw-bold">1,245</div>
            </div>
            <div class="col-4">
                <small class="text-muted">Customers</small>
                <div class="fw-bold">892</div>
            </div>
            <div class="col-4">
                <small class="text-muted">Products</small>
                <div class="fw-bold">156</div>
            </div>
        </div>
    </div>
</div>

Progress Widget

<div class="card">
    <div class="card-body">
        <h6 class="card-title">Project Progress</h6>

        <div class="mb-3">
            <div class="d-flex justify-content-between mb-1">
                <span>Design Phase</span>
                <span class="text-muted">100%</span>
            </div>
            <div class="progress" style="height: 6px;">
                <div class="progress-bar bg-success" style="width: 100%"></div>
            </div>
        </div>

        <div class="mb-3">
            <div class="d-flex justify-content-between mb-1">
                <span>Development</span>
                <span class="text-muted">75%</span>
            </div>
            <div class="progress" style="height: 6px;">
                <div class="progress-bar bg-primary" style="width: 75%"></div>
            </div>
        </div>

        <div class="mb-0">
            <div class="d-flex justify-content-between mb-1">
                <span>Testing</span>
                <span class="text-muted">30%</span>
            </div>
            <div class="progress" style="height: 6px;">
                <div class="progress-bar bg-warning" style="width: 30%"></div>
            </div>
        </div>
    </div>
</div>

Timeline Widget

<div class="card">
    <div class="card-header">
        <h5 class="card-title mb-0">Recent Activity</h5>
    </div>
    <div class="card-body">
        <div class="timeline">
            <div class="timeline-item">
                <div class="timeline-marker bg-primary"></div>
                <div class="timeline-content">
                    <h6 class="mb-0">New Order Received</h6>
                    <small class="text-muted">2 minutes ago</small>
                    <p class="mb-0">Order #12345 from John Doe</p>
                </div>
            </div>
            <div class="timeline-item">
                <div class="timeline-marker bg-success"></div>
                <div class="timeline-content">
                    <h6 class="mb-0">Payment Processed</h6>
                    <small class="text-muted">15 minutes ago</small>
                    <p class="mb-0">$450.00 payment received</p>
                </div>
            </div>
            <div class="timeline-item">
                <div class="timeline-marker bg-info"></div>
                <div class="timeline-content">
                    <h6 class="mb-0">New User Registration</h6>
                    <small class="text-muted">1 hour ago</small>
                    <p class="mb-0">Jane Smith signed up</p>
                </div>
            </div>
        </div>
    </div>
</div>

3. Live Data Widgets

Live data widgets use Laravel Livewire for real-time updates without page refreshes. These widgets automatically refresh data and provide live monitoring capabilities.

Route: /admin/widgets?page=live-data

Characteristics:

  • Real-time data updates
  • Automatic refresh intervals
  • Live monitoring
  • WebSocket integration
  • Polling mechanisms

Real-time Counter

// Livewire Component
namespace App\Livewire;

use Livewire\Component;

class LiveCounter extends Component
{
    public $count = 0;

    public function mount()
    {
        $this->count = Order::where('status', 'pending')->count();
    }

    public function refresh()
    {
        $this->count = Order::where('status', 'pending')->count();
    }

    public function render()
    {
        return view('livewire.live-counter');
    }
}
<!-- livewire/live-counter.blade.php -->
<div class="card" wire:poll.5s="refresh">
    <div class="card-body">
        <h6 class="text-muted">Pending Orders</h6>
        <h2>{{ $count }}</h2>
        <small class="text-muted">Updates every 5 seconds</small>
    </div>
</div>

Live Chart Widget

// Livewire Chart Component
class LiveChart extends Component
{
    public $data = [];

    public function mount()
    {
        $this->refreshData();
    }

    public function refreshData()
    {
        $this->data = [
            'labels' => ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
            'values' => Order::selectRaw('DATE(created_at) as date, COUNT(*) as count')
                ->whereBetween('created_at', [now()->subDays(7), now()])
                ->groupBy('date')
                ->pluck('count')
                ->toArray()
        ];
    }

    public function render()
    {
        return view('livewire.live-chart');
    }
}

Auto-refreshing List

class RecentOrders extends Component
{
    public $orders;

    public function mount()
    {
        $this->loadOrders();
    }

    public function loadOrders()
    {
        $this->orders = Order::latest()->take(5)->get();
    }

    public function render()
    {
        return view('livewire.recent-orders');
    }
}
<div class="card" wire:poll.10s="loadOrders">
    <div class="card-header">
        <h5 class="card-title mb-0">Recent Orders</h5>
    </div>
    <div class="list-group list-group-flush">
        @foreach($orders as $order)
        <div class="list-group-item">
            <div class="d-flex justify-content-between">
                <div>
                    <h6 class="mb-0">Order #{{ $order->id }}</h6>
                    <small class="text-muted">{{ $order->customer_name }}</small>
                </div>
                <div>
                    <span class="badge bg-{{ $order->status_color }}">
                        {{ $order->status }}
                    </span>
                    <div class="text-end">${{ $order->total }}</div>
                </div>
            </div>
        </div>
        @endforeach
    </div>
</div>

Chart Widgets

ApexCharts Integration

<div class="card">
    <div class="card-header">
        <h5 class="card-title mb-0">Revenue Trend</h5>
    </div>
    <div class="card-body">
        <div id="revenueChart"></div>
    </div>
</div>

<script>
var options = {
    series: [{
        name: 'Revenue',
        data: [30, 40, 35, 50, 49, 60, 70, 91, 125]
    }],
    chart: {
        type: 'area',
        height: 350,
        toolbar: {
            show: false
        }
    },
    colors: ['#0d6efd'],
    dataLabels: {
        enabled: false
    },
    stroke: {
        curve: 'smooth'
    },
    xaxis: {
        categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep']
    }
};

var chart = new ApexCharts(document.querySelector("#revenueChart"), options);
chart.render();
</script>

Custom Widgets

Creating a Custom Widget

// app/View/Components/Widgets/CustomWidget.php
namespace App\View\Components\Widgets;

use Illuminate\View\Component;

class CustomWidget extends Component
{
    public $title;
    public $value;
    public $icon;
    public $trend;

    public function __construct($title, $value, $icon = 'info', $trend = null)
    {
        $this->title = $title;
        $this->value = $value;
        $this->icon = $icon;
        $this->trend = $trend;
    }

    public function render()
    {
        return view('components.widgets.custom-widget');
    }
}
<!-- resources/views/components/widgets/custom-widget.blade.php -->
<div class="card">
    <div class="card-body">
        <div class="d-flex justify-content-between align-items-center">
            <div>
                <h6 class="text-muted mb-1">{{ $title }}</h6>
                <h2 class="mb-0">{{ $value }}</h2>
                @if($trend)
                <span class="badge bg-{{ $trend > 0 ? 'success' : 'danger' }}">
                    <i class="material-symbols-rounded">
                        {{ $trend > 0 ? 'trending_up' : 'trending_down' }}
                    </i>
                    {{ abs($trend) }}%
                </span>
                @endif
            </div>
            <div class="widget-icon">
                <i class="material-symbols-rounded text-primary fs-1">{{ $icon }}</i>
            </div>
        </div>
    </div>
</div>

Usage

<x-widgets.custom-widget 
    title="Total Sales"
    value="$125,430"
    icon="payments"
    :trend="12.5"
/>

Widget Grid Layouts

Responsive Grid

<div class="row g-3">
    <div class="col-12 col-sm-6 col-lg-3">
        <!-- Widget 1 -->
    </div>
    <div class="col-12 col-sm-6 col-lg-3">
        <!-- Widget 2 -->
    </div>
    <div class="col-12 col-sm-6 col-lg-3">
        <!-- Widget 3 -->
    </div>
    <div class="col-12 col-sm-6 col-lg-3">
        <!-- Widget 4 -->
    </div>
</div>

Mixed Sizes

<div class="row g-3">
    <div class="col-12 col-lg-8">
        <!-- Large widget (chart) -->
    </div>
    <div class="col-12 col-lg-4">
        <div class="row g-3">
            <div class="col-12">
                <!-- Small widget 1 -->
            </div>
            <div class="col-12">
                <!-- Small widget 2 -->
            </div>
        </div>
    </div>
</div>

Loading States

UltraViolet Pro includes comprehensive loading state support for all widget types, ensuring a smooth user experience while data is being fetched.

Loading State Types

1. Skeleton Loaders

Skeleton loaders provide a visual placeholder that matches the final content structure.

<!-- Skeleton loader for basic widget -->
<div class="card widget-stats">
    <div class="card-body">
        <div class="d-flex justify-content-between align-items-center">
            <div>
                <div class="skeleton-text skeleton-title mb-1"></div>
                <div class="skeleton-text skeleton-value mb-2"></div>
                <div class="skeleton-text skeleton-badge"></div>
            </div>
            <div class="skeleton-icon"></div>
        </div>
    </div>
</div>

2. Spinner Loaders

Spinner loaders indicate ongoing data processing.

<!-- Spinner loader for live data widget -->
<div class="card">
    <div class="card-body d-flex flex-column align-items-center justify-content-center" style="min-height: 200px;">
        <div class="spinner-border text-primary mb-3" role="status" style="width: 3rem; height: 3rem;">
            <span class="visually-hidden">Loading...</span>
        </div>
        <p class="text-muted mb-0">Loading data...</p>
    </div>
</div>

3. Progress Bars

Progress bars show loading progress for long-running operations.

<!-- Progress bar for data processing -->
<div class="card">
    <div class="card-body">
        <h6 class="card-title">Processing Data</h6>
        <div class="progress mb-2" style="height: 8px;">
            <div class="progress-bar progress-bar-striped progress-bar-animated" 
                 role="progressbar" style="width: 45%"></div>
        </div>
        <small class="text-muted">45% complete</small>
    </div>
</div>

Loading State Implementation

Livewire Loading States

// Livewire Component with loading states
class LiveDataWidget extends Component
{
    public $data = [];
    public $isLoading = true;
    public $loadingMessage = 'Loading data...';

    public function mount()
    {
        $this->loadData();
    }

    public function loadData()
    {
        $this->isLoading = true;
        $this->loadingMessage = 'Fetching latest data...';

        // Simulate API call
        $this->data = $this->fetchDataFromAPI();

        $this->isLoading = false;
    }

    public function render()
    {
        return view('livewire.live-data-widget');
    }
}
<!-- Livewire view with loading states -->
<div class="card">
    <div class="card-body">
        @if($isLoading)
            <div class="d-flex flex-column align-items-center justify-content-center" style="min-height: 200px;">
                <div class="spinner-border text-primary mb-3" role="status">
                    <span class="visually-hidden">Loading...</span>
                </div>
                <p class="text-muted mb-0">{{ $loadingMessage }}</p>
            </div>
        @else
            <h6 class="text-muted">Live Data</h6>
            <h2>{{ $data['value'] }}</h2>
            <small class="text-success">+{{ $data['trend'] }}%</small>
        @endif
    </div>
</div>

JavaScript Loading States

// JavaScript loading state management
class WidgetLoader {
    constructor(widgetElement) {
        this.widget = widgetElement;
        this.loadingOverlay = null;
    }

    showLoading(message = 'Loading...') {
        this.loadingOverlay = document.createElement('div');
        this.loadingOverlay.className = 'widget-loading-overlay';
        this.loadingOverlay.innerHTML = `
            <div class="d-flex flex-column align-items-center justify-content-center h-100">
                <div class="spinner-border text-primary mb-3" role="status">
                    <span class="visually-hidden">Loading...</span>
                </div>
                <p class="text-muted mb-0">${message}</p>
            </div>
        `;

        this.widget.style.position = 'relative';
        this.widget.appendChild(this.loadingOverlay);
    }

    hideLoading() {
        if (this.loadingOverlay) {
            this.loadingOverlay.remove();
            this.loadingOverlay = null;
        }
    }

    showError(message = 'An error occurred') {
        this.hideLoading();

        const errorDiv = document.createElement('div');
        errorDiv.className = 'alert alert-danger';
        errorDiv.innerHTML = `
            <i class="material-symbols-rounded me-2">error</i>
            ${message}
        `;

        this.widget.innerHTML = '';
        this.widget.appendChild(errorDiv);
    }
}

// Usage example
const widget = document.getElementById('my-widget');
const loader = new WidgetLoader(widget);

// Show loading
loader.showLoading('Fetching data...');

// Simulate API call
fetch('/api/widget-data')
    .then(response => response.json())
    .then(data => {
        loader.hideLoading();
        // Update widget with data
        updateWidget(data);
    })
    .catch(error => {
        loader.showError('Failed to load data');
    });

Loading State Styling

// Loading state styles
.widget-loading-overlay {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(var(--bs-body-bg-rgb), 0.8);
    backdrop-filter: blur(2px);
    z-index: 10;
    display: flex;
    align-items: center;
    justify-content: center;
}

.skeleton-text {
    background: linear-gradient(90deg, var(--skeleton-bg) 25%, var(--skeleton-shine) 50%, var(--skeleton-bg) 75%);
    background-size: 200% 100%;
    animation: skeleton-loading 1.5s infinite;
    border-radius: 4px;
    height: 1rem;
    margin-bottom: 0.5rem;

    &.skeleton-title {
        width: 60%;
        height: 0.875rem;
    }

    &.skeleton-value {
        width: 40%;
        height: 1.5rem;
    }

    &.skeleton-badge {
        width: 30%;
        height: 0.75rem;
    }
}

.skeleton-icon {
    width: 3rem;
    height: 3rem;
    background: var(--skeleton-bg);
    border-radius: 50%;
    animation: skeleton-loading 1.5s infinite;
}

@keyframes skeleton-loading {
    0% {
        background-position: -200% 0;
    }
    100% {
        background-position: 200% 0;
    }
}

// CSS Variables for theming
:root {
    --skeleton-bg: #e2e8f0;
    --skeleton-shine: #f1f5f9;
}

[data-bs-theme="dark"] {
    --skeleton-bg: #374151;
    --skeleton-shine: #4b5563;
}

Loading State Best Practices

  1. Immediate Feedback: Show loading state immediately when action is triggered
  2. Appropriate Duration: Use skeleton loaders for quick loads, spinners for longer operations
  3. Progress Indication: Show progress bars for operations with known duration
  4. Error Handling: Provide clear error states with retry options
  5. Accessibility: Include proper ARIA labels for screen readers
  6. Performance: Minimize loading state overhead
  7. User Experience: Keep loading states visually consistent with the design

Best Practices

  1. Performance: Cache widget data when possible
  2. Real-time: Use polling sparingly to avoid server load
  3. Responsive: Test widgets on all screen sizes
  4. Accessibility: Include proper ARIA labels
  5. Loading States: Show loaders while data loads
  6. Error Handling: Display fallback content on errors

Production Deployment

All widgets work in both the static HTML and Laravel Livewire editions. For live data widgets in the static HTML version, you may need to connect them to your API endpoints.