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
- Theme Configuration:
config/themes.php- Centralized theme definitions with colors, typography, and metadata - Theme Helpers:
app/Helpers/ThemeHelper.php- PHP functions for accessing theme data throughout the application - CSS Custom Properties:
resources/css/app.css- Automatically generated theme-aware CSS variables - Tailwind Integration:
tailwind.config.js- Theme-aware utility classes using CSS custom properties
Available Themes
- timebank_cc (default) - Gray, black and white professional theme
- uuro - Black, white and cyan theme with vibrant accents (inspired by buuro.net)
- vegetable - Green, natural theme with earth tones (inspired by lekkernassuh.org)
- 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 surfacestheme-brand: Strong brand elements, page headers, primary buttons
Text Colors
theme-text-primary: Main body text, headingstheme-text-secondary: Secondary text, captions, metadatatheme-text-light: Muted text, placeholders, disabled text
Accent Colors
theme-secondary: Secondary brand elements, alternative buttonstheme-accent: Highlights, badges, accent elementstheme-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:clickfor Livewire actions - Use
:disabledfor 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, withtext-theme-secondary hover:text-theme-primary - Use
wire:model.livefor 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.livefor 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 usepx-3 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider align-middle - Sortable headers: add
cursor-pointerandwire:click="sortBy('field')" - Sort indicators:
text-gray-400with ↑/↓ 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-1between 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.livefor modal visibility - Add
wire:keyto 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: 570pxinrounded-3xlcontainer - 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
@errordirectives 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-3for 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
titleattribute for accessibility - Icons with text:
mr-3for spacing - Icon size:
h-5 w-5(standard),h-6 w-6(large) - Use
:disabledfor 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:targetto 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
- 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',
],
],
],
]
-
CSS custom properties are automatically generated via
theme_css_vars()function (no manual CSS needed) -
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>)',
},
}
- 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));
}
- 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-black→bg-theme-brandtext-gray-900→text-theme-text-primaryborder-gray-300→border-primary-300bg-white→bg-theme-backgroundbg-gray-50→bg-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
- Audit component for hard-coded colors and styles
- Replace classes with theme-aware equivalents
- Add PHP logic for theme-specific behavior using helper functions
- Test across all themes using environment variable switching
- 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