- add period-scoped holiday architecture end-to-end - model: scope `SchoolHoliday` to `academic_period_id` - migrations: add holiday-period scoping, academic-period archive lifecycle, and merge migration head - API: extend holidays with manual CRUD, period validation, duplicate prevention, and overlap merge/conflict handling - recurrence: regenerate holiday exceptions using period-scoped holiday sets - improve frontend settings and holiday workflows - bind holiday import/list/manual CRUD to selected academic period - show detailed import outcomes (inserted/updated/merged/skipped/conflicts) - fix file-picker UX (visible selected filename) - align settings controls/dialogs with defined frontend design rules - scope appointments/dashboard holiday loading to active period - add shared date formatting utility - strengthen academic period lifecycle handling - add archive/restore/delete flow and backend validations/blocker checks - extend API client support for lifecycle operations - release/docs updates and cleanup - bump user-facing version to `2026.1.0-alpha.15` with new changelog entry - add tech changelog entry for alpha.15 backend changes - refactor README to concise index and archive historical implementation docs - fix Copilot instruction link diagnostics via local `.github` design-rules reference
12 KiB
Frontend Design Rules
This file is the single source of truth for UI implementation conventions in the dashboard (dashboard/src/).
It applies to all feature work, including new pages, settings tabs, dialogs, and management surfaces.
When proposing or implementing frontend changes, follow these rules unless a specific exception is documented below. This file should be updated whenever a new Syncfusion component is adopted, a color or pattern changes, or an exception is ratified.
1. Component Library — Syncfusion First
Use Syncfusion components as the default choice for every UI element.
The project uses the Syncfusion Material3 theme, registered globally in dashboard/src/main.tsx.
The following CSS packages are imported there and cover all components currently in use:
base, navigations, buttons, inputs, dropdowns, popups, kanban, grids, schedule, filemanager, notifications, layouts, lists, calendars, splitbuttons, icons.
When adding a new Syncfusion component, add its CSS import here — and add the new npm package to optimizeDeps.include in vite.config.ts to avoid Vite import-analysis errors in development.
Use non-Syncfusion elements only when:
- The Syncfusion equivalent does not exist (e.g., native
<input type="file">for file upload) - The Syncfusion component would require significantly more code than a simple HTML element for purely read-only or structural content (e.g.,
<ul>/<li>for plain lists) - A layout-only structure is needed (a wrapper
<div>for spacing is fine)
Never use window.confirm() for destructive action confirmations — use DialogComponent instead.
window.confirm() exists in one place in dashboard.tsx (bulk restart) and is considered a deprecated pattern to avoid.
Do not introduce Tailwind utility classes — Tailwind has been removed from the project.
2. Component Defaults by Purpose
| Purpose | Component | Notes |
|---|---|---|
| Navigation tabs | TabComponent + TabItemDirective |
heightAdjustMode="Auto", controlled with selectedItem state |
| Data list or table | GridComponent |
allowPaging, allowSorting, custom template for status/actions |
| Paginated list | PagerComponent |
When a full grid is too heavy; default page size 5 or 10 |
| Text input | TextBoxComponent |
Use cssClass="e-outline" on form-heavy sections |
| Numeric input | NumericTextBoxComponent |
Always set min, max, step, format |
| Single select | DropDownListComponent |
Always set fields={{ text, value }}; do not add cssClass="e-outline" — only TextBoxComponent uses outline style |
| Boolean toggle | CheckBoxComponent |
Use label prop, handle via change callback |
| Buttons | ButtonComponent |
See section 4 |
| Modal dialogs | DialogComponent |
isModal={true}, showCloseIcon={true}, footer with Cancel + primary |
| Notifications | ToastComponent |
Positioned { X: 'Right', Y: 'Top' }, 3000ms timeout by default |
| Inline info/error | MessageComponent |
Use severity prop: 'Error', 'Warning', 'Info', 'Success' |
| Status/role badges | Plain <span> with inline style |
See section 6 for convention |
| Timeline/schedule | ScheduleComponent |
Used for resource timeline views; see ressourcen.tsx |
| File management | FileManagerComponent |
Used on the Media page for upload and organisation |
| Drag-drop board | KanbanComponent |
Used on the Groups page; retain for drag-drop boards |
| User action menu | DropDownButtonComponent (@syncfusion/ej2-react-splitbuttons) |
Used for header user menu; add to optimizeDeps.include in vite.config.ts |
| File upload | Native <input type="file"> |
No Syncfusion equivalent for raw file input |
3. Layout and Card Structure
Every settings tab section starts with a <div style={{ padding: 20 }}> wrapper.
Content blocks use Syncfusion card classes:
<div className="e-card">
<div className="e-card-header">
<div className="e-card-header-caption">
<div className="e-card-header-title">Title</div>
</div>
</div>
<div className="e-card-content">
{/* content */}
</div>
</div>
Multiple cards in the same tab section use style={{ marginBottom: 20 }} between them.
For full-page views (not inside a settings tab), the top section follows this pattern:
<div style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h2 style={{ margin: 0, fontSize: 24, fontWeight: 600 }}>Page title</h2>
<p style={{ margin: '8px 0 0 0', color: '#6c757d' }}>Subtitle or description</p>
</div>
<ButtonComponent cssClass="e-success" iconCss="e-icons e-plus">New item</ButtonComponent>
</div>
4. Buttons
| Variant | cssClass |
When to use |
|---|---|---|
| Primary action (save, confirm) | e-primary |
Main save or confirm in forms and dialogs |
| Create / add new | e-success + iconCss="e-icons e-plus" |
Top-level create action in page header |
| Destructive (delete, archive) | e-flat e-danger |
Row actions and destructive dialog confirm |
| Secondary / cancel | e-flat |
Cancel in dialog footer, low-priority options |
| Info / edit | e-flat e-primary or e-flat e-info |
Row-level edit and info actions |
| Outline secondary | e-outline |
Secondary actions needing a visible border (e.g., preview URL) |
All async action buttons must be disabled during the in-flight operation: disabled={isBusy}.
Button text must change to indicate the pending state: Speichere…, Erstelle..., Archiviere…, Lösche....
5. Dialogs
All create, edit, and destructive action dialogs use DialogComponent:
isModal={true}showCloseIcon={true}width="500px"for forms (wider if tabular data is shown inside)headerprop with specific context text (include item name where applicable)footerTemplatealways has at minimum: Cancel (e-flat) + primary action (e-primary)- Dialog body wrapped in
<div style={{ padding: 16 }}> - All fields disabled when
formBusyis true
For destructive confirmations (archive, delete), the dialog body must clearly explain what will happen and whether it is reversible.
For blocked actions, use MessageComponent with severity="Warning" or severity="Error" inside the dialog body to show exact blocker details (e.g., linked event count, recurrence spillover).
6. Status and Type Badges
Plain <span> badges with inline style — no external CSS classes needed:
<span style={{
padding: '4px 12px',
borderRadius: '12px',
backgroundColor: color,
color: 'white',
fontSize: '12px',
fontWeight: 500,
display: 'inline-block',
}}>
Label
</span>
See section 12 for the fixed color palette.
Icon conventions: Use inline SVG or icon font classes for small visual indicators next to text. Established precedents:
- Skip-holidays events render a TentTree icon immediately to the left of the main event-type icon; always black (
color: 'black'or no color override). - Recurring events rely on Syncfusion's native lower-right recurrence badge — do not add a custom recurrence icon.
Role badge color mapping (established in users.tsx; apply consistently for any role display):
| Role | Color |
|---|---|
| user | #6c757d (neutral gray) |
| editor | #0d6efd (info blue) |
| admin | #28a745 (success green) |
| superadmin | #dc3545 (danger red) |
7. Toast Notifications
Use a component-local ToastComponent with a ref:
const toastRef = React.useRef<ToastComponent>(null);
// ...
<ToastComponent ref={toastRef} position={{ X: 'Right', Y: 'Top' }} />
Default timeOut: 3000. Use 4000 for messages that need more reading time.
CSS class conventions:
e-toast-success— successful operationse-toast-danger— errorse-toast-warning— non-critical issues or partial resultse-toast-info— neutral informational messages
8. Form Fields
All form labels:
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
Field label *
</label>
Help/hint text below a field:
<div style={{ fontSize: '12px', color: '#666', marginTop: 4 }}>
Hint text here.
</div>
Empty state inside a card:
<div style={{ fontSize: '14px', color: '#666' }}>Keine Einträge vorhanden.</div>
Vertical spacing between field groups: marginBottom: 16.
9. Tab Structure
Top-level and nested tabs use controlled selectedItem state with separate index variables per tab level.
This prevents sub-tab resets when parent state changes.
const [academicTabIndex, setAcademicTabIndex] = React.useState(0);
<TabComponent
heightAdjustMode="Auto"
selectedItem={academicTabIndex}
selected={(e: TabSelectedEvent) => setAcademicTabIndex(e.selectedIndex ?? 0)}
>
<TabItemsDirective>
<TabItemDirective header={{ text: '🗂️ Perioden' }} content={AcademicPeriodsContent} />
<TabItemDirective header={{ text: '📥 Import & Liste' }} content={HolidaysImportAndListContent} />
</TabItemsDirective>
</TabComponent>
Tab header text uses an emoji prefix followed by a German label, consistent with all existing tabs. Each nested tab level has its own separate index state variable.
10. Statistics Summary Cards
Used above grids and lists to show aggregate counts:
<div style={{ marginBottom: 24, display: 'flex', gap: 16 }}>
<div className="e-card" style={{ flex: 1, padding: 16 }}>
<div style={{ fontSize: 14, color: '#6c757d', marginBottom: 4 }}>Label</div>
<div style={{ fontSize: 28, fontWeight: 600, color: '#28a745' }}>42</div>
</div>
</div>
11. Inline Warning Messages
For important warnings inside forms or dialogs:
<div style={{
padding: 12,
backgroundColor: '#fff3cd',
border: '1px solid #ffc107',
borderRadius: 4,
marginBottom: 16,
fontSize: 14,
}}>
⚠️ Warning message text here.
</div>
For structured in-page errors or access-denied states, use MessageComponent:
<MessageComponent severity="Error" content="Fehlermeldung" />
12. Color Palette
Only the following colors are used in status and UI elements across the dashboard. Do not introduce new colors for new components.
| Use | Color |
|---|---|
| Success / active / online | #28a745 |
| Danger / delete / offline | #dc3545 |
| Warning / partial | #f39c12 |
| Info / edit blue | #0d6efd |
| Neutral / archived / subtitle | #6c757d |
| Help / secondary text | #666 |
| Inactive/muted | #868e96 |
| Warning background | #fff3cd |
| Warning border | #ffc107 |
13. Dedicated CSS Files
Use inline styles for settings tab sections and simpler pages.
Only create a dedicated .css file if the component requires complex layout, custom animations, or selector-based styles that are not feasible with inline styles.
Existing precedents: monitoring.css, ressourcen.css.
Do not use Tailwind — it has been removed from the project.
14. Loading States
For full-page loading, use a simple centered placeholder:
<div style={{ padding: 24 }}>
<div style={{ textAlign: 'center', padding: 40 }}>Lade Daten...</div>
</div>
Do not use spinners or animated components unless a Syncfusion component provides them natively (e.g., busy state on ButtonComponent).
15. Locale and Language
All user-facing strings are in German.
Date formatting uses toLocaleString('de-DE') or toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }).
Never use English strings in labels, buttons, tooltips, or dialog headers visible to the end user.
UTC time parsing: The API returns ISO timestamps without a Z suffix (e.g., "2025-11-27T20:03:00"). Always append Z before constructing a Date to ensure correct UTC interpretation:
const utcStr = dateStr.endsWith('Z') ? dateStr : dateStr + 'Z';
const date = new Date(utcStr);
When sending dates back to the API, use date.toISOString() (already UTC with Z).