Files
reactbin/specs/005-ui-polish/tasks.md
agatha 9246f75fdd Feat: Polish Angular UI with cohesive design system
Introduces a shared CSS custom property token layer and applies it
across all five views (library, upload, detail, login, app shell).
Each view now has intentional loading, empty, and error states.

- styles.css: 13 design tokens on :root; shimmer skeleton animation
- Library: 150ms-debounced skeleton loading, empty state with /upload
  link, error card with retry, card hover lift, broken-image fallback
- Upload: token-styled drop-zone, Uploading… spinner, 4s success
  banner, distinct validation vs. network error messages
- Detail: image skeleton, network error card (separate from 404
  not-found card), Owner actions panel, danger tag error styling,
  broken-image fallback
- Login: vertically centred surface card, danger field/server errors,
  Signing in… disabled button
- App shell: 48px fixed header, app name left, sign-out right, no
  reflow on auth state change
- All 24 ESLint errors resolved (including pre-existing auth spec
  issues); ng build and ng lint pass clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 20:03:56 +00:00

243 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tasks: UI Polish & Design System
**Input**: Design documents from `specs/005-ui-polish/`
**Prerequisites**: plan.md ✓, spec.md ✓, research.md ✓, quickstart.md ✓
**Tests**: Component spec tests are included per §5.1 (TDD non-negotiable). Tests are written first and must fail before implementation begins. Karma/Jasmine via Angular CLI test runner.
**Organization**: Phase 2 (design token layer) blocks all user story phases. User story phases (Phase 37) are independent of each other and can proceed in parallel after Phase 2 completes.
**Milestone mapping** (cross-reference with `plan.md` and `quickstart.md`):
Phase 2 = M1 (Tokens) | Phase 3 = M2 (Library) | Phase 4 = M3 (Upload) | Phase 5 = M4 (Detail) | Phase 6 = M5 (Login) | Phase 7 = M6 (Shell)
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies on incomplete tasks)
- **[Story]**: Which user story this task belongs to (US1US5)
- All component files are under `ui/src/app/`
---
## Phase 1: Setup
**Purpose**: Verify baseline state before any changes are made.
- [X] T001 Confirm `ng build` passes with zero errors in `ui/` (baseline gate before any changes)
---
## Phase 2: Foundational — M1: Design Token Layer
**Purpose**: Establish the shared CSS custom property layer in `ui/src/styles.css`. This is the blocking prerequisite for all five user story phases — no component work begins until these tokens exist.
**⚠️ CRITICAL**: No user story phase can begin until T004 passes.
- [X] T002 Add 13 CSS custom property tokens to `:root` in `ui/src/styles.css`: `--bg`, `--surface`, `--surface-raised`, `--border`, `--border-focus`, `--text`, `--text-muted`, `--accent`, `--accent-text`, `--danger`, `--danger-text`, `--radius`, `--radius-chip`, `--transition` — use exact values from research.md Decision 5
- [X] T003 Add `@keyframes shimmer` animation and `.skeleton` utility class to `ui/src/styles.css` using the gradient pattern from research.md Decision 2
- [X] T004 Confirm `ng build` passes with zero errors after token additions (M1 gate — components unchanged, visual output identical)
**Checkpoint**: Design token layer complete. User story phases 37 may now start.
---
## Phase 3: User Story 1 — Library Feels Complete (Priority: P1) 🎯 MVP
**Goal**: The library view has skeleton loading, a styled empty state, an error state with retry, polished cards with hover lift, image error fallback, and responsive layout at 375 px.
**Independent Test**: Throttle network to Slow 3G and hard-refresh `/` — confirm shimmer skeleton appears. Remove all images — confirm styled empty state with "Upload your first image" link. Stop API — confirm error card with Retry button. Hover a card — confirm 2 px lift. Set viewport to 375 px — confirm no horizontal scrollbar.
### Tests for User Story 1 (TDD — write first, confirm failure before T008) ⚠️
- [X] T005 [US1] Add component tests for `showSpinner` debounce flag, `error` flag, skeleton card count, empty-state link, error card retry button, and `onImgError` handler in `ui/src/app/library/library.component.spec.ts`
### Implementation for User Story 1
- [X] T006 [P] [US1] Replace all hardcoded colour and spacing values with CSS token variables in the component styles block of `ui/src/app/library/library.component.ts`
- [X] T007 [US1] Replace `loading = true` boolean with `showSpinner = false` and add 150 ms debounce using `timer(150).pipe(takeUntil(req$))` from research.md Decision 3 in `ui/src/app/library/library.component.ts`
- [X] T008 [US1] Add skeleton loading grid: while `showSpinner` is true render 8 `<div class="skeleton card-skeleton">` placeholders at the same dimensions as real cards in `ui/src/app/library/library.component.ts`
- [X] T009 [US1] Add `error = false` flag; set it on `list()` failure; render an error card with plain-language message and "Retry" button that calls `load()` in `ui/src/app/library/library.component.ts`
- [X] T010 [US1] Replace the plain empty-state `<p>` with a centred panel: Unicode icon (✦), larger muted heading, and a `routerLink="/upload"` "Upload your first image" link in `ui/src/app/library/library.component.ts`
- [X] T011 [US1] Add card hover effect: `transform: translateY(-2px)` and `box-shadow` with `transition: var(--transition)` using `--surface-raised` in `ui/src/app/library/library.component.ts`
- [X] T012 [US1] Add `(error)="onImgError($event)"` on the card thumbnail `<img>` and implement `onImgError` with an inline SVG placeholder (guard against recursive fallback per research.md Decision 4) in `ui/src/app/library/library.component.ts`
- [X] T013 [US1] Check the `auto-fill minmax()` value in the grid at 375 px: if cards overflow horizontally, reduce min card width to `160px` and record the change; if no overflow, document "verified at 160px — no change needed" in a code comment in `ui/src/app/library/library.component.ts`
**Checkpoint**: Library view is fully functional with all loading/empty/error/responsive states.
---
## Phase 4: User Story 2 — Upload Form Feels Finished (Priority: P1)
**Goal**: The upload form has a visually distinct drop-zone, visible in-progress state ("Uploading…" + spinner), a success banner with auto-dismiss, distinct validation vs. network error messages, and a clearly disabled button style.
**Independent Test**: Navigate to `/upload` — confirm dashed drop-zone border. Select a large file and click Upload — confirm button shows "Uploading…" and is disabled. After success — confirm green banner appears then disappears after 4 s. Attempt to upload a `.txt` file — confirm "Unsupported file type" inline error.
### Tests for User Story 2 (TDD — write first, confirm failure before T016) ⚠️
- [X] T014 [US2] Add component tests for `loading` button-disabled state, "Uploading…" label, `showSuccess` banner visibility, auto-dismiss timer, validation error message, and network error message in `ui/src/app/upload/upload.component.spec.ts`
### Implementation for User Story 2
- [X] T015 [P] [US2] Replace all hardcoded colour values with CSS token variables in the component styles block of `ui/src/app/upload/upload.component.ts`
- [X] T016 [US2] Style the drop-zone with a dashed `--accent`-coloured border at 40% opacity; add an active drag state that brightens the border to full `--accent` in `ui/src/app/upload/upload.component.ts`
- [X] T017 [US2] Change submit button label to "Uploading…" and add a CSS spinner `<span>` inside the button while `loading = true` in `ui/src/app/upload/upload.component.ts`
- [X] T018 [US2] Add `showSuccess = false` and `uploadedFilename = ''`; after a successful upload set both, show a green-tinted banner with filename, "Upload another" link, and "View in library" routerLink, then auto-dismiss after 4 s using `setTimeout` in `ui/src/app/upload/upload.component.ts`
- [X] T019 [US2] Show distinct inline error messages: validation errors (wrong type/size from API) show the specific problem; network/server errors show a generic retry message — both rendered below the form without a page reload in `ui/src/app/upload/upload.component.ts`
- [X] T020 [US2] Apply `--text-muted` colour and `opacity: 0.5` to the disabled button style to make the disabled state visually unmistakable in `ui/src/app/upload/upload.component.ts`
**Checkpoint**: Upload form communicates every state clearly and prevents duplicate submission.
---
## Phase 5: User Story 3 — Detail Page Is Well Organised (Priority: P1)
**Goal**: The detail view has a loading skeleton, a network error state with retry, a styled not-found card with back link, a grouped "Owner actions" panel for write controls, danger-styled tag errors, and a broken-image fallback.
**Independent Test**: Throttle to Slow 3G and navigate to `/images/<id>` — confirm skeleton appears. Stop the API and hard-refresh a detail page — confirm error card with retry (not a blank page). Navigate to `/images/00000000-0000-0000-0000-000000000000` — confirm not-found card with "Back to library" button. Log in and open a detail page — confirm write controls are grouped. Open detail page while logged out — confirm write controls absent. Add tag with `!` character — confirm danger-coloured inline error.
### Tests for User Story 3 (TDD — write first, confirm failure before T023) ⚠️
- [X] T021 [US3] Add component tests for skeleton visibility while `loading=true`, network error card when fetch fails (non-404), not-found card when `!image && !loading && !error`, tag error `--danger` style application, and `onImgError` handler in `ui/src/app/detail/detail.component.spec.ts`
### Implementation for User Story 3
- [X] T022 [P] [US3] Replace all hardcoded colour values with CSS token variables in the component styles block of `ui/src/app/detail/detail.component.ts`
- [X] T023 [US3] Add skeleton loading layout while `loading = true`: a full-width grey `.skeleton` rectangle for the image area and two rows of `.skeleton` chip placeholders below in `ui/src/app/detail/detail.component.ts`
- [X] T024 [US3] Add `error = false` flag; set it on API fetch failure (non-404 errors); render an error card with plain-language "Failed to load image" message, "Back to library" link, and a "Retry" button that calls the fetch again — distinct from the not-found state in `ui/src/app/detail/detail.component.ts`
- [X] T025 [US3] Replace the plain `!image && !loading && !error` paragraph with a styled not-found card: centred layout, muted Unicode icon, "Image not found" `<h2>`, and a "Back to library" `routerLink="/"` button in `ui/src/app/detail/detail.component.ts`
- [X] T026 [US3] Wrap the tag-edit input and delete button in a visually distinct "Owner actions" `<section>` with a `--surface` panel, `--border` top separator, and token-based padding/gap in `ui/src/app/detail/detail.component.ts`
- [X] T027 [US3] Apply `color: var(--danger)` and `border-left: 3px solid var(--danger)` with left-padding to the `tagError` inline error element in `ui/src/app/detail/detail.component.ts`
- [X] T028 [US3] Add `(error)="onImgError($event)"` on the full-size `<img>` and implement `onImgError` with an inline SVG broken-link placeholder (guard against recursive fallback) in `ui/src/app/detail/detail.component.ts`
**Checkpoint**: Detail page handles all states (loading, error, not-found, success) gracefully and clearly separates read from write content.
---
## Phase 6: User Story 4 — Login Page Matches the Design (Priority: P2)
**Goal**: The login page uses the shared dark design system, displays field-level validation errors and server error messages in `--danger` colour without a page reload, shows a single server error below the form on bad credentials, and disables the button with "Signing in…" label while in-flight.
**Independent Test**: Navigate to `/login` — confirm dark background, surface card, same font as library. Click Sign In with empty fields — confirm field-level errors without page reload. Enter wrong credentials — confirm single error message below form in danger colour; fields retain values. Throttle network and submit valid credentials — confirm button shows "Signing in…" and is disabled.
### Tests for User Story 4 (TDD — write first, confirm failure before T031) ⚠️
- [X] T029 [US4] Add component tests for field-level validation error display on empty submit, `errorMessage` server error paragraph visibility after failed login, "Signing in…" button label while `loading=true`, and fields-not-cleared behaviour in `ui/src/app/login/login.component.spec.ts`
### Implementation for User Story 4
- [X] T030 [P] [US4] Replace all hardcoded colour values with CSS token variables in the component styles block of `ui/src/app/login/login.component.ts`
- [X] T031 [US4] Centre the login card vertically (`height: 100vh; display: flex; align-items: center; justify-content: center`) and wrap the form in a `--surface` card with `--radius` border-radius and a `1px solid var(--border)` border in `ui/src/app/login/login.component.ts`
- [X] T032 [US4] Apply `color: var(--danger)` to field-level reactive-form validation error `<span>` elements for both username and password fields, and to the `errorMessage` server error paragraph below the form in `ui/src/app/login/login.component.ts`
- [X] T033 [US4] Change submit button label to "Signing in…" and add `disabled` attribute while `loading = true`; style the button with `--accent` background and `--accent-text` foreground matching other views in `ui/src/app/login/login.component.ts`
**Checkpoint**: Login page is visually consistent with the rest of the app and communicates all form states.
---
## Phase 7: User Story 5 — App Shell Is Consistent (Priority: P2)
**Goal**: Every page shares a 48 px fixed-height header with the app name on the left and the sign-out control on the right (when authenticated). The header uses `--surface` background and `--border` bottom border and does not reflow page content on auth state change.
**Independent Test**: Log in and navigate between library, upload, and detail — confirm identical 48 px header on all pages. Log out — confirm sign-out control disappears but header height is unchanged. Visit library without logging in — confirm header is present but sign-out control absent.
### Tests for User Story 5 (TDD — write first, confirm failure before T036) ⚠️
- [X] T034 [US5] Add component tests for header 48 px height, sign-out button visibility when authenticated, sign-out button absence when unauthenticated, sign-out action redirecting to `/login`, and header height unchanged between auth states in `ui/src/app/app.component.spec.ts`
### Implementation for User Story 5
- [X] T035 [P] [US5] Replace all hardcoded colour values with CSS token variables in the component styles block of `ui/src/app/app.component.ts`
- [X] T036 [US5] Style the header `<header>` element with `height: 48px`, `background: var(--surface)`, `border-bottom: 1px solid var(--border)`, and a flex layout placing the app name text on the left and the sign-out button on the right; use `color: var(--text-muted)` and a token-based hover state for the sign-out button; ensure `height` is declared on the host element so the 48 px is preserved regardless of sign-out button visibility in `ui/src/app/app.component.ts`
**Checkpoint**: All five views share a coherent shell. Application feels like one product.
---
## Phase 8: Polish & Cross-Cutting Concerns
**Purpose**: Linting gate, build validation, responsive checks, and visual acceptance walk-through across all milestones.
- [X] T037 [P] Run `ng lint` in `ui/` and confirm zero ESLint and Prettier violations across all modified component files (§7.3 gate)
- [X] T038 [P] Run `ng build` in `ui/` and confirm zero TypeScript or template errors across all modified components
- [ ] T039 [P] Run `ng test --watch=false` in `ui/` (or equivalent build-time check) and confirm all new component spec tests pass with zero regressions
- [ ] T040 [P] Verify upload form layout at 375 px viewport (Chrome DevTools device toolbar → iPhone SE): confirm no horizontal scrollbar and all form elements are usable in `ui/src/app/upload/upload.component.ts`
- [ ] T041 [P] Verify detail page layout at 375 px viewport: confirm image, tags, and (when authenticated) owner actions are all visible without horizontal overflow in `ui/src/app/detail/detail.component.ts`
- [ ] T042 Walk through `specs/005-ui-polish/quickstart.md` scenarios M1M6 in a running `docker compose up` stack and confirm every visual acceptance criterion passes
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies — start immediately
- **Foundational (Phase 2)**: Depends on Phase 1 — **BLOCKS all user story phases**
- **User Stories (Phases 37)**: ALL depend on Phase 2 (T004 gate); independent of each other
- **Polish (Phase 8)**: Depends on all desired user story phases completing
### User Story Dependencies
- **US1 Library (Phase 3, P1)**: Can start after Phase 2 — no dependency on US2US5
- **US2 Upload (Phase 4, P1)**: Can start after Phase 2 — no dependency on US1, US3US5
- **US3 Detail (Phase 5, P1)**: Can start after Phase 2 — no dependency on US1US2, US4US5
- **US4 Login (Phase 6, P2)**: Can start after Phase 2 — no dependency on US1US3, US5
- **US5 App Shell (Phase 7, P2)**: Can start after Phase 2 — best done last as it wraps all views, but technically independent
### Within Each User Story Phase
1. Write component spec tests first (TDD) — they MUST fail before implementation
2. Token replacement task [P] can run alongside test writing (different files)
3. Implementation tasks follow in sequence (each new feature depends on the test that exercises it)
---
## Parallel Execution Examples
### Running US1 (Library) startup in parallel
```text
# Start simultaneously after Phase 2 completes:
Task T005: Write library component tests (spec file)
Task T006: Apply CSS tokens to library component styles
```
### Running all three P1 stories in parallel (Phase 3, 4, 5)
```text
# All can start simultaneously after T004 (Phase 2 gate):
Phase 3 (US1): T005 → T006/T007 → T008 → T009 → T010 → T011 → T012 → T013
Phase 4 (US2): T014 → T015/T016 → T017 → T018 → T019 → T020
Phase 5 (US3): T021 → T022/T023 → T024 → T025 → T026 → T027 → T028
```
---
## Implementation Strategy
### MVP First (US1 + US2 + US3 — all P1 stories)
1. Complete Phase 1: Baseline verification
2. Complete Phase 2: Design token layer (CRITICAL gate)
3. Complete Phases 3, 4, 5 in parallel or sequence (all P1)
4. **STOP and VALIDATE**: Run quickstart.md M1M4 scenarios
5. Add Phase 6 (US4 Login) and Phase 7 (US5 Shell) for complete polish
### Incremental Delivery
1. Phase 1 + Phase 2 → Token layer live (no visible change)
2. Phase 3 (US1) → Library feels complete → Demo
3. Phase 4 (US2) → Upload flow polished → Demo
4. Phase 5 (US3) → Detail page organised → Demo
5. Phase 6 (US4) + Phase 7 (US5) → Full design system applied → Final demo
6. Phase 8 → Lint clean, build clean, all tests pass, quickstart validated
---
## Notes
- `[P]` tasks have no file conflicts with other concurrent `[P]` tasks in the same phase
- TDD order is mandatory: spec tests must be written and confirmed failing before implementation tasks
- All five component files are standalone Angular components — changes are isolated
- `ng build` is the type-check gate; Karma tests require the full Docker stack for browser runner
- No new npm dependencies are introduced in any task
- Commit after each milestone (M1M6) for clean rollback points