DES-23: Add LoadMore infinite-scroll primitive + useLoadMore hook ## Summary S#523
DES-23: Add LoadMore infinite-scroll primitive + useLoadMore hook
## Summary
S#523github-actions[bot] wants to merge 8 commits intomainfrom
Conversation
## 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
|
The following public packages have changed files:
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 |
…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
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.