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

9.9 KiB
Raw Permalink Blame History

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 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.