Preskoči na sadržaj

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>

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>

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>

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