Files
2026-05-27 23:29:53 +01:00

4.0 KiB

Tasks: enterprise-react-infinite-scroll

Change ID: enterprise-react-infinite-scroll Package: @suaveplan/react-infinite-scroll Target path: react/ui/react-infinite-scroll Prerequisite packages on Verdaccio: @suaveplan/error, @suaveplan/react-translate


0. Pre-flight (non-negotiable)

  • 0.1 Confirm clean working tree (git status reports nothing) and no in-flight worktrees with uncommitted work (git worktree list then git status in each)
  • 0.2 If any worktrees exist under .claude/worktrees/, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (git worktree remove) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
  • 0.3 Confirm the package set in MANIFEST.md matches find packages -maxdepth 3 -name package.json count. Drift here means a prior change archived without regenerating the manifest.

1. Scaffold

  • Create directory tree per proposal layout.
  • Write package.json at 0.1.0 with "sideEffects": false, exports map.
  • Dependencies: @suaveplan/error workspace:^, @suaveplan/react-translate workspace:^, zod catalog:forms, @opentelemetry/api catalog:observability, tslib catalog:build.
  • DevDependencies: intersection-observer polyfill for tests (or use @suaveplan/testing-dom which includes it).
  • Write tsconfig.json, vitest.config.ts (include intersection-observer in setupFiles), scripts.

2. Error codes and types

  • Define InfiniteScrollError extending SuaveplanError; code: INFINITE_SCROLL_LOAD_FAILED.
  • Define UseInfiniteScrollOptions: onLoadMore, threshold, rootRef, rootMargin, debounceMs, direction, isEnd, isLoading.
  • Write UseInfiniteScrollOptionsSchema Zod schema.

3. Hook — useInfiniteScroll

  • Returns { ref: RefCallback, isLoading, isEnd }.
  • Uses real IntersectionObserver (no mocks); rootRef passed as root option.
  • direction: 'down' places sentinel after last item; direction: 'up' places it before first.
  • Debounces onLoadMore by debounceMs (default 100ms) to prevent rapid calls.
  • Wraps onLoadMore call in a try/catch; on throw wraps in InfiniteScrollError(INFINITE_SCROLL_LOAD_FAILED).
  • Emits OTel infinite_scroll.load span with infinite_scroll.direction attribute.
  • Write use-infinite-scroll.test.tsx (real DOM with polyfill): sentinel enters viewport → onLoadMore called; isEnd=true stops future calls; debounce reduces calls; rootRef restricts observer root. { timeout: 10000 }.
  • Write use-infinite-scroll.stories.tsx: playground (toggle isEnd/isLoading).
  • Write use-infinite-scroll.md (≥200 words).

4. Component — InfiniteScroll

  • Renders children + sentinel + loading spinner (from @suaveplan/react-translate "Loading...") + end message.
  • Forwards rootRef prop to hook; accepts direction.
  • loadingComponent prop to override default spinner; endComponent to override end message.
  • Write infinite-scroll.test.tsx: renders items, loading state shows spinner, end state shows message, custom components used when provided. { timeout: 5000 }.
  • Write infinite-scroll.stories.tsx: default, reverse-scroll (chat), nested-container, playground.
  • Write infinite-scroll.md (≥200 words).

5. E2E

  • e2e/infinite-scroll.spec.ts — Playwright:
    • Scroll to bottom of list, verify onLoadMore fired (intercept via page evaluate counter).
    • Set isEnd=true, scroll to bottom, verify no further loads.
    • Reverse-scroll: scroll to top of chat list, verify load triggered.

6. Verify and publish

  • bunx turbo lint typecheck test --filter=@suaveplan/react-infinite-scroll — green.
  • bunx turbo test:coverage --filter=@suaveplan/react-infinite-scroll — 100%.
  • bun scripts/publish.ts --package @suaveplan/react-infinite-scroll.
  • Archive change folder.