81 KiB
PrintshopCreator Design System
Version: 1.0 Letzte Aktualisierung: 2025-12-22 Status: Living Document
Inhaltsverzeichnis
- Einleitung
- Architektur
- Farbsystem
- Typografie
- Spacing & Layout
- Komponenten-Bibliothek
- Layout-Patterns
- Code-Standards
- Barrierefreiheit
- 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)
Text Button / Link
<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) stattfont-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-colorsfür smooth Hover-Effekte - Text:
text-gray-900 dark:text-gray-100fü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-fullfür vollständige Rundungtext-xsfür einheitliche Schriftgröße (12px)px-2 py-1fü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", andereclass="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-wrapmitw-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)
- Spalte 1: Datum (
- Header:
bg-slate-100mitfont-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)
- Alter Wert (gelöscht):
- Rundung:
rounded-mdfü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ügenfa-edit- Bearbeitenfa-trash- Löschenfa-save- Speichernfa-times- Schließen/Abbrechenfa-check- Bestätigen/Erfolgfa-exclamation-triangle- Warnungfa-info-circle- Informationfa-search- Suchenfa-filter- Filternfa-cog- Einstellungenfa-user- Benutzerfa-chevron-down- Dropdownfa-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.twigals Base-Template - Form Theme auf
form_div_layout_tailwind.html.twiggesetzt - 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
- Lazy Loading für Bilder:
<img src="/path/to/image.jpg"
loading="lazy"
class="w-full h-auto">
- Alpine.js x-cloak:
<div x-data="{ open: false }" x-cloak>
<!-- Verhindert Flash of Unstyled Content -->
</div>
- 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
- XSS Prevention in Twig:
{# Gut: Auto-escaped #}
{{ user.name }}
{# Vorsicht: Raw HTML #}
{{ content|raw }} {# Nur für vertrauenswürdigen Content! #}
- CSRF Protection:
{{ form_start(form) }} {# CSRF Token automatisch inkludiert #}
- SQL Injection Prevention:
// Gut: Doctrine QueryBuilder
$qb->where('u.email = :email')
->setParameter('email', $email);
Maintenance
- Kommentare für komplexe Logik:
{# Complex Alpine.js state management - do not modify without testing #}
<div x-data="{
activeTab: 'general',
hasChanges: false,
save() { ... }
}">
- TODO-Kommentare:
{# TODO: Migrate to Tailwind pagination once KnpPaginator theme is ready #}
{{ knp_pagination_render(pagination) }}
- Deprecation Warnings:
{# @deprecated Use backend_tailwind_base.html.twig instead #}
{% extends 'backend_base.html.twig' %}
Ressourcen
Dokumentation
- Tailwind CSS: https://tailwindcss.com/docs
- Alpine.js: https://alpinejs.dev
- Symfony Twig: https://twig.symfony.com
- Font Awesome: https://fontawesome.com/v5/search
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.