Controlled pagination pill with lock & fit-scroll

Make the floating pagination pill a controlled component and add lock/fit-aware scroll behavior. UnifiedPagination now accepts activeIndex and onDominantSectionChange, reports observer-determined dominant section (parent may ignore when locked) and only shows/hides based on footer visibility. HomePage implements controlled state (activeIndex, lockedTo) with Prev/Next/jump locking, release on wheel/touch/key or 30s safety timeout, and dot clicks that always navigate and release locks. Extracted scroll math to src/lib/utils/paginationScroll.ts (decideScrollForPageChange) so paging avoids scrolling when a section fits below the sticky header and clamps targets; added unit tests and updated component tests and docs to reflect the new behavior. Removed now-unused onPageChange prop from HomeSection.
This commit is contained in:
kikootwo
2026-05-18 13:21:06 -04:00
parent b1492fc32e
commit 5d9a764151
9 changed files with 614 additions and 56 deletions
+1 -1
View File
@@ -42,7 +42,7 @@ Users customize their home page by adding/removing/reordering sections. Each sec
- **Config Modal:** `src/components/home/HomeSectionConfigModal.tsx` — drag-and-drop (desktop), up/down arrows (mobile), auto-save with debounce
- **Section Component:** `src/components/home/HomeSection.tsx` — renders individual section with color-coded header
- **Home Page:** `src/app/page.tsx` — dynamic sections from user config, gear icon for customize
- **Pagination:** `src/components/ui/UnifiedPagination.tsx`updated to support 1-12 dynamic sections
- **Pagination:** `src/components/ui/UnifiedPagination.tsx`controlled by `HomePage` for `activeIndex`; observer reports dominant section but parent gates updates via `lockedTo` state. Lock set on Prev/Next/jump; released on user scroll input (`wheel` / `touchstart` / Arrow / Page / Home / End keys) or any dot click. Fit-aware scroll via `src/lib/utils/paginationScroll.ts` — no scroll when section fits viewport, otherwise snaps top under sticky header with clamps that structurally prevent scrolling the section out of view. Pill is shown anywhere on main content; only the footer hides it.
## Key Decisions
- 10 section limit per user (total)
+13 -2
View File
@@ -71,8 +71,12 @@ src/components/
- Floating pagination pill at bottom center of viewport
- Minimal design: section label | ← | Page X of Y | →
- Quick jump input (type page number + Enter)
- Auto-shows when scrolling through a section (IntersectionObserver)
- Auto-scrolls to section top on page change
- Free-scroll tracking via IntersectionObserver (reports dominant section to parent)
- Controlled `activeIndex` lives on the home page; pill is observer-aware but parent-decided
- **Lock-to-section on Prev/Next/jump:** pill stays anchored to the paged section until the user generates a scroll input (`wheel`, `touchstart`, `ArrowUp/Down`, `PageUp/Down`, `Home`, `End`) or clicks another section's dot. 30s safety auto-release.
- **Fit-aware scroll:** if the section already fits below the sticky header, paging swaps cards in place (no scroll). Otherwise snaps the section top under the header with breathing room (8px top, 24px bottom). Target Y is clamped to `[0, maxScrollY]` so paging can never scroll the section out of the viewport.
- Dot click on a different section always scrolls (intentional navigation) and releases any active lock.
- Visibility: pill is shown anywhere on homepage main content; hidden only when the footer enters view. Stays visible over the CTA card gap between the last section and the footer.
- Rounded-full design with backdrop blur and subtle shadow
- Responsive grid layouts (1/2/3/4 cols)
- Enhanced CTA section with gradient background (blue-to-indigo)
@@ -168,6 +172,13 @@ interface StickyPaginationProps {
sectionRef: React.RefObject<HTMLElement | null>;
label: string;
}
interface UnifiedPaginationProps {
sections: PaginationSection[];
footerRef?: React.RefObject<HTMLElement | null>;
activeIndex: number; // controlled by parent
onDominantSectionChange: (idx: number) => void; // observer guess; parent decides
}
```
## Custom Hooks