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>
9.9 KiB
Implementation Plan: UI Polish & Design System
Branch: 005-ui-polish | Date: 2026-05-03 | Spec: 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)
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)
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
--surfacecard with--radiusand a subtle border - Use token-based input styles matching the library filter bar
- Display field-level validation errors using
--dangercolour - The submit button uses the same
--accentstyle 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:
--surfacebackground, bottom border using--border - App name / logo mark on the left (text only, no image asset)
- Sign-out button aligned right using
--text-mutedcolour and a simple hover state - Header height:
48pxfixed; 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 upstack. - 375 px viewport check: Chrome DevTools → device toolbar → iPhone SE.
Build gate: ng build must pass with zero errors after every milestone.