Files
timebank-cc-public/references/STYLE_GUIDE.md
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

47 KiB

Timebank.cc Style Guide

This document outlines the styling conventions and theme system for the Timebank.cc application. It serves as the authoritative reference for all UI styling patterns used throughout the application.

Reference Implementation: resources/views/mailings/manage.blade.php and resources/views/livewire/mailings/manage.blade.php demonstrate all core UI patterns and should be consulted when building new views.

Theme System Overview

The application uses a sophisticated configuration-based multi-theme system that allows different installations to have unique visual identities. The system is built on four core components:

Core Architecture

  1. Theme Configuration: config/themes.php - Centralized theme definitions with colors, typography, and metadata
  2. Theme Helpers: app/Helpers/ThemeHelper.php - PHP functions for accessing theme data throughout the application
  3. CSS Custom Properties: resources/css/app.css - Automatically generated theme-aware CSS variables
  4. Tailwind Integration: tailwind.config.js - Theme-aware utility classes using CSS custom properties

Available Themes

  1. timebank_cc (default) - Gray, black and white professional theme
  2. uuro - Black, white and cyan theme with vibrant accents (inspired by buuro.net)
  3. vegetable - Green, natural theme with earth tones (inspired by lekkernassuh.org)
  4. yellow - High-contrast yellow and black theme (inspired by yellowbrick.nl)

Themes are activated via the TIMEBANK_THEME environment variable and can be switched per installation without code changes.

Theme Configuration Structure

Each theme in config/themes.php follows this standardized structure:

'theme_name' => [
    'name' => 'Display Name',
    'description' => 'Theme description',
    'colors' => [
        // Primary color scale (required)
        'primary' => [
            50 => '#F6F8F9',    // Lightest
            100 => '#EBEDEE', 
            200 => '#DCDEDF',
            300 => '#CDCFD0',   // Border colors
            400 => '#B8BABB',   // Muted elements
            500 => '#999B9C',   // Main brand color
            600 => '#6E6F70',
            700 => '#434343',
            800 => '#242424',
            900 => '#1A1A1A',   // Darkest, headers
        ],
        
        // Semantic colors (required)
        'secondary' => '#6B7280',   // Secondary brand color
        'accent' => '#434343',      // Accent color for highlights
        'brand' => '#000000',       // Brand/header backgrounds
        'logo' => '#000000',        // Logo and icon colors
        'background' => '#FFFFFF',  // Page background
        'surface' => '#F9FAFB',     // Card/surface backgrounds
        
        // Text colors (required)
        'text' => [
            'primary' => '#111827',   // Main text
            'secondary' => '#6B7280', // Secondary text, subtitles
            'light' => '#9CA3AF',     // Muted text, placeholders
        ],
        
        // Theme-specific colors (optional)
        'success' => '#00D084',        // Uuro: success states
        'danger' => '#CF2E2E',         // Uuro: error states
        'warning' => '#FF6900',        // Uuro: warning states  
        'info' => '#0693E3',           // Uuro: info states
        'surface_alt' => '#E2E2C7',    // Vegetable: alternative surface
        'neutral' => [                 // Yellow: neutral color scale
            'dark' => '#2F2E2E',
            'medium' => '#5D5B5B',
            'light' => '#EFEFEF',
        ],
        'surface_dark' => '#000000',   // Yellow: dark surface
        'accent_dark' => '#001B2F',    // Yellow: dark accent
    ],
    'typography' => [
        // Base typography (required)
        'font_family_body' => 'Roboto, sans-serif',
        'font_family_heading' => 'Oswald, sans-serif',
        'font_size_base' => '16px',
        'line_height_base' => '1.5',
        'heading_transform' => 'uppercase', // or 'none'
        
        // Theme-specific typography (optional)
        'font_family_quote' => 'Georgia, serif',     // Yellow theme
        'line_height_heading' => '1.4',              // Vegetable theme
        'font_sizes' => [                            // Uuro theme
            'small' => '13px',
            'medium' => '20px', 
            'large' => '36px',
            'x-large' => '42px',
        ],
        'heading_sizes' => [                         // Vegetable theme
            'h1' => '23px',
            'h2' => '36px',
            'h3' => '18px', 
            'h4' => '17px',
        ],
        'font_weights' => [                          // Yellow theme
            'regular' => '400',
            'medium' => '500',
            'semibold' => '600',
            'bold' => '700',
            'extrabold' => '800',
        ],
    ],
]

PHP Theme Helper Functions

The app/Helpers/ThemeHelper.php file provides convenient functions for accessing theme data throughout the application:

Core Functions

// Get current theme data
$theme = theme();           // Returns full theme array with 'id' key added
$themeName = theme_name();  // Returns theme display name (e.g., "Uuro")  
$themeId = theme_id();      // Returns theme identifier (e.g., "uuro")

// Get specific theme values
$color = theme_color('primary.500');           // Returns '#64748B' (hex value)
$logoColor = theme_color('logo');              // Returns logo color
$bodyFont = theme_font('font_family_body');    // Returns typography setting

// Get theme values with defaults
$customColor = theme_color('custom.color', '#000000');  // Returns default if not found
$customFont = theme_font('custom_font', 'Arial');       // Returns default if not found

// Theme checking
if (is_theme('vegetable')) {
    // Vegetable theme specific logic
}

// Access nested values
$textColor = theme('colors.text.primary');     // Direct config access
$fontWeight = theme('typography.font_weights.bold'); // Access nested arrays

Dynamic CSS Variable Generation

// Generate CSS custom properties for current theme
$cssVars = theme_css_vars(); 
// Returns: "--color-primary-500: 100 116 139; --color-brand: 25 2 54; --font-family-body: system-ui, -apple-system, sans-serif; ..."

// Helper function for color conversion
$rgbValue = hexToRgb('#64748B');  // Returns "100 116 139" (space-separated RGB)

CSS Architecture

Automatic CSS Custom Property Generation

Colors from config/themes.php are automatically converted to CSS custom properties in RGB format (enabling alpha transparency):

/* Generated automatically via theme_css_vars() function */
[data-theme="uuro"] {
  --color-primary-50: 248 250 252;      /* From #F8FAFC */
  --color-primary-500: 100 116 139;     /* From #64748B */
  --color-primary-900: 15 23 42;        /* From #0F172A */
  --color-brand: 25 2 54;               /* From #190236 */
  --color-logo: 25 2 54;                /* From #190236 */
  --color-background: 255 255 255;      /* From #FFFFFF */
  --color-text-primary: 0 0 0;          /* From #000000 */
  
  /* Typography variables */
  --font-family-body: system-ui, -apple-system, sans-serif;
  --font-family-heading: system-ui, -apple-system, sans-serif;
  --line-height-base: 1.6;
  --heading-transform: none;
}

Tailwind CSS Integration

Theme colors integrate with Tailwind using RGB format with alpha value support:

// tailwind.config.js
colors: {
  // Primary color scale
  primary: {
    50: 'rgb(var(--color-primary-50) / <alpha-value>)',
    500: 'rgb(var(--color-primary-500) / <alpha-value>)',
    900: 'rgb(var(--color-primary-900) / <alpha-value>)',
  },
  
  // Semantic theme colors
  'theme-secondary': 'rgb(var(--color-secondary) / <alpha-value>)',
  'theme-accent': 'rgb(var(--color-accent) / <alpha-value>)',
  'theme-brand': 'rgb(var(--color-brand) / <alpha-value>)',
  'theme-logo': 'rgb(var(--color-logo) / <alpha-value>)',
  'theme-background': 'rgb(var(--color-background) / <alpha-value>)',
  'theme-surface': 'rgb(var(--color-surface) / <alpha-value>)',
  
  'theme-text': {
    primary: 'rgb(var(--color-text-primary) / <alpha-value>)',
    secondary: 'rgb(var(--color-text-secondary) / <alpha-value>)',
    light: 'rgb(var(--color-text-light) / <alpha-value>)',
  },
}

CSS Utility Classes

Additional utility classes are defined in resources/css/app.css:

.bg-theme-brand {
  background-color: rgb(var(--color-brand));
}

.fill-theme-logo {
  fill: rgb(var(--color-logo));
}

.text-theme-logo {
  color: rgb(var(--color-logo));
}

.border-theme-border {
  border-color: rgb(var(--color-primary-300));
}

Color Usage Guidelines

Primary Color Scale Usage

  • 50-200: Light backgrounds, subtle accents, hover states
  • 300-400: Borders, muted elements, disabled states
  • 500: Main brand color, primary buttons, links
  • 600-900: Text colors, dark backgrounds, headers

Semantic Color Guidelines

Background Colors

  • theme-background: Main page background (typically white)
  • theme-surface: Card backgrounds, modal backgrounds, raised surfaces
  • theme-brand: Strong brand elements, page headers, primary buttons

Text Colors

  • theme-text-primary: Main body text, headings
  • theme-text-secondary: Secondary text, captions, metadata
  • theme-text-light: Muted text, placeholders, disabled text

Accent Colors

  • theme-secondary: Secondary brand elements, alternative buttons
  • theme-accent: Highlights, badges, accent elements
  • theme-logo: Logos, icons, brand symbols

Theme-Specific Color Usage

Uuro Theme Status Colors

// Available via theme_color() function
$success = theme_color('success');  // '#00D084' - vivid green cyan
$danger = theme_color('danger');    // '#CF2E2E' - vivid red  
$warning = theme_color('warning');  // '#FF6900' - luminous orange
$info = theme_color('info');        // '#0693E3' - vivid cyan blue

Vegetable Theme Natural Colors

$surfaceAlt = theme_color('surface_alt');  // '#E2E2C7' - light beige alternative

Yellow Theme Neutral System

$darkNeutral = theme_color('neutral.dark');     // '#2F2E2E'
$mediumNeutral = theme_color('neutral.medium'); // '#5D5B5B' 
$lightNeutral = theme_color('neutral.light');   // '#EFEFEF'
$surfaceDark = theme_color('surface_dark');     // '#000000'
$accentDark = theme_color('accent_dark');       // '#001B2F'

UI Component Patterns

This section documents the standard UI patterns used throughout the application, based on the reference implementation in resources/views/livewire/mailings/manage.blade.php.

Page Layout Structure

Standard Management Page Layout

<x-app-layout>
    <x-slot name="header">
            {{ __('Page Title') }}
    </x-slot>

    <div class="py-6">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-theme-background overflow-hidden shadow-xl sm:rounded-lg">
                <div class="p-12 px-6 sm:px-20 bg-theme-background">

                    <!-- Page title and description -->
                    <div class="mt-4 text-2xl">
                        {{ __('Page Heading') }}
                    </div>

                    <div class="mt-6 text-theme-secondary">
                        {{ __('Page description text...') }}
                    </div>

                    <!-- Main content (Livewire component) -->
                    @livewire('component.name')

                </div>
            </div>
        </div>

        <!-- Optional: Scroll-to-top script -->
        @push('scripts')
        <script>
        document.addEventListener('scroll-to-top', event => {
            window.scrollTo({ top: 0, behavior: 'smooth' });
        });
        </script>
        @endpush
    </div>
</x-app-layout>

Key Patterns:

  • Header slot: text-xl text-theme-light leading-tight
  • Container: max-w-7xl mx-auto sm:px-6 lg:px-8
  • Card wrapper: bg-theme-background overflow-hidden shadow-xl sm:rounded-lg
  • Inner padding: p-12 px-6 sm:px-20 bg-theme-background
  • Page title: mt-4 text-2xl
  • Description: mt-6 text-theme-secondary

Action Buttons Section

Top Action Bar with Primary and Danger Actions

<div class="mb-6 flex items-center justify-between">
    <!-- Primary Action (left side) -->
    <x-jetstream.button wire:click="openCreateModal" class="bg-theme-brand hover:bg-opacity-80">
        {{ __('Create Item') }}
    </x-jetstream.button>

    <!-- Destructive Action (right side) -->
    <x-jetstream.danger-button
        :disabled="$bulkDisabled"
        title="{{ __('Delete selection') }}"
        wire:click.prevent="openBulkDeleteModal">
        <x-icon class="mr-3 h-5 w-5" name="trash" />
        {{ __('Delete') }} {{ __('selection') }}
    </x-jetstream.danger-button>
</div>

Key Patterns:

  • Container: mb-6 flex items-center justify-between
  • Primary button: bg-theme-brand hover:bg-opacity-80
  • Icon spacing: mr-3 h-5 w-5
  • Use wire:click for Livewire actions
  • Use :disabled for reactive disabling

Search and Filters

Search Input with Clear Button

<div class="mb-6">
    <div class="mb-4 flex items-center">
        <div class="relative w-1/3">
            <input
                class="w-full rounded-md border border-theme-primary px-3 py-1 pr-10 text-theme-primary shadow-sm focus:border-theme-primary focus:outline-none focus:ring focus:ring-theme-primary sm:text-sm"
                placeholder="{{ __('Search items') . '...' }}"
                type="text"
                wire:model.live="search">

            @if ($search)
                <button
                    class="absolute inset-y-0 right-0 flex items-center pr-3 text-theme-secondary hover:text-theme-primary focus:outline-none"
                    wire:click.prevent="$set('search', '')">
                    <x-icon mini name="backspace" solid />
                </button>
            @endif
        </div>
    </div>
</div>

Key Patterns:

  • Search container: relative w-1/3
  • Input classes: w-full rounded-md border border-theme-primary px-3 py-1 pr-10 text-theme-primary shadow-sm focus:border-theme-primary focus:outline-none focus:ring focus:ring-theme-primary sm:text-sm
  • Clear button: positioned absolutely, inset-y-0 right-0, with text-theme-secondary hover:text-theme-primary
  • Use wire:model.live for real-time search

Filter Dropdowns Row

<div class="mb-4 flex items-center space-x-3">
    <div>
        <x-select
            :clearable="true"
            class="!w-60"
            placeholder="{{ __('Filter by category') }}"
            wire:model.live="categoryFilter">
            <x-select.option label="{{ __('Option 1') }}" value="option1" />
            <x-select.option label="{{ __('Option 2') }}" value="option2" />
        </x-select>
    </div>

    <div>
        <x-select
            :clearable="true"
            class="!w-60"
            placeholder="{{ __('Filter by status') }}"
            wire:model.live="statusFilter">
            <x-select.option label="{{ __('Active') }}" value="active" />
            <x-select.option label="{{ __('Inactive') }}" value="inactive" />
        </x-select>
    </div>
</div>

Key Patterns:

  • Container: mb-4 flex items-center space-x-3
  • Select width: !w-60 (use ! to override default)
  • Always use :clearable="true" for filters
  • Use wire:model.live for immediate filtering

Data Tables

Complete Table Structure

<div class="bg-white shadow-sm rounded-lg overflow-hidden">
    <table class="min-w-full divide-y divide-gray-200">
        <thead class="bg-gray-50">
            <tr>
                <!-- Checkbox column -->
                <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
                    <!-- Optional: Select all checkbox -->
                </th>

                <!-- Sortable column -->
                <th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer align-middle"
                    wire:click="sortBy('title')">
                    <div class="flex items-center space-x-1">
                        <span>{{ __('Title') }}</span>
                        @if($sortField === 'title')
                            <span class="text-gray-400">
                                @if($sortDirection === 'asc') ↑ @else ↓ @endif
                            </span>
                        @endif
                    </div>
                </th>

                <!-- Non-sortable column -->
                <th class="px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider align-middle">
                    {{ __('Column Name') }}
                </th>

                <!-- Actions column (right-aligned) -->
                <th class="px-3 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider align-middle w-32">
                    {{ __('Actions') }}
                </th>
            </tr>
        </thead>
        <tbody class="bg-white divide-y divide-gray-200">
            @forelse($items as $item)
                <tr>
                    <!-- Checkbox cell -->
                    <td class="px-3 py-4 whitespace-nowrap">
                        <input
                            type="checkbox"
                            value="{{ $item->id }}"
                            wire:model.live="bulkSelected"
                            class="rounded border-gray-300 text-theme-brand focus:ring-theme-brand">
                    </td>

                    <!-- Title cell with subtitle -->
                    <td class="px-3 py-4 whitespace-nowrap">
                        <div>
                            <div class="text-sm font-medium text-gray-900">{{ $item->title }}</div>
                            <div class="text-sm text-gray-500">{{ $item->subtitle }}</div>
                        </div>
                    </td>

                    <!-- Standard text cell -->
                    <td class="px-3 py-4 whitespace-nowrap text-sm text-gray-500">
                        {{ $item->description }}
                    </td>

                    <!-- Status badge cell -->
                    <td class="px-3 py-4 whitespace-nowrap">
                        <span class="inline-flex px-2 py-1 text-xs font-medium rounded-full
                            @if($item->status === 'active') bg-green-100 text-green-800
                            @elseif($item->status === 'pending') bg-yellow-100 text-yellow-800
                            @elseif($item->status === 'inactive') bg-gray-100 text-gray-800
                            @else bg-theme-danger-light text-theme-danger-dark @endif">
                            {{ ucfirst($item->status) }}
                        </span>
                    </td>

                    <!-- Date cell (multi-line format) -->
                    <td class="px-3 py-4 text-sm text-gray-500">
                        @if($item->date)
                            <div class="leading-tight">
                                <div>{{ $item->date->format('M j') }}</div>
                                <div>{{ $item->date->format('Y') }}</div>
                                <div class="text-xs">{{ $item->date->format('H:i') }}</div>
                            </div>
                        @else
                            -
                        @endif
                    </td>

                    <!-- Avatar cell -->
                    <td class="px-3 py-4">
                        @if($item->user)
                            <div class="relative block cursor-pointer"
                                onclick="window.location='{{ url('user/' . $item->user->id) }}'"
                                title="{{ $item->user->name }}">
                                <img class="mx-auto h-6 w-6 rounded-full object-cover outline outline-1 outline-offset-0 outline-gray-600"
                                     src="{{ $item->user->avatar_url }}"
                                     alt="profile">
                            </div>
                        @else
                            <span class="text-sm text-gray-400">-</span>
                        @endif
                    </td>

                    <!-- Actions cell -->
                    <td class="px-3 py-4 whitespace-nowrap text-center text-sm font-medium">
                        <div class="flex items-center justify-center space-x-1">
                            <!-- Edit button -->
                            <x-jetstream.secondary-button
                                title="{{ __('Edit') }}"
                                wire:click="openEditModal({{ $item->id }})">
                                <span wire:loading.remove wire:target="openEditModal">
                                    <x-icon class="h-5 w-5" name="pencil-square" />
                                </span>
                                <span wire:loading wire:target="openEditModal">
                                    <svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                                        <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                                        <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                                    </svg>
                                </span>
                            </x-jetstream.secondary-button>

                            <!-- Delete button -->
                            <x-jetstream.danger-button
                                wire:click="delete({{ $item->id }})"
                                title="{{ __('Delete') }}">
                                <span wire:loading.remove wire:target="delete">
                                    <x-icon class="h-5 w-5" name="trash" solid />
                                </span>
                            </x-jetstream.danger-button>
                        </div>
                    </td>
                </tr>
            @empty
                <tr>
                    <td colspan="8" class="px-6 py-12 text-center text-gray-500">
                        {{ __('No items found.') }}
                    </td>
                </tr>
            @endforelse
        </tbody>
    </table>

    <!-- Pagination -->
    @if($items->hasPages())
        <div class="px-6 py-3 border-t border-gray-200">
            {{ $items->links('livewire.long-paginator') }}
        </div>
    @endif
</div>

Key Patterns:

  • Table wrapper: bg-white shadow-sm rounded-lg overflow-hidden
  • Table: min-w-full divide-y divide-gray-200
  • Header: bg-gray-50, headers use px-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider align-middle
  • Sortable headers: add cursor-pointer and wire:click="sortBy('field')"
  • Sort indicators: text-gray-400 with ↑/↓ characters
  • Body: bg-white divide-y divide-gray-200
  • Cells: px-3 py-4 (standard), px-6 py-3 (checkbox column)
  • Action buttons: space-x-1 between buttons
  • Empty state: full colspan, px-6 py-12 text-center text-gray-500
  • Pagination wrapper: px-6 py-3 border-t border-gray-200

Status Badge Patterns

<!-- Success/Active status -->
<span class="inline-flex px-2 py-1 text-xs font-medium rounded-full bg-green-100 text-green-800">
    {{ __('Active') }}
</span>

<!-- Warning/Pending status -->
<span class="inline-flex px-2 py-1 text-xs font-medium rounded-full bg-yellow-100 text-yellow-800">
    {{ __('Pending') }}
</span>

<!-- Neutral/Draft status -->
<span class="inline-flex px-2 py-1 text-xs font-medium rounded-full bg-gray-100 text-gray-800">
    {{ __('Draft') }}
</span>

<!-- Theme-aware status -->
<span class="inline-flex px-2 py-1 text-xs font-medium rounded-full bg-theme-surface text-theme-primary">
    {{ __('Processing') }}
</span>

<!-- Danger/Error status -->
<span class="inline-flex px-2 py-1 text-xs font-medium rounded-full bg-theme-danger-light text-theme-danger-dark">
    {{ __('Failed') }}
</span>

Key Patterns:

  • Base classes: inline-flex px-2 py-1 text-xs font-medium rounded-full
  • Use semantic colors: green (success), yellow (warning), gray (neutral), theme-danger (error)

Modals

Standard Dialog Modal Structure

<x-jetstream.dialog-modal wire:model.live="showModal" wire:key="showModal" maxWidth="2xl">
    <x-slot name="title">
        {{ __('Modal Title') }}
    </x-slot>

    <x-slot name="content">
        <!-- Modal content here -->
        <div class="space-y-6">
            <!-- Form fields, text, etc. -->
        </div>
    </x-slot>

    <x-slot name="footer">
        <x-jetstream.secondary-button wire:click="closeModal">
            {{ __('Cancel') }}
        </x-jetstream.secondary-button>

        <x-jetstream.button wire:click="save" class="ml-3 bg-theme-brand">
            {{ __('Save') }}
        </x-jetstream.button>
    </x-slot>
</x-jetstream.dialog-modal>

Key Patterns:

  • Use wire:model.live for modal visibility
  • Add wire:key to prevent state issues
  • Standard sizes: sm, md, lg, xl, 2xl
  • Footer buttons: Cancel (secondary) on left, Primary action on right with ml-3
  • Primary actions use bg-theme-brand

Confirmation Modal Pattern

<x-jetstream.dialog-modal wire:model.live="showConfirmModal" wire:key="showConfirmModal">
    <x-slot name="title">
        {{ __('Confirm Action') }}
    </x-slot>

    <x-slot name="content">
        {{ __('Are you sure you want to perform this action?') }}<br>
        {{ __('This action cannot be undone.') }}
        <br><br>
        <strong>{{ __('Details') }}: {{ $itemName }}</strong>
    </x-slot>

    <x-slot name="footer">
        <x-jetstream.secondary-button wire:click="$set('showConfirmModal', false)">
            {{ __('Cancel') }}
        </x-jetstream.secondary-button>

        <x-jetstream.danger-button wire:click="confirmAction" class="ml-3">
            {{ __('Confirm') }}
        </x-jetstream.danger-button>
    </x-slot>
</x-jetstream.dialog-modal>

Key Patterns:

  • Destructive actions use danger-button
  • Include warning text and action details
  • Use $set('property', value) for simple state changes

Preview Modal with Iframe

<x-jetstream.dialog-modal wire:model.live="showPreviewModal" wire:key="showPreviewModal" maxWidth="2xl">
    <x-slot name="title">
        {{ __('Preview') }}
    </x-slot>

    <x-slot name="content">
        <div class="space-y-6">
            <!-- Mobile-sized preview frame -->
            <div class="flex justify-center">
                <div class="border border-gray-300 rounded-3xl overflow-hidden bg-white shadow-inner shadow-2xl" style="width: 322px; height: 570px;">
                    <iframe
                        srcdoc="{!! e($previewHtml) !!}"
                        style="width:100%;height:100%;border:0;background:#f4f4f4;"
                    ></iframe>
                </div>
            </div>

            <!-- Additional content below preview -->
            <div class="border-t border-gray-200 pt-6">
                <h3 class="text-sm font-medium text-gray-700 mb-3">{{ __('Additional Options') }}</h3>
                <!-- More content here -->
            </div>
        </div>
    </x-slot>

    <x-slot name="footer">
        <x-jetstream.secondary-button wire:click="closePreview">
            {{ __('Close') }}
        </x-jetstream.secondary-button>
    </x-slot>
</x-jetstream.dialog-modal>

Key Patterns:

  • Mobile preview: width: 322px; height: 570px in rounded-3xl container
  • Sections separated by: border-t border-gray-200 pt-6
  • Section headers: text-sm font-medium text-gray-700 mb-3

Form Elements

Standard Input with Label

<div>
    <label class="block text-sm text-gray-700 mb-2">
        {{ __('Field Label') }}
    </label>
    <x-input
        wire:model="fieldName"
        type="text"
        placeholder="{{ __('Enter value') }}"
        class="w-full"
    />
    @error('fieldName')
        <p class="mt-1 text-sm text-red-600">{{ $message }}</p>
    @enderror
</div>

Key Patterns:

  • Label: block text-sm text-gray-700 mb-2
  • Error message: mt-1 text-sm text-red-600
  • Always include @error directives below inputs

Checkbox with Label

<label class="flex items-center space-x-3">
    <input
        type="checkbox"
        wire:model="fieldName"
        class="rounded border-gray-300 text-theme-brand shadow-sm focus:border-theme-brand focus:ring focus:ring-theme-brand focus:ring-opacity-50"
    >
    <span class="text-sm text-gray-700">
        {{ __('Option label') }}
    </span>
</label>

Key Patterns:

  • Container: flex items-center space-x-3
  • Checkbox classes: rounded border-gray-300 text-theme-brand shadow-sm focus:border-theme-brand focus:ring focus:ring-theme-brand focus:ring-opacity-50
  • Label text: text-sm text-gray-700

Info Box Pattern

<div class="mt-4 p-3 bg-theme-surface rounded-md border border-theme-border">
    <p class="text-sm text-theme-primary">
        <strong>{{ __('Label:') }}</strong> {{ $value }}
    </p>
    <p class="text-sm text-theme-secondary mt-1">
        {{ __('Additional information text.') }}
    </p>
</div>

Key Patterns:

  • Container: p-3 bg-theme-surface rounded-md border border-theme-border
  • Primary text: text-sm text-theme-primary
  • Secondary text: text-sm text-theme-secondary mt-1

Notification Banners and Alerts

Informational Banner (Grayscale)

<div class="mb-6 rounded-md bg-gray-50 border border-gray-300 p-4">
    <div class="flex">
        <div class="flex-shrink-0">
            <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
                <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" />
            </svg>
        </div>
        <div class="ml-3">
            <h3 class="text-sm font-medium text-gray-800">
                {{ __('Banner Heading') }}
            </h3>
            <div class="mt-2 text-sm text-gray-600">
                <p>
                    {{ __('Informational message content goes here. This is a neutral notification.') }}
                </p>
            </div>
        </div>
    </div>
</div>

Key Patterns:

  • Background: bg-gray-50 (professional, non-alarming)
  • Border: border border-gray-300 (subtle, matches table styling)
  • Icon: h-5 w-5 text-gray-400 (muted)
  • Heading: text-sm font-medium text-gray-800
  • Body text: text-sm text-gray-600
  • Use for: maintenance notices, informational messages, neutral alerts

Warning Banner (Grayscale with Warning Icon)

<div class="mb-6 rounded-md bg-gray-50 border border-gray-300 p-4">
    <div class="flex">
        <div class="flex-shrink-0">
            <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
                <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
            </svg>
        </div>
        <div class="ml-3">
            <h3 class="text-sm font-medium text-gray-800">
                {{ __('Site Under Maintenance') }}
            </h3>
            <div class="mt-2 text-sm text-gray-600">
                <p>
                    {{ __('The site is currently undergoing maintenance. Only users with administrator access can log in at this time.') }}
                </p>
            </div>
        </div>
    </div>
</div>

Key Patterns:

  • Same styling as informational banner
  • Use warning triangle icon for cautionary messages
  • Keep grayscale to avoid alarm
  • Use for: system notices, maintenance messages, non-critical warnings

Error Message (Red)

<div class="mb-4 rounded-md bg-red-50 border border-red-200 p-4">
    <div class="flex">
        <div class="flex-shrink-0">
            <svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
                <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
            </svg>
        </div>
        <div class="ml-3">
            <div class="text-sm text-red-800">
                <p>{{ __('Login is currently disabled due to site maintenance.') }}</p>
            </div>
        </div>
    </div>
</div>

Key Patterns:

  • Background: bg-red-50
  • Border: border border-red-200
  • Icon: h-5 w-5 text-red-400 (X circle for errors)
  • Text: text-sm text-red-800
  • Use for: actual errors, failed actions, critical issues

Success Message (Green)

<div class="mb-4 rounded-md bg-green-50 border border-green-200 p-4">
    <div class="flex">
        <div class="flex-shrink-0">
            <svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
                <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
            </svg>
        </div>
        <div class="ml-3">
            <div class="text-sm text-green-800">
                <p>{{ __('Your changes have been saved successfully.') }}</p>
            </div>
        </div>
    </div>
</div>

Key Patterns:

  • Background: bg-green-50
  • Border: border border-green-200
  • Icon: h-5 w-5 text-green-400 (checkmark circle)
  • Text: text-sm text-green-800
  • Use for: successful operations, confirmations

Banner/Alert Usage Guidelines:

  • Grayscale: Default for informational, maintenance, or neutral system messages
  • Red: Only for actual errors or failed actions
  • Green: Only for successful completions
  • Yellow: Avoid; use grayscale for warnings instead (more professional)
  • Icon size: Always h-5 w-5
  • Icon position: Always in flex-shrink-0 container with ml-3 for text
  • Common SVG icons: info circle, warning triangle, X circle, checkmark circle

Buttons and Actions

Button Component Usage

<!-- Primary action button -->
<x-jetstream.button wire:click="action" class="bg-theme-brand hover:bg-opacity-80">
    {{ __('Primary Action') }}
</x-jetstream.button>

<!-- Secondary action button -->
<x-jetstream.secondary-button wire:click="action" title="{{ __('Tooltip') }}">
    <x-icon class="h-5 w-5" name="icon-name" />
</x-jetstream.secondary-button>

<!-- Danger/destructive button -->
<x-jetstream.danger-button wire:click="delete" :disabled="$isDisabled">
    <x-icon class="mr-3 h-5 w-5" name="trash" />
    {{ __('Delete') }}
</x-jetstream.danger-button>

<!-- Light button (for pagination, filters) -->
<x-jetstream.light-button wire:click="action">
    {{ __('Light Action') }}
</x-jetstream.light-button>

Key Patterns:

  • Primary buttons: bg-theme-brand hover:bg-opacity-80
  • Icon-only buttons: use title attribute for accessibility
  • Icons with text: mr-3 for spacing
  • Icon size: h-5 w-5 (standard), h-6 w-6 (large)
  • Use :disabled for reactive disable state

Loading State Pattern

<x-jetstream.button
    wire:click="action"
    wire:loading.attr="disabled"
    wire:target="action"
    class="bg-theme-brand">
    <span wire:loading.remove wire:target="action">
        {{ __('Submit') }}
    </span>
    <span wire:loading wire:target="action">
        {{ __('Processing...') }}
    </span>
</x-jetstream.button>

OR with spinner:

<x-jetstream.secondary-button wire:click="action">
    <span wire:loading.remove wire:target="action">
        <x-icon class="h-5 w-5" name="icon-name" />
    </span>
    <span wire:loading wire:target="action">
        <svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
            <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
            <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
        </svg>
    </span>
</x-jetstream.secondary-button>

Key Patterns:

  • Use wire:loading.attr="disabled" to prevent double-clicks
  • Use wire:target to scope loading state to specific action
  • Provide text feedback OR spinner during loading
  • Spinner classes: animate-spin h-5 w-5 text-white

Spacing and Layout

Standard Spacing Scale

  • Section spacing: mt-12 (top margin for major sections)
  • Element spacing: mb-6 (bottom margin for groups), mb-4 (between related items)
  • Inline spacing: space-x-1 (tight), space-x-3 (standard), mr-3 (icon before text)
  • Content spacing: space-y-6 (form sections), space-y-3 (form fields)
  • Padding: p-3 (small boxes), p-6 (standard), p-12 (page content)

Styling Best Practices

1. Use Theme-Aware Classes

Recommended:

<div class="bg-theme-brand text-theme-text-primary">
<button class="border-primary-300 bg-theme-surface hover:bg-primary-50">
<header class="bg-primary-900 text-white">

Avoid:

<div class="bg-black text-gray-900">
<button class="border-gray-300 bg-white hover:bg-gray-50">  
<header class="bg-gray-900 text-white">

2. Consistent Component Styling

Form Components

<input class="border border-primary-300 bg-theme-background rounded-md shadow-sm focus:border-primary-500">
<select class="border border-primary-300 bg-theme-background rounded-md">

Cards and Surfaces

<div class="bg-theme-surface rounded-lg shadow border border-primary-200">
<div class="bg-theme-background p-6 rounded-md">

Headers and Navigation

<header class="bg-theme-brand text-white">
<nav class="bg-primary-900 text-theme-text-primary">

3. Semi-Transparent Overlays

Use modern Tailwind opacity syntax for better performance:

<!-- Recommended -->
<div class="bg-theme-brand/60">        <!-- 60% opacity -->
<div class="bg-primary-500/25">        <!-- 25% opacity -->

<!-- Avoid -->
<div class="bg-theme-brand bg-opacity-60">
<div class="bg-primary-500 bg-opacity-25">

4. Logo and Icon Theming

SVG elements should use theme-aware classes:

<svg class="fill-theme-logo w-8 h-8">
  <!-- SVG paths automatically use theme logo color -->
</svg>

<i class="text-theme-logo text-xl"></i>  <!-- Icon fonts -->

Component-Specific Guidelines

Amount Input Component

Unified wrapper approach for multi-part inputs:

<div class="amount-wrapper flex items-center rounded-md border border-primary-300 bg-theme-background shadow-sm overflow-hidden {{ $maxLengthHoursInput > 4 ? 'w-full' : 'w-40' }}">
  <span class="text-theme-text-secondary"></span>
  <input class="border-0 focus:ring-0">
</div>

Event Cards

Full-height cards with image backgrounds:

<div class="relative flex h-[430px] flex-col justify-end bg-theme-background">
  <!-- Image background -->
  <div class="absolute inset-0 z-0 h-full w-full">
    <img class="absolute inset-0 z-0 h-full w-full object-cover">
  </div>
  
  <!-- Semi-transparent overlay -->
  <div class="absolute inset-0 z-10 bg-black/50"></div>
  
  <!-- Fallback gradient -->
  <div class="absolute inset-0 z-0 bg-gradient-to-br from-theme-secondary to-theme-brand"></div>
  
  <!-- Content -->
  <div class="relative z-20 p-14 text-white">
    <h2 class="text-4xl font-semibold">Event Title</h2>
  </div>
</div>

Typography System

Font Family Configuration

Access theme fonts using helper functions:

// In PHP/Livewire components
$bodyFont = theme_font('font_family_body');     // 'Roboto, sans-serif'
$headingFont = theme_font('font_family_heading'); // 'Oswald, sans-serif'  
$quoteFont = theme_font('font_family_quote');   // 'Georgia, serif' (Yellow theme)

Text Transform Rules

  • Timebank_cc: Uppercase headings (heading_transform: 'uppercase')
  • Other themes: Normal case headings (heading_transform: 'none')
// Conditional styling based on theme
if (is_theme('timebank_cc')) {
    $headingClass = 'uppercase';
} else {
    $headingClass = 'normal-case';
}

Development Workflow

Adding New Theme Colors

  1. Add color to theme configuration in config/themes.php:
'themes' => [
    'theme_name' => [
        'colors' => [
            'new-color' => '#FF0000',           // Single color
            'new-scale' => [                   // Color scale  
                50 => '#FFF5F5',
                500 => '#FF0000',
                900 => '#7F1D1D',
            ],
        ],
    ],
]
  1. CSS custom properties are automatically generated via theme_css_vars() function (no manual CSS needed)

  2. Add Tailwind utility classes in tailwind.config.js:

colors: {
  'theme-new-color': 'rgb(var(--color-new-color) / <alpha-value>)',
  'new-scale': {
    50: 'rgb(var(--color-new-scale-50) / <alpha-value>)',
    500: 'rgb(var(--color-new-scale-500) / <alpha-value>)',
    900: 'rgb(var(--color-new-scale-900) / <alpha-value>)',
  },
}
  1. Create additional CSS utility classes if needed in resources/css/app.css:
.bg-theme-new-color {
  background-color: rgb(var(--color-new-color));
}

.border-new-color-500 {
  border-color: rgb(var(--color-new-scale-500));
}
  1. Use in PHP/Livewire components:
$newColor = theme_color('new-color');        // Returns '#FF0000'
$lightShade = theme_color('new-scale.50');   // Returns '#FFF5F5'
$darkShade = theme_color('new-scale.900');   // Returns '#7F1D1D'

Testing Themes

Test theme functionality using helper functions:

# Test individual theme colors
TIMEBANK_THEME=timebank_cc php artisan tinker --execute="echo 'Timebank_cc primary: ' . theme_color('primary.500');"
TIMEBANK_THEME=uuro php artisan tinker --execute="echo 'Uuro primary: ' . theme_color('primary.500');"
TIMEBANK_THEME=vegetable php artisan tinker --execute="echo 'Vegetable primary: ' . theme_color('primary.500');"
TIMEBANK_THEME=yellow php artisan tinker --execute="echo 'Yellow primary: ' . theme_color('primary.500');"

# Test theme metadata
php artisan tinker --execute="echo 'Current theme: ' . theme_name() . ' (' . theme_id() . ')';"

# Test theme-specific colors
TIMEBANK_THEME=uuro php artisan tinker --execute="echo 'Uuro success: ' . theme_color('success');"
TIMEBANK_THEME=vegetable php artisan tinker --execute="echo 'Vegetable surface_alt: ' . theme_color('surface_alt');"

# Test typography
php artisan tinker --execute="echo 'Body font: ' . theme_font('font_family_body');"

# Required after config changes
php artisan config:clear

Environment Setup

# Set theme in .env file
TIMEBANK_THEME=uuro

# Clear Laravel config cache (required after .env changes)
php artisan config:clear

# Rebuild frontend assets with theme
npm run build

Theme Switching Workflow

# Switch to different theme
echo "TIMEBANK_THEME=vegetable" >> .env

# Clear caches
php artisan config:clear
php artisan view:clear

# Rebuild assets
npm run build

# Test theme colors
php artisan tinker --execute="echo theme_name() . ': ' . theme_color('primary.500');"

File Structure

config/
  themes.php                           # Main theme configuration file
  
app/  
  Helpers/
    ThemeHelper.php                    # Theme helper functions and utilities
    
resources/
  css/
    app.css                           # CSS custom properties and utility classes
  views/
    layouts/
      app.blade.php                   # Theme data-attribute integration
    components/
      jetstream/
        application-logo.blade.php    # Theme-aware main logo
        authentication-card-logo.blade.php  # Theme-aware auth logo
        
tailwind.config.js                   # Tailwind theme color integration
references/
  THEME_IMPLEMENTATION.md            # Detailed implementation documentation

Migration from Hard-coded Styles

1. Identify Hard-coded Colors

Class Name Conversions:

  • bg-blackbg-theme-brand
  • text-gray-900text-theme-text-primary
  • border-gray-300border-primary-300
  • bg-whitebg-theme-background
  • bg-gray-50bg-theme-surface

2. Use Theme Helper Functions in PHP

Replace hard-coded values with theme helpers:

// Before: Hard-coded values
$primaryColor = '#999B9C';
$backgroundColor = '#FFFFFF';
$textColor = '#111827';

// After: Theme helpers
$primaryColor = theme_color('primary.500');
$backgroundColor = theme_color('background');  
$textColor = theme_color('text.primary');

3. Conditional Theme Logic

Handle theme-specific differences:

// Theme-specific behavior
if (is_theme('vegetable')) {
    $surfaceColor = theme_color('surface_alt');  // Light beige
    $lineHeight = theme_font('line_height_base'); // 1.8 for readability
} else {
    $surfaceColor = theme_color('surface');      // Standard surface
    $lineHeight = theme_font('line_height_base'); // Theme default
}

// Theme-specific status colors
if (is_theme('uuro')) {
    $successColor = theme_color('success');      // Vivid green cyan
    $dangerColor = theme_color('danger');        // Vivid red
} else {
    $successColor = '#22c55e';                   // Fallback green
    $dangerColor = '#ef4444';                    // Fallback red
}

4. Component Migration Process

  1. Audit component for hard-coded colors and styles
  2. Replace classes with theme-aware equivalents
  3. Add PHP logic for theme-specific behavior using helper functions
  4. Test across all themes using environment variable switching
  5. Verify accessibility and contrast ratios

Accessibility Guidelines

Color Contrast Requirements

  • Maintain WCAG AA contrast ratios (4.5:1) across all themes
  • Test text readability on all background combinations
  • Verify logo visibility on different header backgrounds

Theme Testing Checklist

  • Form elements clearly distinguishable in all themes
  • Focus states visible with theme colors
  • Error states readable with theme danger colors
  • Interactive elements have sufficient contrast
  • Logo/icons visible on theme backgrounds

Performance Considerations

CSS Custom Properties Benefits

  • Efficient theme switching without rebuilding CSS
  • Single CSS bundle supports all themes
  • Runtime theme changes possible (if needed)
  • Excellent browser support and performance

Optimization Guidelines

  • Use shared utility classes when possible
  • Minimize theme-specific CSS overrides
  • Leverage Tailwind's purging for unused theme classes
  • Keep theme configurations lean and focused

Build Performance

  • Themes are compiled at build time for optimal performance
  • CSS custom properties enable single bundle for all themes
  • No runtime theme computation overhead