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

1334 lines
47 KiB
Markdown

# 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:
```php
'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
```php
// 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
```php
// 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):
```css
/* 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:
```javascript
// 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`:
```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
```php
// 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
```php
$surfaceAlt = theme_color('surface_alt'); // '#E2E2C7' - light beige alternative
```
#### Yellow Theme Neutral System
```php
$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
```html
<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
```html
<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
```html
<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
```html
<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
```html
<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
```html
<!-- 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
```html
<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
```html
<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
```html
<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
```html
<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
```html
<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
```html
<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)
```html
<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)
```html
<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)
```html
<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)
```html
<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
```html
<!-- 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
```html
<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:
```html
<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:**
```html
<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:**
```html
<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
```html
<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
```html
<div class="bg-theme-surface rounded-lg shadow border border-primary-200">
<div class="bg-theme-background p-6 rounded-md">
```
#### Headers and Navigation
```html
<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:
```html
<!-- 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:
```html
<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:
```html
<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:
```html
<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:
```php
// 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'`)
```php
// 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`:
```php
'themes' => [
'theme_name' => [
'colors' => [
'new-color' => '#FF0000', // Single color
'new-scale' => [ // Color scale
50 => '#FFF5F5',
500 => '#FF0000',
900 => '#7F1D1D',
],
],
],
]
```
2. **CSS custom properties are automatically generated** via `theme_css_vars()` function (no manual CSS needed)
3. **Add Tailwind utility classes** in `tailwind.config.js`:
```javascript
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>)',
},
}
```
4. **Create additional CSS utility classes** if needed in `resources/css/app.css`:
```css
.bg-theme-new-color {
background-color: rgb(var(--color-new-color));
}
.border-new-color-500 {
border-color: rgb(var(--color-new-scale-500));
}
```
5. **Use in PHP/Livewire components**:
```php
$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:
```bash
# 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
```bash
# 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
```bash
# 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-brand`
- `text-gray-900``text-theme-text-primary`
- `border-gray-300``border-primary-300`
- `bg-white``bg-theme-background`
- `bg-gray-50``bg-theme-surface`
### 2. Use Theme Helper Functions in PHP
Replace hard-coded values with theme helpers:
```php
// 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:
```php
// 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