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/plan.md
Normal file
242
specs/005-ui-polish/plan.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user