{/* Attempt 2 at fixing issue on Vercel build (locally it builds fine) */}
@@ -334,200 +371,176 @@ export function Configurator({ title }: { title: string }) {
>
)}
-
General
-
-
-
-
-
-
setBoxShadow(event.target.value)}
- size="medium"
- />
-
-
-
-
-
-
-
-
-
- {!IS_IFRAME && (
-
- )}
-
- Tokens
-
-
-
-
-
-
-
- Forced Order Deadline
-
- Global deadline settings
-
-
- Individual deadline settings
-
-
-
-
- Integrations
-
-
-
- Customization
-
-
-
-
+
+
+ {!IS_IFRAME && }
+
+
+
+
+
+
+ {!IS_IFRAME && (
+
+ )}
+
+
- Other settings
-
- Toast notifications:
-
- } label="Self-contain in Widget" />
- } label="Dapp mode" />
-
-
-
-
- Progress bar:
-
- } label="Show SWAP progress bar" />
- } label="Hide SWAP progress bar" />
-
-
-
-
- Cross-chain swaps:
-
- } label="Enable cross-chain swaps" />
- } label="Disable cross-chain swaps" />
-
-
-
-
- Custom tokens and lists:
-
- } label="Allow importing custom tokens/lists" />
- } label="Disable importing custom tokens/lists" />
-
-
-
-
- Recent tokens:
-
- } label="Show recent tokens" />
- } label="Hide recent tokens" />
-
-
-
-
- Favorite tokens:
-
- } label="Show favorite tokens" />
- } label="Hide favorite tokens" />
-
-
-
-
- Hide bridge info:
-
- } label="Show bridge info" />
- } label="Hide bridge info" />
-
-
-
-
- Hide orders table:
-
- } label="Show orders table" />
- } label="Hide orders table" />
-
-
-
- Disable trade when price impact is unknown:
-
- } label="Allow trade" />
- } label="Disable trade" />
-
-
-
-
- setRawParams(e.target.value)}
- size="medium"
- />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Global deadline
+
+
+
+
+
+ Per-trade deadlines
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setRawParams(e.target.value)}
+ size="medium"
+ />
+
+
{isDrawerOpen && (
- setIsDrawerOpen(false)}
- sx={{ position: 'fixed', top: '1.3rem', left: '26.7rem' }}
+ sx={(theme) => ({
+ ...DrawerToggleButtonStyled(theme),
+ width: '3.2rem',
+ height: '3.2rem',
+ position: 'absolute',
+ top: '1.8rem',
+ right: '1.2rem',
+ })}
>
-
+
)}
))}
+
+
-
+
{params && (
<>
({
- width: '29rem',
+ width: `var(${DRAWER_WIDTH_CSS_VAR})`,
flexShrink: 0,
'& .MuiDrawer-paper': {
- width: '29rem',
+ width: `var(${DRAWER_WIDTH_CSS_VAR})`,
boxSizing: 'border-box',
display: 'flex',
flexFlow: 'column',
@@ -19,22 +27,26 @@ export const DrawerStyled = (theme: Theme) => ({
background: theme.palette.background.paper,
boxShadow: 'rgba(5, 43, 101, 0.06) 0 1.2rem 1.2rem',
padding: '1.6rem',
+ position: 'relative',
+ overflow: 'hidden',
+ overflowY: 'auto',
},
})
export const ContentStyled = {
- width: '100%',
+ width: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
flexFlow: 'column',
flex: '1 1 auto',
- margin: '0 auto',
+ minWidth: 0,
+ overflow: 'auto',
+ padding: '2rem 1.6rem',
- '> iframe': {
+ '& iframe': {
border: 0,
margin: '0 auto',
- borderRadius: '1.6rem',
overflow: 'auto',
},
}
@@ -45,3 +57,60 @@ export const WalletConnectionWrapper = {
margin: '0 auto 1rem',
width: '100%',
}
+
+export const ResizeHandleStyled = {
+ position: 'absolute',
+ top: 0,
+ right: 0,
+ bottom: 0,
+ width: '0.8rem',
+ cursor: 'col-resize',
+ zIndex: 2,
+
+ '&::before': {
+ content: '""',
+ position: 'absolute',
+ top: '1.6rem',
+ bottom: '1.6rem',
+ left: '50%',
+ transform: 'translateX(-50%)',
+ width: '0.2rem',
+ borderRadius: '999px',
+ backgroundColor: 'divider',
+ },
+
+ '&:hover::before': {
+ backgroundColor: 'text.secondary',
+ },
+}
+
+interface DrawerToggleButtonStyles {
+ width: string
+ height: string
+ borderRadius: string
+ border: string
+ backgroundColor: string
+ color: string
+ boxShadow: string
+ zIndex: number
+ '&:hover': {
+ backgroundColor: string
+ boxShadow: string
+ }
+}
+
+export const DrawerToggleButtonStyled = (theme: Theme): DrawerToggleButtonStyles => ({
+ width: '3.6rem',
+ height: '3.6rem',
+ borderRadius: '50%',
+ border: `1px solid ${theme.palette.divider}`,
+ backgroundColor: theme.palette.background.paper,
+ color: theme.palette.primary.main,
+ boxShadow: 'none',
+ zIndex: 3,
+
+ '&:hover': {
+ backgroundColor: theme.palette.action.hover,
+ boxShadow: 'none',
+ },
+})
diff --git a/apps/widget-configurator/src/app/configurator/types.ts b/apps/widget-configurator/src/app/configurator/types.ts
index 142801ad83a..79f7afd2586 100644
--- a/apps/widget-configurator/src/app/configurator/types.ts
+++ b/apps/widget-configurator/src/app/configurator/types.ts
@@ -24,7 +24,12 @@ export interface ConfiguratorState {
chainId?: SupportedChainId
locale?: string
theme: PaletteMode
+ iframeWidth?: string
+ iframeBackgroundColor?: string
+ iframeBorderRadius?: string
boxShadow?: string
+ widgetPadding?: string
+ widgetBorderRadius?: string
currentTradeType: TradeType
enabledTradeTypes: TradeType[]
enabledWidgetHooks: WidgetHookEvents[]
diff --git a/apps/widget-configurator/src/app/embedDialog/const.ts b/apps/widget-configurator/src/app/embedDialog/const.ts
index 69a21057ec3..40d165e5f99 100644
--- a/apps/widget-configurator/src/app/embedDialog/const.ts
+++ b/apps/widget-configurator/src/app/embedDialog/const.ts
@@ -7,12 +7,15 @@ export const PROVIDER_PARAM_COMMENT =
export const COMMENTS_BY_PARAM_NAME: Record = {
appCode: 'Name of your app (max 50 characters)',
- width: 'Width in pixels (or 100% to use all available space)',
+ width: 'Outer iFrame width (use 100% to fill the available container width)',
+ iframeBackgroundColor: 'Background color of the outer iFrame. Default: transparent',
+ iframeBorderRadius: 'Border radius of the outer iFrame. Use 0 for a flush embed',
chainId: '1 (Mainnet), 100 (Gnosis), 11155111 (Sepolia)',
tokenLists: 'All default enabled token lists. Also see https://tokenlists.org',
sellTokenLists: 'Token lists available only in the sell selector',
buyTokenLists: 'Token lists available only in the buy selector',
- theme: 'light/dark or provide your own color palette, plus optional `boxShadow`',
+ theme:
+ 'light/dark or provide your own color palette, plus optional `boxShadow`, `widgetPadding`, and `widgetBorderRadius`',
tradeType: 'swap, limit or advanced',
sell: 'Sell token. Optionally add amount for sell orders',
buy: 'Buy token. Optionally add amount for buy orders',
diff --git a/apps/widget-configurator/src/declarations.d.ts b/apps/widget-configurator/src/declarations.d.ts
new file mode 100644
index 00000000000..eefdc0db9be
--- /dev/null
+++ b/apps/widget-configurator/src/declarations.d.ts
@@ -0,0 +1 @@
+declare module '*.woff2'
diff --git a/apps/widget-configurator/src/main.tsx b/apps/widget-configurator/src/main.tsx
index f6dd2e02b65..cf38a6f39c6 100644
--- a/apps/widget-configurator/src/main.tsx
+++ b/apps/widget-configurator/src/main.tsx
@@ -1,6 +1,7 @@
import { ReactNode, StrictMode, useMemo } from 'react'
import { CowAnalyticsProvider, initGtm } from '@cowprotocol/analytics'
+import FONT_STUDIO_FEIXEN_BOLD from '@cowprotocol/assets/fonts/StudioFeixenSans-Bold.woff2'
import { CssBaseline, GlobalStyles } from '@mui/material'
import Box from '@mui/material/Box'
@@ -17,7 +18,52 @@ import { initWeb3Modal } from './web3modalConfig'
import { WithLDProvider } from './WithLDProvider'
// Initialize analytics instance
-export const cowAnalytics = initGtm()
+const cowAnalytics = initGtm()
+
+const feixenFontStyles = {
+ '@font-face': {
+ fontFamily: 'studiofeixen',
+ src: `url(${FONT_STUDIO_FEIXEN_BOLD}) format('woff2')`,
+ fontStyle: 'normal',
+ fontWeight: 700,
+ fontDisplay: 'swap',
+ },
+}
+
+const configuratorControlStyles = {
+ MuiFormControlLabel: {
+ styleOverrides: {
+ label: {
+ fontSize: '1.4rem',
+ },
+ },
+ },
+ MuiInputBase: {
+ styleOverrides: {
+ input: {
+ fontSize: '1.4rem',
+
+ '&::placeholder': {
+ fontSize: '1.4rem',
+ },
+ },
+ },
+ },
+ MuiInputLabel: {
+ styleOverrides: {
+ root: {
+ fontSize: '1.4rem',
+ },
+ },
+ },
+ MuiFormHelperText: {
+ styleOverrides: {
+ root: {
+ fontSize: '1.2rem',
+ },
+ },
+ },
+}
const WrapperStyled = {
display: 'flex',
@@ -39,6 +85,7 @@ function Root(): ReactNode {
palette,
typography: commonTypography,
components: {
+ ...configuratorControlStyles,
MuiCssBaseline: {
styleOverrides: {
'@global': {
@@ -68,6 +115,7 @@ function Root(): ReactNode {
+
diff --git a/apps/widget-configurator/tsconfig.spec.json b/apps/widget-configurator/tsconfig.spec.json
index ed534ac56e0..a1f9480206d 100644
--- a/apps/widget-configurator/tsconfig.spec.json
+++ b/apps/widget-configurator/tsconfig.spec.json
@@ -3,10 +3,9 @@
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
- "vitest/globals",
- "vitest/importMeta",
- "vite/client",
- "node"
+ "jest",
+ "node",
+ "vite/client"
]
},
"include": [
diff --git a/libs/widget-lib/src/cowSwapWidget.spec.ts b/libs/widget-lib/src/cowSwapWidget.spec.ts
new file mode 100644
index 00000000000..da7d889902c
--- /dev/null
+++ b/libs/widget-lib/src/cowSwapWidget.spec.ts
@@ -0,0 +1,109 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import { createCowSwapWidget } from './cowSwapWidget'
+import { WidgetMethodsEmit } from './types'
+import { widgetIframeTransport } from './widgetIframeTransport'
+
+describe('createCowSwapWidget', () => {
+ beforeEach(() => {
+ document.body.innerHTML = ''
+ })
+
+ it('updates iframe width and default height when params change', () => {
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+
+ const handler = createCowSwapWidget(container, {
+ params: {
+ appCode: 'widget-test',
+ width: '100%',
+ height: '640px',
+ iframeBackgroundColor: 'red',
+ iframeBorderRadius: '1.6rem',
+ },
+ })
+
+ const iframe = getIframe(container)
+
+ expect(iframe.width).toBe('100%')
+ expect(iframe.height).toBe('640px')
+ expect(iframe.style.height).toBe('')
+ expect(iframe.style.backgroundColor).toBe('red')
+ expect(iframe.style.borderRadius).toBe('1.6rem')
+
+ handler.updateParams({
+ appCode: 'widget-test',
+ width: '320px',
+ height: '432px',
+ iframeBackgroundColor: 'transparent',
+ iframeBorderRadius: '0',
+ })
+
+ expect(iframe.width).toBe('320px')
+ expect(iframe.height).toBe('432px')
+ expect(iframe.style.height).toBe('432px')
+ expect(iframe.style.backgroundColor).toBe('transparent')
+ expect(iframe.style.borderRadius).toBe('0')
+ })
+
+ it('uses the latest height config for resize events after params change', () => {
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+
+ const handler = createCowSwapWidget(container, {
+ params: {
+ appCode: 'widget-test',
+ height: '640px',
+ },
+ })
+
+ const iframe = getIframe(container)
+
+ emitWidgetEvent(WidgetMethodsEmit.UPDATE_HEIGHT, { height: 400 })
+ expect(iframe.style.height).toBe('400px')
+
+ handler.updateParams({
+ appCode: 'widget-test',
+ height: '432px',
+ maxHeight: 350,
+ })
+
+ emitWidgetEvent(WidgetMethodsEmit.UPDATE_HEIGHT, { height: 400 })
+ expect(iframe.style.height).toBe('350px')
+
+ emitWidgetEvent(WidgetMethodsEmit.SET_FULL_HEIGHT, { isUpToSmall: true })
+ expect(iframe.style.height).toBe('432px')
+
+ Object.defineProperty(document.body, 'offsetHeight', {
+ configurable: true,
+ value: 900,
+ })
+
+ emitWidgetEvent(WidgetMethodsEmit.SET_FULL_HEIGHT, { isUpToSmall: false })
+ expect(iframe.style.height).toBe('350px')
+ })
+})
+
+function getIframe(container: HTMLElement): HTMLIFrameElement {
+ const iframe = container.querySelector('iframe')
+
+ if (!iframe) {
+ throw new Error('Expected iframe to be created')
+ }
+
+ return iframe
+}
+
+function emitWidgetEvent(method: WidgetMethodsEmit, payload: object): void {
+ window.dispatchEvent(
+ new MessageEvent('message', {
+ data: {
+ key: widgetIframeTransport.key,
+ method,
+ ...payload,
+ },
+ }),
+ )
+}
diff --git a/libs/widget-lib/src/cowSwapWidget.ts b/libs/widget-lib/src/cowSwapWidget.ts
index 2984180cd9d..59fac939ba8 100644
--- a/libs/widget-lib/src/cowSwapWidget.ts
+++ b/libs/widget-lib/src/cowSwapWidget.ts
@@ -21,14 +21,6 @@ import { widgetIframeTransport } from './widgetIframeTransport'
const DEFAULT_HEIGHT = '640px'
const DEFAULT_WIDTH = '450px'
-/**
- * Reference: IframeResizer (apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.ts)
- * Sometimes MutationObserver doesn't trigger when the height of the widget changes and the widget displays with a scrollbar.
- * To avoid this we add a threshold to the height.
- * 20px
- */
-const HEIGHT_THRESHOLD = 20
-
const noopHandler: CowSwapWidgetHandler = {
updateParams: () => void 0,
updateListeners: () => void 0,
@@ -36,6 +28,11 @@ const noopHandler: CowSwapWidgetHandler = {
destroy: () => void 0,
}
+interface IframeSizingConfig {
+ defaultHeight: string
+ maxHeight?: number
+}
+
/**
* Callback function signature for updating the CoW Swap Widget.
*/
@@ -56,6 +53,7 @@ export function createCowSwapWidget(container: HTMLElement, props: CowSwapWidget
const { params, provider: providerAux, listeners } = props
let provider = providerAux
let currentParams = params
+ let iframeSizing = getIframeSizingConfig(params)
if (typeof window === 'undefined') return noopHandler
@@ -77,7 +75,7 @@ export function createCowSwapWidget(container: HTMLElement, props: CowSwapWidget
windowListeners.push(sendAppCodeOnActivation(iframeWindow, params.appCode))
// 4. Handle widget height changes
- windowListeners.push(...listenToHeightChanges(iframe, params.height, params.maxHeight))
+ windowListeners.push(...listenToHeightChanges(iframe, () => iframeSizing))
// 5. Intercept deeplinks navigation in the iframe
windowListeners.push(interceptDeepLinks())
@@ -114,7 +112,12 @@ export function createCowSwapWidget(container: HTMLElement, props: CowSwapWidget
// 11. Return the handler, so the widget, listeners, and provider can be updated
return {
updateParams: (newParams: CowSwapWidgetParams) => {
+ const prevDefaultHeight = iframeSizing.defaultHeight
+
currentParams = newParams
+ iframeSizing = getIframeSizingConfig(newParams)
+
+ updateIframeElement(iframe, currentParams, prevDefaultHeight)
updateParams(iframeWindow, currentParams, provider)
updateWidgetHooks()
},
@@ -187,11 +190,38 @@ function createIframe(params: CowSwapWidgetParams): HTMLIFrameElement {
iframe.width = width
iframe.height = height
iframe.style.border = '0'
+ iframe.style.backgroundColor = params.iframeBackgroundColor || 'transparent'
+ iframe.style.borderRadius = params.iframeBorderRadius || ''
iframe.allow = 'clipboard-read; clipboard-write'
return iframe
}
+function updateIframeElement(
+ iframe: HTMLIFrameElement,
+ params: CowSwapWidgetParams,
+ previousDefaultHeight: string,
+): void {
+ const { width = DEFAULT_WIDTH } = params
+ const { defaultHeight } = getIframeSizingConfig(params)
+
+ iframe.width = width
+ iframe.height = defaultHeight
+ iframe.style.backgroundColor = params.iframeBackgroundColor || 'transparent'
+ iframe.style.borderRadius = params.iframeBorderRadius || ''
+
+ if (!iframe.style.height || iframe.style.height === previousDefaultHeight) {
+ iframe.style.height = defaultHeight
+ }
+}
+
+function getIframeSizingConfig(params: CowSwapWidgetParams): IframeSizingConfig {
+ return {
+ defaultHeight: params.height || DEFAULT_HEIGHT,
+ maxHeight: params.maxHeight,
+ }
+}
+
/**
* Updates the CoW Swap Widget based on the new settings provided.
* @param params - New params for the widget.
@@ -265,18 +295,16 @@ function interceptDeepLinks(): (payload: MessageEvent) => void {
* @param defaultHeight - Default height for the widget.
* @param maxHeight - Maximum height for the widget.
*/
-function listenToHeightChanges(
- iframe: HTMLIFrameElement,
- defaultHeight = DEFAULT_HEIGHT,
- maxHeight?: number,
-): WindowListener[] {
+function listenToHeightChanges(iframe: HTMLIFrameElement, getIframeSizing: () => IframeSizingConfig): WindowListener[] {
return [
widgetIframeTransport.listenToMessageFromWindow(window, WidgetMethodsEmit.UPDATE_HEIGHT, (data) => {
- const newHeight = data.height ? data.height + HEIGHT_THRESHOLD : undefined
+ const { defaultHeight, maxHeight } = getIframeSizing()
+ const newHeight = data.height
iframe.style.height = newHeight ? `${maxHeight ? Math.min(newHeight, maxHeight) : newHeight}px` : defaultHeight
}),
widgetIframeTransport.listenToMessageFromWindow(window, WidgetMethodsEmit.SET_FULL_HEIGHT, ({ isUpToSmall }) => {
+ const { defaultHeight, maxHeight } = getIframeSizing()
iframe.style.height = isUpToSmall ? defaultHeight : `${maxHeight || document.body.offsetHeight}px`
}),
]
diff --git a/libs/widget-lib/src/types.ts b/libs/widget-lib/src/types.ts
index 467ee94847b..120358bcf3c 100644
--- a/libs/widget-lib/src/types.ts
+++ b/libs/widget-lib/src/types.ts
@@ -166,6 +166,16 @@ export type CowSwapWidgetPalette = {
* Accepts any valid CSS box-shadow value, for example `none` or `0 12px 24px rgba(0, 0, 0, 0.12)`.
*/
boxShadow?: string
+ /**
+ * Overrides the outer widget shell padding around the embedded card.
+ * Accepts any valid CSS padding value, for example `16px 16px 24px` or `0`.
+ */
+ widgetPadding?: string
+ /**
+ * Overrides the main widget card border radius.
+ * Accepts any valid CSS border-radius value, for example `24px`, `16px`, or `1.5rem`.
+ */
+ widgetBorderRadius?: string
} & CowSwapWidgetPaletteParams
export interface CowSwapWidgetSounds {
@@ -220,14 +230,28 @@ export interface CowSwapWidgetParams {
appCode: string
/**
- * The width of the widget in pixels. Default: 400px
+ * The width of the outer iframe element. Accepts CSS width values such as `450px` or `100%`.
+ * Default: `450px`
*/
width?: string
/**
- * The height of the widget in pixels. Default: 600px
+ * The height of the outer iframe element. Accepts CSS height values such as `640px`.
+ * Default: `640px`
*/
height?: string
+ /**
+ * The background color of the outer iframe element.
+ * Default: `transparent`
+ */
+ iframeBackgroundColor?: string
+
+ /**
+ * The border radius of the outer iframe element.
+ * Accepts any valid CSS border-radius value, for example `1.6rem`, `24px`, or `0`.
+ */
+ iframeBorderRadius?: string
+
/**
* The maximum height of the widget in pixels. Default: body.offsetHeight
*/
diff --git a/libs/widget-lib/src/urlUtils.spec.ts b/libs/widget-lib/src/urlUtils.spec.ts
index 755465911ab..11510f1b092 100644
--- a/libs/widget-lib/src/urlUtils.spec.ts
+++ b/libs/widget-lib/src/urlUtils.spec.ts
@@ -95,7 +95,7 @@ describe('buildWidgetUrlQuery', () => {
expect(query.get('palette')).toBe('null')
})
- it('serializes widget shadow inside the theme palette', () => {
+ it('serializes widget shell overrides inside the theme palette', () => {
const query = buildWidgetUrlQuery({
theme: {
baseTheme: 'light',
@@ -109,11 +109,17 @@ describe('buildWidgetUrlQuery', () => {
info: '#0d5ed9',
success: '#007B28',
boxShadow: 'none',
+ widgetPadding: '16px 16px 24px',
+ widgetBorderRadius: '32px',
},
})
expect(query.get('theme')).toBe('light')
- expect(JSON.parse(decodeURIComponent(query.get('palette') || ''))).toMatchObject({ boxShadow: 'none' })
+ expect(JSON.parse(decodeURIComponent(query.get('palette') || ''))).toMatchObject({
+ boxShadow: 'none',
+ widgetPadding: '16px 16px 24px',
+ widgetBorderRadius: '32px',
+ })
})
it('includes locale in the iframe URL', () => {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0615183eb7d..ec3f236dcf5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1247,6 +1247,9 @@ importers:
'@cowprotocol/types':
specifier: workspace:*
version: link:../../libs/types
+ '@cowprotocol/ui':
+ specifier: workspace:*
+ version: link:../../libs/ui
'@cowprotocol/widget-lib':
specifier: workspace:*
version: link:../../libs/widget-lib
@@ -1287,6 +1290,9 @@ importers:
specifier: ^15.5.0
version: 15.5.0(react@19.1.2)
devDependencies:
+ '@testing-library/react':
+ specifier: 16.3.0
+ version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.1.3(@types/react@19.1.3))(@types/react@19.1.3)(react-dom@19.1.2(react@19.1.2))(react@19.1.2)
'@types/react':
specifier: 19.1.3
version: 19.1.3
From 4f10af723bccd75abf863b9d6bcc380b3a804f37 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?=
Date: Tue, 7 Apr 2026 12:25:18 +0200
Subject: [PATCH 002/110] feat: add Widget App URL param to configurator
---
.../hooks/useWidgetParamsAndSettings.ts | 5 ++--
.../src/app/configurator/index.tsx | 27 ++++++++++++++-----
.../src/app/embedDialog/const.ts | 7 +++--
.../app/embedDialog/utils/formatParameters.ts | 14 +++++++---
.../embedDialog/utils/sanitizeParameters.ts | 4 +--
5 files changed, 38 insertions(+), 19 deletions(-)
diff --git a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts
index bd25ef3ecb5..f8093c4b773 100644
--- a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts
+++ b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts
@@ -24,7 +24,8 @@ const getBaseUrl = (): string => {
return 'https://swap.cow.fi'
}
-const DEFAULT_BASE_URL = getBaseUrl()
+/** Resolved once at load; used by the configurator preview and as the default `baseUrl` in built params. */
+export const CONFIGURATOR_DEFAULT_WIDGET_BASE_URL = getBaseUrl()
const getTokenListsParam = (
tokenListUrls: ConfiguratorState['tokenListUrls'],
@@ -212,7 +213,7 @@ function buildWidgetParams(configuratorState: ConfiguratorState): CowSwapWidgetP
tokenLists: getTokenListsParam(tokenListUrls, 'enabled'),
sellTokenLists: getTokenListsParam(tokenListUrls, 'enabledForSell'),
buyTokenLists: getTokenListsParam(tokenListUrls, 'enabledForBuy'),
- baseUrl: DEFAULT_BASE_URL,
+ baseUrl: CONFIGURATOR_DEFAULT_WIDGET_BASE_URL,
tradeType: currentTradeType,
sell: { asset: sellToken, amount: sellTokenAmount ? sellTokenAmount.toString() : undefined },
buy: { asset: buyToken, amount: buyTokenAmount?.toString() },
diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/index.tsx
index a51035073ef..c5ff71f7c37 100644
--- a/apps/widget-configurator/src/app/configurator/index.tsx
+++ b/apps/widget-configurator/src/app/configurator/index.tsx
@@ -58,7 +58,7 @@ import { useProvider } from './hooks/useProvider'
import { useResizableDrawerWidth } from './hooks/useResizableDrawerWidth'
import { useSyncWidgetNetwork } from './hooks/useSyncWidgetNetwork'
import { useToastsManager } from './hooks/useToastsManager'
-import { useWidgetParams } from './hooks/useWidgetParamsAndSettings'
+import { CONFIGURATOR_DEFAULT_WIDGET_BASE_URL, useWidgetParams } from './hooks/useWidgetParamsAndSettings'
import {
ContentStyled,
DRAWER_WIDTH_CSS_VAR,
@@ -170,6 +170,7 @@ export function Configurator({ title }: { title: string }) {
const [customImages] = customImagesState
const [customSounds] = customSoundsState
+ const [widgetAppBaseUrl, setWidgetAppBaseUrl] = useState('')
const [rawParams, setRawParams] = useState()
const [isWidgetDisplayed, setIsWidgetDisplayed] = useState(true)
@@ -294,17 +295,20 @@ export function Configurator({ title }: { title: string }) {
}, [rawParams])
const computedParams = useWidgetParams(state)
- const params = useMemo(
- () => ({
+
+ const params = useMemo(() => {
+ const trimmedWidgetAppBaseUrl = widgetAppBaseUrl.trim()
+
+ return {
...computedParams,
images: customImages,
sounds: customSounds,
customTokens,
...rawParamsObject,
+ ...(trimmedWidgetAppBaseUrl ? { baseUrl: trimmedWidgetAppBaseUrl } : null),
...window.cowSwapWidgetParams,
- }),
- [computedParams, customImages, customSounds, customTokens, rawParamsObject],
- )
+ }
+ }, [computedParams, customImages, customSounds, customTokens, rawParamsObject, widgetAppBaseUrl])
const updateWidget = useCallback(() => {
setIsWidgetDisplayed(false)
@@ -510,6 +514,17 @@ export function Configurator({ title }: { title: string }) {
+ setWidgetAppBaseUrl(e.target.value)}
+ size="medium"
+ placeholder={CONFIGURATOR_DEFAULT_WIDGET_BASE_URL}
+ helperText={`Optional. Sets baseUrl (overrides Raw JSON). Default preview URL: ${CONFIGURATOR_DEFAULT_WIDGET_BASE_URL}`}
+ />
= {
partnerFee: 'Partner fee, in Basis Points (BPS) and a receiver address',
hideRecentTokens: 'Hide the Recent section in the token selector',
hideFavoriteTokens: 'Hide the Favorites section in the token selector',
+ baseUrl: 'URL of the CoW Swap app inside the widget iframe (defaults to production if omitted in embed code)',
}
export const COMMENTS_BY_PARAM_NAME_TYPESCRIPT: Record = {
@@ -32,9 +31,9 @@ export const COMMENTS_BY_PARAM_NAME_TYPESCRIPT: Record = {
export const SANITIZE_PARAMS = {
appCode: 'My Cool App',
-}
+} as const
-export const REMOVE_PARAMS: (keyof CowSwapWidgetParams)[] = ['baseUrl']
+export const WIDGET_CONFIGURATOR_DEFAULT_BASE_URL = 'https://swap.cow.fi'
export const REACT_IMPORT_STATEMENT = `import { CowSwapWidget, CowSwapWidgetParams, TradeType }`
export const IMPORT_STATEMENT = `import { createCowSwapWidget, CowSwapWidgetParams, TradeType }`
diff --git a/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.ts b/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.ts
index b37dbbe7734..6b1e48742d0 100644
--- a/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.ts
+++ b/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.ts
@@ -3,7 +3,11 @@ import { CowSwapWidgetParams } from '@cowprotocol/widget-lib'
import { sanitizeParameters } from './sanitizeParameters'
import { ColorPalette } from '../../configurator/types'
-import { COMMENTS_BY_PARAM_NAME, COMMENTS_BY_PARAM_NAME_TYPESCRIPT, REMOVE_PARAMS } from '../const'
+import {
+ COMMENTS_BY_PARAM_NAME,
+ COMMENTS_BY_PARAM_NAME_TYPESCRIPT,
+ WIDGET_CONFIGURATOR_DEFAULT_BASE_URL,
+} from '../const'
export function formatParameters(
params: CowSwapWidgetParams,
@@ -12,9 +16,11 @@ export function formatParameters(
defaultPalette: ColorPalette,
): string {
const paramsSanitized = sanitizeParameters(params, defaultPalette)
- REMOVE_PARAMS.forEach((propName) => {
- delete paramsSanitized[propName]
- })
+
+ // Do not show baseUrl if it's the default value:
+ if (!params.baseUrl || params.baseUrl === WIDGET_CONFIGURATOR_DEFAULT_BASE_URL) {
+ delete paramsSanitized.baseUrl
+ }
// Stringify params
const formattedParams = JSON.stringify(paramsSanitized, null, 4)
diff --git a/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts b/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts
index 1d7133d04f9..55673f69be6 100644
--- a/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts
+++ b/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts
@@ -3,9 +3,7 @@ import { CowSwapWidgetPalette, CowSwapWidgetPaletteColors, CowSwapWidgetParams }
import { ColorPalette } from '../../configurator/types'
import { SANITIZE_PARAMS } from '../const'
-// TODO: Add proper return type annotation
-// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-export function sanitizeParameters(params: CowSwapWidgetParams, defaultPalette: ColorPalette) {
+export function sanitizeParameters(params: CowSwapWidgetParams, defaultPalette: ColorPalette): CowSwapWidgetParams {
return {
...params,
...SANITIZE_PARAMS,
From b9ef0f3635eba6d359fec06defcf2856f6e1dafa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?=
Date: Wed, 8 Apr 2026 16:37:56 +0200
Subject: [PATCH 003/110] feat: various UI improvements on the widget
configurator, plus additional options
---
.../AppContainer/AppContainer.container.tsx | 12 +-
.../TradeWidget/TradeWidgetForm.tsx | 6 +-
.../src/theme/mapWidgetTheme.test.ts | 12 +-
.../src/theme/mapWidgetTheme.ts | 13 +-
apps/widget-configurator/package.json | 1 +
.../src/app/configurator/consts.ts | 8 +-
.../controls/AppearanceStyleControls.tsx | 225 ++++++++++++++++++
.../IframeBackgroundColorControl.test.tsx | 30 ---
.../controls/IframeBackgroundColorControl.tsx | 85 -------
.../IframeBorderRadiusControl.test.tsx | 24 --
.../controls/IframeBorderRadiusControl.tsx | 91 -------
.../controls/IframeWidthControl.test.tsx | 27 ---
.../controls/IframeWidthControl.tsx | 125 ----------
.../WidgetBorderRadiusControl.test.tsx | 24 --
.../controls/WidgetBorderRadiusControl.tsx | 80 -------
.../controls/WidgetPaddingControl.test.tsx | 30 ---
.../controls/WidgetPaddingControl.tsx | 78 ------
.../controls/WidgetShadowControl.test.tsx | 28 ---
.../controls/WidgetShadowControl.tsx | 105 --------
.../app/configurator/hooks/useJsonState.ts | 94 ++++++++
.../hooks/useWidgetParamsAndSettings.ts | 34 +--
.../src/app/configurator/index.tsx | 76 +++---
.../src/app/configurator/styled.ts | 51 ++--
.../src/app/configurator/types.ts | 12 +-
.../src/app/embedDialog/const.ts | 10 +-
libs/common-utils/src/index.ts | 34 +--
libs/common-utils/tsconfig.lib.json | 2 +-
libs/widget-lib/package.json | 2 +
libs/widget-lib/src/applyElementStyles.ts | 28 +++
.../widget-lib/src/cowSwapWidget.constants.ts | 19 ++
libs/widget-lib/src/cowSwapWidget.spec.ts | 24 +-
libs/widget-lib/src/cowSwapWidget.ts | 70 +++---
libs/widget-lib/src/deepMerge.spec.ts | 43 ++++
libs/widget-lib/src/deepMerge.ts | 55 +++++
libs/widget-lib/src/types.ts | 50 ++--
libs/widget-lib/src/urlUtils.spec.ts | 15 +-
pnpm-lock.yaml | 53 ++---
37 files changed, 700 insertions(+), 976 deletions(-)
create mode 100644 apps/widget-configurator/src/app/configurator/controls/AppearanceStyleControls.tsx
delete mode 100644 apps/widget-configurator/src/app/configurator/controls/IframeBackgroundColorControl.test.tsx
delete mode 100644 apps/widget-configurator/src/app/configurator/controls/IframeBackgroundColorControl.tsx
delete mode 100644 apps/widget-configurator/src/app/configurator/controls/IframeBorderRadiusControl.test.tsx
delete mode 100644 apps/widget-configurator/src/app/configurator/controls/IframeBorderRadiusControl.tsx
delete mode 100644 apps/widget-configurator/src/app/configurator/controls/IframeWidthControl.test.tsx
delete mode 100644 apps/widget-configurator/src/app/configurator/controls/IframeWidthControl.tsx
delete mode 100644 apps/widget-configurator/src/app/configurator/controls/WidgetBorderRadiusControl.test.tsx
delete mode 100644 apps/widget-configurator/src/app/configurator/controls/WidgetBorderRadiusControl.tsx
delete mode 100644 apps/widget-configurator/src/app/configurator/controls/WidgetPaddingControl.test.tsx
delete mode 100644 apps/widget-configurator/src/app/configurator/controls/WidgetPaddingControl.tsx
delete mode 100644 apps/widget-configurator/src/app/configurator/controls/WidgetShadowControl.test.tsx
delete mode 100644 apps/widget-configurator/src/app/configurator/controls/WidgetShadowControl.tsx
create mode 100644 apps/widget-configurator/src/app/configurator/hooks/useJsonState.ts
create mode 100644 libs/widget-lib/src/applyElementStyles.ts
create mode 100644 libs/widget-lib/src/cowSwapWidget.constants.ts
create mode 100644 libs/widget-lib/src/deepMerge.spec.ts
create mode 100644 libs/widget-lib/src/deepMerge.ts
diff --git a/apps/cowswap-frontend/src/modules/application/containers/AppContainer/AppContainer.container.tsx b/apps/cowswap-frontend/src/modules/application/containers/AppContainer/AppContainer.container.tsx
index e12daddeb3a..fc596145bb0 100644
--- a/apps/cowswap-frontend/src/modules/application/containers/AppContainer/AppContainer.container.tsx
+++ b/apps/cowswap-frontend/src/modules/application/containers/AppContainer/AppContainer.container.tsx
@@ -1,4 +1,4 @@
-import { type ReactNode, useMemo, useState } from 'react'
+import { type CSSProperties, type ReactNode, useMemo, useState } from 'react'
import { initPixelAnalytics, useAnalyticsReporter, useCowAnalytics, WebVitalsAnalytics } from '@cowprotocol/analytics'
import { useFeatureFlags, useMediaQuery } from '@cowprotocol/common-hooks'
@@ -12,7 +12,7 @@ import { useDarkModeManager } from 'legacy/state/user/hooks'
import { OrdersPanel } from 'modules/account'
import { AffiliateTraderModal } from 'modules/affiliate'
-import { useInjectedWidgetMetaData } from 'modules/injectedWidget'
+import { useInjectedWidgetMetaData, useInjectedWidgetParams } from 'modules/injectedWidget'
import { useSpeechBubbleNotification } from 'modules/notifications'
import { useInitializeUtm } from 'modules/utm'
@@ -77,6 +77,7 @@ export function AppContainer({ children }: AppContainerProps): ReactNode {
useInitializeUtm()
const isInjectedWidgetMode = isInjectedWidget()
+ const { bodyWrapperStyle, appWrapperStyle } = useInjectedWidgetParams()
const [darkMode] = useDarkModeManager()
const [pageBackgroundVariant, setPageBackgroundVariant] = useState('default')
const [pageScene, setPageScene] = useState(null)
@@ -109,7 +110,10 @@ export function AppContainer({ children }: AppContainerProps): ReactNode {
return (
-
+
@@ -120,6 +124,8 @@ export function AppContainer({ children }: AppContainerProps): ReactNode {
{isYieldEnabled && }
{
// eslint-disable-next-line max-lines-per-function, complexity
export function TradeWidgetForm(props: TradeWidgetProps): ReactNode {
const isInjectedWidgetMode = isInjectedWidget()
- const { standaloneMode, hideOrdersTable } = useInjectedWidgetParams()
+ const { standaloneMode, hideOrdersTable, cardStyle } = useInjectedWidgetParams()
const isMobile = useMediaQuery(Media.upToSmall(false))
const tradeTypeInfo = useTradeTypeInfoFromUrl()
@@ -219,7 +219,7 @@ export function TradeWidgetForm(props: TradeWidgetProps): ReactNode {
return (
<>
-
+
{shouldLockForAlternativeOrder ? : }
{isInjectedWidgetMode && standaloneMode && }
diff --git a/apps/cowswap-frontend/src/theme/mapWidgetTheme.test.ts b/apps/cowswap-frontend/src/theme/mapWidgetTheme.test.ts
index 661af28a268..367837052a7 100644
--- a/apps/cowswap-frontend/src/theme/mapWidgetTheme.test.ts
+++ b/apps/cowswap-frontend/src/theme/mapWidgetTheme.test.ts
@@ -5,25 +5,23 @@ import { mapWidgetTheme } from './mapWidgetTheme'
import type { DefaultTheme } from 'styled-components/macro'
describe('mapWidgetTheme', () => {
- it('maps custom widget shadow to the main widget container shadow', () => {
+ it('merges palette colors and maps paper to button text', () => {
const defaultTheme = {
boxShadow1: '0 12px 12px rgba(5, 43, 101, 0.06)',
paper: '#ffffff',
} as DefaultTheme
const widgetTheme: Partial = {
+ baseTheme: 'dark',
paper: '#101010',
- boxShadow: 'none',
- widgetPadding: '16px 16px 24px',
- widgetBorderRadius: '32px',
+ primary: '#ffffff',
}
const result = mapWidgetTheme(widgetTheme, defaultTheme)
expect(result.paper).toBe('#101010')
expect(result.buttonTextCustom).toBe('#101010')
- expect(result.boxShadow1).toBe('none')
- expect(result.widgetPadding).toBe('16px 16px 24px')
- expect(result.widgetBorderRadius).toBe('32px')
+ expect(result.primary).toBe('#ffffff')
+ expect(result.boxShadow1).toBe('0 12px 12px rgba(5, 43, 101, 0.06)')
})
})
diff --git a/apps/cowswap-frontend/src/theme/mapWidgetTheme.ts b/apps/cowswap-frontend/src/theme/mapWidgetTheme.ts
index 01d1a964d5a..3b6324ed893 100644
--- a/apps/cowswap-frontend/src/theme/mapWidgetTheme.ts
+++ b/apps/cowswap-frontend/src/theme/mapWidgetTheme.ts
@@ -2,21 +2,18 @@ import type { CowSwapWidgetPalette } from '@cowprotocol/widget-lib'
import { DefaultTheme } from 'styled-components/macro'
-// Map the provided data from consumer to styled-components theme
+/**
+ * Map the provided data from consumer to styled-components theme
+ */
export function mapWidgetTheme(
widgetTheme: Partial | undefined,
defaultTheme: DefaultTheme,
): DefaultTheme {
if (!widgetTheme) return defaultTheme
- const { boxShadow, widgetPadding, widgetBorderRadius, ...widgetPalette } = widgetTheme
-
return {
...defaultTheme,
- ...widgetPalette,
- ...(widgetPalette.paper ? { buttonTextCustom: widgetPalette.paper } : null),
- ...(boxShadow ? { boxShadow1: boxShadow } : null),
- ...(widgetPadding ? { widgetPadding } : null),
- ...(widgetBorderRadius ? { widgetBorderRadius } : null),
+ ...widgetTheme,
+ ...(widgetTheme.paper ? { buttonTextCustom: widgetTheme.paper } : null),
}
}
diff --git a/apps/widget-configurator/package.json b/apps/widget-configurator/package.json
index 44c2cb06135..e7e3a221233 100644
--- a/apps/widget-configurator/package.json
+++ b/apps/widget-configurator/package.json
@@ -33,6 +33,7 @@
"@cowprotocol/ui": "workspace:*",
"@cowprotocol/widget-lib": "workspace:*",
"@cowprotocol/widget-react": "workspace:*",
+ "csstype": "^3.1.3",
"@mui/icons-material": "^5.17.1",
"@mui/material": "^5.17.1",
"@web3modal/ethers5": "^4.1.9",
diff --git a/apps/widget-configurator/src/app/configurator/consts.ts b/apps/widget-configurator/src/app/configurator/consts.ts
index 0f8c0231461..5fe43cb9a67 100644
--- a/apps/widget-configurator/src/app/configurator/consts.ts
+++ b/apps/widget-configurator/src/app/configurator/consts.ts
@@ -57,18 +57,14 @@ export const DEFAULT_DARK_PALETTE: CowSwapWidgetPaletteParams = {
success: '#00D897',
}
+// TODO: Make it possible to define per-theme props in the style properties.
+// TODO: This is not used.
// Keep in sync with the widget theme defaults from libs/ui/src/colors.ts
export const DEFAULT_WIDGET_SHADOW = {
light: '0 12px 12px rgba(5, 43, 101, 0.06)',
dark: '0 24px 32px rgba(0, 0, 0, 0.06)',
} as const
-export const DEFAULT_IFRAME_WIDTH = '100%'
-export const MIN_IFRAME_WIDTH_PX = 360
-export const DEFAULT_IFRAME_BACKGROUND_COLOR = 'transparent'
-export const DEFAULT_IFRAME_BORDER_RADIUS = '1.6rem'
-export const DEFAULT_WIDGET_BORDER_RADIUS = '24px'
-
export const COW_LISTENERS: CowWidgetEventListeners = [
{
event: CowWidgetEvents.ON_TOAST_MESSAGE,
diff --git a/apps/widget-configurator/src/app/configurator/controls/AppearanceStyleControls.tsx b/apps/widget-configurator/src/app/configurator/controls/AppearanceStyleControls.tsx
new file mode 100644
index 00000000000..84d69f597c8
--- /dev/null
+++ b/apps/widget-configurator/src/app/configurator/controls/AppearanceStyleControls.tsx
@@ -0,0 +1,225 @@
+import type { ChangeEvent, ReactNode } from 'react'
+
+import Box from '@mui/material/Box'
+import Stack from '@mui/material/Stack'
+import TextField from '@mui/material/TextField'
+import Typography from '@mui/material/Typography'
+
+import type { JsonState, OnJsonStateChange } from '../hooks/useJsonState'
+import type * as CSS from 'csstype'
+
+export interface AppearanceStyleControlsProps {
+ iframeStyleJson: JsonState
+ onIframeStyleJson: OnJsonStateChange
+ appWrapperStyleJson: JsonState
+ onAppWrapperStyleJson: OnJsonStateChange
+ bodyWrapperStyleJson: JsonState
+ onBodyWrapperStyleJson: OnJsonStateChange
+ cardStyleJson: JsonState
+ onCardStyleJson: OnJsonStateChange
+}
+
+function jsonHelperText(hasError: boolean): string {
+ return hasError ? 'Invalid JSON.' : 'Optional. CamelCase CSS properties as JSON, e.g. {"padding": "12px"}'
+}
+
+// eslint-disable-next-line max-lines-per-function
+export function AppearanceStyleControls({
+ iframeStyleJson,
+ onIframeStyleJson,
+ appWrapperStyleJson,
+ onAppWrapperStyleJson,
+ bodyWrapperStyleJson,
+ onBodyWrapperStyleJson,
+ cardStyleJson,
+ onCardStyleJson,
+}: AppearanceStyleControlsProps): ReactNode {
+ // Individual fields change handlers:
+ const handleIframeFieldChange = (e: ChangeEvent): void => {
+ onIframeStyleJson(e.target.name.split('.')[1] as keyof CSS.Properties, e.target.value)
+ }
+ const handleAppWrapperFieldChange = (e: ChangeEvent): void => {
+ onAppWrapperStyleJson(e.target.name.split('.')[1] as keyof CSS.Properties, e.target.value)
+ }
+ const handleBodyWrapperFieldChange = (e: ChangeEvent): void => {
+ onBodyWrapperStyleJson(e.target.name.split('.')[1] as keyof CSS.Properties, e.target.value)
+ }
+ const handleCardFieldChange = (e: ChangeEvent): void => {
+ onCardStyleJson(e.target.name.split('.')[1] as keyof CSS.Properties, e.target.value)
+ }
+
+ // JSON fields change handlers:
+ const handleIframeJsonChange = (e: ChangeEvent): void => {
+ onIframeStyleJson(null, e.target.value)
+ }
+ const handleBodyWrapperJsonChange = (e: ChangeEvent): void => {
+ onBodyWrapperStyleJson(null, e.target.value)
+ }
+ const handleAppWrapperJsonChange = (e: ChangeEvent): void => {
+ onAppWrapperStyleJson(null, e.target.value)
+ }
+ const handleCardJsonChange = (e: ChangeEvent): void => {
+ onCardStyleJson(null, e.target.value)
+ }
+
+ return (
+
+
+
+ Iframe (host)
+
+
+
+
+
+
+
+
+
+
+ #appWrapper (inside iframe)
+
+
+
+
+
+
+
+
+ #bodyWrapper (inside iframe)
+
+
+
+
+
+
+
+
+ #card (inside iframe)
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/widget-configurator/src/app/configurator/controls/IframeBackgroundColorControl.test.tsx b/apps/widget-configurator/src/app/configurator/controls/IframeBackgroundColorControl.test.tsx
deleted file mode 100644
index d73a3b975eb..00000000000
--- a/apps/widget-configurator/src/app/configurator/controls/IframeBackgroundColorControl.test.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { fireEvent, render, screen } from '@testing-library/react'
-
-import { IframeBackgroundColorControl } from './IframeBackgroundColorControl'
-
-jest.mock('mui-color-input', () => ({
- MuiColorInput: (props: { label: string; onChange: (value: string) => void; value: string }) => (
- props.onChange(event.target.value)} />
- ),
-}))
-
-describe('IframeBackgroundColorControl', () => {
- it('adds tooltip text explaining what the iFrame background color changes', () => {
- render()
-
- const helpButton = screen.getByLabelText('Explain iFrame background color')
-
- expect(helpButton.getAttribute('title')).toContain('outer iFrame element')
- expect(helpButton.getAttribute('title')).toContain('transparent')
- })
-
- it('seeds the custom color when enabling a custom iFrame background', () => {
- const onChange = jest.fn()
-
- render()
-
- fireEvent.click(screen.getByRole('button', { name: /custom/i }))
-
- expect(onChange).toHaveBeenCalledWith('#ffffff')
- })
-})
diff --git a/apps/widget-configurator/src/app/configurator/controls/IframeBackgroundColorControl.tsx b/apps/widget-configurator/src/app/configurator/controls/IframeBackgroundColorControl.tsx
deleted file mode 100644
index 391dbdb7cac..00000000000
--- a/apps/widget-configurator/src/app/configurator/controls/IframeBackgroundColorControl.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import { ReactNode, useEffect, useState } from 'react'
-
-import Button from '@mui/material/Button'
-import Collapse from '@mui/material/Collapse'
-import Stack from '@mui/material/Stack'
-import Typography from '@mui/material/Typography'
-import { MuiColorInput } from 'mui-color-input'
-
-import { SettingHeading } from './SettingHeading'
-
-import { DEFAULT_IFRAME_BACKGROUND_COLOR } from '../consts'
-
-const TOOLTIP_TITLE =
- 'Sets the background color on the outer iFrame element. Default is transparent, so the host page shows through.'
-
-type IframeBackgroundColorMode = 'default' | 'custom'
-
-interface IframeBackgroundColorControlProps {
- value: string
- defaultCustomColor: string
- onChange(value: string): void
-}
-
-export function IframeBackgroundColorControl({
- value,
- defaultCustomColor,
- onChange,
-}: IframeBackgroundColorControlProps): ReactNode {
- const [colorMode, setColorMode] = useState(value ? 'custom' : 'default')
-
- useEffect(() => {
- setColorMode(value ? 'custom' : 'default')
- }, [value])
-
- const handleModeSelect = (nextMode: IframeBackgroundColorMode): void => {
- setColorMode(nextMode)
-
- if (nextMode === 'default') {
- onChange('')
-
- return
- }
-
- if (!value) {
- onChange(defaultCustomColor)
- }
- }
-
- return (
-
-
-
- Default: {DEFAULT_IFRAME_BACKGROUND_COLOR}
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/apps/widget-configurator/src/app/configurator/controls/IframeBorderRadiusControl.test.tsx b/apps/widget-configurator/src/app/configurator/controls/IframeBorderRadiusControl.test.tsx
deleted file mode 100644
index ab90ae2da15..00000000000
--- a/apps/widget-configurator/src/app/configurator/controls/IframeBorderRadiusControl.test.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { fireEvent, render, screen } from '@testing-library/react'
-
-import { IframeBorderRadiusControl } from './IframeBorderRadiusControl'
-
-describe('IframeBorderRadiusControl', () => {
- it('adds tooltip text explaining what the iFrame border radius changes', () => {
- render()
-
- const helpButton = screen.getByLabelText('Explain iFrame border radius')
-
- expect(helpButton.getAttribute('title')).toContain('outer iFrame element')
- expect(helpButton.getAttribute('title')).toContain('inner widget card radius')
- })
-
- it('seeds a flush embed radius when enabling a custom iFrame radius', () => {
- const onChange = jest.fn()
-
- render()
-
- fireEvent.click(screen.getByRole('button', { name: /custom/i }))
-
- expect(onChange).toHaveBeenCalledWith('0')
- })
-})
diff --git a/apps/widget-configurator/src/app/configurator/controls/IframeBorderRadiusControl.tsx b/apps/widget-configurator/src/app/configurator/controls/IframeBorderRadiusControl.tsx
deleted file mode 100644
index 48a70d9882d..00000000000
--- a/apps/widget-configurator/src/app/configurator/controls/IframeBorderRadiusControl.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import { ReactNode, useEffect, useState } from 'react'
-
-import Button from '@mui/material/Button'
-import Collapse from '@mui/material/Collapse'
-import Stack from '@mui/material/Stack'
-import TextField from '@mui/material/TextField'
-import Typography from '@mui/material/Typography'
-
-import { SettingHeading } from './SettingHeading'
-
-import { DEFAULT_IFRAME_BORDER_RADIUS } from '../consts'
-
-const TOOLTIP_TITLE =
- 'Controls the border radius of the outer iFrame element. It does not change the inner widget card radius.'
-
-type IframeBorderRadiusMode = 'default' | 'custom'
-
-interface IframeBorderRadiusControlProps {
- value: string
- onChange(value: string): void
-}
-
-function getRadiusMode(value: string): IframeBorderRadiusMode {
- return value === DEFAULT_IFRAME_BORDER_RADIUS ? 'default' : 'custom'
-}
-
-export function IframeBorderRadiusControl({ value, onChange }: IframeBorderRadiusControlProps): ReactNode {
- const [radiusMode, setRadiusMode] = useState(() => getRadiusMode(value))
-
- useEffect(() => {
- setRadiusMode(getRadiusMode(value))
- }, [value])
-
- const handleModeSelect = (nextMode: IframeBorderRadiusMode): void => {
- setRadiusMode(nextMode)
-
- if (nextMode === 'default') {
- onChange(DEFAULT_IFRAME_BORDER_RADIUS)
-
- return
- }
-
- if (!value || value === DEFAULT_IFRAME_BORDER_RADIUS) {
- onChange('0')
- }
- }
-
- const handleBlur = (): void => {
- if (radiusMode === 'custom' && !value.trim()) {
- onChange('0')
- }
- }
-
- return (
-
-
-
- Default: {DEFAULT_IFRAME_BORDER_RADIUS}
-
-
-
-
-
-
- onChange(event.target.value)}
- />
-
-
- )
-}
diff --git a/apps/widget-configurator/src/app/configurator/controls/IframeWidthControl.test.tsx b/apps/widget-configurator/src/app/configurator/controls/IframeWidthControl.test.tsx
deleted file mode 100644
index fd54918ff4a..00000000000
--- a/apps/widget-configurator/src/app/configurator/controls/IframeWidthControl.test.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { fireEvent, render, screen } from '@testing-library/react'
-
-import { IframeWidthControl } from './IframeWidthControl'
-
-describe('IframeWidthControl', () => {
- it('shows tooltip text explaining what iFrame width controls', () => {
- render()
-
- const helpButton = screen.getByLabelText('Explain iFrame width')
-
- fireEvent.click(helpButton)
-
- expect(screen.getByRole('tooltip')).not.toBeNull()
- expect(screen.getByText(/outer iFrame element/i)).not.toBeNull()
- expect(screen.getByText(/100% of the available container width/i)).not.toBeNull()
- })
-
- it('seeds the minimum width when enabling a custom iFrame width', () => {
- const onChange = jest.fn()
-
- render()
-
- fireEvent.click(screen.getByRole('button', { name: /custom/i }))
-
- expect(onChange).toHaveBeenCalledWith('360px')
- })
-})
diff --git a/apps/widget-configurator/src/app/configurator/controls/IframeWidthControl.tsx b/apps/widget-configurator/src/app/configurator/controls/IframeWidthControl.tsx
deleted file mode 100644
index ffc1c994d4e..00000000000
--- a/apps/widget-configurator/src/app/configurator/controls/IframeWidthControl.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-import { ChangeEvent, ReactNode, useEffect, useState } from 'react'
-
-import Button from '@mui/material/Button'
-import Collapse from '@mui/material/Collapse'
-import Stack from '@mui/material/Stack'
-import TextField from '@mui/material/TextField'
-import Typography from '@mui/material/Typography'
-
-import { SettingHeading } from './SettingHeading'
-
-import { DEFAULT_IFRAME_WIDTH, MIN_IFRAME_WIDTH_PX } from '../consts'
-
-const TOOLTIP_TITLE =
- 'Controls the width of the outer iFrame element. Default uses 100% of the available container width.'
-
-type IframeWidthMode = 'default' | 'custom'
-
-interface IframeWidthControlProps {
- value: string
- onChange(value: string): void
-}
-
-function getWidthInputValue(value: string): string {
- return value.replace(/px$/, '')
-}
-
-export function IframeWidthControl({ value, onChange }: IframeWidthControlProps): ReactNode {
- const [widthMode, setWidthMode] = useState(value ? 'custom' : 'default')
- const [customWidth, setCustomWidth] = useState(() => getWidthInputValue(value))
-
- useEffect(() => {
- setWidthMode(value ? 'custom' : 'default')
- setCustomWidth(getWidthInputValue(value))
- }, [value])
-
- const handleModeSelect = (nextMode: IframeWidthMode): void => {
- setWidthMode(nextMode)
-
- if (nextMode === 'default') {
- onChange('')
-
- return
- }
-
- if (!value) {
- const nextWidth = String(MIN_IFRAME_WIDTH_PX)
-
- setCustomWidth(nextWidth)
- onChange(`${nextWidth}px`)
- }
- }
-
- const handleWidthChange = (event: ChangeEvent): void => {
- const nextWidth = event.target.value
- setCustomWidth(nextWidth)
-
- const parsedWidth = Number(nextWidth)
-
- if (!nextWidth || Number.isNaN(parsedWidth) || parsedWidth < MIN_IFRAME_WIDTH_PX) {
- return
- }
-
- onChange(`${Math.round(parsedWidth)}px`)
- }
-
- const handleBlur = (): void => {
- if (widthMode !== 'custom') {
- return
- }
-
- const parsedWidth = Number(customWidth)
- const normalizedWidth =
- !customWidth || Number.isNaN(parsedWidth)
- ? MIN_IFRAME_WIDTH_PX
- : Math.max(MIN_IFRAME_WIDTH_PX, Math.round(parsedWidth))
-
- setCustomWidth(String(normalizedWidth))
- onChange(`${normalizedWidth}px`)
- }
-
- const helperText =
- customWidth && Number(customWidth) < MIN_IFRAME_WIDTH_PX
- ? `Minimum custom width is ${MIN_IFRAME_WIDTH_PX}px.`
- : 'Uses a fixed pixel width for the outer iFrame.'
-
- return (
-
-
-
- Default: {DEFAULT_IFRAME_WIDTH}
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/apps/widget-configurator/src/app/configurator/controls/WidgetBorderRadiusControl.test.tsx b/apps/widget-configurator/src/app/configurator/controls/WidgetBorderRadiusControl.test.tsx
deleted file mode 100644
index ed14b45c6ac..00000000000
--- a/apps/widget-configurator/src/app/configurator/controls/WidgetBorderRadiusControl.test.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { fireEvent, render, screen } from '@testing-library/react'
-
-import { WidgetBorderRadiusControl } from './WidgetBorderRadiusControl'
-
-describe('WidgetBorderRadiusControl', () => {
- it('adds tooltip text explaining what the widget corner radius changes', () => {
- render()
-
- const helpButton = screen.getByLabelText('Explain Widget corner radius')
-
- expect(helpButton.getAttribute('title')).toContain('main inner widget card')
- expect(helpButton.getAttribute('title')).toContain('outer iFrame')
- })
-
- it('seeds the default radius when enabling a custom widget radius', () => {
- const onChange = jest.fn()
-
- render()
-
- fireEvent.click(screen.getByRole('button', { name: /custom/i }))
-
- expect(onChange).toHaveBeenCalledWith('24px')
- })
-})
diff --git a/apps/widget-configurator/src/app/configurator/controls/WidgetBorderRadiusControl.tsx b/apps/widget-configurator/src/app/configurator/controls/WidgetBorderRadiusControl.tsx
deleted file mode 100644
index 4d7f2b1751a..00000000000
--- a/apps/widget-configurator/src/app/configurator/controls/WidgetBorderRadiusControl.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { ReactNode, useEffect, useState } from 'react'
-
-import Button from '@mui/material/Button'
-import Collapse from '@mui/material/Collapse'
-import Stack from '@mui/material/Stack'
-import TextField from '@mui/material/TextField'
-import Typography from '@mui/material/Typography'
-
-import { SettingHeading } from './SettingHeading'
-
-import { DEFAULT_WIDGET_BORDER_RADIUS } from '../consts'
-
-const TOOLTIP_TITLE =
- 'Overrides the border radius of the main inner widget card. It does not change the outer iFrame border radius.'
-
-type WidgetBorderRadiusMode = 'default' | 'custom'
-
-interface WidgetBorderRadiusControlProps {
- value: string
- onChange(value: string): void
-}
-
-export function WidgetBorderRadiusControl({ value, onChange }: WidgetBorderRadiusControlProps): ReactNode {
- const [radiusMode, setRadiusMode] = useState(value ? 'custom' : 'default')
-
- useEffect(() => {
- setRadiusMode(value ? 'custom' : 'default')
- }, [value])
-
- const handleModeSelect = (nextMode: WidgetBorderRadiusMode): void => {
- setRadiusMode(nextMode)
-
- if (nextMode === 'default') {
- onChange('')
-
- return
- }
-
- if (!value) {
- onChange(DEFAULT_WIDGET_BORDER_RADIUS)
- }
- }
-
- return (
-
-
-
- Default: {DEFAULT_WIDGET_BORDER_RADIUS}
-
-
-
-
-
-
- onChange(event.target.value)}
- />
-
-
- )
-}
diff --git a/apps/widget-configurator/src/app/configurator/controls/WidgetPaddingControl.test.tsx b/apps/widget-configurator/src/app/configurator/controls/WidgetPaddingControl.test.tsx
deleted file mode 100644
index 9b7165cf865..00000000000
--- a/apps/widget-configurator/src/app/configurator/controls/WidgetPaddingControl.test.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { fireEvent, render, screen } from '@testing-library/react'
-
-import { WidgetPaddingControl } from './WidgetPaddingControl'
-
-const DEFAULT_WIDGET_PADDING = '16px 16px 24px'
-
-describe('WidgetPaddingControl', () => {
- it('reveals the custom input only when custom mode is enabled', () => {
- const onChange = jest.fn()
-
- render()
-
- expect(screen.queryByLabelText('Custom widget padding')).toBeNull()
-
- fireEvent.click(screen.getByRole('button', { name: 'Custom' }))
-
- expect(onChange).toHaveBeenCalledWith(DEFAULT_WIDGET_PADDING)
- expect(screen.getByLabelText('Custom widget padding')).not.toBeNull()
- })
-
- it('resets back to the default state', () => {
- const onChange = jest.fn()
-
- render()
-
- fireEvent.click(screen.getByRole('button', { name: 'Default' }))
-
- expect(onChange).toHaveBeenCalledWith('')
- })
-})
diff --git a/apps/widget-configurator/src/app/configurator/controls/WidgetPaddingControl.tsx b/apps/widget-configurator/src/app/configurator/controls/WidgetPaddingControl.tsx
deleted file mode 100644
index 91ed1ef6fec..00000000000
--- a/apps/widget-configurator/src/app/configurator/controls/WidgetPaddingControl.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import { ReactNode, useEffect, useState } from 'react'
-
-import Box from '@mui/material/Box'
-import Button from '@mui/material/Button'
-import Collapse from '@mui/material/Collapse'
-import Stack from '@mui/material/Stack'
-import TextField from '@mui/material/TextField'
-import Typography from '@mui/material/Typography'
-
-const DEFAULT_WIDGET_PADDING = '16px 16px 24px'
-
-type PaddingMode = 'default' | 'custom'
-
-interface WidgetPaddingControlProps {
- value: string
- onChange(value: string): void
-}
-
-export function WidgetPaddingControl({ value, onChange }: WidgetPaddingControlProps): ReactNode {
- const [paddingMode, setPaddingMode] = useState(value ? 'custom' : 'default')
-
- useEffect(() => {
- setPaddingMode(value ? 'custom' : 'default')
- }, [value])
-
- const handleModeSelect = (nextMode: PaddingMode): void => {
- setPaddingMode(nextMode)
-
- if (nextMode === 'default') {
- onChange('')
-
- return
- }
-
- if (!value) {
- onChange(DEFAULT_WIDGET_PADDING)
- }
- }
-
- return (
-
-
- Widget padding
-
-
- Default: {DEFAULT_WIDGET_PADDING}
-
-
-
-
-
-
- onChange(event.target.value)}
- />
-
-
- )
-}
diff --git a/apps/widget-configurator/src/app/configurator/controls/WidgetShadowControl.test.tsx b/apps/widget-configurator/src/app/configurator/controls/WidgetShadowControl.test.tsx
deleted file mode 100644
index 900b4c54488..00000000000
--- a/apps/widget-configurator/src/app/configurator/controls/WidgetShadowControl.test.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { fireEvent, render, screen } from '@testing-library/react'
-
-import { WidgetShadowControl } from './WidgetShadowControl'
-
-describe('WidgetShadowControl', () => {
- it('shows the theme default shadow and supports the none option', () => {
- const onChange = jest.fn()
-
- render()
-
- expect(screen.getByText(/theme default: 0 12px 12px rgba\(5, 43, 101, 0.06\)/i)).not.toBeNull()
-
- fireEvent.click(screen.getByRole('button', { name: 'None' }))
-
- expect(onChange).toHaveBeenCalledWith('none')
- })
-
- it('reveals the custom input and seeds it from the theme default when needed', () => {
- const onChange = jest.fn()
-
- render()
-
- fireEvent.click(screen.getByRole('button', { name: 'Custom' }))
-
- expect(onChange).toHaveBeenCalledWith('0 24px 32px rgba(0, 0, 0, 0.06)')
- expect(screen.getByLabelText('Custom widget shadow')).not.toBeNull()
- })
-})
diff --git a/apps/widget-configurator/src/app/configurator/controls/WidgetShadowControl.tsx b/apps/widget-configurator/src/app/configurator/controls/WidgetShadowControl.tsx
deleted file mode 100644
index a1894e3ad8a..00000000000
--- a/apps/widget-configurator/src/app/configurator/controls/WidgetShadowControl.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import { ReactNode, useEffect, useMemo, useState } from 'react'
-
-import { PaletteMode } from '@mui/material'
-import Box from '@mui/material/Box'
-import Button from '@mui/material/Button'
-import Collapse from '@mui/material/Collapse'
-import Stack from '@mui/material/Stack'
-import TextField from '@mui/material/TextField'
-import Typography from '@mui/material/Typography'
-
-import { DEFAULT_WIDGET_SHADOW } from '../consts'
-
-type ShadowMode = 'default' | 'none' | 'custom'
-
-interface WidgetShadowControlProps {
- value: string
- mode: PaletteMode
- onChange(value: string): void
-}
-
-function getThemeDefaultShadow(mode: PaletteMode): string {
- return mode === 'dark' ? DEFAULT_WIDGET_SHADOW.dark : DEFAULT_WIDGET_SHADOW.light
-}
-
-function getShadowMode(value: string): ShadowMode {
- if (!value) return 'default'
- if (value === 'none') return 'none'
-
- return 'custom'
-}
-
-export function WidgetShadowControl({ value, mode, onChange }: WidgetShadowControlProps): ReactNode {
- const themeDefaultShadow = useMemo(() => getThemeDefaultShadow(mode), [mode])
- const [shadowMode, setShadowMode] = useState(() => getShadowMode(value))
-
- useEffect(() => {
- setShadowMode(getShadowMode(value))
- }, [value])
-
- const handleModeSelect = (nextMode: ShadowMode): void => {
- setShadowMode(nextMode)
-
- if (nextMode === 'default') {
- onChange('')
-
- return
- }
-
- if (nextMode === 'none') {
- onChange('none')
-
- return
- }
-
- if (!value || value === 'none') {
- onChange(themeDefaultShadow)
- }
- }
-
- return (
-
-
- Widget shadow
-
-
- Theme default: {themeDefaultShadow}
-
-
-
-
-
-
-
- onChange(event.target.value)}
- />
-
-
- )
-}
diff --git a/apps/widget-configurator/src/app/configurator/hooks/useJsonState.ts b/apps/widget-configurator/src/app/configurator/hooks/useJsonState.ts
new file mode 100644
index 00000000000..0d81b0cffc4
--- /dev/null
+++ b/apps/widget-configurator/src/app/configurator/hooks/useJsonState.ts
@@ -0,0 +1,94 @@
+import { useCallback, useState } from 'react'
+
+export const EMPTY_JSON_STATE: InitialJsonState
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
diff --git a/apps/widget-configurator/src/app/configurator/styled.ts b/apps/widget-configurator/src/app/configurator/styled.ts
index e8bff3b2bee..549638d8748 100644
--- a/apps/widget-configurator/src/app/configurator/styled.ts
+++ b/apps/widget-configurator/src/app/configurator/styled.ts
@@ -1,4 +1,4 @@
-import { Theme } from '@mui/material/styles'
+import type { SxProps, Theme } from '@mui/material/styles'
export const DRAWER_WIDTH_CSS_VAR = '--widget-configurator-drawer-width'
@@ -29,26 +29,45 @@ export const DrawerStyled = (theme: Theme) => ({
padding: '1.6rem',
position: 'relative',
overflow: 'hidden',
- overflowY: 'auto',
+ overflowY: 'scroll',
},
})
-export const ContentStyled = {
- width: 0,
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- flexFlow: 'column',
- flex: '1 1 auto',
- minWidth: 0,
- overflow: 'auto',
- padding: '2rem 1.6rem',
+const TRANSPARENCY_CHECKER_PX = 8
+const CONTENT_PADDING_PX = 16
- '& iframe': {
- border: 0,
- margin: '0 auto',
+export const ContentStyled: SxProps = (theme) => {
+ const isDark = theme.palette.mode === 'dark'
+ const squareA = theme.palette.grey[isDark ? 900 : 200]
+ const squareB = theme.palette.grey[isDark ? 800 : 300]
+ const base = theme.palette.grey[isDark ? 900 : 200]
+ const pattern = `repeating-conic-gradient(from 90deg, ${squareA} 0% 25%, ${squareB} 0% 50%)`
+
+ return {
+ width: 0,
+ display: 'flex',
+ justifyContent: 'flex-start',
+ alignItems: 'flex-start',
+ flexFlow: 'column',
+ flex: '1 1 auto',
+ minWidth: 0,
overflow: 'auto',
- },
+ padding: `${CONTENT_PADDING_PX}px`,
+ backgroundImage: `${pattern}, linear-gradient(${base}, ${base})`,
+ backgroundSize: `${TRANSPARENCY_CHECKER_PX}px ${TRANSPARENCY_CHECKER_PX}px, 100% 100%`,
+ backgroundRepeat: 'repeat, no-repeat',
+ backgroundPosition: `right ${CONTENT_PADDING_PX}px top ${CONTENT_PADDING_PX}px, 0 0`,
+ backgroundClip: 'content-box, border-box',
+ backgroundOrigin: 'content-box, border-box',
+
+ '& iframe': {
+ display: 'block',
+ border: 0,
+ margin: '0 auto',
+ outline: '1px dashed cyan',
+ //overflow: 'auto',
+ },
+ }
}
export const WalletConnectionWrapper = {
diff --git a/apps/widget-configurator/src/app/configurator/types.ts b/apps/widget-configurator/src/app/configurator/types.ts
index 79f7afd2586..a30f6b1f0b2 100644
--- a/apps/widget-configurator/src/app/configurator/types.ts
+++ b/apps/widget-configurator/src/app/configurator/types.ts
@@ -9,6 +9,8 @@ import {
import { PaletteMode } from '@mui/material'
+import type * as CSS from 'csstype'
+
export type ColorPalette = {
[key in CowSwapWidgetPaletteColors]: string
}
@@ -24,12 +26,10 @@ export interface ConfiguratorState {
chainId?: SupportedChainId
locale?: string
theme: PaletteMode
- iframeWidth?: string
- iframeBackgroundColor?: string
- iframeBorderRadius?: string
- boxShadow?: string
- widgetPadding?: string
- widgetBorderRadius?: string
+ iframeStyle: CSS.Properties
+ appWrapperStyle: CSS.Properties
+ bodyWrapperStyle: CSS.Properties
+ cardStyle: CSS.Properties
currentTradeType: TradeType
enabledTradeTypes: TradeType[]
enabledWidgetHooks: WidgetHookEvents[]
diff --git a/apps/widget-configurator/src/app/embedDialog/const.ts b/apps/widget-configurator/src/app/embedDialog/const.ts
index 23c78334ff2..5a02d2cfe75 100644
--- a/apps/widget-configurator/src/app/embedDialog/const.ts
+++ b/apps/widget-configurator/src/app/embedDialog/const.ts
@@ -6,14 +6,16 @@ export const PROVIDER_PARAM_COMMENT =
export const COMMENTS_BY_PARAM_NAME: Record = {
appCode: 'Name of your app (max 50 characters)',
width: 'Outer iFrame width (use 100% to fill the available container width)',
- iframeBackgroundColor: 'Background color of the outer iFrame. Default: transparent',
- iframeBorderRadius: 'Border radius of the outer iFrame. Use 0 for a flush embed',
chainId: '1 (Mainnet), 100 (Gnosis), 11155111 (Sepolia)',
tokenLists: 'All default enabled token lists. Also see https://tokenlists.org',
sellTokenLists: 'Token lists available only in the sell selector',
buyTokenLists: 'Token lists available only in the buy selector',
- theme:
- 'light/dark or provide your own color palette, plus optional `boxShadow`, `widgetPadding`, and `widgetBorderRadius`',
+ theme: 'light/dark or provide your own color palette',
+ iframeStyle:
+ 'Host iframe CSS (e.g. backgroundColor, borderRadius, boxShadow, border). Width/height use top-level width & height.',
+ appWrapperStyle: 'Optional inline styles on the top-level app wrapper (inside the iframe)',
+ bodyWrapperStyle: 'Optional inline styles on the body wrapper (inside the iframe)',
+ cardStyle: 'Optional inline styles on the main trade widget card (inside the iframe)',
tradeType: 'swap, limit or advanced',
sell: 'Sell token. Optionally add amount for sell orders',
buy: 'Buy token. Optionally add amount for buy orders',
diff --git a/libs/common-utils/src/index.ts b/libs/common-utils/src/index.ts
index 618c450ef5e..08701792a80 100644
--- a/libs/common-utils/src/index.ts
+++ b/libs/common-utils/src/index.ts
@@ -1,29 +1,38 @@
+export { default as formatLocaleNumber } from './formatLocaleNumber'
+export { getAvailableDestinationChains } from './getAvailableDestinationChains'
export * from './address'
export * from './amountFormat/index'
export * from './anonymizeLink'
export * from './areFractionsEqual'
+export * from './areSetsEqual'
export * from './async'
export * from './buildPriceFromCurrencyAmounts'
+export * from './cache'
export * from './calculateGasMargin'
export * from './capitalizeFirstLetter'
+export * from './clamp-value'
export * from './contenthashToUri'
-export * from './currencyAmountToTokenAmount'
+export * from './cowProtocolContracts'
export * from './currencyAmountToString'
+export * from './currencyAmountToTokenAmount'
export * from './deepEqual'
export * from './displayTime'
export * from './doesTokenMatchSymbolOrAddress'
export * from './env'
export * from './environments'
+export * from './errors'
export * from './explorer'
export * from './featureFlags'
+export * from './fetch'
export * from './format'
-export { default as formatLocaleNumber } from './formatLocaleNumber'
export * from './fractionUtils'
export * from './genericPropsChecker'
export * from './getAddress'
export * from './getAvailableChains'
export * from './getChainIdImmediately'
+export * from './getCurrencyAddress'
export * from './getCurrentChainIdFromUrl'
+export * from './getDeprecatedChains'
export * from './getExplorerLink'
export * from './getIntOrFloat'
export * from './getIsNativeToken'
@@ -31,16 +40,18 @@ export * from './getIsWrapOrUnwrap'
export * from './getQuoteUnsupportedToken'
export * from './getRandomInt'
export * from './getWrappedToken'
+export * from './i18n'
export * from './isEnoughAmount'
export * from './isFractionFalsy'
-export * from './isInjectedWidget'
export * from './isIframe'
+export * from './isInjectedWidget'
export * from './isSellOrder'
export * from './isSupportedChainId'
export * from './isZero'
export * from './jotai/atomWithPartialUpdate'
export * from './legacyAddressUtils'
export * from './localStorage'
+export * from './logger'
export * from './maxAmountSpend'
export * from './misc'
export * from './node'
@@ -52,27 +63,16 @@ export * from './resolveENSContentHash'
export * from './retry'
export * from './safeNamehash'
export * from './sentry'
+export * from './swap'
export * from './time'
export * from './toggleBodyClass'
-export * from './tokens'
export * from './tokenLabel'
+export * from './tokens'
export * from './tooltips'
+export * from './trimTrailingZeros'
export * from './tryParseCurrencyAmount'
export * from './tryParseFractionalAmount'
export * from './uriToHttp'
export * from './userAgent'
-export * from './getDeprecatedChains'
-export * from './getCurrencyAddress'
-export * from './fetch'
-export * from './cache'
export * from './utmParameters'
-export * from './areSetsEqual'
-export * from './trimTrailingZeros'
-export * from './clamp-value'
export * from './validation/isValidIntegerFactory'
-export * from './swap'
-export * from './i18n'
-export * from './cowProtocolContracts'
-export * from './errors'
-export * from './logger'
-export { getAvailableDestinationChains } from './getAvailableDestinationChains'
diff --git a/libs/common-utils/tsconfig.lib.json b/libs/common-utils/tsconfig.lib.json
index f5da466c98a..36e4d03abe9 100644
--- a/libs/common-utils/tsconfig.lib.json
+++ b/libs/common-utils/tsconfig.lib.json
@@ -14,5 +14,5 @@
"**/*.spec.jsx",
"**/*.test.jsx"
],
- "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
+ "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx", "../widget-lib/src/deepMerge.spec.ts"]
}
diff --git a/libs/widget-lib/package.json b/libs/widget-lib/package.json
index f6114c63c2c..045a99527c9 100644
--- a/libs/widget-lib/package.json
+++ b/libs/widget-lib/package.json
@@ -24,6 +24,8 @@
}
},
"dependencies": {
+ "@cowprotocol/common-utils": "workspace:*",
+ "csstype": "^3.1.3",
"@cowprotocol/cow-sdk": "8.0.5",
"@cowprotocol/events": "workspace:*",
"@cowprotocol/iframe-transport": "workspace:*"
diff --git a/libs/widget-lib/src/applyElementStyles.ts b/libs/widget-lib/src/applyElementStyles.ts
new file mode 100644
index 00000000000..2b67f0a3e1f
--- /dev/null
+++ b/libs/widget-lib/src/applyElementStyles.ts
@@ -0,0 +1,28 @@
+import type * as CSS from 'csstype'
+
+/**
+ * Assigns camelCase CSS properties to a DOM element's style.
+ * Values are stringified; callers should use explicit units in JSON (e.g. `"100px"`).
+ */
+export function assignElementStyles(element: HTMLElement, styles: CSS.Properties | undefined): void {
+ if (!styles) {
+ return
+ }
+
+ if (Object.keys(styles).length === 0) {
+ element.removeAttribute('style')
+ return
+ }
+
+ for (const key of Object.keys(styles)) {
+ const styleKey = key as keyof CSS.Properties
+ const value = styles[styleKey]
+
+ if (value === undefined || (value !== null && typeof value !== 'string')) {
+ continue
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ element.style[styleKey as any] = value || ''
+ }
+}
diff --git a/libs/widget-lib/src/cowSwapWidget.constants.ts b/libs/widget-lib/src/cowSwapWidget.constants.ts
new file mode 100644
index 00000000000..307dd81b022
--- /dev/null
+++ b/libs/widget-lib/src/cowSwapWidget.constants.ts
@@ -0,0 +1,19 @@
+import { CowSwapWidgetParams } from './types'
+
+export const DEFAULT_WIDGET_PARAMS = {
+ appCode: 'Unknown',
+ /*
+ iframeStyle: {
+ display: 'block',
+ width: '100%',
+ minWidth: '420px',
+ height: 'auto', // TODO: Before the default was 640px, I think.
+ border: '0',
+ backgroundColor: 'transparent',
+ borderRadius: '1.6rem', // TODO: USe px
+ },
+ cardStyle: {
+ // borderRadius: '24px',
+ },
+ */
+} as const satisfies CowSwapWidgetParams
diff --git a/libs/widget-lib/src/cowSwapWidget.spec.ts b/libs/widget-lib/src/cowSwapWidget.spec.ts
index da7d889902c..dd9e2a40319 100644
--- a/libs/widget-lib/src/cowSwapWidget.spec.ts
+++ b/libs/widget-lib/src/cowSwapWidget.spec.ts
@@ -20,8 +20,7 @@ describe('createCowSwapWidget', () => {
appCode: 'widget-test',
width: '100%',
height: '640px',
- iframeBackgroundColor: 'red',
- iframeBorderRadius: '1.6rem',
+ iframeStyle: { backgroundColor: 'red', borderRadius: '1.6rem' },
},
})
@@ -37,8 +36,7 @@ describe('createCowSwapWidget', () => {
appCode: 'widget-test',
width: '320px',
height: '432px',
- iframeBackgroundColor: 'transparent',
- iframeBorderRadius: '0',
+ iframeStyle: { backgroundColor: 'transparent', borderRadius: '0' },
})
expect(iframe.width).toBe('320px')
@@ -48,6 +46,24 @@ describe('createCowSwapWidget', () => {
expect(iframe.style.borderRadius).toBe('0')
})
+ it('applies iframeStyle to the iframe', () => {
+ const container = document.createElement('div')
+ document.body.appendChild(container)
+
+ createCowSwapWidget(container, {
+ params: {
+ appCode: 'widget-test',
+ iframeStyle: { backgroundColor: 'blue', margin: '12px', border: '2px solid green' },
+ },
+ })
+
+ const iframe = getIframe(container)
+
+ expect(iframe.style.backgroundColor).toBe('blue')
+ expect(iframe.style.margin).toBe('12px')
+ expect(iframe.style.border).toBe('2px solid green')
+ })
+
it('uses the latest height config for resize events after params change', () => {
const container = document.createElement('div')
document.body.appendChild(container)
diff --git a/libs/widget-lib/src/cowSwapWidget.ts b/libs/widget-lib/src/cowSwapWidget.ts
index 59fac939ba8..1421f1e07c2 100644
--- a/libs/widget-lib/src/cowSwapWidget.ts
+++ b/libs/widget-lib/src/cowSwapWidget.ts
@@ -1,6 +1,9 @@
import { CowWidgetEventListeners } from '@cowprotocol/events'
import { IframeRpcProviderBridge } from '@cowprotocol/iframe-transport'
+import { assignElementStyles } from './applyElementStyles'
+import { DEFAULT_WIDGET_PARAMS } from './cowSwapWidget.constants'
+import { deepMerge } from './deepMerge'
import { IframeCowEventEmitter } from './IframeCowEventEmitter'
import { IframeSafeSdkBridge } from './IframeSafeSdkBridge'
import {
@@ -18,9 +21,6 @@ import {
import { buildWidgetPath, buildWidgetUrl, buildWidgetUrlQuery } from './urlUtils'
import { widgetIframeTransport } from './widgetIframeTransport'
-const DEFAULT_HEIGHT = '640px'
-const DEFAULT_WIDTH = '450px'
-
const noopHandler: CowSwapWidgetHandler = {
updateParams: () => void 0,
updateListeners: () => void 0,
@@ -52,13 +52,13 @@ export interface CowSwapWidgetHandler {
export function createCowSwapWidget(container: HTMLElement, props: CowSwapWidgetProps): CowSwapWidgetHandler {
const { params, provider: providerAux, listeners } = props
let provider = providerAux
- let currentParams = params
- let iframeSizing = getIframeSizingConfig(params)
+ let currentParams = deepMerge(params, DEFAULT_WIDGET_PARAMS)
+ let prevHeight = currentParams.iframeStyle?.height
if (typeof window === 'undefined') return noopHandler
// 1. Create a brand new iframe
- const iframe = createIframe(params)
+ const iframe = createIframe(currentParams)
// 2. Clear the content (delete any previous iFrame if it exists)
container.innerHTML = ''
@@ -72,10 +72,15 @@ export function createCowSwapWidget(container: HTMLElement, props: CowSwapWidget
// 3. Send appCode (once the widget posts the ACTIVATE message)
const windowListeners: WindowListener[] = []
- windowListeners.push(sendAppCodeOnActivation(iframeWindow, params.appCode))
+ windowListeners.push(sendAppCodeOnActivation(iframeWindow, currentParams.appCode))
// 4. Handle widget height changes
- windowListeners.push(...listenToHeightChanges(iframe, () => iframeSizing))
+ windowListeners.push(
+ ...listenToHeightChanges(iframe, () => ({
+ defaultHeight: currentParams.iframeStyle?.height || 'auto',
+ maxHeight: currentParams.maxHeight,
+ })),
+ )
// 5. Intercept deeplinks navigation in the iframe
windowListeners.push(interceptDeepLinks())
@@ -112,12 +117,14 @@ export function createCowSwapWidget(container: HTMLElement, props: CowSwapWidget
// 11. Return the handler, so the widget, listeners, and provider can be updated
return {
updateParams: (newParams: CowSwapWidgetParams) => {
- const prevDefaultHeight = iframeSizing.defaultHeight
-
- currentParams = newParams
- iframeSizing = getIframeSizingConfig(newParams)
-
- updateIframeElement(iframe, currentParams, prevDefaultHeight)
+ const nextHeight = newParams.iframeStyle?.height ?? prevHeight
+ currentParams = deepMerge(
+ { ...newParams, iframeStyle: { ...newParams.iframeStyle, height: nextHeight } },
+ DEFAULT_WIDGET_PARAMS,
+ )
+ prevHeight = currentParams.iframeStyle?.height
+
+ updateIframeElement(iframe, currentParams)
updateParams(iframeWindow, currentParams, provider)
updateWidgetHooks()
},
@@ -182,45 +189,28 @@ function updateProvider(
* @returns The generated HTMLIFrameElement.
*/
function createIframe(params: CowSwapWidgetParams): HTMLIFrameElement {
- const { width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT } = params
-
const iframe = document.createElement('iframe')
iframe.src = buildWidgetUrl(params)
- iframe.width = width
- iframe.height = height
- iframe.style.border = '0'
- iframe.style.backgroundColor = params.iframeBackgroundColor || 'transparent'
- iframe.style.borderRadius = params.iframeBorderRadius || ''
iframe.allow = 'clipboard-read; clipboard-write'
+ updateIframeElement(iframe, params)
+
return iframe
}
-function updateIframeElement(
- iframe: HTMLIFrameElement,
- params: CowSwapWidgetParams,
- previousDefaultHeight: string,
-): void {
- const { width = DEFAULT_WIDTH } = params
- const { defaultHeight } = getIframeSizingConfig(params)
-
- iframe.width = width
- iframe.height = defaultHeight
- iframe.style.backgroundColor = params.iframeBackgroundColor || 'transparent'
- iframe.style.borderRadius = params.iframeBorderRadius || ''
-
- if (!iframe.style.height || iframe.style.height === previousDefaultHeight) {
- iframe.style.height = defaultHeight
- }
+function updateIframeElement(iframe: HTMLIFrameElement, params: CowSwapWidgetParams): void {
+ assignElementStyles(iframe, params.iframeStyle)
}
+/*
function getIframeSizingConfig(params: CowSwapWidgetParams): IframeSizingConfig {
return {
- defaultHeight: params.height || DEFAULT_HEIGHT,
+ defaultHeight: params.iframeStyle?.height || DEFAULT_WIDGET_PARAMS.iframeStyle.height,
maxHeight: params.maxHeight,
}
}
+*/
/**
* Updates the CoW Swap Widget based on the new settings provided.
@@ -239,8 +229,8 @@ function updateParams(
const pathname = buildWidgetPath(params)
const search = buildWidgetUrlQuery(params).toString()
- // Omit theme from appParams
- const { theme: _theme, hooks: _hooks, ...appParams } = params
+ // Omit theme, hooks, and host-only iframe styles from appParams
+ const { theme: _theme, hooks: _hooks, iframeStyle: _iframeStyle, ...appParams } = params
widgetIframeTransport.postMessageToWindow(contentWindow, WidgetMethodsListen.UPDATE_PARAMS, {
urlParams: {
diff --git a/libs/widget-lib/src/deepMerge.spec.ts b/libs/widget-lib/src/deepMerge.spec.ts
new file mode 100644
index 00000000000..55addbc40d5
--- /dev/null
+++ b/libs/widget-lib/src/deepMerge.spec.ts
@@ -0,0 +1,43 @@
+import { deepMerge } from './deepMerge'
+
+describe('deepMerge', () => {
+ it('fills missing keys from base', () => {
+ expect(deepMerge({ a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 })
+ })
+
+ it('lets overrides win for primitives', () => {
+ expect(deepMerge({ a: 2 }, { a: 1, b: 0 })).toEqual({ a: 2, b: 0 })
+ })
+
+ it('merges nested plain objects', () => {
+ const base = { nested: { x: 0, y: 1 }, z: 9 }
+ const overrides = { nested: { x: 2 } }
+ expect(deepMerge(overrides, base)).toEqual({ nested: { x: 2, y: 1 }, z: 9 })
+ })
+
+ it('clones override-only nested objects without mutating inputs', () => {
+ const inner = { only: true }
+ const overrides = { nested: inner }
+ const base = {}
+ const out = deepMerge(overrides, base)
+ expect(out).toEqual({ nested: { only: true } })
+ expect(out.nested).not.toBe(inner)
+ })
+
+ it('does not mutate arguments', () => {
+ const a = { nested: { x: 1 } }
+ const b = { nested: { y: 2 }, z: 3 }
+ deepMerge(a, b)
+ expect(a).toEqual({ nested: { x: 1 } })
+ expect(b).toEqual({ nested: { y: 2 }, z: 3 })
+ })
+
+ it('treats arrays as leaves', () => {
+ expect(deepMerge({ items: [1, 2] }, { items: [9] })).toEqual({ items: [1, 2] })
+ })
+
+ it('uses base when override key is undefined', () => {
+ const overrides: { a: number | undefined } = { a: undefined }
+ expect(deepMerge(overrides, { a: 1 })).toEqual({ a: 1 })
+ })
+})
diff --git a/libs/widget-lib/src/deepMerge.ts b/libs/widget-lib/src/deepMerge.ts
new file mode 100644
index 00000000000..d3709c1f75e
--- /dev/null
+++ b/libs/widget-lib/src/deepMerge.ts
@@ -0,0 +1,55 @@
+function isPlainObject(value: unknown): value is Record {
+ if (value === null || typeof value !== 'object') {
+ return false
+ }
+
+ const proto = Object.getPrototypeOf(value)
+ return proto === Object.prototype || proto === null
+}
+
+/**
+ * Recursively merges two plain objects into a new object without mutating the inputs.
+ *
+ * For each key, the value from `overrides` wins when it is not `undefined`.
+ * When both values are plain objects, they are merged recursively with the same rule.
+ * Arrays, `Date`, and other non-plain objects are treated as leaves (no recursive merge).
+ *
+ * @param overrides - Values that take precedence (e.g. user-provided options). Must not be nullish.
+ * @param base - Fallback values (e.g. defaults). Keys missing from `overrides` or set to `undefined` there are taken from `base`.
+ * @returns A new object containing the merged result.
+ *
+ * @remarks
+ * Prototype pollution is avoided by building the result from a null-prototype object.
+ *
+ * @example
+ * ```ts
+ * const defaults = { a: 1, nested: { x: 0, y: 1 } }
+ * const user = { nested: { x: 2 } }
+ * deepMerge(user, defaults) // { a: 1, nested: { x: 2, y: 1 } }
+ * ```
+ */
+export function deepMerge(overrides: T, base: U): T & U {
+ const keys = new Set([
+ ...Object.keys(base as Record),
+ ...Object.keys(overrides as Record),
+ ])
+
+ const merged = Object.create(null) as Record
+
+ for (const key of keys) {
+ const vOverride = (overrides as Record)[key]
+ const vBase = (base as Record)[key]
+
+ if (isPlainObject(vOverride) && isPlainObject(vBase)) {
+ merged[key] = deepMerge(vOverride, vBase)
+ } else if (isPlainObject(vOverride) && vBase === undefined) {
+ merged[key] = deepMerge(vOverride, {})
+ } else if (vOverride !== undefined) {
+ merged[key] = vOverride
+ } else {
+ merged[key] = vBase
+ }
+ }
+
+ return merged as T & U
+}
diff --git a/libs/widget-lib/src/types.ts b/libs/widget-lib/src/types.ts
index 120358bcf3c..7aa1b8ee7c0 100644
--- a/libs/widget-lib/src/types.ts
+++ b/libs/widget-lib/src/types.ts
@@ -6,6 +6,8 @@ import {
OnTradeParamsPayload,
} from '@cowprotocol/events'
+import type * as CSS from 'csstype'
+
export type { SupportedChainId } from '@cowprotocol/cow-sdk'
export type { OnTradeParamsPayload } from '@cowprotocol/events'
@@ -161,21 +163,6 @@ export type CowSwapWidgetPaletteParams = { [K in CowSwapWidgetPaletteColors]: st
export type CowSwapWidgetPalette = {
baseTheme: CowSwapTheme
- /**
- * Overrides the main widget card shadow.
- * Accepts any valid CSS box-shadow value, for example `none` or `0 12px 24px rgba(0, 0, 0, 0.12)`.
- */
- boxShadow?: string
- /**
- * Overrides the outer widget shell padding around the embedded card.
- * Accepts any valid CSS padding value, for example `16px 16px 24px` or `0`.
- */
- widgetPadding?: string
- /**
- * Overrides the main widget card border radius.
- * Accepts any valid CSS border-radius value, for example `24px`, `16px`, or `1.5rem`.
- */
- widgetBorderRadius?: string
} & CowSwapWidgetPaletteParams
export interface CowSwapWidgetSounds {
@@ -232,30 +219,45 @@ export interface CowSwapWidgetParams {
/**
* The width of the outer iframe element. Accepts CSS width values such as `450px` or `100%`.
* Default: `450px`
+ *
+ * @deprecated Use iframeStyle.width instead.
*/
width?: string
/**
* The height of the outer iframe element. Accepts CSS height values such as `640px`.
* Default: `640px`
+ *
+ * @deprecated Use iframeStyle.height instead.
*/
height?: string
/**
- * The background color of the outer iframe element.
- * Default: `transparent`
+ * The maximum height of the widget in pixels. Default: body.offsetHeight
+ *
+ * @deprecated Use iframeStyle.maxHeight instead.
+ */
+ maxHeight?: number
+
+ /**
+ * Extra inline styles for the outer iframe element (host page only; not sent into the iframe app).
+ * Applied after width/height attributes. Use e.g. `backgroundColor`, `borderRadius`, `boxShadow`, `border`.
*/
- iframeBackgroundColor?: string
+ iframeStyle?: CSS.Properties
/**
- * The border radius of the outer iframe element.
- * Accepts any valid CSS border-radius value, for example `1.6rem`, `24px`, or `0`.
+ * Inline styles for the top-level app wrapper (inside the iframe).
*/
- iframeBorderRadius?: string
+ appWrapperStyle?: CSS.Properties
/**
- * The maximum height of the widget in pixels. Default: body.offsetHeight
+ * Inline styles for the body wrapper (inside the iframe).
*/
- maxHeight?: number
+ bodyWrapperStyle?: CSS.Properties
+
+ /**
+ * Inline styles for the main trade widget card (inside the iframe).
+ */
+ cardStyle?: CSS.Properties
/**
* Network ID.
@@ -482,7 +484,7 @@ export type WidgetEventsPayloadMap = WidgetMethodsEmitPayloadMap & WidgetMethods
export type WidgetMethodsEmitPayloads = WidgetMethodsEmitPayloadMap[WidgetMethodsEmit]
export type WidgetMethodsListenPayloads = WidgetMethodsListenPayloadMap[WidgetMethodsListen]
-export type CowSwapWidgetAppParams = Omit
+export type CowSwapWidgetAppParams = Omit
export interface UpdateParamsPayload {
urlParams: {
diff --git a/libs/widget-lib/src/urlUtils.spec.ts b/libs/widget-lib/src/urlUtils.spec.ts
index 11510f1b092..7384db84b4d 100644
--- a/libs/widget-lib/src/urlUtils.spec.ts
+++ b/libs/widget-lib/src/urlUtils.spec.ts
@@ -95,7 +95,7 @@ describe('buildWidgetUrlQuery', () => {
expect(query.get('palette')).toBe('null')
})
- it('serializes widget shell overrides inside the theme palette', () => {
+ it('serializes color palette without legacy shell layout keys', () => {
const query = buildWidgetUrlQuery({
theme: {
baseTheme: 'light',
@@ -108,18 +108,15 @@ describe('buildWidgetUrlQuery', () => {
alert: '#DB971E',
info: '#0d5ed9',
success: '#007B28',
- boxShadow: 'none',
- widgetPadding: '16px 16px 24px',
- widgetBorderRadius: '32px',
},
})
expect(query.get('theme')).toBe('light')
- expect(JSON.parse(decodeURIComponent(query.get('palette') || ''))).toMatchObject({
- boxShadow: 'none',
- widgetPadding: '16px 16px 24px',
- widgetBorderRadius: '32px',
- })
+ const palette = JSON.parse(decodeURIComponent(query.get('palette') || ''))
+ expect(palette.primary).toBe('#052b65')
+ expect(palette.boxShadow).toBeUndefined()
+ expect(palette.widgetPadding).toBeUndefined()
+ expect(palette.widgetBorderRadius).toBeUndefined()
})
it('includes locale in the iframe URL', () => {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ec3f236dcf5..32615d5a521 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1265,6 +1265,9 @@ importers:
'@web3modal/ethers5':
specifier: ^4.1.9
version: 4.1.9(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)(utf-8-validate@5.0.10)
+ csstype:
+ specifier: ^3.1.3
+ version: 3.2.3
ethers:
specifier: 5.7.2
version: 5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10)
@@ -1493,7 +1496,7 @@ importers:
version: 5.5.1(@lingui/babel-plugin-lingui-macro@5.5.1(babel-plugin-macros@3.1.0))(babel-plugin-macros@3.1.0)(react@19.1.2)
'@wagmi/core':
specifier: ^3.1.0
- version: 3.3.1(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(ox@0.11.3(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.2))(viem@2.45.0(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))
+ version: 3.3.1(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(ox@0.11.3(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.45.0(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))
'@web3-react/core':
specifier: ^8.2.3
version: 8.2.3(@types/react@19.1.3)(bufferutil@4.0.8)(immer@10.0.2)(react@19.1.2)(utf-8-validate@5.0.10)
@@ -2284,6 +2287,9 @@ importers:
libs/widget-lib:
dependencies:
+ '@cowprotocol/common-utils':
+ specifier: workspace:*
+ version: link:../common-utils
'@cowprotocol/cow-sdk':
specifier: 8.0.5
version: 8.0.5(@openzeppelin/merkle-tree@1.0.8)(ajv@8.17.1)(cross-fetch@4.0.0(encoding@0.1.13))(encoding@0.1.13)(ipfs-only-hash@4.0.0(encoding@0.1.13))(multiformats@9.9.0)
@@ -2293,6 +2299,9 @@ importers:
'@cowprotocol/iframe-transport':
specifier: workspace:*
version: link:../iframe-transport
+ csstype:
+ specifier: ^3.1.3
+ version: 3.2.3
libs/widget-react:
dependencies:
@@ -20703,7 +20712,7 @@ snapshots:
'@popperjs/core': 2.11.8
'@types/react-transition-group': 4.4.12(@types/react@19.1.3)
clsx: 2.1.1
- csstype: 3.1.3
+ csstype: 3.2.3
prop-types: 15.8.1
react: 19.1.2
react-dom: 19.1.2(react@19.1.2)
@@ -20727,7 +20736,7 @@ snapshots:
dependencies:
'@babel/runtime': 7.28.6
'@emotion/cache': 11.14.0
- csstype: 3.1.3
+ csstype: 3.2.3
prop-types: 15.8.1
react: 19.1.2
optionalDependencies:
@@ -20742,7 +20751,7 @@ snapshots:
'@mui/types': 7.2.24(@types/react@19.1.3)
'@mui/utils': 5.17.1(@types/react@19.1.3)(react@19.1.2)
clsx: 2.1.1
- csstype: 3.1.3
+ csstype: 3.2.3
prop-types: 15.8.1
react: 19.1.2
optionalDependencies:
@@ -23340,11 +23349,11 @@ snapshots:
'@types/styled-system@5.1.16':
dependencies:
- csstype: 3.1.3
+ csstype: 3.2.3
'@types/styled-system__css@5.0.17':
dependencies:
- csstype: 3.1.3
+ csstype: 3.2.3
'@types/tmp@0.2.6': {}
@@ -23699,7 +23708,7 @@ snapshots:
chalk: 4.1.2
css-what: 6.1.0
cssesc: 3.0.0
- csstype: 3.1.3
+ csstype: 3.2.3
deep-object-diff: 1.1.9
deepmerge: 4.3.1
media-query-parser: 2.0.2
@@ -23987,22 +23996,6 @@ snapshots:
- react
- use-sync-external-store
- '@wagmi/core@3.3.1(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(ox@0.11.3(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.1.2))(viem@2.45.0(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))':
- dependencies:
- eventemitter3: 5.0.1
- mipd: 0.0.7(typescript@5.9.3)
- viem: 2.45.0(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12)
- zustand: 5.0.0(@types/react@19.1.3)(immer@10.0.2)(react@19.1.2)(use-sync-external-store@1.4.0(react@19.1.2))
- optionalDependencies:
- '@tanstack/query-core': 5.90.20
- ox: 0.11.3(typescript@5.9.3)(zod@4.1.12)
- typescript: 5.9.3
- transitivePeerDependencies:
- - '@types/react'
- - immer
- - react
- - use-sync-external-store
-
'@wagmi/core@3.3.1(@tanstack/query-core@5.90.20)(@types/react@19.1.3)(immer@10.0.2)(ox@0.11.3(typescript@5.9.3)(zod@4.1.12))(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(viem@2.45.0(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.1.12))':
dependencies:
eventemitter3: 5.0.1
@@ -26793,7 +26786,7 @@ snapshots:
dom-helpers@5.2.1:
dependencies:
'@babel/runtime': 7.28.6
- csstype: 3.1.3
+ csstype: 3.2.3
dom-serializer@1.4.1:
dependencies:
@@ -28592,7 +28585,7 @@ snapshots:
dependencies:
'@types/html-minifier-terser': 6.1.0
html-minifier-terser: 6.1.0
- lodash: 4.17.23
+ lodash: 4.17.21
pretty-error: 4.0.0
tapable: 2.3.0
webpack: 5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17))
@@ -29887,7 +29880,7 @@ snapshots:
jss@10.10.0:
dependencies:
'@babel/runtime': 7.28.6
- csstype: 3.1.3
+ csstype: 3.2.3
is-in-browser: 1.1.3
tiny-warning: 1.0.3
@@ -31950,7 +31943,7 @@ snapshots:
pretty-error@4.0.0:
dependencies:
- lodash: 4.17.23
+ lodash: 4.17.21
renderkid: 3.0.0
optional: true
@@ -32181,7 +32174,7 @@ snapshots:
'@types/lodash': 4.17.0
base16: 1.0.0
color: 3.2.1
- csstype: 3.1.3
+ csstype: 3.2.3
lodash.curry: 4.1.1
react-clientside-effect@1.2.6(react@19.1.2):
@@ -32680,7 +32673,7 @@ snapshots:
css-select: 4.3.0
dom-converter: 0.2.0
htmlparser2: 6.1.0
- lodash: 4.17.23
+ lodash: 4.17.21
strip-ansi: 6.0.1
optional: true
@@ -35361,7 +35354,7 @@ snapshots:
fast-json-stable-stringify: 2.1.0
fs-extra: 9.1.0
glob: 7.2.3
- lodash: 4.17.23
+ lodash: 4.17.21
pretty-bytes: 5.6.0
rollup: 2.79.2
rollup-plugin-terser: 7.0.2(rollup@2.79.2)
From 94a6a6ba2a7e0b105b1adcf5641efbc2d7940172 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?=
Date: Wed, 8 Apr 2026 17:09:31 +0200
Subject: [PATCH 004/110] fix: fix sidebar resizing
---
.../hooks/useResizableDrawerWidth.test.ts | 2 +-
.../hooks/useResizableDrawerWidth.ts | 8 ++++++-
.../src/app/configurator/index.tsx | 24 +++++++++++++++----
.../src/app/configurator/styled.ts | 18 +++++++++-----
4 files changed, 39 insertions(+), 13 deletions(-)
diff --git a/apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.test.ts b/apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.test.ts
index e6f0f47aae6..c71051787e2 100644
--- a/apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.test.ts
+++ b/apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.test.ts
@@ -2,7 +2,7 @@ import { clampDrawerWidth } from './useResizableDrawerWidth'
describe('clampDrawerWidth', () => {
it('does not allow widths below the minimum', () => {
- expect(clampDrawerWidth(200, 1600)).toBe(220)
+ expect(clampDrawerWidth(200, 1600)).toBe(380)
})
it('does not allow widths above the configured maximum', () => {
diff --git a/apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.ts b/apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.ts
index 1a532a5fe1f..1c25b293117 100644
--- a/apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.ts
+++ b/apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.ts
@@ -11,7 +11,7 @@ import {
import { DRAWER_WIDTH_CSS_VAR } from '../styled'
const DEFAULT_DRAWER_WIDTH = 320
-const MIN_DRAWER_WIDTH = 220
+const MIN_DRAWER_WIDTH = 380
const MAX_DRAWER_WIDTH = 720
const MIN_PREVIEW_WIDTH = 360
@@ -27,6 +27,7 @@ export function clampDrawerWidth(nextWidth: number, viewportWidth = getViewportW
interface UseResizableDrawerWidthResult {
drawerWidth: number
+ isResizing: boolean
handleResizeStart: (event: ReactPointerEvent) => void
}
@@ -36,14 +37,17 @@ function setDrawerWidthCssVar(container: HTMLElement | null, width: number): voi
container.style.setProperty(DRAWER_WIDTH_CSS_VAR, `${width}px`)
}
+// eslint-disable-next-line max-lines-per-function
export function useResizableDrawerWidth(containerRef: RefObject): UseResizableDrawerWidthResult {
const resizeStateRef = useRef<{ startX: number; startWidth: number } | null>(null)
const [drawerWidth, setDrawerWidth] = useState(() => clampDrawerWidth(DEFAULT_DRAWER_WIDTH))
+ const [isResizing, setIsResizing] = useState(false)
const currentWidthRef = useRef(drawerWidth)
const animationFrameRef = useRef(null)
const stopResizing = useCallback((): void => {
resizeStateRef.current = null
+ setIsResizing(false)
document.body.style.cursor = ''
document.body.style.userSelect = ''
}, [])
@@ -120,6 +124,7 @@ export function useResizableDrawerWidth(containerRef: RefObject(NetworkOptions[0])
const [{ chainId }, setNetworkControlState] = networkControlState
@@ -322,7 +324,7 @@ export function Configurator({ title }: { title: string }) {
return (
{!isDrawerOpen && (
@@ -343,7 +345,15 @@ export function Configurator({ title }: { title: string }) {
)}
- DrawerStyled(theme)} variant="persistent" anchor="left" open={isDrawerOpen}>
+ ({
+ ...DrawerStyled(theme),
+ transition: isResizing ? 'none' : DRAWER_TRANSITION,
+ })}
+ variant="persistent"
+ anchor="left"
+ open={isDrawerOpen}
+ >
{!IS_IFRAME && (
@@ -560,10 +570,14 @@ export function Configurator({ title }: { title: string }) {
))}
-
-
+ {isDrawerOpen && (
+
+
+
+ )}
+
{params && (
<>
diff --git a/apps/widget-configurator/src/app/configurator/styled.ts b/apps/widget-configurator/src/app/configurator/styled.ts
index 549638d8748..ef4140ff743 100644
--- a/apps/widget-configurator/src/app/configurator/styled.ts
+++ b/apps/widget-configurator/src/app/configurator/styled.ts
@@ -10,7 +10,8 @@ export const WrapperStyled = {
overflowX: 'hidden',
}
-// TODO: Add proper return type annotation
+export const DRAWER_TRANSITION = 'width 225ms cubic-bezier(0, 0, 0.2, 1)'
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export const DrawerStyled = (theme: Theme) => ({
width: `var(${DRAWER_WIDTH_CSS_VAR})`,
@@ -28,8 +29,7 @@ export const DrawerStyled = (theme: Theme) => ({
boxShadow: 'rgba(5, 43, 101, 0.06) 0 1.2rem 1.2rem',
padding: '1.6rem',
position: 'relative',
- overflow: 'hidden',
- overflowY: 'scroll',
+ overflowY: 'auto',
},
})
@@ -77,12 +77,18 @@ export const WalletConnectionWrapper = {
width: '100%',
}
+export const ResizeHandleWrapperStyled = {
+ position: 'relative',
+ width: 0,
+ height: '100%',
+ flexShrink: 0,
+}
+
export const ResizeHandleStyled = {
position: 'absolute',
- top: 0,
- right: 0,
- bottom: 0,
+ inset: 0,
width: '0.8rem',
+ marginLeft: '-0.4rem',
cursor: 'col-resize',
zIndex: 2,
From 99e0e465a5b5b69226b00f080daff9fb543f7fe1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?=
Date: Wed, 8 Apr 2026 17:21:41 +0200
Subject: [PATCH 005/110] feat: add chess pattern behind widget
---
.../src/app/configurator/styled.ts | 20 +++++++++++--------
libs/widget-react/src/lib/CowSwapWidget.tsx | 2 +-
2 files changed, 13 insertions(+), 9 deletions(-)
diff --git a/apps/widget-configurator/src/app/configurator/styled.ts b/apps/widget-configurator/src/app/configurator/styled.ts
index ef4140ff743..684f7caaa29 100644
--- a/apps/widget-configurator/src/app/configurator/styled.ts
+++ b/apps/widget-configurator/src/app/configurator/styled.ts
@@ -53,14 +53,18 @@ export const ContentStyled: SxProps = (theme) => {
minWidth: 0,
overflow: 'auto',
padding: `${CONTENT_PADDING_PX}px`,
- backgroundImage: `${pattern}, linear-gradient(${base}, ${base})`,
- backgroundSize: `${TRANSPARENCY_CHECKER_PX}px ${TRANSPARENCY_CHECKER_PX}px, 100% 100%`,
- backgroundRepeat: 'repeat, no-repeat',
- backgroundPosition: `right ${CONTENT_PADDING_PX}px top ${CONTENT_PADDING_PX}px, 0 0`,
- backgroundClip: 'content-box, border-box',
- backgroundOrigin: 'content-box, border-box',
-
- '& iframe': {
+ backgroundColor: base,
+
+ '& > div': {
+ backgroundImage: `${pattern}`,
+ backgroundSize: `${TRANSPARENCY_CHECKER_PX}px ${TRANSPARENCY_CHECKER_PX}px`,
+ backgroundRepeat: 'repeat',
+ backgroundPosition: `right ${CONTENT_PADDING_PX}px top ${CONTENT_PADDING_PX}px`,
+ backgroundClip: 'content-box',
+ backgroundOrigin: 'content-box',
+ },
+
+ '& > div > iframe': {
display: 'block',
border: 0,
margin: '0 auto',
diff --git a/libs/widget-react/src/lib/CowSwapWidget.tsx b/libs/widget-react/src/lib/CowSwapWidget.tsx
index 71a021c4b2c..3a13de2a2a0 100644
--- a/libs/widget-react/src/lib/CowSwapWidget.tsx
+++ b/libs/widget-react/src/lib/CowSwapWidget.tsx
@@ -127,7 +127,7 @@ export function CowSwapWidget(props: CowSwapWidgetProps): JSX.Element {
}
// Render widget container
- return
+ return
}
function areParamsHooksDifferent(prev: CowSwapWidgetParams, next: CowSwapWidgetParams): boolean {
From e2b35280b60f5691ab33385e72fa1ba3cbd1ee17 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?=
Date: Wed, 8 Apr 2026 17:45:07 +0200
Subject: [PATCH 006/110] feat: update AccordionSection style to be as wide as
possible to maximize real state utilization
---
.../app/configurator/controls/AccordionSection.tsx | 12 +++++++++---
.../src/app/configurator/index.tsx | 2 +-
.../src/app/configurator/styled.ts | 2 +-
3 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/apps/widget-configurator/src/app/configurator/controls/AccordionSection.tsx b/apps/widget-configurator/src/app/configurator/controls/AccordionSection.tsx
index 8372b784c1e..0baf95ef36c 100644
--- a/apps/widget-configurator/src/app/configurator/controls/AccordionSection.tsx
+++ b/apps/widget-configurator/src/app/configurator/controls/AccordionSection.tsx
@@ -19,11 +19,17 @@ export function AccordionSection({ title, defaultExpanded = false, children }: A
disableGutters
defaultExpanded={defaultExpanded}
elevation={0}
+ slotProps={{ transition: { unmountOnExit: true } }}
sx={{
- border: (theme) => `1px solid ${theme.palette.divider}`,
- borderRadius: '1.2rem',
+ borderTop: (theme) => `1px solid ${theme.palette.divider}`,
+ borderRadius: '0 !important',
overflow: 'hidden',
+
'&:before': { display: 'none' },
+
+ '&:last-child': {
+ borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
+ },
}}
>
-
+
{children}
diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/index.tsx
index e96507cf486..5d6ec6c3f3d 100644
--- a/apps/widget-configurator/src/app/configurator/index.tsx
+++ b/apps/widget-configurator/src/app/configurator/index.tsx
@@ -370,7 +370,7 @@ export function Configurator({ title }: { title: string }) {
>
)}
-
+
{!IS_IFRAME && }
diff --git a/apps/widget-configurator/src/app/configurator/styled.ts b/apps/widget-configurator/src/app/configurator/styled.ts
index 684f7caaa29..82318d5d5ae 100644
--- a/apps/widget-configurator/src/app/configurator/styled.ts
+++ b/apps/widget-configurator/src/app/configurator/styled.ts
@@ -29,7 +29,7 @@ export const DrawerStyled = (theme: Theme) => ({
boxShadow: 'rgba(5, 43, 101, 0.06) 0 1.2rem 1.2rem',
padding: '1.6rem',
position: 'relative',
- overflowY: 'auto',
+ overflowY: 'scroll',
},
})
From a03bbd9e819b312911614bf99fa7c76a686c9905 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?=
Date: Wed, 8 Apr 2026 17:54:53 +0200
Subject: [PATCH 007/110] feat: allow only one expanded accordion at a time
---
.../controls/AccordionSection.tsx | 8 +--
.../src/app/configurator/index.tsx | 53 +++++++++++++++----
2 files changed, 48 insertions(+), 13 deletions(-)
diff --git a/apps/widget-configurator/src/app/configurator/controls/AccordionSection.tsx b/apps/widget-configurator/src/app/configurator/controls/AccordionSection.tsx
index 0baf95ef36c..9315354d928 100644
--- a/apps/widget-configurator/src/app/configurator/controls/AccordionSection.tsx
+++ b/apps/widget-configurator/src/app/configurator/controls/AccordionSection.tsx
@@ -9,15 +9,17 @@ import Typography from '@mui/material/Typography'
interface AccordionSectionProps {
title: string
- defaultExpanded?: boolean
+ expanded: boolean
+ onChange: (expanded: boolean) => void
children: ReactNode
}
-export function AccordionSection({ title, defaultExpanded = false, children }: AccordionSectionProps): ReactNode {
+export function AccordionSection({ title, expanded, onChange, children }: AccordionSectionProps): ReactNode {
return (
onChange(isExpanded)}
elevation={0}
slotProps={{ transition: { unmountOnExit: true } }}
sx={{
diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/index.tsx
index 5d6ec6c3f3d..771d97e3f7f 100644
--- a/apps/widget-configurator/src/app/configurator/index.tsx
+++ b/apps/widget-configurator/src/app/configurator/index.tsx
@@ -112,6 +112,11 @@ export function Configurator({ title }: { title: string }) {
}
const [isDrawerOpen, setIsDrawerOpen] = useState(true)
+ const [expandedSection, setExpandedSection] = useState('Basics')
+ const toggleSection = useCallback(
+ (title: string) => (isExpanded: boolean) => setExpandedSection(isExpanded ? title : null),
+ [],
+ )
const { drawerWidth, isResizing, handleResizeStart } = useResizableDrawerWidth(configuratorRef)
const networkControlState = useState(NetworkOptions[0])
@@ -371,12 +376,16 @@ export function Configurator({ title }: { title: string }) {
)}
-
+
{!IS_IFRAME && }
-
+
{!IS_IFRAME && (
@@ -393,7 +402,7 @@ export function Configurator({ title }: { title: string }) {
/>
-
+
-
+
-
+
-
+
-
+
Global deadline
@@ -495,16 +516,28 @@ export function Configurator({ title }: { title: string }) {
-
+
-
+
-
+
Date: Thu, 9 Apr 2026 01:06:56 +0200
Subject: [PATCH 008/110] feat: break down configurator into smaller components
---
.../configurator-sidebar.component.tsx | 513 ++++++++++++++
.../configurator-sidebar.styles.ts | 33 +
.../controls/sidebar-controls.component.tsx | 34 +
.../controls/sidebar-controls.styles.ts | 54 ++
.../footer/sidebar-footer.component.tsx | 55 ++
.../footer/sidebar-footer.styles.ts | 0
.../controls/AccordionSection.test.tsx | 0
.../controls/AccordionSection.tsx | 0
.../controls/AddCustomListDialog.tsx | 6 +-
.../controls/AppearanceStyleControls.tsx | 2 +-
.../controls/BooleanSwitchControl.test.tsx | 0
.../controls/BooleanSwitchControl.tsx | 0
.../controls/ConfiguratorBrandHeader.test.tsx | 0
.../controls/ConfiguratorBrandHeader.tsx | 0
.../controls/CurrencyInputControl.tsx | 0
.../controls/CurrentTradeTypeControl.tsx | 2 +-
.../controls/CustomImagesControl.tsx | 0
.../controls/CustomSoundsControl.tsx | 0
.../controls/DeadlineControl.tsx | 0
.../controls/HelpTooltipButton.tsx | 0
.../controls/LocaleControl.test.tsx | 0
.../controls/LocaleControl.tsx | 0
.../controls/ModeControl.test.tsx | 0
.../{ => components}/controls/ModeControl.tsx | 0
.../controls/NetworkControl.tsx | 0
.../controls/PaletteControl.test.tsx | 0
.../controls/PaletteControl.tsx | 0
.../controls/PartnerFeeControl.tsx | 0
.../controls/SettingHeading.tsx | 0
.../controls/ThemeControl.test.tsx | 0
.../controls/ThemeControl.tsx | 2 +-
.../controls/TokenListControl.tsx | 0
.../controls/TradeModesControl.tsx | 0
.../controls/WidgetHooksControl.tsx | 2 +-
.../src/app/configurator/consts.ts | 9 +
.../configurator/hooks/useEmbedDialogState.ts | 22 -
.../hooks/useResizableDrawerWidth.ts | 33 +-
.../hooks/useSyncWidgetNetwork.ts | 2 +-
.../configurator/hooks/useToastsManager.tsx | 19 +-
.../hooks/useWidgetParamsAndSettings.ts | 19 +-
.../src/app/configurator/index.tsx | 663 +++---------------
.../src/app/configurator/styled.ts | 99 +--
.../src/app/configurator/types.ts | 7 +
.../src/theme/paletteOptions.ts | 2 +-
44 files changed, 835 insertions(+), 743 deletions(-)
create mode 100644 apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.component.tsx
create mode 100644 apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.styles.ts
create mode 100644 apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.component.tsx
create mode 100644 apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.styles.ts
create mode 100644 apps/widget-configurator/src/app/configurator/components/configurator-sidebar/footer/sidebar-footer.component.tsx
create mode 100644 apps/widget-configurator/src/app/configurator/components/configurator-sidebar/footer/sidebar-footer.styles.ts
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/AccordionSection.test.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/AccordionSection.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/AddCustomListDialog.tsx (96%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/AppearanceStyleControls.tsx (98%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/BooleanSwitchControl.test.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/BooleanSwitchControl.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/ConfiguratorBrandHeader.test.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/ConfiguratorBrandHeader.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/CurrencyInputControl.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/CurrentTradeTypeControl.tsx (96%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/CustomImagesControl.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/CustomSoundsControl.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/DeadlineControl.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/HelpTooltipButton.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/LocaleControl.test.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/LocaleControl.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/ModeControl.test.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/ModeControl.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/NetworkControl.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/PaletteControl.test.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/PaletteControl.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/PartnerFeeControl.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/SettingHeading.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/ThemeControl.test.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/ThemeControl.tsx (97%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/TokenListControl.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/TradeModesControl.tsx (100%)
rename apps/widget-configurator/src/app/configurator/{ => components}/controls/WidgetHooksControl.tsx (97%)
delete mode 100644 apps/widget-configurator/src/app/configurator/hooks/useEmbedDialogState.ts
diff --git a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.component.tsx b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.component.tsx
new file mode 100644
index 00000000000..7e03c0b029e
--- /dev/null
+++ b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.component.tsx
@@ -0,0 +1,513 @@
+import { ChangeEvent, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react'
+
+import { SupportedLocale, DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK } from '@cowprotocol/common-const'
+import { useAvailableChains } from '@cowprotocol/common-hooks'
+import { CowSwapWidgetParams, TokenInfo, TradeType, WidgetHookEvents } from '@cowprotocol/widget-lib'
+
+import Box from '@mui/material/Box'
+import Drawer from '@mui/material/Drawer'
+import Stack from '@mui/material/Stack'
+import TextField from '@mui/material/TextField'
+import Typography from '@mui/material/Typography'
+import { useWeb3ModalAccount } from '@web3modal/ethers5/react'
+
+import { DRAWER_TRANSITION, DrawerStyled, WalletConnectionWrapper } from './configurator-sidebar.styles'
+import { SidebarFooter } from './footer/sidebar-footer.component'
+
+import { ColorModeContext } from '../../../../theme/ColorModeContext'
+import { DEFAULT_STATE, DEFAULT_TOKEN_LISTS, IS_IFRAME, TRADE_MODES } from '../../consts'
+import { useColorPaletteManager } from '../../hooks/useColorPaletteManager'
+import { useJsonState, EMPTY_JSON_STATE } from '../../hooks/useJsonState'
+import { useSyncWidgetNetwork } from '../../hooks/useSyncWidgetNetwork'
+import { UseToastsManagerReturn } from '../../hooks/useToastsManager'
+import { CONFIGURATOR_DEFAULT_WIDGET_BASE_URL } from '../../hooks/useWidgetParamsAndSettings'
+import { ConfiguratorState, TokenListItem, WidgetMode } from '../../types'
+import { AccordionSection } from '../controls/AccordionSection'
+import { AppearanceStyleControls } from '../controls/AppearanceStyleControls'
+import { BooleanSwitchControl } from '../controls/BooleanSwitchControl'
+import { ConfiguratorBrandHeader } from '../controls/ConfiguratorBrandHeader'
+import { CurrencyInputControl } from '../controls/CurrencyInputControl'
+import { CurrentTradeTypeControl } from '../controls/CurrentTradeTypeControl'
+import { CustomImagesControl } from '../controls/CustomImagesControl'
+import { CustomSoundsControl } from '../controls/CustomSoundsControl'
+import { DeadlineControl } from '../controls/DeadlineControl'
+import { LocaleControl } from '../controls/LocaleControl'
+import { ModeControl } from '../controls/ModeControl'
+import { NetworkControl, NetworkOption, NetworkOptions } from '../controls/NetworkControl'
+import { PaletteControl } from '../controls/PaletteControl'
+import { PartnerFeeControl } from '../controls/PartnerFeeControl'
+import { ThemeControl } from '../controls/ThemeControl'
+import { TokenListControl } from '../controls/TokenListControl'
+import { TradeModesControl } from '../controls/TradeModesControl'
+import { WidgetHooksControl } from '../controls/WidgetHooksControl'
+
+import type * as CSS from 'csstype'
+
+export interface ConfiguratorSidebarProps {
+ title: string
+ isOpen: boolean
+ isResizing: boolean
+ isSnippetOpen: boolean
+ onSnippetToggle: () => void
+ onStateChange: (state: ConfiguratorState) => void
+ toastManager: UseToastsManagerReturn
+}
+
+// eslint-disable-next-line max-lines-per-function
+export function ConfiguratorSidebar({
+ title,
+ isOpen,
+ isResizing,
+ isSnippetOpen,
+ onSnippetToggle,
+ onStateChange,
+ toastManager,
+}: ConfiguratorSidebarProps): ReactNode {
+ const availableChains = useAvailableChains()
+
+ const [expandedSection, setExpandedSection] = useState('Basics')
+
+ const toggleSection = useCallback(
+ (title: string) => (isExpanded: boolean) => setExpandedSection(isExpanded ? title : null),
+ [],
+ )
+
+ // Basics Section:
+
+ const { mode } = useContext(ColorModeContext)
+
+ const [widgetMode, setWidgetMode] = useState('dapp')
+ const standaloneMode = widgetMode === 'standalone'
+
+ const selectWidgetMode = (event: React.ChangeEvent): void => {
+ setWidgetMode(event.target.value as WidgetMode)
+ }
+
+ const localeState = useState('')
+ const [locale] = localeState
+
+ // Trade Setup Section:
+
+ const networkControlState = useState(NetworkOptions[0])
+ const [{ chainId }, setNetworkControlState] = networkControlState
+
+ const tradeTypeState = useState(TRADE_MODES[0])
+ const [currentTradeType] = tradeTypeState
+
+ const tradeModesState = useState(TRADE_MODES)
+ const [enabledTradeTypes] = tradeModesState
+
+ const [disableCrossChainSwap, setDisableCrossChainSwap] = useState(false)
+ const setAllowCrossChainSwap = useCallback((enabled: boolean) => setDisableCrossChainSwap(!enabled), [])
+
+ // Tokens Section:
+
+ const sellTokenState = useState(DEFAULT_STATE.sellToken)
+ const sellTokenAmountState = useState(DEFAULT_STATE.sellAmount)
+ const [sellToken] = sellTokenState
+ const [sellTokenAmount] = sellTokenAmountState
+
+ const buyTokenState = useState(DEFAULT_STATE.buyToken)
+ const buyTokenAmountState = useState(DEFAULT_STATE.buyAmount)
+ const [buyToken] = buyTokenState
+ const [buyTokenAmount] = buyTokenAmountState
+
+ const tokenListUrlsState = useState(DEFAULT_TOKEN_LISTS)
+ const customTokensState = useState([])
+
+ // Theme Colors Section:
+
+ const paletteManager = useColorPaletteManager(mode)
+ const { colorPalette, defaultPalette } = paletteManager
+
+ // Layout Section:
+
+ const [iframeStyleJson, setIframeStyleJson] = useJsonState(EMPTY_JSON_STATE)
+ const [cardStyleJson, setCardStyleJson] = useJsonState(EMPTY_JSON_STATE)
+ const [appWrapperStyleJson, setAppWrapperStyleJson] = useJsonState(EMPTY_JSON_STATE)
+ const [bodyWrapperStyleJson, setBodyWrapperStyleJson] = useJsonState(EMPTY_JSON_STATE)
+
+ // Behavior Section:
+
+ const [disableProgressBar, setDisableProgressBar] = useState(false)
+ const setShowProgressBar = useCallback((enabled: boolean) => setDisableProgressBar(!enabled), [])
+
+ const [disableTokenImport, setDisableTokenImport] = useState(false)
+ const setAllowTokenImport = useCallback((enabled: boolean) => setDisableTokenImport(!enabled), [])
+
+ const [hideRecentTokens, setHideRecentTokens] = useState(false)
+ const setShowRecentTokens = useCallback((enabled: boolean) => setHideRecentTokens(!enabled), [])
+
+ const [hideFavoriteTokens, setHideFavoriteTokens] = useState(false)
+ const setShowFavoriteTokens = useCallback((enabled: boolean) => setHideFavoriteTokens(!enabled), [])
+
+ const [hideBridgeInfo, setHideBridgeInfo] = useState(false)
+ const setShowBridgeInfo = useCallback((enabled: boolean) => setHideBridgeInfo(!enabled), [])
+
+ const [hideOrdersTable, setHideOrdersTable] = useState(false)
+ const setShowOrdersTable = useCallback((enabled: boolean) => setHideOrdersTable(!enabled), [])
+
+ const [disableTradeWhenPriceImpactIsUnknown, setDisableTradeWhenPriceImpactIsUnknown] = useState(false)
+ const setBlockUnknownPriceImpact = useCallback((enabled: boolean) => {
+ setDisableTradeWhenPriceImpactIsUnknown(enabled)
+ }, [])
+
+ const [disableTradeWhenPriceImpactIsHigherThan, setDisableTradeWhenPriceImpactIsHigherThan] = useState<
+ number | undefined
+ >()
+
+ const setBlockPriceImpactAboveValue = useCallback((event: ChangeEvent) => {
+ const nextValue = event.target.value.trim()
+
+ if (!nextValue) {
+ setDisableTradeWhenPriceImpactIsHigherThan(undefined)
+
+ return
+ }
+
+ const parsedValue = Number(nextValue)
+
+ if (Number.isNaN(parsedValue)) return
+
+ setDisableTradeWhenPriceImpactIsHigherThan(parsedValue)
+ }, [])
+
+ // Deadlines Section:
+
+ const deadlineState = useState()
+ const [deadline] = deadlineState
+ const swapDeadlineState = useState()
+ const [swapDeadline] = swapDeadlineState
+ const limitDeadlineState = useState()
+ const [limitDeadline] = limitDeadlineState
+ const advancedDeadlineState = useState()
+ const [advancedDeadline] = advancedDeadlineState
+
+ // Integrations Section:
+
+ const partnerFeeBpsState = useState(0)
+
+ // Customization Section:
+
+ const customImagesState = useState({})
+ const customSoundsState = useState({})
+ const [widgetAppBaseUrl, setWidgetAppBaseUrl] = useState('')
+ const [rawParamsJson, setRawParamsJson] = useJsonState>(EMPTY_JSON_STATE)
+
+ const handleRawParamsJsonChange = (e: ChangeEvent): void => {
+ setRawParamsJson(null, e.target.value)
+ }
+
+ // Advanced Section:
+
+ const widgetHooksState = useState([])
+
+ // Merge and propagate state:
+
+ const { chainId: walletChainId, isConnected } = useWeb3ModalAccount()
+
+ const [enabledWidgetHooks] = widgetHooksState
+ const [tokenListUrls] = tokenListUrlsState
+ const [customTokens] = customTokensState
+ const [partnerFeeBps] = partnerFeeBpsState
+ const [customImages] = customImagesState
+ const [customSounds] = customSoundsState
+
+ // Don't change chainId in the widget URL if the user is connected to a wallet
+ // Because useSyncWidgetNetwork() will send a request to change the network
+ const effectiveChainId = IS_IFRAME ? undefined : !isConnected || !walletChainId ? chainId : walletChainId
+
+ const configuratorState: ConfiguratorState = useMemo(
+ () => ({
+ deadline,
+ swapDeadline,
+ limitDeadline,
+ advancedDeadline,
+ chainId: effectiveChainId,
+ locale: locale || undefined,
+ theme: mode,
+ iframeStyle: iframeStyleJson.mergedValue,
+ appWrapperStyle: appWrapperStyleJson.mergedValue,
+ bodyWrapperStyle: bodyWrapperStyleJson.mergedValue,
+ cardStyle: cardStyleJson.mergedValue,
+ currentTradeType,
+ enabledTradeTypes,
+ enabledWidgetHooks,
+ sellToken,
+ sellTokenAmount,
+ buyToken,
+ buyTokenAmount,
+ tokenListUrls,
+ customColors: colorPalette,
+ defaultColors: defaultPalette,
+ partnerFeeBps,
+ partnerFeeRecipient: DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK[chainId],
+ standaloneMode,
+ disableToastMessages: toastManager.disableToastMessages,
+ disableProgressBar,
+ disableCrossChainSwap,
+ disableTokenImport,
+ hideRecentTokens,
+ hideFavoriteTokens,
+ hideBridgeInfo,
+ hideOrdersTable,
+ disableTradeWhenPriceImpactIsUnknown,
+ disableTradeWhenPriceImpactIsHigherThan,
+ customImages,
+ customSounds,
+ customTokens,
+ rawParams: rawParamsJson.mergedValue,
+ }),
+ [
+ deadline,
+ swapDeadline,
+ limitDeadline,
+ advancedDeadline,
+ effectiveChainId,
+ chainId,
+ locale,
+ mode,
+ iframeStyleJson.mergedValue,
+ appWrapperStyleJson.mergedValue,
+ bodyWrapperStyleJson.mergedValue,
+ cardStyleJson.mergedValue,
+ currentTradeType,
+ enabledTradeTypes,
+ enabledWidgetHooks,
+ sellToken,
+ sellTokenAmount,
+ buyToken,
+ buyTokenAmount,
+ tokenListUrls,
+ colorPalette,
+ defaultPalette,
+ partnerFeeBps,
+ standaloneMode,
+ toastManager.disableToastMessages,
+ disableProgressBar,
+ disableCrossChainSwap,
+ disableTokenImport,
+ hideRecentTokens,
+ hideFavoriteTokens,
+ hideBridgeInfo,
+ hideOrdersTable,
+ disableTradeWhenPriceImpactIsUnknown,
+ disableTradeWhenPriceImpactIsHigherThan,
+ customImages,
+ customSounds,
+ customTokens,
+ rawParamsJson.mergedValue,
+ ],
+ )
+
+ useEffect(() => {
+ onStateChange(configuratorState)
+ }, [configuratorState, onStateChange])
+
+ useSyncWidgetNetwork(chainId, setNetworkControlState, standaloneMode)
+
+ return (
+ ({
+ ...DrawerStyled(theme),
+ transition: isResizing ? 'none' : DRAWER_TRANSITION,
+ })}
+ variant="persistent"
+ anchor="left"
+ open={isOpen}
+ >
+
+
+ {!IS_IFRAME && (
+ <>
+ {!standaloneMode && (
+
+ {/* Attempt 2 at fixing issue on Vercel build (locally it builds fine) */}
+ {/* Error: apps/widget-configurator/src/app/configurator/index.tsx:272:17 - error TS2339: Property 'w3m-button' does not exist on type 'JSX.IntrinsicElements'.*/}
+ {/* Fix from https://github.com/reown-com/appkit/issues/3093 */}
+ {/* @ts-ignore */}
+
+
+ )}
+ >
+ )}
+
+
+
+ {!IS_IFRAME && }
+
+
+
+
+
+
+ {!IS_IFRAME && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Global deadline
+
+
+
+
+
+ Per-trade deadlines
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setWidgetAppBaseUrl(e.target.value)}
+ size="medium"
+ placeholder={CONFIGURATOR_DEFAULT_WIDGET_BASE_URL}
+ helperText={`Optional. Sets baseUrl (overrides Raw JSON). Default preview URL: ${CONFIGURATOR_DEFAULT_WIDGET_BASE_URL}`}
+ />
+
+
+
+
+
+
+ )
+}
diff --git a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.styles.ts b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.styles.ts
new file mode 100644
index 00000000000..4865fd3e7af
--- /dev/null
+++ b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.styles.ts
@@ -0,0 +1,33 @@
+import type { Theme } from '@mui/material/styles'
+
+export const DRAWER_TRANSITION = 'width 225ms cubic-bezier(0, 0, 0.2, 1)'
+
+export const DRAWER_WIDTH_CSS_VAR = '--widget-configurator-drawer-width'
+
+// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+export const DrawerStyled = (theme: Theme) => ({
+ width: `var(${DRAWER_WIDTH_CSS_VAR})`,
+ flexShrink: 0,
+
+ '& .MuiDrawer-paper': {
+ width: `var(${DRAWER_WIDTH_CSS_VAR})`,
+ boxSizing: 'border-box',
+ display: 'flex',
+ flexFlow: 'column',
+ gap: '1.6rem',
+ height: '100%',
+ border: 0,
+ background: theme.palette.background.paper,
+ boxShadow: 'rgba(5, 43, 101, 0.06) 0 1.2rem 1.2rem',
+ padding: '1.6rem',
+ position: 'relative',
+ overflowY: 'scroll',
+ },
+})
+
+export const WalletConnectionWrapper = {
+ display: 'flex',
+ justifyContent: 'center',
+ margin: '0 auto 1rem',
+ width: '100%',
+}
diff --git a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.component.tsx b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.component.tsx
new file mode 100644
index 00000000000..fc80c9cc1f0
--- /dev/null
+++ b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.component.tsx
@@ -0,0 +1,34 @@
+import { ReactNode } from 'react'
+
+import { Box, IconButton } from '@mui/material'
+
+import {
+ sidebarControlsZeroWidthColumnSx,
+ sidebarToggleOpenButton,
+ sidebarResizeHandle,
+} from './sidebar-controls.styles'
+
+export interface SidebarControlsProps {
+ isSidebarOpen: boolean
+ toggleSidebarOpen: () => void
+ onResizeStart: (event: React.PointerEvent) => void
+}
+
+export function SidebarControls({ isSidebarOpen, toggleSidebarOpen, onResizeStart }: SidebarControlsProps): ReactNode {
+ return (
+
+
+ {isSidebarOpen ? '<' : '>'}
+
+
+ {isSidebarOpen ? (
+
+ ) : null}
+
+ )
+}
diff --git a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.styles.ts b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.styles.ts
new file mode 100644
index 00000000000..c0faab1685c
--- /dev/null
+++ b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.styles.ts
@@ -0,0 +1,54 @@
+import { SxProps } from '@mui/material'
+import { Theme } from '@mui/material/styles'
+
+export const sidebarControlsZeroWidthColumnSx: SxProps = {
+ position: 'relative',
+ width: 0,
+ height: '100%',
+ flexShrink: 0,
+}
+
+export const sidebarToggleOpenButton: SxProps = (theme: Theme) => ({
+ position: 'fixed',
+ top: '1.6rem',
+ left: '1.6rem',
+
+ width: '3.6rem',
+ height: '3.6rem',
+ borderRadius: '50%',
+ border: `1px solid ${theme.palette.divider}`,
+ backgroundColor: theme.palette.background.paper,
+ color: theme.palette.primary.main,
+ boxShadow: 'none',
+ zIndex: 3,
+
+ '&:hover': {
+ backgroundColor: theme.palette.action.hover,
+ boxShadow: 'none',
+ },
+})
+
+export const sidebarResizeHandle: SxProps = {
+ position: 'absolute',
+ inset: 0,
+ width: '0.8rem',
+ marginLeft: '-0.4rem',
+ cursor: 'col-resize',
+ zIndex: 2,
+
+ '&::before': {
+ content: '""',
+ position: 'absolute',
+ top: '1.6rem',
+ bottom: '1.6rem',
+ left: '50%',
+ transform: 'translateX(-50%)',
+ width: '0.2rem',
+ borderRadius: '999px',
+ backgroundColor: 'divider',
+ },
+
+ '&:hover::before': {
+ backgroundColor: 'text.secondary',
+ },
+}
diff --git a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/footer/sidebar-footer.component.tsx b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/footer/sidebar-footer.component.tsx
new file mode 100644
index 00000000000..383c20a1465
--- /dev/null
+++ b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/footer/sidebar-footer.component.tsx
@@ -0,0 +1,55 @@
+import React, { ReactNode, useMemo } from 'react'
+
+import List from '@mui/material/List'
+import ListItemButton from '@mui/material/ListItemButton'
+import ListItemText from '@mui/material/ListItemText'
+
+import { UTM_PARAMS } from '../../../consts'
+
+interface LinkConfig {
+ label: string
+ url?: string
+ onClick?: () => void
+}
+
+export interface SidebarFooterProps {
+ isSnippetOpen: boolean
+ onSnippetToggle: () => void
+}
+
+export function SidebarFooter({ isSnippetOpen, onSnippetToggle }: SidebarFooterProps): ReactNode {
+ const links: LinkConfig[] = useMemo(() => {
+ return [
+ isSnippetOpen
+ ? { label: 'View preview', onClick: onSnippetToggle }
+ : { label: 'View code snippet', onClick: onSnippetToggle },
+ { label: 'Widget web', url: `https://cow.fi/widget/?${UTM_PARAMS}` },
+ {
+ label: 'Developer docs',
+ url: `https://docs.cow.fi/cow-protocol/tutorials/widget?${UTM_PARAMS}`,
+ },
+ ]
+ }, [isSnippetOpen, onSnippetToggle])
+
+ return (
+
+ {links.map(({ label, url, onClick }) => (
+
+
+
+ ))}
+
+ )
+}
diff --git a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/footer/sidebar-footer.styles.ts b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/footer/sidebar-footer.styles.ts
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/apps/widget-configurator/src/app/configurator/controls/AccordionSection.test.tsx b/apps/widget-configurator/src/app/configurator/components/controls/AccordionSection.test.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/AccordionSection.test.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/AccordionSection.test.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/AccordionSection.tsx b/apps/widget-configurator/src/app/configurator/components/controls/AccordionSection.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/AccordionSection.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/AccordionSection.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/AddCustomListDialog.tsx b/apps/widget-configurator/src/app/configurator/components/controls/AddCustomListDialog.tsx
similarity index 96%
rename from apps/widget-configurator/src/app/configurator/controls/AddCustomListDialog.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/AddCustomListDialog.tsx
index 6524a570793..dcedf23a402 100644
--- a/apps/widget-configurator/src/app/configurator/controls/AddCustomListDialog.tsx
+++ b/apps/widget-configurator/src/app/configurator/components/controls/AddCustomListDialog.tsx
@@ -15,9 +15,9 @@ import {
} from '@mui/material'
import Tabs from '@mui/material/Tabs'
-import { DEFAULT_CUSTOM_TOKENS } from '../consts'
-import { parseCustomTokensInput } from '../utils/parseCustomTokensInput'
-import { validateURL } from '../utils/validateURL'
+import { DEFAULT_CUSTOM_TOKENS } from '../../consts'
+import { parseCustomTokensInput } from '../../utils/parseCustomTokensInput'
+import { validateURL } from '../../utils/validateURL'
const jsonTextAreaStyles = {
fontFamily: 'monospace',
diff --git a/apps/widget-configurator/src/app/configurator/controls/AppearanceStyleControls.tsx b/apps/widget-configurator/src/app/configurator/components/controls/AppearanceStyleControls.tsx
similarity index 98%
rename from apps/widget-configurator/src/app/configurator/controls/AppearanceStyleControls.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/AppearanceStyleControls.tsx
index 84d69f597c8..f586277af17 100644
--- a/apps/widget-configurator/src/app/configurator/controls/AppearanceStyleControls.tsx
+++ b/apps/widget-configurator/src/app/configurator/components/controls/AppearanceStyleControls.tsx
@@ -5,7 +5,7 @@ import Stack from '@mui/material/Stack'
import TextField from '@mui/material/TextField'
import Typography from '@mui/material/Typography'
-import type { JsonState, OnJsonStateChange } from '../hooks/useJsonState'
+import type { JsonState, OnJsonStateChange } from '../../hooks/useJsonState'
import type * as CSS from 'csstype'
export interface AppearanceStyleControlsProps {
diff --git a/apps/widget-configurator/src/app/configurator/controls/BooleanSwitchControl.test.tsx b/apps/widget-configurator/src/app/configurator/components/controls/BooleanSwitchControl.test.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/BooleanSwitchControl.test.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/BooleanSwitchControl.test.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/BooleanSwitchControl.tsx b/apps/widget-configurator/src/app/configurator/components/controls/BooleanSwitchControl.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/BooleanSwitchControl.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/BooleanSwitchControl.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/ConfiguratorBrandHeader.test.tsx b/apps/widget-configurator/src/app/configurator/components/controls/ConfiguratorBrandHeader.test.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/ConfiguratorBrandHeader.test.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/ConfiguratorBrandHeader.test.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/ConfiguratorBrandHeader.tsx b/apps/widget-configurator/src/app/configurator/components/controls/ConfiguratorBrandHeader.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/ConfiguratorBrandHeader.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/ConfiguratorBrandHeader.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/CurrencyInputControl.tsx b/apps/widget-configurator/src/app/configurator/components/controls/CurrencyInputControl.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/CurrencyInputControl.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/CurrencyInputControl.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/CurrentTradeTypeControl.tsx b/apps/widget-configurator/src/app/configurator/components/controls/CurrentTradeTypeControl.tsx
similarity index 96%
rename from apps/widget-configurator/src/app/configurator/controls/CurrentTradeTypeControl.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/CurrentTradeTypeControl.tsx
index 92673ac6211..e22421d30ce 100644
--- a/apps/widget-configurator/src/app/configurator/controls/CurrentTradeTypeControl.tsx
+++ b/apps/widget-configurator/src/app/configurator/components/controls/CurrentTradeTypeControl.tsx
@@ -7,7 +7,7 @@ import InputLabel from '@mui/material/InputLabel'
import MenuItem from '@mui/material/MenuItem'
import Select from '@mui/material/Select'
-import { TRADE_MODES } from '../consts'
+import { TRADE_MODES } from '../../consts'
const LABEL = 'Current trade type'
diff --git a/apps/widget-configurator/src/app/configurator/controls/CustomImagesControl.tsx b/apps/widget-configurator/src/app/configurator/components/controls/CustomImagesControl.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/CustomImagesControl.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/CustomImagesControl.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/CustomSoundsControl.tsx b/apps/widget-configurator/src/app/configurator/components/controls/CustomSoundsControl.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/CustomSoundsControl.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/CustomSoundsControl.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/DeadlineControl.tsx b/apps/widget-configurator/src/app/configurator/components/controls/DeadlineControl.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/DeadlineControl.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/DeadlineControl.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/HelpTooltipButton.tsx b/apps/widget-configurator/src/app/configurator/components/controls/HelpTooltipButton.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/HelpTooltipButton.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/HelpTooltipButton.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/LocaleControl.test.tsx b/apps/widget-configurator/src/app/configurator/components/controls/LocaleControl.test.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/LocaleControl.test.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/LocaleControl.test.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/LocaleControl.tsx b/apps/widget-configurator/src/app/configurator/components/controls/LocaleControl.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/LocaleControl.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/LocaleControl.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/ModeControl.test.tsx b/apps/widget-configurator/src/app/configurator/components/controls/ModeControl.test.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/ModeControl.test.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/ModeControl.test.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/ModeControl.tsx b/apps/widget-configurator/src/app/configurator/components/controls/ModeControl.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/ModeControl.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/ModeControl.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/NetworkControl.tsx b/apps/widget-configurator/src/app/configurator/components/controls/NetworkControl.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/NetworkControl.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/NetworkControl.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/PaletteControl.test.tsx b/apps/widget-configurator/src/app/configurator/components/controls/PaletteControl.test.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/PaletteControl.test.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/PaletteControl.test.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/PaletteControl.tsx b/apps/widget-configurator/src/app/configurator/components/controls/PaletteControl.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/PaletteControl.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/PaletteControl.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/PartnerFeeControl.tsx b/apps/widget-configurator/src/app/configurator/components/controls/PartnerFeeControl.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/PartnerFeeControl.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/PartnerFeeControl.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/SettingHeading.tsx b/apps/widget-configurator/src/app/configurator/components/controls/SettingHeading.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/SettingHeading.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/SettingHeading.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/ThemeControl.test.tsx b/apps/widget-configurator/src/app/configurator/components/controls/ThemeControl.test.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/ThemeControl.test.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/ThemeControl.test.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/ThemeControl.tsx b/apps/widget-configurator/src/app/configurator/components/controls/ThemeControl.tsx
similarity index 97%
rename from apps/widget-configurator/src/app/configurator/controls/ThemeControl.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/ThemeControl.tsx
index bb50762aefd..04019b9ff3c 100644
--- a/apps/widget-configurator/src/app/configurator/controls/ThemeControl.tsx
+++ b/apps/widget-configurator/src/app/configurator/components/controls/ThemeControl.tsx
@@ -10,7 +10,7 @@ import MenuItem from '@mui/material/MenuItem'
import Select, { type SelectChangeEvent } from '@mui/material/Select'
import Typography from '@mui/material/Typography'
-import { ColorModeContext } from '../../../theme/ColorModeContext'
+import { ColorModeContext } from '../../../../theme/ColorModeContext'
const AUTO = 'auto'
diff --git a/apps/widget-configurator/src/app/configurator/controls/TokenListControl.tsx b/apps/widget-configurator/src/app/configurator/components/controls/TokenListControl.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/TokenListControl.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/TokenListControl.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/TradeModesControl.tsx b/apps/widget-configurator/src/app/configurator/components/controls/TradeModesControl.tsx
similarity index 100%
rename from apps/widget-configurator/src/app/configurator/controls/TradeModesControl.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/TradeModesControl.tsx
diff --git a/apps/widget-configurator/src/app/configurator/controls/WidgetHooksControl.tsx b/apps/widget-configurator/src/app/configurator/components/controls/WidgetHooksControl.tsx
similarity index 97%
rename from apps/widget-configurator/src/app/configurator/controls/WidgetHooksControl.tsx
rename to apps/widget-configurator/src/app/configurator/components/controls/WidgetHooksControl.tsx
index 8a22a95288f..1d8654d7693 100644
--- a/apps/widget-configurator/src/app/configurator/controls/WidgetHooksControl.tsx
+++ b/apps/widget-configurator/src/app/configurator/components/controls/WidgetHooksControl.tsx
@@ -10,7 +10,7 @@ import MenuItem from '@mui/material/MenuItem'
import OutlinedInput from '@mui/material/OutlinedInput'
import Select, { SelectChangeEvent } from '@mui/material/Select'
-import { WIDGET_HOOKS } from '../consts'
+import { WIDGET_HOOKS } from '../../consts'
const LABEL = 'Widget hooks'
const EMPTY_VALUE_LABEL = 'No hooks selected'
diff --git a/apps/widget-configurator/src/app/configurator/consts.ts b/apps/widget-configurator/src/app/configurator/consts.ts
index 5fe43cb9a67..8ee2f797925 100644
--- a/apps/widget-configurator/src/app/configurator/consts.ts
+++ b/apps/widget-configurator/src/app/configurator/consts.ts
@@ -4,12 +4,21 @@ import { CowSwapWidgetPaletteParams, TokenInfo, TradeType, WidgetHookEvents } fr
import { TokenListItem } from './types'
+export const UTM_PARAMS = 'utm_content=cow-widget-configurator&utm_medium=web&utm_source=widget.cow.fi' as const
+
// CoW DAO addresses
export const TRADE_MODES = [TradeType.SWAP, TradeType.LIMIT, TradeType.ADVANCED, TradeType.YIELD]
export const WIDGET_HOOKS = Object.values(WidgetHookEvents)
+export const DEFAULT_STATE = {
+ sellToken: 'USDC',
+ buyToken: 'COW',
+ sellAmount: 100000,
+ buyAmount: 0,
+} as const /* satisfies Partial */
+
// Sourced from https://tokenlists.org/
export const DEFAULT_TOKEN_LISTS: TokenListItem[] = [
{ url: `${COW_CDN}/tokens/CowSwap.json`, enabled: true, enabledForSell: false, enabledForBuy: false },
diff --git a/apps/widget-configurator/src/app/configurator/hooks/useEmbedDialogState.ts b/apps/widget-configurator/src/app/configurator/hooks/useEmbedDialogState.ts
deleted file mode 100644
index 0274b82b0ff..00000000000
--- a/apps/widget-configurator/src/app/configurator/hooks/useEmbedDialogState.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useMemo, useState } from 'react'
-
-// TODO: Add proper return type annotation
-// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-export function useEmbedDialogState(initialOpen = false) {
- const [open, setOpen] = useState(initialOpen)
-
- return useMemo(() => {
- // TODO: Add proper return type annotation
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
- const handleOpen = () => setOpen(true)
- // TODO: Add proper return type annotation
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
- const handleClose = () => setOpen(false)
-
- return {
- dialogOpen: open,
- handleDialogClose: handleClose,
- handleDialogOpen: handleOpen,
- }
- }, [open])
-}
diff --git a/apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.ts b/apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.ts
index 1c25b293117..831aa183c7f 100644
--- a/apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.ts
+++ b/apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.ts
@@ -1,14 +1,4 @@
-import {
- PointerEvent as ReactPointerEvent,
- RefObject,
- useCallback,
- useEffect,
- useLayoutEffect,
- useRef,
- useState,
-} from 'react'
-
-import { DRAWER_WIDTH_CSS_VAR } from '../styled'
+import React, { RefObject, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
const DEFAULT_DRAWER_WIDTH = 320
const MIN_DRAWER_WIDTH = 380
@@ -28,17 +18,20 @@ export function clampDrawerWidth(nextWidth: number, viewportWidth = getViewportW
interface UseResizableDrawerWidthResult {
drawerWidth: number
isResizing: boolean
- handleResizeStart: (event: ReactPointerEvent) => void
+ handleResizeStart: (event: React.PointerEvent) => void
}
-function setDrawerWidthCssVar(container: HTMLElement | null, width: number): void {
+function setDrawerWidthCssVar(container: HTMLElement | null, cssVarName: string, width: number): void {
if (!container) return
- container.style.setProperty(DRAWER_WIDTH_CSS_VAR, `${width}px`)
+ container.style.setProperty(cssVarName, `${width}px`)
}
// eslint-disable-next-line max-lines-per-function
-export function useResizableDrawerWidth(containerRef: RefObject): UseResizableDrawerWidthResult {
+export function useResizableDrawerWidth(
+ containerRef: RefObject,
+ cssVarName: string,
+): UseResizableDrawerWidthResult {
const resizeStateRef = useRef<{ startX: number; startWidth: number } | null>(null)
const [drawerWidth, setDrawerWidth] = useState(() => clampDrawerWidth(DEFAULT_DRAWER_WIDTH))
const [isResizing, setIsResizing] = useState(false)
@@ -57,15 +50,15 @@ export function useResizableDrawerWidth(containerRef: RefObject {
currentWidthRef.current = drawerWidth
- setDrawerWidthCssVar(containerRef.current, drawerWidth)
- }, [containerRef, drawerWidth])
+ setDrawerWidthCssVar(containerRef.current, cssVarName, drawerWidth)
+ }, [containerRef, cssVarName, drawerWidth])
useEffect(() => {
const handlePointerMove = (event: PointerEvent): void => {
@@ -116,7 +109,7 @@ export function useResizableDrawerWidth(containerRef: RefObject): void => {
+ (event: React.PointerEvent): void => {
if (event.button !== 0) return
resizeStateRef.current = {
diff --git a/apps/widget-configurator/src/app/configurator/hooks/useSyncWidgetNetwork.ts b/apps/widget-configurator/src/app/configurator/hooks/useSyncWidgetNetwork.ts
index ddbd1d139c3..12a742a6184 100644
--- a/apps/widget-configurator/src/app/configurator/hooks/useSyncWidgetNetwork.ts
+++ b/apps/widget-configurator/src/app/configurator/hooks/useSyncWidgetNetwork.ts
@@ -4,7 +4,7 @@ import type { SupportedChainId } from '@cowprotocol/cow-sdk'
import { useWeb3ModalAccount, useSwitchNetwork } from '@web3modal/ethers5/react'
-import { getNetworkOption, NetworkOption } from '../controls/NetworkControl'
+import { getNetworkOption, NetworkOption } from '../components/controls/NetworkControl'
export function useSyncWidgetNetwork(
chainId: SupportedChainId,
diff --git a/apps/widget-configurator/src/app/configurator/hooks/useToastsManager.tsx b/apps/widget-configurator/src/app/configurator/hooks/useToastsManager.tsx
index cc778cebc04..e5b1bb26b5f 100644
--- a/apps/widget-configurator/src/app/configurator/hooks/useToastsManager.tsx
+++ b/apps/widget-configurator/src/app/configurator/hooks/useToastsManager.tsx
@@ -9,10 +9,15 @@ import {
import { COW_LISTENERS } from '../consts'
+export interface UseToastsManagerReturn {
+ disableToastMessages: boolean
+ setToastMessagesInDappMode: (enabled: boolean) => void
+ toasts: (ReactElement | string)[]
+ closeToast: () => void
+}
+
// TODO: Break down this large function into smaller functions
-// TODO: Add proper return type annotation
-// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-export function useToastsManager(setListeners: (listeners: CowWidgetEventListeners) => void) {
+export function useToastsManager(setListeners: (listeners: CowWidgetEventListeners) => void): UseToastsManagerReturn {
const isInitRef = useRef(false)
const [disableToastMessages, setDisableToastMessages] = useState(false)
const [toasts, setToasts] = useState<(ReactElement | string)[]>([])
@@ -23,17 +28,13 @@ export function useToastsManager(setListeners: (listeners: CowWidgetEventListene
setToasts((t) => [...t, message])
}
- const closeToast = useCallback((_: unknown, reason?: string) => {
- if (reason === 'clickaway') {
- return
- }
-
+ const closeToast = useCallback(() => {
setToasts((t) => t.slice(1))
}, [])
const setToastMessagesInDappMode = useCallback(
(enabled: boolean) => {
- closeToast(undefined)
+ closeToast()
setDisableToastMessages(enabled)
},
[closeToast],
diff --git a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts
index a9fcaa1bd13..51a5ff5a0d1 100644
--- a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts
+++ b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts
@@ -149,7 +149,10 @@ function getPartnerFeeParam(
}
}
-function buildWidgetParams(configuratorState: ConfiguratorState): CowSwapWidgetParams {
+// eslint-disable-next-line max-lines-per-function
+function buildWidgetParams(configuratorState: ConfiguratorState | null): CowSwapWidgetParams | null {
+ if (!configuratorState) return null
+
const {
chainId,
locale,
@@ -186,8 +189,15 @@ function buildWidgetParams(configuratorState: ConfiguratorState): CowSwapWidgetP
disableTradeWhenPriceImpactIsHigherThan,
slippage,
enabledWidgetHooks,
+ customImages,
+ customSounds,
+ customTokens,
+ rawParams,
} = configuratorState
+ // TODO: Can we automatically trim all values and avoid adding those that are not needed? Would that be better or worse (as then those props that are not provided)
+ // rely on the widget app logic to use the default values, which potentially means more bugs / breaking changes?
+
return {
appCode: 'CoW Widget: Configurator',
chainId,
@@ -222,9 +232,14 @@ function buildWidgetParams(configuratorState: ConfiguratorState): CowSwapWidgetP
whenPriceImpactIsHigherThan: disableTradeWhenPriceImpactIsHigherThan,
},
hooks: getWidgetHooks(enabledWidgetHooks),
+ images: customImages,
+ sounds: customSounds,
+ customTokens,
+ ...rawParams,
+ ...window.cowSwapWidgetParams,
}
}
-export function useWidgetParams(configuratorState: ConfiguratorState): CowSwapWidgetParams {
+export function useWidgetParams(configuratorState: ConfiguratorState | null): CowSwapWidgetParams | null {
return useMemo(() => buildWidgetParams(configuratorState), [configuratorState])
}
diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/index.tsx
index 771d97e3f7f..d150395a748 100644
--- a/apps/widget-configurator/src/app/configurator/index.tsx
+++ b/apps/widget-configurator/src/app/configurator/index.tsx
@@ -1,318 +1,79 @@
-import { ChangeEvent, CSSProperties, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
+import React, { CSSProperties, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
import { useCowAnalytics } from '@cowprotocol/analytics'
-import { DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK, SupportedLocale } from '@cowprotocol/common-const'
-import { useAvailableChains } from '@cowprotocol/common-hooks'
import { CowWidgetEventListeners } from '@cowprotocol/events'
-import { CowSwapWidgetParams, TokenInfo, TradeType, WidgetHookEvents } from '@cowprotocol/widget-lib'
+import { CowSwapWidgetParams } from '@cowprotocol/widget-lib'
import { CowSwapWidget } from '@cowprotocol/widget-react'
-import ChromeReaderModeIcon from '@mui/icons-material/ChromeReaderMode'
import CloseIcon from '@mui/icons-material/Close'
-import CodeIcon from '@mui/icons-material/Code'
-import KeyboardDoubleArrowLeftIcon from '@mui/icons-material/KeyboardDoubleArrowLeft'
-import KeyboardDoubleArrowRightIcon from '@mui/icons-material/KeyboardDoubleArrowRight'
-import LanguageIcon from '@mui/icons-material/Language'
-import { IconButton, Snackbar } from '@mui/material'
+import { CircularProgress, IconButton, Snackbar } from '@mui/material'
import Box from '@mui/material/Box'
-import Button from '@mui/material/Button'
-import Drawer from '@mui/material/Drawer'
-import Fab from '@mui/material/Fab'
-import List from '@mui/material/List'
-import ListItemButton from '@mui/material/ListItemButton'
-import ListItemIcon from '@mui/material/ListItemIcon'
-import ListItemText from '@mui/material/ListItemText'
-import Stack from '@mui/material/Stack'
-import TextField from '@mui/material/TextField'
-import Typography from '@mui/material/Typography'
import { useWeb3ModalAccount, useWeb3ModalTheme } from '@web3modal/ethers5/react'
-import { COW_LISTENERS, DEFAULT_TOKEN_LISTS, IS_IFRAME, TRADE_MODES } from './consts'
-import { AccordionSection } from './controls/AccordionSection'
-import { AppearanceStyleControls } from './controls/AppearanceStyleControls'
-import { BooleanSwitchControl } from './controls/BooleanSwitchControl'
-import { ConfiguratorBrandHeader } from './controls/ConfiguratorBrandHeader'
-import { CurrencyInputControl } from './controls/CurrencyInputControl'
-import { CurrentTradeTypeControl } from './controls/CurrentTradeTypeControl'
-import { CustomImagesControl } from './controls/CustomImagesControl'
-import { CustomSoundsControl } from './controls/CustomSoundsControl'
-import { DeadlineControl } from './controls/DeadlineControl'
-import { LocaleControl } from './controls/LocaleControl'
-import { ModeControl } from './controls/ModeControl'
-import { NetworkControl, NetworkOption, NetworkOptions } from './controls/NetworkControl'
-import { PaletteControl } from './controls/PaletteControl'
-import { PartnerFeeControl } from './controls/PartnerFeeControl'
-import { ThemeControl } from './controls/ThemeControl'
-import { TokenListControl } from './controls/TokenListControl'
-import { TradeModesControl } from './controls/TradeModesControl'
-import { WidgetHooksControl } from './controls/WidgetHooksControl'
-import { useColorPaletteManager } from './hooks/useColorPaletteManager'
-import { useEmbedDialogState } from './hooks/useEmbedDialogState'
-import { EMPTY_JSON_STATE, useJsonState } from './hooks/useJsonState'
+import { ConfiguratorSidebar } from './components/configurator-sidebar/configurator-sidebar.component'
+import { DRAWER_WIDTH_CSS_VAR } from './components/configurator-sidebar/configurator-sidebar.styles'
+import { SidebarControls } from './components/configurator-sidebar/controls/sidebar-controls.component'
+import { COW_LISTENERS, IS_IFRAME } from './consts'
import { useProvider } from './hooks/useProvider'
import { useResizableDrawerWidth } from './hooks/useResizableDrawerWidth'
-import { useSyncWidgetNetwork } from './hooks/useSyncWidgetNetwork'
import { useToastsManager } from './hooks/useToastsManager'
-import { CONFIGURATOR_DEFAULT_WIDGET_BASE_URL, useWidgetParams } from './hooks/useWidgetParamsAndSettings'
-import {
- ContentStyled,
- DRAWER_TRANSITION,
- DRAWER_WIDTH_CSS_VAR,
- DrawerToggleButtonStyled,
- DrawerStyled,
- ResizeHandleStyled,
- ResizeHandleWrapperStyled,
- WalletConnectionWrapper,
- WrapperStyled,
-} from './styled'
-import { ConfiguratorState, TokenListItem } from './types'
+import { useWidgetParams } from './hooks/useWidgetParamsAndSettings'
+import { configuratorCheckeredCanvasSx, configuradorRootSx } from './styled'
+import { ConfiguratorState } from './types'
import { AnalyticsCategory } from '../../common/analytics/types'
-import { ColorModeContext } from '../../theme/ColorModeContext'
import { EmbedDialog } from '../embedDialog'
-import type * as CSS from 'csstype'
-
declare global {
interface Window {
cowSwapWidgetParams?: Partial
}
}
-const DEFAULT_STATE = {
- sellToken: 'USDC',
- buyToken: 'COW',
- sellAmount: 100000,
- buyAmount: 0,
-}
-
-const UTM_PARAMS = 'utm_content=cow-widget-configurator&utm_medium=web&utm_source=widget.cow.fi'
-
-export type WidgetMode = 'dapp' | 'standalone'
-
-// TODO: Break down this large function into smaller functions
-// TODO: Add proper return type annotation
-// TODO: Reduce function complexity by extracting logic
-// eslint-disable-next-line max-lines-per-function, @typescript-eslint/explicit-function-return-type
-export function Configurator({ title }: { title: string }) {
+export function Configurator({ title }: { title: string }): ReactNode {
const configuratorRef = useRef(null)
const { setThemeMode } = useWeb3ModalTheme()
- const { chainId: walletChainId, isConnected } = useWeb3ModalAccount()
+ const { isConnected } = useWeb3ModalAccount()
const provider = useProvider()
const cowAnalytics = useCowAnalytics()
- const [listeners, setListeners] = useState(COW_LISTENERS)
- const { mode } = useContext(ColorModeContext)
-
- const [widgetMode, setWidgetMode] = useState('dapp')
- const standaloneMode = widgetMode === 'standalone'
-
- const selectWidgetMode = (event: ChangeEvent): void => {
- setWidgetMode(event.target.value as WidgetMode)
- }
-
- const [isDrawerOpen, setIsDrawerOpen] = useState(true)
- const [expandedSection, setExpandedSection] = useState('Basics')
- const toggleSection = useCallback(
- (title: string) => (isExpanded: boolean) => setExpandedSection(isExpanded ? title : null),
- [],
- )
- const { drawerWidth, isResizing, handleResizeStart } = useResizableDrawerWidth(configuratorRef)
-
- const networkControlState = useState(NetworkOptions[0])
- const [{ chainId }, setNetworkControlState] = networkControlState
-
- const tradeTypeState = useState(TRADE_MODES[0])
- const [currentTradeType] = tradeTypeState
-
- const localeState = useState('')
- const [locale] = localeState
-
- const tradeModesState = useState(TRADE_MODES)
- const [enabledTradeTypes] = tradeModesState
-
- const widgetHooksState = useState([])
- const [enabledWidgetHooks] = widgetHooksState
-
- const sellTokenState = useState(DEFAULT_STATE.sellToken)
- const sellTokenAmountState = useState(DEFAULT_STATE.sellAmount)
- const [sellToken] = sellTokenState
- const [sellTokenAmount] = sellTokenAmountState
-
- const buyTokenState = useState(DEFAULT_STATE.buyToken)
- const buyTokenAmountState = useState(DEFAULT_STATE.buyAmount)
- const [buyToken] = buyTokenState
- const [buyTokenAmount] = buyTokenAmountState
+ // Widget Configurator UI:
- const deadlineState = useState()
- const [deadline] = deadlineState
- const swapDeadlineState = useState()
- const [swapDeadline] = swapDeadlineState
- const limitDeadlineState = useState()
- const [limitDeadline] = limitDeadlineState
- const advancedDeadlineState = useState()
- const [advancedDeadline] = advancedDeadlineState
+ // Note these theme is for the widget configurator UI, not for the widget app / preview.
- const tokenListUrlsState = useState(DEFAULT_TOKEN_LISTS)
- const customTokensState = useState([])
- const [tokenListUrls] = tokenListUrlsState
- const [customTokens] = customTokensState
+ const [widgetTheme, _] = useState<'light' | 'dark'>('dark') // TODO: To be implemented...
- const partnerFeeBpsState = useState(0)
- const [partnerFeeBps] = partnerFeeBpsState
-
- const customImagesState = useState({})
- const customSoundsState = useState({})
- const [customImages] = customImagesState
- const [customSounds] = customSoundsState
-
- const [widgetAppBaseUrl, setWidgetAppBaseUrl] = useState('')
- const [rawParams, setRawParams] = useState()
- const [isWidgetDisplayed, setIsWidgetDisplayed] = useState(true)
-
- const paletteManager = useColorPaletteManager(mode)
- const { colorPalette, defaultPalette } = paletteManager
-
- const [iframeStyleJson, setIframeStyleJson] = useJsonState(EMPTY_JSON_STATE)
- const [cardStyleJson, setCardStyleJson] = useJsonState(EMPTY_JSON_STATE)
- const [appWrapperStyleJson, setAppWrapperStyleJson] = useJsonState(EMPTY_JSON_STATE)
- const [bodyWrapperStyleJson, setBodyWrapperStyleJson] = useJsonState(EMPTY_JSON_STATE)
-
- const { dialogOpen, handleDialogClose, handleDialogOpen } = useEmbedDialogState()
-
- const { closeToast, toasts, setToastMessagesInDappMode, disableToastMessages } = useToastsManager(setListeners)
- const firstToast = toasts?.[0]
-
- const [disableProgressBar, setDisableProgressBar] = useState(false)
- const setShowProgressBar = useCallback((enabled: boolean) => setDisableProgressBar(!enabled), [])
-
- const [disableCrossChainSwap, setDisableCrossChainSwap] = useState(false)
- const setAllowCrossChainSwap = useCallback((enabled: boolean) => setDisableCrossChainSwap(!enabled), [])
-
- const [disableTokenImport, setDisableTokenImport] = useState(false)
- const setAllowTokenImport = useCallback((enabled: boolean) => setDisableTokenImport(!enabled), [])
-
- const [hideRecentTokens, setHideRecentTokens] = useState(false)
- const setShowRecentTokens = useCallback((enabled: boolean) => setHideRecentTokens(!enabled), [])
-
- const [hideFavoriteTokens, setHideFavoriteTokens] = useState(false)
- const setShowFavoriteTokens = useCallback((enabled: boolean) => setHideFavoriteTokens(!enabled), [])
-
- const [hideBridgeInfo, setHideBridgeInfo] = useState(false)
- const setShowBridgeInfo = useCallback((enabled: boolean) => setHideBridgeInfo(!enabled), [])
+ // TODO: Pass resolved theme from MUI
+ useEffect(() => {
+ setThemeMode(widgetTheme)
+ }, [setThemeMode, widgetTheme])
- const [hideOrdersTable, setHideOrdersTable] = useState(false)
- const setShowOrdersTable = useCallback((enabled: boolean) => setHideOrdersTable(!enabled), [])
+ const [isWidgetReady, __] = useState(true) // TODO: To be implemented... Only if using latest production or localhost, sa older versions do not send events, so we do not know when they are ready.
+ const [isSidebarOpen, setIsSidebarOpen] = useState(true)
+ const [isSnippetOpen, setIsSnippetOpen] = useState(false)
+ const { drawerWidth, isResizing, handleResizeStart } = useResizableDrawerWidth(configuratorRef, DRAWER_WIDTH_CSS_VAR)
- const [disableTradeWhenPriceImpactIsUnknown, setDisableTradeWhenPriceImpactIsUnknown] = useState(false)
- const setBlockUnknownPriceImpact = useCallback((enabled: boolean) => {
- setDisableTradeWhenPriceImpactIsUnknown(enabled)
+ const handleSidebarToggle = useCallback(() => {
+ setIsSidebarOpen((prev) => !prev)
}, [])
- const [disableTradeWhenPriceImpactIsHigherThan, setDisableTradeWhenPriceImpactIsHigherThan] = useState<
- number | undefined
- >()
- const setBlockPriceImpactAboveValue = useCallback((event: ChangeEvent) => {
- const nextValue = event.target.value.trim()
-
- if (!nextValue) {
- setDisableTradeWhenPriceImpactIsHigherThan(undefined)
-
- return
- }
-
- const parsedValue = Number(nextValue)
-
- if (Number.isNaN(parsedValue)) return
-
- setDisableTradeWhenPriceImpactIsHigherThan(parsedValue)
+ const handleSnippetToggle = useCallback(() => {
+ setIsSnippetOpen((prev) => !prev)
}, [])
- const LINKS = [
- { icon: , label: 'View embed code', onClick: () => handleDialogOpen() },
- { icon: , label: 'Widget web', url: `https://cow.fi/widget/?${UTM_PARAMS}` },
- {
- icon: ,
- label: 'Developer docs',
- url: `https://docs.cow.fi/cow-protocol/tutorials/widget?${UTM_PARAMS}`,
- },
- ]
+ // Widget Configurator State:
- // Don't change chainId in the widget URL if the user is connected to a wallet
- // Because useSyncWidgetNetwork() will send a request to change the network
- const state: ConfiguratorState = {
- deadline,
- swapDeadline,
- limitDeadline,
- advancedDeadline,
- chainId: IS_IFRAME ? undefined : !isConnected || !walletChainId ? chainId : walletChainId,
- locale: locale || undefined,
- theme: mode,
- iframeStyle: iframeStyleJson.mergedValue,
- appWrapperStyle: appWrapperStyleJson.mergedValue,
- bodyWrapperStyle: bodyWrapperStyleJson.mergedValue,
- cardStyle: cardStyleJson.mergedValue,
- currentTradeType,
- enabledTradeTypes,
- enabledWidgetHooks,
- sellToken,
- sellTokenAmount,
- buyToken,
- buyTokenAmount,
- tokenListUrls,
- customColors: colorPalette,
- defaultColors: defaultPalette,
- partnerFeeBps,
- partnerFeeRecipient: DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK[chainId],
- standaloneMode,
- disableToastMessages,
- disableProgressBar,
- disableCrossChainSwap,
- disableTokenImport,
- hideRecentTokens,
- hideFavoriteTokens,
- hideBridgeInfo,
- hideOrdersTable,
- disableTradeWhenPriceImpactIsUnknown,
- disableTradeWhenPriceImpactIsHigherThan,
- }
-
- const rawParamsObject = useMemo(() => {
- if (!rawParams) return undefined
- try {
- return JSON.parse(rawParams)
- } catch {
- return undefined
- }
- }, [rawParams])
-
- const computedParams = useWidgetParams(state)
-
- const params = useMemo(() => {
- const trimmedWidgetAppBaseUrl = widgetAppBaseUrl.trim()
-
- return {
- ...computedParams,
- images: customImages,
- sounds: customSounds,
- customTokens,
- ...rawParamsObject,
- ...(trimmedWidgetAppBaseUrl ? { baseUrl: trimmedWidgetAppBaseUrl } : null),
- ...window.cowSwapWidgetParams,
- }
- }, [computedParams, customImages, customSounds, customTokens, rawParamsObject, widgetAppBaseUrl])
+ const [configuratorState, setConfiguratorState] = useState(null)
- const updateWidget = useCallback(() => {
- setIsWidgetDisplayed(false)
+ const params = useWidgetParams(configuratorState)
- setTimeout(() => setIsWidgetDisplayed(true), 100)
- }, [])
+ const [listeners, setListeners] = useState(COW_LISTENERS)
+ const toastManager = useToastsManager(setListeners)
+ const { closeToast, toasts } = toastManager
+ const firstToast = toasts?.[0]
- useEffect(() => {
- setThemeMode(mode)
- }, [setThemeMode, mode])
+ // Analytics: Fire an event to GA when user connect a wallet.
- // Fire an event to GA when user connect a wallet
useEffect(() => {
if (isConnected) {
cowAnalytics.sendEvent({
@@ -322,325 +83,59 @@ export function Configurator({ title }: { title: string }) {
}
}, [isConnected, cowAnalytics])
- useSyncWidgetNetwork(chainId, setNetworkControlState, standaloneMode)
+ let configuratorContent: React.ReactNode = null
- const availableChains = useAvailableChains()
+ if (!isWidgetReady || !params || !configuratorState) {
+ configuratorContent = (
+
+
+
+ )
+ } else if (isSnippetOpen) {
+ configuratorContent = (
+
+ )
+ } else {
+ configuratorContent = (
+
+
+
+ )
+ }
return (
- {!isDrawerOpen && (
- {
- e.stopPropagation()
- setIsDrawerOpen(true)
- }}
- sx={(theme) => ({
- ...DrawerToggleButtonStyled(theme),
- position: 'fixed',
- top: '1.6rem',
- left: '1.6rem',
- })}
- >
-
-
- )}
-
- ({
- ...DrawerStyled(theme),
- transition: isResizing ? 'none' : DRAWER_TRANSITION,
- })}
- variant="persistent"
- anchor="left"
- open={isDrawerOpen}
- >
-
-
- {!IS_IFRAME && (
- <>
- {!standaloneMode && (
-
- {/* Attempt 2 at fixing issue on Vercel build (locally it builds fine) */}
- {/* Error: apps/widget-configurator/src/app/configurator/index.tsx:272:17 - error TS2339: Property 'w3m-button' does not exist on type 'JSX.IntrinsicElements'.*/}
- {/* Fix from https://github.com/reown-com/appkit/issues/3093 */}
- {/* @ts-ignore */}
-
-
- )}
- >
- )}
-
-
-
- {!IS_IFRAME && }
-
-
-
-
-
-
- {!IS_IFRAME && (
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Global deadline
-
-
-
-
-
- Per-trade deadlines
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- setWidgetAppBaseUrl(e.target.value)}
- size="medium"
- placeholder={CONFIGURATOR_DEFAULT_WIDGET_BASE_URL}
- helperText={`Optional. Sets baseUrl (overrides Raw JSON). Default preview URL: ${CONFIGURATOR_DEFAULT_WIDGET_BASE_URL}`}
- />
- setRawParams(e.target.value)}
- size="medium"
- />
-
-
-
-
-
- {isDrawerOpen && (
- setIsDrawerOpen(false)}
- sx={(theme) => ({
- ...DrawerToggleButtonStyled(theme),
- width: '3.2rem',
- height: '3.2rem',
- position: 'absolute',
- top: '1.8rem',
- right: '1.2rem',
- })}
- >
-
-
- )}
-
-
- {LINKS.map(({ label, icon, url, onClick }) => (
-
- {icon}
-
-
- ))}
-
-
+
- {isDrawerOpen && (
-
-
-
- )}
+
-
- {params && (
- <>
-
- {isWidgetDisplayed && (
-
- )}
- >
- )}
-
+ {configuratorContent}
- handleDialogOpen()}
- >
-
- View Embed Code
-
= {
display: 'flex',
flexFlow: 'row nowrap',
width: '100%',
@@ -10,33 +8,10 @@ export const WrapperStyled = {
overflowX: 'hidden',
}
-export const DRAWER_TRANSITION = 'width 225ms cubic-bezier(0, 0, 0.2, 1)'
-
-// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-export const DrawerStyled = (theme: Theme) => ({
- width: `var(${DRAWER_WIDTH_CSS_VAR})`,
- flexShrink: 0,
-
- '& .MuiDrawer-paper': {
- width: `var(${DRAWER_WIDTH_CSS_VAR})`,
- boxSizing: 'border-box',
- display: 'flex',
- flexFlow: 'column',
- gap: '1.6rem',
- height: '100%',
- border: 0,
- background: theme.palette.background.paper,
- boxShadow: 'rgba(5, 43, 101, 0.06) 0 1.2rem 1.2rem',
- padding: '1.6rem',
- position: 'relative',
- overflowY: 'scroll',
- },
-})
-
const TRANSPARENCY_CHECKER_PX = 8
const CONTENT_PADDING_PX = 16
-export const ContentStyled: SxProps = (theme) => {
+export const configuratorCheckeredCanvasSx: SxProps = (theme) => {
const isDark = theme.palette.mode === 'dark'
const squareA = theme.palette.grey[isDark ? 900 : 200]
const squareB = theme.palette.grey[isDark ? 800 : 300]
@@ -73,73 +48,3 @@ export const ContentStyled: SxProps = (theme) => {
},
}
}
-
-export const WalletConnectionWrapper = {
- display: 'flex',
- justifyContent: 'center',
- margin: '0 auto 1rem',
- width: '100%',
-}
-
-export const ResizeHandleWrapperStyled = {
- position: 'relative',
- width: 0,
- height: '100%',
- flexShrink: 0,
-}
-
-export const ResizeHandleStyled = {
- position: 'absolute',
- inset: 0,
- width: '0.8rem',
- marginLeft: '-0.4rem',
- cursor: 'col-resize',
- zIndex: 2,
-
- '&::before': {
- content: '""',
- position: 'absolute',
- top: '1.6rem',
- bottom: '1.6rem',
- left: '50%',
- transform: 'translateX(-50%)',
- width: '0.2rem',
- borderRadius: '999px',
- backgroundColor: 'divider',
- },
-
- '&:hover::before': {
- backgroundColor: 'text.secondary',
- },
-}
-
-interface DrawerToggleButtonStyles {
- width: string
- height: string
- borderRadius: string
- border: string
- backgroundColor: string
- color: string
- boxShadow: string
- zIndex: number
- '&:hover': {
- backgroundColor: string
- boxShadow: string
- }
-}
-
-export const DrawerToggleButtonStyled = (theme: Theme): DrawerToggleButtonStyles => ({
- width: '3.6rem',
- height: '3.6rem',
- borderRadius: '50%',
- border: `1px solid ${theme.palette.divider}`,
- backgroundColor: theme.palette.background.paper,
- color: theme.palette.primary.main,
- boxShadow: 'none',
- zIndex: 3,
-
- '&:hover': {
- backgroundColor: theme.palette.action.hover,
- boxShadow: 'none',
- },
-})
diff --git a/apps/widget-configurator/src/app/configurator/types.ts b/apps/widget-configurator/src/app/configurator/types.ts
index a30f6b1f0b2..c4f22213a71 100644
--- a/apps/widget-configurator/src/app/configurator/types.ts
+++ b/apps/widget-configurator/src/app/configurator/types.ts
@@ -1,6 +1,7 @@
import type { SupportedChainId } from '@cowprotocol/cow-sdk'
import {
CowSwapWidgetPaletteColors,
+ CowSwapWidgetParams,
PartnerFee,
SlippageConfig,
TradeType,
@@ -22,6 +23,8 @@ export interface TokenListItem {
enabledForBuy: boolean
}
+export type WidgetMode = 'dapp' | 'standalone'
+
export interface ConfiguratorState {
chainId?: SupportedChainId
locale?: string
@@ -58,4 +61,8 @@ export interface ConfiguratorState {
disableTradeWhenPriceImpactIsUnknown: boolean
disableTradeWhenPriceImpactIsHigherThan: number | undefined
slippage?: SlippageConfig
+ customImages: CowSwapWidgetParams['images']
+ customSounds: CowSwapWidgetParams['sounds']
+ customTokens: CowSwapWidgetParams['customTokens']
+ rawParams: Partial
}
diff --git a/apps/widget-configurator/src/theme/paletteOptions.ts b/apps/widget-configurator/src/theme/paletteOptions.ts
index a13b95e597e..cca12e67e7c 100644
--- a/apps/widget-configurator/src/theme/paletteOptions.ts
+++ b/apps/widget-configurator/src/theme/paletteOptions.ts
@@ -16,7 +16,7 @@ export const darkPalette: PaletteOptions = {
disabled: 'rgba(202,233,255,0.6)',
},
background: {
- paper: 'rgb(12, 38, 75)',
+ paper: 'rgb(22, 23, 31)',
default: 'rgb(12, 38, 75)',
},
cow: {
From e037f0dde490c53ffbaaba75b89b88625d34e3e3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?=
Date: Thu, 9 Apr 2026 11:21:49 +0200
Subject: [PATCH 009/110] feat: add autoResizeEnabled prop to avoid scrollbar
flash while the iframe resizes
---
.../injectedWidget/updaters/IframeResizer.ts | 20 +++++-
.../configurator-sidebar.component.tsx | 19 ++++++
.../controls/BooleanSwitchControl.tsx | 20 +++++-
.../hooks/useWidgetParamsAndSettings.ts | 2 +
.../src/app/configurator/index.tsx | 10 ++-
.../src/app/configurator/styled.ts | 66 +++++++++----------
.../src/app/configurator/types.ts | 2 +
libs/widget-lib/src/types.ts | 8 +++
8 files changed, 107 insertions(+), 40 deletions(-)
diff --git a/apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.ts b/apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.ts
index 7802fd2aa3a..063c3b26ed4 100644
--- a/apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.ts
+++ b/apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.ts
@@ -7,12 +7,28 @@ import { WidgetMethodsEmit, widgetIframeTransport } from '@cowprotocol/widget-li
import { openModalState } from 'common/state/openModalState'
+import { useInjectedWidgetParams } from '../hooks/useInjectedWidgetParams'
+
export function IframeResizer(): null {
const isModalOpen = useAtomValue(openModalState)
const previousHeightRef = useRef(0)
+ const { autoResizeEnabled } = useInjectedWidgetParams()
+
+ useLayoutEffect(() => {
+ // Checking for `autoResizeEnabled === undefined` here to preserve the old behavior of the widget, when `autoResizeEnabled` didn't exist:
+ if (!isIframe() || !isInjectedWidget() || autoResizeEnabled === undefined) return
+
+ if (autoResizeEnabled) {
+ document.documentElement.style.overflow = 'hidden'
+ } else {
+ document.documentElement.style.removeProperty('overflow')
+ }
+ }, [autoResizeEnabled])
+ // eslint-disable-next-line complexity
useLayoutEffect(() => {
- if (!isIframe() || !isInjectedWidget()) return
+ // Checking for `autoResizeEnabled === false` here to preserve the old behavior of the widget, when `autoResizeEnabled` didn't exist:
+ if (!isIframe() || !isInjectedWidget() || autoResizeEnabled === false) return
const contentElement = getContentElement(document)
@@ -71,7 +87,7 @@ export function IframeResizer(): null {
resizeObserver?.disconnect()
mutationObserver?.disconnect()
}
- }, [isModalOpen])
+ }, [isModalOpen, autoResizeEnabled])
return null
}
diff --git a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.component.tsx b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.component.tsx
index 7e03c0b029e..eeaa87230db 100644
--- a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.component.tsx
+++ b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.component.tsx
@@ -122,6 +122,9 @@ export function ConfiguratorSidebar({
// Layout Section:
+ const [autoResizeEnabled, setAutoResizeEnabled] = useState(true)
+ const [showIframeOutline, setShowIframeOutline] = useState(true)
+
const [iframeStyleJson, setIframeStyleJson] = useJsonState(EMPTY_JSON_STATE)
const [cardStyleJson, setCardStyleJson] = useJsonState(EMPTY_JSON_STATE)
const [appWrapperStyleJson, setAppWrapperStyleJson] = useJsonState(EMPTY_JSON_STATE)
@@ -226,6 +229,7 @@ export function ConfiguratorSidebar({
chainId: effectiveChainId,
locale: locale || undefined,
theme: mode,
+ showIframeOutline,
iframeStyle: iframeStyleJson.mergedValue,
appWrapperStyle: appWrapperStyleJson.mergedValue,
bodyWrapperStyle: bodyWrapperStyleJson.mergedValue,
@@ -243,6 +247,7 @@ export function ConfiguratorSidebar({
partnerFeeBps,
partnerFeeRecipient: DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK[chainId],
standaloneMode,
+ autoResizeEnabled,
disableToastMessages: toastManager.disableToastMessages,
disableProgressBar,
disableCrossChainSwap,
@@ -267,6 +272,7 @@ export function ConfiguratorSidebar({
chainId,
locale,
mode,
+ showIframeOutline,
iframeStyleJson.mergedValue,
appWrapperStyleJson.mergedValue,
bodyWrapperStyleJson.mergedValue,
@@ -283,6 +289,7 @@ export function ConfiguratorSidebar({
defaultPalette,
partnerFeeBps,
standaloneMode,
+ autoResizeEnabled,
toastManager.disableToastMessages,
disableProgressBar,
disableCrossChainSwap,
@@ -379,6 +386,18 @@ export function ConfiguratorSidebar({
+
+
void
helperText?: ReactNode
+ tooltip?: string
}
-export function BooleanSwitchControl({ checked, label, onChange, helperText }: BooleanSwitchControlProps): ReactNode {
+export function BooleanSwitchControl({
+ checked,
+ label,
+ onChange,
+ helperText,
+ tooltip,
+}: BooleanSwitchControlProps): ReactNode {
+ const labelContent = tooltip ? (
+
+ {label}
+
+ ) : (
+ label
+ )
+
return (
onChange(nextChecked)} />}
/>
diff --git a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts
index 51a5ff5a0d1..977a70766db 100644
--- a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts
+++ b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts
@@ -177,6 +177,7 @@ function buildWidgetParams(configuratorState: ConfiguratorState | null): CowSwap
partnerFeeBps,
partnerFeeRecipient,
standaloneMode,
+ autoResizeEnabled,
disableToastMessages,
disableProgressBar,
disableCrossChainSwap,
@@ -217,6 +218,7 @@ function buildWidgetParams(configuratorState: ConfiguratorState | null): CowSwap
bodyWrapperStyle,
cardStyle,
standaloneMode,
+ autoResizeEnabled,
disableToastMessages,
disableProgressBar,
disableCrossChainSwap,
diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/index.tsx
index d150395a748..70bda9ff2ea 100644
--- a/apps/widget-configurator/src/app/configurator/index.tsx
+++ b/apps/widget-configurator/src/app/configurator/index.tsx
@@ -65,6 +65,8 @@ export function Configurator({ title }: { title: string }): ReactNode {
const [configuratorState, setConfiguratorState] = useState(null)
+ const showIframeOutline = configuratorState?.showIframeOutline ?? true
+
const params = useWidgetParams(configuratorState)
const [listeners, setListeners] = useState(COW_LISTENERS)
@@ -87,8 +89,10 @@ export function Configurator({ title }: { title: string }): ReactNode {
if (!isWidgetReady || !params || !configuratorState) {
configuratorContent = (
-
-
+
+
+
+
)
} else if (isSnippetOpen) {
@@ -102,7 +106,7 @@ export function Configurator({ title }: { title: string }): ReactNode {
)
} else {
configuratorContent = (
-
+
= {
const TRANSPARENCY_CHECKER_PX = 8
const CONTENT_PADDING_PX = 16
-export const configuratorCheckeredCanvasSx: SxProps = (theme) => {
- const isDark = theme.palette.mode === 'dark'
- const squareA = theme.palette.grey[isDark ? 900 : 200]
- const squareB = theme.palette.grey[isDark ? 800 : 300]
- const base = theme.palette.grey[isDark ? 900 : 200]
- const pattern = `repeating-conic-gradient(from 90deg, ${squareA} 0% 25%, ${squareB} 0% 50%)`
+export const configuratorCheckeredCanvasSx =
+ (showIframeOutline: boolean): SxProps =>
+ (theme) => {
+ const isDark = theme.palette.mode === 'dark'
+ const squareA = theme.palette.grey[isDark ? 900 : 200]
+ const squareB = theme.palette.grey[isDark ? 800 : 300]
+ const base = theme.palette.grey[isDark ? 900 : 200]
+ const pattern = `repeating-conic-gradient(from 90deg, ${squareA} 0% 25%, ${squareB} 0% 50%)`
- return {
- width: 0,
- display: 'flex',
- justifyContent: 'flex-start',
- alignItems: 'flex-start',
- flexFlow: 'column',
- flex: '1 1 auto',
- minWidth: 0,
- overflow: 'auto',
- padding: `${CONTENT_PADDING_PX}px`,
- backgroundColor: base,
+ return {
+ flex: '1 1 auto',
+ overflow: 'auto',
+ padding: `${CONTENT_PADDING_PX}px`,
+ backgroundColor: base,
- '& > div': {
- backgroundImage: `${pattern}`,
- backgroundSize: `${TRANSPARENCY_CHECKER_PX}px ${TRANSPARENCY_CHECKER_PX}px`,
- backgroundRepeat: 'repeat',
- backgroundPosition: `right ${CONTENT_PADDING_PX}px top ${CONTENT_PADDING_PX}px`,
- backgroundClip: 'content-box',
- backgroundOrigin: 'content-box',
- },
+ '& > div': {
+ minWidth: '100%',
+ minHeight: '100%',
+ backgroundImage: `${pattern}`,
+ backgroundSize: `${TRANSPARENCY_CHECKER_PX}px ${TRANSPARENCY_CHECKER_PX}px`,
+ backgroundRepeat: 'repeat',
+ backgroundPosition: `right ${CONTENT_PADDING_PX}px top ${CONTENT_PADDING_PX}px`,
+ backgroundClip: 'content-box',
+ backgroundOrigin: 'content-box',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
- '& > div > iframe': {
- display: 'block',
- border: 0,
- margin: '0 auto',
- outline: '1px dashed cyan',
- //overflow: 'auto',
- },
+ '& > div > iframe': {
+ display: 'block',
+ border: 0,
+ margin: '0 auto',
+ outline: showIframeOutline ? '1px dashed cyan' : 'none',
+ },
+ }
}
-}
diff --git a/apps/widget-configurator/src/app/configurator/types.ts b/apps/widget-configurator/src/app/configurator/types.ts
index c4f22213a71..3763fbfd91f 100644
--- a/apps/widget-configurator/src/app/configurator/types.ts
+++ b/apps/widget-configurator/src/app/configurator/types.ts
@@ -29,6 +29,7 @@ export interface ConfiguratorState {
chainId?: SupportedChainId
locale?: string
theme: PaletteMode
+ showIframeOutline: boolean
iframeStyle: CSS.Properties
appWrapperStyle: CSS.Properties
bodyWrapperStyle: CSS.Properties
@@ -50,6 +51,7 @@ export interface ConfiguratorState {
partnerFeeBps: number
partnerFeeRecipient: PartnerFee['recipient']
standaloneMode: boolean
+ autoResizeEnabled: boolean
disableToastMessages: boolean
disableProgressBar: boolean
disableCrossChainSwap: boolean
diff --git a/libs/widget-lib/src/types.ts b/libs/widget-lib/src/types.ts
index 7aa1b8ee7c0..3968c00e7f0 100644
--- a/libs/widget-lib/src/types.ts
+++ b/libs/widget-lib/src/types.ts
@@ -375,6 +375,14 @@ export interface CowSwapWidgetParams {
*/
disableProgressBar?: boolean
+ /**
+ * Dynamically adjust the iframe height to match the content height.
+ * If enabled, no scrollbar will be shown inside the widget iframe.
+ *
+ * Defaults to true.
+ */
+ autoResizeEnabled?: boolean
+
/**
* Disables showing the toast messages.
* Some UI might want to disable it and subscribe to WidgetMethodsEmit.ON_TOAST_MESSAGE event to handle the toast messages itself.
From b36c65ca58016ed1923a16e208bdc8b9261af942 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?=
Date: Thu, 9 Apr 2026 14:02:26 +0200
Subject: [PATCH 010/110] feat: improve sidebar and snippet layout and styles
---
apps/widget-configurator/package.json | 1 +
.../configurator-sidebar.component.tsx | 37 ++--
.../configurator-sidebar.styles.ts | 41 ++--
.../controls/sidebar-controls.component.tsx | 5 +-
.../controls/sidebar-controls.styles.ts | 35 ++-
.../footer/sidebar-footer.component.tsx | 206 ++++++++++++++----
.../footer/sidebar-footer.styles.ts | 0
.../header/sidebar-header.component.tsx | 88 ++++++++
.../components/controls/AccordionSection.tsx | 6 +-
.../controls/ConfiguratorBrandHeader.test.tsx | 25 ---
.../controls/ConfiguratorBrandHeader.tsx | 56 -----
.../configurator/hooks/useToastsManager.tsx | 2 +-
.../src/app/configurator/index.tsx | 29 +--
.../src/app/configurator/styled.ts | 9 +-
.../src/app/embedDialog/index.tsx | 95 +++++---
libs/widget-react/src/lib/CowSwapWidget.tsx | 2 +-
pnpm-lock.yaml | 11 +-
17 files changed, 411 insertions(+), 237 deletions(-)
delete mode 100644 apps/widget-configurator/src/app/configurator/components/configurator-sidebar/footer/sidebar-footer.styles.ts
create mode 100644 apps/widget-configurator/src/app/configurator/components/configurator-sidebar/header/sidebar-header.component.tsx
delete mode 100644 apps/widget-configurator/src/app/configurator/components/controls/ConfiguratorBrandHeader.test.tsx
delete mode 100644 apps/widget-configurator/src/app/configurator/components/controls/ConfiguratorBrandHeader.tsx
diff --git a/apps/widget-configurator/package.json b/apps/widget-configurator/package.json
index e7e3a221233..1e352108de1 100644
--- a/apps/widget-configurator/package.json
+++ b/apps/widget-configurator/package.json
@@ -43,6 +43,7 @@
"mui-color-input": "^2.0.1",
"react": "19.1.2",
"react-dom": "19.1.2",
+ "react-feather": "^2.0.10",
"react-inlinesvg": "^4.1.5",
"react-syntax-highlighter": "^15.5.0"
},
diff --git a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.component.tsx b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.component.tsx
index eeaa87230db..95df2ea40b1 100644
--- a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.component.tsx
+++ b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.component.tsx
@@ -9,9 +9,10 @@ import Drawer from '@mui/material/Drawer'
import Stack from '@mui/material/Stack'
import TextField from '@mui/material/TextField'
import Typography from '@mui/material/Typography'
+import type { Theme } from '@mui/material/styles'
import { useWeb3ModalAccount } from '@web3modal/ethers5/react'
-import { DRAWER_TRANSITION, DrawerStyled, WalletConnectionWrapper } from './configurator-sidebar.styles'
+import { getDrawerSx } from './configurator-sidebar.styles'
import { SidebarFooter } from './footer/sidebar-footer.component'
import { ColorModeContext } from '../../../../theme/ColorModeContext'
@@ -25,7 +26,7 @@ import { ConfiguratorState, TokenListItem, WidgetMode } from '../../types'
import { AccordionSection } from '../controls/AccordionSection'
import { AppearanceStyleControls } from '../controls/AppearanceStyleControls'
import { BooleanSwitchControl } from '../controls/BooleanSwitchControl'
-import { ConfiguratorBrandHeader } from '../controls/ConfiguratorBrandHeader'
+import { SidebarHeader } from './header/sidebar-header.component'
import { CurrencyInputControl } from '../controls/CurrencyInputControl'
import { CurrentTradeTypeControl } from '../controls/CurrentTradeTypeControl'
import { CustomImagesControl } from '../controls/CustomImagesControl'
@@ -48,6 +49,7 @@ export interface ConfiguratorSidebarProps {
isOpen: boolean
isResizing: boolean
isSnippetOpen: boolean
+ onSidebarToggle: () => void
onSnippetToggle: () => void
onStateChange: (state: ConfiguratorState) => void
toastManager: UseToastsManagerReturn
@@ -59,6 +61,7 @@ export function ConfiguratorSidebar({
isOpen,
isResizing,
isSnippetOpen,
+ onSidebarToggle,
onSnippetToggle,
onStateChange,
toastManager,
@@ -315,31 +318,14 @@ export function ConfiguratorSidebar({
return (
({
- ...DrawerStyled(theme),
- transition: isResizing ? 'none' : DRAWER_TRANSITION,
- })}
+ sx={(theme: Theme) => getDrawerSx(theme, isResizing)}
variant="persistent"
anchor="left"
open={isOpen}
>
-
-
- {!IS_IFRAME && (
- <>
- {!standaloneMode && (
-
- {/* Attempt 2 at fixing issue on Vercel build (locally it builds fine) */}
- {/* Error: apps/widget-configurator/src/app/configurator/index.tsx:272:17 - error TS2339: Property 'w3m-button' does not exist on type 'JSX.IntrinsicElements'.*/}
- {/* Fix from https://github.com/reown-com/appkit/issues/3093 */}
- {/* @ts-ignore */}
-
-
- )}
- >
- )}
+
-
+
{!IS_IFRAME && }
@@ -526,7 +512,12 @@ export function ConfiguratorSidebar({
-
+
)
}
diff --git a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.styles.ts b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.styles.ts
index 4865fd3e7af..905078da8cc 100644
--- a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.styles.ts
+++ b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.styles.ts
@@ -1,33 +1,24 @@
-import type { Theme } from '@mui/material/styles'
+import type { CSSObject, Theme } from '@mui/material/styles'
export const DRAWER_TRANSITION = 'width 225ms cubic-bezier(0, 0, 0.2, 1)'
export const DRAWER_WIDTH_CSS_VAR = '--widget-configurator-drawer-width'
-// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
-export const DrawerStyled = (theme: Theme) => ({
- width: `var(${DRAWER_WIDTH_CSS_VAR})`,
- flexShrink: 0,
-
- '& .MuiDrawer-paper': {
+export function getDrawerSx(theme: Theme, isResizing: boolean): CSSObject {
+ return {
width: `var(${DRAWER_WIDTH_CSS_VAR})`,
- boxSizing: 'border-box',
- display: 'flex',
- flexFlow: 'column',
- gap: '1.6rem',
- height: '100%',
- border: 0,
- background: theme.palette.background.paper,
- boxShadow: 'rgba(5, 43, 101, 0.06) 0 1.2rem 1.2rem',
- padding: '1.6rem',
- position: 'relative',
- overflowY: 'scroll',
- },
-})
+ flexShrink: 0,
+ transition: isResizing ? 'none' : DRAWER_TRANSITION,
-export const WalletConnectionWrapper = {
- display: 'flex',
- justifyContent: 'center',
- margin: '0 auto 1rem',
- width: '100%',
+ '& .MuiDrawer-paper': {
+ width: `var(${DRAWER_WIDTH_CSS_VAR})`,
+ boxSizing: 'border-box',
+ height: '100%',
+ border: 0,
+ background: theme.palette.background.paper,
+ boxShadow: 'none',
+ position: 'relative',
+ overflowY: 'scroll',
+ },
+ }
}
diff --git a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.component.tsx b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.component.tsx
index fc80c9cc1f0..1500a48acde 100644
--- a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.component.tsx
+++ b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.component.tsx
@@ -1,4 +1,5 @@
import { ReactNode } from 'react'
+import { ChevronLeft, ChevronRight } from 'react-feather'
import { Box, IconButton } from '@mui/material'
@@ -20,10 +21,12 @@ export function SidebarControls({ isSidebarOpen, toggleSidebarOpen, onResizeStar
- {isSidebarOpen ? '<' : '>'}
+ {isSidebarOpen ? : }
{isSidebarOpen ? (
diff --git a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.styles.ts b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.styles.ts
index c0faab1685c..3efdcd7221d 100644
--- a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.styles.ts
+++ b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.styles.ts
@@ -3,28 +3,39 @@ import { Theme } from '@mui/material/styles'
export const sidebarControlsZeroWidthColumnSx: SxProps = {
position: 'relative',
+ zIndex: 2000,
width: 0,
height: '100%',
flexShrink: 0,
}
export const sidebarToggleOpenButton: SxProps = (theme: Theme) => ({
- position: 'fixed',
- top: '1.6rem',
- left: '1.6rem',
-
- width: '3.6rem',
- height: '3.6rem',
+ position: 'absolute',
+ top: "50%",
+ left: 0,
+ width: 48,
+ height: 48,
+ p: 0,
+ pl:"24px",
+ pr: "4px",
+ transform: 'translate(-50%, -50%)',
borderRadius: '50%',
- border: `1px solid ${theme.palette.divider}`,
- backgroundColor: theme.palette.background.paper,
+ border: `none`,
+ backgroundColor: theme.palette.grey[theme.palette.mode === 'dark' ? 900 : 200],
color: theme.palette.primary.main,
boxShadow: 'none',
zIndex: 3,
+ transition: 'opacity 0.2s ease-in-out, transform 0.2s ease-in-out',
+
+ '&[aria-hidden="true"]': {
+ opacity: 0,
+ pointerEvents: 'none',
+ },
'&:hover': {
- backgroundColor: theme.palette.action.hover,
boxShadow: 'none',
+ backgroundColor: theme.palette.grey[theme.palette.mode === 'dark' ? 900 : 200],
+ transform: 'translate(-50%, -50%) scale(2)',
},
})
@@ -39,11 +50,11 @@ export const sidebarResizeHandle: SxProps = {
'&::before': {
content: '""',
position: 'absolute',
- top: '1.6rem',
- bottom: '1.6rem',
+ top: 16,
+ bottom: 16,
left: '50%',
transform: 'translateX(-50%)',
- width: '0.2rem',
+ width: 4,
borderRadius: '999px',
backgroundColor: 'divider',
},
diff --git a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/footer/sidebar-footer.component.tsx b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/footer/sidebar-footer.component.tsx
index 383c20a1465..a9eca3bb057 100644
--- a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/footer/sidebar-footer.component.tsx
+++ b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/footer/sidebar-footer.component.tsx
@@ -1,55 +1,177 @@
-import React, { ReactNode, useMemo } from 'react'
+import React, { ReactNode } from 'react'
-import List from '@mui/material/List'
-import ListItemButton from '@mui/material/ListItemButton'
-import ListItemText from '@mui/material/ListItemText'
+import { ChevronLeft, ChevronRight, Code, Eye, Moon, Sun } from 'react-feather'
+
+import Box from '@mui/material/Box'
+import Button from '@mui/material/Button'
+import IconButton from '@mui/material/IconButton'
+import Link from '@mui/material/Link'
+import Tooltip from '@mui/material/Tooltip'
import { UTM_PARAMS } from '../../../consts'
-interface LinkConfig {
- label: string
- url?: string
- onClick?: () => void
-}
+const WIDGET_WEB_URL = `https://cow.fi/widget/?${UTM_PARAMS}`
+const DEVELOPER_DOCS_URL = `https://docs.cow.fi/cow-protocol/tutorials/widget?${UTM_PARAMS}`
export interface SidebarFooterProps {
+ isSidebarOpen: boolean
+ onSidebarToggle: () => void
isSnippetOpen: boolean
onSnippetToggle: () => void
}
-export function SidebarFooter({ isSnippetOpen, onSnippetToggle }: SidebarFooterProps): ReactNode {
- const links: LinkConfig[] = useMemo(() => {
- return [
- isSnippetOpen
- ? { label: 'View preview', onClick: onSnippetToggle }
- : { label: 'View code snippet', onClick: onSnippetToggle },
- { label: 'Widget web', url: `https://cow.fi/widget/?${UTM_PARAMS}` },
- {
- label: 'Developer docs',
- url: `https://docs.cow.fi/cow-protocol/tutorials/widget?${UTM_PARAMS}`,
- },
- ]
- }, [isSnippetOpen, onSnippetToggle])
-
- return (
-
+
+
+ t.palette.background.paper,
+ borderTop: (t) => `1px solid ${t.palette.divider}`,
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 2,
+ px: 2,
+ pt: 2,
+ mt: "auto",
+ }}
>
- {links.map(({ label, url, onClick }) => (
-
+ }
+ sx={{
+ borderRadius: 1,
+ border: '1px solid',
+ borderColor: 'divider',
+ pl: 1.5,
+ pr: 2,
+ py: 1,
+ fontSize: '12px',
+ fontWeight: "bold",
+ textTransform: 'uppercase',
+ color: 'text.primary',
+ height: 40,
+ mr: "auto",
+
+ '& .MuiButton-endIcon': { ml: 1.5 },
+ }}
+ >
+ {snippetLabel}
+
+
+
+ {}}
+ aria-label={themeLabel}
+ size="small"
+ sx={iconOnlyButtonSx}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Widget web
+
+
-
-
- ))}
-
- )
+ Developer docs
+
+
+
+ >)
}
diff --git a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/footer/sidebar-footer.styles.ts b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/footer/sidebar-footer.styles.ts
deleted file mode 100644
index e69de29bb2d..00000000000
diff --git a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/header/sidebar-header.component.tsx b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/header/sidebar-header.component.tsx
new file mode 100644
index 00000000000..2159de96f57
--- /dev/null
+++ b/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/header/sidebar-header.component.tsx
@@ -0,0 +1,88 @@
+import { ReactNode } from 'react'
+
+import { Color, Font, ProductLogo, ProductVariant } from '@cowprotocol/ui'
+
+import Box from '@mui/material/Box'
+import Typography from '@mui/material/Typography'
+import { IS_IFRAME } from '../../../consts'
+
+const BRAND_COLOR: Record = {
+ dark: Color.blue300Primary,
+ light: Color.blueDark2,
+}
+
+export type ThemeMode = 'dark' | 'light'
+
+export interface SidebarHeaderProps {
+ title: string
+ themeMode: ThemeMode
+ standaloneMode: boolean
+}
+
+export function SidebarHeader({
+ title,
+ themeMode,
+ standaloneMode
+}: SidebarHeaderProps): ReactNode {
+ const brandColor = BRAND_COLOR[themeMode]
+
+ return (
+ theme.palette.background.paper,
+ borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
+ }}
+ >
+
+
+
+ {title}
+
+
+
+ {!IS_IFRAME && (
+ <>
+ {!standaloneMode && (
+
+ {/* Attempt 2 at fixing issue on Vercel build (locally it builds fine) */}
+ {/* Error: apps/widget-configurator/src/app/configurator/index.tsx:272:17 - error TS2339: Property 'w3m-button' does not exist on type 'JSX.IntrinsicElements'.*/}
+ {/* Fix from https://github.com/reown-com/appkit/issues/3093 */}
+ {/* @ts-ignore */}
+
+
+ )}
+ >
+ )}
+
+ )
+}
diff --git a/apps/widget-configurator/src/app/configurator/components/controls/AccordionSection.tsx b/apps/widget-configurator/src/app/configurator/components/controls/AccordionSection.tsx
index 9315354d928..5833adce868 100644
--- a/apps/widget-configurator/src/app/configurator/components/controls/AccordionSection.tsx
+++ b/apps/widget-configurator/src/app/configurator/components/controls/AccordionSection.tsx
@@ -23,15 +23,11 @@ export function AccordionSection({ title, expanded, onChange, children }: Accord
elevation={0}
slotProps={{ transition: { unmountOnExit: true } }}
sx={{
- borderTop: (theme) => `1px solid ${theme.palette.divider}`,
+ borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
borderRadius: '0 !important',
overflow: 'hidden',
'&:before': { display: 'none' },
-
- '&:last-child': {
- borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
- },
}}
>
({
- Font: {
- family: 'studiofeixen',
- weight: {
- bold: 700,
- },
- },
- ProductVariant: {
- CowSwap: 'cowSwap',
- },
- ProductLogo: () => ,
-}))
-
-import { ConfiguratorBrandHeader } from './ConfiguratorBrandHeader'
-
-describe('ConfiguratorBrandHeader', () => {
- it('renders the CoW Widget heading with the shared product logo', () => {
- render()
-
- expect(screen.getByRole('heading', { name: 'CoW Widget' })).not.toBeNull()
- expect(screen.getByTestId('product-logo')).not.toBeNull()
- })
-})
diff --git a/apps/widget-configurator/src/app/configurator/components/controls/ConfiguratorBrandHeader.tsx b/apps/widget-configurator/src/app/configurator/components/controls/ConfiguratorBrandHeader.tsx
deleted file mode 100644
index 94563b7a7ae..00000000000
--- a/apps/widget-configurator/src/app/configurator/components/controls/ConfiguratorBrandHeader.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { ReactNode } from 'react'
-
-import { Color, Font, ProductLogo, ProductVariant } from '@cowprotocol/ui'
-
-import Box from '@mui/material/Box'
-import Typography from '@mui/material/Typography'
-
-interface ConfiguratorBrandHeaderProps {
- title: string
- themeMode: 'dark' | 'light'
-}
-
-const BRAND_COLOR: Record = {
- dark: Color.blue300Primary,
- light: Color.blueDark2,
-}
-
-export function ConfiguratorBrandHeader({ title, themeMode }: ConfiguratorBrandHeaderProps): ReactNode {
- const brandColor = BRAND_COLOR[themeMode]
-
- return (
-
-
-
- {title}
-
-
- )
-}
diff --git a/apps/widget-configurator/src/app/configurator/hooks/useToastsManager.tsx b/apps/widget-configurator/src/app/configurator/hooks/useToastsManager.tsx
index e5b1bb26b5f..115fbd11066 100644
--- a/apps/widget-configurator/src/app/configurator/hooks/useToastsManager.tsx
+++ b/apps/widget-configurator/src/app/configurator/hooks/useToastsManager.tsx
@@ -65,7 +65,7 @@ export function useToastsManager(setListeners: (listeners: CowWidgetEventListene
},
})
} else {
- openToast('Disable, toast messages. Self-contained widget toasts.')
+ openToast('Toast messages are disabled. Self-contained widget toasts.')
}
setListeners(newListeners)
diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/index.tsx
index 70bda9ff2ea..36ae033766e 100644
--- a/apps/widget-configurator/src/app/configurator/index.tsx
+++ b/apps/widget-configurator/src/app/configurator/index.tsx
@@ -1,3 +1,5 @@
+/* eslint-disable max-lines-per-function */
+
import React, { CSSProperties, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
import { useCowAnalytics } from '@cowprotocol/analytics'
@@ -48,7 +50,7 @@ export function Configurator({ title }: { title: string }): ReactNode {
setThemeMode(widgetTheme)
}, [setThemeMode, widgetTheme])
- const [isWidgetReady, __] = useState(true) // TODO: To be implemented... Only if using latest production or localhost, sa older versions do not send events, so we do not know when they are ready.
+ const [isWidgetReady, __] = useState(true) // TODO: To be implemented... Only if using latest production or localhost, as older versions do not send events, so we do not know when they are ready.
const [isSidebarOpen, setIsSidebarOpen] = useState(true)
const [isSnippetOpen, setIsSnippetOpen] = useState(false)
const { drawerWidth, isResizing, handleResizeStart } = useResizableDrawerWidth(configuratorRef, DRAWER_WIDTH_CSS_VAR)
@@ -95,25 +97,25 @@ export function Configurator({ title }: { title: string }): ReactNode {