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>
243 lines
9.9 KiB
Markdown
243 lines
9.9 KiB
Markdown
# 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 ms–debounced 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)
|
||
```
|
||
|
||
M2–M5 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.
|