Skip to content

DES-23: Add LoadMore infinite-scroll primitive + useLoadMore hook ## Summary S#523

Open
github-actions[bot] wants to merge 8 commits intomainfrom
develop
Open

DES-23: Add LoadMore infinite-scroll primitive + useLoadMore hook ## Summary S#523
github-actions[bot] wants to merge 8 commits intomainfrom
develop

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

If this change should result in new package versions please add a changeset before merging. You can do so by clicking the link provided by changeset bot below.

## Summary

Brings Origin's `Pagination` compound component up to parity with the
Base UI idioms used by the rest of Origin (matching the style of DES-18
#26842 and DES-19 #26829), and softens the API so callers without a
known total are no longer blocked.

[DES-21](https://lightspark.atlassian.net/browse/DES-21) (parent epic:
[DES-20](https://lightspark.atlassian.net/browse/DES-20)).

## Changes

- **`render` prop on every part.** Each part now goes through
`useRender`, gaining a `render` prop so consumers can swap the rendered
element. The motivating case is rendering `Pagination.Previous` /
`Pagination.Next` as `<a>` for shareable per-page URLs and
middle-click-to-new-tab.
- **`data-*` state attributes.** Component state surfaces via
`useRender`'s `state` + `stateAttributesMapping`:
  - Root: `data-page`, `data-first-page`, `data-last-page`
- Prev/Next: `data-disabled` mirrors the resolved disabled state (so
anchor renders pick up the disabled visual treatment uniformly with
`<button>` renders)
- **`aria-disabled` on every render path.** Anchors can't carry the
native `disabled` attribute, so `aria-disabled` is set whenever the part
is in its disabled state regardless of the rendered element.
- **`totalItems` is now optional.** When omitted:
- `Pagination.Next` no longer auto-disables — consumers control via the
`disabled` prop
- `Pagination.Range` requires a custom children render fn or no-ops with
a `devWarn`
  - `data-last-page` is absent (never present-and-empty)
Prefer the forthcoming `Pager` primitive (DES-22) for fully
unknown-total flows; this escape hatch unblocks consumers with partial
knowledge.
- **`usePaginationContext` is exported** (both as a named export and on
the compound) so consumers can build custom parts on top of context,
matching the `Combobox.useFilter` dual-surface pattern.
- **CSS gains `[data-disabled]` selectors** alongside the existing
`:disabled` selectors so anchor renders pick up the disabled visual
treatment.

## Out of scope

- Shared SCSS extraction for DES-22's `Pager` — DES-22 will duplicate
the styles. Pagination's button SCSS is unchanged in location and
structure.
- Analytics call surface stays as `useTrackedCallback` with format
`component.interaction`. Direction (`"next"` / `"previous"`) is added to
metadata.
- No page clamping, no new parts, no visual changes for existing
callers.

## Stories

- `URLBased`: anchor renders for Prev/Next
- `WithoutTotals`: optional-totals usage with custom Range children

## Test plan

- [x] `yarn workspace @lightsparkdev/origin test:unit` — 436 passed (15
new for Pagination)
- [x] `yarn workspace @lightsparkdev/origin lint` — clean (only 2
pre-existing warnings outside this PR)
- [x] `yarn workspace @lightsparkdev/origin format` — clean
- [x] `yarn workspace @lightsparkdev/origin types` — clean
- [ ] Reviewer: skim Storybook `URLBased` and `WithoutTotals` stories
- [ ] Reviewer: confirm the `Pagination.Next` no-auto-disable behaviour
matches the intent for unknown-totals flows

Made with [Cursor](https://cursor.com)

[DES-21]:
https://lightspark.atlassian.net/browse/DES-21?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[DES-20]:
https://lightspark.atlassian.net/browse/DES-20?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

GitOrigin-RevId: 646153fe7d4023be440f9b0572580ae1e6e5671e
## Summary
- expose Origin Button's existing Base UI `render` / `nativeButton`
support in its TypeScript props so product wrappers can render it as a
typed router link without reimplementing visuals
- add a focused Grid `NageButton` wrapper that only owns typed routing
props (`to`, `toParams`, `hash`) around Origin Button
- keep the branch intentionally narrow: no legacy shared UI Button
import, no Emotion compatibility layer, no legacy prop mapping, and no
consumer migration yet
- add a Vitest contract test for routing and transparent Origin prop
pass-through

## Validation
- `yarn vitest run src/uma-nage/components/NageButton.test.tsx
--environment jsdom`
- `yarn tsc --noEmit --pretty false` in `js/apps/private/site`
- `yarn types` in `js/packages/origin`
- `yarn vite build` in `js/apps/private/site`

GitOrigin-RevId: 633ace9159779598d69b44177bbfd3ba9ffe233a
…6920)

## Summary

Ships a new Origin compound primitive `LoadMore` and a
transport-agnostic
companion hook `useLoadMore` for forward-only infinite scroll. Third of
three
sibling pagination primitives under epic
[DES-20](https://lightspark.atlassian.net/browse/DES-20)
(after [DES-21](https://lightspark.atlassian.net/browse/DES-21)
`Pagination` and
[DES-22](https://lightspark.atlassian.net/browse/DES-22) `Pager`).

Resolves [DES-23](https://lightspark.atlassian.net/browse/DES-23).

## Component API

`LoadMore` follows the new Origin idiom standard — `forwardRef`
everywhere,
exported context hook (`useLoadMoreContext`), `data-*` state attributes,
Base
UI `useRender` `render` escape hatch on every overridable part.

- **`Root`** — headless context provider over `{ hasMore, loading,
onLoadMore, analyticsName }`. Renders only its children.
- **`Trigger`** — composes Origin's `Button` by default; swap with
`render={<Button variant=\"ghost\" />}` (or anything else).
Auto-disables when `!hasMore || loading`, forwards `aria-busy`, exposes
`data-loading` / `data-has-more` / `data-disabled`.
- **`Sentinel`** — `IntersectionObserver`-backed invisible trigger with
stable refs so the observer effect doesn't re-subscribe on every state
change. SSR-safe; includes a post-load re-evaluation pass for cases
where the new page didn't grow tall enough to scroll the sentinel out of
view. \`disabled\` renders no DOM at all.
- **`Status`** — \`aria-live=\"polite\"\` + \`aria-atomic\` SR-only slot
with render-prop children: \`{({ loading, hasMore }) => loading ?
\"Loading more results\" : !hasMore ? \"End of results\" : \"\"}\`.

## Hook API

\`useLoadMore\` is a generalisation of nage's
\`useGridApiPaginatedQuery\`:
same request-id race guard, same item accumulation, same
\`JSON.stringify(resetOn)\` reset semantics — but accepts a generic
\`fetchPage(cursor)\` callback instead of being hard-coded to the Grid
API.

\`\`\`ts
const { items, hasMore, loading, loadingMore, loadMore, refetch, error }
=
  useLoadMore({ fetchPage: (cursor) => …, resetOn: [filter] });
\`\`\`

- Maintains an internal \`requestIdRef\` so a slow first response cannot
clobber state set by a later \`refetch\` / \`resetOn\` change.
- \`fetchPage\` is read from a ref, so consumers don't need
\`useCallback\`.
- Rejected \`fetchPage\` lands in \`result.error\` (coerced to
\`Error\`); \`loading\` / \`loadingMore\` clear; existing \`items\` are
preserved. Error clears on the next fetch start.

## Analytics

Following the \`component.interaction\` convention:
- Trigger: \`\${name}.click\` with \`metadata: { part: \"trigger\" }\`.
- Sentinel: \`\${name}.intersect\` with \`metadata: { part: \"sentinel\"
}\`.

The part is in metadata, not the event name. Adds \`\"intersect\"\` to
\`InteractionType\`.

## Tests

- **Vitest** (\`useLoadMore.unit.test.ts\`, 10 tests): initial fetch,
\`enabled: false\` toggle, accumulation across pages, \`hasMore: false\`
gates \`loadMore\`, concurrent-loadMore is a no-op, race-guard against a
slow initial response, \`resetOn\` change resets and refetches,
\`refetch\` clears items, error capture preserves prior items, error
clears on next fetch.
- **Playwright CT** (\`LoadMore.test.tsx\`): Trigger enabled /
disabled-when-no-more / disabled-while-loading / custom render; Sentinel
scroll-in / no-refire-while-loading / disabled-renders-nothing; Status
default / loading / end variants; throws when used outside Root;
analytics emit; end-to-end pagination through \`useLoadMore\`.

## Verification

\`\`\`
yarn workspace @lightsparkdev/origin types        # clean
yarn workspace @lightsparkdev/origin test:unit    # 431 pass (10 new)
yarn workspace @lightsparkdev/origin lint # clean (2 pre-existing
warnings unchanged)
yarn workspace @lightsparkdev/origin format       # clean
\`\`\`

## Files

- New: \`js/packages/origin/src/components/LoadMore/\` (component, hook,
scss, stories, test stories, CT tests, hook unit tests, index)
- Modified: \`js/packages/origin/src/index.ts\` (barrel adds),
\`js/packages/origin/src/components/Analytics/AnalyticsContext.tsx\`
(\`\"intersect\"\` interaction type)

## Out of scope

- Migrating existing \`useGridApiPaginatedQuery\` consumers — follow-up.
- Visual loading skeletons — consumers compose \`Skeleton\` themselves.
- Bidirectional infinite scroll — DES-23 is forward-only.

[DES-23]: https://lightspark.atlassian.net/browse/DES-23

Made with [Cursor](https://cursor.com)

[DES-20]:
https://lightspark.atlassian.net/browse/DES-20?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[DES-21]:
https://lightspark.atlassian.net/browse/DES-21?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
[DES-22]:
https://lightspark.atlassian.net/browse/DES-22?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

GitOrigin-RevId: c12f0de6e4763ee0584785572614e9d54705d282
@github-actions github-actions Bot requested a review from a team as a code owner April 30, 2026 21:58
@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented Apr 30, 2026

The following public packages have changed files:

Changed Current version
@lightsparkdev/lightspark-cli 0.1.18
@lightsparkdev/lightspark-sdk 1.9.18
@lightsparkdev/crypto-wasm 0.1.25
@lightsparkdev/origin 0.14.2
@lightsparkdev/oauth 0.1.67
@lightsparkdev/core 1.5.1
@lightsparkdev/ui 1.1.19

There are no existing changesets for this branch. If the changes in this PR should result in new published versions for the packages above please add a changeset. Any packages that depend on the planned releases will be updated and released automatically in a separate PR.

Each changeset corresponds to an update in the CHANGELOG for the packages listed in the changeset. Therefore, you should add a changeset for each noteable package change that this PR contains. For example, if a PR adds two features - one feature for packages A and B and one feature for package C - you should add two changesets. One changeset for packages A and B and one changeset for package C, with a description of each feature. The feature description will end up being the CHANGELOG entry for the packages in the changeset.

No releases planned.

Last updated by commit 4d67f34

jaymantri and others added 5 commits May 1, 2026 18:18
…de conflict (TS2430) (#26931)

## What's broken

`LoadMoreTriggerProps` in
`js/packages/origin/src/components/LoadMore/LoadMore.tsx` extends
`Omit<ButtonProps, "onClick" | "disabled" | "loading">` and then
redeclares `render` with a wider state type (`TriggerRenderState` adds
`hasMore` and `loading` on top of `ButtonState`). Because `Omit` doesn't
drop `render`, TypeScript flags the override as incompatible:

```
TS2430: Interface 'LoadMoreTriggerProps' incorrectly extends interface 'Omit<ButtonProps, "onClick" | "loading" | "disabled">'.
  Types of property 'render' are incompatible.
  Type 'ButtonState' is missing the following properties from type 'TriggerRenderState': hasMore, loading
```

This is currently failing the site app's `tsc` (run during `yarn build`)
on every open PR.

## The fix

Add `"render"` to the `Omit` clause so the trigger's wider render-state
declaration is the only one on `LoadMoreTriggerProps`:

```ts
export interface LoadMoreTriggerProps
  extends Omit<ButtonProps, "onClick" | "disabled" | "loading" | "render"> {
```

One-token change.

## Why it slipped past origin's tests

DES-23 (#26920) introduced the regression. Origin's `test:unit` runs
vitest but does not type-check the site app, so the conflict only
surfaces when `apps/private/site` runs `tsc` as part of `yarn build`.

## Verification

- `yarn workspace @lightsparkdev/origin test:unit` → 447 tests pass
- `yarn workspace @lightsparkdev/origin lint && … format` → clean (only
pre-existing warnings)
- `cd apps/private/site && find . -maxdepth 3 -name
'tsconfig.tsbuildinfo' -delete && yarn tsc` → passes cleanly, no
`LoadMore` errors

## Urgency

Blocking the site build on all open PRs — please land ASAP.

Made with [Cursor](https://cursor.com)

GitOrigin-RevId: c77577a1f91e3e9c6f2e86b31124021c29175e29
## Reason

A standalone browser-based example app is needed to demonstrate and
manually exercise the full Grid Global Accounts API lifecycle, including
credential creation, verification, session management, and wallet
operations across all three supported authentication types (EMAIL_OTP,
OAUTH, and PASSKEY).

## Overview

Adds a new Vite + TypeScript single-page example app at
`js/apps/examples/grid-global-accounts-example-app` that covers:

- **Platform auth**: API client ID/secret input with sandbox and
production mode selection. Sandbox uses magic string constants
(`sandbox-valid-signature`, `000000`, `sandbox-valid-oidc-token`,
`sandbox-valid-passkey-signature`). Production mode generates a
client-side P-256 keypair, HPKE-decrypts the
`encryptedSessionSigningKey` returned by Verify using `@turnkey/crypto`,
and stamps `payloadToSign` values via `@turnkey/api-key-stamper`.
- **Customer setup**: Create customer and fetch internal account
balance, with auto-propagation of account/credential/session IDs into a
shared wallet context used across all tabs.
- **Per-type lifecycle tabs** for EMAIL_OTP, OAUTH, and PASSKEY, each
covering: wallet creation, credential verification → session,
rechallenge, and two-step signed-retry flows for adding a second
credential, deleting a credential, deleting a session, and exporting the
wallet.
- **External account creation** for both `SPARK_WALLET` and
`USD_ACCOUNT` types, quote creation with `payloadToSign` extraction,
payload signing (sandbox magic or real Turnkey stamp), and quote
execution.
- A Vite dev server proxy that rewrites `/api` requests to
`https://api.lightspark.com/grid/2025-10-13`.

The app is registered on port `3106` in `settings.json`.

## Test Plan

Run `yarn dev` from the app directory and manually exercise each tab's
lifecycle against the sandbox environment using the pre-filled magic
values. Verify that signed-retry flows correctly populate `requestId`
from step 1 and forward it with `Grid-Wallet-Signature` in step 2. For
production mode, generate a P-256 key, run a Verify step, then use "Sign
payload" before executing a quote to confirm HPKE decryption and Turnkey
stamping work end-to-end.

GitOrigin-RevId: fe887c117e70114303ebf6de67b9449fc8059c7b
## Summary
- lowers Origin reset/global selectors with `:where(...)` so component
and app styles can override Origin defaults without separate overrides
- splits Origin's public stylesheet into root/document/scopable
internals and adds `@lightsparkdev/origin/scope.scss`
- scopes reusable Origin global rules under `html.origin` while keeping
token/font root setup available at document level
- switches the private site to import the scoped Origin stylesheet,
toggling `html.origin` for auth and Grid/Nage routes while preserving
Emotion globals on legacy routes
- preserves the `--doc-height` viewport resize sync for both paths:
Emotion `GlobalStyles` keeps its updater for other apps, while
Origin-scoped site routes mount a small equivalent because they
intentionally skip `GlobalStyles`
- adds legacy `SuisseIntl` / `SuisseIntl-Mono` font-family aliases for
existing UI typography consumers when Origin globals are active
- removes unused `pretty-scrollbar` globals from both Origin and Emotion
global styles
- updates Origin package exports/files/package checks so SCSS
entrypoints are published and package validation ignores non-JS style
entrypoints in `attw`
- fixes the Origin `LoadMore` trigger type conflict exposed once the
private site imports Origin styles

## Validation
- `git diff --check`
- `yarn workspace @lightsparkdev/origin package:checks`
- `yarn workspace @lightsparkdev/origin lint:styles`
- `yarn workspace @lightsparkdev/origin build:styles`
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Button/Button.test.tsx`
- `yarn workspace @lightsparkdev/site exec eslint src/Root.tsx`
- `yarn workspace @lightsparkdev/ui exec eslint src/styles/global.tsx`
- `yarn turbo run types --filter=@lightsparkdev/site`
- pre-commit hook passed earlier for the global stylesheet split (`yarn
install`, `yarn format`)
- Playwright spot checks on local `start:dev`:
- `/login` has `html.origin`, Origin body styles (`14px / 20px "Suisse
Intl"`), Origin background/text tokens, and the body breakpoint marker
- RSK `/dashboard` has no `html.origin`, keeps Emotion globals (`12px /
14.52px Montserrat`), and keeps the breakpoint marker
- RSK `/transactions/sent` keeps Emotion globals and restored
transaction empty-state/card spacing (`320x128`, `32px` padding)

## Notes
- This PR is now the base of the button-render work; #26933 stacks on
top of it.
- `scope.scss` intentionally prefixes Origin global rules with
`html.origin`; non-Origin routes continue to use the existing Emotion
global stylesheet.
- Storybook-only local changes used for visual testing remain
uncommitted.

GitOrigin-RevId: d6ae738f069fe1daffb41301762dd50bc553cab4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants