diff --git a/.specify/feature.json b/.specify/feature.json index 5585a29..2ff2f40 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1 @@ -{ - "feature_directory": "specs/004-jwt-bearer-auth" -} +{"feature_directory": "specs/005-ui-polish"} diff --git a/CLAUDE.md b/CLAUDE.md index ed92c81..09fda75 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,5 @@ For additional context about technologies to be used, project structure, shell commands, and other important information, read the current plan at -`specs/004-jwt-bearer-auth/plan.md`. +`specs/005-ui-polish/plan.md`. diff --git a/specs/005-ui-polish/checklists/requirements.md b/specs/005-ui-polish/checklists/requirements.md new file mode 100644 index 0000000..89b0698 --- /dev/null +++ b/specs/005-ui-polish/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: UI Polish & Design System + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-05-03 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [X] No implementation details (languages, frameworks, APIs) +- [X] Focused on user value and business needs +- [X] Written for non-technical stakeholders +- [X] All mandatory sections completed + +## Requirement Completeness + +- [X] No [NEEDS CLARIFICATION] markers remain +- [X] Requirements are testable and unambiguous +- [X] Success criteria are measurable +- [X] Success criteria are technology-agnostic (no implementation details) +- [X] All acceptance scenarios are defined +- [X] Edge cases are identified +- [X] Scope is clearly bounded +- [X] Dependencies and assumptions identified + +## Feature Readiness + +- [X] All functional requirements have clear acceptance criteria +- [X] User scenarios cover primary flows +- [X] Feature meets measurable outcomes defined in Success Criteria +- [X] No implementation details leak into specification + +## Notes + +- All items pass. Spec is ready for `/speckit-plan`. diff --git a/specs/005-ui-polish/plan.md b/specs/005-ui-polish/plan.md new file mode 100644 index 0000000..555468c --- /dev/null +++ b/specs/005-ui-polish/plan.md @@ -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 `
` 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 `` 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 `
` 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.
diff --git a/specs/005-ui-polish/quickstart.md b/specs/005-ui-polish/quickstart.md
new file mode 100644
index 0000000..b60210c
--- /dev/null
+++ b/specs/005-ui-polish/quickstart.md
@@ -0,0 +1,155 @@
+# Quickstart: UI Polish Visual Acceptance Scenarios
+
+Use this guide to manually verify each milestone after implementation.
+Run `docker compose up` before starting. Open the browser at `http://localhost:4200`.
+
+---
+
+## M1 — Design Token Layer
+
+**Goal**: Tokens exist; visual output is identical to before.
+
+1. Open the library, upload, detail, and login pages.
+2. Open browser DevTools → Elements → `` or `
` with a centred panel: Unicode icon (✦), larger muted heading, and a `routerLink="/upload"` "Upload your first image" link in `ui/src/app/library/library.component.ts`
+- [X] T011 [US1] Add card hover effect: `transform: translateY(-2px)` and `box-shadow` with `transition: var(--transition)` using `--surface-raised` in `ui/src/app/library/library.component.ts`
+- [X] T012 [US1] Add `(error)="onImgError($event)"` on the card thumbnail ` Failed to load image. Please check your connection. {{ tagError }} {{ tagError }} Image not found. {{ activeFilters.length ? 'No images match these filters.' : 'No images yet. Upload your first!' }} Failed to load images. Please check your connection. {{ selectedFile ? selectedFile.name : 'Drag & drop or click to browse' }}` and implement `onImgError` with an inline SVG placeholder (guard against recursive fallback per research.md Decision 4) in `ui/src/app/library/library.component.ts`
+- [X] T013 [US1] Check the `auto-fill minmax()` value in the grid at 375 px: if cards overflow horizontally, reduce min card width to `160px` and record the change; if no overflow, document "verified at 160px — no change needed" in a code comment in `ui/src/app/library/library.component.ts`
+
+**Checkpoint**: Library view is fully functional with all loading/empty/error/responsive states.
+
+---
+
+## Phase 4: User Story 2 — Upload Form Feels Finished (Priority: P1)
+
+**Goal**: The upload form has a visually distinct drop-zone, visible in-progress state ("Uploading…" + spinner), a success banner with auto-dismiss, distinct validation vs. network error messages, and a clearly disabled button style.
+
+**Independent Test**: Navigate to `/upload` — confirm dashed drop-zone border. Select a large file and click Upload — confirm button shows "Uploading…" and is disabled. After success — confirm green banner appears then disappears after 4 s. Attempt to upload a `.txt` file — confirm "Unsupported file type" inline error.
+
+### Tests for User Story 2 (TDD — write first, confirm failure before T016) ⚠️
+
+- [X] T014 [US2] Add component tests for `loading` button-disabled state, "Uploading…" label, `showSuccess` banner visibility, auto-dismiss timer, validation error message, and network error message in `ui/src/app/upload/upload.component.spec.ts`
+
+### Implementation for User Story 2
+
+- [X] T015 [P] [US2] Replace all hardcoded colour values with CSS token variables in the component styles block of `ui/src/app/upload/upload.component.ts`
+- [X] T016 [US2] Style the drop-zone with a dashed `--accent`-coloured border at 40% opacity; add an active drag state that brightens the border to full `--accent` in `ui/src/app/upload/upload.component.ts`
+- [X] T017 [US2] Change submit button label to "Uploading…" and add a CSS spinner `` inside the button while `loading = true` in `ui/src/app/upload/upload.component.ts`
+- [X] T018 [US2] Add `showSuccess = false` and `uploadedFilename = ''`; after a successful upload set both, show a green-tinted banner with filename, "Upload another" link, and "View in library" routerLink, then auto-dismiss after 4 s using `setTimeout` in `ui/src/app/upload/upload.component.ts`
+- [X] T019 [US2] Show distinct inline error messages: validation errors (wrong type/size from API) show the specific problem; network/server errors show a generic retry message — both rendered below the form without a page reload in `ui/src/app/upload/upload.component.ts`
+- [X] T020 [US2] Apply `--text-muted` colour and `opacity: 0.5` to the disabled button style to make the disabled state visually unmistakable in `ui/src/app/upload/upload.component.ts`
+
+**Checkpoint**: Upload form communicates every state clearly and prevents duplicate submission.
+
+---
+
+## Phase 5: User Story 3 — Detail Page Is Well Organised (Priority: P1)
+
+**Goal**: The detail view has a loading skeleton, a network error state with retry, a styled not-found card with back link, a grouped "Owner actions" panel for write controls, danger-styled tag errors, and a broken-image fallback.
+
+**Independent Test**: Throttle to Slow 3G and navigate to `/images/
`, and a "Back to library" `routerLink="/"` button in `ui/src/app/detail/detail.component.ts`
+- [X] T026 [US3] Wrap the tag-edit input and delete button in a visually distinct "Owner actions" `
` and implement `onImgError` with an inline SVG broken-link placeholder (guard against recursive fallback) in `ui/src/app/detail/detail.component.ts`
+
+**Checkpoint**: Detail page handles all states (loading, error, not-found, success) gracefully and clearly separates read from write content.
+
+---
+
+## Phase 6: User Story 4 — Login Page Matches the Design (Priority: P2)
+
+**Goal**: The login page uses the shared dark design system, displays field-level validation errors and server error messages in `--danger` colour without a page reload, shows a single server error below the form on bad credentials, and disables the button with "Signing in…" label while in-flight.
+
+**Independent Test**: Navigate to `/login` — confirm dark background, surface card, same font as library. Click Sign In with empty fields — confirm field-level errors without page reload. Enter wrong credentials — confirm single error message below form in danger colour; fields retain values. Throttle network and submit valid credentials — confirm button shows "Signing in…" and is disabled.
+
+### Tests for User Story 4 (TDD — write first, confirm failure before T031) ⚠️
+
+- [X] T029 [US4] Add component tests for field-level validation error display on empty submit, `errorMessage` server error paragraph visibility after failed login, "Signing in…" button label while `loading=true`, and fields-not-cleared behaviour in `ui/src/app/login/login.component.spec.ts`
+
+### Implementation for User Story 4
+
+- [X] T030 [P] [US4] Replace all hardcoded colour values with CSS token variables in the component styles block of `ui/src/app/login/login.component.ts`
+- [X] T031 [US4] Centre the login card vertically (`height: 100vh; display: flex; align-items: center; justify-content: center`) and wrap the form in a `--surface` card with `--radius` border-radius and a `1px solid var(--border)` border in `ui/src/app/login/login.component.ts`
+- [X] T032 [US4] Apply `color: var(--danger)` to field-level reactive-form validation error `` elements for both username and password fields, and to the `errorMessage` server error paragraph below the form in `ui/src/app/login/login.component.ts`
+- [X] T033 [US4] Change submit button label to "Signing in…" and add `disabled` attribute while `loading = true`; style the button with `--accent` background and `--accent-text` foreground matching other views in `ui/src/app/login/login.component.ts`
+
+**Checkpoint**: Login page is visually consistent with the rest of the app and communicates all form states.
+
+---
+
+## Phase 7: User Story 5 — App Shell Is Consistent (Priority: P2)
+
+**Goal**: Every page shares a 48 px fixed-height header with the app name on the left and the sign-out control on the right (when authenticated). The header uses `--surface` background and `--border` bottom border and does not reflow page content on auth state change.
+
+**Independent Test**: Log in and navigate between library, upload, and detail — confirm identical 48 px header on all pages. Log out — confirm sign-out control disappears but header height is unchanged. Visit library without logging in — confirm header is present but sign-out control absent.
+
+### Tests for User Story 5 (TDD — write first, confirm failure before T036) ⚠️
+
+- [X] T034 [US5] Add component tests for header 48 px height, sign-out button visibility when authenticated, sign-out button absence when unauthenticated, sign-out action redirecting to `/login`, and header height unchanged between auth states in `ui/src/app/app.component.spec.ts`
+
+### Implementation for User Story 5
+
+- [X] T035 [P] [US5] Replace all hardcoded colour values with CSS token variables in the component styles block of `ui/src/app/app.component.ts`
+- [X] T036 [US5] Style the header `
{{ image.filename }}
-
+
-
+
Sign In
-
+ Sign In
+
+