printshopcreator/src/new/docs/design-system.md
2025-12-31 16:47:12 +01:00

81 KiB

PrintshopCreator Design System

Version: 1.0 Letzte Aktualisierung: 2025-12-22 Status: Living Document


Inhaltsverzeichnis

  1. Einleitung
  2. Architektur
  3. Farbsystem
  4. Typografie
  5. Spacing & Layout
  6. Komponenten-Bibliothek
  7. Layout-Patterns
  8. Code-Standards
  9. Barrierefreiheit
  10. Migration Guide

Einleitung

Zweck

Dieses Design System definiert die visuellen und funktionalen Standards für alle Module im PrintshopCreator Backend. Es gewährleistet:

  • Konsistenz: Einheitliches Erscheinungsbild über alle 32 Bundles hinweg
  • Effizienz: Wiederverwendbare Komponenten reduzieren Entwicklungszeit
  • Wartbarkeit: Zentrale Standards erleichtern Updates und Änderungen
  • User Experience: Konsistente Interaktionsmuster verbessern die Benutzerfreundlichkeit

Zielgruppe

  • Frontend-Entwickler
  • Backend-Entwickler (Symfony/Twig)
  • UI/UX Designer
  • Product Owner

Architektur

Tech Stack

Technologie Version Verwendung
Tailwind CSS 3.x Primäres CSS-Framework
Alpine.js 3.x UI State Management
Symfony 6.x Backend Framework
Twig 3.x Template Engine
Font Awesome 5.7.2 Icon Library

Framework-Strategie

Primär: Tailwind CSS

  • Alle neuen Komponenten und Module verwenden Tailwind CSS
  • Utility-First Ansatz für maximale Flexibilität
  • Custom PSC-Farbschema über Tailwind-Konfiguration

Legacy: Bootstrap 4/5

  • Wird schrittweise durch Tailwind ersetzt
  • Keine neuen Bootstrap-Komponenten mehr erstellen
  • Bestehende Bootstrap-Komponenten bei Änderungen zu Tailwind migrieren

Dateistruktur

templates/
├── backend_tailwind_base.html.twig    # Primäres Base-Template (verwenden!)
├── backend_base.html.twig             # Legacy Bootstrap (nicht verwenden)
├── form_div_layout_tailwind.html.twig # Form Theme (verwenden!)
└── [bundle]/
    └── backend/
        ├── edit.html.twig
        ├── list.html.twig
        └── components/

assets/
├── tailwind/
│   ├── backend.css                    # Hauptstyles
│   └── login.css
└── backend/dashboard/                 # Legacy (nicht verwenden)

Farbsystem

Primäre Farbpalette

PSC Brand Color

/* Primary Brand Color */
--color-psc-50:  #f0f9ff;
--color-psc-100: #e0f2fe;
--color-psc-200: #bae6fd;
--color-psc-300: #7dd3fc;
--color-psc-400: #38bdf8;
--color-psc-500: #0ea5e9;  /* Haupt-Markenfarbe */
--color-psc-600: #0284c7;
--color-psc-700: #0369a1;
--color-psc-800: #075985;
--color-psc-900: #0c4a6e;

Tailwind-Klassen: bg-psc-500, text-psc-600, border-psc-400, etc.

Status-Farben

/* Success - Grün */
--color-success: #10b981;      /* bg-green-500 */
--color-success-light: #d1fae5; /* bg-green-100 */

/* Warning - Orange/Gelb */
--color-warning: #f59e0b;      /* bg-orange-500 */
--color-warning-light: #fef3c7; /* bg-orange-100 */

/* Error - Rot */
--color-error: #ef4444;        /* bg-red-500 */
--color-error-light: #fee2e2;   /* bg-red-100 */

/* Info - Blau */
--color-info: #3b82f6;         /* bg-blue-500 */
--color-info-light: #dbeafe;    /* bg-blue-100 */

Neutrale Farben

/* Graustufen */
--color-gray-50:  #f9fafb;
--color-gray-100: #f3f4f6;
--color-gray-200: #e5e7eb;
--color-gray-300: #d1d5db;
--color-gray-500: #6b7280;
--color-gray-700: #374151;
--color-gray-900: #111827;

Dark Mode

/* Dark Mode Variablen */
dark:bg-boxdark         /* #24303f */
dark:bg-strokedark      /* #2e3a47 */
dark:text-bodydark      /* #aeb7c0 */

Verwendung:

<div class="bg-white dark:bg-boxdark">
  <h2 class="text-gray-900 dark:text-bodydark">Titel</h2>
</div>

Badge-Farben

WICHTIG: Für Ja/Nein-Anzeigen immer badge-yes und badge-no Klassen verwenden!

Typ Klasse Farbe Verwendung
Ja / Aktiv badge-yes Grün (bg-lime-500 + weiß) true, ja, aktiv, enabled
Nein / Inaktiv badge-no Orange (bg-orange-500 + weiß) false, nein, inaktiv, disabled
Info - bg-blue-500 + text-white Information, Hinweis
Draft - bg-yellow-100 + text-yellow-800 Entwurf, In Bearbeitung

Typografie

Schriftarten

/* System Font Stack */
font-family: system-ui, -apple-system, BlinkMacSystemFont,
             "Segoe UI", Roboto, "Helvetica Neue", Arial,
             sans-serif;

Tailwind-Klasse: font-sans (Standard)

Schriftgrößen

Größe Tailwind Pixel Verwendung
XS text-xs 12px Labels, Badges, Hinweise
SM text-sm 14px Body Text, Formulare
Base text-base 16px Standard Body Text
LG text-lg 18px Hervorgehobener Text
XL text-xl 20px Überschriften (H3)
2XL text-2xl 24px Überschriften (H2)
3XL text-3xl 30px Seitentitel (H1)

Schriftgewichte

Gewicht Tailwind Wert Verwendung
Normal font-normal 400 Body Text
Medium font-medium 500 Buttons, Labels
Semibold font-semibold 600 Wichtige Labels
Bold font-bold 700 Überschriften

Typografie-Beispiele

<!-- Seitentitel -->
<h1 class="text-3xl font-bold text-gray-900 dark:text-bodydark">
  Seitentitel
</h1>

<!-- Abschnitt-Überschrift -->
<h2 class="text-xl font-medium text-gray-900 dark:text-bodydark mb-4">
  Abschnittstitel
</h2>

<!-- Form Label -->
<label class="block uppercase text-xs font-bold mb-2 text-gray-700">
  Feldname
</label>

<!-- Body Text -->
<p class="text-sm text-gray-600 dark:text-bodydark">
  Beschreibungstext
</p>

Spacing & Layout

Spacing-System

Tailwind verwendet ein 4px-basiertes Spacing-System:

Wert Pixel Verwendung
0.5 2px Minimaler Abstand
1 4px Sehr enger Abstand
2 8px Enger Abstand
4 16px Standard Abstand
6 24px Mittlerer Abstand
8 32px Großer Abstand
12 48px Sehr großer Abstand

Layout-Konstanten

/* Sidebar */
--sidebar-width: 20rem;              /* 320px */
--collapsed-sidebar-width: 5.4rem;   /* 86px */

/* Container */
--max-content-width: 1280px;         /* max-w-7xl */

Responsive Breakpoints

/* Tailwind Breakpoints */
sm:  640px   /* Tablet Portrait */
md:  768px   /* Tablet Landscape */
lg:  1024px  /* Desktop */
xl:  1280px  /* Large Desktop */
2xl: 1536px  /* Extra Large Desktop */

Grid-System

<!-- Standard 12-Spalten Grid -->
<div class="grid grid-cols-12 gap-4">
  <div class="col-span-12 md:col-span-6 lg:col-span-4">
    <!-- Content -->
  </div>
</div>

<!-- Flexbox Layout -->
<div class="flex flex-col md:flex-row gap-4">
  <div class="flex-1"><!-- Content --></div>
  <div class="flex-1"><!-- Content --></div>
</div>

Komponenten-Bibliothek

1. Buttons

WICHTIG: Einheitliche Rundung - Alle interaktiven Elemente verwenden rounded-md (6px)

Primary Button (Haupt-Aktion)

<button class="inline-flex items-center justify-center gap-2 px-4 py-2
               text-sm font-medium text-white
               bg-psc-500 hover:bg-psc-600
               rounded-md shadow-lg
               hover:ring-2 hover:ring-psc-500 hover:ring-offset-1
               transition-all duration-200
               disabled:opacity-50 disabled:cursor-not-allowed">
  <svg class="w-4 h-4"><!-- Icon --></svg>
  Speichern
</button>

Verwendung: Primäre Aktionen (Speichern, Erstellen, Absenden)

CSS-Klasse: .psc-button-save (bereits definiert in backend.css)

Secondary Button

<button class="inline-flex items-center justify-center gap-2 px-4 py-2
               text-sm font-medium text-psc-600
               bg-white hover:bg-gray-50
               border border-psc-500
               rounded-md shadow-sm
               hover:ring-2 hover:ring-psc-500 hover:ring-offset-1
               transition-all duration-200">
  Abbrechen
</button>

Verwendung: Sekundäre Aktionen (Abbrechen, Zurück)

Danger Button

<button class="inline-flex items-center justify-center gap-2 px-4 py-2
               text-sm font-medium text-white
               bg-red-500 hover:bg-red-600
               rounded-md shadow-lg
               hover:ring-2 hover:ring-red-500 hover:ring-offset-1
               transition-all duration-200">
  <svg class="w-4 h-4"><!-- Icon --></svg>
  Löschen
</button>

Verwendung: Destruktive Aktionen (Löschen, Entfernen)

<a href="#" class="text-sm text-psc-500 hover:text-psc-600
                   hover:underline transition-colors">
  Bearbeiten
</a>

Verwendung: Tertäre Aktionen, Links

Button-Gruppe

<div class="inline-flex rounded-md shadow-sm" role="group">
  <button class="px-4 py-2 text-sm font-medium text-gray-900 bg-white
                 border border-gray-200 rounded-l-md hover:bg-gray-100">
    Links
  </button>
  <button class="px-4 py-2 text-sm font-medium text-gray-900 bg-white
                 border-t border-b border-gray-200 hover:bg-gray-100">
    Mitte
  </button>
  <button class="px-4 py-2 text-sm font-medium text-gray-900 bg-white
                 border border-gray-200 rounded-r-md hover:bg-gray-100">
    Rechts
  </button>
</div>

2. Cards

Standard Card

<div class="rounded-md border border-gray-200 bg-white
            shadow-lg dark:border-strokedark dark:bg-boxdark">
  <!-- Header (optional) -->
  <div class="border-b border-gray-200 px-7.5 py-4
              dark:border-strokedark">
    <h3 class="text-xl font-medium text-gray-900 dark:text-bodydark">
      Card Titel
    </h3>
  </div>

  <!-- Body -->
  <div class="px-7.5 py-6">
    <p class="text-sm text-gray-600 dark:text-bodydark">
      Card Content
    </p>
  </div>

  <!-- Footer (optional) -->
  <div class="border-t border-gray-200 px-7.5 py-4
              dark:border-strokedark">
    <div class="flex justify-end gap-2">
      <button class="...">Abbrechen</button>
      <button class="...">Speichern</button>
    </div>
  </div>
</div>

Compact Card

<div class="rounded-md border border-gray-200 bg-white p-4
            shadow-sm hover:shadow-md transition-shadow
            dark:border-strokedark dark:bg-boxdark">
  <h4 class="text-base font-medium mb-2">Titel</h4>
  <p class="text-sm text-gray-600">Content</p>
</div>

Dashboard Stat Card

<div class="rounded-md border border-gray-200 bg-white px-7.5 py-6
            shadow-lg dark:border-strokedark dark:bg-boxdark">
  <div class="flex items-center justify-between">
    <div>
      <h4 class="text-sm font-medium text-gray-600 dark:text-bodydark">
        Statistik Name
      </h4>
      <span class="text-3xl font-bold text-gray-900 dark:text-white mt-2">
        1,234
      </span>
    </div>
    <div class="flex h-12 w-12 items-center justify-center
                rounded-full bg-psc-100">
      <svg class="w-6 h-6 text-psc-500"><!-- Icon --></svg>
    </div>
  </div>
  <span class="text-sm text-gray-500 mt-2">
    +12% seit letztem Monat
  </span>
</div>

3. Forms

Text Input

<div class="mb-4">
  <label class="block uppercase text-xs font-bold mb-2 text-gray-700
                dark:text-bodydark" for="field-id">
    Feldname
  </label>
  <input type="text" id="field-id" name="field-name"
         class="w-full px-3 py-2 text-sm
                border border-gray-300 rounded-md
                focus:outline-none focus:ring-2 focus:ring-psc-500
                focus:border-transparent
                dark:bg-boxdark dark:border-strokedark dark:text-bodydark
                disabled:bg-gray-100 disabled:cursor-not-allowed"
         placeholder="Platzhalter">
  <p class="mt-1 text-xs text-gray-500">Hilfetext (optional)</p>
</div>

Select / Dropdown

<div class="mb-4">
  <label class="block uppercase text-xs font-bold mb-2 text-gray-700">
    Auswahl
  </label>
  <select class="w-full px-3 py-2 text-sm
                 border border-gray-300 rounded-md
                 focus:outline-none focus:ring-2 focus:ring-psc-500
                 focus:border-transparent
                 dark:bg-boxdark dark:border-strokedark">
    <option>Option 1</option>
    <option>Option 2</option>
    <option>Option 3</option>
  </select>
</div>

Checkbox

<div class="flex items-center mb-4">
  <input type="checkbox" id="checkbox-id"
         class="w-4 h-4 text-psc-500 border-gray-300 rounded
                focus:ring-2 focus:ring-psc-500">
  <label for="checkbox-id" class="ml-2 text-sm text-gray-700">
    Checkbox Label
  </label>
</div>

Toggle Switch

<label class="flex items-center cursor-pointer">
  <div class="relative">
    <input type="checkbox" class="sr-only peer">
    <div class="w-11 h-6 bg-gray-200 rounded-full peer
                peer-checked:bg-psc-500
                peer-focus:ring-2 peer-focus:ring-psc-500
                transition-colors"></div>
    <div class="absolute left-1 top-1 w-4 h-4 bg-white rounded-full
                peer-checked:translate-x-5 transition-transform"></div>
  </div>
  <span class="ml-3 text-sm text-gray-700">Toggle Label</span>
</label>

Form Validation States

<!-- Error State -->
<div class="mb-4">
  <label class="block uppercase text-xs font-bold mb-2 text-gray-700">
    Feldname
  </label>
  <input type="text"
         class="w-full px-3 py-2 text-sm
                border-2 border-red-500 rounded-md
                focus:outline-none focus:ring-2 focus:ring-red-500">
  <p class="mt-1 text-xs text-red-500">
    <svg class="w-4 h-4 inline"><!-- Error Icon --></svg>
    Dieses Feld ist erforderlich
  </p>
</div>

<!-- Success State -->
<div class="mb-4">
  <label class="block uppercase text-xs font-bold mb-2 text-gray-700">
    Feldname
  </label>
  <input type="text"
         class="w-full px-3 py-2 text-sm
                border-2 border-green-500 rounded-md
                focus:outline-none focus:ring-2 focus:ring-green-500">
  <p class="mt-1 text-xs text-green-500">
    <svg class="w-4 h-4 inline"><!-- Success Icon --></svg>
    Eingabe gültig
  </p>
</div>

4. Tables

Standard Datentabelle

WICHTIG: Neue verbesserte Table-Header Styles

Design-Specs für Table-Header:

  • Background: bg-slate-50 (heller als Body) / dark:bg-gray-800
  • Border: border-b-2 border-gray-200 (doppelte Stärke für Trennung)
  • Text: text-xs font-semibold text-gray-600 uppercase tracking-wider
  • Padding: px-4 py-4 (mehr vertikales Padding als Body)
  • Font-Weight: font-semibold (600) statt font-medium

Design-Specs für Table-Body:

  • Border: border-t border-gray-100 (subtiler als Header)
  • Padding: px-4 py-3 (konsistent mit Header horizontal)
  • Hover: hover:bg-gray-50 dark:hover:bg-gray-800/50
  • Transition: transition-colors für smooth Hover-Effekte
  • Text: text-gray-900 dark:text-gray-100 für normale Zellen
<div class="overflow-x-auto">
  <table class="min-w-full text-sm">
    <!-- Header mit verbessertem Design -->
    <thead class="bg-slate-50 dark:bg-gray-800">
      <tr class="border-b-2 border-gray-200 dark:border-gray-700">
        <th class="px-4 py-4 text-left text-xs font-semibold text-gray-600
                   uppercase tracking-wider dark:text-gray-300">
          <a href="?sort=name" class="hover:text-psc-500">
            Name
            <svg class="w-4 h-4 inline"><!-- Sort Icon --></svg>
          </a>
        </th>
        <th class="px-4 py-4 text-left text-xs font-semibold text-gray-600
                   uppercase tracking-wider dark:text-gray-300">
          Status
        </th>
        <th class="px-4 py-4 text-right text-xs font-semibold text-gray-600
                   uppercase tracking-wider dark:text-gray-300">
          Aktionen
        </th>
      </tr>
    </thead>

    <!-- Body -->
    <tbody class="bg-white dark:bg-boxdark">
      <tr class="border-t border-gray-100 hover:bg-gray-50
                 dark:border-gray-700 dark:hover:bg-gray-800/50
                 transition-colors">
        <td class="px-4 py-3 text-gray-900 dark:text-gray-100">
          Beispiel Name
        </td>
        <td class="px-4 py-3">
          <span class="badge-yes">Aktiv</span>
        </td>
        <td class="px-4 py-3 text-right">
          <div class="flex gap-2 justify-end">
            <!-- Bearbeiten - GRÜN -->
            <a href="#" title="Bearbeiten">
              <svg xmlns="http://www.w3.org/2000/svg" fill="none"
                   viewBox="0 0 24 24" stroke-width="1.5"
                   stroke="currentColor"
                   class="table-icon text-green-600 hover:text-green-700">
                <path stroke-linecap="round" stroke-linejoin="round"
                      d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
              </svg>
            </a>
            <!-- Löschen - ROT -->
            <a href="#" title="Löschen"
               onclick="return confirm('Wirklich löschen?')">
              <svg xmlns="http://www.w3.org/2000/svg" fill="none"
                   viewBox="0 0 24 24" stroke-width="1.5"
                   stroke="currentColor"
                   class="table-icon text-red-600 hover:text-red-700">
                <path stroke-linecap="round" stroke-linejoin="round"
                      d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
              </svg>
            </a>
          </div>
        </td>
      </tr>
    </tbody>
  </table>
</div>

Responsive Table (Mobile-Friendly)

<div class="overflow-x-auto">
  <table class="min-w-full">
    <thead class="hidden md:table-header-group">
      <!-- Header nur auf Desktop -->
    </thead>
    <tbody>
      <!-- Auf Mobile: Card-Layout -->
      <tr class="block md:table-row border-b md:border-0">
        <td class="block md:table-cell px-4 py-2" data-label="Name:">
          <span class="font-bold md:hidden">Name: </span>
          Beispiel
        </td>
        <td class="block md:table-cell px-4 py-2" data-label="Status:">
          <span class="font-bold md:hidden">Status: </span>
          Aktiv
        </td>
      </tr>
    </tbody>
  </table>
</div>

UID/ID Badges in Tabellen

Design für technische IDs/UIDs als klickbare Badges

<!-- UID als klickbarer Badge -->
<td class="px-4 py-3">
  <a href="/edit/123"
     class="inline-flex items-center px-2.5 py-1 rounded-md
            text-xs font-mono font-medium
            bg-gray-100 text-gray-800 border border-gray-200
            hover:bg-psc-50 hover:text-psc-700 hover:border-psc-300
            transition-colors
            dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600
            dark:hover:bg-psc-900 dark:hover:text-psc-300">
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
         stroke-width="1.5" stroke="currentColor"
         class="w-3.5 h-3.5 mr-1.5 text-gray-500 dark:text-gray-400">
      <path stroke-linecap="round" stroke-linejoin="round"
            d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5l-3.9 19.5m-2.1-19.5l-3.9 19.5" />
    </svg>
    123
  </a>
</td>

Nicht-klickbare UID (falls kein Link benötigt):

<td class="px-4 py-3">
  <span class="inline-flex items-center px-2.5 py-1 rounded-md
               text-xs font-mono font-medium
               bg-gray-100 text-gray-800 border border-gray-200
               dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600">
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
         stroke-width="1.5" stroke="currentColor"
         class="w-3.5 h-3.5 mr-1.5 text-gray-500 dark:text-gray-400">
      <path stroke-linecap="round" stroke-linejoin="round"
            d="M5.25 8.25h15m-16.5 7.5h15m-1.8-13.5l-3.9 19.5m-2.1-19.5l-3.9 19.5" />
    </svg>
    ABC-123
  </span>
</td>

Design-Specs für UID/ID Badges:

  • Font: font-mono (Monospace für technische Werte)
  • Größe: text-xs (12px)
  • Padding: px-2.5 py-1
  • Rundung: rounded-md (mittlere Rundung)
  • Icon: Hash (#) Symbol, w-3.5 h-3.5
  • Hover: PSC-Farbe bei Links
  • Border: Subtiler Border für bessere Abgrenzung

In Twig:

{# Klickbare UID #}
<a href="{{ path('route_edit', {id: item.id}) }}"
   class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-mono font-medium bg-gray-100 text-gray-800 border border-gray-200 hover:bg-psc-50 hover:text-psc-700 hover:border-psc-300 transition-colors dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600">
    <svg class="w-3.5 h-3.5 mr-1.5 text-gray-500"><!-- Hash Icon --></svg>
    {{ item.id }}
</a>

Tabellen-Aktionen mit Icons

WICHTIG: Einheitliche Aktions-Icon-Farben

Alle Aktions-Icons in Tabellen müssen folgende Farben verwenden:

Aktion Farbe Tailwind-Klasse Verwendung
Löschen Rot text-red-600 Alle Delete/Remove-Aktionen
Bearbeiten Grün text-green-600 Alle Edit/Update-Aktionen
Alle anderen Blau text-blue-600 View, Download, Info, etc.

Icon-Größe: Alle Tabellen-Icons sollten die Klasse table-icon (entspricht w-6) verwenden.

<td class="px-6 py-4 whitespace-nowrap text-right">
  <div class="flex gap-2 justify-end">

    <!-- Bearbeiten (GRÜN) -->
    <a href="/edit/123" title="Bearbeiten">
      <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
           stroke-width="1.5" stroke="currentColor"
           class="table-icon text-green-600 hover:text-green-700">
        <path stroke-linecap="round" stroke-linejoin="round"
              d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
      </svg>
    </a>

    <!-- Details anzeigen (BLAU) -->
    <a href="/view/123" title="Details anzeigen">
      <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
           stroke-width="1.5" stroke="currentColor"
           class="table-icon text-blue-600 hover:text-blue-700">
        <path stroke-linecap="round" stroke-linejoin="round"
              d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
        <path stroke-linecap="round" stroke-linejoin="round"
              d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
      </svg>
    </a>

    <!-- Download (BLAU) -->
    <a href="/download/123" target="_blank" title="Download">
      <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
           stroke-width="1.5" stroke="currentColor"
           class="table-icon text-blue-600 hover:text-blue-700">
        <path stroke-linecap="round" stroke-linejoin="round"
              d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
      </svg>
    </a>

    <!-- Löschen (ROT) -->
    <a href="/delete/123" title="Löschen"
       onclick="return confirm('Wirklich löschen?')">
      <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
           stroke-width="1.5" stroke="currentColor"
           class="table-icon text-red-600 hover:text-red-700">
        <path stroke-linecap="round" stroke-linejoin="round"
              d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
      </svg>
    </a>

  </div>
</td>

Weitere Icon-Aktionen (alle BLAU):

  • Info/Hilfe: text-blue-600
  • Kopieren/Duplizieren: text-blue-600
  • Export: text-blue-600
  • Drucken: text-blue-600
  • Verschieben: text-blue-600
  • Teilen: text-blue-600

5. Badges & Tags

Ja/Nein Status Badges

WICHTIG: Einheitliche Ja/Nein-Anzeige

Für Ja/Nein-Anzeigen (Boolean-Werte) in Tabellen und Listen müssen immer die vordefinierten Klassen verwendet werden:

Status Klasse Farbe Verwendung
Ja badge-yes Grün (bg-lime-500) true, aktiv, ja, enabled
Nein badge-no Orange (bg-orange-500) false, inaktiv, nein, disabled
<!-- JA Badge (GRÜN) - Vordefinierte Klasse verwenden! -->
<span class="badge-yes">Ja</span>
<!-- Rendert als: rounded-full, text-xs, bg-lime-500, px-2 py-1 -->

<!-- NEIN Badge (ORANGE) - Vordefinierte Klasse verwenden! -->
<span class="badge-no">Nein</span>
<!-- Rendert als: rounded-full, text-xs, bg-orange-500, px-2 py-1 -->

In Twig Templates:

{# Boolean-Wert anzeigen #}
{% if item.active %}
    <span class="badge-yes">{{'yes'|trans}}</span>
{% else %}
    <span class="badge-no">{{'no'|trans}}</span>
{% endif %}

{# In Tabellen #}
<td class="px-6 py-4 whitespace-nowrap">
    {% if product.isPublished %}
        <span class="badge-yes">Veröffentlicht</span>
    {% else %}
        <span class="badge-no">Entwurf</span>
    {% endif %}
</td>

CSS-Definition (bereits in assets/tailwind/css/backend.css):

.badge-yes {
    @apply inline-flex items-center rounded-full bg-lime-500 px-2 py-1
           text-xs font-medium text-white hover:bg-opacity-90
           justify-center shadow-xl;
}

.badge-no {
    @apply inline-flex items-center rounded-full bg-orange-500 px-2 py-1
           text-xs font-medium text-white hover:bg-opacity-90
           justify-center shadow-xl;
}

Design-Specs:

  • Rundung: rounded-full (vollständig abgerundet)
  • Schriftgröße: text-xs (12px)
  • Padding: px-2 py-1 (horizontal 8px, vertikal 4px)
  • Font-Weight: font-medium (500)

Weitere Status Badges

<!-- Info Badge -->
<span class="inline-flex items-center px-2 py-1 rounded-full
             text-xs font-medium bg-blue-500 text-white shadow-xl">
  Information
</span>

<!-- Warning Badge -->
<span class="inline-flex items-center px-2 py-1 rounded-full
             text-xs font-medium bg-yellow-500 text-white shadow-xl">
  Warnung
</span>

<!-- Success Badge -->
<span class="inline-flex items-center px-2 py-1 rounded-full
             text-xs font-medium bg-green-500 text-white shadow-xl">
  Erfolgreich
</span>

<!-- Draft Badge (mit hellem Hintergrund) -->
<span class="inline-flex items-center px-2 py-1 rounded-full
             text-xs font-medium bg-yellow-100 text-yellow-800">
  Entwurf
</span>

<!-- Danger Badge -->
<span class="inline-flex items-center px-2 py-1 rounded-full
             text-xs font-medium bg-red-500 text-white shadow-xl">
  Fehler
</span>

WICHTIG: Alle Status-Badges verwenden:

  • rounded-full für vollständige Rundung
  • text-xs für einheitliche Schriftgröße (12px)
  • px-2 py-1 für konsistentes Padding

Count Badge

<button class="relative">
  Benachrichtigungen
  <span class="absolute -top-2 -right-2
               inline-flex items-center justify-center
               w-5 h-5 text-xs font-bold text-white
               bg-red-500 rounded-full">
    3
  </span>
</button>

6. Alerts & Flash Messages

Success Alert

<div class="flex items-center p-4 mb-4 text-sm text-green-800
            bg-green-100 rounded-md border border-green-200
            dark:bg-green-900 dark:text-green-200"
     role="alert">
  <svg class="w-5 h-5 mr-3 flex-shrink-0"><!-- Success Icon --></svg>
  <div>
    <span class="font-medium">Erfolgreich!</span>
    Ihre Änderungen wurden gespeichert.
  </div>
  <button type="button" class="ml-auto text-green-800 hover:text-green-900">
    <svg class="w-4 h-4"><!-- Close Icon --></svg>
  </button>
</div>

Error Alert

<div class="flex items-center p-4 mb-4 text-sm text-red-800
            bg-red-100 rounded-md border border-red-200"
     role="alert">
  <svg class="w-5 h-5 mr-3 flex-shrink-0"><!-- Error Icon --></svg>
  <div>
    <span class="font-medium">Fehler!</span>
    Es ist ein Problem aufgetreten.
  </div>
</div>

Warning Alert

<div class="flex items-center p-4 mb-4 text-sm text-orange-800
            bg-orange-100 rounded-md border border-orange-200"
     role="alert">
  <svg class="w-5 h-5 mr-3 flex-shrink-0"><!-- Warning Icon --></svg>
  <div>
    <span class="font-medium">Achtung!</span>
    Bitte überprüfen Sie Ihre Eingaben.
  </div>
</div>

Info Alert

<div class="flex items-center p-4 mb-4 text-sm text-blue-800
            bg-blue-100 rounded-md border border-blue-200"
     role="alert">
  <svg class="w-5 h-5 mr-3 flex-shrink-0"><!-- Info Icon --></svg>
  <div>
    <span class="font-medium">Hinweis:</span>
    Zusätzliche Informationen...
  </div>
</div>

7. Modal / Dialog

<!-- Modal Trigger -->
<button data-bs-toggle="modal" data-bs-target="#modal-id"
        class="...">
  Modal öffnen
</button>

<!-- Modal Structure -->
<div class="modal fade" id="modal-id" tabindex="-1" role="dialog">
  <div class="modal-dialog modal-dialog-centered" role="document">
    <div class="modal-content rounded-md border-0 shadow-xl">

      <!-- Header -->
      <div class="modal-header border-b border-gray-200 px-6 py-4">
        <h3 class="text-xl font-medium text-gray-900">
          Modal Titel
        </h3>
        <button type="button" class="text-gray-400 hover:text-gray-600"
                data-bs-dismiss="modal">
          <svg class="w-6 h-6"><!-- Close Icon --></svg>
        </button>
      </div>

      <!-- Body -->
      <div class="modal-body px-6 py-4">
        <p class="text-sm text-gray-600">
          Modal Content
        </p>
      </div>

      <!-- Footer -->
      <div class="modal-footer border-t border-gray-200 px-6 py-4">
        <button type="button" class="..." data-bs-dismiss="modal">
          Abbrechen
        </button>
        <button type="button" class="...">
          Bestätigen
        </button>
      </div>

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

Alpine.js Modal (empfohlen)

<div x-data="{ open: false }">
  <!-- Trigger -->
  <button @click="open = true" class="...">
    Modal öffnen
  </button>

  <!-- Modal Overlay -->
  <div x-show="open"
       x-transition:enter="transition ease-out duration-300"
       x-transition:enter-start="opacity-0"
       x-transition:enter-end="opacity-100"
       x-transition:leave="transition ease-in duration-200"
       x-transition:leave-start="opacity-100"
       x-transition:leave-end="opacity-0"
       class="fixed inset-0 z-50 overflow-y-auto"
       @click="open = false">

    <!-- Modal Container -->
    <div class="flex items-center justify-center min-h-screen px-4">

      <!-- Backdrop -->
      <div class="fixed inset-0 bg-black opacity-50"></div>

      <!-- Modal Content -->
      <div @click.stop
           class="relative bg-white rounded-md shadow-xl max-w-lg w-full
                  dark:bg-boxdark"
           x-transition:enter="transition ease-out duration-300"
           x-transition:enter-start="opacity-0 scale-90"
           x-transition:enter-end="opacity-100 scale-100">

        <!-- Header -->
        <div class="border-b border-gray-200 px-6 py-4
                    dark:border-strokedark">
          <h3 class="text-xl font-medium">Modal Titel</h3>
        </div>

        <!-- Body -->
        <div class="px-6 py-4">
          <p class="text-sm text-gray-600 dark:text-bodydark">
            Modal Content
          </p>
        </div>

        <!-- Footer -->
        <div class="border-t border-gray-200 px-6 py-4 flex justify-end gap-2
                    dark:border-strokedark">
          <button @click="open = false" class="...">Abbrechen</button>
          <button @click="open = false" class="...">Bestätigen</button>
        </div>

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

8. Navigation

Sidebar Navigation

Siehe backend_tailwind_base.html.twig für vollständige Implementierung.

Key Features:

  • Collapsible mit Alpine.js
  • Icons (Font Awesome)
  • Active States
  • Responsive (Mobile-Toggle)
<aside x-bind:class="$store.sideBar.isOpen ? 'w-80' : 'w-21.5'"
       class="fixed left-0 top-0 z-999 flex h-screen flex-col
              overflow-y-hidden bg-boxdark duration-300 ease-linear
              lg:static lg:translate-x-0">

  <!-- Sidebar Header -->
  <div class="flex items-center justify-between gap-2 px-6 py-5.5">
    <a href="/" class="flex items-center gap-3">
      <img src="/logo.svg" alt="Logo" class="h-8">
      <span x-show="$store.sideBar.isOpen"
            class="text-white text-xl font-bold">
        PrintshopCreator
      </span>
    </a>
    <button @click="$store.sideBar.toggle()" class="lg:hidden">
      <svg class="w-6 h-6 text-white"><!-- Menu Icon --></svg>
    </button>
  </div>

  <!-- Sidebar Menu -->
  <nav class="px-4 py-4">
    <ul class="space-y-1">
      <!-- Menu Item -->
      <li>
        <a href="/dashboard"
           class="group relative flex items-center gap-3 rounded-md
                  px-4 py-2 font-medium text-bodydark1
                  hover:bg-graydark hover:text-white
                  {{ app.request.attributes.get('_route') == 'dashboard' ? 'bg-graydark text-white' : '' }}">
          <svg class="w-5 h-5"><!-- Icon --></svg>
          <span x-show="$store.sideBar.isOpen">Dashboard</span>
        </a>
      </li>

      <!-- Menu Group (mit Submenu) -->
      <li x-data="{ open: false }">
        <a @click="open = !open"
           class="group relative flex items-center justify-between gap-3
                  rounded-md px-4 py-2 font-medium text-bodydark1
                  hover:bg-graydark hover:text-white cursor-pointer">
          <div class="flex items-center gap-3">
            <svg class="w-5 h-5"><!-- Icon --></svg>
            <span x-show="$store.sideBar.isOpen">Produkte</span>
          </div>
          <svg x-show="$store.sideBar.isOpen"
               x-bind:class="open ? 'rotate-180' : ''"
               class="w-4 h-4 transition-transform">
            <!-- Arrow Icon -->
          </svg>
        </a>

        <!-- Submenu -->
        <ul x-show="open"
            x-collapse
            class="mt-1 ml-8 space-y-1">
          <li>
            <a href="/products/list"
               class="flex items-center gap-3 rounded-md px-4 py-2
                      text-sm text-bodydark1 hover:text-white">
              Produktliste
            </a>
          </li>
          <li>
            <a href="/products/create"
               class="flex items-center gap-3 rounded-md px-4 py-2
                      text-sm text-bodydark1 hover:text-white">
              Neues Produkt
            </a>
          </li>
        </ul>
      </li>
    </ul>
  </nav>

</aside>

Breadcrumbs

<nav class="mb-6" aria-label="Breadcrumb">
  <ol class="flex items-center space-x-2 text-sm">
    <li>
      <a href="/" class="text-psc-500 hover:text-psc-600">
        Home
      </a>
    </li>
    <li>
      <svg class="w-4 h-4 text-gray-400"><!-- Chevron --></svg>
    </li>
    <li>
      <a href="/products" class="text-psc-500 hover:text-psc-600">
        Produkte
      </a>
    </li>
    <li>
      <svg class="w-4 h-4 text-gray-400"><!-- Chevron --></svg>
    </li>
    <li class="text-gray-500" aria-current="page">
      Produktdetails
    </li>
  </ol>
</nav>

Tabs

<div x-data="{ activeTab: 'tab1' }" class="mb-6">
  <!-- Tab Headers -->
  <div class="border-b border-gray-200 dark:border-strokedark">
    <ul class="flex space-x-8">
      <li>
        <button @click="activeTab = 'tab1'"
                x-bind:class="activeTab === 'tab1'
                  ? 'border-psc-500 text-psc-600'
                  : 'border-transparent text-gray-500 hover:text-gray-700'"
                class="py-4 px-1 border-b-2 font-medium text-sm
                       transition-colors">
          Allgemein
        </button>
      </li>
      <li>
        <button @click="activeTab = 'tab2'"
                x-bind:class="activeTab === 'tab2'
                  ? 'border-psc-500 text-psc-600'
                  : 'border-transparent text-gray-500 hover:text-gray-700'"
                class="py-4 px-1 border-b-2 font-medium text-sm
                       transition-colors">
          Einstellungen
        </button>
      </li>
    </ul>
  </div>

  <!-- Tab Content -->
  <div class="mt-6">
    <div x-show="activeTab === 'tab1'">
      <!-- Tab 1 Content -->
    </div>
    <div x-show="activeTab === 'tab2'">
      <!-- Tab 2 Content -->
    </div>
  </div>
</div>

9. Pagination

<!-- KnpPaginator mit Tailwind Theme -->
<div class="flex items-center justify-between border-t border-gray-200
            bg-white px-4 py-3 sm:px-6 dark:border-strokedark dark:bg-boxdark">

  <!-- Mobile Pagination -->
  <div class="flex flex-1 justify-between sm:hidden">
    <a href="?page={{ pagination.current - 1 }}"
       class="relative inline-flex items-center px-4 py-2 text-sm
              font-medium text-gray-700 bg-white border border-gray-300
              rounded-md hover:bg-gray-50">
      Zurück
    </a>
    <a href="?page={{ pagination.current + 1 }}"
       class="relative ml-3 inline-flex items-center px-4 py-2 text-sm
              font-medium text-gray-700 bg-white border border-gray-300
              rounded-md hover:bg-gray-50">
      Weiter
    </a>
  </div>

  <!-- Desktop Pagination -->
  <div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
    <div>
      <p class="text-sm text-gray-700 dark:text-bodydark">
        Zeige <span class="font-medium">{{ pagination.first }}</span> bis
        <span class="font-medium">{{ pagination.last }}</span> von
        <span class="font-medium">{{ pagination.total }}</span> Ergebnissen
      </p>
    </div>
    <div>
      <nav class="isolate inline-flex -space-x-px rounded-md shadow-sm">
        {% for page in pagination.pagesInRange %}
          <a href="?page={{ page }}"
             class="relative inline-flex items-center px-4 py-2 text-sm
                    font-medium
                    {{ page == pagination.current
                      ? 'z-10 bg-psc-500 text-white'
                      : 'bg-white text-gray-700 hover:bg-gray-50' }}
                    border border-gray-300">
            {{ page }}
          </a>
        {% endfor %}
      </nav>
    </div>
  </div>

</div>

10. Loading States

Spinner

<!-- Inline Spinner -->
<svg class="animate-spin h-5 w-5 text-psc-500"
     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>

<!-- Button mit Spinner -->
<button class="inline-flex items-center gap-2 px-4 py-2
               bg-psc-500 text-white rounded-md"
        disabled>
  <svg class="animate-spin h-4 w-4"><!-- Spinner --></svg>
  Lädt...
</button>

Skeleton Loader

<div class="animate-pulse space-y-4">
  <div class="h-4 bg-gray-200 rounded w-3/4"></div>
  <div class="h-4 bg-gray-200 rounded"></div>
  <div class="h-4 bg-gray-200 rounded w-5/6"></div>
</div>

Progress Bar

<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
  <div class="bg-psc-500 h-2.5 rounded-full transition-all duration-300"
       style="width: 45%"></div>
</div>

11. Dropdowns

<div x-data="{ open: false }" class="relative inline-block text-left">
  <!-- Dropdown Button -->
  <button @click="open = !open"
          type="button"
          class="inline-flex justify-between items-center w-full
                 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-psc-500">
    Optionen
    <svg class="w-5 h-5 ml-2 -mr-1"
         x-bind:class="open ? 'rotate-180' : ''">
      <!-- Chevron Icon -->
    </svg>
  </button>

  <!-- Dropdown Menu -->
  <div x-show="open"
       @click.away="open = false"
       x-transition:enter="transition ease-out duration-100"
       x-transition:enter-start="opacity-0 scale-95"
       x-transition:enter-end="opacity-100 scale-100"
       x-transition:leave="transition ease-in duration-75"
       x-transition:leave-start="opacity-100 scale-100"
       x-transition:leave-end="opacity-0 scale-95"
       class="absolute right-0 z-10 mt-2 w-56 origin-top-right
              bg-white rounded-md shadow-lg ring-1 ring-black
              ring-opacity-5 focus:outline-none
              dark:bg-boxdark">
    <div class="py-1">
      <a href="#"
         class="block px-4 py-2 text-sm text-gray-700
                hover:bg-gray-100 hover:text-gray-900
                dark:text-bodydark dark:hover:bg-graydark">
        <svg class="w-4 h-4 inline mr-2"><!-- Icon --></svg>
        Bearbeiten
      </a>
      <a href="#"
         class="block px-4 py-2 text-sm text-gray-700
                hover:bg-gray-100 hover:text-gray-900
                dark:text-bodydark dark:hover:bg-graydark">
        <svg class="w-4 h-4 inline mr-2"><!-- Icon --></svg>
        Duplizieren
      </a>
      <div class="border-t border-gray-100 dark:border-strokedark"></div>
      <a href="#"
         class="block px-4 py-2 text-sm text-red-600
                hover:bg-red-50 hover:text-red-700">
        <svg class="w-4 h-4 inline mr-2"><!-- Icon --></svg>
        Löschen
      </a>
    </div>
  </div>
</div>

12. Empty States

<div class="text-center py-12">
  <svg class="mx-auto h-12 w-12 text-gray-400"
       fill="none" stroke="currentColor" viewBox="0 0 24 24">
    <!-- Empty Icon (z.B. Folder, Document, etc.) -->
  </svg>
  <h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-bodydark">
    Keine Einträge vorhanden
  </h3>
  <p class="mt-1 text-sm text-gray-500 dark:text-bodydark">
    Erstellen Sie Ihren ersten Eintrag, um loszulegen.
  </p>
  <div class="mt-6">
    <button class="inline-flex items-center px-4 py-2
                   border border-transparent shadow-sm text-sm
                   font-medium rounded-md text-white bg-psc-500
                   hover:bg-psc-600">
      <svg class="w-5 h-5 mr-2"><!-- Plus Icon --></svg>
      Neu erstellen
    </button>
  </div>
</div>

Layout-Patterns

Standard-Seitenlayout

{# templates/your-bundle/backend/list.html.twig #}
{% extends 'backend_tailwind_base.html.twig' %}

{% block title %}Seitentitel{% endblock %}

{% block body %}
<div class="mx-auto max-w-7xl">

  {# Page Header #}
  <div class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
    <h1 class="text-3xl font-bold text-gray-900 dark:text-bodydark">
      Seitentitel
    </h1>
    <div class="flex gap-2">
      <a href="{{ path('route_create') }}" class="psc-button-save">
        <svg class="w-4 h-4"><!-- Plus Icon --></svg>
        Neu erstellen
      </a>
    </div>
  </div>

  {# Flash Messages #}
  {% for label, messages in app.flashes %}
    {% for message in messages %}
      <div class="alert alert-{{ label }} mb-4">
        {{ message }}
      </div>
    {% endfor %}
  {% endfor %}

  {# Main Content #}
  <div class="rounded-md border border-gray-200 bg-white shadow-lg
              dark:border-strokedark dark:bg-boxdark">
    <div class="px-7.5 py-6">
      {# Your content here #}
    </div>
  </div>

</div>
{% endblock %}

Liste-Seite (CRUD List)

{% extends 'backend_tailwind_base.html.twig' %}

{% block body %}
<div class="mx-auto max-w-7xl">

  {# Header mit Search & Filters #}
  <div class="mb-6">
    <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
      <h1 class="text-3xl font-bold">{{ title }}</h1>
      <a href="{{ path('route_create') }}" class="psc-button-save">
        Neu erstellen
      </a>
    </div>

    {# Search & Filters #}
    <div class="mt-4 flex flex-col gap-4 md:flex-row">
      <div class="flex-1">
        <input type="text"
               placeholder="Suchen..."
               class="w-full px-4 py-2 border border-gray-300 rounded-md
                      focus:ring-2 focus:ring-psc-500">
      </div>
      <select class="px-4 py-2 border border-gray-300 rounded-md
                     focus:ring-2 focus:ring-psc-500">
        <option>Alle Status</option>
        <option>Aktiv</option>
        <option>Inaktiv</option>
      </select>
    </div>
  </div>

  {# Table Card #}
  <div class="rounded-md border bg-white shadow-lg
              dark:border-strokedark dark:bg-boxdark">
    <div class="overflow-x-auto">
      <table class="min-w-full divide-y divide-gray-200">
        <thead class="bg-gray-50 dark:bg-boxdark">
          <tr>
            <th class="px-6 py-3 text-left text-xs font-medium
                       text-gray-500 uppercase">
              {{ knp_pagination_sortable(pagination, 'Name', 'name') }}
            </th>
            <th class="px-6 py-3 text-left text-xs font-medium
                       text-gray-500 uppercase">
              Status
            </th>
            <th class="px-6 py-3 text-right text-xs font-medium
                       text-gray-500 uppercase">
              Aktionen
            </th>
          </tr>
        </thead>
        <tbody class="bg-white divide-y divide-gray-200 dark:bg-boxdark">
          {% for item in pagination %}
            <tr class="hover:bg-gray-50 dark:hover:bg-boxdark-2">
              <td class="px-6 py-4 whitespace-nowrap">
                {{ item.name }}
              </td>
              <td class="px-6 py-4 whitespace-nowrap">
                {% if item.active %}
                  <span class="badge-yes">Aktiv</span>
                {% else %}
                  <span class="badge-no">Inaktiv</span>
                {% endif %}
              </td>
              <td class="px-6 py-4 whitespace-nowrap text-right">
                <a href="{{ path('route_edit', {id: item.id}) }}"
                   class="text-psc-500 hover:text-psc-600 mr-3">
                  Bearbeiten
                </a>
                <a href="{{ path('route_delete', {id: item.id}) }}"
                   class="text-red-500 hover:text-red-600"
                   onclick="return confirm('Wirklich löschen?')">
                  Löschen
                </a>
              </td>
            </tr>
          {% else %}
            <tr>
              <td colspan="3" class="px-6 py-12 text-center text-gray-500">
                Keine Einträge vorhanden
              </td>
            </tr>
          {% endfor %}
        </tbody>
      </table>
    </div>

  {# Pagination #}
  {{ knp_pagination_render(pagination, 'tailwind_pagination.html.twig') }}
  </div>

</div>
{% endblock %}

List-Header im block header (Titel + Aktion rechts)

{% block header %}
    <div class="flex flex-wrap items-center gap-4 justify-between w-full">
        <div>
            <h1 class="text-psc text-2xl font-medium flex flex-row gap-1">
                <svg xmlns="http://www.w3.org/2000/svg" class="h-8"><!-- Icon --></svg>
                Module Name <span class="text-gray-500">Liste</span>
            </h1>
        </div>
        <div class="flex flex-wrap items-center gap-4 justify-end shrink-0 ml-auto">
            <a href="{{ path('route_create') }}" class="psc-button-save">
                Neu erstellen
            </a>
        </div>
    </div>
{% endblock %}

KNP Pagination mit Sortable-Template

{{ knp_pagination_render(pagination, 'tailwind_pagination.html.twig', {}, {
    'sortableTemplate': 'tailwind_sortable.html.twig'
}) }}

Edit/Create-Seite (CRUD Form)

WICHTIG: Verwende immer das vertikale Tab-Layout für Edit/Create-Seiten mit mehreren Sektionen!

Standard Edit-Seite mit vertikalen Tabs

{% extends 'backend_tailwind_base.html.twig' %}
{% form_theme form 'form_div_layout_tailwind.html.twig' %}

{% block header %}
    <div>
        <h1 class="text-psc text-2xl font-medium flex flex-row gap-1">
            <svg xmlns="http://www.w3.org/2000/svg" class="h-8"><!-- Icon --></svg>
            Module Name <span class="text-gray-500">Bearbeiten</span>
        </h1>
    </div>
    <div class="flex flex-wrap items-center gap-4 justify-start shrink-0">
        <a href="{{ path('module_list') }}" class="inline-flex items-center justify-center py-1 gap-1 font-medium rounded-md px-4 text-sm text-white shadow-lg bg-gray-500 hover:bg-gray-600 hover:ring-2 hover:ring-gray-500 hover:ring-offset-2 min-h-[2.25rem]">
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="button-icon">
                <path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
            </svg>
            Zurück
        </a>
    </div>
{% endblock %}

{% block body %}
<div class="w-full flex flex-col gap-6">
    {{ form_start(form, {attr: {class: ''}}) }}

    {# Vertikales Tab-Layout #}
    <div class="tab-group flex-none md:flex w-full" data-dui-orientation="vertical">

        {# Tab Navigation - Links (2/12) #}
        <div role="tablist" class="relative mr-5 rounded-md flex flex-col p-1 w-full md:w-2/12">
            <div class="tab-indicator absolute left-0 w-1 bg-psc-500 transition-transform duration-300"></div>

            <a href="#" class="tab-link flex items-center text-sm active px-4 py-2 relative" data-dui-tab-target="general">
                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="button-icon">
                    <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
                    <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
                </svg>
                Allgemein
            </a>

            <a href="#" class="tab-link flex items-center text-sm px-4 py-2 relative" data-dui-tab-target="settings">
                <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="button-icon">
                    <path stroke-linecap="round" stroke-linejoin="round" d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 0 1 1.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.559.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.894.149c-.424.07-.764.383-.929.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 0 1-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.398.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 0 1-.12-1.45l.527-.737c.25-.35.272-.806.108-1.204-.165-.397-.506-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.108-1.204l-.526-.738a1.125 1.125 0 0 1 .12-1.45l.773-.773a1.125 1.125 0 0 1 1.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894Z" />
                    <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
                </svg>
                Einstellungen
            </a>
        </div>

        {# Content Area - Rechts (10/12) #}
        <div class="rounded-md w-full border bg-white p-5 shadow-lg dark:border-strokedark dark:bg-boxdark">

            {# General Tab #}
            <div id="general" class="tab-content w-full text-stone-500 text-sm block">
                <h6 class="text-sm mt-3 mb-6 font-bold uppercase">Allgemein</h6>

                {# Felder in 3er-Gruppen (4/12 Breite) #}
                <div class="flex flex-wrap">
                    <div class="w-full lg:w-4/12 px-4">
                        {{ form_row(form.title) }}
                    </div>
                    <div class="w-full lg:w-4/12 px-4">
                        {{ form_row(form.position) }}
                    </div>
                    <div class="w-full lg:w-4/12 px-4">
                        {{ form_row(form.active) }}
                    </div>
                </div>

                {# Felder in 2er-Gruppen (6/12 Breite) #}
                <div class="flex flex-wrap">
                    <div class="w-full lg:w-6/12 px-4">
                        {{ form_row(form.startDate) }}
                    </div>
                    <div class="w-full lg:w-6/12 px-4">
                        {{ form_row(form.endDate) }}
                    </div>
                </div>

                {# Einzelnes Feld (volle Breite) #}
                <div class="flex flex-wrap">
                    <div class="w-full px-4">
                        {{ form_row(form.description) }}
                    </div>
                </div>
            </div>

            {# Settings Tab #}
            <div id="settings" class="tab-content w-full text-stone-500 text-sm hidden">
                <h6 class="text-sm mt-3 mb-6 font-bold uppercase">Einstellungen</h6>

                <div class="flex flex-wrap">
                    <div class="w-full px-4">
                        {{ form_row(form.settings) }}
                    </div>
                </div>
            </div>

        </div>
    </div>

    {# Save Button außerhalb der Card #}
    <div class="text-end my-2">
        {{ form_widget(form.save, {
            attr: {
                class: 'inline-flex items-center justify-center py-1 gap-1 font-medium rounded-md px-4 text-sm text-white shadow-lg bg-psc-500 hover:bg-psc-600 hover:ring-2 hover:ring-psc-500 hover:ring-offset-2 min-h-[2.25rem]'
            }
        }) }}
    </div>

    {{ form_end(form) }}

    {# Changes History (nur bei Edit, nicht bei Create) #}
    <div class="rounded-md border bg-white px-7.5 py-6 shadow-lg dark:border-strokedark dark:bg-boxdark">
        <h2 class="ml-4 text-psc text-xl font-medium mb-4">Änderungen</h2>
        <div class="">
            <div class="w-full grid grid-cols-10 border-t border-stroke px-4 bg-slate-100 py-4.5 dark:border-strokedark md:px-6 2xl:px-7.5">
                <div class="col-span-2 px-2 py-3">
                    <p class="font-medium">Datum</p>
                </div>
                <div class="col-span-2 px-2 py-3">
                    <p class="font-medium">Benutzer</p>
                </div>
                <div class="col-span-6 px-2 py-3">
                    <p class="font-medium">Änderungen</p>
                </div>
            </div>
            {% for change in changes %}
                <div class="w-full grid grid-cols-10 border-t border-stroke px-4 py-4.5 dark:border-strokedark md:px-6 2xl:px-7.5">
                    <div class="col-span-2 px-2 py-3">{{ change.created|date('H:i:s d.m.Y') }}</div>
                    <div class="col-span-2 px-2 py-3">{{ change.username }}</div>
                    <div class="col-span-6 px-2 py-3">
                        {% for key, set in change.changeset %}
                            {% if set|length > 1 and set[1] is not iterable %}
                                <strong>{{ key }}</strong> <span class="badge-no"><del>{{ set[0] }}</del></span> <span class="badge-yes">{% if set[1] is null %}0{% else %}{{ set[1] }}{% endif %}</span><br>
                            {% endif %}
                        {% endfor %}
                    </div>
                </div>
            {% endfor %}
        </div>
    </div>
</div>
{% endblock %}

Delete-Seite (Confirmation Card)

WICHTIG: Delete immer als eigene Bestätigungs-Card mit Header, Body, Footer.

{% extends 'backend_tailwind_base.html.twig' %}
{% form_theme form 'form_div_layout_tailwind.html.twig' %}

{% block header %}
    <div>
        <h1 class="text-3xl font-bold text-gray-900 dark:text-bodydark flex items-center gap-2">
            <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8"><!-- Icon --></svg>
            Module Name <span class="text-gray-500">Löschen</span>
        </h1>
    </div>
    <div class="flex flex-wrap items-center gap-4 justify-start shrink-0">
        <a href="{{ path('module_list') }}" class="inline-flex items-center justify-center py-1 gap-1 font-medium rounded-md px-4 text-sm text-white shadow-lg bg-gray-500 hover:bg-gray-600 hover:ring-2 hover:ring-gray-500 hover:ring-offset-2 min-h-[2.25rem]">
            <svg xmlns="http://www.w3.org/2000/svg" class="button-icon"><!-- Icon --></svg>
            Zurück
        </a>
    </div>
{% endblock %}

{% block body %}
<div class="flex flex-col gap-6">
    <div class="rounded-md border border-red-200 bg-white shadow-lg dark:border-strokedark dark:bg-boxdark">
        <div class="border-b border-red-200 bg-red-50 px-7.5 py-4 dark:border-strokedark dark:bg-red-900/20">
            <div class="flex items-center gap-3">
                <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-red-600"><!-- Warn Icon --></svg>
                <h3 class="text-xl font-medium text-red-900 dark:text-red-200">Löschen?</h3>
            </div>
        </div>
        <div class="px-7.5 py-6">
            <p class="text-gray-700 dark:text-bodydark">
                Bist du sicher, dass du diesen Eintrag löschen willst?
            </p>
            <div class="mt-4 p-4 bg-gray-50 rounded-md border border-gray-200 dark:bg-boxdark-2 dark:border-strokedark">
                <h5 class="text-lg font-medium text-gray-900 dark:text-bodydark">
                    {{ item.title }}
                </h5>
            </div>
            <div class="mt-4 p-4 bg-yellow-50 rounded-md border border-yellow-200">
                <p class="text-sm text-yellow-800">Diese Aktion kann nicht rückgängig gemacht werden.</p>
            </div>
        </div>
        <div class="border-t border-gray-200 px-7.5 py-4 bg-gray-50 dark:border-strokedark dark:bg-boxdark-2">
            {{ form_start(form) }}
            <div class="flex flex-wrap items-center gap-3 justify-end">
                {{ form_widget(form.no, {
                    attr: {
                        class: 'inline-flex items-center justify-center py-2 gap-2 font-medium rounded-md px-4 text-sm text-psc-600 border border-psc-500 bg-white hover:bg-gray-50 hover:ring-2 hover:ring-psc-500 hover:ring-offset-1 shadow-sm'
                    }
                }) }}
                {{ form_widget(form.yes, {
                    attr: {
                        class: 'inline-flex items-center justify-center py-2 gap-2 font-medium rounded-md px-4 text-sm text-white shadow-lg bg-red-500 hover:bg-red-600 hover:ring-2 hover:ring-red-500 hover:ring-offset-1'
                    }
                }) }}
            </div>
            {{ form_end(form) }}
        </div>
    </div>
</div>
{% endblock %}

Design-Specs für Edit/Create-Seiten

Header:

  • Titel: Links mit Icon, Format: Module Name <span class="text-gray-500">Aktion</span>
  • Zurück-Button: Rechts, IMMER Grau (bg-gray-500), nie PSC-Farbe
  • Icon: Entsprechend dem Modul (Payment, CMS, etc.)

Vertikale Tab-Navigation:

  • Breite: w-full md:w-2/12 (2 von 12 Spalten auf Desktop)
  • Position: Links
  • Indikator: Blaue Linie links (w-1 bg-psc-500)
  • Tab-Links: Mit Icons, px-4 py-2, aktiver Tab: class="active"
  • Rundung: rounded-md

Content-Bereich:

  • Breite: Volle Breite minus Tab-Navigation
  • Card: rounded-md border bg-white p-5 shadow-lg
  • Tab-Content: Erstes Tab class="block", andere class="hidden"
  • Überschrift: text-sm mt-3 mb-6 font-bold uppercase

Feld-Layout (Flex-Wrap System):

{# 3 Felder nebeneinander (Desktop) #}
<div class="flex flex-wrap">
    <div class="w-full lg:w-4/12 px-4">
        {{ form_row(form.field1) }}
    </div>
    <div class="w-full lg:w-4/12 px-4">
        {{ form_row(form.field2) }}
    </div>
    <div class="w-full lg:w-4/12 px-4">
        {{ form_row(form.field3) }}
    </div>
</div>

{# 2 Felder nebeneinander (Desktop) #}
<div class="flex flex-wrap">
    <div class="w-full lg:w-6/12 px-4">
        {{ form_row(form.field1) }}
    </div>
    <div class="w-full lg:w-6/12 px-4">
        {{ form_row(form.field2) }}
    </div>
</div>

{# 1 Feld (volle Breite) #}
<div class="flex flex-wrap">
    <div class="w-full px-4">
        {{ form_row(form.description) }}
    </div>
</div>

WICHTIG: Grid vs. Flex-Wrap:

  • NICHT verwenden: grid grid-cols-1 md:grid-cols-2 gap-6
  • Verwenden: flex flex-wrap mit w-full lg:w-4/12 px-4

Save-Button:

  • Position: Außerhalb der Card, rechts (text-end my-2)
  • Farbe: PSC-Orange (bg-psc-500)
  • Rundung: rounded-md
  • WICHTIG: Verwende {{ form_widget(form.save, {attr: {...}}) }} statt manuellem <button> Tag, um doppelte Buttons zu vermeiden

Button-Farbschema:

  • Zurück: Grau (bg-gray-500) - Neutrale Aktion
  • Speichern: PSC (bg-psc-500) - Primäre Aktion
  • Abbrechen: Weiß mit PSC-Border - Sekundäre Aktion
  • Löschen: Rot (bg-red-500) - Destruktive Aktion

Changes-History:

WICHTIG: Changes-History kommt NACH dem Formular, NICHT als Tab!

Die Changes-History wird als separate Card nach {{ form_end(form) }} platziert, nicht als Tab in der vertikalen Navigation.

{{ form_end(form) }}

{# Changes History Card - NACH dem Formular #}
<div class="rounded-md border bg-white px-7.5 py-6 shadow-lg dark:border-strokedark dark:bg-boxdark">
    <h2 class="ml-4 text-psc text-xl font-medium mb-4">{{ 'Changes'|trans }}</h2>
    <div class="">
        {# Header-Zeile #}
        <div class="w-full grid grid-cols-10 border-t border-stroke px-4 bg-slate-100 py-4.5 dark:border-strokedark md:px-6 2xl:px-7.5">
            <div class="col-span-2 px-2 py-3">
                <p class="font-medium">{{ 'Date'|trans }}</p>
            </div>
            <div class="col-span-2 px-2 py-3">
                <p class="font-medium">{{ 'Username'|trans }}</p>
            </div>
            <div class="col-span-6 px-2 py-3">
                <p class="font-medium">{{ 'Changes'|trans }}</p>
            </div>
        </div>

        {# Daten-Zeilen #}
        {% for change in changes %}
            <div class="w-full grid grid-cols-10 border-t border-stroke px-4 py-4.5 dark:border-strokedark md:px-6 2xl:px-7.5">
                <div class="col-span-2 px-2 py-3">{{ change.created|date('H:i:s d.m.Y') }}</div>
                <div class="col-span-2 px-2 py-3">{{ change.username }}</div>
                <div class="col-span-6 px-2 py-3">
                    {% for key, set in change.changeset %}
                        {% if set|length > 1 and set[1] is not iterable %}
                            <strong>{{ key }}</strong>
                            <span class="badge-no"><del>{{ set[0] }}</del></span>
                            <span class="badge-yes">{% if set[1] is null %}0{% else %}{{ set[1] }}{% endif %}</span><br>
                        {% endif %}
                    {% endfor %}
                </div>
            </div>
        {% endfor %}
    </div>
</div>

Design-Specs:

  • Position: Nach {{ form_end(form) }}, NICHT als Tab
  • Nur bei Edit: Nicht bei Create-Seiten zeigen
  • Layout: Grid mit 10 Spalten (grid-cols-10)
    • Spalte 1: Datum (col-span-2)
    • Spalte 2: Benutzer (col-span-2)
    • Spalte 3: Änderungen (col-span-6)
  • Header: bg-slate-100 mit font-medium, py-4.5
  • Daten-Zeilen: border-t border-stroke, py-4.5
  • Badges für Änderungen:
    • Alter Wert (gelöscht): <span class="badge-no"><del>Wert</del></span> (Orange/Rot)
    • Neuer Wert: <span class="badge-yes">Wert</span> (Grün)
  • Rundung: rounded-md für die Card
  • Titel: text-psc text-xl font-medium mb-4

Dashboard Layout

{% extends 'backend_tailwind_base.html.twig' %}

{% block body %}
<div class="mx-auto max-w-7xl">

  <h1 class="mb-6 text-3xl font-bold">Dashboard</h1>

  {# Stats Grid #}
  <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4 mb-6">

    {% for stat in stats %}
      <div class="rounded-md border bg-white px-7.5 py-6 shadow-lg
                  dark:border-strokedark dark:bg-boxdark">
        <div class="flex items-center justify-between">
          <div>
            <h4 class="text-sm font-medium text-gray-600 dark:text-bodydark">
              {{ stat.label }}
            </h4>
            <span class="text-3xl font-bold text-gray-900 dark:text-white mt-2">
              {{ stat.value }}
            </span>
          </div>
          <div class="flex h-12 w-12 items-center justify-center
                      rounded-full bg-{{ stat.color }}-100">
            <svg class="w-6 h-6 text-{{ stat.color }}-500">
              {{ stat.icon|raw }}
            </svg>
          </div>
        </div>
        {% if stat.change %}
          <span class="text-sm {{ stat.change > 0 ? 'text-green-500' : 'text-red-500' }} mt-2">
            {{ stat.change > 0 ? '+' : '' }}{{ stat.change }}%
          </span>
        {% endif %}
      </div>
    {% endfor %}

  </div>

  {# Content Grid #}
  <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">

    {# Chart Card #}
    <div class="rounded-md border bg-white shadow-lg
                dark:border-strokedark dark:bg-boxdark">
      <div class="border-b border-gray-200 px-7.5 py-4
                  dark:border-strokedark">
        <h3 class="text-xl font-medium">Statistiken</h3>
      </div>
      <div class="px-7.5 py-6">
        {# Chart here #}
      </div>
    </div>

    {# Activity List #}
    <div class="rounded-md border bg-white shadow-lg
                dark:border-strokedark dark:bg-boxdark">
      <div class="border-b border-gray-200 px-7.5 py-4
                  dark:border-strokedark">
        <h3 class="text-xl font-medium">Letzte Aktivitäten</h3>
      </div>
      <div class="px-7.5 py-6">
        <ul class="space-y-4">
          {% for activity in activities %}
            <li class="flex items-start gap-4">
              <div class="flex-shrink-0">
                <div class="h-10 w-10 rounded-full bg-gray-200
                            flex items-center justify-center">
                  <svg class="w-5 h-5 text-gray-500"><!-- Icon --></svg>
                </div>
              </div>
              <div class="flex-1">
                <p class="text-sm text-gray-900 dark:text-bodydark">
                  {{ activity.description }}
                </p>
                <p class="text-xs text-gray-500 mt-1">
                  {{ activity.timestamp|date('d.m.Y H:i') }}
                </p>
              </div>
            </li>
          {% endfor %}
        </ul>
      </div>
    </div>

  </div>

</div>
{% endblock %}

Code-Standards

Tailwind CSS Richtlinien

1. Utility-First Prinzip

Gut:

<button class="px-4 py-2 bg-psc-500 text-white rounded-md">
  Button
</button>

Schlecht (Custom CSS vermeiden):

<button class="custom-button">Button</button>
<style>.custom-button { padding: 0.5rem 1rem; ... }</style>

2. Wiederverwendbare Komponenten via @apply

Nur für häufig genutzte Pattern in assets/tailwind/backend.css:

/* Gut: Wiederverwendbare Button-Klasse */
.psc-button-save {
  @apply inline-flex items-center justify-center gap-2 px-4 py-2
         text-sm font-medium text-white
         bg-psc-500 hover:bg-psc-600
         rounded-sm shadow-lg
         hover:ring-2 hover:ring-psc-500 hover:ring-offset-1
         transition-all duration-200;
}

/* Gut: Wiederverwendbare Badge-Klasse */
.badge-yes {
  @apply inline-flex items-center px-2 py-1 rounded-sm
         text-xs font-medium bg-lime-500 text-white;
}

Verwendung:

<button class="psc-button-save">Speichern</button>
<span class="badge-yes">Aktiv</span>

3. Responsive Design

Mobile-First Ansatz verwenden:

<!-- Gut -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
  <!-- Content -->
</div>

<!-- Gut -->
<div class="flex flex-col md:flex-row">
  <!-- Content -->
</div>

4. Dark Mode Konsistenz

Immer Dark Mode Varianten hinzufügen:

<div class="bg-white dark:bg-boxdark
            text-gray-900 dark:text-bodydark
            border-gray-200 dark:border-strokedark">
  Content
</div>

Twig Template Richtlinien

1. Base Template Verwendung

Immer verwenden:

{% extends 'backend_tailwind_base.html.twig' %}

Nie verwenden:

{% extends 'backend_base.html.twig' %}  {# Legacy Bootstrap #}

2. Block-Struktur

{% extends 'backend_tailwind_base.html.twig' %}

{% block title %}Seitentitel{% endblock %}

{% block body %}
  {# Hauptinhalt #}
{% endblock %}

{% block javascripts %}
  {{ parent() }}
  {# Zusätzliche Scripts #}
{% endblock %}

3. Form Theme

{% form_theme form 'form_div_layout_tailwind.html.twig' %}

Nicht verwenden:

{% form_theme form 'tailwind_formtheme.html.twig' %}

4. Naming Conventions

Routen:

# Gut: Konsistentes Naming
psc_product_list:
  path: /products

psc_product_create:
  path: /products/create

psc_product_edit:
  path: /products/{id}/edit

psc_product_delete:
  path: /products/{id}/delete

Templates:

templates/
  product/
    backend/
      list.html.twig
      edit.html.twig
      _form.html.twig      # Partial

Alpine.js Richtlinien

1. State Management

Globaler Store (in Base Template):

Alpine.store('sideBar', {
  isOpen: true,
  toggle() {
    this.isOpen = !this.isOpen
  }
})

Lokaler State:

<div x-data="{ open: false, selected: null }">
  <!-- Component -->
</div>

2. Event Handling

<!-- Click -->
<button @click="open = true">Open</button>

<!-- Click Outside -->
<div @click.away="open = false">Menu</div>

<!-- Keyboard -->
<input @keydown.escape="open = false">

3. Transitions

Immer Tailwind-kompatible Transitions:

<div x-show="open"
     x-transition:enter="transition ease-out duration-300"
     x-transition:enter-start="opacity-0 transform scale-90"
     x-transition:enter-end="opacity-100 transform scale-100"
     x-transition:leave="transition ease-in duration-200"
     x-transition:leave-start="opacity-100 transform scale-100"
     x-transition:leave-end="opacity-0 transform scale-90">
  Content
</div>

Icon-Verwendung

Font Awesome

Standard-Größen:

<!-- Small -->
<i class="fas fa-edit w-4 h-4"></i>

<!-- Medium (Standard) -->
<i class="fas fa-save w-5 h-5"></i>

<!-- Large -->
<i class="fas fa-chart-bar w-6 h-6"></i>

Mit Tailwind:

<button class="inline-flex items-center gap-2">
  <i class="fas fa-plus w-4 h-4"></i>
  Neu erstellen
</button>

Häufig verwendete Icons:

  • fa-plus - Erstellen/Hinzufügen
  • fa-edit - Bearbeiten
  • fa-trash - Löschen
  • fa-save - Speichern
  • fa-times - Schließen/Abbrechen
  • fa-check - Bestätigen/Erfolg
  • fa-exclamation-triangle - Warnung
  • fa-info-circle - Information
  • fa-search - Suchen
  • fa-filter - Filtern
  • fa-cog - Einstellungen
  • fa-user - Benutzer
  • fa-chevron-down - Dropdown
  • fa-chevron-right - Pfeil/Navigation

CSS Klassen-Organisation

Reihenfolge der Utility-Klassen:

<div class="
  {# Layout #}
  flex items-center justify-between

  {# Spacing #}
  px-4 py-2 gap-2

  {# Sizing #}
  w-full h-12

  {# Typography #}
  text-sm font-medium

  {# Colors #}
  text-white bg-psc-500

  {# Borders #}
  border border-gray-200 rounded-sm

  {# Effects #}
  shadow-lg

  {# Transitions #}
  transition-all duration-200

  {# States #}
  hover:bg-psc-600 focus:ring-2

  {# Responsive #}
  md:flex-row lg:w-1/2

  {# Dark Mode #}
  dark:bg-boxdark dark:text-bodydark
">
  Content
</div>

Barrierefreiheit

ARIA Labels

<!-- Navigation -->
<nav aria-label="Hauptnavigation">
  <ul>...</ul>
</nav>

<!-- Buttons -->
<button aria-label="Schließen" @click="modal = false">
  <svg><!-- X Icon --></svg>
</button>

<!-- Form Errors -->
<input type="text"
       aria-describedby="error-message"
       aria-invalid="true">
<p id="error-message" role="alert">Fehler: Feld ist erforderlich</p>

Keyboard Navigation

<!-- Tab-Index -->
<button tabindex="0">Klickbar</button>

<!-- Skip Link -->
<a href="#main-content"
   class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4
          focus:z-50 focus:px-4 focus:py-2 focus:bg-psc-500 focus:text-white">
  Zum Hauptinhalt springen
</a>

Focus States

Immer sichtbare Focus States:

<button class="focus:outline-none focus:ring-2 focus:ring-psc-500
               focus:ring-offset-2">
  Button
</button>

<input type="text"
       class="focus:outline-none focus:ring-2 focus:ring-psc-500
              focus:border-transparent">

Screen Reader Only

<span class="sr-only">Nur für Screen Reader</span>

Migration Guide

Von Bootstrap zu Tailwind

Button-Migration

Vorher (Bootstrap):

<button class="btn btn-primary">Speichern</button>

Nachher (Tailwind):

<button class="psc-button-save">Speichern</button>

Card-Migration

Vorher (Bootstrap):

<div class="card">
  <div class="card-header">Titel</div>
  <div class="card-body">Content</div>
</div>

Nachher (Tailwind):

<div class="rounded-md border bg-white shadow-lg
            dark:border-strokedark dark:bg-boxdark">
  <div class="border-b px-7.5 py-4 dark:border-strokedark">
    <h3 class="text-xl font-medium">Titel</h3>
  </div>
  <div class="px-7.5 py-6">
    Content
  </div>
</div>

Form-Migration

Vorher (Bootstrap):

<div class="form-group">
  <label class="form-label">Feldname</label>
  <input type="text" class="form-control">
</div>

Nachher (Tailwind):

<div class="mb-4">
  <label class="block uppercase text-xs font-bold mb-2 text-gray-700">
    Feldname
  </label>
  <input type="text"
         class="w-full px-3 py-2 text-sm border border-gray-300 rounded-md
                focus:ring-2 focus:ring-psc-500">
</div>

Checkliste für neue Module

  • Verwendet backend_tailwind_base.html.twig als Base-Template
  • Form Theme auf form_div_layout_tailwind.html.twig gesetzt
  • Alle Komponenten verwenden Tailwind CSS (kein Bootstrap)
  • Dark Mode Support implementiert (dark: Varianten)
  • Responsive Design implementiert (Mobile-First)
  • Wiederverwendbare PSC-Button-Klassen verwendet
  • Standard Badge-Klassen für Status verwendet
  • Icons mit konsistenten Größen (w-4, w-5, w-6)
  • Spacing konsistent mit Tailwind-System (px-7.5, py-6, etc.)
  • Alpine.js für interaktive Komponenten verwendet
  • ARIA-Labels für Barrierefreiheit hinzugefügt
  • Focus States sichtbar und konsistent
  • KnpPagination mit Tailwind-Theme

Best Practices

Performance

  1. Lazy Loading für Bilder:
<img src="/path/to/image.jpg"
     loading="lazy"
     class="w-full h-auto">
  1. Alpine.js x-cloak:
<div x-data="{ open: false }" x-cloak>
  <!-- Verhindert Flash of Unstyled Content -->
</div>
  1. Minimize DOM Queries:
// Gut: State in Alpine.js
<div x-data="{ count: 0 }">
  <span x-text="count"></span>
</div>

// Schlecht: jQuery DOM Manipulation
$('#count').text(count);

Security

  1. XSS Prevention in Twig:
{# Gut: Auto-escaped #}
{{ user.name }}

{# Vorsicht: Raw HTML #}
{{ content|raw }}  {# Nur für vertrauenswürdigen Content! #}
  1. CSRF Protection:
{{ form_start(form) }}  {# CSRF Token automatisch inkludiert #}
  1. SQL Injection Prevention:
// Gut: Doctrine QueryBuilder
$qb->where('u.email = :email')
   ->setParameter('email', $email);

Maintenance

  1. Kommentare für komplexe Logik:
{# Complex Alpine.js state management - do not modify without testing #}
<div x-data="{
  activeTab: 'general',
  hasChanges: false,
  save() { ... }
}">
  1. TODO-Kommentare:
{# TODO: Migrate to Tailwind pagination once KnpPaginator theme is ready #}
{{ knp_pagination_render(pagination) }}
  1. Deprecation Warnings:
{# @deprecated Use backend_tailwind_base.html.twig instead #}
{% extends 'backend_base.html.twig' %}

Ressourcen

Dokumentation

Tools

  • Tailwind CSS IntelliSense (VS Code Extension)
  • Alpine.js DevTools (Browser Extension)
  • Symfony Toolbar (Development)

Weitere Module/Bundles

  • KnpMenuBundle: Navigation-Generierung
  • KnpPaginatorBundle: Pagination
  • LiipImagineBundle: Image Processing

Changelog

Version Datum Änderungen
1.0 2025-12-22 Initiales Design System basierend auf Codebase-Analyse

Kontakt & Feedback

Für Fragen, Vorschläge oder Verbesserungen zum Design System bitte ein Issue im Projekt-Repository erstellen oder das Entwicklungsteam kontaktieren.


Hinweis: Dieses Dokument ist ein "Living Document" und wird kontinuierlich aktualisiert, wenn neue Komponenten hinzugefügt oder bestehende Patterns verbessert werden.