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 119 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 89fc07e |
…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
## Summary - add opt-in sortable headers to the shared UI table component - enable sorting on treasury flow columns using raw numeric/date values while preserving formatted cells - move treasury action buttons into a single top-right row and remove the flows caption ## Testing - yarn workspace @lightsparkdev/ui types - yarn workspace @lightsparkdev/ui build - ./node_modules/.bin/prettier --check packages/ui/src/components/Table/Table.tsx apps/private/ops/src/pages/ops/treasury/OpsTreasury.tsx - ../../node_modules/.bin/eslint src/components/Table/Table.tsx - ../../../node_modules/.bin/eslint src/pages/ops/treasury/OpsTreasury.tsx ## Notes - yarn workspace @lightsparkdev/ops types currently fails on existing CurrencyUnit/CurrencyUnitType mismatches outside this change path, including the pre-existing treasury amount cell. GitOrigin-RevId: 1246dad76c3c9aaf796f90f89b415ced3e5acd30
## Reason The ops DLQ page showed blank **Task Args** / **Task Kwargs** on the message detail. The snapshot rewrite (#27848) made the page read from persisted snapshots, which intentionally store only metadata (no args/kwargs) for data-at-rest safety — so the payload was never surfaced. Fixes the originating Slack report. ## Overview Keep snapshots metadata-only, but surface the live payload transiently: - **sparkcore** `OPS_create_dead_letter_queue_snapshot` now reads the live queue **with** payload (`include_task_payload=True`) and returns the live tasks (real `task_args`/`task_kwargs`) on a new `messages` field, plus `is_current`. The persisted snapshot row stays metadata-only (`to_snapshot_message()`). The recent-snapshot window now only dedups the snapshot **DB write** — every refresh still reads the live queue, so args/kwargs are always returned (no stale-cache path). - **ops UI** shows the live data as **\"Current · N messages\"** (args/kwargs populated on the detail page) as the primary view, and falls back to the latest persisted snapshot (metadata-only, **read-only**) only when the live queue is empty. - Detail navigation passes the message via **in-memory router state** (never the URL) so payloads don't land in history/logs; `actionable` state gates retry/delete so historical snapshot rows are read-only end-to-end. - Shared `Table` gains optional `onClickRow().state` pass-through (additive) to support the above — this restores the intended router-state navigation the detail page already reads. ### Reviewer notes (accepted, documented) - The live response is bounded by the existing `MAX_DLQ_MESSAGES_TO_READ = 200` cap and matches the pre-snapshot live page behavior; no separate payload byte-cap was added (a fail-the-refresh cap would be a worse failure mode). - cmd/ctrl-click opens the detail in a new tab without the in-memory payload and shows \"Message not found\" — deliberate, since the payload is kept out of the URL; normal click works. ## Test Plan - New `test_create_dead_letter_queue_snapshot.py` (GraphQL boundary, SQS mocked): a fresh read returns `messages` with real args/kwargs while the **persisted** snapshot stays metadata-only; empty queue returns no current messages / no snapshot; repeated refreshes within the window both return live payload and persist only one snapshot row. - `uv run ruff check`, `uv run ty check`, `uv run scripts/export-graphql.py`, `yarn gql-codegen`, eslint on changed FE files, `@lightsparkdev/ui` build (typechecks `Table.tsx`). - Rendered the page in a local harness (mocked GraphQL): live \"Current\" list with populated args/kwargs on the detail; read-only snapshot fallback (no checkboxes, no retry/delete); state-nav verified to keep the payload out of the URL. ## Private [Plan](https://s3.console.aws.amazon.com/s3/object/lightspark-dev-bolt-logs?prefix=jobs/primal-plasma/plan.md) (S3, internal only) ## Public Ops DLQ page now shows Celery task args/kwargs again, read live from the queue. --- 🤖 [primal-plasma](https://zeus.dev.dev.sparkinfra.net/#/arc?id=primal-plasma)[(#1)](https://zeus.dev.dev.sparkinfra.net/#/instance?id=primal-plasma) | [Feedback](https://zeus.dev.dev.sparkinfra.net/feedback) Original PR: lightsparkdev/webdev#29470 --------- Co-authored-by: Bolt Agent <bolt@lightspark.com> Co-authored-by: bsiaotickchong <bsiaotickchong@users.noreply.github.com> GitOrigin-RevId: 9b6276580be285ebaffea6121db2442291be78a0
## Reason Replace #28877 with equivalent dependency changes opened by @coreymartin, since the dependency-maintainer gate needs a JS dependency maintainer to author this bump. Per follow-up, this PR now targets `main` directly and includes the Origin Base UI utility cleanup plus the React 1.6 retarget in one PR. ## Overview - Removes Origin's vendored Base UI utility shim and direct Base UI utility imports. - Updates Origin's Base UI utility dependency path so `@base-ui/react` owns `@base-ui/utils@0.3.1` transitively. - Updates Origin's `@base-ui/react` dependency to `^1.6.0`. - Leaves `date-fns` and `@date-fns/tz` uninstalled; they remain optional peer metadata from Base UI only. ## Test Plan - [x] `cd js && yarn install` -- passed with existing peer warnings - [x] `git diff --check` - [x] `cd js && yarn workspace @lightsparkdev/origin types` - [x] `cd js && yarn workspace @lightsparkdev/origin build` - [x] `cd js && yarn workspace @lightsparkdev/origin test:unit src/components/Pagination/Pagination.unit.test.tsx src/components/Pager/Pager.unit.test.tsx src/components/LoadMore/LoadMore.unit.test.tsx src/components/LoadMore/useLoadMore.unit.test.ts` -- 48 tests passed - [x] `cd js && yarn why @base-ui/react && yarn why @base-ui/utils && yarn why date-fns && yarn why @date-fns/tz` -- confirmed Base UI React 1.6.0, transitive Base UI utils 0.3.1, and no installed date packages - [x] Commit hook: `yarn install` and JS `yarn format` passed --------- Co-authored-by: jaymantri <jay@lightspark.com> GitOrigin-RevId: a9ea280c60ab8cca9337fe2e1c669f1e30b85ed3
## Summary Polish pass on the Grid Billing reconciliation ops UI (from a Slack request with screenshots). Seven fixes: 1. **Titles** → `Grid Billing — Reconciliation` on both the detail view and the table view (were `— Transaction` / `— Transactions`). 2. **Payment id** — the external Grid transaction id at the top of the detail view now renders in neutral gray instead of link-blue (it's Grid's own id for the incoming payment, not an LSID and not a link). 3. **Transaction ID column** added to the table, linking each row's ent id to its ops inspector page (`OpsInspectorDetails`). The cell link `stopPropagation`s so it doesn't also trigger the row's detail-page navigation. 4. **`Attribute` button** — dropped the ellipsis (`Attribute…` → `Attribute`). 5. **Attribution search** now accepts **either a platform LSID/UUID** (resolved directly via `EntUmaaasPlatform.gen_nullable`) **or a case-insensitive name prefix**. Previously name-prefix only, so pasting an LSID returned nothing. Unparseable input falls back to the name search (returns empty rather than erroring). 6. **Modal width** — attribute / reject / match modal contents used `minWidth: 420` inside a 460px modal (420 + 56px inner padding overflowed); switched to `width: 100%` so they fit. 7. **`Pending review` badge** → yellow, in both the table and the detail view. Added an additive `warning` `BadgeKind` to the shared `@lightsparkdev/ui` Badge (light-yellow bg + amber text, using existing `warningBackground`/`warningText` tokens). `Rejected`/`Unattributed` stay red. ## Test plan - New pytest cases in `test_grid_billing_queries.py` for the attribution search: by LSID, by raw UUID, an LSID for a non-billing platform → empty, and a garbage string → empty. Full file: **18 passing**. - `uv run ty check` + `ruff check`/`format` clean; schema regenerated (`export-graphql.py` + `gql-codegen` — description-only diff). - ops app `tsc --noEmit` clean; eslint + prettier clean; `@lightsparkdev/ui` builds with the new badge kind. - Frontend rendered in a screenshot harness (the ops app has no unit-test runner) — all seven fixes verified visually; posted to the Slack thread. - `bolt-codex-review`: no P0/P1/P2 findings. ## Private [Plan](https://s3.console.aws.amazon.com/s3/object/lightspark-dev-bolt-logs?prefix=jobs/covert-vertex/plan.md) (S3, internal only) ## Public Ops-only Grid Billing reconciliation page polish. --- 🤖 [covert-vertex](https://zeus.dev.dev.sparkinfra.net/#/arc?id=covert-vertex)[(#1)](https://zeus.dev.dev.sparkinfra.net/#/instance?id=covert-vertex) | [Feedback](https://zeus.dev.dev.sparkinfra.net/feedback) Original PR: lightsparkdev/webdev#29672 --------- Co-authored-by: Bolt Agent <bolt@lightspark.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: kphurley7 <kphurley7@users.noreply.github.com> GitOrigin-RevId: 880f0b6ecc4a4306c5cd47bccaa11defe0d09e87
…abel restructure, Table separate borders (DES-77) (#29805) ## Reason The Grid dashboard table-system work (stacked PR to follow) needs several Origin primitives extended, and a Figma token-alignment sweep plus the DES-77 Table hairline fix were due regardless of that consumer. ## Overview - **ChipFilter**: values can now be `ReactNode`; numeric values are coerced into the dismiss label and an explicit empty `valueLabel` is honored as an a11y fallback; interactive-value styling is gated behind a new typed `ChipFilter.Trigger` compound part (promoted from a `data-chip-trigger` opt-in), with a dev warning when `valueLabel` is missing. - **CentralIcon** no longer emits an inline `currentcolor` style, so stylesheet rules can reach icon svgs directly. - Icon, border, and surface tokens aligned with the Figma spec across Menu, Combobox, Breadcrumb, Chart, DatePicker, NavigationMenu, and Shortcut (dead `data-icon` selectors dropped; DatePicker nav hover re-gated on hover capability). - **Field**: label gains a structured `Field.LabelSuffix` slot; description and error flow as block text instead of flex. - **Table** moves to `border-collapse: separate` so hairlines render at token width (DES-77). - `devWarnOnce` dedupes dev warnings to once per session; token-probe test helper shared via test-utils; changeset added covering the consumer-facing changes. ## Test Plan - 481 Origin unit + component tests passing - `tsc` clean - `stylelint` clean Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com> GitOrigin-RevId: c4ca500464d3557d299d857f57ddcdcc2d4c8456
## Summary Moves the remaining site Vitest wiring from #29808 into this maintainer-owned PR so the test/package changes can land together: - replaces the placeholder site test scripts with `vitest run` / `vitest` - adds `jsdom@^29.1.1` for the site and Origin workspaces - adds the site Vitest config, shared setup file, and React date picker stubs - applies the test fixes needed for the site suite to run under Vitest ## Validation - `cd js && yarn install --immutable` - `cd js && yarn run build --filter=@lightsparkdev/site` - `cd js && yarn workspace @lightsparkdev/site test` (93 files / 842 tests passed) - `cd js && yarn deps:check && yarn turbo run test gql-codegen lint format circular-deps package:checks --filter=@lightsparkdev/site --filter=@lightsparkdev/origin` GitOrigin-RevId: 923519379eff40c7d3a7ef6c81fe315a1a173d2b
…846)
## What was broken
Submitting a form that contains `PhoneInput` and reading
`FormData.get("phoneNumber")` returned the selected country code (e.g.
`"US"`) instead of the phone number the user typed.
Why: Base UI's Select renders a hidden `<input>` purely for form
serialization. When a consumer wraps the whole phone input in one
`Field.Root` — the natural composition for a labeled field — the phone
`<Input name="phoneNumber">` registers its name on the field, and the
Select's hidden input inherits that same name. Because the hidden input
sits earlier in the DOM, `FormData.get("phoneNumber")` returns its value
— the country — instead of the phone number.
## The fix
`CountrySelect` now establishes its own field boundary: a nested
`Field.Root` styled `display: contents`, so it adds no visual box. A
nested `Field.Root` is Base UI's only mechanism to stop field-context
inheritance — there is no opt-out prop. Inside the boundary the hidden
input no longer sees the outer field's name: it is nameless by default
(excluded from serialization), and passing `name` to `CountrySelect`
serializes the country under its own key.
## State propagation
The boundary also blocks the outer field's disabled/invalid context, so
`CountrySelect` re-threads PhoneInput's own `disabled`/`invalid` props
(the documented way to drive PhoneInput state) into the nested field —
the country trigger keeps its disabled behavior and `aria-invalid`.
**Intentional behavior change:** state set only on the outer
`Field.Root` and not mirrored on `PhoneInput.Root` no longer reaches the
country trigger; Base UI has no public API to read across a field
boundary, and Origin's styling never depended on it.
## Consumer test change
`NagePhoneInputField.test.tsx` (site app) previously asserted *two*
controls named `phoneNumber` and told them apart with an `aria-hidden`
filter — a workaround that encoded the buggy behavior. Post-fix those
assertions would fail, so this PR reverts them to the strict form:
exactly one control named `phoneNumber`, and the hidden serialization
input carries no name.
## Validation
- Origin unit suite: 486 tests / 13 files passed (includes new
PhoneInput serialization and state-propagation tests).
- PhoneInput Playwright CT suite: 17/17 passed (invalid-state test now
also asserts the trigger is marked invalid).
- Site vitest suite: 849 tests / 93 files passed, including the restored
strict test.
- `tsc`, ESLint + stylelint, and Prettier clean in both workspaces.
## Context
- Raised by @kphurley7 on #29808
([thread](lightsparkdev/webdev#29808 (comment))).
- Base UI supports one control per `Field.Root` (mui/base-ui#3928).
- DES-51 tracks the consumer workaround this PR removes.
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: 88e29422990ece6b3dd400ffb6df19dafe138205
## Reason The hermetic UI workflow now runs Playwright, but the repo still carried legacy Cypress tests, scripts, dependencies, and CI path filters. Keeping both made the active UI test path harder to reason about and kept Cypress in the JS dependency graph. ## Overview - Remove the private site Cypress test tree, config, runner script, and package dependencies. - Remove dead Cypress scripts/dependencies from `uma-bridge`. - Map `@lightsparkdev/site` `test:ui` to Playwright and rename `pw-*` aliases to `test:ui:*` scripts. - Update `ui-test-hermetic` to run `yarn test:ui`. - Replace the old `window.Cypress` automation flag with `window.lightsparkTestAutomation` for Playwright fixtures. ## Test Plan - `yarn install --immutable` - `yarn run --top-level turbo run build:deps --filter=@lightsparkdev/site` - `yarn workspace @lightsparkdev/site lint` - `yarn workspace @lightsparkdev/site types` - `yarn workspace @lightsparkdev/site test:ui --list` - `yarn why cypress` returns no resolved package GitOrigin-RevId: 5b5097e9da696b30131915f49203c02b8771ad78
## Reason Origin needs a one-time password input for verification-code flows. Base UI 1.6.0 (already Origin's pinned version) ships an `otp-field` primitive; this wraps it in Origin styling and conventions rather than hand-rolling slot management, paste distribution, or keyboard navigation. Figma spec: [Origin design system — OTP field](https://www.figma.com/design/3JvbUyTqbbPL8cCpwSX0j4/Origin-design-system?node-id=7171-2883). Design and code are token/geometry parity-verified. ## Overview - New `OTPField` compound component (`Root` / `Input` / `Separator`) wrapping `@base-ui/react/otp-field`, exported from the package root. - `Root` defaults to 6 numeric slots with `length` configurable, supports controlled/uncontrolled `value`, `onValueChange`, `onValueComplete`, `validationType`, `disabled`, `readOnly`, `mask`, and `autoSubmit` (all Base UI Root props pass through). - A childless `Root` auto-renders its slots with per-slot `aria-label`s ("Character N of M"); explicit children support grouped layouts (`123-456`) with `Separator`. - Field integration follows the house pattern: compose inside `Field.Root` with `Field.Label` / `Field.Error`; the label associates with the first slot and invalid state cascades to every slot via Base UI's Field context. - Styling uses the shared `input` / `input-focus` / `input-critical` mixins and tokens only; 8px cell gap (`--spacing-xs`) so critical halos on adjacent invalid cells don't overlap. - Storybook stories follow the Input/Select convention (bare component stories + one `WithField` story carrying the Field/invalid composition), plus a minor changeset and SKILL.md entry. ### Thermos review fixes applied - Deleted the `aria-label` render-shim in `Input`; the auto-render branch passes labels as plain props and Base UI natively handles the `aria-labelledby` swap and first-slot guard. This also stops the shim from silently stripping a consumer's custom `aria-labelledby`. - Deduplicated CT vs unit tests: static attribute contracts live only in the vitest suite; Playwright CT keeps browser-dependent behavior (typing/focus, paste, keyboard nav, accessible-name resolution, invalid styling, axe, computed-style checks). - `Separator` now merges function-form `className` like its siblings and types props via the Base UI `Separator.Props` idiom. - `OTPField` exports moved to their alphabetical slot in `src/index.ts`. - `children ?? autoSlots` so `children={null}` renders the default slots. ### Known follow-ups (deliberately out of scope) - Shared `mergeClassName` helper for the function-form `className` merge pattern (touches Field/Combobox too). - Input-chrome SCSS mixin for the border/focus/critical block (pre-existing package-wide duplication). - Dev warning on grouped child-count vs `length` mismatch. - Bare-usage first-slot labelling (`WithField` is the documented composition). ## Test Plan - [x] `yarn workspace @lightsparkdev/origin test:unit` — 495 passed (17 OTPField unit tests) - [x] `yarn workspace @lightsparkdev/origin types` - [x] `yarn workspace @lightsparkdev/origin lint` — eslint + stylelint clean (2 pre-existing warnings in DatePicker/Sidebar untouched) - [x] OTPField Playwright CT suite — 14 passed (typing, paste, keyboard nav, a11y, invalid styling, axe, gap/size checks) - [x] Full origin CT suite run earlier in the branch — 953 passed; 7 failures reproduced identically on clean origin/main (pre-existing) - [x] Visual QA in Storybook against the Figma spec Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com> GitOrigin-RevId: 5ddc7fdf97bfde48150f69e1f49094c21f0664ab
## Reason
Turbo's local-only cache makes the JS CI jobs repeat the same work
across runners. We want a remote Turbo cache for CI, but backed by
GitHub Actions cache rather than Vercel-hosted cache infrastructure.
## Overview
- Upgrade `turbo` to `^2.9.9`.
- Add a shared `setup-turbo-remote-cache` action using
`rharkor/caching-for-turbo` with the GitHub cache provider.
- Invoke the shared cache setup from the common Yarn install action and
the dev-cli Spark JS build path.
- Expand workflow path filters so changes to the shared cache action
exercise the affected CI flows.
## Test Plan
- `yarn install --immutable --mode=skip-build`
- `ruby -e 'require "yaml"; ARGV.each { |p| YAML.load_file(p); puts "OK
#{p}" }' .github/actions/setup-turbo-remote-cache/action.yml
.github/actions/yarn-nm-install/action.yml
.github/actions/setup-dev-cli/action.yml .github/workflows/ci.yaml
.github/workflows/deploy-origin-storybook.yaml
.github/workflows/js-lightspark-sdk-hermetic.yaml
.github/workflows/pr-ui-preview-deploy.yaml
.github/workflows/ssp-spark-hermetic-dev-cli.yaml`
- `git diff --check`
- `yarn turbo run build --dry=json`
- Pre-commit hook: `yarn install`, `yarn format`
GitOrigin-RevId: c550e188d273df57f8cb1d0dcb4f6995537dcb34
## Reason Origin's `Loader` only ships the 3-dot pulse, which is sized and weighted for inline/button contexts. The uma-nage transfer status badge (stacked follow-up PR) needs a compact circular spinner that reads as "in progress" at small sizes. The Figma Origin Loader component set now has a `Style=Ring` option; this implements it as a variant rather than a new component. Figma: [Loader component set (`Style=Ring`)](https://www.figma.com/design/3JvbUyTqbbPL8cCpwSX0j4/Origin-design-system?node-id=7217-14) ## Overview - Adds `variant?: "dots" | "ring"` to `Loader` (default `"dots"`, so Button loading and all existing consumers are unchanged). `LoaderVariant` is exported alongside `LoaderProps`. - The ring matches the Figma `Style=Ring` spec: circular track stroked with `--border-secondary`, rotating quarter-arc indicator on `--stroke-primary`, 2px stroke (`--stroke-lg`), round caps, 24x24 reference geometry. - Optional `size` prop (ring only) scales the whole svg — strokes scale proportionally like `CentralIcon`, so a 12px ring renders a ~1px stroke. Rotation is `0.5s linear infinite` and respects `prefers-reduced-motion`. - Accessibility contract preserved: `label` → `role="status"` / `aria-label` + visually hidden text; the svg is `aria-hidden`. - Tests split per the OTPField convention: static attribute contracts in `Loader.unit.test.tsx` (vitest), browser-dependent behavior in a new Playwright CT suite (`Loader.test.tsx` + `Loader.test-stories.tsx`) covering rendered geometry, computed token strokes, rotation animation, reduced motion, and axe scans for both variants. - `Ring` / `RingSmall` Storybook stories plus a patch changeset. Intended consumer: the uma-nage transfer status badge (stacked follow-up PR — not touched here). ## Test Plan - [x] `yarn workspace @lightsparkdev/origin test:unit` — 515 passed (9 Loader unit tests) - [x] Loader Playwright CT suite — 8 passed (geometry at 24px/12px, token strokes, rotation, reduced motion, axe on both variants, accessible name) - [x] `yarn workspace @lightsparkdev/origin types` — clean - [x] `yarn workspace @lightsparkdev/origin lint` — eslint + stylelint, no new findings (2 pre-existing warnings in DatePicker/Sidebar) - [x] `yarn workspace @lightsparkdev/origin format` — clean - [x] Visual check of `Ring` / `RingSmall` stories in Storybook — QA'd locally on port 6006 Made with [Cursor](https://cursor.com) GitOrigin-RevId: d5b072f466611641686b34dc1a1e84c9af3a8323
## Reason Locked-country phone contexts (e.g. KES mobile money flows) need a locked, non-interactive country cap on the phone input — the country is fixed by the product, so a country selector is wrong UX. This adds `PhoneInput.LockedCountry`, a static "leading cap" part (flag + dial code, no chevron, no Select machinery) matching the Figma `_Trigger` `Locked` variant spec. Origin stays data-agnostic: consumers supply the flag and dial code. Product-side country-selection logic (which country to lock and when) lands separately in Nage. > Note: this part was originally named `PinnedCountry`; it was renamed to `LockedCountry` to align terminology with the Figma spec's `Locked` axis and Nage's `countrySelection` `mode: "locked"`. The branch name keeps the old `pinned` wording. ## Overview - `PhoneInput.LockedCountry`: static, non-focusable `div` cap rendered in place of the country Select trigger — no combobox semantics, skipped in tab order. - Locked-specific padding/border-separator styles matching the Figma spec; root keeps the 36px height. - Story, Playwright CT tests (axe, focus order, layout/computed styles), Vitest unit tests (DOM shape/semantics), and a patch changeset. ## Storybook preview - PhoneInput / Locked: https://dev.dev.sparkinfra.net/app/origin-storybook-pr-30094/?path=/story/components-phoneinput--locked ## Test Plan - `yarn test:unit` in `js/packages/origin` — 519/519 passing (includes new LockedCountry unit tests). - `yarn test:ct PhoneInput` — 21/21 passing (axe on locked variant, no-combobox/Tab focus-order check, locked padding + border separator, 36px height). - `yarn types` and `yarn lint` clean. Made with [Cursor](https://cursor.com) GitOrigin-RevId: bc565498475fdf16c6958bc3c94654861ff74f64
## Why
The `copy-public-js` job spends most of its time initializing Copybara
even when a push contains no files that can sync to
`lightsparkdev/js-sdk`. Recent runs show this is usually a no-op path,
so the workflow should cheaply skip those runs while keeping the skip
criteria tied to the Copybara config.
## Changes
- Download the pinned official Copybara release from GitHub instead of
the Lightspark CDN, with SHA256 verification.
- Use the latest preinstalled Java available on the GitHub runner,
preferring Java 25 with Java 21 fallback.
- Add a preflight helper that parses `origin_files` from
`js/copy.bara.sky` and checks the pushed git range for eligible file
changes.
- Run Copybara with shallow origin/destination fetches first, then retry
the full fetch path if the shallow run cannot complete.
## Validation
- `ruby -e 'require "yaml"; YAML.load_file(".github/workflows/js.yaml");
puts "yaml ok"'`
- Embedded workflow `run:` scripts passed `bash -n`.
- `python3 -m py_compile scripts/gha/copybara_eligible_changes.py`
- `git diff --check -- .github/workflows/js.yaml js/copy.bara.sky
scripts/gha/copybara_eligible_changes.py`
- `java -jar /tmp/copybara-latest.jar validate --validate-starlark
STRICT js/copy.bara.sky`
- Negative helper check on the current private-only range returned no
eligible files.
- Positive helper check on recent public JS commit `d5b072f...` returned
the public `js/packages/origin/**` files and ignored the excluded
changeset.
- Pre-commit hook passed, including JS install and format.
Known existing caveat: `actionlint` still reports pre-existing issues in
`.github/workflows/js.yaml` unrelated to this Copybara job, including
merge-group path filtering, custom runner labels, and older shellcheck
findings outside the changed block.
GitOrigin-RevId: ff830574cefab41d26f8e951156f45e4bd8a1999
## Summary - generate `tn` typename constants from each consuming app schema instead of sharing runtime typenames from `@lightsparkdev/gql` - move the node/type guard helpers into site and ops so they reference app-local generated typenames - keep app codegen cache-safe by declaring `@lightsparkdev/gql` as the generator package dependency and hashing the shared `generate-typenames.mjs` script in each app `gql-codegen` task - move the node password modal into site and remove the remaining private-ui dependency on site-only gql/store code - remove unused private gql outputs/fragments/schema JSONs and trim `@lightsparkdev/gql` to introspection/tooling scripts - keep generated ops Ent query literals stable by printing generated GraphQL documents before writing them ## Validation - `yarn install --immutable` - `yarn turbo run gql-codegen --filter=@lightsparkdev/site --filter=@lightsparkdev/ops --filter=@lightsparkdev/uma-bridge --filter=@lightsparkdev/private-ui` - `yarn turbo run gql-codegen --filter=@lightsparkdev/site --filter=@lightsparkdev/ops --filter=@lightsparkdev/uma-bridge` - `yarn run --top-level turbo run build:deps --filter=@lightsparkdev/site --filter=@lightsparkdev/ops --filter=@lightsparkdev/uma-bridge` - `yarn turbo run types --filter=@lightsparkdev/site --filter=@lightsparkdev/ops --filter=@lightsparkdev/uma-bridge --filter=@lightsparkdev/private-ui` - `yarn turbo run lint --filter=@lightsparkdev/site --filter=@lightsparkdev/ops --filter=@lightsparkdev/uma-bridge --filter=@lightsparkdev/private-ui` - `node packages/private/gql/scripts/generate-typenames.mjs --schema --output /tmp/typenames.tsx` rejects the missing `--schema` value as expected - `yarn workspace @lightsparkdev/site test src/uma-nage/billing/GridBilling.test.tsx src/uma-nage/billing/GridBillingInvoiceDetails.test.tsx src/uma-nage/components/CommandCenter.test.tsx src/uma-nage/customers/customerDetailsPresentation.test.ts` - `yarn playwright test playwright/tests/10-payments.spec.ts --list` - `git diff --check && git diff --cached --check` ## Notes - Full local Playwright was not run because the local suite expects the full backend test environment; the payment spec list check passed, and Corey manually verified the node password modal on mainnet after the site-local move. GitOrigin-RevId: e90fbadc215286b09f579b5de0c01cf57ecc23ee
## Summary Adds a `label` slot to Origin's `Item` component, rendered inline after the title in the title row (per the Origin Figma spec, node 7269-385 — intended for badges like "Coming soon" or "Beta"). - New `label?: React.ReactNode` prop on `Item`, rendered in the title row after the title text - Styles in `Item.module.scss` for inline alignment and spacing - Storybook story, Playwright CT test stories + tests, and a patch changeset included ## Stacking PR #30043 stacks on this — its badge commit consumes the new `label` prop. That branch will be rebased to drop the duplicated origin commit once this merges. ## Verification - `tsc --noEmit` clean in `js/packages/origin` - Item Playwright CT suite: 15/15 passing Made with [Cursor](https://cursor.com) Co-authored-by: Cursor <cursoragent@cursor.com> GitOrigin-RevId: 32444a221e2d4dea5a23924b6fc799e8c520b82d
## Reason Real public-JS Copybara runs still spend about 100 seconds initializing the webdev origin, even after non-public changes were taught to skip the job. The pinned Copybara release supports partial origin fetches, which directly avoid hydrating unrelated monorepo blobs. ## Overview Enable `partial_fetch` only for the webdev-to-js-sdk origin. The reverse workflow still fetches the whole repository because its `origin_files` glob is `**`, which Copybara does not support with partial fetches. ## Test Plan - Pinned Copybara v20260706 strict config validation passes. - The Copybara eligibility helper still recognizes `js/copy.bara.sky`. - Fresh-output-root no-op dry run: 55.44s baseline, 15.67s with partial fetch (~72% faster), with both returning Copybara's expected no-change status. - Remote-branch eligible-change dry run completed successfully in 24.74s, transformed the config commit, created a local destination commit, and skipped the remote push under `--dry-run`. GitOrigin-RevId: 398a2855b3856e7c50944e82e6529d841c2e961c
Pagination was passing `useRender` a props array force-cast to `Record<string, unknown>`, bypassing Base UI's public contract. This applies the same fix as Breadcrumb in #30178: each call now passes a single merged object (className joined via clsx, defaults spread before element props), with no casts and no behavior change. Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com> GitOrigin-RevId: c504ae51ccfa777924b930a3c89bc6a8cf38801a
## Summary
- Adds a `render` prop to `Breadcrumb.Link`, implemented with Base UI's
`useRender` (`@base-ui/react/use-render`), so consumers can pass
router-aware link components — e.g. `render={<LinkBase to={...} />}` —
instead of hand-rolling `useHref` + click interception.
- Default tag stays `a`; props merge per Base UI semantics (classNames
join, event handlers compose, refs merge). `href` remains required when
no `render` element is provided.
- Adds a Storybook story, a Playwright CT case, and Vitest unit tests
covering render replacement, className merging, event-handler
composition, and ref merging, plus a patch changeset.
The NAGE settings branch (`ajay/nage-settings-origin`) will consume this
for the settings breadcrumb.
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
GitOrigin-RevId: d4d57aa0b0bfb3d93aa307f4d68197f0503b97a0
## Summary - invoke custom error `toJSON` methods with their owning object as the receiver - add regression coverage for serializers that read instance state - add a patch changeset for `@lightsparkdev/core` ## Why Marker SDK errors expose a receiver-dependent `toJSON` method. Calling it unbound masked a handled Marker configuration error with an uncaught TypeError, causing Nage Playwright setup to fail and blocking 68 dependent tests. ## Validation - `yarn workspace @lightsparkdev/core test` — 70 passed - core format, lint, and build passed - filtered site production build passed - full local UI hermetic rerun: Nage setup passed and previously blocked Nage suites ran; 110 passed, 3 failed, 3 skipped, 10 serial dependents did not run - remaining UI failures are unrelated: Stripe redirect landed on dashboard, RSK API-token delete control timeout, and Nage command-center shortcut timeout - `bolt-codex-review` — no P0/P1/P2 findings ## Local hook note The repository-wide pre-commit format hook was bypassed after package checks passed because unrelated ops GraphQL generation fails at `ent-queries.tsx:35456` with a pre-existing syntax error. Requested by @coreymartin Slack thread: https://lightsparkgroup.slack.com/archives/D0BF4GR03PA/p1783969354352799 --- 🤖 [magnetic-summit](https://zeus.dev.dev.sparkinfra.net/#/arc?id=magnetic-summit)[(#1)](https://zeus.dev.dev.sparkinfra.net/#/instance?id=magnetic-summit) | [Feedback](https://zeus.dev.dev.sparkinfra.net/feedback) Original PR: lightsparkdev/webdev#30279 GitOrigin-RevId: c10c39f5afbc9c24319982fa7c9c25f55ef2f250
## Reason PR previews use a unique `VITE_BASENAME` for each pull request. Declaring frontend environment variables on the root `build` task, together with Turbo's Vite framework inference, made that PR-specific value part of shared package and GraphQL codegen hashes. This prevented otherwise identical work from being restored from the GitHub-backed Turbo cache across previews. ## Overview - Scope output-affecting frontend environment variables to the app builds that consume them. - Disable framework inference for the preview Turbo command now that its environment inputs are explicit. - Keep the preview path in each final app build hash while allowing shared dependencies and codegen tasks to retain stable hashes. ## Test Plan - `yarn install --immutable` - Prettier check for all changed Turbo JSON and workflow files - `git diff --check` - Focused `site`, `ops`, and `uma-bridge` Turbo build: 14/14 tasks succeeded - Identical preview-path rerun: 14/14 tasks cached in 171ms - Different preview path: 11/14 tasks cached; only the three final app builds reran - Pre-commit JS formatting hook: 28/28 tasks succeeded GitOrigin-RevId: 20466a07062183e1b8f0dbb74afd5ffcc4bf1b21
## Reason We need a way to exercise the real Grid REST API against a `StrigaGridSwitch` platform locally — driving quotes, deposits, withdrawals, and onboarding — to validate the switch end-to-end during development. There was no local harness for this. ## Overview Adds a local, **sandbox-only** e2e harness for the Striga Grid switch under `sparkcore/scripts/striga_harness/`, plus its web UI: - **`seed.py`** — seeds a local SQLite DB into a fully-configured Striga Grid platform: grid switch, `EntGridStrigaPlatformConfig` (Striga creds), Grid API token, a customer linked to a funded sandbox user, and EUR/BTC/USDC internal accounts. Writes `.grid-creds.json`. - **`proxy.py`** — stdlib HTTP proxy on `:8787`. Serves the built UI, injects HTTP Basic auth from `.grid-creds.json`, forwards `/grid/*` to the local server (CORS bypass), and serves `/harness/creds`. - **`ngrok.py`** — prints the Striga webhook URL for the platform. - **UI** (`js/apps/examples/striga-grid-harness/`) — a Vite + React app built on the Origin design system (Card/Field/Input/Textarea/Button/Select/Switch/Table/Badge/Alert). List/create customers, poll internal-account balances, create + execute quotes, sandbox-fund deposits, transfer-out withdrawals, onboarding (KYC link, email/phone verification), and a full request/response log. Builds to `dist/`, which `proxy.py` serves. Dev-only tooling; not wired into CI. `harness.config.json` and `.grid-creds.json` are gitignored (sandbox secrets / per-run creds). ## Test Plan Seeded a local platform against the Striga sandbox and drove flows through the UI: - Listed + selected the seeded customer; internal-account balances render. - **Sandbox fund** → EUR balance moved 10000 → 15000. - **Live EUR→BTC quote** (real sandbox rate ~54,612 EUR/BTC) → **execute** → `COMPLETED`; EUR 15000→10000, BTC 0→90,226 sats. - **Create customer** creates the Striga user (phone + address accepted); the immediately-following wallet fetch surfaces the known pre-KYC wallet-provisioning gap tracked in AT-5682 (out of scope here). Lefthook pre-commit (ruff / ty / pylint + `yarn format`) passes; `yarn workspace @lightsparkdev/striga-grid-harness build` (tsc + vite) passes. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> GitOrigin-RevId: d492f2b53afd2ed9ed5f60aba62a0b47d8a50384
## Reason Bolt exposes a frontend at a URL such as: `https://zeus.dev.dev.sparkinfra.net/agent/<job-id>:<port>/` The app is therefore mounted under a public path prefix, but Zeus removes that prefix before forwarding the request into the agent pod and records it in `X-Forwarded-Prefix`. A normal Vite development server assumes that the browser-facing URL and the URL received by Vite have the same path and host. That mismatch caused several distinct failures: 1. Vite rejected the forwarded Zeus `Host` header with “Blocked request.” 2. Vite was configured with the public base path, but received stripped SPA, asset, `/@fs`, and HMR paths from Zeus. 3. Browser code generated root-relative backend URLs such as `/graphql/frontend`, which went to the Zeus dashboard instead of `/agent/<job>:<port>/graphql/frontend` and returned dashboard HTML. 4. Backend paths that did reach Vite had to remain unprefixed internally so Vite's `server.proxy` rules could match them and forward them to Tilt. This PR makes the shared Vite configuration understand that complete browser → Zeus → Vite → backend boundary. The behavior is enabled only for Bolt sessions with a valid `BOLT_ZEUS_BASE_URL` and a non-root Vite base. This also revives and completes the intent of #27948, which was closed by stale automation. ## Request flow For a frontend base of `/agent/test-job:3000`: ### Frontend document and asset request 1. The browser requests `/agent/test-job:3000/@fs/...` or an SPA route. 2. Zeus selects the job and port, removes `/agent/test-job:3000`, and forwards `/@fs/...` with `X-Forwarded-Prefix: /agent/test-job:3000`. 3. The Vite plugin validates that the header exactly equals its configured base and restores the prefix before Vite's base-path middleware runs. 4. Vite serves the correct module, static asset, or SPA fallback. ### Backend request 1. The injected browser bootstrap changes a same-host request such as `/graphql/frontend` to `/agent/test-job:3000/graphql/frontend` before it leaves the page. 2. Zeus routes it to the agent and forwards the stripped `/graphql/frontend` path with `X-Forwarded-Prefix`. 3. The Vite middleware recognizes that `/graphql/frontend` belongs to `server.proxy` and deliberately leaves it stripped. 4. Vite's proxy matches the path and forwards it to `VITE_PROXY_TARGET` (for the hermetic Site environment, `http://app.minikube.local`). The same client-side behavior covers Fetch, `Request` objects, XMLHttpRequest/axios, and WebSockets. It preserves external URLs and URLs that already contain the Bolt prefix. ### HMR WebSocket Vite handles WebSocket upgrades before normal Connect middleware. The plugin therefore installs an early `upgrade` listener that restores the validated prefix only for the `vite-hmr` and `vite-ping` subprotocols. Application GraphQL WebSockets remain bare after Zeus stripping so Vite's WebSocket proxy can match them. ## Changes ### Exact Zeus host allowlisting - Parse the hostname from `BOLT_ZEUS_BASE_URL`. - Add only that hostname to Vite's `server.allowedHosts`. - Retain Vite's normal DNS-rebinding protection for every other host and for all non-Bolt development. - Ignore malformed `BOLT_ZEUS_BASE_URL` values instead of weakening host protection. ### Browser-side backend routing - Inject a `head-prepend` bootstrap before application modules run. - Prefix same-host HTTP(S) and WS(S) destinations for: - `window.fetch`, including `Request` inputs - `XMLHttpRequest.prototype.open` and therefore axios's browser transport - `window.WebSocket` - Do not modify external destinations or already-prefixed paths. This prevents Site GraphQL, GraphQL subscriptions, Grid REST calls, UI logging, Ops GraphiQL, UMA Bridge, and future root-relative proxy routes from escaping to the Zeus dashboard origin. ### Server-side prefix restoration - Validate `X-Forwarded-Prefix` against the configured Vite base. - Restore it for SPA routes, static assets, `/@fs` modules, and other Vite-served resources. - Avoid double-prefixing paths. - Read Vite's final merged `server.proxy` table so both shared and app-specific backend routes remain bare and continue matching their proxy rules. ### HMR upgrade handling - Restore the prefix for stripped `vite-hmr` and `vite-ping` upgrade requests before Vite's own upgrade listener runs. - Leave other WebSocket protocols untouched so application WebSocket proxying still works. ### Turbo and tests - Pass `BOLT_ZEUS_BASE_URL` through the persistent Turbo `start` task so it reaches Vite when apps are launched through Turbo. - Add a package test command using Node's built-in test runner. - Add six live-server/boundary tests covering: - unchanged host protection outside Bolt - accepted Zeus host and rejected untrusted host - early client bootstrap injection - Fetch, Request, XHR, external URL, already-prefixed URL, and WebSocket rewriting - HMR WebSocket upgrade returning `101 Switching Protocols` - full browser prefix → Zeus strip → Vite proxy → backend path handling - consumer-added proxy routes from final merged Vite configuration ## Scope and limitations - This supports Bolt's current path-based Zeus proxy; it does not introduce dedicated preview hostnames. - The app still needs to start Vite with its public Bolt path as the base, for example through `VITE_BASENAME=/agent/<job>:<port>/`. - `VITE_PROXY_TARGET` remains responsible for selecting the backend environment. Hermetic Site development should use `http://app.minikube.local` to reach Tilt without local TLS-certificate issues. - When `BOLT_ZEUS_BASE_URL` is absent or the base is `/`, none of the Bolt plugin, host allowlist, or browser networking changes are activated. ## Test plan - `yarn workspace @lightsparkdev/vite test` — 6/6 pass - `yarn workspace @lightsparkdev/site turbo:build` — pass - Turbo dry-run confirms `BOLT_ZEUS_BASE_URL` reaches the Site start task - Real Site through the Zeus-style boundary: - exact Zeus host plus validated `X-Forwarded-Prefix`: `200` - untrusted host: `403` - SPA HTML/bootstrap: `200 text/html` - `/@fs` SCSS module: `200 text/javascript` - named GraphQL request through Tilt: backend `application/json`, not Zeus HTML - stripped HMR WebSocket request: `101 Switching Protocols` - Repository pre-commit install and formatting checks --- 🤖 [resolute-reactor-2](https://zeus.dev.dev.sparkinfra.net/#/arc?id=resolute-reactor)[(#2)](https://zeus.dev.dev.sparkinfra.net/#/instance?id=resolute-reactor-2) | [Feedback](https://zeus.dev.dev.sparkinfra.net/feedback) --------- Co-authored-by: Aaron Kanter <aaron@lightspark.com> GitOrigin-RevId: ecc2672d17d55f53a09c04c0456a1bf09db006ac
## Reason Make backend target setup discoverable and reusable for engineers and agents working across private JS frontends, while making the canonical hermetic UI workflow recoverable and diagnosable. ## Overview - Adds shared `js/apps/private` agent guidance with `AGENTS.md -> CLAUDE.md`. - Keeps `docs/local-backends.md` as the stable router and common-instructions entry point, with mode and OS details split under `docs/local-backends/`. - Documents the three supported private frontend backend modes: - `start:dev` against hosted dev through Cognito-authenticated local Vite proxying. - `mise sparkcore-dev` for a local Sparkcore process using hosted dev data. - a seeded hermetic backend through Tilt and Kind. - Moves reusable hermetic setup into `js/apps/private/scripts/hermetic-backend.sh`; Site retains its Playwright-specific seeding, gatekeepers, static deploy, and test execution. - Keeps app READMEs and Site's Playwright README as thin links to the authoritative shared docs. - Enables the existing hosted-dev proxy flow for Ops and UMA Bridge and documents the app commands. - Makes routing-node initialization fail fast and recover from mismatches between LND wallet state and DB secrets: - stale DB secrets for a missing wallet are cleared; - an initialized wallet with missing/incomplete secrets resets only the verified routing-node Helm release and PVC; - node status and secrets are committed transactionally with SQL error handling. - Captures the full hermetic run log and structured phase timings under `test-results/hermetic-run/`, including setup failures and the first terminal Tilt error. - Makes `--clean` preserve the managed hosts block while rebuilding Kind, avoiding an unnecessary sudo prompt; standalone teardown keeps its existing host cleanup behavior. - Resolves runner-owned manifest paths absolutely so the runner is safe when invoked outside the Site directory. - Disables Marker, GTM, and Segment when Site is running under Playwright automation. - Fixes the Stripe billing flow by forcing persisted app state out of test mode before submission and asserting the successful billing-plan mutation. - Stabilizes Nage command-center coverage and production behavior by giving asynchronous sidebar sections stable IDs, so late gatekeeper results cannot remount and close an active command center. - Keeps unfiltered RSK and Nage projects scheduled in parallel while leaving stateful tests single-pass by default. ## Test Plan Latest local verification on head `a00cbd5296`: - `./playwright/run-tests.sh --clean` -> `126 passed` in 15.2m, including Stripe billing and all command-center cases on a newly created Kind cluster. - `./playwright/run-tests.sh --test-filter='28-nage-command-center.spec.ts'` -> `10 passed` in 2.5m. - Routing recovery fault injection: removed the active routing node's DB secret row, reran initialization, and verified only the expected Helm release/PVC was replaced; the node returned `READY` with one complete secret row and a funded wallet. - `bash js/apps/private/scripts/tilt-resource.test.sh` - `bash js/apps/private/scripts/hermetic-backend.test.sh` - Shellcheck on all changed shell scripts and helper tests. - `yarn workspace @lightsparkdev/site test src/uma-nage/components/SideNav.test.tsx` - `yarn workspace @lightsparkdev/site types` - `yarn workspace @lightsparkdev/site format` - `yarn workspace @lightsparkdev/site lint` - `yarn workspace @lightsparkdev/private-ui format` - `yarn workspace @lightsparkdev/private-ui lint` - `git diff --check` - Pre-commit Python, JS install, formatting, lint, and shellcheck hooks passed. GitOrigin-RevId: 7765e33b48232259e9634ae83c176a77a5203e42
The shellcheck filter's awk pattern only matched shebangs with the interpreter glued to `#!`, but POSIX allows whitespace there - so the six tracked scripts written as `#! /bin/bash` (both `js/` test/start scripts and `scripts/gha/find-image-tags.sh`) were never linted. Noticed while adding the release scripts in #30380. The filter now allows optional whitespace after `#!`; zsh stays excluded. Bringing the six files under lint surfaced real findings in two of them. `js/start.sh` built command strings and `eval`'d them; it now builds arrays, which hand turbo the same argv (including the filter globs, unexpanded) without the eval. `find-image-tags.sh` gets `SC2016` directives - the backticks in its `--query` strings are JMESPath literals, and switching to double quotes would turn them into command substitution. Testing: `./scripts/shellcheck.sh` checks 78 files (up from 72) and passes; checked the new pattern against shebang variants (`#!/bin/bash`, `#! /bin/bash`, `#!/usr/bin/env bash`, zsh forms still excluded); confirmed the array form of `start.sh` passes `--filter='./apps/private/*'` through literally even when the glob matches files, same as the eval did. GitOrigin-RevId: d968e3de6314e70a003d9980a43d6e10dda81d43
## 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
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.