Skip to main content
Back to ScopeForged

ScopeForged Documentation

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

Dashboards & UI/Portal UI

Portal UI Guide

Last Updated: 2026-01-12 Status: Implemented Plan Reference: 033-portal-use-updates.md, 056-portal-ui-enhancement.md, 091-portal-ui-improvement.md


Overview

The Portal UI provides a consistent, responsive, and accessible user interface for both administrators and clients. It uses Tailwind CSS for styling, Blade components for reusability, and Alpine.js for interactivity.

Plan 056 Enhancements (2026-01-10):

  • Full dark mode support with system preference detection
  • Enhanced mobile responsiveness with utility classes
  • Skeleton loading states and animations
  • Skip links for accessibility (WCAG 2.1 AA)
  • Theme toggle component with light/dark/system options
  • Sticky navigation with improved mobile experience

Plan 091 Enhancements (2026-01-12):

  • Mobile bottom sheet navigation for portal (<x-portal.mobile-nav>)
  • Dark mode toggle button component (<x-dark-mode-toggle>)
  • Accessible form input component (<x-form.accessible-input>)
  • Staggered animations for lists (.stagger-children)
  • Slide-in-right animation utility

Table of Contents

  1. Dark Mode
  2. Layouts
  3. Components
  4. Theme Toggle
  5. Responsive Design
  6. Loading States
  7. Accessibility
  8. Animations
  9. Navigation
  10. Forms
  11. Related Features

Dark Mode

How It Works

Dark mode is implemented using Tailwind CSS's class strategy and a JavaScript ThemeService.

  1. Tailwind Config: darkMode: 'class' enables class-based dark mode
  2. ThemeService: Manages theme state, localStorage, and system preference detection
  3. CSS Variables: Theme colors defined as CSS custom properties

ThemeService

Located at resources/js/services/ThemeService.js:

// Get current theme
window.themeService.getCurrentTheme();

// Set theme ('light', 'dark', or 'system')
window.themeService.setTheme('dark');

// Toggle between light and dark
window.themeService.toggleTheme();

// Check if dark mode is active
window.themeService.isDark();

// Listen for theme changes
document.addEventListener('themechange', (e) => {
    console.log('Theme changed to:', e.detail.theme);
});

CSS Theme Variables

Defined in resources/css/theme.css:

:root {
    --color-bg-primary: #ffffff;
    --color-bg-secondary: #f9fafb;
    --color-text-primary: #111827;
    --color-border: #e5e7eb;
    --color-accent: #2563eb;
}

.dark {
    --color-bg-primary: #111827;
    --color-bg-secondary: #1f2937;
    --color-text-primary: #f9fafb;
    --color-border: #374151;
    --color-accent: #3b82f6;
}

Using Dark Mode Classes

All components support dark mode with the dark: prefix:

<div class="bg-white dark:bg-gray-800 text-gray-900 dark:text-white">
    <p class="text-gray-600 dark:text-gray-400">Subtitle text</p>
    <div class="border border-gray-200 dark:border-gray-700">
        Content with border
    </div>
</div>

Flash Prevention

The layout includes an inline script to prevent flash of incorrect theme:

<script>
    (function() {
        const theme = localStorage.getItem('theme');
        if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
            document.documentElement.classList.add('dark');
        }
    })();
</script>

Layouts

Available Layouts

LayoutPathPurposeDark Mode
applayouts/app.blade.phpAuthenticated usersYes
guestlayouts/guest.blade.phpLogin/registerYes

Layout Features

Both layouts include:

  • Skip links for accessibility
  • Theme toggle component
  • Meta theme-color for mobile browsers
  • Flash prevention script
  • Alpine.js cloak style

Layout Structure

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="scroll-smooth">
<head>
    <meta name="theme-color" content="#ffffff">

    {{-- Flash prevention script --}}
    <script>/* theme detection */</script>

    @vite(['resources/css/app.css', 'resources/js/app.js'])

    <style>[x-cloak] { display: none !important; }</style>
</head>
<body class="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
    <x-skip-links />

    <nav id="main-navigation">
        @include('layouts.navigation')
    </nav>

    <main id="main-content">
        {{ $slot }}
    </main>
</body>
</html>

Components

UI Components

Located in resources/views/components/ui/:

Button

<x-ui.button variant="primary" size="md">Save</x-ui.button>
<x-ui.button variant="danger" :loading="$saving">Delete</x-ui.button>
<x-ui.button variant="ghost" href="/back">Cancel</x-ui.button>

Variants: primary, secondary, outline, danger, success, warning, ghost, link

Sizes: xs, sm, md, lg, xl

Card

<x-ui.card>
    <x-slot name="header">Card Title</x-slot>
    Card content
    <x-slot name="footer">Footer</x-slot>
</x-ui.card>

<x-ui.card :hover="true" :padding="false">
    Hoverable card without default padding
</x-ui.card>

Badge

<x-ui.badge variant="success">Active</x-ui.badge>
<x-ui.badge variant="danger" size="sm">Overdue</x-ui.badge>

Variants: default, primary, success, warning, danger, info

Input

<x-ui.input name="email" type="email" :error="$errors->has('email')" />

Theme Toggle

Usage

{{-- Basic usage --}}
<x-theme-toggle />

{{-- With custom class --}}
<x-theme-toggle class="me-4" />

Features

  • Light/Dark/System mode options
  • Dropdown menu interface
  • Persists preference to localStorage
  • Respects system preference when set to "System"
  • Updates meta theme-color for mobile browsers

Alpine.js Component

The theme toggle uses the themeToggle Alpine component:

Alpine.data('themeToggle', () => ({
    theme: 'system',
    open: false,

    setTheme(value) {
        // Handles theme switching
    },

    toggleTheme() {
        // Quick toggle between light/dark
    },

    get isDark() {
        return document.documentElement.classList.contains('dark');
    }
}));

Responsive Design

Breakpoints

BreakpointMin WidthUse Case
sm640pxMobile landscape
md768pxTablets
lg1024pxLaptops
xl1280pxDesktops
2xl1536pxLarge screens

Utility Classes

Located in resources/css/responsive.css:

/* Container responsive */
.container-responsive { /* Responsive container with breakpoints */ }

/* Card grid - 1 col mobile, 2 tablet, 3-4 desktop */
.card-grid { @apply grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4; }

/* Stats grid - 2 mobile, 4 desktop */
.stats-grid { @apply grid-cols-2 md:grid-cols-4; }

/* Dashboard layout - stacked mobile, sidebar desktop */
.dashboard-layout { @apply flex-col lg:flex-row; }

/* Responsive text sizing */
.text-responsive-lg { @apply text-base sm:text-lg md:text-xl; }

/* Stack on mobile */
.stack-mobile { @apply flex-col sm:flex-row; }

Responsive Table

<x-responsive-table :headers="$headers" :mobileCardView="true">
    @foreach($items as $item)
    <tr>
        <td>{{ $item->name }}</td>
        <td>{{ $item->status }}</td>
    </tr>
    @endforeach

    <x-slot name="cards">
        @foreach($items as $item)
        <x-mobile-card :title="$item->name">
            <x-mobile-card-row label="Status" :value="$item->status" />
        </x-mobile-card>
        @endforeach
    </x-slot>
</x-responsive-table>

Loading States

Skeleton Components

Located in resources/views/components/skeletons/:

Card Skeleton

<x-skeletons.card :lines="3" :avatar="true" />

Table Row Skeleton

<x-skeletons.table-row :columns="5" />

Text Skeleton

<x-skeletons.text :lines="3" width="3/4" />

Stat Skeleton

<x-skeletons.stat />

Loading Overlay

<x-loading-overlay message="Saving changes...">
    <div x-data="{ loading: false }">
        {{-- Content that can be overlaid --}}
    </div>
</x-loading-overlay>

Spinner

<x-spinner size="md" color="blue" />
<x-spinner size="sm" color="white" />

Loading Dots

<x-loading-dots text="Loading" size="md" />

Accessibility

Automatically included in layouts:

<x-skip-links />

Provides quick navigation:

  • "Skip to main content" -> #main-content
  • "Skip to navigation" -> #main-navigation

Focus Management

The FocusService (resources/js/services/FocusService.js) provides:

// Trap focus in a modal
const release = window.focusService.trapFocus(modalElement);
release(); // Call to release trap

// Save and restore focus
window.focusService.saveFocus();
// ... after modal closes
window.focusService.restoreFocus();

// Focus first invalid field
window.focusService.focusFirstInvalid(form);

// Announce to screen readers
window.focusService.announce('Item saved', 'polite');

Keyboard Detection

// Automatically adds class when using keyboard
document.body.classList.contains('using-keyboard')

ARIA Attributes

Components include proper ARIA attributes:

<button aria-label="Toggle theme"
        :aria-expanded="open"
        aria-haspopup="true">

Reduced Motion

CSS respects user preference:

@media (prefers-reduced-motion: reduce) {
    .skeleton,
    .hover-lift {
        animation: none !important;
        transition: none !important;
    }
}

Animations

CSS Animation Classes

Located in resources/css/animations.css:

ClassEffect
skeletonShimmer loading effect
hover-liftLift on hover with shadow
press-effectScale down on click
pulse-dotPulsing notification indicator
spinContinuous rotation
shakeError shake animation

Tailwind Animations

Configured in tailwind.config.js:

animation: {
    'shimmer': 'shimmer 1.5s infinite',
    'fade-in': 'fadeIn 150ms ease-out',
    'slide-down': 'slideDown 200ms ease-out',
    'scale-in': 'scaleIn 150ms ease-out',
}

Transition Utilities

.transition-default { @apply transition-all duration-150 ease-in-out; }
.transition-slow { @apply transition-all duration-300 ease-in-out; }
.transition-fast { @apply transition-all duration-75 ease-in-out; }

Admin Navigation

The navigation is sticky and dark-mode aware:

<nav class="bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700 sticky top-0 z-40">

Admin Items: Dashboard, Clients, Projects, Invoices, Reports, More...

Portal Items: Dashboard, Projects, Invoices

Theme Toggle in Navigation

<div class="hidden sm:flex sm:items-center sm:ms-6">
    <x-theme-toggle class="me-2" />
    <x-realtime.notification-bell />
    {{-- User dropdown --}}
</div>

Mobile Navigation

Responsive hamburger menu with smooth transitions:

<button @click="open = !open" class="...">
    <svg :class="{'hidden': open, 'inline-flex': !open}"><!-- hamburger --></svg>
    <svg :class="{'hidden': !open, 'inline-flex': open}"><!-- close --></svg>
</button>

Forms

Input Components

Standard input with dark mode:

<x-ui.input
    name="email"
    type="email"
    :error="$errors->has('email')"
    :disabled="$readonly"
/>

Form Grid

<div class="form-grid">
    <x-ui.input name="first_name" />
    <x-ui.input name="last_name" />
</div>

<div class="form-grid-3">
    <x-ui.input name="city" />
    <x-ui.input name="state" />
    <x-ui.input name="zip" />
</div>

Technical Architecture

CSS Files

FilePurpose
resources/css/app.cssMain entry, imports others
resources/css/theme.cssTheme variables, dark mode
resources/css/responsive.cssResponsive utilities
resources/css/animations.cssAnimations, transitions

JavaScript Services

ServicePurpose
ThemeService.jsTheme management, localStorage
FocusService.jsFocus trapping, accessibility

Component Directories

components/
├── ui/           # Enhanced UI components
│   ├── button.blade.php
│   ├── card.blade.php
│   ├── badge.blade.php
│   └── input.blade.php
├── skeletons/    # Loading skeletons
│   ├── card.blade.php
│   ├── table-row.blade.php
│   ├── text.blade.php
│   └── stat.blade.php
├── theme-toggle.blade.php
├── skip-links.blade.php
├── loading-overlay.blade.php
├── loading-dots.blade.php
├── spinner.blade.php
├── responsive-table.blade.php
├── mobile-card.blade.php
└── mobile-card-row.blade.php

Dependencies

FeatureRelationship
Tailwind CSSStyling framework
Alpine.jsJavaScript interactivity
ViteAsset bundling
BladeTemplate engine

Complementary Features

FeatureDescription
Admin DashboardAdmin UI
Client DashboardPortal UI
RealtimeLive updates

Best Practices

For Dark Mode

  1. Always include dark variants for colors
  2. Test both modes during development
  3. Use CSS variables for consistent theming
  4. Consider image variants for dark mode

For Accessibility

  1. Use semantic HTML (nav, main, article)
  2. Include skip links in layouts
  3. Ensure keyboard navigation works
  4. Maintain WCAG 2.1 AA contrast ratios
  5. Test with screen readers

For Performance

  1. Purge unused CSS in production
  2. Use skeleton states for perceived performance
  3. Minimize JavaScript bundle size
  4. Use SVG icons instead of icon fonts

Troubleshooting

IssueSolution
Dark mode flashesEnsure inline script is before CSS
Theme not persistingCheck localStorage access
Skeleton not animatingCheck reduced-motion setting
Focus trap not workingVerify focusable elements exist

See Also