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 89 commits into
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 97933dc |
…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
… (#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
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.