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 147 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 3bd7681 |
…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
## Reason Webdev Turbo artifacts currently live only in GitHub Actions cache, so Bolt and local clients cannot restore them. The Yarn download cache also lets ARM64 publish an archive that leaves x64 redownloading optional packages on every run. ## Overview - Moves trusted Webdev CI publishing and same-repository PR restores to the private S3-backed Turbo cache, with local-only fallback when OIDC or S3 is unavailable. - Restricts publishing to trusted main workflows; forks, `pull_request_target`, merge queues, arbitrary-ref dev-cli workflows, and Spark checkouts do not use the shared namespace. - Adds indexed-Git workflow/action hashing and the exact Node runtime to Turbo global inputs. - Partitions the Yarn download cache by runner OS and architecture and pins Node 20 callers to 20.19.6. - Adds a SHA-512-pinned, read-only local portability canary; default local restores remain gated on a successful macOS artifact canary. Infrastructure dependency: lightsparkdev/ops#3412 ## Test Plan - Parsed every changed workflow/action YAML file with `yq`. - Passed ShellCheck and Bash syntax checks for both Turbo helper scripts. - Verified the access matrix for trusted main, same-repo PR, fork PR, `pull_request_target`, merge-group, hotfix, and foreign-repository contexts. - Verified a clean indexed-input hash and distinct Turbo hashes under Node 20 versus Node 24. - Verified Spark/dev-cli paths cannot initialize the Webdev shared cache. - `git diff --check`. GitOrigin-RevId: 33ecd14d37c2a15a267d80a1bcc12434db829011
## Summary - Adds 8 destination currencies from Daya Tech Amendment No. 1 — **BGN, CZK, HUF, NOK, PLN, RON, SEK, CHF** — to the `CurrencyUnit` enum, mirroring the merged ARS addition (PR #26191). - Unblocks the ops corridor-fees CSV importer (Fees Manager → Import Corridor Fees CSV): it rejected these as unknown currencies, and the import is all-or-nothing, so the full 14-row amendment CSV couldn't load. - All 8 are standard 2-decimal ISO fiats, so `is_fiat()` yields `decimal_precision=2` via the fallback — no `CurrencyConfig` registration needed. No importer change: `normalizeCurrency` validates against the generated `CurrencyUnit` enum. ## Changes Hand-edited source of truth: - `sparkcore/sparklib/money/currency_unit.py` — 8 entries added to `FiatCurrencyUnit` and the GraphQL `CurrencyUnit` enum - `js/packages/core/src/utils/currency.ts` — 8 entries at every ARS anchor (enum variants, conversion/format/type maps, `abbrCurrencyUnit`, 2-decimal list) Regenerated (not hand-edited): - `sparkcore/graphql_schemas/*.graphql` via `scripts/export-graphql.py` - `js/**/generated/graphql-schema.ts`, introspection JSONs, `ent-queries.tsx` via `yarn gql-codegen` Test: - `sparkcore/sparklib/money/__tests__/test_currency_unit.py` — asserts each new currency is a valid `CurrencyUnit`, is fiat (not crypto), and has `decimal_precision() == 2` ## Test plan - `uv run pytest sparklib/money/__tests__/` — 50 passed (incl. 8 new) - `uv run ruff check` + `ruff format --check` — clean - `yarn turbo run types format --filter=@lightsparkdev/core` — clean - Diff is 826 insertions, 0 deletions (purely additive, additive enum values keep existing SDL stable) ## Private [Plan](https://s3.console.aws.amazon.com/s3/object/lightspark-dev-bolt-logs?prefix=jobs/turbulent-sentinel-2/plan.md) (S3, internal only) ## Public Add BGN, CZK, HUF, NOK, PLN, RON, SEK, and CHF to the supported currency unit set. Fixes ENG-10916 --- 🤖 [turbulent-sentinel-2](https://zeus.dev.dev.sparkinfra.net/#/arc?id=turbulent-sentinel)[(#2)](https://zeus.dev.dev.sparkinfra.net/#/instance?id=turbulent-sentinel-2) | [Feedback](https://zeus.dev.dev.sparkinfra.net/feedback) Co-authored-by: Jacky <jacky@lightspark.com> GitOrigin-RevId: a7e05772bb28db90d568372434dcba1186b2ac8b
Socket flagged a critical DoS CVE (GHSA-23hp-3jrh-7fpw) in the transitive tar@7.5.15 dependency pulled in by node-gyp. Pin tar via a yarn resolution to 7.5.20, which also picks up several other tar security fixes released since 7.5.15. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> GitOrigin-RevId: 40e32b5927d15360beecb5ccf6cb60067b8483ef
…0270)
## Outcome
Origin now provides the reusable, provider-free `DataTable` and
`FilterBar` foundation consumed by the stacked Site adoption in #30271.
Generic table and filter behavior lives in Origin; product data,
queries, routes, copy, and policy remain in Site.
## Current API and behavior
- Provider-free `useCursorTablePagination` exposes the `{ pageSize,
cursor }` request consumed by any data provider
- `DataTable.Root pagination={{ controller, page, ariaLabel }}` owns
completed-page retention, loading attribution, scope/reset behavior,
skeleton context, the standard footer, and the count/range/page-size
view
- The Root `ariaLabel` labels both the table caption and pagination
landmark
- Null or undefined cell values render `-` by default; columns can still
render explicit content
- Origin owns generic SCREAMING_SNAKE_CASE humanization and a reusable
`Intl` date formatter while consumers retain domain labels and display
choices such as UTC
- A childless `FilterBar.Root` renders the standard Pills/Add/Clear
composition, while explicit children remain supported
- String filter state is singular and lossless; enum arrays use repeated
URL parameters with unambiguous hydration for legacy comma-delimited
links
- Public consumers use `createUrlBackedFiltersHook`; low-level URL codec
helpers remain internal implementation details
- Date filter editors synchronously seed each open from one value shared
by the draft and DatePicker default calendar month; semantic
committed-state changes resync the editor while equivalent rerenders
preserve in-progress drafts
- DatePicker preserves internal drafts across rerenders with
semantically equal committed ranges, resets on semantic changes, and
completes end-only calendar drafts without discarding the existing end
- Normalized cursor pages distinguish exact counts from lower-bound
counts
- The generic registration channel, row modifier opener, and existing
Table contracts remain reusable package APIs
- Low-level pagination factories and redundant composition helpers were
removed or internalized, preserving custom footers, explicit skeletons,
explicit FilterBar children, native `onClick` rows, and legacy non-draft
DatePicker behavior
- One consolidated changeset covers the package update; comments, docs,
and stories were cleaned up for the final surface
## Package boundary
Origin has no Apollo, GraphQL, router, Zustand, dayjs, Site-private, or
product-domain dependencies. Site continues to own provider mapping,
typed navigation, fields, routes, copy, and policy.
> “make sure to put any generic table helper functions/classes/types etc
into the origin package instead”
The current split follows that feedback: reusable table, filter,
pagination, date, enum, registration, and row-activation behavior lives
in Origin. Site keeps only product integration and policy.
## Validation
- Full Origin unit suite: **673 tests** passed
- Targeted unit checks also passed: **12 DatePicker tests**, **18
DataTable tests**, and **93 focused Origin tests**; these overlap the
full unit suite and are not additive
- Final DateFilter-focused validation: **91 focused unit tests** and
**31 focused browser tests** passed locally
- Full Origin component suite: **65 tests** passed
- Origin typecheck, lint, formatting, build, package, Storybook,
autodocs, publint, attw, diagnostics, and diff checks passed locally
- Previous-head GitHub CI completed and Faraday review found no new
findings. This description does not claim current-head remote green
This PR is based on `main`. Review and merge it before #30271.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: 67d09369d738567963ea350b1e298f69403c6e16
…er page (#30922) ## Summary In the ops inspector, fields that hold a spark transfer's spark id now render as links to the spark transfer page (`/ops/spark/transfer/[transfer_id]`), with a copy-to-clipboard button, instead of plain text. Matched field names: `transfer_spark_id` and `external_spark_id`, plus prefixed variants like `outbound_transfer_spark_id` / `inbound_transfer_external_spark_id`. The transfer page accepts either an SSP transfer id or an SO transfer (spark) id, so the spark id can be linked directly. `GridSparkTokenSettlementInfo.spark_transfer_id` is deliberately **not** matched — per its ent schema it holds a hex-encoded token transaction hash, not a transfer id (caught by review). ## Changes - `OpsInspectorDetailsComponents.tsx`: new shared `SparkTransferLink` component. - `OpsInspectorEntity.tsx` (generic ent renderer — the default inspector page for all types): string fields whose name matches the spark-transfer-id pattern render as `SparkTransferLink` + copy button. Applies recursively to object/array subfields. - Legacy pages that render these fields as plain text updated to use the link: `OpsInspectorDetailsSparkCoopExitRequest` (Swap Transfer Spark Id), `OpsInspectorDetailsSparkLeavesSwapRequest` (Outbound Transfer Spark Id). Fields covered today via the generic renderer: `ClaimStaticDeposit.transfer_spark_id`, `CoopExitRequest.transfer_spark_id`, `LightningSendRequest.transfer_spark_id`, `LeavesSwapRequest.outbound_transfer_spark_id`. `external_spark_id` is matched for parity with sparkcore ent field naming if/when exposed. ## Testing - `yarn types` (tsc), `yarn lint` (eslint), and `yarn prettier --check` pass on the changed files in `@lightsparkdev/ops`. - The ops app has no component test infrastructure (no `test` script); this is a rendering-only change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> GitOrigin-RevId: 0106862643b2152d450b8adeb6638a02fa7aec7e
## Reason The Striga grid harness had no UI to exercise the SCA surface, which is needed to drive and live-test the SCA work (factor enrollment, per-transaction authorize, login sessions, beneficiary trust) against the sandbox. ## Overview Add an "SCA (Striga EU)" section covering the full SCA surface: factor enroll/list/delete (TOTP end-to-end, passkey manual), per-transaction quote authorize + resend, session login, security-event recording, 2FA reset, and beneficiary trust/untrust. All actions target the active customer and log every request/response like the existing panels. TOTP confirm derives the real time-based code from the enroll-start secret (the sandbox rejects `123456` for TOTP), so enrollment is one click. ## Test Plan - `yarn workspace @lightsparkdev/striga-grid-harness build` builds clean. - Used live this session to drive the full end-to-end USDC-on-Solana withdrawal (SCA login → quote → execute → per-transaction authorize → `PROCESSING`) against the Striga sandbox. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LcXu9FTutwVvjLVyivTz5U GitOrigin-RevId: 91d8701ed6f3bf66176d24f99d5895cb2b2f3c33
…31191) ## Reason The harness could only run against localhost. `seed.py` provisions a platform in the local DB and writes `.grid-creds.json`; for dev or prod the platform already exists, so there was nothing to point the UI at short of hand-writing that file. ## Overview **A `0 · Connection` panel** takes the three values that cannot be discovered — Grid API base URL and the platform's `client_id` / `client_secret` — and lets the UI find the rest (customer, accounts) through the endpoints the other panels already call. **`POST /harness/creds`** verifies the credentials against the target before persisting, so a typo surfaces there rather than as a puzzling 401 on every later panel action. It *merges* into the existing file, so saving a customer does not clear the token and vice versa. **Secrets keep the property the seeded flow had:** they are POSTed once and the dev server injects the auth header on proxied requests. The `GET` still strips them and now reports `has_credentials`, so the panel can show connection state without the secret reaching the browser. **The API version prefix now comes from `base_url`** instead of being hardcoded at 13 sites (`/grid/rc` across `App.tsx` and `sca/scaApi.ts`). Environments do not all serve the same one — there is a `/grid/rc` blueprint and a dated `/grid/2025-10-13` — so a deployed target on the dated prefix would otherwise 404 on every call. ### Switching environments requires a dev-server restart Vite fixes the proxy target at startup. Mutating the options object `bypass` receives does **not** change it — verified by pointing `base_url` at a dead port, where requests still reached the original target. Left alone, that is a silent mis-target: a save pointing at prod would be served by localhost and look like it succeeded. So a guard middleware blocks `/grid/*` with a **409 naming both origins** whenever the configured target and the startup target disagree, and the panel says the same on a host change rather than leaving the 409 to be discovered. ## Test Plan Verified against the running harness (local Grid server + dev server): | Check | Result | |---|---| | `GET /harness/creds` | secrets absent, `has_credentials` reported | | `POST` with a wrong secret | 400 carrying the provider status; **file not modified** | | `POST` with valid credentials | 200, no secrets echoed back | | Proxied `GET /grid/rc/customers` | 200 — auth injected server-side | | Saving a customer | credentials and `base_url` preserved (merge, not replace) | | Target mismatch without restart | **409** with both origins named | | After restoring the target | 200 again | Also: `tsc --noEmit` clean, `build` succeeds, the 14 existing tests pass, and both pre-commit hooks (python + js) green. `.grid-creds.json` was restored to its original values afterward. Not covered: a real deployed target. I have no dev/prod platform to point at yet, so the untested path is whether a given environment's version prefix and `GRID_API` gatekeeper line up. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LcXu9FTutwVvjLVyivTz5U GitOrigin-RevId: 87438d31068118b4347ca50095a863dbb43b0b29
## Summary PR 1 of 2. Adds the public Origin foundation for content-driven DataTable sizing and stable loading behavior. - use native auto layout for content columns while preserving explicit structural widths - add opt-in bounded text with accessible full-value disclosure - provide cold/warm loading behavior with spacious skeleton columns and a reduced-motion-safe first reveal ## Boundary Origin owns generic table presentation, accessibility, and loading behavior. This PR contains no Nage, routing, GraphQL, or product query policy. ## Jira - [AT-5994 — Non-USD receiving currency is truncated](https://lightspark.atlassian.net/browse/AT-5994) ## Test plan - [x] Focused DataTable unit tests (32 passed) - [x] Focused DataTable and Table Chromium component tests (49 passed) - [x] Origin TypeScript checks - [x] Origin lint (no errors; 2 existing unrelated warnings) - [x] Origin Prettier check and staged diff check ## Stack - PR 1: this PR - PR 2: lightsparkdev/webdev#31304 --------- Co-authored-by: Cursor <cursoragent@cursor.com> GitOrigin-RevId: 5fd06b8d1538f5b400833a0ca7753ec285f44b55
## Summary - Close Add Filter after applying a multi-select or exclusive enum option so the interaction settles - Keep applied multi-select pill editors open for consecutive value toggles - Apply this as a generic Origin fix with no Site changes ## Test plan - [x] Confirmed both new add-menu close assertions fail on `origin/main` because the menu remains open - [x] Focused enum menu policy tests: 3 passed - [x] FilterBar component tests: 32 passed - [x] FilterBar unit tests: 79 passed - [x] Origin typecheck, lint, and format checks Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com> GitOrigin-RevId: bf979f3f18de786a4ffbf0664df27ecec1ed4878
GitOrigin-RevId: dd7d71c045ea8f2d99ea4fb0557ebabdd4c7ca38
GitOrigin-RevId: 5b92edaf4c8a9b174f10f675a400689ef3d00a94
GitOrigin-RevId: 77d04e5c342a06e9264f0c44ea43829a9c54b518
GitOrigin-RevId: c9ff540a5c3f8a3d7840e3612450109c35f44178
GitOrigin-RevId: f4cd33734364c0f050ed747850f8aee5e80249e1
GitOrigin-RevId: 0d1bfdbceb0109a15be57417952d9564598dea31
GitOrigin-RevId: 4f2c64b7131728a14398b2c7afed1dcb3149918e
GitOrigin-RevId: 72c333ebd880b8c75c8c0c5c9824a23e18202fa2
…er (#32248) GitOrigin-RevId: 9e2502d31e4912598b2e45545dd713b74c12b913
… session key (#31761) GitOrigin-RevId: c0b324487de205e1a3fa2b74088c8815ba55c69e
…uth login (#31975) GitOrigin-RevId: c65e343503194037b886d5114c9f320094c25aea
GitOrigin-RevId: c4ab833f15871e40af6440fa68fcc471bd4ac2ee
GitOrigin-RevId: 7154b2caa28d4c16264bb667f350fab527788360
GitOrigin-RevId: 46fd64af590f547b7af44832e98300ee6a60b05c
GitOrigin-RevId: a6366382b90e0ccd469b4d0dee7fc7e9df9aabac
GitOrigin-RevId: 125f1a6702f1743413bc4703046b1c123d124a2c
GitOrigin-RevId: 78f409151a7b6fd081c60bf298328f5da4187e3e
GitOrigin-RevId: 8e3a7785f86251f9cecc5aaeb62bafd2b292a7e2
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.