# Implementation Plan: UI Polish & Design System **Branch**: `005-ui-polish` | **Date**: 2026-05-03 | **Spec**: [spec.md](spec.md) **Input**: Feature specification from `specs/005-ui-polish/spec.md` ## Summary Refine the existing Angular SPA from functional-but-bare to intentional and finished. All changes are purely front-end: a shared design-token layer (CSS custom properties) is introduced in `styles.css`, and each of the five views (library, upload, detail, login, app shell) is updated to use those tokens and to handle loading, empty, and error states consistently. No new dependencies, no new API endpoints. --- ## Technical Context **Language/Version**: TypeScript 5 / Angular 19 (standalone components, no NgModules) **Primary Dependencies**: Angular 19, RxJS 7 (already installed; no new deps added) **Storage**: N/A — UI-only feature **Testing**: Karma / Jasmine (Angular CLI default; `npm test`) **Target Platform**: Browser SPA (desktop-primary, 375 px minimum viewport) **Project Type**: Web application — UI layer only **Performance Goals**: Loading indicators must not flash on sub-150 ms responses **Constraints**: No new npm dependencies; no external icon or component library **Scale/Scope**: Five component files + one global CSS file --- ## Constitution Check *GATE: Must pass before Phase 0 research. Re-checked after Phase 1 design.* | Principle | Status | Notes | |-----------|--------|-------| | §2.1 Strict separation of concerns — UI knows nothing about storage or DB | ✅ Pass | No API or storage changes | | §2.2 Dependency direction — UI → API only | ✅ Pass | No new API calls introduced | | §2.3 Storage abstraction | ✅ Pass | Not touched | | §2.4 Auth abstraction — identity resolution via AuthProvider | ✅ Pass | Auth logic unchanged; FR-006 (hide write controls) already implemented | | §2.6 No speculative abstraction | ✅ Pass | Tokens centralised because all five views use them; no hypothetical interfaces | | §3.3 Error shape | ✅ Pass | UI consumes existing error envelopes; no API change | | §5.1 TDD non-negotiable | ✅ Pass | All template and state changes will have Angular component tests written first | | §5.2 Test pyramid | ✅ Pass | Unit tests (Karma) cover state logic; E2E visual check is the acceptance gate | | §6 Tech stack | ✅ Pass | Angular + TypeScript; no new languages or frameworks added | | §7.2 No hardcoded values | ✅ Pass | Colours/spacing moved to CSS custom properties, not hardcoded further | | §7.3 Linting non-optional | ✅ Pass | ESLint + Prettier enforced; `ng build` type-check must pass | | §8 Scope boundaries | ✅ Pass | UI-only; no multi-user, no OR/NOT tags, no OIDC | **Constitution Check result: ALL GATES PASS** No violations. No complexity justification table required. --- ## Project Structure ### Documentation (this feature) ```text specs/005-ui-polish/ ├── plan.md ← this file ├── research.md ← Phase 0 output (complete) ├── quickstart.md ← Phase 1 output (visual acceptance scenarios) └── tasks.md ← Phase 2 output (/speckit-tasks — not yet created) ``` *No `data-model.md` or `contracts/` — this feature introduces no new data entities and no API surface changes.* ### Source Code (affected files) ```text ui/ └── src/ ├── styles.css ← Add CSS custom properties (design tokens) └── app/ ├── app.component.ts ← Polish header shell ├── library/ │ └── library.component.ts ← Skeleton load, empty state, error state, card polish ├── upload/ │ └── upload.component.ts ← Drop-zone polish, in-progress state, success/error states ├── detail/ │ └── detail.component.ts ← Loading state, not-found state, section organisation └── login/ └── login.component.ts ← Visual alignment with design system ``` --- ## Milestones ### Milestone 1 — Design Token Layer (blocks all other milestones) Extract the shared colour, spacing, and motion values already present across the five components into CSS custom properties on `:root` in `styles.css`. **Deliverable**: `:root` block in `styles.css` with 13 named tokens (see research.md Decision 5). Each existing component still renders identically (tokens match current hard-coded values exactly). `ng build` passes. **Token set**: ``` --bg, --surface, --surface-raised, --border, --border-focus, --text, --text-muted, --accent, --accent-text, --danger, --danger-text, --radius, --radius-chip, --transition ``` --- ### Milestone 2 — Library View (US1) **Loading state**: Replace the current `loading = true` boolean with the 150 ms–debounced spinner pattern (see research.md Decision 3). While loading, render a grid of skeleton cards (same dimensions as real cards) using the shimmer CSS class (see research.md Decision 2). **Empty state**: The existing empty-state `

` is already functional. Polish it: centred layout, muted icon (✦ or similar Unicode), larger text, and a prominent "Upload your first image" link that navigates to `/upload`. **Error state**: Add an `error: boolean` flag to the component. If the `list()` call errors, set `error = true` and render an error card with a retry button that calls `load()` again. **Card polish**: Apply tokens to card background, border-radius, and tag chips. Add a subtle `box-shadow` and `transform: translateY(-2px)` on hover (using `--transition`). Ensure the card thumbnail `` has an `(error)` fallback (see research.md Decision 4). **Responsive**: The existing `auto-fill minmax(200px, 1fr)` grid already handles narrow viewports. Verify it does not overflow at 375 px; reduce min card width to 160 px if needed. --- ### Milestone 3 — Upload View (US2) **Drop-zone polish**: Apply token-based border and background to the existing drag-and-drop zone. Add a dashed border accent colour (`--accent` at 40% opacity) on active drag state. **In-progress state**: The existing `loading` flag already disables the button. Add a visible spinner or animated label ("Uploading…") inside the button while in-flight so the state change is unmistakable. **Success state**: After a successful upload, show a brief success banner (green-tinted surface, tick character) with a "Upload another" link and a "View in library" link. Auto-dismiss after 4 seconds or on navigation. **Error states**: Distinct messages for validation errors (wrong type/size — already returned by API) vs. network/server errors (generic retry). Both displayed inline below the form, not in a modal. **Double-submit prevention**: Already implemented (button disabled while `loading`). Confirm the disabled style is visually clear using `--text-muted` and reduced opacity. --- ### Milestone 4 — Detail View (US3) **Loading state**: Add a skeleton layout while `loading = true`: a grey rectangle at full width for the image area, and two skeleton chip rows below. **Not-found state**: The existing `!image && !loading` condition renders a plain text paragraph. Replace with a styled not-found card: centred layout, muted icon, "Image not found" heading, and a "Back to library" button. **Section organisation**: Visually separate the image area, tags section, and write controls with consistent spacing using `--surface` panels and token-based gaps. Write controls (tag input + delete button) should be grouped in a visually distinct "Owner actions" area when visible. **Tag error**: The existing `tagError` renders inline. Apply `--danger` colour and a left border accent to make it unmistakable. **Broken image**: Add `(error)` handler on the full-size `` in the detail view (inline SVG placeholder showing a broken-link icon). --- ### Milestone 5 — Login View (US4) Apply the token-based design system to the login form: - Centre the card vertically and horizontally on the page - Wrap the form in a `--surface` card with `--radius` and a subtle border - Use token-based input styles matching the library filter bar - Display field-level validation errors using `--danger` colour - The submit button uses the same `--accent` style as the library upload button - In-progress state: button text changes to "Signing in…", button disabled No layout changes to the existing reactive-form structure. --- ### Milestone 6 — App Shell (US5) The existing `app.component.ts` header already conditionally renders the sign-out button. Polish: - Slim top bar: `--surface` background, bottom border using `--border` - App name / logo mark on the left (text only, no image asset) - Sign-out button aligned right using `--text-muted` colour and a simple hover state - Header height: `48px` fixed; does not reflow page content on state change --- ## Implementation Order ``` Milestone 1 (tokens) ↓ Milestone 2 (library) ─┐ Milestone 3 (upload) ├─ can proceed in parallel after M1 Milestone 4 (detail) │ Milestone 5 (login) ─┘ Milestone 6 (shell) ← last (touches app.component which wraps all views) ``` M2–M5 are independent of each other (different component files). M6 is last because the app shell wraps all views and its final state is easiest to validate once the inner views are stable. --- ## Testing Strategy **Unit tests (Karma/Jasmine)**: - All new state variables (`error`, `showSpinner`, skeleton visibility) are tested via component spec files. - Template conditionals (`*ngIf="error"`, `*ngIf="loading"`) are verified with fixture queries. - The `(error)` image fallback handler is tested by simulating an error event. - Existing tests must continue to pass — no regressions. **Visual acceptance (manual, quickstart.md)**: - Each milestone has a corresponding scenario in quickstart.md. - Visual checks are performed in a running `docker compose up` stack. - 375 px viewport check: Chrome DevTools → device toolbar → iPhone SE. **Build gate**: `ng build` must pass with zero errors after every milestone.