diff --git a/.specify/feature.json b/.specify/feature.json index 6a319dc..006ee44 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1 +1 @@ -{"feature_directory": "specs/014-r2-cdn-serving"} +{"feature_directory":"specs/015-library-pagination"} diff --git a/CLAUDE.md b/CLAUDE.md index db74e03..b763979 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/014-r2-cdn-serving/plan.md`. +`specs/015-library-pagination/plan.md`. diff --git a/specs/015-library-pagination/checklists/requirements.md b/specs/015-library-pagination/checklists/requirements.md new file mode 100644 index 0000000..e2cc2f0 --- /dev/null +++ b/specs/015-library-pagination/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: Library Pagination UI + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-05-09 +**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. Ready for `/speckit-plan`. diff --git a/specs/015-library-pagination/contracts/pagination-query.md b/specs/015-library-pagination/contracts/pagination-query.md new file mode 100644 index 0000000..d052a14 --- /dev/null +++ b/specs/015-library-pagination/contracts/pagination-query.md @@ -0,0 +1,52 @@ +# Contract: Image List Pagination Query + +No new API endpoints are introduced. This document records the existing API contract the UI relies on for pagination. + +## Endpoint + +``` +GET /api/v1/images?limit={limit}&offset={offset}&tags={tags} +``` + +## Parameters + +| Parameter | Type | Required | Description | +|-----------|---------|----------|--------------------------------------------------| +| `limit` | integer | No | Images per page. UI sends `24`. Max is 100. | +| `offset` | integer | No | Number of images to skip. UI computes `(page-1) * 24`. | +| `tags` | string | No | Comma-separated tag names for AND-filter. | + +## Response + +```json +{ + "items": [ /* ImageRecord[] */ ], + "total": 143, + "limit": 24, + "offset": 48 +} +``` + +| Field | Type | Description | +|----------|---------|--------------------------------------------------| +| `total` | integer | Total images matching the filter (all pages). | +| `limit` | integer | Page size echoed back. | +| `offset` | integer | Offset echoed back. | +| `items` | array | Images for this page only. | + +## UI-Computed Values + +``` +totalPages = Math.ceil(total / limit) // e.g. ceil(143 / 24) = 6 +currentPage = offset / limit + 1 // e.g. 48 / 24 + 1 = 3 +offset = (page - 1) * limit // e.g. (3 - 1) * 24 = 48 +``` + +## URL State + +| Query Param | Source | Example | +|-------------|---------------------|------------------| +| `page` | current page number | `?page=3` | +| `tags` | active tag filters | `?tags=cat,funny` | + +Both params coexist: `/?page=3&tags=cat,funny` diff --git a/specs/015-library-pagination/plan.md b/specs/015-library-pagination/plan.md new file mode 100644 index 0000000..cd44303 --- /dev/null +++ b/specs/015-library-pagination/plan.md @@ -0,0 +1,82 @@ +# Implementation Plan: Library Pagination UI + +**Branch**: `015-library-pagination` | **Date**: 2026-05-09 | **Spec**: [spec.md](spec.md) +**Input**: Feature specification from `specs/015-library-pagination/spec.md` + +## Summary + +Replace the current "Load more" append-on-scroll pattern in the library with discrete page navigation (Previous/Next buttons, page indicator, total count). Page state is persisted to the URL query string for bookmarkability. No API or backend changes required — the API already supports `limit` and `offset` parameters. + +## Technical Context + +**Language/Version**: TypeScript (strict), Angular latest stable +**Primary Dependencies**: Angular Router (query params for URL state), Angular HttpClient (existing) +**Storage**: N/A — UI-only change +**Testing**: Angular TestBed / Jasmine (existing test suite) +**Target Platform**: Browser SPA +**Project Type**: UI feature within existing Angular standalone component +**Performance Goals**: Page load of 24 images replaces 50-image Load More; no regression +**Constraints**: Must preserve existing tag filter query param (`?tags=`) when updating page param; must not break existing spec tests +**Scale/Scope**: Single component change (`library.component.ts`) + its spec file + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +| Principle | Status | Notes | +|-----------|--------|-------| +| §2.1 Strict separation | ✅ PASS | UI communicates with API only via HTTP; no storage or DB knowledge in component | +| §2.6 No speculative abstraction | ✅ PASS | No new abstractions introduced; pagination is a concrete change to one component | +| §3.2 OpenAPI as contract | ✅ PASS | Uses existing `GET /api/v1/images?limit=&offset=` contract; no new endpoints | +| §3.4 Pagination | ✅ PASS | This feature is the UI surface for the API pagination already in place | +| §5.1 Tests alongside implementation | ✅ REQUIRED | Component spec must be updated alongside each changed behaviour | +| §5.4 Test gate | ✅ REQUIRED | UI tests must pass; `make test-unit` passes before task marked done | +| §6 Tech stack | ✅ PASS | Angular + TypeScript strict — no new dependencies needed | +| §7.3 Linting | ✅ REQUIRED | ESLint + Prettier enforced; no lint regressions | +| §8 Scope boundaries | ✅ PASS | Pagination is explicitly required (§3.4); no out-of-scope additions | + +**Post-Phase-1 re-check**: No contracts or data model introduced; no new violations. + +## Project Structure + +### Documentation (this feature) + +```text +specs/015-library-pagination/ +├── plan.md ← this file +├── research.md ← Phase 0 output +├── quickstart.md ← Phase 1 output +├── contracts/ +│ └── pagination-query.md ← Phase 1 output +└── tasks.md ← Phase 2 output (/speckit-tasks) +``` + +### Source Code (changes only) + +```text +ui/src/app/library/ +├── library.component.ts ← primary change +└── library.component.spec.ts ← tests updated alongside +``` + +No other files change. No new files added to source tree. + +## Key Design Decisions + +### Page size: 24 +Fixed at 24 images per page (spec FR-011). Fits common grid widths (2/3/4/6 columns), is a meaningful reduction from the current silent 50-image cap, and divides cleanly. Not user-configurable. + +### Replace, don't append +Current `loadMore()` appends items to the array. The new `goToPage(n)` replaces `this.images` entirely. The `offset` field becomes derived from page: `offset = (page - 1) * limit`. + +### URL state via Angular Router query params +- `?page=2` added alongside existing `?tags=cat,funny` +- Use `queryParamsHandling: 'merge'` when updating page to preserve tag params +- Use `queryParamsHandling: 'merge'` when updating tags to preserve page reset (page always resets to 1 on filter change, so page param is removed or set to 1) +- On `ngOnInit`, read `page` from `snapshot.queryParamMap`; clamp to valid range + +### Out-of-page-range handling +If URL `?page=99` is requested but only 3 pages exist: silently load page 1. No error state. + +### Pagination controls visibility +Only shown when `totalPages > 1`. Total pages = `Math.ceil(total / limit)`. diff --git a/specs/015-library-pagination/quickstart.md b/specs/015-library-pagination/quickstart.md new file mode 100644 index 0000000..40e0d04 --- /dev/null +++ b/specs/015-library-pagination/quickstart.md @@ -0,0 +1,41 @@ +# Quickstart: Library Pagination UI + +## Happy Path — Navigating Pages + +**Setup**: Library contains more than 24 images. + +1. Open `http://localhost:4200/` +2. Confirm the image grid shows 24 images (not 50). +3. Confirm "Page 1 of N" indicator and total count are visible above or below the grid. +4. Confirm "Previous" button is absent or disabled. +5. Click "Next" → grid replaces with the next 24 images; indicator updates to "Page 2 of N". +6. Click "Previous" → first 24 images return; indicator shows "Page 1 of N". +7. Navigate to the last page → "Next" is absent or disabled. + +## Happy Path — URL State + +1. Navigate to page 3 via "Next" button twice. +2. Copy URL from address bar (should contain `?page=3`). +3. Open URL in a new tab → page 3 loads directly. +4. Press browser Back → page 2 loads. + +## Happy Path — Tag Filter Resets Page + +1. Navigate to page 2. +2. Add a tag filter. +3. Confirm page resets to 1; URL shows `?page=1&tags=` (or just `?tags=`). + +## Edge Case — Single Page + +1. Filter to a tag with fewer than 25 images. +2. Confirm no pagination controls are rendered. + +## Edge Case — Out-of-Range URL + +1. Manually enter `/?page=9999` in the address bar. +2. Confirm page 1 loads with no error message. + +## Edge Case — Empty Library + +1. With no images uploaded, open `/`. +2. Confirm the existing empty state is shown; no pagination controls visible. diff --git a/specs/015-library-pagination/research.md b/specs/015-library-pagination/research.md new file mode 100644 index 0000000..14dfcd3 --- /dev/null +++ b/specs/015-library-pagination/research.md @@ -0,0 +1,33 @@ +# Research: Library Pagination UI + +## Decision: Angular Router query params for URL state + +**Decision**: Use `this.router.navigate([], { queryParams: { page: n }, queryParamsHandling: 'merge' })` for page navigation and `snapshot.queryParamMap.get('page')` on init. + +**Rationale**: The library component already uses Angular Router for `?tags=` query params (added in feature 007). Extending the same pattern to `?page=` is the natural fit and keeps a single source of truth in the URL. The `queryParamsHandling: 'merge'` flag ensures that navigating to a new page does not erase the active `?tags=` filter, and vice versa. + +**Alternatives considered**: +- Component-local state only (no URL): rejected — FR-008 requires bookmarkable URLs +- `queryParamsHandling: ''` (replace): rejected — would erase `?tags=` param when changing pages + +--- + +## Decision: Replace `loadMore()` with `goToPage(page: number)` + +**Decision**: Remove `loadMore()`, `hasMore`, and the append pattern. Replace with `goToPage(n)` that sets `this.images = []` and loads from `offset = (page - 1) * limit`. + +**Rationale**: The spec requires discrete pages (FR-001, FR-006). Keeping `loadMore()` alongside pagination would create conflicting UX. Clean removal is simpler and avoids two code paths. + +**Alternatives considered**: +- Keep `loadMore()` as a fallback: rejected — two navigation patterns in one view is confusing + +--- + +## Decision: No new dependencies + +**Decision**: Implement using existing Angular Router, HttpClient, and CDR. No pagination library imported. + +**Rationale**: The pagination logic is trivial (previous/next buttons, a counter, clamped page index). Pulling in a library for two buttons and a text label adds bundle weight and a dependency for no meaningful gain. + +**Alternatives considered**: +- `ngx-pagination`: rejected — overkill for two-button prev/next pattern diff --git a/specs/015-library-pagination/spec.md b/specs/015-library-pagination/spec.md new file mode 100644 index 0000000..9c6f53b --- /dev/null +++ b/specs/015-library-pagination/spec.md @@ -0,0 +1,84 @@ +# Feature Specification: Library Pagination UI + +**Feature Branch**: `015-library-pagination` +**Created**: 2026-05-09 +**Status**: Draft +**Input**: User description: "Pagination UI for the image library" + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Navigate Pages of Images (Priority: P1) + +A user with a large image library currently sees at most 50 images and a "Load more" button that appends more images below. There is no way to jump to a specific point in the library or know how many images exist in total. This story replaces the append-on-load pattern with page-by-page navigation: Previous/Next buttons and a "Page N of M" indicator so the user always knows where they are. + +**Why this priority**: The core usability gap — a library of any meaningful size is effectively unnavigable today. Without this, the feature has no value. + +**Independent Test**: Load the library page. Confirm a page indicator ("Page 1 of N") is visible and the total image count is shown. Click "Next" — confirm the next set of images loads and the indicator updates. Click "Previous" — confirm the first set returns. On the first page, "Previous" is absent or disabled. On the last page, "Next" is absent or disabled. Changing a tag filter resets to page 1. + +**Acceptance Scenarios**: + +1. **Given** the library has more images than fit on one page, **When** the page loads, **Then** only the first page of images is shown with a "Next" button and "Page 1 of N" indicator visible. +2. **Given** the user is on page 1, **When** they click "Next", **Then** the next page of images replaces the current grid (not appended) and the indicator updates to "Page 2 of N". +3. **Given** the user is on the last page, **When** they view the page, **Then** the "Next" button is absent or disabled and "Previous" is present. +4. **Given** the user is on page 1, **When** they view the page, **Then** the "Previous" button is absent or disabled. +5. **Given** the library has fewer images than one page, **When** the page loads, **Then** no pagination controls are shown. +6. **Given** active tag filters are applied, **When** the user changes the filter, **Then** the page resets to 1 and the indicator updates. + +--- + +### User Story 2 - Page State Reflected in URL (Priority: P2) + +The current library URL is always `/`. After implementing page navigation, a user who shares or bookmarks a URL should land on the same page they were viewing, not always page 1. + +**Why this priority**: Useful for bookmarking and sharing a specific point in the library, but the library is fully functional without it. + +**Independent Test**: Navigate to page 3 of the library. Copy the URL from the browser address bar. Open it in a new tab. Confirm page 3 loads directly. Confirm the Back button in the browser returns to page 2. + +**Acceptance Scenarios**: + +1. **Given** the user navigates to page 3, **When** the page URL is copied and opened in a new tab, **Then** page 3 loads directly without navigating through prior pages. +2. **Given** the user navigates Next through several pages, **When** they press the browser Back button, **Then** the previous page is restored. +3. **Given** the URL includes a page number beyond the total pages available, **When** the page loads, **Then** page 1 is shown rather than an error. + +--- + +### Edge Cases + +- What happens when the total drops below the current page (e.g., images deleted in another session)? → Display page 1. +- What happens when the library is empty? → No pagination controls shown; existing empty state displayed. +- What happens when only one page of results exists? → No pagination controls shown. +- What happens when a filter change results in fewer pages than the current page? → Reset to page 1. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The library MUST display images in discrete pages rather than an appending list. +- **FR-002**: The library MUST show a page indicator displaying the current page number and total page count (e.g., "Page 2 of 7"). +- **FR-003**: The library MUST show the total number of images matching the current filters (e.g., "143 images"). +- **FR-004**: A "Next" control MUST be available on all pages except the last; a "Previous" control MUST be available on all pages except the first. +- **FR-005**: Pagination controls MUST NOT be shown when all images fit on a single page. +- **FR-006**: Navigating to a new page MUST replace the displayed images, not append to them. +- **FR-007**: Changing a tag filter MUST reset the current page to 1. +- **FR-008**: The current page number MUST be reflected in the URL query string so that the URL is bookmarkable and shareable. +- **FR-009**: Loading a URL with a page parameter MUST display the correct page directly. +- **FR-010**: A page parameter beyond the available range MUST silently fall back to page 1. +- **FR-011**: The page size (number of images per page) MUST be 24 images. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: A user can navigate from page 1 to any other page using only Previous/Next controls within 2 clicks per page. +- **SC-002**: The total image count and current position are visible without scrolling on page load. +- **SC-003**: A bookmarked or shared page URL loads the correct page 100% of the time (within the valid range). +- **SC-004**: Changing a tag filter always resets to page 1 with no stale images from the previous page visible. +- **SC-005**: Pages with fewer images than the page size (the last page) display correctly without layout breakage. + +## Assumptions + +- Page size is fixed at 24 images; no user-configurable page size is required. +- The API already supports `limit` and `offset` parameters; no backend changes are needed. +- The existing "Load more" / infinite-scroll pattern is fully replaced by page navigation. +- Browser history integration (Back/Forward) is satisfied by URL query parameter updates. +- Mobile responsiveness of pagination controls is required to match the existing library layout. diff --git a/specs/015-library-pagination/tasks.md b/specs/015-library-pagination/tasks.md new file mode 100644 index 0000000..8c98ac8 --- /dev/null +++ b/specs/015-library-pagination/tasks.md @@ -0,0 +1,82 @@ +# Tasks: Library Pagination UI + +**Input**: Design documents from `specs/015-library-pagination/` +**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, contracts/pagination-query.md ✅, quickstart.md ✅ + +**Tests**: Tests accompany each implementation task per §5.1. All changes are in `ui/src/app/library/library.component.ts` and its spec file. + +**Organization**: No setup or foundational phase needed — the Angular project and library component already exist. Phase 1 implements US1 (page navigation controls). Phase 2 adds US2 (URL state). Polish runs lint and manual verification. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to + +--- + +## Phase 1: User Story 1 — Previous/Next Page Navigation (Priority: P1) 🎯 MVP + +**Goal**: Replace the "Load more" append pattern with discrete Previous/Next page navigation, a "Page N of M" indicator, and a total image count. Page size changes from 50 to 24. + +**Independent Test**: With at least 25 images in the library, open `/`. Confirm 24 images are shown, a "Page 1 of N" indicator is visible, "Previous" is absent, and "Next" is present. Click "Next" — confirm the grid is replaced (not appended) with the next 24 images and the indicator updates. Click "Previous" — confirm the first page returns. Apply a tag filter — confirm the page resets to 1. + +- [X] T001 [US1] Write tests in `ui/src/app/library/library.component.spec.ts` covering: (1) page indicator text "Page 1 of N" renders when totalPages > 1; (2) total count text renders (e.g. "143 images"); (3) "Next" button present when not on last page; (4) "Previous" button absent on first page; (5) "Previous" present and "Next" absent on last page; (6) no pagination controls rendered when all images fit on one page (total ≤ 24); (7) clicking "Next" calls `imageService.list` with offset=24; (8) clicking "Previous" from page 2 calls `imageService.list` with offset=0; (9) applying a filter resets to page 1 (offset=0). Run `ng test` and confirm the new tests FAIL (implementation pending). + +- [X] T002 [US1] Update `ui/src/app/library/library.component.ts`: (a) change `private readonly limit = 50` to `private readonly limit = 24`; (b) remove `hasMore` property and `loadMore()` method; (c) add properties `currentPage = 1`, `totalPages = 1`, `total = 0`; (d) rename/replace `load()` to call `imageService.list(this.activeFilters, this.limit, (this.currentPage - 1) * this.limit)` and on success set `this.images = res.items` (replace, not append), `this.total = res.total`, `this.totalPages = Math.ceil(res.total / this.limit)`, clamp `currentPage` to `Math.max(1, Math.min(this.currentPage, this.totalPages))`; (e) add `nextPage()` that increments `currentPage` and calls `load()`; (f) add `prevPage()` that decrements `currentPage` and calls `load()`; (g) in `applyFilter()`, reset `this.currentPage = 1` before calling `load()`; (h) replace the ` + +

{{ total }} images

+ + +
+ + Page {{ currentPage }} of {{ totalPages }} + +
`, styles: [` @@ -119,7 +127,11 @@ const PLACEHOLDER_SVG = `data:image/svg+xml,