Skip to content

feat: add modal display mode to swap widget demo#12421

Merged
kaladinlight merged 2 commits into
developfrom
feat/swap-widget-demo-modal
Jun 10, 2026
Merged

feat: add modal display mode to swap widget demo#12421
kaladinlight merged 2 commits into
developfrom
feat/swap-widget-demo-modal

Conversation

@kaladinlight

@kaladinlight kaladinlight commented Jun 10, 2026

Copy link
Copy Markdown
Member

Description

Adds an Inline / Modal display toggle to the swap widget demo app's customizer to better demonstrate how a client would integrate the widget behind a button.

  • New WidgetModal demo component: a host-style "Swap crypto" CTA (themed via the demo's accent color) that opens the widget in a centered overlay with blurred backdrop. Dismissable via backdrop click, Escape, or close button; body scroll is locked while open.
  • The toggle lives in the demo customizer and persists alongside the existing demo config in localStorage. Works on both the internal and external (host-owned AppKit) wallet demo pages.
  • In modal mode the CTA column stretches to the customizer's height and centers the button, mirroring the inline widget's 420px footprint.

Demo-only change: nothing under src/demo/ is part of the published SDK (dist is built from src/index.ts via tsup; verified the built index.css/index.js contain no demo code or styles).

Issue (if applicable)

closes #

Risk

Near zero — isolated to the swap widget demo app. No widget, SDK, or web app code paths affected. No on-chain transaction changes.

What protocols, transaction types, wallets or contract interactions might be affected by this PR?

None.

Testing

Engineering

  1. cd packages/swap-widget && pnpm dev
  2. In the customizer, switch Display to Modal — the widget is replaced by a "Swap crypto" button
  3. Click it: the widget opens in a centered overlay; verify backdrop click, Escape, and the × button all close it
  4. Verify the toggle persists across reloads and works on both the Internal and External demo pages
  5. pnpm run build and confirm dist/index.css contains no demo- classes

Operations

  • 🏁 Demo app only — not user-facing in the web app and not part of the published SDK

Screenshots (if applicable)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Demo now lets you switch between inline and modal display modes from the customizer, with the choice persisted.
    • Modal view adds a centered launch panel/button, fixed overlay, close control, Escape-to-close keyboard support, and prevents background scrolling.
    • Modal appearance includes fade/scale/pop animations and polished backdrop styling for a smoother experience.

Adds an Inline/Modal display toggle to the demo customizer. Modal mode
renders a host-style "Swap crypto" CTA that opens the widget in a
centered overlay (backdrop/Escape/close button to dismiss), showing how
a client integration would present the widget behind a button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kaladinlight kaladinlight requested a review from a team as a code owner June 10, 2026 21:03
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f042c935-8610-4d2c-a00c-613fe8453ed4

📥 Commits

Reviewing files that changed from the base of the PR and between 4641f1a and 8729f28.

📒 Files selected for processing (3)
  • packages/swap-widget/src/demo/ExternalWalletApp.tsx
  • packages/swap-widget/src/demo/InternalWalletApp.tsx
  • packages/swap-widget/src/demo/WidgetModal.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/swap-widget/src/demo/WidgetModal.tsx
  • packages/swap-widget/src/demo/ExternalWalletApp.tsx
  • packages/swap-widget/src/demo/InternalWalletApp.tsx

📝 Walkthrough

Walkthrough

This PR adds a modal display mode to the swap widget demo. Users can toggle between inline and modal presentation via a "Display" setting in the customizer, which persists to localStorage. A new WidgetModal component wraps the widget in a modal overlay with keyboard/scroll handling, styled with CSS animations, and is integrated into both demo wallet applications.

Changes

Modal Display Mode Feature

Layer / File(s) Summary
Display Mode state management and persistence
packages/swap-widget/src/demo/DemoCustomizer.tsx
DisplayMode type (`'inline'
WidgetModal component with state and effects
packages/swap-widget/src/demo/WidgetModal.tsx
New WidgetModal component receives children and manages internal isOpen state. Memoized open/close callbacks toggle it. A useEffect registers a keydown listener to close on Escape, modifies document.body.style.overflow to prevent background scrolling, and cleans up properly. Render tree shows launch button, modal overlay, backdrop, close button, and children container conditionally.
Modal CSS styling and animations
packages/swap-widget/src/demo/App.css
Modal-specific .demo-widget-container variant stretches and centers. .demo-launch layout and .demo-launch-btn button styled with sizing, padding, shadow, hover/active states. Fixed overlay, backdrop, modal structure, and positioned close button. Two keyframe animations (demo-modal-fade-in, demo-modal-pop-in) control opacity, position, and scale transitions.
Modal integration in demo wallet apps
packages/swap-widget/src/demo/ExternalWalletApp.tsx, packages/swap-widget/src/demo/InternalWalletApp.tsx
Both apps import WidgetModal and destructure displayMode from useDemoTheme. Inline SwapWidget JSX refactored into a widget constant with partner code, theme, swap callbacks, and UI toggles. Widget container conditionally applies modal CSS class when displayMode === 'modal' and wraps widget with <WidgetModal> in that case; otherwise renders directly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • shapeshift/web#12411: Modifies packages/swap-widget/src/demo/DemoCustomizer.tsx localStorage persistence logic alongside this PR's displayMode field addition, overlapping at the same demo state/hydration layer.

Poem

🐰 A modal mode hops into view,
With Escape to dismiss, and overflow too—
Inline or floating, the widget takes flight,
Styled with a pop and a gentle fade-in light,
The demo now hops, cheerful and bright.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add modal display mode to swap widget demo' clearly and directly describes the main change: introducing a modal display mode option to the swap widget demo application.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/swap-widget-demo-modal

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
packages/swap-widget/src/demo/InternalWalletApp.tsx (1)

32-42: ⚡ Quick win

Consider wrapping the widget constant in useMemo.

The widget constant is a derived JSX value that should be wrapped in useMemo per coding guidelines. This mirrors the same recommendation for ExternalWalletApp.tsx.

♻️ Suggested refactor
+ const widget = useMemo(
+   () => (
-  const widget = (
     <SwapWidget
       partnerCode={partnerCode || undefined}
       theme={themeConfig}
       onSwapSuccess={handleSwapSuccess}
       onSwapError={handleSwapError}
       showPoweredBy={true}
       showConnectButton={true}
       walletConnectProjectId={PROJECT_ID}
     />
+   ),
+   [partnerCode, themeConfig, handleSwapSuccess, handleSwapError],
-  )
+ )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swap-widget/src/demo/InternalWalletApp.tsx` around lines 32 - 42,
The JSX constant widget should be memoized with React.useMemo to avoid
recreating the SwapWidget on every render; replace the plain widget assignment
with useMemo(() => (<SwapWidget .../>), [...]) and include all props and
handlers as dependencies (partnerCode or partnerCode-derived value, themeConfig,
handleSwapSuccess, handleSwapError, PROJECT_ID and any booleans like
showPoweredBy/showConnectButton) so the memo updates correctly when inputs
change.

Source: Coding guidelines

packages/swap-widget/src/demo/WidgetModal.tsx (1)

14-25: 💤 Low value

Consider using the close callback for consistency.

The keyboard event handler on line 17 calls setIsOpen(false) directly, while a close callback is already defined on line 12 for this exact purpose. Using close() instead would follow the DRY principle and maintain consistency with the open callback pattern.

♻️ Suggested refactor
   useEffect(() => {
     if (!isOpen) return
     const onKeyDown = (e: KeyboardEvent) => {
-      if (e.key === 'Escape') setIsOpen(false)
+      if (e.key === 'Escape') close()
     }
     window.addEventListener('keydown', onKeyDown)
     document.body.style.overflow = 'hidden'
     return () => {
       window.removeEventListener('keydown', onKeyDown)
       document.body.style.overflow = ''
     }
-  }, [isOpen])
+  }, [isOpen, close])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swap-widget/src/demo/WidgetModal.tsx` around lines 14 - 25, The
Escape key handler in the useEffect should call the existing close callback
instead of directly calling setIsOpen(false); update the onKeyDown function in
WidgetModal (the handler currently referencing setIsOpen) to call close() so the
modal is closed via the canonical callback, keeping behavior consistent with the
open/close pattern and avoiding direct state mutation; ensure close is in scope
and leave the addEventListener/removeEventListener and body overflow cleanup
as-is.
packages/swap-widget/src/demo/ExternalWalletApp.tsx (1)

81-90: ⚡ Quick win

Consider wrapping the widget constant in useMemo.

The widget constant is a derived JSX value based on partnerCode, themeConfig, and the callback handlers. Per coding guidelines, derived values should be wrapped in useMemo to prevent unnecessary recreation on every render.

♻️ Suggested refactor
+ const widget = useMemo(
+   () => (
-  const widget = (
     <SwapWidget
       partnerCode={partnerCode || undefined}
       theme={themeConfig}
       onSwapSuccess={handleSwapSuccess}
       onSwapError={handleSwapError}
       showPoweredBy={true}
       showConnectButton={false}
     />
+   ),
+   [partnerCode, themeConfig, handleSwapSuccess, handleSwapError],
-  )
+ )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/swap-widget/src/demo/ExternalWalletApp.tsx` around lines 81 - 90,
The JSX constant widget (the <SwapWidget ... /> created from partnerCode,
themeConfig, handleSwapSuccess, handleSwapError) should be memoized with useMemo
to avoid recreating the element each render; update ExternalWalletApp to
import/use React.useMemo and wrap the widget creation in useMemo with a
dependency array containing partnerCode, themeConfig, handleSwapSuccess, and
handleSwapError so the SwapWidget is only recreated when those values change.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/swap-widget/src/demo/ExternalWalletApp.tsx`:
- Around line 81-90: The JSX constant widget (the <SwapWidget ... /> created
from partnerCode, themeConfig, handleSwapSuccess, handleSwapError) should be
memoized with useMemo to avoid recreating the element each render; update
ExternalWalletApp to import/use React.useMemo and wrap the widget creation in
useMemo with a dependency array containing partnerCode, themeConfig,
handleSwapSuccess, and handleSwapError so the SwapWidget is only recreated when
those values change.

In `@packages/swap-widget/src/demo/InternalWalletApp.tsx`:
- Around line 32-42: The JSX constant widget should be memoized with
React.useMemo to avoid recreating the SwapWidget on every render; replace the
plain widget assignment with useMemo(() => (<SwapWidget .../>), [...]) and
include all props and handlers as dependencies (partnerCode or
partnerCode-derived value, themeConfig, handleSwapSuccess, handleSwapError,
PROJECT_ID and any booleans like showPoweredBy/showConnectButton) so the memo
updates correctly when inputs change.

In `@packages/swap-widget/src/demo/WidgetModal.tsx`:
- Around line 14-25: The Escape key handler in the useEffect should call the
existing close callback instead of directly calling setIsOpen(false); update the
onKeyDown function in WidgetModal (the handler currently referencing setIsOpen)
to call close() so the modal is closed via the canonical callback, keeping
behavior consistent with the open/close pattern and avoiding direct state
mutation; ensure close is in scope and leave the
addEventListener/removeEventListener and body overflow cleanup as-is.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 32fd5cc6-50e7-4048-bc1b-dd36cf98dbc6

📥 Commits

Reviewing files that changed from the base of the PR and between 2777a51 and 4641f1a.

📒 Files selected for processing (5)
  • packages/swap-widget/src/demo/App.css
  • packages/swap-widget/src/demo/DemoCustomizer.tsx
  • packages/swap-widget/src/demo/ExternalWalletApp.tsx
  • packages/swap-widget/src/demo/InternalWalletApp.tsx
  • packages/swap-widget/src/demo/WidgetModal.tsx

Memoize the SwapWidget element in both demo apps and close the modal
via the canonical close callback in the Escape handler.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kaladinlight kaladinlight enabled auto-merge (squash) June 10, 2026 21:22
@kaladinlight kaladinlight merged commit 1e62d75 into develop Jun 10, 2026
4 checks passed
@kaladinlight kaladinlight deleted the feat/swap-widget-demo-modal branch June 10, 2026 21:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant