- 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
329 lines
12 KiB
Markdown
329 lines
12 KiB
Markdown
# 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:
|
||
|
||
```jsx
|
||
<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:
|
||
|
||
```jsx
|
||
<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)
|
||
- `header` prop with specific context text (include item name where applicable)
|
||
- `footerTemplate` always has at minimum: Cancel (`e-flat`) + primary action (`e-primary`)
|
||
- Dialog body wrapped in `<div style={{ padding: 16 }}>`
|
||
- All fields disabled when `formBusy` is 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:
|
||
|
||
```jsx
|
||
<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`:
|
||
|
||
```jsx
|
||
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 operations
|
||
- `e-toast-danger` — errors
|
||
- `e-toast-warning` — non-critical issues or partial results
|
||
- `e-toast-info` — neutral informational messages
|
||
|
||
---
|
||
|
||
## 8. Form Fields
|
||
|
||
All form labels:
|
||
|
||
```jsx
|
||
<label style={{ display: 'block', marginBottom: 8, fontWeight: 500 }}>
|
||
Field label *
|
||
</label>
|
||
```
|
||
|
||
Help/hint text below a field:
|
||
|
||
```jsx
|
||
<div style={{ fontSize: '12px', color: '#666', marginTop: 4 }}>
|
||
Hint text here.
|
||
</div>
|
||
```
|
||
|
||
Empty state inside a card:
|
||
|
||
```jsx
|
||
<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.
|
||
|
||
```jsx
|
||
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:
|
||
|
||
```jsx
|
||
<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:
|
||
|
||
```jsx
|
||
<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`:
|
||
|
||
```jsx
|
||
<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:
|
||
|
||
```jsx
|
||
<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:
|
||
|
||
```tsx
|
||
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`).
|