Shared UI Components¶
This document defines the reusable UI components shared between the Admin Portal and Quota Portal. Components are built with Twig templates and styled using Tailwind CSS.
Tech Stack¶
- Templating: Twig
- CSS Framework: Tailwind CSS
- JavaScript: Alpine.js (for interactive components)
- Icons: Heroicons or similar SVG icon set
Layout Components¶
Page Layout¶
Standard page structure with sidebar navigation and content area.
<!-- templates/layouts/portal.html.twig -->
<div class="min-h-screen bg-gray-100">
<!-- Top Navigation -->
<nav class="bg-white shadow-sm border-b border-gray-200">
<div class="px-4 py-3 flex justify-between items-center">
<div class="flex items-center gap-4">
<img src="/logo.svg" alt="HNS" class="h-8">
<span class="text-lg font-semibold text-gray-900">Admin Portal</span>
</div>
<div class="flex items-center gap-4">
<span class="text-sm text-gray-600">{{ user.email }}</span>
<button class="text-sm text-red-700 hover:text-red-800">Logout</button>
</div>
</div>
</nav>
<div class="flex">
<!-- Sidebar -->
<aside class="w-64 bg-white shadow-sm min-h-screen">
{% include 'components/sidebar.html.twig' %}
</aside>
<!-- Main Content -->
<main class="flex-1 p-6">
{% block content %}{% endblock %}
</main>
</div>
</div>
Sidebar Navigation¶
Collapsible sidebar with grouped navigation items.
<!-- templates/components/sidebar.html.twig -->
<nav class="py-4">
{% for group in navigation %}
<div class="px-4 py-2">
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider">
{{ group.label }}
</h3>
</div>
<ul class="space-y-1">
{% for item in group.items %}
<li>
<a href="{{ item.url }}"
class="flex items-center px-4 py-2 text-sm {% if item.active %}
bg-red-50 text-red-700 border-r-2 border-red-700
{% else %}
text-gray-700 hover:bg-gray-50
{% endif %}">
{{ item.icon|raw }}
<span class="ml-3">{{ item.label }}</span>
{% if item.badge %}
<span class="ml-auto bg-red-100 text-red-700 text-xs px-2 py-0.5 rounded-full">
{{ item.badge }}
</span>
{% endif %}
</a>
</li>
{% endfor %}
</ul>
{% endfor %}
</nav>
Breadcrumbs¶
Navigation breadcrumb trail.
<!-- templates/components/breadcrumbs.html.twig -->
<nav class="flex mb-4" aria-label="Breadcrumb">
<ol class="flex items-center space-x-2">
{% for crumb in breadcrumbs %}
<li class="flex items-center">
{% if not loop.first %}
<svg class="w-4 h-4 text-gray-400 mx-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
</svg>
{% endif %}
{% if crumb.url %}
<a href="{{ crumb.url }}" class="text-sm text-gray-500 hover:text-gray-700">
{{ crumb.label }}
</a>
{% else %}
<span class="text-sm text-gray-900 font-medium">{{ crumb.label }}</span>
{% endif %}
</li>
{% endfor %}
</ol>
</nav>
Data Display¶
Data Table¶
Sortable, filterable data table with pagination.
Features: - Sortable column headers - Filter row with search/dropdowns - Pagination controls - Row selection checkboxes - Row action buttons - Export button
<!-- templates/components/data-table.html.twig -->
<div class="bg-white shadow rounded-lg overflow-hidden">
<!-- Table Header with Filters -->
<div class="px-4 py-3 border-b border-gray-200 bg-gray-50">
<div class="flex flex-wrap items-center justify-between gap-4">
<!-- Search -->
<div class="flex-1 min-w-64">
<input type="text" placeholder="Search..."
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm
focus:ring-red-500 focus:border-red-500">
</div>
<!-- Filters -->
<div class="flex items-center gap-2">
<select class="px-3 py-2 border border-gray-300 rounded-md text-sm">
<option value="">All Statuses</option>
{% for status in statuses %}
<option value="{{ status.value }}">{{ status.label }}</option>
{% endfor %}
</select>
<input type="date" class="px-3 py-2 border border-gray-300 rounded-md text-sm">
</div>
<!-- Actions -->
<div class="flex items-center gap-2">
<button class="px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 rounded-md">
Export CSV
</button>
</div>
</div>
</div>
<!-- Table -->
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
{% if selectable %}
<th class="px-4 py-3 w-10">
<input type="checkbox" class="rounded border-gray-300 text-red-700
focus:ring-red-500">
</th>
{% endif %}
{% for column in columns %}
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider
{% if column.sortable %}cursor-pointer hover:bg-gray-100{% endif %}">
<div class="flex items-center gap-1">
{{ column.label }}
{% if column.sortable %}
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"/>
</svg>
{% endif %}
</div>
</th>
{% endfor %}
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">
Actions
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for row in rows %}
<tr class="hover:bg-gray-50">
{% if selectable %}
<td class="px-4 py-3">
<input type="checkbox" value="{{ row.id }}"
class="rounded border-gray-300 text-red-700 focus:ring-red-500">
</td>
{% endif %}
{% for cell in row.cells %}
<td class="px-4 py-3 text-sm text-gray-900">
{{ cell|raw }}
</td>
{% endfor %}
<td class="px-4 py-3 text-right text-sm">
{% for action in row.actions %}
<a href="{{ action.url }}"
class="text-red-700 hover:text-red-900 ml-3">
{{ action.label }}
</a>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Pagination -->
<div class="px-4 py-3 border-t border-gray-200 bg-gray-50">
<div class="flex items-center justify-between">
<div class="text-sm text-gray-500">
Showing {{ pagination.from }} to {{ pagination.to }} of {{ pagination.total }} results
</div>
<div class="flex items-center gap-1">
<button class="px-3 py-1 text-sm border border-gray-300 rounded-md
hover:bg-gray-100 disabled:opacity-50">
Previous
</button>
{% for page in pagination.pages %}
<button class="px-3 py-1 text-sm rounded-md
{% if page == pagination.current %}
bg-red-700 text-white
{% else %}
border border-gray-300 hover:bg-gray-100
{% endif %}">
{{ page }}
</button>
{% endfor %}
<button class="px-3 py-1 text-sm border border-gray-300 rounded-md
hover:bg-gray-100 disabled:opacity-50">
Next
</button>
</div>
</div>
</div>
</div>
Stats Cards¶
Dashboard statistics display.
<!-- templates/components/stats-cards.html.twig -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{% for stat in stats %}
<div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">{{ stat.label }}</p>
<p class="text-2xl font-semibold text-gray-900 mt-1">{{ stat.value }}</p>
{% if stat.change %}
<p class="text-sm mt-2 {% if stat.change > 0 %}text-green-600{% else %}text-red-600{% endif %}">
{{ stat.change > 0 ? '+' : '' }}{{ stat.change }}% from yesterday
</p>
{% endif %}
</div>
<div class="p-3 rounded-full {{ stat.bgColor ?? 'bg-gray-100' }}">
{{ stat.icon|raw }}
</div>
</div>
</div>
{% endfor %}
</div>
Status Badges¶
Color-coded status indicators.
<!-- templates/components/badge.html.twig -->
{% set colors = {
'success': 'bg-green-100 text-green-800',
'warning': 'bg-yellow-100 text-yellow-800',
'error': 'bg-red-100 text-red-800',
'info': 'bg-blue-100 text-blue-800',
'neutral': 'bg-gray-100 text-gray-800',
'purple': 'bg-purple-100 text-purple-800',
'orange': 'bg-orange-100 text-orange-800',
'cyan': 'bg-cyan-100 text-cyan-800'
} %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ colors[variant] ?? colors.neutral }}">
{% if dot %}
<span class="w-2 h-2 rounded-full mr-1.5
{% if variant == 'success' %}bg-green-400
{% elseif variant == 'warning' %}bg-yellow-400
{% elseif variant == 'error' %}bg-red-400
{% else %}bg-gray-400{% endif %}"></span>
{% endif %}
{{ label }}
</span>
Status Badge Mappings¶
| Entity | Status | Variant | Color |
|---|---|---|---|
| Match | Draft | neutral | Gray |
| Published | success | Green | |
| Cancelled | error | Red | |
| Closed | info | Blue | |
| Ticket | Available | success | Green |
| Reserved | warning | Yellow | |
| Sold | info | Blue | |
| Blocked | error | Red | |
| Technical | purple | Purple | |
| Official | orange | Orange | |
| Allocated | cyan | Cyan | |
| Maintenance | neutral | Gray | |
| Quota | Active | success | Green |
| Past Deadline | warning | Yellow | |
| Cancelled | error | Red | |
| Quota Ticket | ALLOCATED | warning | Yellow |
| RESERVED | info | Blue | |
| SOLD | success | Green | |
| Delegated | cyan | Cyan |
Form Components¶
Input Field¶
Standard text input with label and validation.
<!-- templates/components/form/input.html.twig -->
<div class="mb-4">
<label for="{{ id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ label }}
{% if required %}
<span class="text-red-500">*</span>
{% endif %}
</label>
<input type="{{ type ?? 'text' }}"
id="{{ id }}"
name="{{ name }}"
value="{{ value }}"
placeholder="{{ placeholder }}"
{% if required %}required{% endif %}
class="w-full px-3 py-2 border rounded-md text-sm
{% if error %}
border-red-500 focus:ring-red-500 focus:border-red-500
{% else %}
border-gray-300 focus:ring-red-500 focus:border-red-500
{% endif %}">
{% if error %}
<p class="mt-1 text-sm text-red-500">{{ error }}</p>
{% endif %}
{% if hint %}
<p class="mt-1 text-sm text-gray-500">{{ hint }}</p>
{% endif %}
</div>
Select Dropdown¶
Single and multi-select dropdowns.
<!-- templates/components/form/select.html.twig -->
<div class="mb-4">
<label for="{{ id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ label }}
{% if required %}
<span class="text-red-500">*</span>
{% endif %}
</label>
<select id="{{ id }}"
name="{{ name }}"
{% if multiple %}multiple{% endif %}
{% if required %}required{% endif %}
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm
focus:ring-red-500 focus:border-red-500">
{% if placeholder %}
<option value="">{{ placeholder }}</option>
{% endif %}
{% for option in options %}
<option value="{{ option.value }}"
{% if option.value == value %}selected{% endif %}>
{{ option.label }}
</option>
{% endfor %}
</select>
</div>
Date/Time Picker¶
Date and datetime inputs.
<!-- templates/components/form/datetime.html.twig -->
<div class="mb-4">
<label for="{{ id }}" class="block text-sm font-medium text-gray-700 mb-1">
{{ label }}
{% if required %}
<span class="text-red-500">*</span>
{% endif %}
</label>
<input type="{{ includeTime ? 'datetime-local' : 'date' }}"
id="{{ id }}"
name="{{ name }}"
value="{{ value }}"
{% if min %}min="{{ min }}"{% endif %}
{% if max %}max="{{ max }}"{% endif %}
{% if required %}required{% endif %}
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm
focus:ring-red-500 focus:border-red-500">
</div>
Checkbox¶
Single checkbox with label.
<!-- templates/components/form/checkbox.html.twig -->
<div class="flex items-start mb-4">
<input type="checkbox"
id="{{ id }}"
name="{{ name }}"
value="{{ value ?? '1' }}"
{% if checked %}checked{% endif %}
class="h-4 w-4 mt-0.5 text-red-700 border-gray-300 rounded
focus:ring-red-500">
<label for="{{ id }}" class="ml-2 text-sm text-gray-700">
{{ label }}
{% if hint %}
<span class="block text-gray-500">{{ hint }}</span>
{% endif %}
</label>
</div>
Radio Group¶
Radio button options.
<!-- templates/components/form/radio-group.html.twig -->
<fieldset class="mb-4">
<legend class="block text-sm font-medium text-gray-700 mb-2">
{{ label }}
{% if required %}
<span class="text-red-500">*</span>
{% endif %}
</legend>
<div class="space-y-2">
{% for option in options %}
<div class="flex items-start">
<input type="radio"
id="{{ id }}_{{ option.value }}"
name="{{ name }}"
value="{{ option.value }}"
{% if option.value == value %}checked{% endif %}
class="h-4 w-4 mt-0.5 text-red-700 border-gray-300
focus:ring-red-500">
<label for="{{ id }}_{{ option.value }}" class="ml-2 text-sm text-gray-700">
{{ option.label }}
{% if option.description %}
<span class="block text-gray-500">{{ option.description }}</span>
{% endif %}
</label>
</div>
{% endfor %}
</div>
</fieldset>
File Upload¶
File input with drag-and-drop zone.
<!-- templates/components/form/file-upload.html.twig -->
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ label }}
</label>
<div class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center
hover:border-gray-400 transition-colors cursor-pointer"
x-data="{ dragging: false }"
@dragover.prevent="dragging = true"
@dragleave="dragging = false"
@drop.prevent="dragging = false; handleDrop($event)"
:class="{ 'border-red-500 bg-red-50': dragging }">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
</svg>
<p class="mt-2 text-sm text-gray-600">
<span class="font-medium text-red-700">Click to upload</span> or drag and drop
</p>
<p class="mt-1 text-xs text-gray-500">{{ acceptText ?? 'CSV, Excel up to 10MB' }}</p>
<input type="file"
name="{{ name }}"
accept="{{ accept }}"
class="hidden">
</div>
</div>
Modal Components¶
Confirmation Modal¶
Standard confirmation dialog.
<!-- templates/components/modal/confirm.html.twig -->
<div x-data="{ open: false }"
x-show="open"
x-cloak
class="fixed inset-0 z-50 overflow-y-auto"
@open-confirm.window="open = true; message = $event.detail.message">
<!-- Backdrop -->
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"
@click="open = false"></div>
<!-- Modal -->
<div class="flex min-h-screen items-center justify-center p-4">
<div class="relative bg-white rounded-lg shadow-xl max-w-md w-full p-6">
<div class="flex items-center gap-4">
<div class="flex-shrink-0 w-10 h-10 rounded-full bg-red-100 flex items-center justify-center">
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<div>
<h3 class="text-lg font-medium text-gray-900" x-text="title"></h3>
<p class="mt-1 text-sm text-gray-500" x-text="message"></p>
</div>
</div>
<div class="mt-6 flex justify-end gap-3">
<button @click="open = false"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white
border border-gray-300 rounded-md hover:bg-gray-50">
Cancel
</button>
<button @click="$dispatch('confirm'); open = false"
class="px-4 py-2 text-sm font-medium text-white bg-red-700
rounded-md hover:bg-red-800">
Confirm
</button>
</div>
</div>
</div>
</div>
Detail Modal¶
Modal for viewing details without leaving page.
<!-- templates/components/modal/detail.html.twig -->
<div x-data="{ open: false }"
x-show="open"
x-cloak
class="fixed inset-0 z-50 overflow-y-auto">
<div class="fixed inset-0 bg-black bg-opacity-50" @click="open = false"></div>
<div class="flex min-h-screen items-center justify-center p-4">
<div class="relative bg-white rounded-lg shadow-xl max-w-2xl w-full">
<!-- Header -->
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900">{{ title }}</h3>
<button @click="open = false" class="text-gray-400 hover:text-gray-500">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
<!-- Content -->
<div class="px-6 py-4">
{% block modal_content %}{% endblock %}
</div>
<!-- Footer -->
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50 flex justify-end gap-3">
{% block modal_actions %}
<button @click="open = false"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white
border border-gray-300 rounded-md hover:bg-gray-50">
Close
</button>
{% endblock %}
</div>
</div>
</div>
</div>
Button Components¶
Primary Button¶
Main call-to-action button.
<!-- Primary (HNS Red) -->
<button class="px-4 py-2 text-sm font-medium text-white bg-red-700
rounded-md hover:bg-red-800 focus:outline-none focus:ring-2
focus:ring-offset-2 focus:ring-red-500
disabled:opacity-50 disabled:cursor-not-allowed">
{{ label }}
</button>
Secondary Button¶
Secondary action button.
<!-- Secondary (outline) -->
<button class="px-4 py-2 text-sm font-medium text-gray-700 bg-white
border border-gray-300 rounded-md hover:bg-gray-50
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
{{ label }}
</button>
Danger Button¶
Destructive action button.
<!-- Danger (for destructive actions) -->
<button class="px-4 py-2 text-sm font-medium text-white bg-red-600
rounded-md hover:bg-red-700 focus:outline-none focus:ring-2
focus:ring-offset-2 focus:ring-red-500">
{{ label }}
</button>
Button Sizes¶
<!-- Small -->
<button class="px-3 py-1.5 text-xs ...">Small</button>
<!-- Medium (default) -->
<button class="px-4 py-2 text-sm ...">Medium</button>
<!-- Large -->
<button class="px-6 py-3 text-base ...">Large</button>
Step Indicator¶
Multi-step process progress indicator.
<!-- templates/components/step-indicator.html.twig -->
<nav class="mb-8">
<ol class="flex items-center">
{% for step in steps %}
<li class="relative {% if not loop.last %}flex-1{% endif %}">
<div class="flex items-center">
<!-- Step circle -->
<div class="flex items-center justify-center w-10 h-10 rounded-full
{% if step.status == 'complete' %}
bg-red-700 text-white
{% elseif step.status == 'current' %}
border-2 border-red-700 text-red-700
{% else %}
border-2 border-gray-300 text-gray-500
{% endif %}">
{% if step.status == 'complete' %}
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
{% else %}
{{ step.number }}
{% endif %}
</div>
<!-- Connector line -->
{% if not loop.last %}
<div class="flex-1 ml-4 h-0.5
{% if step.status == 'complete' %}bg-red-700{% else %}bg-gray-300{% endif %}">
</div>
{% endif %}
</div>
<!-- Step label -->
<div class="mt-2">
<span class="text-sm font-medium
{% if step.status == 'current' %}text-red-700{% else %}text-gray-500{% endif %}">
{{ step.label }}
</span>
</div>
</li>
{% endfor %}
</ol>
</nav>
Alert Components¶
Alert Box¶
Contextual alert messages.
<!-- templates/components/alert.html.twig -->
{% set styles = {
'info': 'bg-blue-50 border-blue-400 text-blue-700',
'success': 'bg-green-50 border-green-400 text-green-700',
'warning': 'bg-yellow-50 border-yellow-400 text-yellow-700',
'error': 'bg-red-50 border-red-400 text-red-700'
} %}
<div class="border-l-4 p-4 mb-4 {{ styles[variant] }}">
<div class="flex">
<div class="flex-shrink-0">
<!-- Icon based on variant -->
</div>
<div class="ml-3">
{% if title %}
<h3 class="text-sm font-medium">{{ title }}</h3>
{% endif %}
<p class="text-sm {% if title %}mt-1{% endif %}">{{ message }}</p>
</div>
{% if dismissible %}
<button class="ml-auto -mr-1 text-current opacity-50 hover:opacity-75">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
{% endif %}
</div>
</div>
Loading States¶
Spinner¶
Loading indicator.
<!-- templates/components/spinner.html.twig -->
<svg class="animate-spin h-5 w-5 text-red-700" 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>
Skeleton Loader¶
Content placeholder while loading.
<!-- templates/components/skeleton.html.twig -->
<div class="animate-pulse">
<div class="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
<div class="h-4 bg-gray-200 rounded w-1/2"></div>
</div>
Card Component¶
Content container card.
<!-- templates/components/card.html.twig -->
<div class="bg-white shadow rounded-lg overflow-hidden">
{% if header %}
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">{{ header }}</h3>
</div>
{% endif %}
<div class="px-6 py-4">
{% block card_content %}{% endblock %}
</div>
{% if footer %}
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50">
{% block card_footer %}{% endblock %}
</div>
{% endif %}
</div>
Tab Navigation¶
Tab-based content switching.
<!-- templates/components/tabs.html.twig -->
<div x-data="{ activeTab: '{{ defaultTab }}' }">
<!-- Tab Headers -->
<div class="border-b border-gray-200">
<nav class="flex -mb-px">
{% for tab in tabs %}
<button @click="activeTab = '{{ tab.id }}'"
:class="activeTab === '{{ tab.id }}'
? 'border-red-700 text-red-700'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
class="px-4 py-3 text-sm font-medium border-b-2 transition-colors">
{{ tab.label }}
{% if tab.badge %}
<span class="ml-2 px-2 py-0.5 text-xs rounded-full bg-gray-100">
{{ tab.badge }}
</span>
{% endif %}
</button>
{% endfor %}
</nav>
</div>
<!-- Tab Panels -->
<div class="py-4">
{% for tab in tabs %}
<div x-show="activeTab === '{{ tab.id }}'" x-cloak>
{% block tab_content %}{% endblock %}
</div>
{% endfor %}
</div>
</div>
Tailwind Color Palette¶
Primary Colors (HNS Brand)¶
| Use | Tailwind Class | Hex |
|---|---|---|
| Primary | bg-red-700 |
#B91C1C |
| Primary Hover | bg-red-800 |
#991B1B |
| Primary Light | bg-red-50 |
#FEF2F2 |
| Primary Text | text-red-700 |
#B91C1C |
Status Colors¶
| Status | Background | Text |
|---|---|---|
| Success | bg-green-100 |
text-green-800 |
| Warning | bg-yellow-100 |
text-yellow-800 |
| Error | bg-red-100 |
text-red-800 |
| Info | bg-blue-100 |
text-blue-800 |
| Neutral | bg-gray-100 |
text-gray-800 |
Last Updated: January 2026