--- phase: 5 slug: dashboard-calendar status: draft shadcn_initialized: false preset: none created: 2026-06-23 --- # Phase 05 — UI Design Contract > Visual and interaction contract for the Dashboard & Calendar phase. Generated by gsd-ui-researcher, verified by gsd-ui-checker. --- ## Design System | Property | Value | |----------|-------| | Tool | none (shadcn token convention used manually) | | Preset | not applicable | | Component library | Custom components following shadcn CSS variable naming (--background, --foreground, --card, --primary, etc.) | | Icon library | Inline SVG (established pattern across all phases) | | Font | Inter, system-ui, -apple-system, sans-serif | **Note:** Project uses shadcn-compatible CSS variable tokens in OKLCH color space (established Phase 01) but does not use `components.json` or the shadcn CLI. All components are hand-written. Phase 05 continues this pattern for consistency. --- ## Spacing Scale Declared values (must be multiples of 4): | Token | Value | Usage | |-------|-------|-------| | xs | 4px | Icon gaps, inline padding, grid gap between widget cells | | sm | 8px | Compact element spacing, widget internal padding minimum | | md | 16px | Default element spacing, widget content padding | | lg | 24px | Section padding, settings form group spacing | | xl | 32px | Layout gaps, dashboard grid outer padding | | 2xl | 48px | Empty state vertical spacing | | 3xl | 64px | Page-level spacing (not used in this phase) | Exceptions: - Grid cell size: 40px per grid unit (react-grid-layout rowHeight). Derived from 8-point scale (5 x 8px). - Grid margin/padding: 16px (consistent with `md` token) - Touch target minimum: 44px for drag handles and widget action buttons (accessibility) --- ## Typography | Role | Size | Weight | Line Height | Usage | |------|------|--------|-------------|-------| | Label | 12px | 400 (regular) | 1.4 | Widget type labels, category badges, timestamps, muted hints, markdown code blocks (monospace) | | Body | 14px | 400 (regular) | 1.5 | Widget content, settings form labels, descriptions, Notes widget title (semibold variant) | | Heading | 18px | 600 (semibold) | 1.3 | Settings section headings, widget catalog title | | Display | 28px | 600 (semibold) | 1.2 | Clock widget time display | **Exactly 2 weights used:** 400 (regular) for body/label text, 600 (semibold) for headings, display, and inline emphasis (bold, Notes title). Labels are differentiated from body by smaller size (12px) and `muted-foreground` color — no third weight needed. **Clock widget responsive exception:** Digital clock uses Display size (28px) at weight 600. When widget is resized larger (4x4+), clock scales proportionally via CSS `clamp()` up to approximately 40px. This is NOT a fifth type size — it is the Display size with responsive scaling applied via `font-size: clamp(28px, 4vw, 40px)`. Analog clock face uses SVG, not text. **Markdown editor (Notes widget):** Inherits Body typography (14px/400/1.5). Bold uses weight 600. Code blocks use Label size (12px) in monospace. Notes widget title uses Body size (14px) at weight 600 (semibold) — differentiated from content by weight, not a separate size. --- ## Color All values reference existing CSS custom properties from `globals.css`. | Role | Token | Light Value | Dark Value | Usage | |------|-------|-------------|------------|-------| | Dominant (60%) | `--background` | oklch(0.99 0 0) | oklch(0.23 0.01 260) | Dashboard background, settings page background | | Secondary (30%) | `--card` / `--sidebar` | oklch(1 0 0) / oklch(0.97 0 0) | oklch(0.27 0.01 260) / oklch(0.20 0.01 260) | Widget cards, settings sub-sidebar, calendar event cards | | Accent (10%) | `--primary` | oklch(0.91 0.19 102) | oklch(0.91 0.19 102) | See reserved-for list below | | Destructive | `--destructive` | oklch(0.55 0.2 27) | oklch(0.55 0.2 27) | Widget delete button (in edit mode only) | **Accent reserved for:** - Edit-mode toggle button (active state) — pencil icon button top-right - "Widget hinzufuegen" / "Add widget" button in edit mode - Active/selected widget border highlight during drag - Save confirmation indicator when leaving edit mode - Settings navigation active item indicator - Calendar event accent stripe (left border on event cards) **Additional semantic colors for calendar:** - Calendar source color dots: Each calendar source gets a user-assigned color from a fixed palette of 8 colors. Colors are rendered as small dots (8px circle) next to event entries. Palette: `oklch(0.65 0.2 27)` (red), `oklch(0.65 0.2 150)` (green), `oklch(0.65 0.2 260)` (blue), `oklch(0.65 0.2 310)` (purple), `oklch(0.75 0.15 70)` (orange), `oklch(0.65 0.15 200)` (teal), `oklch(0.65 0.15 340)` (pink), `oklch(0.55 0 0)` (gray). --- ## Component Inventory ### Dashboard Grid | Component | Description | Source | |-----------|-------------|--------| | `DashboardGrid` | react-grid-layout wrapper. 12-column grid, rowHeight 40px, margin [16, 16]. Handles drag/drop and resize. | New component | | `EditModeToggle` | Pencil icon button, top-right of dashboard area. Toggles `isEditing` state. Active state: primary background. | New component (D-01) | | `WidgetCard` | Wrapper for all widgets. Rounded card with border, shadow-sm. In edit mode: shows drag handle (top), resize handle (bottom-right), delete button (top-right). | New component | | `AddWidgetButton` | Visible only in edit mode. Opens widget catalog modal. Uses primary color. | New component (D-01) | | `WidgetCatalog` | Modal dialog showing available widget types as selectable cards (4 types: Clock, Search, Calendar, Notes). Each card shows icon + name + brief description. Selecting adds widget to grid at next available position. | New component | ### Widgets | Component | Min Size (cols x rows) | Description | |-----------|----------------------|-------------| | `ClockWidget` | 2x2 | Digital clock (default). Shows time in configured timezone. Optional date below. Configurable style (analog/digital) in settings. | | `SearchWidget` | 3x2 | Left: provider dropdown. Center: search input. Right: search button. Opens new browser tab. | | `CalendarWidget` | 3x3 | Shows upcoming events list from selected calendar sources. Read-only. Each event: time, title, source color dot. | | `NotesWidget` | 2x3 | Markdown editor with compact toolbar (Bold, Italic, Underline, List, Checkbox, Link, Code). Autosave with 1s debounce. Editable title above content. | ### Settings Page | Component | Description | |-----------|-------------| | `SettingsLayout` | Layout with sub-sidebar (left, 220px) and content area (right). Accessible via user avatar menu in header. | | `SettingsSubSidebar` | Left navigation within settings. Phase 05 categories: "Dashboard" (top-level), with sub-items: "Widgets", "Kalender" / "Calendar". | | `WidgetSettingsPanel` | Lists all placed widget instances. Each expandable to show widget-specific config (clock: timezone + date toggle + style; search: custom providers; notes: title). | | `CalendarSettingsPanel` | Manages calendar sources. List of configured sources + "Quelle hinzufuegen" / "Add source" button. Source form: name, type (CalDAV/Exchange/ICS), URL, credentials (optional). Toggle per source for visibility in widget. | --- ## Interaction Contracts ### Edit Mode Flow (D-01, D-02, D-03) 1. **Default state:** Dashboard shows widgets in their saved layout. No drag handles visible. Widgets are interactive (clock ticks, notes editable, search functional, calendar scrollable). 2. **Enter edit mode:** User clicks pencil icon (top-right, positioned at `right: 16px, top: 16px` relative to dashboard content area). Icon transitions to checkmark icon. Background gets subtle overlay `oklch(0 0 0 / 0.02)` in light, `oklch(1 1 0 / 0.02)` in dark. 3. **In edit mode:** Grid lines become visible (dashed border, `--border` color at 30% opacity). Each widget shows: drag handle bar (top, 6px height, `--muted` color), delete X button (top-right corner, `--destructive` on hover), resize handle (bottom-right corner, diagonal grip lines). "Widget hinzufuegen" floating button appears bottom-right. 4. **Drag/resize:** Widgets snap to grid. Other widgets reflow automatically (react-grid-layout default behavior). Dragged widget gets `ring-2 ring-primary` highlight and slight scale `scale(1.02)` with `shadow-lg`. 5. **Exit edit mode:** User clicks checkmark icon. Layout saves via API call. Checkmark transitions back to pencil icon. Edit mode UI elements (handles, grid lines, add button) disappear with 150ms fade. 6. **Empty dashboard (D-02):** Shows centered empty state: grid icon (48x48, muted), heading "Keine Widgets aktiv" / "No active widgets", body text "Klicken Sie auf Bearbeiten, um Widgets hinzuzufuegen" / "Click edit to add widgets", and the edit-mode pencil button is visible. ### Widget Catalog Modal 1. **Trigger:** "Widget hinzufuegen" button (only in edit mode). 2. **Appearance:** Centered modal, 480px max-width, with `bg-card` background, border, shadow-xl. Title: "Widget hinzufuegen" / "Add widget". 3. **Content:** 2x2 grid of selectable widget type cards. Each card: 56px icon area (muted background), widget name (14px semibold), one-line description (12px muted-foreground). 4. **Selection:** Click on card immediately adds widget instance to dashboard at first available grid position. Modal closes. Dashboard remains in edit mode. 5. **Close:** X button top-right or click outside modal. 6. **Widget types shown:** - Uhr / Clock: clock icon, "Zeigt die aktuelle Uhrzeit an" / "Shows the current time" - Suchleiste / Search: search icon, "Schnellsuche im Web" / "Quick web search" - Kalender / Calendar: calendar icon, "Kommende Termine" / "Upcoming events" - Notizen / Notes: file-text icon, "Freitext-Notizen mit Markdown" / "Free-text notes with Markdown" ### Widget Delete (D-07) 1. **Trigger:** Delete X button on widget card (visible only in edit mode). 2. **Behavior:** Immediate removal from grid (no confirmation dialog -- user can re-add). Layout reflows. 3. **Persistence:** Deletion is persisted when user exits edit mode (saves layout). ### Settings Navigation (D-19, D-20) 1. **Entry point:** User clicks avatar in header -> dropdown menu -> "Einstellungen" / "Settings" link. Navigates to `/settings`. 2. **Layout:** Sub-sidebar (220px, `bg-sidebar` background, left border `border-sidebar-border`) with settings categories. Content area fills remaining width. 3. **Active indicator:** Active settings category has `bg-sidebar-accent text-sidebar-accent-foreground` background, matching main sidebar active state pattern. 4. **Back navigation:** Settings page header includes "Zurueck zum Dashboard" / "Back to Dashboard" link with left arrow icon. ### Calendar Source Management (D-08, D-09, D-11) 1. **Source list:** Each source shows: color dot, name, type badge (CalDAV/Exchange/ICS), toggle switch for widget visibility, edit/delete actions. 2. **Add source form:** Appears below list or in expandable section. Fields: Name (text), Type (select: CalDAV, Exchange, ICS), URL (text), Username (text, optional for CalDAV/Exchange), Password (password, optional for CalDAV/Exchange). Save button. 3. **Validation:** URL field validates URL format. Name is required. Type is required. 4. **Connection test:** After save, system attempts to fetch events. Success: green checkmark next to source. Failure: orange warning icon with tooltip showing error. ### Search Widget Interaction (D-14, D-15) 1. **Layout:** Horizontal. Left: dropdown (120px width, shows current provider name). Center: text input (flex-1). Right: search button (icon + "Suchen" / "Search" on wider sizes, icon-only on narrow). 2. **Behavior:** Enter key or button click opens `{provider_url}?q={query}` in new browser tab (`window.open`). 3. **Default providers:** Google, Bing, DuckDuckGo. Selected provider persists per widget instance. ### Notes Widget Interaction (D-16, D-17, D-18) 1. **Layout:** Top: editable title (14px semibold, contenteditable or input — Body size at weight 600). Below: compact toolbar (icon buttons: B, I, U, List, Checkbox, Link, Code). Below: editor area (fills remaining widget height, scrollable). 2. **Autosave:** Content saves to API 1000ms after last keystroke. No save indicator needed (saves silently). On API error: show small red dot in widget top-right corner. 3. **Markdown rendering:** Live WYSIWYG editing (not split-view). Toolbar buttons apply formatting inline. ### Responsive Behavior (D-21, D-22) 1. **Desktop (>= 768px):** 12-column grid. Grid scales proportionally with container width. `--current-sidebar-width` CSS variable adjusts available space. 2. **Mobile (< 768px):** Widgets stack vertically in a single column. Each widget takes full width. Order matches grid layout top-to-bottom, left-to-right. Edit mode is available but drag/resize is disabled -- only add/delete widgets. --- ## Copywriting Contract ### German (Primary) | Element | Copy | |---------|------| | Primary CTA (edit mode) | "Widget hinzufuegen" | | Edit mode toggle (enter) | aria-label: "Dashboard bearbeiten" | | Edit mode toggle (exit/save) | aria-label: "Aenderungen speichern" | | Empty state heading | "Keine Widgets aktiv" | | Empty state body | "Klicken Sie auf Bearbeiten, um Widgets hinzuzufuegen." | | Widget catalog title | "Widget hinzufuegen" | | Widget delete tooltip | "Widget entfernen" | | Settings link (header menu) | "Einstellungen" | | Settings back link | "Zurueck zum Dashboard" | | Settings category: widgets | "Widgets" | | Settings category: calendar | "Kalender" | | Calendar add source CTA | "Quelle hinzufuegen" | | Calendar source empty state | "Keine Kalenderquellen eingerichtet. Fuegen Sie eine Quelle hinzu, um Termine anzuzeigen." | | Calendar widget empty (no sources) | "Keine Kalenderquellen konfiguriert" | | Calendar widget empty (no events) | "Keine anstehenden Termine" | | Calendar connection success | "Verbindung erfolgreich" | | Calendar connection error | "Verbindung fehlgeschlagen. Bitte ueberpruefen Sie die URL und Zugangsdaten." | | Search widget placeholder | "Suchen..." | | Notes widget default title | "Notiz" | | Notes autosave error indicator | tooltip: "Speichern fehlgeschlagen" | | Error state (layout load) | "Dashboard konnte nicht geladen werden. Bitte laden Sie die Seite neu." | | Error state (widget save) | "Aenderungen konnten nicht gespeichert werden. Bitte versuchen Sie es erneut." | | Clock widget date format | "dd. MMMM yyyy" (e.g. "23. Juni 2026") | | Settings source delete confirm | "Moechten Sie diese Kalenderquelle wirklich loeschen?" | | Settings source delete CTA | "Quelle loeschen" | | Settings provider delete confirm | "Moechten Sie diesen Suchanbieter wirklich loeschen?" | | Settings provider delete CTA | "Anbieter loeschen" | | Destructive cancel button | "Abbrechen" | ### English | Element | Copy | |---------|------| | Primary CTA (edit mode) | "Add widget" | | Edit mode toggle (enter) | aria-label: "Edit dashboard" | | Edit mode toggle (exit/save) | aria-label: "Save changes" | | Empty state heading | "No active widgets" | | Empty state body | "Click edit to add widgets to your dashboard." | | Widget catalog title | "Add widget" | | Widget delete tooltip | "Remove widget" | | Settings link (header menu) | "Settings" | | Settings back link | "Back to Dashboard" | | Settings category: widgets | "Widgets" | | Settings category: calendar | "Calendar" | | Calendar add source CTA | "Add source" | | Calendar source empty state | "No calendar sources configured. Add a source to display events." | | Calendar widget empty (no sources) | "No calendar sources configured" | | Calendar widget empty (no events) | "No upcoming events" | | Calendar connection success | "Connection successful" | | Calendar connection error | "Connection failed. Please check the URL and credentials." | | Search widget placeholder | "Search..." | | Notes widget default title | "Note" | | Notes autosave error indicator | tooltip: "Save failed" | | Error state (layout load) | "Could not load dashboard. Please reload the page." | | Error state (widget save) | "Could not save changes. Please try again." | | Clock widget date format | "MMMM dd, yyyy" (e.g. "June 23, 2026") | | Settings source delete confirm | "Are you sure you want to delete this calendar source?" | | Settings source delete CTA | "Delete source" | | Settings provider delete confirm | "Are you sure you want to delete this search provider?" | | Settings provider delete CTA | "Delete provider" | | Destructive cancel button | "Cancel" | ### Destructive Actions | Action | Confirmation Approach | |--------|----------------------| | Delete widget (edit mode) | No confirmation (instant, re-addable, not persisted until save) | | Delete calendar source | Confirmation dialog: heading + body text + "Quelle loeschen"/"Delete source" button (destructive color) + "Abbrechen"/"Cancel" button | | Delete custom search provider | Confirmation dialog: heading + body text + "Anbieter loeschen"/"Delete provider" button (destructive color) + "Abbrechen"/"Cancel" button | --- ## Registry Safety | Registry | Blocks Used | Safety Gate | |----------|-------------|-------------| | shadcn official | not initialized — no CLI blocks used | not applicable | | Third-party | none | not applicable | **Note:** No component registry is used. All components are hand-written following shadcn CSS variable conventions. react-grid-layout is installed as an npm package (not a registry block). Markdown editor library selection is at executor discretion (recommended: lightweight option like @tiptap/starter-kit or similar, evaluated during planning). --- ## Widget Size Constraints (D-06) | Widget Type | Min (cols x rows) | Default (cols x rows) | Max | |-------------|-------------------|----------------------|-----| | Clock | 2 x 2 | 2 x 2 | 4 x 4 | | Search | 3 x 2 | 6 x 2 | 12 x 2 | | Calendar | 3 x 3 | 4 x 6 | 12 x 12 | | Notes | 2 x 3 | 3 x 4 | 12 x 12 | --- ## State Management | Store | Purpose | Persistence | |-------|---------|-------------| | `dashboard-store` (Zustand) | Client-side edit mode state, current layout, widget instances, dirty flag | Memory only (layout fetched from API on mount) | | API: `GET /dashboard/layout` | Fetch user's saved layout + widget configs | PostgreSQL via Prisma | | API: `PUT /dashboard/layout` | Save layout + widget configs | PostgreSQL via Prisma | | API: `GET /calendar/sources` | Fetch user's calendar sources | PostgreSQL via Prisma | | API: `POST /calendar/sources` | Add calendar source | PostgreSQL via Prisma | | API: `DELETE /calendar/sources/:id` | Delete calendar source | PostgreSQL via Prisma | | API: `GET /calendar/events` | Fetch events from all active sources | Proxied from external calendar servers | --- ## Accessibility | Concern | Implementation | |---------|----------------| | Edit mode toggle | `aria-label` updates based on state. `aria-pressed` attribute. | | Drag and drop | react-grid-layout provides keyboard support via arrow keys when focused. Each widget has `role="article"` and `aria-label` with widget type + instance name. | | Widget catalog modal | `role="dialog"`, `aria-modal="true"`, focus trap, Escape to close. Widget type cards are buttons with `aria-label`. | | Color contrast | All text meets WCAG 2.1 AA (4.5:1 for body, 3:1 for large text). Calendar color dots are decorative only -- source name is always shown as text. | | Settings navigation | Sub-sidebar uses `role="navigation"` with `aria-label="Einstellungen"` / `"Settings"`. Active item uses `aria-current="page"`. | | Keyboard navigation | Tab order: edit toggle -> widgets (left-to-right, top-to-bottom) -> add widget button (in edit mode). Settings: sub-sidebar items -> form fields. | --- ## Checker Sign-Off - [ ] Dimension 1 Copywriting: PASS - [ ] Dimension 2 Visuals: PASS - [ ] Dimension 3 Color: PASS - [ ] Dimension 4 Typography: PASS - [ ] Dimension 5 Spacing: PASS - [ ] Dimension 6 Registry Safety: PASS **Approval:** pending