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>
18 KiB
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.
- T001 Confirm
ng buildpasses with zero errors inui/(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.
- T002 Add 13 CSS custom property tokens to
:rootinui/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 - T003 Add
@keyframes shimmeranimation and.skeletonutility class toui/src/styles.cssusing the gradient pattern from research.md Decision 2 - T004 Confirm
ng buildpasses 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) ⚠️
- T005 [US1] Add component tests for
showSpinnerdebounce flag,errorflag, skeleton card count, empty-state link, error card retry button, andonImgErrorhandler inui/src/app/library/library.component.spec.ts
Implementation for User Story 1
- 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 - T007 [US1] Replace
loading = trueboolean withshowSpinner = falseand add 150 ms debounce usingtimer(150).pipe(takeUntil(req$))from research.md Decision 3 inui/src/app/library/library.component.ts - T008 [US1] Add skeleton loading grid: while
showSpinneris true render 8<div class="skeleton card-skeleton">placeholders at the same dimensions as real cards inui/src/app/library/library.component.ts - T009 [US1] Add
error = falseflag; set it onlist()failure; render an error card with plain-language message and "Retry" button that callsload()inui/src/app/library/library.component.ts - T010 [US1] Replace the plain empty-state
<p>with a centred panel: Unicode icon (✦), larger muted heading, and arouterLink="/upload""Upload your first image" link inui/src/app/library/library.component.ts - T011 [US1] Add card hover effect:
transform: translateY(-2px)andbox-shadowwithtransition: var(--transition)using--surface-raisedinui/src/app/library/library.component.ts - T012 [US1] Add
(error)="onImgError($event)"on the card thumbnail<img>and implementonImgErrorwith an inline SVG placeholder (guard against recursive fallback per research.md Decision 4) inui/src/app/library/library.component.ts - T013 [US1] Check the
auto-fill minmax()value in the grid at 375 px: if cards overflow horizontally, reduce min card width to160pxand record the change; if no overflow, document "verified at 160px — no change needed" in a code comment inui/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) ⚠️
- T014 [US2] Add component tests for
loadingbutton-disabled state, "Uploading…" label,showSuccessbanner visibility, auto-dismiss timer, validation error message, and network error message inui/src/app/upload/upload.component.spec.ts
Implementation for User Story 2
- 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 - 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--accentinui/src/app/upload/upload.component.ts - T017 [US2] Change submit button label to "Uploading…" and add a CSS spinner
<span>inside the button whileloading = trueinui/src/app/upload/upload.component.ts - T018 [US2] Add
showSuccess = falseanduploadedFilename = ''; 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 usingsetTimeoutinui/src/app/upload/upload.component.ts - 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 - T020 [US2] Apply
--text-mutedcolour andopacity: 0.5to the disabled button style to make the disabled state visually unmistakable inui/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) ⚠️
- 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--dangerstyle application, andonImgErrorhandler inui/src/app/detail/detail.component.spec.ts
Implementation for User Story 3
- 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 - T023 [US3] Add skeleton loading layout while
loading = true: a full-width grey.skeletonrectangle for the image area and two rows of.skeletonchip placeholders below inui/src/app/detail/detail.component.ts - T024 [US3] Add
error = falseflag; 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 inui/src/app/detail/detail.component.ts - T025 [US3] Replace the plain
!image && !loading && !errorparagraph with a styled not-found card: centred layout, muted Unicode icon, "Image not found"<h2>, and a "Back to library"routerLink="/"button inui/src/app/detail/detail.component.ts - T026 [US3] Wrap the tag-edit input and delete button in a visually distinct "Owner actions"
<section>with a--surfacepanel,--bordertop separator, and token-based padding/gap inui/src/app/detail/detail.component.ts - T027 [US3] Apply
color: var(--danger)andborder-left: 3px solid var(--danger)with left-padding to thetagErrorinline error element inui/src/app/detail/detail.component.ts - T028 [US3] Add
(error)="onImgError($event)"on the full-size<img>and implementonImgErrorwith an inline SVG broken-link placeholder (guard against recursive fallback) inui/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) ⚠️
- T029 [US4] Add component tests for field-level validation error display on empty submit,
errorMessageserver error paragraph visibility after failed login, "Signing in…" button label whileloading=true, and fields-not-cleared behaviour inui/src/app/login/login.component.spec.ts
Implementation for User Story 4
- 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 - T031 [US4] Centre the login card vertically (
height: 100vh; display: flex; align-items: center; justify-content: center) and wrap the form in a--surfacecard with--radiusborder-radius and a1px solid var(--border)border inui/src/app/login/login.component.ts - T032 [US4] Apply
color: var(--danger)to field-level reactive-form validation error<span>elements for both username and password fields, and to theerrorMessageserver error paragraph below the form inui/src/app/login/login.component.ts - T033 [US4] Change submit button label to "Signing in…" and add
disabledattribute whileloading = true; style the button with--accentbackground and--accent-textforeground matching other views inui/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) ⚠️
- 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 inui/src/app/app.component.spec.ts
Implementation for User Story 5
- T035 [P] [US5] Replace all hardcoded colour values with CSS token variables in the component styles block of
ui/src/app/app.component.ts - T036 [US5] Style the header
<header>element withheight: 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; usecolor: var(--text-muted)and a token-based hover state for the sign-out button; ensureheightis declared on the host element so the 48 px is preserved regardless of sign-out button visibility inui/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.
- T037 [P] Run
ng lintinui/and confirm zero ESLint and Prettier violations across all modified component files (§7.3 gate) - T038 [P] Run
ng buildinui/and confirm zero TypeScript or template errors across all modified components - T039 [P] Run
ng test --watch=falseinui/(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.mdscenarios M1–M6 in a runningdocker compose upstack 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
- Write component spec tests first (TDD) — they MUST fail before implementation
- Token replacement task [P] can run alongside test writing (different files)
- Implementation tasks follow in sequence (each new feature depends on the test that exercises it)
Parallel Execution Examples
Running US1 (Library) startup in parallel
# 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)
# 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)
- Complete Phase 1: Baseline verification
- Complete Phase 2: Design token layer (CRITICAL gate)
- Complete Phases 3, 4, 5 in parallel or sequence (all P1)
- STOP and VALIDATE: Run quickstart.md M1–M4 scenarios
- Add Phase 6 (US4 Login) and Phase 7 (US5 Shell) for complete polish
Incremental Delivery
- Phase 1 + Phase 2 → Token layer live (no visible change)
- Phase 3 (US1) → Library feels complete → Demo
- Phase 4 (US2) → Upload flow polished → Demo
- Phase 5 (US3) → Detail page organised → Demo
- Phase 6 (US4) + Phase 7 (US5) → Full design system applied → Final demo
- 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 buildis 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