Skip to content

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

Open
github-actions[bot] wants to merge 89 commits into
mainfrom
develop
Open

DES-23: Add LoadMore infinite-scroll primitive + useLoadMore hook ## Summary S#523
github-actions[bot] wants to merge 89 commits into
mainfrom
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

github-actions Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor Author

The following public packages have changed files:

Changed Current version
@lightsparkdev/lightspark-cli 0.1.18
@lightsparkdev/lightspark-sdk 1.9.18
@lightsparkdev/eslint-config 0.0.1
@lightsparkdev/crypto-wasm 0.1.25
@lightsparkdev/origin 0.14.2
@lightsparkdev/static 0.0.0
@lightsparkdev/oauth 0.1.67
@lightsparkdev/core 1.5.1
@lightsparkdev/vite 0.0.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 97933dc

jaymantri and others added 25 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
…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
## Summary
- Add an Origin Storybook main-branch deploy workflow that publishes to
`s3://lightspark-dev-web/app/origin-storybook/` for
`https://dev.dev.sparkinfra.net/app/origin-storybook/`.
- Switch Origin Storybook static builds from `@storybook/nextjs` to
React Vite, matching the package as a static SPA bundle.
- Fix Vite CSS Modules handling for Combobox chips and add the missing
Suisse font assets used by Origin tokens.
- No ops PR is needed: the existing webdev `github-actions` dev role
already allows writes to `lightspark-dev-web/*`.

## Test Plan
- `yarn turbo run build-sb --filter=@lightsparkdev/origin --force`
- `rg -n "@use|@include|url\\(/fonts/SuisseIntl|src=\\\"/|href=\\\"/"
js/packages/origin/storybook-static/index.html
js/packages/origin/storybook-static/iframe.html
js/packages/origin/storybook-static/assets -g "*.css"` (no matches)
- `ruby -e "require \"yaml\"; YAML.load_file(ARGV.fetch(0)); puts
\"ok\"" .github/workflows/deploy-origin-storybook.yaml`
- `git diff --check`
- Local Playwright smoke against `http://127.0.0.1:8081/`: Storybook
loaded, 0 console errors; one Storybook 11 ariaLabel warning from
manager UI.
- `yarn workspace @lightsparkdev/origin lint` (0 errors; 2 pre-existing
accessibility warnings)
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Combobox/Combobox.test.tsx` (21 passed)

GitOrigin-RevId: 1f59c53078e04db5a741a90a85789ab5cf491845
## Summary
- add Origin Storybook to the existing PR UI preview deployment matrix
- deploy Storybook previews under `/app/origin-storybook-pr-<PR>/`,
using the existing generic `/app/*` routing instead of changing
`/preview/*` behavior
- pass the preview base path through Turbo for `build-sb`
- delete `/app/origin-storybook-pr-<PR>/` during existing PR preview
cleanup
- include a small Origin finalizer comment change so this PR triggers a
real preview deployment

## Validation
- `bash -n scripts/gha/detect-ui-preview-apps.sh`
- `node --check
js/packages/origin/scripts/finalize-storybook-static.mjs`
- `git diff --check`
- YAML parse for preview deploy and cleanup workflows
- detector matrix test for
`js/packages/origin/scripts/finalize-storybook-static.mjs`
- `ORIGIN_STORYBOOK_BASE_PATH=/app/origin-storybook-pr-27001/ yarn turbo
run build-sb --filter=@lightsparkdev/origin --force`
- generated HTML check: expected `/app/origin-storybook-pr-27001/`
`<base>` paths and no inline scripts in `index.html` / `iframe.html`
- PR UI Preview Deploy passed, including Origin Storybook upload to
`s3://lightspark-dev-web/app/origin-storybook-pr-27001/`

GitOrigin-RevId: 203ef90b92ae10f85a4df0b5e3c4a60dc58c3c71
## Reason

Scoped Origin globals were adding `html.origin` specificity to reset
selectors. That let resets like a transparent button background outrank
component classes until hover, so components consuming
`@lightsparkdev/origin/scope.scss` could render with reset styling
instead of their intended variant styles.

## Overview

Wrap the scoped entrypoint in zero-specificity `:where(...)` selectors.
This keeps scoped globals limited to `.origin` routes while allowing
Origin component classes to win over resets normally.

## Test Plan

- `git diff --check`
- `yarn workspace @lightsparkdev/origin lint:styles`
- `yarn workspace @lightsparkdev/origin types`
- `yarn workspace @lightsparkdev/origin playwright test -c
playwright-ct.config.ts src/components/Button/Button.test.tsx`
- Confirmed `html.origin :where(button)` is gone and the scoped selector
uses `:where(html.origin)`

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

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: f42b15ebe010458ab483d7ab19904e7de85fdce8
…#26924)

## Summary

Aligns Origin's Combobox with [Base UI's documented multi-select
pattern](https://base-ui.com/react/components/combobox#multiple-select)
and deletes the workarounds DES-18 layered on top of the original
divergence.

The audit traced the workarounds back to one architectural choice:
`Combobox.Value` always wrapped `BaseCombobox.Value` in an extra
`<span>`, even though Base UI's Value is renderless. To keep that span
from breaking the flex layout in `Combobox.Chips`, the file carried
`valueWithChildren { display: contents }`. From there, a `:has(.chip)`
rule (which itself needed a synthetic marker class), and a `.chip +
.input` adjacent-sibling margin all stacked up to recover spacing. None
of these appear in Base UI's documented pattern.

Pulling that thread further surfaced two more layers of accidental
complexity (an `AnchorContext` that the InputGroup primitive made
redundant, and a single-select branch on `Combobox.Value` that no
consumer used). Both are dropped here.

## What changed

1. **`Combobox.Value` is renderless when `children` is a render
function.** Chips and Input become direct flex children of
`Combobox.Chips` with no intermediate `<span>`. Single-select path
unchanged.
2. **Drop the synthetic `.chip` marker class and the `:has(.chip)`
rule.** `Combobox.Chip` applies `Chip.module.scss`'s `root` + `sm`
directly. Wrapper `padding-left` is reduced via `:has(.chips)` (which
always matches in multi-select).
3. **Replace `.chip + .input { margin-inline-start }` with `.chips
.input { padding-inline-start }`.** Same visual outcome, no
adjacent-sibling dependency, mirrors Base UI's demo (Input owns its own
left padding).
4. **`Combobox.ItemCheckbox` no longer overwrites consumer `render`.**
Default `render` is applied before `{...props}`, so a consumer-supplied
`render` wins.
5. **`Combobox.Separator` wraps `BaseCombobox.Separator`** instead of a
hand-rolled `<div role="separator">`, matching the rest of the file and
exposing the `render` escape hatch.
6. **`Combobox.InputWrapper` now wraps `BaseCombobox.InputGroup`.** The
exported `Combobox.InputWrapper` name is preserved (consumers don't
migrate). Migration retires the `&:has(.input:disabled)` style hack in
favor of `&[data-disabled]` (always available), and adds
`&[data-focused]` alongside `:focus-within` so the focus ring fires
under `Field.Root` via the data attr and outside `Field.Root` via the
legacy selector. The existing `&[data-invalid]` rule now actually fires
under `Field.Root` (previously the raw `<div>` carried no such attr).
7. **Replace the broken `Multiple` story with the chips pattern.** The
previous story used the single-select layout with `multiple` —
selections were made but not surfaced anywhere (no `Combobox.Value`
render, no chips). New `Multiple` story matches Base UI's canonical
multi-select example, with `aria-label` on `Combobox.ChipRemove`
("Remove Apple") rather than `Combobox.Chip` (the chip's value is its
own visible label; the remove button needs disambiguation).
8. **Drop `AnchorContext`.** Post item 6, `BaseCombobox.InputGroup`
self-registers as `inputGroupElement` in the combobox store
([`ComboboxInputGroup.js:55`](https://github.com/mui/base-ui/blob/master/packages/react/src/combobox/input-group/ComboboxInputGroup.tsx)),
and `BaseCombobox.Positioner` resolves its anchor as `anchor ??
(inputInsidePopup ? triggerElement : inputGroupElement ?? inputElement)`
([`ComboboxPositioner.js:59`](https://github.com/mui/base-ui/blob/master/packages/react/src/combobox/positioner/ComboboxPositioner.tsx)).
Origin's manual ref-forwarding through context was pointing at the same
DOM node Base UI auto-resolves to, so the entire context plumbing is
dead. Dropping it also unblocks Base UI's input-inside-popup pattern
(which the hardcoded override silently closed off).
9. **Collapse `Combobox.Value` to a `BaseCombobox.Value` pass-through.**
The remaining single-select branch (wrapping in `<span class="value">`)
had zero call sites — every story and the playground page render the
selected value via `Combobox.Input`, not `Combobox.Value`. The `.value`
SCSS rule, dual-mode forwardRef wrapper, dev-mode `console.warn`, and
the `ConformanceValue` test fixture (already skipped) are all gone.
`ValueProps` is now `BaseCombobox.Value.Props`.

## Deferred to follow-up

- **`Combobox.Chip` child-splitting.** Currently splits children by
type-equality with `ChipRemove` and wraps the rest in `<span
class={chipStyles.label}>`. Load-bearing for label-text styling reuse
with standalone `Chip` — removal requires a SCSS shuffle and parallel
cleanup of standalone `Chip`. Filed as a separate ticket.

Resolves [DES-24](https://lightspark.atlassian.net/browse/DES-24).
Follows up on [DES-18](https://lightspark.atlassian.net/browse/DES-18)
(#26842).

## Test plan

- [x] `yarn workspace @lightsparkdev/origin test:unit` — 447 pass
- [x] `yarn workspace @lightsparkdev/origin lint` — clean (only 2
pre-existing unrelated warnings in `DatePicker` and `Sidebar`)
- [x] `yarn workspace @lightsparkdev/origin format` — clean
- [x] `apps/private/site` `yarn tsc` — clean
- [x] Visual: `Default` story unchanged (border, chevron, focus ring on
input click, popup open/close)
- [x] Visual: `Disabled` story renders with reduced opacity via
`&[data-disabled]`
- [x] Visual: `Multiple` story (empty / one chip / many-chip overflow)
renders identically; cursor still has breathing room when typing after a
chip; selecting items live now adds chips with `Remove <fruit>`
accessible names on the dismiss buttons.
- [x] Visual: `WithField` story — `data-focused` fires on focus and
applies `--input-focus` ring; `data-invalid` fires on
blur-with-empty-value and applies `--border-critical` +
`--input-focus-critical`. Verified via runtime inspection of computed
styles on the live `[role=group]` element.
- [x] Anchor resolution: with `AnchorContext` removed, runtime check on
`Default`, `Multiple`, `WithGroups`, `WithField` confirms popup
`--anchor-width` matches the `[role=group]` element's
`getBoundingClientRect().width` to within 1px sub-pixel rounding. Base
UI's auto-resolve from `inputGroupElement` is working.

## Consumer impact

None expected. The single-select `Value` path is unchanged at the
call-site level (consumers don't pass `<Combobox.Value />` for
single-select today). Multi-select `Value` no longer renders the outer
`<span>`, but consumers don't reach into that DOM.
`Combobox.InputWrapper`'s exported name is preserved; the underlying
primitive change to `BaseCombobox.InputGroup` is transparent.
`Combobox.Value` is now a direct re-export of `BaseCombobox.Value`;
props are forwarded natively. Grid `AddCustomerPanel.tsx`
`FieldMultiCombobox` continues to work without changes.

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

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

GitOrigin-RevId: 1479d119d378b69bcce7cd9fd92fdac0969482dd
## Summary
- make dev proxy cookie-file ALB cookies override stale localhost
browser ALB cookies
- detect Cognito redirects and ALB-generated HTML 500s as stale dev
proxy session signals
- launch the existing Cognito cookie refresh flow and return explicit
retry guidance
- let the cookie refresh script honor the configured cookie file path

## Testing
- node --check js/packages/vite/index.js
- node --check js/apps/private/scripts/dev-proxy-cookies.mjs
- prettier --check js/packages/vite/index.js
js/apps/private/scripts/dev-proxy-cookies.mjs
- git diff --check -- js/packages/vite/index.js
js/apps/private/scripts/dev-proxy-cookies.mjs
- pre-commit hook: js yarn install + yarn format
- manual Playwright repro: LoginWithPassword on stale localhost ALB
cookies returned GraphQL Invalid credentials instead of ALB HTML 500
after fix

GitOrigin-RevId: 4bd794889f9b52b5f51ea73c2595100d95fc1eb0
## Summary
- migrate the ops DLQ page to DataManagerTable pagination and task-name
filtering
- add shared table row selection so retry/delete only uses selected DLQ
messages
- add DLQ GraphQL cursor/page_info support for internal and paycore
schemas with stable SQS snapshot pagination

## Validation
- yarn eslint src/pages/ops/dead-letter-queue/DeadLetterQueue.tsx
- yarn eslint src/components/Table/Table.tsx
- yarn types in js/apps/private/ops
- yarn types in js/packages/ui
- uv run python -m py_compile
sparklib/graphql/objects/root_to_dead_letter_queue_messages_connection.py
sparklib/graphql/queries/ops/dead_letter_queue_messages_query.py
sparklib/graphql/mutations/ops/manage_dead_letter_queue_message.py
- git diff --check

GitOrigin-RevId: 1d44acfc14220c2d7717fe90cc12faa8697bb7f7
## Reason

Field.Root should expose the same Base UI composition surface that
Origin consumers expect from other primitives. Allowing the Base UI
`render` prop avoids product-side workarounds when a product needs to
render the field root as a semantic or framework-specific element.

Jira: https://lightspark.atlassian.net/browse/DES-26

## Overview

This exposes the inherited Base UI `render` prop on `Field.Root` and
keeps Origin root styling merged for both string and stateful
`className` callbacks. The component test stories and CT coverage
exercise a custom rendered root, invalid state propagation, class
merging, and stateful root class names.

Separate visual Storybook review passed. The unrelated focus/error
contrast issue found during review was filed as DES-27.

## Test Plan

- `git diff --check`
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Field/Field.test.tsx`
- `yarn workspace @lightsparkdev/origin types`
- No app Playwright run: direct Origin component CT changed; no app
Playwright overlap.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: 6f8fd21ba4b04b1747777d098808e983a8a7a98a
## Reason

DES-25 needs `Combobox.Chip` composition to match Base UI pass-through
behavior instead of relying on child introspection/type checks. This
keeps wrapped `ChipRemove` usage working and brings standalone `Chip`
into the same direct-children rendering model.

## Overview

- Remove the `Combobox.Chip` child split between label content and
`ChipRemove`; children now pass directly through to Base UI.
- Render standalone `Chip` children directly for parity, while moving
default chip typography/color styling to the root and preserving dismiss
spacing.
- Add Combobox and Chip coverage for arbitrary child content and wrapped
chip removal.

Refs DES-25.

## Test Plan

- User visually reviewed and approved Storybook.
- `git diff --check`
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Combobox/Combobox.test.tsx
src/components/Chip/Chip.test.tsx`
- `yarn workspace @lightsparkdev/origin types`
- `yarn workspace @lightsparkdev/origin lint:styles`

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

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: ceeef4f54667f17f2a0666fb3bafabad5e8050fb
## Reason

Field.Label is a flex container, so relying on JSX whitespace between
label text and suffix children is unreliable. DES-30 needs a consistent
tokenized gap so suffix content such as "(optional)" does not visually
run into the label.

https://lightspark.atlassian.net/browse/DES-30

## Overview

Adds the Origin Field label gap using `var(--spacing-3xs)` and covers
the suffix case with Storybook and component-test fixtures.

## Test Plan

- User visually reviewed the Field Storybook state and confirmed it
looks good
- `git diff --check`
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Field/Field.test.tsx`
- `yarn workspace @lightsparkdev/origin types`
- `yarn workspace @lightsparkdev/origin lint:styles`

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

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: a5eea0aa22374e07e8102ff6ca15003cfd00cd0b
## Reason

Select popups should consume Base UI's available-height and
available-width variables like Combobox and Autocomplete, so long option
lists stay bounded by the viewport instead of extending offscreen.

Refs [DES-29](https://lightspark.atlassian.net/browse/DES-29).

## Overview

- Bounds the Select popup and list with Base UI sizing variables.
- Lets long Select lists scroll within the popup.
- Adds a long-list Storybook example and component-test regression
coverage.

## Test Plan

- User visually reviewed the LongList Storybook story and approved.
- `git diff --check`
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Select/Select.test.tsx`
- `yarn workspace @lightsparkdev/origin types`
- `yarn workspace @lightsparkdev/origin lint:styles`

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

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: 5ba77d455fdefa102a307d57d6a2694f5ea77d97
## Reason

Base UI Button should keep button semantics and not be used as the
link-rendering path. This adds a generic Origin `ButtonLink` so
anchor-based actions can preserve link semantics while sharing Button
visuals.

DES-31: https://lightspark.atlassian.net/browse/DES-31

## Overview

- Add `ButtonLink` as an anchor-rendering visual button API in Origin,
including `href` and render-composition support.
- Keep `NageButton` action mode on Origin `Button`, and route mode on
`ButtonLink` composed with the typed router `Link` so routed Nage
buttons remain `role=link`.
- Add ButtonLink stories/tests and NageButton routed-link semantics
coverage.

## Test Plan

- User visually reviewed Storybook and approved.
- `git diff --check`
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Button/Button.test.tsx`
- `yarn workspace @lightsparkdev/site vitest run
src/uma-nage/components/NageButton.test.tsx --environment jsdom`
- `yarn workspace @lightsparkdev/origin types`
- `yarn workspace @lightsparkdev/site types`
- Skipped app Playwright: this change has focused Origin component and
NageButton jsdom coverage, with no router or route-level changes.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: 990f3a61857ce0f06361b8057b082c75b9bee1a0
## Reason

DES-28: Align `Tabs.Panel` with the underlying Base UI primitive content
slot so product layouts can own their own panel presentation instead of
inheriting card-like styling from Origin.

https://lightspark.atlassian.net/browse/DES-28

## Overview

This removes layout, padding, border, overflow, and corner styling from
`Tabs.Panel` while preserving the hidden-state behavior needed for
inactive panels. Storybook examples now apply their card presentation at
the story level so the component stays thin over Base UI without adding
a public `variant` or `unstyled` API.

## Test Plan

- User visually reviewed Storybook and approved
- `git diff --check`
- `yarn workspace @lightsparkdev/origin test:ct
src/components/Tabs/Tabs.test.tsx`
- `yarn workspace @lightsparkdev/origin types`
- `yarn workspace @lightsparkdev/origin lint:styles`

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

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: e773751e727a25590db98d79ca5876496a40274e
## Summary

Introduces `Pager`, a new compound primitive in `@lightsparkdev/origin`
for cursor / keyset / time-window pagination. Tracks
[DES-22](https://lightspark.atlassian.net/browse/DES-22).

`Pagination` requires `page` + `totalItems`. Cursor-based APIs don't
have totals (counting is expensive and often meaningless), so consumers
today fake totals or maintain a cursor↔page mapping just to use the
visual button group. `Pager` absorbs that friction: it accepts only
`hasPrevious` / `hasNext` plus `onPrevious` / `onNext`. Visually it is
intentionally a copy of `Pagination.Navigation`.

## API

- `Pager.Root` — `<nav aria-label="Pager">`, owns context and analytics
wrapping. Carries `data-no-previous` / `data-no-next` boundary attrs.
- `Pager.Navigation` — `<div role="group" aria-label="Page
navigation">`. The joined-pill container.
- `Pager.Previous` / `Pager.Next` — `<button>` with auto-derived
`disabled` (overridable), `data-direction`, `data-disabled`, default
chevron icon. Composes consumer `onClick`; `event.preventDefault()`
suppresses the context handler.
- `Pager.Status` — `<span role="status" aria-live="polite"
aria-atomic="true">`. Always mounted so the live region survives empty
renders.
- `usePagerContext` / `PagerContext` — exported for advanced
composition.

Every part forwards refs and accepts `render` via Base UI's `useRender`,
so `render={<a href="?after=…" />}` swaps the underlying element while
preserving handlers, refs, ARIA, and class names.

## Decisions

- **Visual parity with `Pagination` is byte-identical and inlined.** No
`composes:`, no shared SCSS partial. Pager owns its own complete CSS. If
DES-21 later extracts the shared rules into a Pagination-namespaced
mixin, it can DRY both modules in a follow-up.
- **Analytics follow `component.interaction`.** With `analyticsName` set
on Root, clicks fire `Pager.click` with `metadata: { direction:
"previous" | "next" }` via `useTrackedCallback`.
- **Status renders the `<span>` even when children are empty** so the
live region stays mounted across renders.

## Test plan

- [x] `yarn workspace @lightsparkdev/origin test:unit` (20 new vitest
specs, 441 total passing)
- [x] `yarn workspace @lightsparkdev/origin lint` (0 errors)
- [x] `yarn workspace @lightsparkdev/origin format` (clean)
- [x] `yarn workspace @lightsparkdev/origin types` (clean)
- [ ] Playwright CT specs in `Pager.test.tsx` cover structure, derived
disabled, click composition (incl. `preventDefault`), keyboard
activation, render-prop swap to `<a>`, data-attrs, and visual parity
with `Pagination` via `boundingBox` + computed `border-radius` /
`box-shadow` / `background-color`. Ready to run via `test:ct` when CI
executes the suite.
- [ ] Storybook covers Default, NoPreviousCursor, NoNextCursor,
BothEdges, WithoutStatus, WithRenderPropAsLink,
SideBySideWithPagination.

## Coordination

DES-21 owns any future extraction of the shared Prev/Next button styles
into a Pagination-namespaced SCSS partial. This PR inlines the rules as
a deliberate duplicate per the explicit decision recorded on DES-22.

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

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

GitOrigin-RevId: 0b175dbcd4355bdec285074bc9498c570568d5d2
## Summary

- Increase Yarn npm minimal age gate from 1 day to 3 days for the JS
workspace.
- Keep @lightsparkdev packages preapproved through the existing wildcard
exemption.

## Impact

New npm package versions must now be at least 4320 minutes old before
Yarn considers them for installation, reducing exposure to freshly
published compromised packages.

## Validation

- yarn config get npmMinimalAgeGate
- pre-commit hook: yarn install
- pre-commit hook: yarn format

GitOrigin-RevId: bdc27882e2e466827479bcd3828674bc6521af81
## Reason

The backend stack (#27196 → #27206) wires USDT-on-Tron all the way through paycore, the LSP grid switch, and the rental layer — but the Grid dashboard frontend has no way to actually pick USDT or Tron in the payout flow. This PR adds the four missing pieces in `js/apps/private/site/src/uma-nage` so platforms with USDT in their backend config see it as a funding source and Tron as the network.

## Overview

**Currency-and-network plumbing** (`currencyFields.ts`):
- `USDT` registered as a crypto currency with `TRON` as its only supported network (matches our backend: USDT-Tron is the only USDT corridor we support).
- `TRON → TRON_WALLET` added to `CRYPTO_NETWORK_TO_ACCOUNT_TYPE`.
- Tron Base58Check address validator (`^T[1-9A-HJ-NP-Za-km-z]{33}$`) added to `CRYPTO_ADDRESS_VALIDATORS`. **Not EVM-style** — Tron uses its own base58check encoding, not 0x hex; getting this wrong was the most likely subtle bug.
- USDT entry in `CURRENCY_ACCOUNT_FIELDS` with the same single "Wallet address" field that USDC has.

**Quote-flow network selector** (`EnterAmountPanel.tsx::getCryptoNetworkOptions`):
- When the realtime-funding currency is USDT, return only the Tron option (`TRON_TESTNET` in sandbox / local-dev, `TRON_MAINNET` in prod).
- USDC keeps its current Base / Polygon / Solana options unchanged.

**Platform-config gating**: works automatically. `realtimeFundingOptions` is built from `platformConfig.supportedCurrencies` (lines 172–180), so USDT only appears in the funding-source dropdown if the platform has USDT enabled server-side. No new feature flag needed on the frontend; no risk of USDT appearing for platforms that don't have it.

**Tron chain icon** (`packages/ui/src/icons/chains/`):
- New `Tron.tsx` component (Tron-brand red circle + angular logo).
- Added to `ChainIcon`'s `Chain` type and `CHAIN_COMPONENTS` map.

## Test Plan

- 8 new test cases in `currencyFields.test.ts` for the Tron Base58Check validator: canonical addresses, T-prefix requirement, length, disallowed base58 chars (`0`, `O`, `I`, `l`), EVM-style address rejection.
- 17/17 `currencyFields.test.ts` cases pass total (8 new + 9 existing for Solana / Base / Polygon).
- `yarn lint && yarn format && yarn tsc --noEmit` all clean across `@lightsparkdev/site` and `@lightsparkdev/ui`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

GitOrigin-RevId: 50ac10aa773bb53f4cf5107811ff95059d7ae1f6
## Summary

Makes the local site Playwright E2E workflow Kind-first and
self-contained. The normal path is now:

```bash
cd js/apps/private/site
./playwright/run-tests.sh
```

The runner creates or repairs the Kind cluster if needed, starts Tilt
with the lightning profile, waits for core backend resources,
verifies/restarts `sparkcore-app` when Kubernetes readiness disagrees
with Tilt, seeds Nage/Grid prerequisites, deploys the built site as a
static nginx pod, and runs Playwright against
`https://app.minikube.local`.

### Local runner and environment management

- **`run-tests.sh`**: Single entry point for local E2E runs. Handles
Kind setup/repair/recreate, Tilt startup, backend readiness, stale
Sparkcore recovery, static-site deploy, ingress guarding, Nage/Grid seed
checks, and local retries defaulting to single-pass.
- **Stripe billing**: The Kind/static-deploy path now runs the real
Stripe PaymentElement billing flow locally. The old local Stripe
skip/exclusion flags, minikube auto-exclusion, and direct GraphQL
billing fallback have been removed.
- **`--clean`**: Runs and waits for `playwright/destroy-test-env.sh`
before proceeding, giving a fresh local app state in one command.
- **`destroy-test-env.sh`**: Tears down Tilt, Kind/minikube runtime
state, managed hosts entries, Playwright auth/results, `dist`, and turbo
daemons.
- **`setup-kind.sh` / `k8s/` manifests**: Fallback Kind setup and static
nginx deployment resources for serving the site inside the local
cluster.

### Test and fixture fixes

- **RSK/Nage setup projects**: Split setup state so RSK tests can run
even if Nage setup fails; Nage onboarding now verifies Grid readiness
and USDB support.
- **Go-live/billing flow**: Runs the real Stripe HTTPS redirect path on
Kind and keeps only a small real-UI retry for Stripe PaymentElement
submit timing.
- **Payments and UI flakes**: Adds targeted waits/recovery for local
backend latency and two-worker concurrency races.
- **Local seeding**: Ensures billing plans, UI test gatekeepers, and
Nage/Grid switch data exist before tests run.

### Docs

- **README.md**: Updated for the current Kind-first flow, `--clean`,
local troubleshooting, two-worker project execution, current trace/video
behavior, and the fact that Kind runs the full billing flow with Stripe.

## Test plan

- [x] `bash -n js/apps/private/site/playwright/run-tests.sh
js/apps/private/site/playwright/destroy-test-env.sh
js/apps/private/site/playwright/setup-kind.sh`
- [x] `git diff --check`
- [x] `cd js/apps/private/site && yarn lint`
- [x] `cd js/apps/private/site && ./playwright/run-tests.sh
--test-filter=playwright/tests/00-go-live-billing.spec.ts` (Kind/static
deploy, Stripe included; 8 passed)
- [x] Local Kind setup path exercised after `destroy-test-env.sh`:
recreated Kind, configured DNS/registry, started Tilt, and reached
backend resource waits
- [ ] `cd js/apps/private/site && yarn types` currently fails in
unrelated existing app source files with broad
`CurrencyUnit`/`CurrencyUnitType` mismatches outside this PR's changed
files
- [ ] CI hermetic Playwright run

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: ec87440e08a583e495329f28e0a83651d011459c
akanter and others added 30 commits June 4, 2026 05:46
… (#28134)

> **[Claim this PR](https://zeus.dev.dev.sparkinfra.net/github/claim?job=thermal-warden-3&repo=webdev&pr=28134&sig=403f9bbde53e8391af5a51cdf192187bbbc6a9f9146ad54d6eee8fa38130bdbe)** — take ownership under your GitHub account

## Reason
Follow-up to the gatekeeper search UX (#28091): make the results list keyboard-navigable so you can search and select a GK without the mouse — arrow up/down to move a highlighted row, Enter to open it.

## Overview
Adds an **opt-in** keyboard-navigation capability to the shared `Table`/`DataManagerTable` (default off → no change to any existing table), then wires it into the gatekeeper page.

- **`packages/ui/.../Table/Table.tsx`** — new `keyboardRowNavigation` prop plus optional controlled `activeRowIndex` / `onActiveRowIndexChange`. When on: highlights an active row, moves it with ArrowUp/ArrowDown (clamped, scroll-into-view), resets to the top row (0) whenever the result set changes, uses a **roving tabindex** (active row `0`, others `-1`), sets `role="grid"` so `aria-selected` is valid, and styles the active row (reusing the hover background + a left accent). Enter on a focused row keeps using the existing click-nav path.
- **gatekeeper page** (`OpsGkOverview` + `OpsGkListTable`) — owns `activeRowIndex`, drives it from the search box (arrows move the highlight, Enter opens the highlighted GK), and passes it through. Top row is pre-highlighted, so **type → Enter opens the first result** with no arrow presses. The existing `?search=`/`?name=` behavior is unchanged.

Incorporates all four review notes from the plan (exhaustive-deps via a ref so the reset effect is dep-free; page owns navigation so no `onClickDataRow` signature clash; roving tabindex; `role="grid"`).

## Test Plan
The ops app has no test harness, so verified via a Puppeteer harness rendering the real `OpsGkOverviewPage` with a fake Apollo layer: results render with row 0 highlighted, ArrowDown/ArrowUp move the highlight (`active` 0→1→2→1), Enter from the search box navigates to the highlighted GK, `role="grid"` and roving `tabindex` confirmed. `tsc` + `eslint` clean on `packages/ui` and ops (incl. no `react-hooks/exhaustive-deps` violation), and a production `vite build` of ops succeeds. Real CI runs `js-workspaces / check` + `test` + `build-and-deploy (ops)`.

## Private
[Plan](https://s3.console.aws.amazon.com/s3/object/lightspark-dev-bolt-logs?prefix=jobs/thermal-warden-3/plan.md) (S3, internal only — includes the reviewer-refined design)

## Public
Keyboard navigation for the ops gatekeeper results list.

---
🤖 [thermal-warden](https://zeus.dev.dev.sparkinfra.net/#/arc?id=thermal-warden)[(#3)](https://zeus.dev.dev.sparkinfra.net/#/instance?id=thermal-warden-3) | [Feedback](https://zeus.dev.dev.sparkinfra.net/feedback)

GitOrigin-RevId: cab21c0ee52fbb084b1139ec482559cf7b857cbf
## Summary

- Adds the read-only Grid Home foundation for platform balances, recent
transactions, payout volume, account details, and funding-instruction
display.
- `GRID_DASHBOARD_HOME_ENABLED` gates Home route and nav exposure; when
the gate is off, Home is not exposed through the route or navigation.
- Moves crypto/network icons into the shared UI package, fixes
Lightning/Spark icon fidelity, and cleans Home styling/imports back to
the intended Emotion/UI-package patterns.

## Gate / rollout notes

- Home is read-only in this PR. It does not add Transfer, Receive/Add
Funds, Withdraw, Send, payout creation, or external-account ownership
semantics.
- Funding instructions are display-only here; money movement and setup
action flows are layered in later PRs.
- Styling/icon changes are asset fidelity, shared reuse, and
implementation cleanup only; they do not change Home's gate semantics.

## Test plan / validation

- Focused Home, route-gating, `NageApp`, account drawer, amount
formatting, account display, transaction display, funding-instruction,
and Grid API query tests passed.
- Type/lint validation passed for the touched Grid UI areas.
- Known local blocker: broader Home test runs hit React 19 `findDOMNode`
behavior in the local test harness; the focused coverage above passed.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: 7a4cb721ca9ae418ca52828ed57c85655ff0a439
…8259)

## Summary

- Wires up China (CNY) as a mobile-wallet payout corridor — AliPay and
WeChatPay. `bank_name` on the request disambiguates which wallet to
settle through.
- The upstream provider's CN bank payers are B2B-only on the sender side
(registered_name + registration_number), so the bank rail isn't usable
for our consumer C2C flow — explicitly scoped out of this PR.
- Sender extras for CNY (NATIONALITY + COUNTRY_OF_RESIDENCE + ID_TYPE +
ID_NUMBER) are the union of AliPay C2C and WeChatPay C2C published
sender requirements — same shape as the existing EGP extras.
- Account-create / read paths are intentionally deferred to a follow-up:
`CURRENCY_TO_ACCOUNT_TYPE[CNY]` and the `CurrencyUnit.CNY` case in
`gen_convert_to_external_account_info` both need
`CnyExternalAccountInfo` / `CnyExternalAccountCreateInfo` Pydantic
models from grid-api, which generate post-merge after the new partials
propagate via `sync-external-accounts.yml` and the next grid-api mirror
regen lands in `webdev/grid-api/`.

## Test plan

- [x] `uv run pytest sparkcore/grid/utils/__tests__/
sparkcore/bridge/extend_integration/__tests__/
sparkcore/grid/destination_resolution/__tests__/
sparkcore/bridge/__tests__/` — 638 passed
- [x] `uv run pytest sparkcore/grid/__itests__/ -m 'not minikube_spark'`
— 413 passed
- [x] `uv run ruff format && uv run ruff check && uv run ty check` —
clean on touched files
- [ ] After this PR merges + the upstream auto-sync PR lands + the next
mirror regen lands in `webdev/grid-api/`: open the follow-up to add
`CURRENCY_TO_ACCOUNT_TYPE[CNY]` and the
`gen_convert_to_external_account_info` case
- [ ] Live: once the model wiring follow-up lands and deploys, create a
CNY external account with `bankName: "AliPay"` (or `"WeChatPay"`) +
`phoneNumber: "+86..."`, quote against it, verify the upstream provider
accepts the rate

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
GitOrigin-RevId: c8d6406b0a5cb64d6d47da939fe737ed041e0f19
## Summary
- Upgrade `libphonenumber-js` to `1.13.6` for current phone-number
metadata.
- Add `libphonenumber-js` directly to `@lightsparkdev/site`.
- Align existing direct dependencies in `@lightsparkdev/ui` and
`@lightsparkdev/uma-bridge`.
- Add a narrow Yarn age-gate preapproval for `libphonenumber-js@1.13.6`,
since the repo quarantine gate blocks just-published packages by
default.

## Validation
- `yarn deps:check`
- `yarn install --immutable`
- pre-commit `yarn install` and `yarn format`

## Notes
- Existing Yarn peer dependency warnings remain unchanged.

GitOrigin-RevId: 1cba149d36f31d43f49330c78d434a89408effa8
## Summary
- Use Origin's existing measured label width helper to choose x-axis
label density
- Omit edge x-axis labels after thinning to avoid boundary crowding
- Preserve existing Origin chart styling and public APIs

## Checks
- yarn workspace @lightsparkdev/origin types
- yarn workspace @lightsparkdev/origin format
- git diff --check -- js/packages/origin/src/components/Chart

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 76b97cdca1ffd1d681593c61dc43aaaef530a61a
## Reason

[DES-58](https://lightspark.atlassian.net/browse/DES-58) exposed that
long PhoneInput country menus could be clipped because popup chrome and
list scrolling were owned by the same element. This moves scroll
ownership to the list for the ungrouped popup components that need
bounded long-list behavior, while keeping the popup responsible for
border, radius, shadow, and overflow clipping.

## Overview

- Keep PhoneInput, Combobox, and Autocomplete popup chrome clipped while
their listbox content owns max-height, padding, overscroll behavior, and
vertical scrolling.
- Add long-list stories and component tests for the affected components
so the scroll boundary stays explicit.
- Intentionally leave Select unchanged because grouped Select content
still relies on popup-level scrolling.

## Storybook preview

- Components/PhoneInput: LongCountryList:
https://dev.dev.sparkinfra.net/app/origin-storybook-pr-28492/?path=/story/components-phoneinput--long-country-list
- Components/Combobox: LongList:
https://dev.dev.sparkinfra.net/app/origin-storybook-pr-28492/?path=/story/components-combobox--long-list
- Components/Autocomplete: LongList:
https://dev.dev.sparkinfra.net/app/origin-storybook-pr-28492/?path=/story/components-autocomplete--long-list

## Test Plan

- yarn workspace @lightsparkdev/origin lint
- yarn workspace @lightsparkdev/origin types
- yarn workspace @lightsparkdev/origin test:ct
src/components/Autocomplete/Autocomplete.test.tsx
src/components/Combobox/Combobox.test.tsx
src/components/PhoneInput/PhoneInput.test.tsx

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: f64eb0f37b312e55c51114f9f5638085870d2d05
## Reason

[DES-36](https://lightspark.atlassian.net/browse/DES-36) needs
edit-existing forms to avoid showing a noisy clear affordance for saved
country/nationality values until the user is actually interacting with
the field. Origin now makes that active behavior the default so
consumers get the quieter edit-existing state without product-level
props.

## Overview

Adds `visibility` to `Combobox.Clear` with `active` and `always` modes
while preserving Base UI's ownership of clear behavior and clearable
state.

`active` is the new default: the clear affordance is hidden at rest and
appears while the field is focused or open. This intentionally changes
existing `<Combobox.Clear />` consumers from "always visible when
clearable" to "visible while active when clearable." Consumers that need
the previous/create-flow behavior can opt into `visibility="always"`.
Consumers that should not expose a clear affordance should omit
`<Combobox.Clear />`.

## QA Notes

Existing Combobox.Clear consumers should be checked with the new default
in mind:

- Edit-existing surfaces should no longer show the clear icon at rest
when they load with saved values.
- Focusing/opening the combobox should reveal the clear affordance when
Base UI reports a clearable value.
- Create/select flows that want the clear icon visible while a value
exists should use `visibility="always"`.
- Flows that should not offer clearing should omit `<Combobox.Clear />`.

## Storybook preview

- Components/Combobox / Default:
https://dev.dev.sparkinfra.net/app/origin-storybook-pr-28544/?path=/story/components-combobox--default
- Components/Combobox / Default Active Clear:
https://dev.dev.sparkinfra.net/app/origin-storybook-pr-28544/?path=/story/components-combobox--with-clear
- Components/Combobox / Always Clear:
https://dev.dev.sparkinfra.net/app/origin-storybook-pr-28544/?path=/story/components-combobox--always-clear

## Test Plan

- `mise exec -- node -v` -> `v20.19.6`
- `mise exec -- corepack yarn --version` -> `4.13.0`
- `mise exec -- corepack yarn workspace @lightsparkdev/origin playwright
test -c playwright-ct.config.ts
src/components/Combobox/Combobox.test.tsx`
- `mise exec -- corepack yarn workspace @lightsparkdev/origin types`
- `mise exec -- corepack yarn workspace @lightsparkdev/origin lint`
(passes with existing warnings in DatePicker/Sidebar)
- `mise exec -- corepack yarn workspace @lightsparkdev/origin format`

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: 9423d49750e801bb7e60f4a26b75457040dd226d
## Summary

Removes the unused `js/packages/static/images/lightspark-logo.svg`
asset. The `@lightsparkdev/static` workspace metadata is intentionally
kept so this cleanup does not require a `js/yarn.lock` update. Fully
removing the workspace package would require a JS dependency maintainer
to recreate the lockfile change because CI gates lockfile changes by
push actor.

- The SVG is referenced nowhere in code.
- The `static/images/...` imports elsewhere resolve to
`packages/ui/src/static/`, not this package.
- The old infra sync for `js/packages/static/` was removed after the
logo moved to `sparkcore/sparkcore/static/`.

## Why this PR should exist

The asset is orphaned and creates audit noise. Removing only the unused
file is low-risk and avoids unrelated dependency-lockfile churn.

## Testing

- `yarn install --immutable`
- `rg -n
'@lightsparkdev/static|packages/static|static/images/lightspark-logo|lightspark-logo.svg'
js -g '!yarn.lock'`

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
GitOrigin-RevId: f19bf82424df3ddb155dcb608bf22ad9f2f75e43
## Summary
- Recreates the DES-51 Nage phone input foundation on a fresh branch
from current `origin/main` to avoid the superseded PR's lockfile guard
history issue.
- Introduces E.164-only parsing helpers plus an Origin
`PhoneInput`/`Field` backed component using local round country flag
assets.
- Refactors the field around a single parsed draft model, keeps external
`value` as canonical E.164, and delays internal validation visibility
until blur unless a previously accepted value becomes invalid.
- Covers country-change behavior, controlled value sync, validation
notifications, repeated invalid edits, delayed internal error
visibility, and the `+447911123456` Crown Dependency case as `GG`, with
field tests using real Origin components and role-based queries.

## Scope
- Foundation files only under
`js/apps/private/site/src/uma-nage/components/phone-input/`.
- No package version bump files, KYB, payouts, settings, or other
product callsite migrations in this PR.
- Supersedes #28414.

## Test plan
- `mise exec -- node -v` from `js` — `v20.19.6`.
- `mise exec -- yarn workspace @lightsparkdev/site exec prettier --check
src/uma-nage/components/phone-input/NagePhoneInputField.tsx
src/uma-nage/components/phone-input/NagePhoneInputField.test.tsx` from
`js` — passed.
- `mise exec -- yarn workspace @lightsparkdev/site exec eslint
src/uma-nage/components/phone-input/NagePhoneInputField.tsx
src/uma-nage/components/phone-input/NagePhoneInputField.test.tsx` from
`js` — passed.
- `mise exec -- yarn workspace @lightsparkdev/site exec vitest run
src/uma-nage/components/phone-input/NagePhoneInputField.test.tsx
src/uma-nage/components/phone-input/phoneNumber.test.ts` from `js` — 30
tests passed.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: 5ada43a118245619dbf0fc52058399ed8e336980
## Summary
- Normalize post-login redirect targets against the active Vite/router
basename.
- Prevent UI preview links from redirecting to duplicated paths like
`/preview/pr-28544/preview/pr-28544/`.
- Preserve the existing full-page redirect behavior for `/ops` paths,
including preview ops paths.

## Testing
- `yarn workspace @lightsparkdev/site exec vitest run
src/hooks/loginRedirect.test.ts`
- `yarn workspace @lightsparkdev/site exec eslint
src/hooks/useLoginAndRedirect.tsx src/hooks/loginRedirect.ts
src/hooks/loginRedirect.test.ts`
- `yarn workspace @lightsparkdev/site exec prettier --check
src/hooks/useLoginAndRedirect.tsx src/hooks/loginRedirect.ts
src/hooks/loginRedirect.test.ts`

## Notes
- Full `@lightsparkdev/site` typecheck is blocked locally by existing
unresolved workspace package exports such as `@lightsparkdev/ui/router`.

GitOrigin-RevId: d0319351285faf26145bf5d920525218a47fbcad
## Summary

- Adds reusable Receive/Add Funds primitives for platform and customer
scopes: account selection, amount entry, setup rows, funds-target
helpers, funding-instruction rendering, and sandbox funding support.
- Requires active, eligible internal accounts before opening the new Add
Funds flow, revalidates stale selected accounts, and hardens sandbox
busy/close lifecycle behavior.
- Preserves existing read-only funding-instruction access where users
can view instructions but cannot start write actions.

## Gate / rollout notes

- Home and Customers Add Funds surfacing is gated by
`GRID_DASHBOARD_ADD_FUNDS_ENABLED`. It does not gate existing read-only
funding instructions.
- Payouts Add Funds V2 is independently gated by
`GRID_DASHBOARD_PAYOUTS_ADD_FUNDS_V2_ENABLED`, allowing Payouts to roll
out separately from Home/Customer Add Funds.
- Sandbox funding mutation is sandbox-only and remains behind the
explicit Add Funds flow; production funding instructions stay read-only
display unless a permitted action is available.
- Role/write permissions are authorization checks enforced inside the
flow/provider and are separate from rollout GKs.
- This PR does not add Transfer, Withdraw, Send, payout creation, or
external-account ownership semantics.

## Test plan / validation

- Focused Add Funds provider, customer/home/internal account drawer,
Sandbox Funding, receive/add-funds target, funds-target, money input,
account display, and funding-instruction tests passed.
- Type/lint validation passed for the touched Grid UI areas.
- Boundary checks were run against the previous stack slice to keep this
PR scoped to Receive/Add Funds behavior.

## How to review

This is large (+8,940 / −1,255 across 54 files vs main), but ~4,100 of
the inserted lines (~46%) are tests and test utilities. The biggest
non-test files are `receive-add-funds/ReceiveInstructions.tsx` (+596),
`payouts/panels/LegacyAddFundsPanel.tsx` (+565),
`receive-add-funds/SandboxFundingSection.tsx` (+374),
`payouts/AddFundsFlowProvider.tsx` (+344/−72), and
`receive-add-funds/useAddFundsDrawer.tsx` (+294).

### What changed since your last review

- **Funds-target unification** (`a3a605a813`, renamed in `37fac7e8d7`):
the new Add Funds flow now keys off a shared `FundsTarget` in
`utils/fundsTarget.ts` — formerly `OwnerScope` / `utils/ownerScope.ts` —
which replaced payouts' original funds-target type. Only
`LegacyFundsTarget` remains, confined to
`payouts/LegacyAddFundsFlowProvider.tsx`.
- **Currency eligibility guard** (`6965c94e48`):
`receive-add-funds/receiveAddFundsTarget.ts` (formerly
`receiveAddFundsScope.ts`) now owns a shared eligibility check that
excludes accounts with incomplete currency data, used by both Home and
customer scopes. It still imports `allowedFundingCurrencies` from
`payouts/utils/`; relocating that module and adding a cross-flow
eligibility rule-matrix test is tracked in
[DES-65](https://lightspark.atlassian.net/browse/DES-65).
- **Drawer machine unification** (`2ed9f32365`): new
`receive-add-funds/useAddFundsDrawer.tsx` (294 lines) holds the
open/busy/close state machine that Home and the customer drawer
previously each implemented. `Home.tsx` dropped ~195 lines;
`customers/CustomerAddFundsDrawer.tsx` is now a 54-line shim that binds
the customer funds target and renders the shared drawer. Payouts'
`AddFundsFlowProvider` is intentionally not folded in — it goes away
with the legacy retirement tracked in
[DES-57](https://lightspark.atlassian.net/browse/DES-57).

### Suggested reading order

1. Target/eligibility model: `utils/fundsTarget.ts`,
`receive-add-funds/receiveAddFundsTarget.ts`
2. The drawer machine: `receive-add-funds/useAddFundsDrawer.tsx`
3. Call sites: `home/Home.tsx`, `customers/CustomerAddFundsDrawer.tsx`
4. Payouts panel changes: `payouts/AddFundsFlowProvider.tsx`,
`payouts/panels/AddFundsPanel.tsx`
5. Tests

### Gating

All new surfacing is behind `GRID_DASHBOARD_ADD_FUNDS_ENABLED`
(Home/customer entry points, which themselves sit behind the
`GRID_DASHBOARD_HOME_ENABLED` /
`GRID_DASHBOARD_CUSTOMER_PROFILE_ENABLED` route gates) and
`GRID_DASHBOARD_PAYOUTS_ADD_FUNDS_V2_ENABLED` for the payouts path.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: d2255efa0b2346a8a7841856f0acf510dee9add0
Adds a **Run V3 flow** button to the EMAIL_OTP tab of the Grid Global Accounts example app that exercises the secure OTP login end-to-end:

1. `POST /auth/credentials/{id}/challenge` → `otpEncryptionTargetBundle`
2. HPKE-seal `{clientPublicKey, otpCodeAttempt}` locally via `@turnkey/crypto` (`hpkeEncrypt` + `bs58check`)
3. `POST /verify` `{encryptedOtpBundle}` → **202** + `verificationToken`
4. sign the token with the TEK → `Grid-Wallet-Signature`
5. `POST /verify` retry → **200** AuthSession (no `encryptedSessionSigningKey`)

Test harness for verifying the V3 flow against sandbox/prod. The OTP code never leaves the client in plaintext and the TEK private key stays client-side.

Verified: `tsc` clean, and a cross-language round-trip (JS `@turnkey/crypto` seal ↔ Python sandbox `open_encrypted_otp_bundle`) passes. Depends on the secure-OTP backend stack (base PR).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

GitOrigin-RevId: 8c6684bc6487de26e0df0b51e3e207a63631f12f
…nv-driven grid URL (#28470)

## What
Wire the grid-global-accounts example app to a **real WebAuthn ceremony** (Touch ID register/sign) with OTP-session-key caching, and make the dev backend URL **env-driven** (`GRID_URL` env var, defaulting to production) instead of a hardcoded dev host.

## Why
P4 example app, PR 0 in `40-example-app-design.md` §5/§6. The branch carried uncommitted real-WebAuthn-ceremony + OTP-session-caching work plus a config hazard: `vite.config.ts` had `PROD_GRID_URL` pointed at a dev host (`api.dev.dev.sparkinfra.net`), a local convenience that must not land in git. This PR commits the in-flight work and un-breaks the config so dev pointing is local-only (`GRID_URL=... yarn dev`) and never committed.

## Place in the stack
Base: `06-02-_js_gga_example_app_add_v3_secure_otp_e2e_flow` (the branch holding the uncommitted V3-OTP e2e work). First PR of the **P4 example-app** stack.

## Notable points
- `PROD_GRID_URL` now resolves from `process.env.GRID_URL ?? "https://api.lightspark.com"` — production by default, dev override never persisted.
- No behavior change beyond what was already live on the branch; manual test tool (no automated UI tests). Type gate: `yarn workspace ... build` (tsc + vite) + `yarn lint && yarn format`.

---
Part of the Turnkey login-family migration program. See `sparkcore/sparkcore/grid/docs/login-migration/00-program-plan.md`.

GitOrigin-RevId: 8c7d9891683ccf6739b7045c753e2c4b0456803e
…client/ui + flows modules (#28471)

## What
Split the ~1450-line `src/main.ts` into a small ES-module tree: `config.ts`, `turnkey.ts` (crypto), `webauthn.ts` (ceremonies), `api-client.ts`, `ui.ts`, and a `flows/` directory (`customer`, `email-otp`, `oauth`, `passkey`, `manage`, `money`, shared `context`). `main.ts` becomes a thin bootstrap.

## Why
P4 example app, PR 1 in `40-example-app-design.md` §1.5/§5. The single file mixed Turnkey crypto, HTTP, logging, DOM wiring, and every flow handler, and accreted into a tool only its author could drive. Carving it into modules lands first so later PRs touch small files; the `manage.ts` extraction also removes the 3× delete/export duplication (one shared panel instead of one per credential type).

## Place in the stack
Base: #28470 (real WebAuthn ceremony + env-driven URL). Second PR of the **P4 example-app** stack.

## Notable points
- **Pure refactor, no behavior change** — mechanical module move; `index.html` untouched. `tsc` + manual sandbox smoke prove equivalence.
- Manual test tool (no automated UI tests). Type gate: `build` + `lint`/`format`.

---
Part of the Turnkey login-family migration program. See `sparkcore/sparkcore/grid/docs/login-migration/00-program-plan.md`.

GitOrigin-RevId: fe1f4c2ca75d37fb07e37b9902241db970ce26d8
…add-passkey (#28472)

## What
Add `session.ts` — one object that holds the client keypair / encrypted bundle / TEK and exposes "do we have a signing key?" + a model badge — and render a **session status chip** (account id, credential id, session id, signing-key ready/none, model). Make the add-passkey button **disabled-with-tooltip** ("log in first") when there's no session.

## Why
P4 example app, PR 2 in `40-example-app-design.md` §1.1/§1.5/§5. The app had two invisible session models (Verify-bundle vs OTP-TEK) funneling through one `cachedSessionKeys` global, producing the "No client keypair — run a Verify first" trap: a stamp after an OTP login threw even though you *had* logged in. Centralizing session state into one object and surfacing it in a chip kills the trap by showing state instead of throwing on use, and replaces the runtime throw on add-passkey with a disabled button + tooltip.

## Place in the stack
Base: #28471 (module split). Third PR of the **P4 example-app** stack.

## Notable points
- Anticipates the login-family migration: `session.ts` makes both models explicit so the future flip (passkey/oauth converge on the OTP client-key-is-session model) is a one-line change per flow. `// MIGRATION:` breadcrumbs mark the exact switch points.
- Manual test tool; type gate: `build` + `lint`/`format`.

---
Part of the Turnkey login-family migration program. See `sparkcore/sparkcore/grid/docs/login-migration/00-program-plan.md`.

GitOrigin-RevId: d6ed5ebc66d4ba72991861f6d915998567854d9b
…8473)

## What
Prune dead / reject-only flows and fix stale PR-number comments: demote the EMAIL_OTP "Add second" reject demo and OAUTH rechallenge no-op to Advanced (or remove), fold the duplicate EMAIL_OTP rechallenge into guided login, and reword the stale `"PR #28427:"` / `"PR 4 flow:"` comments to describe behavior.

## Why
P4 example app, PR 3 in `40-example-app-design.md` §2/§5. These flows existed only to exercise reject paths or duplicated a guided step, and the PR-number comments anchor readers to specific (now-irrelevant) PRs rather than describing what the code does. Small, low-risk cleanup independent of the UI restructure.

## Place in the stack
Base: #28472 (session.ts + status chip). Fourth PR of the **P4 example-app** stack.

## Notable points
- Deletions/rewrites only; no new behavior. Reject/no-op demos are demoted, not silently lost.
- Manual test tool; type gate: `build` + `lint`/`format`.

---
Part of the Turnkey login-family migration program. See `sparkcore/sparkcore/grid/docs/login-migration/00-program-plan.md`.

GitOrigin-RevId: 5158156f8af7183ce150e63815e4d2dd563b377b
…_MAGIC (#28474)

## What
Make mode (sandbox vs production) the primary switch: move the scattered `value="sandbox-..."` HTML seeds into a single `SANDBOX_MAGIC` map (`mode.ts`/`config.ts`), drive field visibility + seeding from mode in JS, persist the chosen mode to `localStorage`, and hide ceremony (Touch ID) buttons in sandbox / hide magic fields in production.

## Why
P4 example app, PR 4 in `40-example-app-design.md` §1.4/§5. Every field was pre-seeded with `sandbox-*` placeholders indistinguishable from real values, and `SANDBOX_SIG` was silently injected into signed-retry headers — in production mode these are wrong with no UI signal which fields are magic. Sourcing seeds from one labeled constant map, injected only in sandbox mode, removes the "looks real" problem at the source; persisting mode stops a reload silently reverting to sandbox.

## Place in the stack
Base: #28473 (prune dead flows). Fifth PR of the **P4 example-app** stack.

## Notable points
- Field visibility/seeding is now JS-driven keyed on mode rather than hardcoded in `index.html`.
- Manual test tool; type gate: `build` + `lint`/`format`. Manual smoke: sandbox click-through + (where a device is available) one production Touch-ID round-trip.

---
Part of the Turnkey login-family migration program. See `sparkcore/sparkcore/grid/docs/login-migration/00-program-plan.md`.

GitOrigin-RevId: 7f45be16fefa89a0d169b06d53d7183d06d8004b
…oggle (#28475)

## What
The headline UX change: add opinionated **guided login/manage flows** (each button runs the whole chain, logs every leg, updates the session chip, and owns its intermediate state as flow-local variables) and collapse the raw single-step buttons under a per-section **"Advanced (manual steps)"** toggle.

## Why
P4 example app, PR 5 in `40-example-app-design.md` §1.2/§1.3/§5. The real sequences are multi-step and order-sensitive, enforced only by thrown strings, with intermediate state (target bundle, requestId, payloadToSign, challenge) leaking into visible inputs the user had to shuttle between steps. Guided buttons ("Log in (Email OTP/Passkey/OAuth)", "Add passkey", "Add OAuth", "Delete credential/session", "Export wallet") chain the steps and hold state internally; the manual fields stay available under `<details>` for debugging.

## Place in the stack
Base: #28474 (sandbox/production mode split). Sixth (final) PR of the **P4 example-app** stack — the largest DOM churn, landing last on the now-clean modules + session object.

## Notable points
- Guided flows own intermediate state as flow-local variables instead of DOM inputs; manual single-step buttons preserved (opt-in) for inspecting `requestId`/`payloadToSign` between legs.
- The login-family wiring (flip passkey/oauth to the no-bundle / client-key-is-session model) is **deferred** to a follow-up that depends on P1/P2/P3; `// MIGRATION:` breadcrumbs mark the exact call sites.
- Manual test tool; type gate: `build` + `lint`/`format`.

---
Part of the Turnkey login-family migration program. See `sparkcore/sparkcore/grid/docs/login-migration/00-program-plan.md`.

GitOrigin-RevId: 4de6cbf6b14b4ecea1925ec3ab3e6bc7f40083ed
Sets up shellcheck for webdev, modeled on the ops setup. Every tracked
shell script (anything with a `bash`/`sh`/`dash`/`ksh` shebang or a `#
shellcheck shell=` directive) is linted by `scripts/shellcheck.sh`, run
from a lefthook pre-commit job (staged files) and a CI job gated through
`ci.yaml` (full sweep). The version is pinned in `.mise.toml`.

Two things differ from a straight copy of ops:

- **Submodule-hardened discovery.** webdev has 8 submodules; the ops
one-awk-pass aborts the whole batch on the first gitlink (`read error
(Is a directory)`), which silently dropped more than half of webdev's
shell files. The discovery here filters non-regular files first. This is
latent in ops too and should be ported back (separate PR).
- **Cleaned up the existing backlog.** Turning this on surfaced ~86
findings across 32 files. Most fixes are mechanical (quoting, arithmetic
`$`); genuinely intentional word-splitting (`eval $(minikube
docker-env)`, `$AWS_PROFILE_FLAG`, helm `$*_EXTRA_ARGS`, `$CMD`) keeps a
per-line `disable` with a reason. One was a real bug: `if ! $(kubectl
... &>/dev/null)` in both `minikube-build-deploy*.sh` ran the empty
captured output as a command, so the "is it deployed?" guard never
actually fired - now it tests kubectl's exit code.

Testing: `./scripts/shellcheck.sh` passes (70 files, exit 0); `bash -n`
/ `sh -n` clean on every edited script; both workflow YAMLs parse.
GitOrigin-RevId: a363a9b904d62bea65ff359666d1904bc02c2b6a
…views + debug mode) (#28569)

## Summary
Redesigns the Grid Global Accounts example app from a single 1,127-line
`index.html` of "boxes + copied IDs" into a polished **React +
`@lightsparkdev/origin`** app with two persona views and a debug mode —
so it reads like what a Platform and a Customer actually see when
integrating Grid, with logs/IDs/raw payloads hidden behind a Debug
toggle.

## What changed
- **React + Origin**: vanilla TS → React 19 + Vite
(`@vitejs/plugin-react`) + `@lightsparkdev/origin` (styles +
components), mirroring `grid-kyc-demo`'s wiring.
- **Two persona views** behind a top switcher:
- **Platform** — config/connection panel, customers table,
create-customer, and "Act as" (selects a customer + switches to its
Customer view).
- **Customer** — real embedded-wallet flows for the active customer:
login (OTP/OAuth/Passkey), wallet home, fund, pay (quote+execute),
settings (manage credentials/sessions, export).
- **Debug mode** (off by default): a docked debug console
(request/response log), per-card raw-JSON expanders, and a context chip
that reveals customer/account/session IDs — all hidden until toggled.
- **Integration logic reused, not rewritten**:
`api-client`/`turnkey`/`webauthn`/`session` + flow orchestration were
decoupled from the old `ui.ts` via a `Reporter` interface; only the
rendering layer was rebuilt. The vanilla `ui.ts`/`main.ts` are deleted.

## Notes
- Design + plan committed in `REDESIGN.md` / `REDESIGN_PLAN.md`.
- Built/verified on Node ≥20.19; `build` (tsc+vite) + `vitest` green.
Every operation runs its real flow (nothing auto-signed); the persona
switch only scopes the active customer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: b77c9c3d76f67898f969272d37cf93a0396a91c9
GitOrigin-RevId: c2003a5dd586cf79a197b385b208d7db556d86c8
## Reason

New worktrees currently start with an empty project-local Yarn cache,
which makes initial installs slower even when the same dependencies were
already fetched elsewhere on the machine. Yarn supports a shared global
package cache, and this repo does not commit `.yarn/cache`, so enabling
it improves local worktree setup without changing the `node_modules`
linker.

## Overview

Set `enableGlobalCache: true` in `js/.yarnrc.yml`. This keeps
`nodeLinker: node-modules` and the default `nmMode: classic`; each
worktree still materializes its own `node_modules` tree.

## Test Plan

- `cd js && env -u YARN_ENABLE_GLOBAL_CACHE -u YARN_NM_MODE yarn config
get enableGlobalCache`
- `cd js && env -u YARN_ENABLE_GLOBAL_CACHE -u YARN_NM_MODE yarn config
get cacheFolder`
- `cd js && env -u YARN_ENABLE_GLOBAL_CACHE -u YARN_NM_MODE yarn config
get nmMode`
- `cd js && env -u YARN_ENABLE_GLOBAL_CACHE -u YARN_NM_MODE yarn install
--immutable --mode=skip-build`
- pre-commit hook: `yarn install` and `yarn format`

GitOrigin-RevId: cb8b20e3746092055e726c70937b4148ec6d2a26
## Summary

- Fixes DES-67 by removing default ARIA menu semantics from Origin
Sidebar navigation containers.
- Keeps explicit `role` forwarding on `Sidebar.Menu` as an escape hatch
for consumers that implement complete menu/menuitem semantics and
keyboard behavior.
- Clarifies in stories/tests that Sidebar navigation is a
layout/navigation grouping primitive; command menus should use Origin
Menu, and tree semantics should use `Sidebar.Tree`.

## Accessibility decision

`Sidebar.Menu` and expandable submenu containers no longer default to
`role=\"menu\"` because sidebar navigation items do not implement full
ARIA menu keyboard behavior. Consumers can still pass `role=\"menu\"`
explicitly when they own the corresponding `menuitem` semantics and
interaction model.

## Storybook preview

Origin Storybook:
https://dev.dev.sparkinfra.net/app/origin-storybook-pr-28838/

- `Components/Sidebar/Default`:
https://dev.dev.sparkinfra.net/app/origin-storybook-pr-28838/?path=/story/components-sidebar--default
- `Components/Sidebar/WithTreeItems`:
https://dev.dev.sparkinfra.net/app/origin-storybook-pr-28838/?path=/story/components-sidebar--with-tree-items
- `Components/Sidebar/AllItemVariants`:
https://dev.dev.sparkinfra.net/app/origin-storybook-pr-28838/?path=/story/components-sidebar--all-item-variants

## Verification

- `mise exec -- corepack yarn workspace @lightsparkdev/origin test:unit
src/components/Sidebar/Sidebar.unit.test.tsx`
- `mise exec -- corepack yarn workspace @lightsparkdev/origin exec
prettier --check src/components/Sidebar/parts.tsx
src/components/Sidebar/Sidebar.unit.test.tsx
src/components/Sidebar/Sidebar.stories.tsx`
- `mise exec -- corepack yarn workspace @lightsparkdev/origin exec
eslint src/components/Sidebar/parts.tsx
src/components/Sidebar/Sidebar.unit.test.tsx
src/components/Sidebar/Sidebar.stories.tsx`
- `mise exec -- corepack yarn workspace @lightsparkdev/origin types`

Jira: https://lightspark.atlassian.net/browse/DES-67

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: 76341209322fdce512d9c5204eeba89f2985e363
## Reason

Explain *why* this change is being made.

## Overview

For large or complex changes, describe what is being changed.

## Test Plan

Explain how you tested the change.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 5a3c39f6a0fca59dc3d4adcbcfc36cf5f3a0a516
## Reason

Explain *why* this change is being made.

## Overview

For large or complex changes, describe what is being changed.

## Test Plan

Explain how you tested the change.

GitOrigin-RevId: e2af7580732403291965caade8de2384ea5e2ec4
## Reason

The Grid launch stack introduced several crypto icon names that mixed
network and token concepts. This PR replaces those launch-stack
names/assets with an explicit `Network*` / `Token*` taxonomy in
`@lightsparkdev/ui/icons` so Nage can render receive networks and
token/currency icons consistently before the stack lands.

Repo-wide scans found no non-Nage consumers of the removed legacy names,
and those names were introduced in the same unlanded launch stack.

## Overview

- Replaces launch-stack crypto icon names/assets with explicit
`Network*` and `Token*` exports in `@lightsparkdev/ui/icons`.
- Updates Nage mappings for receive network icons and token/currency
icons.
- Adds `USDB` and `SAT` / `SATS` / `SATOSHI` -> BTC icon mapping.
- Removes obsolete Spark/Lightning PNG assets and routes those render
paths through UI SVGs.
- Makes no fiat flag changes and adds no environment-specific icon
variants.

## Test Plan

- `mise exec -- yarn workspace @lightsparkdev/ui build`
- `mise exec -- yarn workspace @lightsparkdev/ui package:checks`
- `mise exec -- yarn workspace @lightsparkdev/site types`
- `mise exec -- yarn workspace @lightsparkdev/ui-test-app test
SolanaIcons.test.tsx`
- `mise exec -- yarn workspace @lightsparkdev/site exec vitest run
src/uma-nage/components/cryptoCurrencyIcons.test.tsx
src/uma-nage/receive-add-funds/CryptoNetworkCard.test.tsx
src/uma-nage/home/Home.test.tsx`

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: c0a28072309d8c939a7d90127dac21f7768f51a5
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.