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 11 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 4d66177 |
…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
…n domain (#26977) ## What Small change to BarChart so signed-value bars anchor at the zero line — negatives hang down, positives grow up — instead of all rendering from the plot bottom. Sheets, Looker, d3 defaults, and recharts all do this; Origin was the odd one out. For each non-stacked bar we compute `anchor = clamp(0, yMin, yMax)` and draw between `anchor` and the value: - **All-positive data** — anchor lands at `yMin` (bottom). Visual identical to before. - **Mixed signs** — anchor is `0`. Positives grow up, negatives hang down. - **All-negative data** — anchor lands at `yMax` (top). Bars hang down to their value. Same treatment applied to the horizontal orientation. Stacked path is intentionally untouched — cumulative semantics already differ from the simple value→height mapping. ## Why Came up while building a daily net inflow/outflow bar chart in lighthouse — the chart's domain spanned negative values, but every red bar was rendered from the bottom of the plot area up to the value, which made small negative days look as severe as the worst negative day. ## Not a breaking change - No API change — no props added, removed, or retyped. - All-positive data renders pixel-identical (`clamp(0, yMin, yMax) = yMin` when yMin is 0). - Only diffs are mixed-sign and all-negative charts, which were arguably broken before this. ## Notes - Originally proposed against the old origin repo at lightsparkdev/origin#129; moved here per @coreymartin. - Lighthouse currently has a small recharts-based bar chart bridging the signed-data case ([lighthouse#383](lightsparkdev/lighthouse#383)). Plan is to drop that bridge and use Origin directly once this lands. ## Test plan - [ ] Existing storybook bar charts (all-positive) render identically — visual diff is a no-op. - [ ] Mixed-sign story: bars cross the zero line cleanly. - [ ] Horizontal orientation: bars extend left of the zero column for negatives. - [ ] Stacked path unchanged. GitOrigin-RevId: 807866e8c7aa64e986b8b31f370630e162cabb6c
## Reason The Nage login flow is starting to adopt Origin buttons, and the auth page needs the Origin-backed actions to render with the same visual treatment and spacing as the existing SSO action. ## Overview - Builds on the scoped Origin globals that landed in #26900, now that this PR targets `main` directly. - Adds `fullWidth` support to Origin `Button` and covers it in tests/stories. - Bridges the app theme to Origin's `data-theme` tokens for Origin components rendered in the private site. - Updates login email and SSO actions to use `NageButton` with the previous 10px button spacing preserved at the auth form layout level. - Adds the Origin mono font asset needed by the scoped Origin stylesheet. ## Test Plan - `git diff --check` - `yarn workspace @lightsparkdev/origin package:checks` - `yarn workspace @lightsparkdev/origin lint:styles` - `yarn workspace @lightsparkdev/origin test:ct src/components/Button/Button.test.tsx` - `yarn workspace @lightsparkdev/site exec eslint src/Root.tsx src/components/AuthForm.tsx src/pages/login/Login.tsx src/uma-nage/components/NageButton.test.tsx` - `yarn turbo run types --filter=@lightsparkdev/site` GitOrigin-RevId: 5ea673b4ae149244197416c602ad5c936e116c1b
## Summary - add `checks-tests` as the JS check command that keeps `gql-codegen`, lint, format, circular dependency checks, and package checks in the same Turbo task flow while also running `test` - run `yarn checks-tests` from the JS workflow check job, keeping `.lightsparkapienv` for tests and removing the obsolete `.lightsparkenv` setup - make Origin package `test` run Vitest unit tests only, with Playwright component tests under `test:ct` / `test:all` - make Origin package `build` run TypeScript checks before emitting styles ## Notes - Measured recent `ui-test-hermetic` Chromium install steps at 25s, 26s, 33s, 37s, and 42s, so this PR intentionally avoids adding Playwright browser installation to JS CI for now. ## Test Plan - `yarn install --immutable` - `yarn checks-tests` - `git diff --check` - pre-commit JS format hook GitOrigin-RevId: 64f9c2942a870c40540faaeb9d19f119736cf171
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.