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:
2026-05-03 20:03:56 +00:00
parent 5179786261
commit 9246f75fdd
23 changed files with 1777 additions and 181 deletions

180
specs/005-ui-polish/spec.md Normal file
View File

@@ -0,0 +1,180 @@
# Feature Specification: UI Polish & Design System
**Feature Branch**: `005-ui-polish`
**Created**: 2026-05-03
**Status**: Draft
**Input**: User description: "Polish the Angular UI with a cohesive visual design. The three main views — library (image grid), upload form, and image detail — should feel intentional and finished. Add proper loading states, empty states, and error states to each view. The overall aesthetic should be dark-themed and minimal, fitting a personal tool used frequently. The login page should also match the design system."
## User Scenarios & Testing *(mandatory)*
### User Story 1 — Library Feels Complete (Priority: P1)
The owner opens the app and is greeted by a polished image grid. While images
load, something visually coherent fills the space so the page doesn't feel
broken. If the library is empty, a helpful prompt explains what to do. If the
request fails, a clear error message appears with a way to retry.
**Why this priority**: The library is the landing page and the most-visited
view. Its quality sets first impressions for every session.
**Independent Test**: Open the app with no images uploaded — confirm an
intentional empty state is shown. Upload an image, return to the library —
confirm the grid renders cleanly with consistent card sizing. Throttle the
network — confirm a loading indicator appears before images arrive.
**Acceptance Scenarios**:
1. **Given** the library is loading, **When** the page first renders, **Then** a skeleton or spinner occupies the grid area so the layout does not jump or appear blank.
2. **Given** no images have been uploaded, **When** the library loads successfully, **Then** an empty-state message is shown explaining that no images exist yet, with a visible prompt to upload the first image.
3. **Given** the image fetch fails (network error), **When** the library loads, **Then** an error message is shown with a retry action; the page does not display a blank grid or a raw error code.
4. **Given** images exist, **When** the library renders, **Then** all image cards have consistent size, spacing, and visual weight; tag chips are readable and do not overflow their cards.
---
### User Story 2 — Upload Form Feels Finished (Priority: P1)
The owner navigates to the upload page and finds a form that clearly communicates
its state at every step: idle with a helpful drop-zone, active while uploading
with visible progress, and resolved with success or a plain-language error.
**Why this priority**: Upload is the primary write action. A rough upload
experience erodes confidence in the whole tool.
**Independent Test**: Upload a valid image and confirm the flow from drop-zone
through in-progress indicator to success result is smooth and clearly
communicated. Attempt an upload with an invalid file type and confirm a
plain-language validation error appears without a page reload.
**Acceptance Scenarios**:
1. **Given** the upload page is idle, **When** no file is selected, **Then** a drop-zone with clear instructions is visible; the submit button is visibly disabled.
2. **Given** a file is selected and uploading, **When** the upload is in progress, **Then** the submit button is disabled and a visible in-progress indicator is shown; the user cannot accidentally submit twice.
3. **Given** an upload succeeds, **When** the server responds, **Then** a success confirmation is shown and the owner can navigate onward without confusion.
4. **Given** an upload fails due to an invalid file type or size, **When** the server responds, **Then** a plain-language error message is shown identifying the problem; the form remains usable for another attempt.
5. **Given** an upload fails due to a network or server error, **When** the server responds, **Then** a generic error message is shown with guidance to retry.
---
### User Story 3 — Detail Page Is Well Organised (Priority: P1)
The owner opens an image's detail page and finds the image prominently displayed,
tag management clearly grouped, and write controls (edit tags, delete) visually
distinct from read content. Visitors who are not logged in see the image and
tags but no write controls. Loading and error states are handled gracefully.
**Why this priority**: The detail page is where tag curation and deletion
happen — the two most common editing actions after upload.
**Independent Test**: Open a detail page while logged in — confirm write
controls are visible and clearly grouped. Open the same page while logged out —
confirm write controls are hidden. Navigate to a non-existent image ID — confirm
a not-found state is shown rather than a blank or broken page.
**Acceptance Scenarios**:
1. **Given** the detail page is loading, **When** the route is first entered, **Then** a loading indicator is shown in place of the image and metadata.
2. **Given** the image exists and the owner is logged in, **When** the page renders, **Then** the image is the focal point; tags are displayed below; tag editing and delete controls are clearly grouped and visually differentiated from read content.
3. **Given** the image exists and the visitor is not logged in, **When** the page renders, **Then** the image and tags are visible; no tag-edit input or delete button is present.
4. **Given** a non-existent image ID is requested, **When** the page loads, **Then** a not-found state is shown with a link back to the library; no raw error code or blank area is displayed.
5. **Given** a tag update fails, **When** the owner submits a tag change, **Then** an inline error message explains the failure; the image and other tags remain visible.
---
### User Story 4 — Login Page Matches the Design (Priority: P2)
The owner lands on the login page (directly or after a redirect) and finds a
form that visually belongs to the same application as the library and detail
page. The form clearly communicates validation errors and submission state.
**Why this priority**: Login is visited infrequently. A consistent visual
treatment matters, but functional correctness (already implemented) is more
critical than aesthetic alignment.
**Independent Test**: Navigate to `/login` directly — confirm the page uses the
same colour scheme, typography, and spacing as the rest of the app. Submit with
empty fields — confirm visible validation errors appear without a page reload.
**Acceptance Scenarios**:
1. **Given** the login page loads, **When** the owner views it, **Then** the page uses the same dark background, colour palette, and typographic scale as all other views.
2. **Given** the owner submits with empty username or password, **When** the form is submitted, **Then** inline validation messages appear on the relevant fields without a page reload or server round-trip.
3. **Given** the owner submits invalid credentials, **When** the server rejects them, **Then** a single error message is shown below the form; the fields are not cleared.
4. **Given** the form is submitting, **When** the request is in-flight, **Then** the submit button is disabled and shows an in-progress label so the owner cannot submit twice.
---
### User Story 5 — App Shell Is Consistent (Priority: P2)
Every page shares a consistent outer frame: a slim header that shows the
sign-out control when logged in. The header does not compete with page content
for visual attention but is always present and usable.
**Why this priority**: A coherent shell ties the individual views together into
a single application rather than a collection of pages.
**Independent Test**: Navigate between library, detail, and upload while logged
in — confirm the header is consistent across all views. Sign out and visit a
public page — confirm the sign-out control is absent.
**Acceptance Scenarios**:
1. **Given** the owner is logged in, **When** viewing any page, **Then** a slim header is present with a sign-out control; it does not draw excessive visual attention away from the page content.
2. **Given** the visitor is not logged in, **When** viewing the library or a detail page, **Then** the header is present but contains no sign-out control.
3. **Given** the owner clicks sign out in the header, **When** the action completes, **Then** they are redirected to the login page and the header no longer shows the sign-out control.
---
### Edge Cases
- What happens if an image fails to load (broken URL or storage outage)? The card or detail view should show a placeholder, not the browser's default broken-image icon.
- What happens on a very narrow viewport (mobile browser)? Cards should stack or resize; the layout must not overflow horizontally.
- What happens if a tag is very long? Tag chips must truncate or wrap without breaking the card or detail layout.
- What happens during slow network conditions? Loading states must appear promptly and not flash on fast connections.
---
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: Every view (library, upload, detail, login) MUST display a loading indicator while async data or actions are in progress.
- **FR-002**: The library MUST display a meaningful empty-state message with a call to action when no images exist.
- **FR-003**: All four views MUST display plain-language error messages when an operation fails; raw HTTP status codes or stack traces MUST NOT be shown to the user.
- **FR-004**: The upload form MUST disable the submit control while an upload is in progress to prevent duplicate submissions.
- **FR-005**: The detail page MUST show a not-found state (with a back link) when the requested image does not exist.
- **FR-006**: Write controls on the detail page (tag editing, delete) MUST be hidden for unauthenticated visitors and visible only to the logged-in owner.
- **FR-007**: All views MUST share a consistent set of visual tokens: background colours, text colours, spacing scale, border radii, and interactive-element styles.
- **FR-008**: The application MUST be usable on viewports as narrow as 375 px (iPhone SE width) without horizontal overflow.
- **FR-009**: Loading indicators MUST NOT flash on connections fast enough to resolve in under 150 ms; debounced or skeleton-based approaches are preferred.
- **FR-010**: Broken image assets (failed loads) MUST display a visible placeholder rather than the browser's default broken-image icon.
### Key Entities
- **Design token set**: The shared palette, spacing scale, and typographic rules that all views derive from (background, surface, border, text-primary, text-muted, accent, danger).
- **Loading state**: A visual treatment applied to any view or element while data is being fetched or an action is in progress.
- **Empty state**: A purposeful layout shown when a collection has zero items, including explanatory text and a next-action prompt.
- **Error state**: A purposeful layout shown when an operation fails, including a plain-language description and (where applicable) a retry action.
---
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Every view transitions from loading to content (or error/empty) without a layout shift visible to the naked eye.
- **SC-002**: All five views pass a visual consistency check: an observer can identify them as belonging to the same application by colour, typography, and spacing alone.
- **SC-003**: The library, upload, and detail views each render without horizontal scrollbars on a 375 px wide viewport.
- **SC-004**: Each error condition (network failure, validation failure, not-found) produces a user-visible message within the current view — zero conditions result in a silent failure or blank screen.
- **SC-005**: Loading indicators do not appear on responses that complete in under 150 ms in a local development environment (no flicker on fast connections).
---
## Assumptions
- The existing dark colour palette already in the components (#1a1a1a backgrounds, #e0e0e0 text, #4a9eff accent) is the correct base; the polish work refines and extends it rather than replacing it wholesale.
- No external component library or icon set is introduced; any icons needed are either inline SVG or Unicode characters to avoid new dependencies.
- The app remains a single-page application; no server-side rendering or route-level transitions are in scope.
- Mobile layout is "good enough to use" at 375 px rather than a fully optimised mobile-first redesign; a dedicated mobile redesign is out of scope.
- No new API endpoints are needed; all changes are purely front-end.
- Animations and transitions are minimal — a single standard duration applied consistently; no complex motion design.
- FR-006 (hiding write controls for unauthenticated visitors) is already implemented in the detail component; this spec confirms the behaviour is preserved and visually correct, not that it needs to be built from scratch.