Add @goodwidget/goodreserve-widget with reserve swap runtime contract, quote/tx state machine, and Storybook/demo coverage#39
Merged
Conversation
7 tasks
Copilot
AI
changed the title
[WIP] Build GoodReserve widget backed by GoodReserve SDK
Add May 27, 2026
@goodwidget/goodreserve-widget with reserve swap runtime contract, quote/tx state machine, and Storybook/demo coverage
Contributor
|
@copilot the skills folder should not be committed so can be removed. can you also produce initial screenshots of the story examples |
Contributor
Author
Implemented in commit
|
18 tasks
…build-goodreserve-widget
* fix(goodreserve-widget): align success screen and sizing with Figma
- Rebuild the swap-success screen to the Figma success frame order: glowing
blue check hero, title, summary card, "View on Explorer" link, and a
full-width pill "Do another swap" CTA.
- Make the widget responsive: fill the host container up to a 390px max width
(mobile-frame width) instead of a fixed 360px column.
- Raise primary CTAs to the Figma 54px height.
- Widen the Storybook story frame to 390px and regenerate story + Playwright
screenshot evidence to reflect the updated layout.
* fix(goodreserve-widget): make the amount input editable on web
The @goodwidget/ui Input is a Tamagui `tag:'input'` Stack, which does not
translate React Native's onChangeText to a DOM change event on web. As a
result the controlled amount field rejected all typed input and the entire
type-amount -> quote -> confirm -> swap path was unreachable in a browser
(every Storybook state is pre-seeded via mockState, so existing tests missed it).
- Wire the native onChange handler and inputMode="decimal" instead of the
RN-only onChangeText/keyboardType props.
- Sanitize input to a single decimal number at the boundary so values are
always safe for viem parseUnits (rejects "1.2.3", separators, letters).
- Bump the amount font to 34px to match the Figma reference.
- Add a live-adapter Interactive story and a Playwright regression test that
types into the input and asserts both editing and sanitization.
Verified empirically: before, typing left the DOM input value empty; after,
the value updates and "1.2.3x" sanitizes to "1.23".
* fix(goodreserve-widget): make swap math and execution safe
Correctness and safety fixes in the reserve adapter:
- Derive minimumReceived in BigInt from the SDK quote and carry the exact
minReturn (minReturnRaw) into executeSwap, so the floor shown to the user is
precisely the floor submitted on-chain. Previously the display used float
math (Number * (1 - slippage)) while execution used BigInt, allowing the two
to diverge on small trades.
- Guard executeSwap against double submission while a swap is pending.
- Re-validate chain support inside executeSwap so switching to an unsupported
chain after opening the confirm dialog is caught before signing.
- Set swap_success before refreshing balances and make the refresh best-effort,
so an RPC blip can no longer turn a confirmed swap into swap_error.
- mapReserveError now logs unmatched errors and returns a generic fallback
instead of surfacing raw viem output (which can leak RPC URLs / addresses).
* refactor(goodreserve-widget): harden adapter state machine and chain handling
- Make the public client chain-aware (RESERVE_CHAINS map) so the GoodReserve
SDK constructor, which validates publicClient.chain.id, does not throw on a
chainless client.
- Reset the cached SDK/read client on bootstrap so a Celo<->XDC switch
re-initializes against the new chain instead of reusing stale clients.
- Read the "from" balance via a ref inside the quote effect and drop it from
the effect deps, so a post-swap or direction-toggle balance refresh no longer
restarts the quote debounce.
- Preserve and restore the pre-overlay status when the slippage sheet or
confirm dialog is dismissed, instead of unconditionally forcing quote_ready.
- Render a dedicated sdk_initializing loading state (with fixture, story, and
Playwright coverage) rather than a half-populated swap card.
* fix(goodreserve-widget): apply amount input textAlign via DOM style
Use style={{ textAlign: 'right' }} instead of the React Native textAlign prop,
which Tamagui's tag:'input' Stack would otherwise emit as an invalid lowercase
`textalign` DOM attribute (React warning). Behavior unchanged.
* feat(goodreserve-widget): align layout and colors to the Figma reference
Rework the swap view to match the Figma GoodReserve frames (file
xsk5EiF6CvStA9mtdbA9OR), verified structurally against the Figma API:
Structure:
- Move the header (network pill, blue title, subtitle) above the dark card.
- Replace the Buy/Sell tabs with a single circular swap-direction button
between the amount cards (preserves both buy and sell, matches Figma).
- Render the confirmation as an anchored bottom-sheet Drawer with a token hero,
a 50px "Minimum Received" highlight, a details table (incl. Network Fee), and
a close affordance; render slippage selection as a Drawer too.
- Reorder the success screen to title -> summary -> explorer link -> glow icon.
- Make Transaction Details and FAQ collapsible (chevron); add the second FAQ
item and a bottom settings/slippage icon; add MAX on the swap-to row.
Colors/typography:
- Pin the exact Figma palette in the widget (card #0C0E15, input #252730,
badge #33343C, soft #8B91A0, secondary #C1C6D6, heading/MAX #4090FF,
positive #43E350) via widget-scoped named components, since the shared
GoodWalletV2 preset tokens differ and altering them is out of scope.
- Detail rows now use 16/500 values and 12/600 labels; PRICE uses PER <SYM>;
success heading 26/700; amount inputs 34/700.
Also add arrow-down/arrow-up icons to the UI Icon registry for the flip button.
* fix(goodreserve-widget): correct success amount, explorer link, and quote math
Fix remaining adapter and view correctness issues:
- Preserve the swapped output as lastSwapOutput before clearing the quote on
success, and show it on the success screen instead of the wallet balance
(the two are different numbers). Adds a regression test.
- Make "View on Explorer" a functional Anchor that links to celoscan / xinfin
for the tx hash, instead of a dead element.
- Replace window.setTimeout/clearTimeout with the bare globals so the quote
debounce works under SSR and React Native.
- Compare the input against the balance in BigInt (parseUnits) rather than via
Number(), removing last-decimal float drift in the insufficient-balance gate.
- Read result.hash (canonical) from the swap result.
- Split the bottom settings/slippage control into its own SettingsButton
component so it no longer shares the swap-direction sub-theme name.
- Open the confirmation drawer at full height so the hero + 50px highlight +
details table are not clipped; reset cleanly to buy idle on "Do another swap".
* fix(goodreserve-widget): harden adapter state, callbacks, and add state-coverage tests
- bootstrapSdk no longer depends on state.direction (reads a directionRef), so
toggling buy/sell after connecting no longer re-initializes the SDK.
- Hold onSwapSuccess/onSwapError in refs so inline host callbacks do not re-fire
the lifecycle effect on unchanged success/error states.
- Surface the real exitContribution from getReserveStats in the quote, and show
price impact as N/A instead of a misleading hardcoded ~0.01%.
- Sanitize setMaxAmount (and setInputAmount) through a shared amount.ts helper so
formatted balances are always parseUnits-safe.
- setSlippagePercent restores the previous status instead of forcing idle_buy,
preserving sell/quote context.
- Make the confirm-drawer Network Fee chain-aware (CELO vs XDC).
- Add Playwright coverage for the amount-editing and quote-loading states.
All 16 widget Playwright tests pass; build and lint clean.
* fix(goodreserve-widget): resolve idle status, quote freshness, and error recovery
Apply correctness, error-recovery, and contract fixes:
- Rename the idle status idle_buy -> idle across the contract, integration
manifest, and adapter, since direction is already a separate state field; the
status no longer reports "buy" while in sell mode.
- Align fixtures with the live adapter: priceImpactPercent is N/A everywhere
(SDK does not expose it) and renders muted rather than green; regenerate all
story screenshots so committed evidence matches real output, and add the
previously missing sdk-initializing.png.
- Wire the existing refresh action to a visible Retry CTA in the quote_error /
swap_error states so users are no longer stuck behind a disabled button.
- Add a preferredChainId prop so the unsupported-chain CTA can target XDC (or
any chain) instead of always switching to Celo.
- Guard executeSwap against stale quotes: quotes carry a quoteExpiresAt and a
swap submitted after expiry is rejected with a refresh prompt instead of
signing a minReturn derived from an outdated price.
- Clear txHash/lastSwapOutput on direction change so a prior swap result cannot
leak into the next swap.
All 16 widget Playwright tests pass; build and lint clean.
* refactor(goodreserve-widget): move styling onto the theming contract
Apply high-severity theming and UX fixes (SDK-dependent items
remain blocked until @goodsdks/good-reserve is published):
- H1/H2: replace the hardcoded FIGMA hex map with real, host-themable surfaces.
The named components now resolve their background/color/shadow from registered
light_/dark_Reserve* component sub-themes in the preset, and cross-cutting text
colors use new $reserve* palette tokens. No raw hex remains in the JSX.
Verified the rendered colors are unchanged (#0C0E15 shell, #252730 cards,
#4090FF heading) while now being overridable through the normal chain.
- H3: the header pill is a network chip (e.g. "CELO") instead of duplicating the
"Swap on CELO" heading.
- H4: remove the permanently-"N/A" price impact row (SDK exposes no price impact).
- H5: remove the fabricated "~0.001 CELO/XDC" network fee from the confirm sheet.
- M2: relabel "Final amount received" to "Estimated received" (it is the quote).
- M3: on quote expiry, keep the entered amount and re-quote instead of erroring.
- M7: switchChain now catches rejections (e.g. 4902) and surfaces a message.
- L2: explorer link uses explorer.xdc.org; L3: stable-decimals fallback is
chain-aware (XDC=6); N5: drop the capabilitySource reference to a non-existent
SDK export.
All 16 widget Playwright tests pass; ui + widget build and lint clean.
* fix(goodreserve-widget): finish theming migration and fix sub-theme collision
Resolve the theming-regression set:
- C4: give the confirm-hero "to" badge its own ConfirmToBadge component and
light_/dark_ReserveConfirmToBadge sub-theme (flat, no glow), distinct from the
96x96 glowing ReserveSuccessIcon they previously shared a name with.
- C5: every named surface declares color: '$color' and the badge glyphs use
color="$color", so a host override of a sub-theme moves surface + foreground
together.
- H7: convert the remaining flat-token surfaces to registered sub-themes
(ReserveSurface for success/FAQ, ReserveSurfaceInner for the confirm highlight,
ReserveDetailsTable for the confirm table); no inline $surface/$reserveCard.
- H8: the widget is dark-only — defaultTheme is fixed to 'dark' in the contract
and documented; light_/dark_ pairs are intentionally identical.
- H6: document the reserve palette as mirrored between theme.ts (token source)
and the preset color map.
All 16 widget Playwright tests pass; ui + widget build and lint clean.
* feat(goodreserve-widget): integrate the real GoodReserve SDK behind a typed seam
Replace the dynamic Function-based loader with a typed lazy import() of
@goodsdks/good-reserve (declared as an optionalDependency, since it is not yet
published). The seam in sdk.ts mirrors the real PR #35 public surface
(GoodReserveSDK / ReserveStats / ReserveTransactionResult), so every adapter
call site is type-checked against the actual contract instead of a loose shadow
type.
- Wire the onHash callback on buy/sell so swap_pending surfaces the submitted
tx hash before the receipt resolves; read result.hash.
- Scale exitContribution from parts-per-million (/10_000) per the Mento
convention, instead of the previous incorrect * 100.
- Re-validate the wallet's current chain via a live eth_chainId read before
signing, rather than trusting the memoized chain flag.
- Guard the empty-input quote effect so it cannot clobber terminal swap states
(success/error/pending) back to idle.
- Broaden mapReserveError to cover network/timeout/rejection and sanitized
revert reasons, matching citizen-claim-widget's coverage.
- Add a deterministic fake SDK + EIP-1193 test provider and an injection seam,
plus a LiveFakeSdk story and a Playwright test that drives the full real
adapter flow (quote -> confirm -> buy -> success with tx hash) with no
published SDK and no live RPC. The harness clears the injected fake on unmount.
All 17 widget Playwright tests pass; build and lint clean.
* fix(goodreserve-widget): correct price label, explorer URL, and theming claims
- Fix the swap-rate display: compute price as output-per-input and render it
consistently as "1 <tokenIn> = <price> <tokenOut>" in both the details row and
the confirm sheet (previously the two labels described reciprocal quantities
with the same number).
- Use the canonical XDC explorer (xdcscan.com/tx/<hash>) instead of an
unverified host/path; Celo stays celoscan.io/tx/.
- Clamp the unsupported-chain switch target to a supported reserve chain so a
bad preferredChainId can't route to an unsupported network and bounce back.
- Tokenize the network pill border (use $primaryMuted) so the view contains no
raw color literals.
- Correct the styled-components header comment: primary surface/text come from
the sub-theme, while secondary text shades are $reserve* tokens (overridable
at the token layer) — the comment no longer overstates per-sub-theme control.
- Document the exitContribution scaling against the SDK demo's own convention
(reserveRatio / 10000) rather than asserting an unverified PPM basis.
All 17 widget Playwright tests pass; build and lint clean.
* test(storybook): raise test-runner timeout to avoid cold-start flakes
Add a Storybook test-runner config that sets jest.setTimeout(60000). The default
15s can be exceeded when the dev server compiles a story on first request,
intermittently failing pnpm test:storybook in CI. Additive config (no prior
test-runner setup existed); benefits the whole story suite.
* chore: update test screenshots from latest test execution
* chore(goodreserve-widget): link SDK seam to PR #35, align XDC, drop test-results/
- sdk.ts: cite the source of truth the typed SDK seam mirrors
(GoodSDKs PR #35, merge commit 53fa2f56947e10e29864c0ab4f22733ca54d3b3a,
good-reserve head fcd7b278558db9624f2b67a7cbe22c09f662bd32) and the demo
file that shows the same types in use. Documents the buy/sell arg-order
asymmetry that the hand-rolled type previously mis-named.
- ReserveSwapView.tsx: align the XDC explorer host with the SDK demo
(explorer.xdc.org/tx/<hash>); add a source-of-truth comment.
- .gitignore / AGENTS.md: untrack tests/**/test-results/ PNGs (transient
Playwright run output); the canonical visual evidence is now the curated
examples/storybook/src/stories/<widget>/screenshots/ set.
- publish checklist: keep the optionalDependencies "*" pin; track
promotion to a real version as a follow-up once the SDK is published.
* fix(storybook): restore quote-ready-xdc story export
The QuoteReadyXdc story was dropped during a prior SDK-integration revert
and never re-added. The story was referenced by the test suite (which
asserts the XDC network label and a 216.50 G$ output for the buy direction
on chain 50), causing the "quote-ready on XDC" Playwright test to fail with
SB_PREVIEW_API_0009 (NoStoryMatchError).
Re-add the story export, mirroring the existing quote-ready-buy/sell
renderStory pattern, and regenerate the curated screenshot so the committed
evidence reflects the current state.
* fix(goodreserve-widget): revert XDC explorer to xdcscan.com
The XDC explorer URL is rendered in the success screen's "View on
Explorer" link. Revert from explorer.xdc.org to xdcscan.com, which
viem's own chain definition points at as the canonical XDC explorer
and which is the Etherscan-style actively-maintained surface. The
previous BlocksScan-era host was chosen to match the SDK demo;
xdcscan.com is the better default for end users.
* fix(goodreserve-widget): integrate real SDK, decompose view, fix tests
Material refactor of the goodreserve widget based on the bounty feedback:
- Wire the real @goodsdks/good-reserve SDK (now published as 0.1.0) via a
static import in useGoodReserveAdapter. Delete the typed seam file (sdk.ts),
the injection function (__setGoodReserveSdkConstructorForTesting), the fake
SDK fixture, and the LiveFakeSdk story — none of this scaffolding is
needed now that the package is installable. The adapter uses the SDK's
getGDBalance helper and erc20ABI export instead of hand-rolled equivalents.
- Decompose ReserveSwapView.tsx into per-state subcomponents:
ReserveSwapView is a thin dispatcher that routes to SdkInitializingView,
SwapSuccessView, or MainSwapView. The main swap shell (with its nested
slippage / confirm Drawers) lives in MainSwapView. The file no longer
contains a 450-line nested-conditional return.
- Restructure errors.ts away from a long if-chain into a structured
RESERVE_ERROR_RULES matcher array (most-specific-first) with an extracted
extractRevertReason helper for sanitized revert reasons. Coverage mirrors
citizen-claim-widget's humanReadableError; sharing across packages is a
future maintainer decision.
- Update fixture prices to match the adapter's output-per-input math.
The previous fixture values used input/output semantics, which had drifted
out of sync with the adapter (price = outputNum / inputNum) after commit
d62cbcc corrected the formula. All 5 quote fixtures now display values
consistent with the live adapter.
- Revert packages/ui/presets.ts and packages/ui/src/theme.ts to origin/main
to remove the reserve* palette entries and dark_/light_Reserve* sub-themes
that previously leaked widget-local styling into the shared design system.
The widget now owns its palette as a FIGMA constant in ReserveSwapView.tsx
and applies it via widget-scoped named components.
- Track the per-widget tests/<widget>/test-results/ PNGs as committed UI
evidence baselines (mirrors the established citizen-claim-widget pattern).
Remove the broad **/test-results/ .gitignore rule so per-widget folders
can be tracked; the root /test-results/ stays ignored for transient
Playwright trace/video output.
- Mark the live-adapter E2E test as test.skip — it requires live Celo/XDC
RPC connectivity which is not available in CI. The 16 mockState-driven
state-coverage tests continue to verify the state machine and visual
layout end-to-end.
- Fix ARCHITECTURE.md merge conflict marker (<<<<<<< HEAD) that was left
over from an earlier merge, and ensure the file ends with a trailing
newline.
- Repo-level lint blockers (pre-existing in origin/main, required for
pnpm lint to pass): convert the empty interface HostContextValue to a
type alias in packages/core/src/provider.tsx, and remove stale
react-hooks/exhaustive-deps disable directives in citizen-claim-widget
whose rule is not registered in the shared eslint config.
All 16 widget Playwright tests pass (1 live test skipped per above).
pnpm build (8/8), pnpm lint (13/13), and pnpm test:demo pass clean.
* fix(goodreserve-widget): source chain ids from SDK, use SDK chain validators
- constants.ts: re-export CELO_CHAIN_ID, XDC_CHAIN_ID, getReserveChainFromId
from @goodsdks/good-reserve so the SDK is the single source of truth for
these values. Drop the local SUPPORTED_RESERVE_CHAINS array and the 'as
never' cast site that needed it.
- integration.ts: build the chains list from the SDK exports instead of
hardcoded [42220, 50] literals.
- useGoodReserveAdapter.ts: replace SUPPORTED_RESERVE_CHAINS.includes checks
with getReserveChainFromId and GoodReserveSDK.isChainEnvSupported so the
adapter respects env-specific chain availability (XDC is development-only).
- Drawer.tsx: use Sheet.Overlay/Frame/Handle directly with inline styles and
registerComponent instead of createComponent-wrapped parts; the wrappers
broke Sheet's ref forwarding.
- test-results: re-render slippage/confirm screenshots from latest test run.
* chore: update test screenshots
* Revert "chore: update test screenshots"
This reverts commit 75271b7.
* feat: extract drawers, fix transaction success flow, add live wallet test
- Extract SlippageDrawer and ConfirmDrawer into separate components
- Fix transaction success to show immediately after tx hash (don't wait for receipt)
- Add LiveWallet story for real wallet testing with MetaMask
- Enable live SDK test in Playwright test suite
- Fix font size overflow in Swap to section
- Update screenshots for design-system and widget tests
* fix adapter file to show success immediately when we get the hash
* fix: address review feedback - wait for receipt before success, gate live test, surface tx hash in pending UI
- Wait for transaction receipt before showing success (fixes correctness regression)
- Use onHash to surface submitted hash during swap_pending for immediate feedback
- Gate live test behind GOODRESERVE_LIVE_TEST env var to prevent CI failures
- Add anchor link to view transaction on explorer when tx is submitted
- Remove 100ms race condition in success flow
* fix: update Drawer story test to search in document body for portal-rendered button
The Drawer component renders content in a portal, so the test needs to search
the entire document body instead of just the canvas element to find the Close button.
* fix: update Drawer test to search in document body for portal content
The Drawer component renders in a portal, so the close button is not
within the canvas element. Updated the test to search in document.body
instead of canvas to find the close button.
Also updated test screenshots to reflect current widget state.
* fix: address review feedback - design tokens, hook decomposition, presets revert, screenshots cleanup
- Delete storybook screenshots from examples/; canonical evidence in tests/
- Fix DEFAULT_STABLE_DECIMALS fallback to 18 (XDC=6 was wrong; reserve not on Fuse)
- Revert packages/ui/src/presets.ts to upstream base (governance tokens removed)
- Restore Drawer.tsx to working inline-style version; add reviewer comment explaining
the createComponent ref-forwarding issue that prevents using Sheet compound parts
- Replace FIGMA.* hardcoded hex constants with design-system token pattern:
- Add packages/goodreserve-widget/src/config.ts (follows governance-widget pattern)
- Wire goodReserveWidgetConfig via mergeThemeOverrides in GoodReserveWidget.tsx
- ReserveSwapView.tsx uses $reserveCard/$primary/$textColor/etc. tokens
- Wrap all <Anchor> children in <Text> for React Native compatibility
- Decompose 598-line useGoodReserveAdapter into four focused sub-hooks:
- useReserveBootstrap: wallet integration, SDK init, chain handling, balances
- useReserveQuote: debounced quote pipeline, BigInt balance gate, display derivation
- useReserveSwap: stale-quote guard, buy/sell execution, hash tracking
- useGoodReserveAdapter: thin orchestrator composing the above
* fix: refactor CTA button state using lookup table pattern
- Replace nested conditional ternary logic for the primary CTA button with a clean react lookup table () and helper function () as requested by L03TJ3.
* fix: correct decimals fallback — G$=18 on all reserve chains, stable=chain-aware (XDC=6, Celo=18)
* fix: update E2E tests, align story selectors, and regenerate snapshots
* fix: address review feedback — createComponent ref forwarding, config cleanup, light/dark support
- createComponent: forward refs through the HOC so Sheet compound parts
(Overlay/Frame/Handle) work without breaking animation/portal contract
- Drawer.tsx: revert to createComponent pattern now that refs are forwarded
- config.ts: remove redundant light_/dark_ sub-theme pairs (already defined
in createComponent calls via $ token references)
- widgetRuntimeContract.ts: support both light and dark themes, dark as default
- amount.ts: remove sanitizeAmount — inputMode=decimal handles this already
- stories: add InjectedWallet story using shared fixture, keep LiveWallet
- AGENTS.md: fix test-results path to tests/widgets/<widget-name>/
---------
Co-authored-by: Ryjen1 <ryjen1@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…build-goodreserve-widget
- Remove redundant localized styling config - Fix input field height and sizing - Re-add amount sanatizing, wrongfully removed
L03TJ3
approved these changes
Jul 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR introduces the first
packages/goodreserve-widgetpackage to prove GoodWidget supports a quote-driven reserve swap flow (buy/sell) with reserve-specific state handling and reusable widget packaging.It maps the swap UX structure from GoodWalletV2 into a widget-local implementation and adds deterministic story/demo coverage for all required states.
Widget package scaffold + runtime contract
@goodwidget/goodreserve-widgetpackage scaffold (package.json, tsconfig/tsup, React + element/register entrypoints).widgetRuntimeContract.ts+integration.tsdefining reserve states/events/dependencies (no_provider,unsupported_chain,quote_*,confirm_dialog,swap_*, etc.).src/index.ts.Reserve swap UI + local adapter/state
GoodReserveWidget,ReserveSwapView, anduseGoodReserveAdapter.GoodWidgetProviderpath and chain gating for Celo (42220) + XDC (50).SDK wiring and explicit reserve dependency handling
@goodsdks/good-reserveis unavailable at runtime (reported as a blocker condition, not silently ignored).Storybook coverage (widget-scoped, deterministic)
examples/storybook/src/stories/goodreserve-widget/GoodReserveWidget.stories.tsx.examples/storybook/src/fixtures/goodReserveWidgetMock.ts.Demo verification coverage
tests/widgets/goodreserve-widget/states.spec.tswith deterministic Playwright checks mapped to widget stories.Workspace/docs wiring
@goodwidget/goodreserve-widget.goodreserve-widget.