docs: extract frontend design rules and add presentation persistence fix
Create FRONTEND_DESIGN_RULES.md as the single source of truth for all dashboard UI conventions, including component library (Syncfusion first), component defaults table, layout structure, buttons, dialogs, badges, toasts, form fields, tabs, statistics cards, warnings, color palette, CSS files, loading states, locale rules, and icon conventions (TentTree for skip-holidays events). Move embedded design rules from ACADEMIC_PERIODS_CRUD_BUILD_PLAN.md to the new file and replace with a reference link for maintainability. Update copilot-instructions.md to point to FRONTEND_DESIGN_RULES.md and remove redundant Syncfusion/Tailwind prose from the Theming section. Add reference blockquote to README.md under Frontend Features directing readers to FRONTEND_DESIGN_RULES.md. Bug fix: Presentation events now reliably persist page_progress and auto_progress flags across create, update, and detached occurrence flows so display settings survive round-trips to the API. Files changed: - Created: FRONTEND_DESIGN_RULES.md (15 sections, 340+ lines) - Modified: ACADEMIC_PERIODS_CRUD_BUILD_PLAN.md (extract rules, consolidate) - Modified: .github/copilot-instructions.md (link to new rules file) - Modified: README.md (reference blockquote)
This commit is contained in:
328
FRONTEND_DESIGN_RULES.md
Normal file
328
FRONTEND_DESIGN_RULES.md
Normal file
@@ -0,0 +1,328 @@
|
||||
# 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 }}` |
|
||||
| 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`).
|
||||
Reference in New Issue
Block a user