# UI Design Guidelines (Mila / Phoenix LiveView + DaisyUI) ## Purpose This document defines Mila’s **UI system** to ensure **UX consistency**, **accessibility**, and **maintainability** across Phoenix LiveView pages: - consistent DaisyUI usage - typography & spacing - button intent & labeling - list/search/filter UX - tables behavior (row click, tooltips, alignment) - flash/toast UX (position, stacking, auto-dismiss, tones) - standard page skeletons (index/detail/form) - microcopy conventions (German “du” tone) > Engineering practices (LiveView load budget, testing, security, etc.) are defined in `docs/CODE_GUIDELINES.md`. > This document focuses on **visual + UX** consistency and references engineering rules where needed. --- ## 1) Principles ### 1.1 Components first (no raw DaisyUI classes in views) - **MUST:** Use `MvWeb.CoreComponents` (e.g. `<.button>`, `<.header>`, `<.table>`, `<.input>`, `<.flash_group>`, `<.form_section>`). - **MUST NOT:** Write DaisyUI component classes directly in LiveViews/HEEX (e.g. `btn`, `alert`, `table`, `input`, `select`, `tooltip`) unless you are implementing them **inside** CoreComponents. - **MAY:** Use Tailwind for layout only: `flex`, `grid`, `gap-*`, `p-*`, `max-w-*`, `sm:*`, etc. ### 1.2 DaisyUI for look, Tailwind for layout - DaisyUI: component visuals + semantic variants (`btn-primary`, `alert-error`, `badge`, `tooltip`). - Tailwind: spacing, alignment, responsiveness. ### 1.3 Semantics over hard-coded colors - **MUST NOT:** Use “status colors” in views (`bg-green-500`, `text-blue-500`, …). - **MUST:** Express intent via component props / DaisyUI semantic variants. --- ## 2) Page Skeleton & “Chrome” (mandatory) ### 2.1 Standard page layout Every authenticated page should follow the same structure: 1) `<.header>` (title + optional subtitle + actions) 2) content area with consistent vertical rhythm (`mt-6 space-y-6`) 3) optional footer actions for forms **MUST:** Use `<.header>` on every page (except login/public pages). **SHOULD:** Put short explanations into `<:subtitle>` rather than sprinkling random text blocks. **Template:** ```heex <.header> Title <:subtitle>Short explanation of what the page is for. <:actions> <.button variant="primary" navigate={...}>Primary action
## 3) Typography (system) Use these standard roles: | Role | Use | Class | |---|---|---| | Page title (H1) | main page title | `text-xl font-semibold leading-8` | | Subtitle | helper under title | `text-sm text-base-content/70` | | Section title (H2) | section headings | `text-lg font-semibold` | | Helper text | under inputs | `text-sm text-base-content/70` | | Fine print | small hints | `text-xs text-base-content/60` | | Empty state | no data | `text-base-content/60 italic` | | Destructive text | danger | `text-error` | **MUST:** Page titles via `<.header>`. **MUST:** Section titles via `<.form_section title="…">` (for forms) or a consistent section wrapper (if you introduce a `<.card>` later). --- ## 4) States: Loading, Empty, Error (mandatory consistency) ### 4.1 Loading state - **MUST:** Show a consistent loading indicator when data is not ready. - **MUST NOT:** Render empty states while loading (avoid flicker). - **SHOULD:** Prefer “skeleton rows” for tables or a spinner in content area. ### 4.2 Empty state pattern Empty states must be consistent: - short message - optional primary CTA (“Create …”) - optional secondary help link **Example:** ```heex

No members yet.

<.button variant="primary" navigate={~p"/members/new"}>Create member
### 4.3 Error state pattern - **MUST:** Use flash/toast for global errors. - **SHOULD:** Also show inline error state near the relevant content area if the page cannot proceed. --- ## 5) Buttons (intent, labels, variants) ### 5.1 Decision rule: action vs status - **MUST:** Button labels describe **actions** (verb-first): - ✅ Save, Create member, Send invite, Import CSV - ❌ Active, Success, Done (status belongs elsewhere) - **MUST:** Status belongs in badges/labels or read-only text, not in CTAs. ### 5.2 Standard variants (mandatory set) Buttons must be rendered via `<.button>` and mapped to DaisyUI internally. **Supported variants:** - `primary` (main CTA) - `secondary` (supporting) - `neutral` (cancel/back) - `ghost` (low emphasis; table/toolbars) - `outline` (alternative CTA) - `danger` (destructive) - `link` (inline; rare) - `icon` (icon-only) **Sizes:** `sm`, `md` (default), `lg` (rare) ### 5.3 Placement rules - Header CTA inside `<.header><:actions>`. - Form footer: primary right; cancel/secondary left. - Tables: use `ghost`/`icon` for row actions (avoid `primary` inside rows). ### 5.4 Primary vs Secondary (UX consistency rules) #### One primary action per screen - MUST: Each screen/section has at most one **primary** action (e.g. Save, Create, Start import). - SHOULD: Additional actions are secondary/neutral/ghost, not additional primary. #### Primary vs Secondary meaning - Primary = the most important/most common action to complete the user task. - Secondary = supporting actions (Cancel/Back/Edit in tool contexts), lower emphasis. #### Order and placement (choose and apply consistently) We follow these ordering rules: - MUST: Order buttons by priority: **Primary → Secondary → Tertiary**. - Forms: Decide once (primary-left OR primary-right) and apply everywhere. - Dialogs/confirmations: Place the confirmation action consistently (e.g. trailing edge, confirmation closest to edge). #### Cancel/Back consistency - MUST: Cancel/Back is **never** styled as primary. - MUST: Cancel/Back placement is consistent across the app (same side, same label). #### Implementation requirement - MUST: Use CoreComponents (`<.button>`) with `variant`/`size` props. - MUST NOT: Use ad-hoc classes like `class="secondary"` on `<.button>`; instead extend CoreComponents to support `secondary`, `neutral`, `ghost`, `danger`, etc. #### Ghost buttons (accessibility requirements) Ghost buttons are allowed for low-emphasis actions (toolbars, table actions), but: - MUST: Focus indicator is clearly visible (do not remove outlines). - MUST: UI contrast for the control (and meaningful icons) meets WCAG non-text contrast (≥ 3:1). - MUST: Icon-only ghost buttons provide an accessible name (`aria-label`) and preferably a tooltip. - SHOULD: Hit target is large enough for touch/motor accessibility (recommend ~44x44px). If these cannot be met, use `secondary`/`outline` instead of `ghost`. --- ## 6) Forms (structure + interaction rules) ### 6.1 Structure - **MUST:** Forms are grouped into `<.form_section title="…">`. - **MUST:** All inputs via `<.input>`. ### 6.2 Validation timing (consistent UX) - **MUST:** Validate on submit always. - **SHOULD:** Validate on change only where it helps; use debounce to avoid “error spam”. - **MUST:** Define a consistent “when errors appear” rule: - Preferred: show field errors after first submit attempt OR after the field has been touched (pick one and apply everywhere). > Engineering note (implementation): follow LiveView load budget in `CODE_GUIDELINES.md` (no DB reads on `phx-change` by default). ### 6.3 Required fields - **MUST:** Required fields are marked consistently (UI indicator + accessible text). - **SHOULD:** If required-ness is configurable via settings, display it consistently in the form. --- ## 7) Lists, Search & Filters (mandatory UX consistency) ### 7.1 Standard filter/search bar pattern - **MUST:** All list pages use the same search/filter placement (choose one layout and apply everywhere). - Recommended: top area above the table, aligned with page actions. - **MUST:** Always provide “Clear filters” when filters are active. - **MUST:** Filter state is reflected in URL params (so reload/back/share works consistently). ### 7.2 URL behavior (UX rule) - Use `push_patch` for in-page state changes: filters, sorting, pagination, tabs. - Use `push_navigate` for actual page transitions: details, edit, new. --- ## 8) Tables (mandatory UX) ### 8.1 Default behavior: row click opens details - **DEFAULT:** Clicking a row navigates to the details page. - **EXCEPTIONS:** Highly interactive rows may disable row-click (document why). - **Row outline (CoreComponents):** When `row_click` is set, rows get a subtle hover and focus-within ring (theme-friendly). Use `selected_row_id` to show a stronger selected outline (e.g. from URL `?highlight=id` or last selection); the Back link from detail can use `?highlight=id` so the row is visually selected when returning to the index. **IMPORTANT (correctness with our `<.table>` CoreComponent):** Our table implementation attaches the `phx-click` to the **``** when `row_click` is set. That means click events bubble from inner elements up to the cell unless we stop propagation. So, for interactive elements inside a clickable row, you must **stop propagation using `Phoenix.LiveView.JS.stop_propagation/1`**, not a custom attribute. ✅ Correct pattern (one click handler that both stops propagation and triggers an event): ```heex <.table id="members" rows={@members} row_click={fn m -> JS.navigate(~p"/members/#{m.id}") end} > <:col :let={m} label="Name"> <%= m.last_name %>, <%= m.first_name %> <:col :let={m} label="Newsletter"> JS.stop_propagation()} /> <:action :let={m}> <.button variant="ghost" size="sm" navigate={~p"/members/#{m.id}/edit"} phx-click={JS.stop_propagation()} > Edit Notes: - The checkbox uses `phx-click={JS.push(...) |> JS.stop_propagation()}` so it won’t trigger row navigation. - The Edit button also stops propagation to avoid accidental row navigation when clicked. ### 8.2 Tooltips (mandatory where needed) - **MUST:** Tooltips for: - icon-only actions - truncated content - status badges that require explanation - **MUST:** Provide tooltips via a shared wrapper (recommended `<.tooltip>` CoreComponent). - **MUST NOT:** Scatter ad-hoc tooltip markup in views. ### 8.3 Alignment & density conventions - **MUST:** Text columns left-aligned. - **MUST:** Numeric columns right-aligned. - **MUST:** Action column right-aligned. - **SHOULD:** Table density is consistent: - default density for most tables - a single “dense” option only if needed (via a prop, not per-page random classes) ### 8.4 Truncation standard - **MUST:** Truncate long values consistently (same max widths for name/email-like fields). - **MUST:** Tooltip reveals full value when truncated. ### 8.5 Loading/Lists/Tables: keep filters visible on desktop - On **desktop (lg: breakpoint)** only: list pages with large datasets (e.g. Members overview) keep the page header and filter/search bar visible while the user scrolls. Only the table body scrolls inside a constrained area (`lg:max-h-[calc(100vh-)] lg:overflow-auto`). This preserves context and avoids losing filters when scrolling. - On **mobile**, sticky headers are not used; the layout uses normal flow (header and table scroll with the page) to preserve vertical space. - When the table is inside such a scroll container, use the CoreComponents table’s `sticky_header={true}` so the table’s `` stays sticky within the scroll area on desktop (`lg:sticky lg:top-0`, opaque background `bg-base-100`, z-index). Sticky areas must not overlap content at 200% zoom; focus order must remain header → filters → table. --- ## 9) Flash / Toast messages (mandatory UX) ### 9.1 Location + stacking - **MUST:** Position flash/toasts at the bottom of the viewport (pick bottom-right or bottom-center; be consistent). - **MUST:** Stack all flash messages with consistent spacing. - **SHOULD:** Newest appears on top. ### 9.2 Auto-dismiss - **MUST:** Flash messages disappear automatically: - info/success: 4–6s - warning: 6–8s - error: 8–12s (or manual dismiss for critical errors) - **MUST:** Keep a dismiss button for accessibility and user control. - **Status:** Not yet implemented. See [feature-roadmap](docs/feature-roadmap.md) → Flash: Auto-dismiss and consistency. ### 9.3 Variants (unified) - Supported semantic variants: `info`, `success`, `warning`, `error`. - **MUST:** Use the same variants for all flash types, : e.g. `success` for copy success, no separate tone or styling. This keeps flash UX consistent across the app. ### 9.4 Accessibility - Flash must work with screen readers (live region behavior belongs in the flash component implementation). - See `CODE_GUIDELINES.md` Accessibility → live regions. --- ## 10) Mutations & feedback patterns (create/update/delete/import) ### 10.1 Mutation feedback is always two-part For create/update/delete: - **MUST:** Show a toast/flash message - **MUST:** Show a visible UI update (navigate, row removed, values updated) No “silent success”. ### 10.2 Destructive actions: one standard confirmation pattern - **MUST:** All destructive actions use the same confirm style and wording conventions. - Choose one approach and standardize: - `JS.confirm("…")` everywhere (simple, consistent) - or a modal component everywhere (more flexible, more work) **Recommended copy style:** - Title/confirm text is clear and specific (what will be deleted, consequences). - Buttons: `Cancel` (neutral) + `Delete` (danger). --- ## 11) Detail pages (consistent structure) Detail pages should not drift into random layouts. **MUST:** Use consistent structure: - header with primary action (Edit) - sections/cards for grouped info - “Danger zone” section at bottom for destructive actions --- ## 12) Navigation rules (UX consistency) - **MUST:** `push_patch` for in-page state: sorting, filtering, pagination, tabs. - **MUST:** `push_navigate` for page transitions: detail/edit/new. - **SHOULD:** Back button behavior must feel predictable (URL reflects state). --- ## 13) Microcopy conventions (German “du” tone + glossary) ### 13.1 Tone - **MUST:** All German user-facing text uses informal address (“du”). - **MUST:** Use consistent verbs for common actions: - Save: “Speichern” - Cancel: “Abbrechen” - Delete: “Löschen” - Edit: “Bearbeiten” ### 13.2 Preferred terms (starter glossary) - Member: “Mitglied” - Fee/Contribution: “Beitrag” - Settings: “Einstellungen” - Group: “Gruppe” - Import/Export: “Import/Export” - Clear filters: “Filter zurücksetzen” (use when filters are active; button label in list/filter UX) Add to this glossary when new terminology appears. --- ## 14) Destructive actions: Delete flow (canonical) This section defines the canonical delete flow for list/detail/form resources (e.g. members). Use it as the single pattern; do not introduce a second pattern elsewhere. ### Tables: no row action buttons - **MUST NOT:** Show Edit or Delete as row action buttons (or dropdown actions) in list/table views. - **MUST:** Remove any existing edit/delete row actions from tables so that the only way to edit or delete is via the flow below. ### Navigation: row click → details - **MUST:** Clicking a table row navigates to the resource details page (e.g. `/members/:id`). - **MUST NOT:** Use the table for primary edit/delete actions. ### Edit: from details header, not from table - **MUST:** Provide a clear primary “Edit” CTA in the details page header (e.g. “Edit member”). - **MUST:** Edit is reached from the details page (e.g. “Edit member” button in header), not from the list/table. ### Delete: only via “Danger zone” - **MUST:** Delete is available only in a dedicated “Danger zone” section at the bottom of the page. - **MUST:** Use the same “Danger zone” on both the details page and the edit form when the user is authorized to destroy the resource. - **MUST NOT:** Place delete in the table, in the header next to Edit, or in any other location outside the Danger zone. ### Danger zone layout and wording (canonical pattern) - **Heading:** “Danger zone” (H2, `aria-labelledby` for the section, semantic colour e.g. `text-error`). - **Explanatory text:** One short paragraph stating that the action cannot be undone and mentioning consequences (e.g. related data removed). Use `text-base-content/70` for the text. - **Layout:** Section with heading outside a bordered box; content inside a single bordered, rounded box (`border border-base-300 rounded-lg p-4 bg-base-100`). - **Button:** One destructive action only (e.g. “Delete member”). Use CoreComponents `<.button variant="danger">`. No primary or secondary actions mixed inside the Danger zone. ### Confirmation and button semantics - **MUST:** Use a single confirmation step (e.g. `data-confirm` / browser confirm or one modal). Do not introduce a second confirmation pattern in this flow. - **Confirm copy:** Message must include the resource name and state that the action “cannot be undone” (e.g. “Are you sure you want to delete %{name}? This action cannot be undone.”). - **Button:** Accessible label (visible text + `aria-label` that includes the resource name, e.g. “Delete member %{name}”). Icon (e.g. trash) is optional and must not replace the text label for the primary action. ### Accessibility - **MUST:** Button has an accessible name (`aria-label` when icon-only or in addition to visible text as above). - **MUST:** Focus and keyboard: button is focusable and activatable via keyboard; focus management must not trap the user. - **MUST:** Contrast and visibility: Danger zone heading and button use semantic danger styling with sufficient contrast (WCAG AA). ### Authorization visibility - **MUST:** Show the Danger zone only when the current user is authorized to destroy the resource (e.g. `can?(current_user, :destroy, resource)`). - **MUST NOT:** Show the Danger zone or the delete button when the user cannot destroy the resource; no “disabled” delete button for unauthorized users. ---