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>
This commit is contained in:
242
specs/005-ui-polish/tasks.md
Normal file
242
specs/005-ui-polish/tasks.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# 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 3–7) 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 (US1–US5)
|
||||
- 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 3–7 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 M1–M6 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 3–7)**: 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 US2–US5
|
||||
- **US2 Upload (Phase 4, P1)**: Can start after Phase 2 — no dependency on US1, US3–US5
|
||||
- **US3 Detail (Phase 5, P1)**: Can start after Phase 2 — no dependency on US1–US2, US4–US5
|
||||
- **US4 Login (Phase 6, P2)**: Can start after Phase 2 — no dependency on US1–US3, 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 M1–M4 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 (M1–M6) for clean rollback points
|
||||
Reference in New Issue
Block a user