From 4641f1a479a7dc774ed63665dd49176b0aa3e6cf Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:02:21 -0600 Subject: [PATCH 1/2] feat: add modal display mode to swap widget demo 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 --- packages/swap-widget/src/demo/App.css | 111 ++++++++++++++++++ .../swap-widget/src/demo/DemoCustomizer.tsx | 54 ++++++++- .../src/demo/ExternalWalletApp.tsx | 29 +++-- .../src/demo/InternalWalletApp.tsx | 31 +++-- packages/swap-widget/src/demo/WidgetModal.tsx | 75 ++++++++++++ 5 files changed, 277 insertions(+), 23 deletions(-) create mode 100644 packages/swap-widget/src/demo/WidgetModal.tsx diff --git a/packages/swap-widget/src/demo/App.css b/packages/swap-widget/src/demo/App.css index 4d777f1883a..c9af1134008 100644 --- a/packages/swap-widget/src/demo/App.css +++ b/packages/swap-widget/src/demo/App.css @@ -460,6 +460,117 @@ body, justify-content: center; } +.demo-widget-container.demo-widget-container-modal { + align-self: stretch; + align-items: center; +} + +.demo-launch { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 420px; + max-width: 100%; + min-height: 320px; + padding: 24px; +} + +.demo-launch-btn { + display: flex; + align-items: center; + gap: 10px; + padding: 16px 32px; + border: none; + border-radius: 14px; + background: var(--demo-accent); + color: #ffffff; + font-size: 16px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; + box-shadow: 0 8px 24px color-mix(in srgb, var(--demo-accent), transparent 65%); +} + +.demo-launch-btn:hover { + transform: translateY(-1px); + box-shadow: 0 10px 28px color-mix(in srgb, var(--demo-accent), transparent 55%); +} + +.demo-launch-btn:active { + transform: scale(0.98); +} + +.demo-modal-overlay { + position: fixed; + inset: 0; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + overflow-y: auto; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(6px); + animation: demo-modal-fade-in 0.15s ease; +} + +.demo-modal-backdrop { + position: absolute; + inset: 0; + border: none; + padding: 0; + background: transparent; + cursor: default; +} + +.demo-modal { + position: relative; + margin: auto; + animation: demo-modal-pop-in 0.2s ease; +} + +.demo-modal-close { + position: absolute; + top: -40px; + right: 0; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + border-radius: 50%; + background: rgba(255, 255, 255, 0.12); + color: #ffffff; + cursor: pointer; + transition: background 0.15s ease; +} + +.demo-modal-close:hover { + background: rgba(255, 255, 255, 0.24); +} + +@keyframes demo-modal-fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes demo-modal-pop-in { + from { + opacity: 0; + transform: translateY(8px) scale(0.98); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + .demo-footer { padding: 20px; text-align: center; diff --git a/packages/swap-widget/src/demo/DemoCustomizer.tsx b/packages/swap-widget/src/demo/DemoCustomizer.tsx index 64495e4a5e2..0ed7bde4169 100644 --- a/packages/swap-widget/src/demo/DemoCustomizer.tsx +++ b/packages/swap-widget/src/demo/DemoCustomizer.tsx @@ -68,11 +68,14 @@ const DEFAULT_LIGHT: ThemeColors = { bg: '#f8f9fc', card: '#ffffff', accent: '#3 const STORAGE_KEY = 'ssw-demo-config' +export type DisplayMode = 'inline' | 'modal' + type StoredConfig = { partnerCode?: string darkColors?: ThemeColors lightColors?: ThemeColors selectedPreset?: string | null + displayMode?: DisplayMode } const loadConfig = (): StoredConfig => { @@ -90,17 +93,18 @@ export const useDemoTheme = (theme: 'light' | 'dark') => { const [darkColors, setDarkColors] = useState(stored.darkColors ?? DEFAULT_DARK) const [lightColors, setLightColors] = useState(stored.lightColors ?? DEFAULT_LIGHT) const [selectedPreset, setSelectedPreset] = useState(stored.selectedPreset ?? null) + const [displayMode, setDisplayMode] = useState(stored.displayMode ?? 'inline') useEffect(() => { try { localStorage.setItem( STORAGE_KEY, - JSON.stringify({ partnerCode, darkColors, lightColors, selectedPreset }), + JSON.stringify({ partnerCode, darkColors, lightColors, selectedPreset, displayMode }), ) } catch { // ignore write failures (e.g. storage unavailable) } - }, [partnerCode, darkColors, lightColors, selectedPreset]) + }, [partnerCode, darkColors, lightColors, selectedPreset, displayMode]) const currentColors = theme === 'dark' ? darkColors : lightColors @@ -135,6 +139,8 @@ export const useDemoTheme = (theme: 'light' | 'dark') => { setLightColors, selectedPreset, setSelectedPreset, + displayMode, + setDisplayMode, themeConfig, demoStyle, } @@ -158,6 +164,8 @@ export const DemoCustomizer = ({ theme, setTheme, state }: DemoCustomizerProps) setLightColors, selectedPreset, setSelectedPreset, + displayMode, + setDisplayMode, } = state const currentColors = theme === 'dark' ? darkColors : lightColors @@ -310,6 +318,48 @@ ${formatColors(lightColors, 'light')} +
+ Display +
+ + +
+
+
Background Color
diff --git a/packages/swap-widget/src/demo/ExternalWalletApp.tsx b/packages/swap-widget/src/demo/ExternalWalletApp.tsx index 80fa96debc5..b02cc0e633c 100644 --- a/packages/swap-widget/src/demo/ExternalWalletApp.tsx +++ b/packages/swap-widget/src/demo/ExternalWalletApp.tsx @@ -7,6 +7,7 @@ import { SwapWidget } from '../components/SwapWidget' import { initializeAppKit } from '../config/appkit' import { truncateAddress } from '../types' import { DemoCustomizer, useDemoTheme } from './DemoCustomizer' +import { WidgetModal } from './WidgetModal' const PROJECT_ID = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID @@ -67,7 +68,7 @@ const ExternalDemoBody = ({ theme, setTheme }: ExternalDemoBodyProps) => { const [showCustomizer, setShowCustomizer] = useState(true) const themeState = useDemoTheme(theme) - const { themeConfig, partnerCode, demoStyle } = themeState + const { themeConfig, partnerCode, demoStyle, displayMode } = themeState const handleSwapSuccess = useCallback((txHash: string) => { console.log('Swap successful:', txHash) @@ -77,6 +78,17 @@ const ExternalDemoBody = ({ theme, setTheme }: ExternalDemoBodyProps) => { console.error('Swap failed:', error) }, []) + const widget = ( + + ) + return (
@@ -141,15 +153,12 @@ const ExternalDemoBody = ({ theme, setTheme }: ExternalDemoBodyProps) => { )} -
- +
+ {displayMode === 'modal' ? {widget} : widget}
diff --git a/packages/swap-widget/src/demo/InternalWalletApp.tsx b/packages/swap-widget/src/demo/InternalWalletApp.tsx index a424299b79e..3eb41afbd86 100644 --- a/packages/swap-widget/src/demo/InternalWalletApp.tsx +++ b/packages/swap-widget/src/demo/InternalWalletApp.tsx @@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from 'react' import { SwapWidget } from '../components/SwapWidget' import { DemoCustomizer, useDemoTheme } from './DemoCustomizer' +import { WidgetModal } from './WidgetModal' const PROJECT_ID = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID @@ -18,7 +19,7 @@ const InternalDemoBody = ({ theme, setTheme }: InternalDemoBodyProps) => { const [showCustomizer, setShowCustomizer] = useState(true) const themeState = useDemoTheme(theme) - const { themeConfig, partnerCode, demoStyle } = themeState + const { themeConfig, partnerCode, demoStyle, displayMode } = themeState const handleSwapSuccess = useCallback((txHash: string) => { console.log('Swap successful:', txHash) @@ -28,6 +29,18 @@ const InternalDemoBody = ({ theme, setTheme }: InternalDemoBodyProps) => { console.error('Swap failed:', error) }, []) + const widget = ( + + ) + return (
@@ -94,16 +107,12 @@ const InternalDemoBody = ({ theme, setTheme }: InternalDemoBodyProps) => { )} -
- +
+ {displayMode === 'modal' ? {widget} : widget}
diff --git a/packages/swap-widget/src/demo/WidgetModal.tsx b/packages/swap-widget/src/demo/WidgetModal.tsx new file mode 100644 index 00000000000..4ee1135e4b7 --- /dev/null +++ b/packages/swap-widget/src/demo/WidgetModal.tsx @@ -0,0 +1,75 @@ +import type { ReactNode } from 'react' +import { useCallback, useEffect, useState } from 'react' + +type WidgetModalProps = { + children: ReactNode +} + +export const WidgetModal = ({ children }: WidgetModalProps) => { + const [isOpen, setIsOpen] = useState(false) + + const open = useCallback(() => setIsOpen(true), []) + const close = useCallback(() => setIsOpen(false), []) + + useEffect(() => { + if (!isOpen) return + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') setIsOpen(false) + } + window.addEventListener('keydown', onKeyDown) + document.body.style.overflow = 'hidden' + return () => { + window.removeEventListener('keydown', onKeyDown) + document.body.style.overflow = '' + } + }, [isOpen]) + + return ( + <> +
+ +
+ + {isOpen && ( +
+ + {children} +
+
+ )} + + ) +} From 8729f284e77dd4204f3b60b558ddba7c758ea2e7 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:20:13 -0600 Subject: [PATCH 2/2] chore: address coderabbit review feedback 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 --- .../src/demo/ExternalWalletApp.tsx | 23 +++++++++-------- .../src/demo/InternalWalletApp.tsx | 25 +++++++++++-------- packages/swap-widget/src/demo/WidgetModal.tsx | 4 +-- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/swap-widget/src/demo/ExternalWalletApp.tsx b/packages/swap-widget/src/demo/ExternalWalletApp.tsx index b02cc0e633c..8b090dc74e2 100644 --- a/packages/swap-widget/src/demo/ExternalWalletApp.tsx +++ b/packages/swap-widget/src/demo/ExternalWalletApp.tsx @@ -1,7 +1,7 @@ import './App.css' import { useAppKit, useAppKitAccount } from '@reown/appkit/react' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { SwapWidget } from '../components/SwapWidget' import { initializeAppKit } from '../config/appkit' @@ -78,15 +78,18 @@ const ExternalDemoBody = ({ theme, setTheme }: ExternalDemoBodyProps) => { console.error('Swap failed:', error) }, []) - const widget = ( - + const widget = useMemo( + () => ( + + ), + [partnerCode, themeConfig, handleSwapSuccess, handleSwapError], ) return ( diff --git a/packages/swap-widget/src/demo/InternalWalletApp.tsx b/packages/swap-widget/src/demo/InternalWalletApp.tsx index 3eb41afbd86..7125deb75bf 100644 --- a/packages/swap-widget/src/demo/InternalWalletApp.tsx +++ b/packages/swap-widget/src/demo/InternalWalletApp.tsx @@ -1,6 +1,6 @@ import './App.css' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { SwapWidget } from '../components/SwapWidget' import { DemoCustomizer, useDemoTheme } from './DemoCustomizer' @@ -29,16 +29,19 @@ const InternalDemoBody = ({ theme, setTheme }: InternalDemoBodyProps) => { console.error('Swap failed:', error) }, []) - const widget = ( - + const widget = useMemo( + () => ( + + ), + [partnerCode, themeConfig, handleSwapSuccess, handleSwapError], ) return ( diff --git a/packages/swap-widget/src/demo/WidgetModal.tsx b/packages/swap-widget/src/demo/WidgetModal.tsx index 4ee1135e4b7..549e57c7026 100644 --- a/packages/swap-widget/src/demo/WidgetModal.tsx +++ b/packages/swap-widget/src/demo/WidgetModal.tsx @@ -14,7 +14,7 @@ export const WidgetModal = ({ children }: WidgetModalProps) => { 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' @@ -22,7 +22,7 @@ export const WidgetModal = ({ children }: WidgetModalProps) => { window.removeEventListener('keydown', onKeyDown) document.body.style.overflow = '' } - }, [isOpen]) + }, [isOpen, close]) return ( <>