Files
reactbin/specs/005-ui-polish/plan.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
9.9 KiB
Markdown
Raw Permalink 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.
# 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 msdebounced 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 `<p>` 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 `<img>` 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 `<img>` 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)
```
M2M5 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.