Files
infoscreen/FRONTEND_DESIGN_RULES.md
Olaf b5f5f30005 feat: period-scoped holiday management, archive lifecycle, and docs/release sync
- 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
2026-03-31 12:25:55 +00:00

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)
  • 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:

<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 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:

<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).