From 4e2b539cb81ecd70b1882fdc43dbd300b7e0a51e Mon Sep 17 00:00:00 2001 From: fairlighteth <31534717+fairlighteth@users.noreply.github.com> Date: Mon, 6 Apr 2026 18:05:25 +0100 Subject: [PATCH 001/110] feat(widget-configurator): enhance configurator with new controls and styles --- .../containers/App/styled.test.tsx | 100 ++++ .../application/containers/App/styled.ts | 32 +- .../updaters/BalancesDevtools.test.tsx | 44 ++ .../updaters/BalancesDevtools.tsx | 4 +- .../updaters/IframeResizer.test.tsx | 273 +++++++++++ .../injectedWidget/updaters/IframeResizer.ts | 63 ++- .../trade/containers/TradeWidget/styled.tsx | 2 +- .../src/theme/mapWidgetTheme.test.ts | 4 + .../src/theme/mapWidgetTheme.ts | 4 +- apps/cowswap-frontend/src/theme/types.ts | 2 + apps/widget-configurator/package.json | 2 + .../src/app/configurator/consts.ts | 12 + .../controls/AccordionSection.test.tsx | 39 ++ .../controls/AccordionSection.tsx | 47 ++ .../controls/BooleanSwitchControl.test.tsx | 29 ++ .../controls/BooleanSwitchControl.tsx | 37 ++ .../controls/ConfiguratorBrandHeader.test.tsx | 25 + .../controls/ConfiguratorBrandHeader.tsx | 56 +++ .../controls/HelpTooltipButton.tsx | 150 ++++++ .../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 +++++ .../controls/LocaleControl.test.tsx | 11 + .../configurator/controls/LocaleControl.tsx | 14 +- .../controls/ModeControl.test.tsx | 19 + .../app/configurator/controls/ModeControl.tsx | 54 +++ .../controls/PaletteControl.test.tsx | 50 ++ .../configurator/controls/PaletteControl.tsx | 33 +- .../configurator/controls/SettingHeading.tsx | 20 + .../controls/ThemeControl.test.tsx | 51 ++ .../configurator/controls/ThemeControl.tsx | 81 +++- .../WidgetBorderRadiusControl.test.tsx | 24 + .../controls/WidgetBorderRadiusControl.tsx | 80 +++ .../controls/WidgetHooksControl.tsx | 18 +- .../controls/WidgetPaddingControl.test.tsx | 30 ++ .../controls/WidgetPaddingControl.tsx | 78 +++ .../controls/WidgetShadowControl.test.tsx | 28 ++ .../controls/WidgetShadowControl.tsx | 105 ++++ .../hooks/useResizableDrawerWidth.test.ts | 15 + .../hooks/useResizableDrawerWidth.ts | 135 ++++++ .../configurator/hooks/useToastsManager.tsx | 12 +- .../hooks/useWidgetParamsAndSettings.ts | 184 ++++--- .../src/app/configurator/index.tsx | 459 +++++++++--------- .../src/app/configurator/styled.ts | 83 +++- .../src/app/configurator/types.ts | 5 + .../src/app/embedDialog/const.ts | 7 +- .../widget-configurator/src/declarations.d.ts | 1 + apps/widget-configurator/src/main.tsx | 50 +- apps/widget-configurator/tsconfig.spec.json | 7 +- libs/widget-lib/src/cowSwapWidget.spec.ts | 109 +++++ libs/widget-lib/src/cowSwapWidget.ts | 58 ++- libs/widget-lib/src/types.ts | 28 +- libs/widget-lib/src/urlUtils.spec.ts | 10 +- pnpm-lock.yaml | 6 + 57 files changed, 2769 insertions(+), 403 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/application/containers/App/styled.test.tsx create mode 100644 apps/cowswap-frontend/src/modules/balancesAndAllowances/updaters/BalancesDevtools.test.tsx create mode 100644 apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.test.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/AccordionSection.test.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/AccordionSection.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/BooleanSwitchControl.test.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/BooleanSwitchControl.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/ConfiguratorBrandHeader.test.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/ConfiguratorBrandHeader.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/HelpTooltipButton.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/IframeBackgroundColorControl.test.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/IframeBackgroundColorControl.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/IframeBorderRadiusControl.test.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/IframeBorderRadiusControl.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/IframeWidthControl.test.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/IframeWidthControl.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/LocaleControl.test.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/ModeControl.test.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/ModeControl.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/PaletteControl.test.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/SettingHeading.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/ThemeControl.test.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/WidgetBorderRadiusControl.test.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/WidgetBorderRadiusControl.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/WidgetPaddingControl.test.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/WidgetPaddingControl.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/WidgetShadowControl.test.tsx create mode 100644 apps/widget-configurator/src/app/configurator/controls/WidgetShadowControl.tsx create mode 100644 apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.test.ts create mode 100644 apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.ts create mode 100644 apps/widget-configurator/src/declarations.d.ts create mode 100644 libs/widget-lib/src/cowSwapWidget.spec.ts diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/styled.test.tsx b/apps/cowswap-frontend/src/modules/application/containers/App/styled.test.tsx new file mode 100644 index 00000000000..682fe7196e4 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/application/containers/App/styled.test.tsx @@ -0,0 +1,100 @@ +import { render } from '@testing-library/react' +import { ThemeProvider as StyledComponentsThemeProvider } from 'styled-components/macro' + +import { AppWrapper, BodyWrapper, Marginer } from './styled' + +describe('AppWrapper', () => { + it('does not enforce a minimum viewport height in widget mode', () => { + const { getByTestId } = render( + + + , + ) + + expect(window.getComputedStyle(getByTestId('app-wrapper')).minHeight).toBe('auto') + }) + + it('keeps the full-page minimum height outside widget mode', () => { + const { getByTestId } = render( + + + , + ) + + expect(window.getComputedStyle(getByTestId('app-wrapper')).minHeight).toBe('100vh') + }) +}) + +describe('BodyWrapper', () => { + it('does not flex-grow in widget mode', () => { + const { getByTestId } = render( + + + , + ) + + expect(window.getComputedStyle(getByTestId('body-wrapper')).flex).toBe('0 0 auto') + }) + + it('uses the widget shell padding by default', () => { + const { getByTestId } = render( + + + , + ) + + const computedStyle = window.getComputedStyle(getByTestId('body-wrapper')) + + expect(computedStyle.paddingTop).toBe('16px') + expect(computedStyle.paddingRight).toBe('16px') + expect(computedStyle.paddingBottom).toBe('24px') + expect(computedStyle.paddingLeft).toBe('16px') + }) + + it('allows overriding widget shell padding from the theme palette', () => { + const { getByTestId } = render( + + + , + ) + + const computedStyle = window.getComputedStyle(getByTestId('body-wrapper')) + + expect(computedStyle.paddingTop).toBe('8px') + expect(computedStyle.paddingRight).toBe('12px') + expect(computedStyle.paddingBottom).toBe('20px') + expect(computedStyle.paddingLeft).toBe('12px') + }) + + it('keeps the legacy flex behavior outside widget mode', () => { + const { getByTestId } = render( + + + , + ) + + expect(window.getComputedStyle(getByTestId('body-wrapper')).flex).toBe('1 1 auto') + }) +}) + +describe('Marginer', () => { + it('does not add vertical spacing in widget mode', () => { + const { getByTestId } = render( + + + , + ) + + expect(window.getComputedStyle(getByTestId('marginer')).marginTop).toBe('0px') + }) + + it('keeps the legacy spacing outside widget mode', () => { + const { getByTestId } = render( + + + , + ) + + expect(window.getComputedStyle(getByTestId('marginer')).marginTop).toBe('5rem') + }) +}) diff --git a/apps/cowswap-frontend/src/modules/application/containers/App/styled.ts b/apps/cowswap-frontend/src/modules/application/containers/App/styled.ts index 7276a6c89d6..11d1056fe8d 100644 --- a/apps/cowswap-frontend/src/modules/application/containers/App/styled.ts +++ b/apps/cowswap-frontend/src/modules/application/containers/App/styled.ts @@ -13,7 +13,7 @@ import IMAGE_BACKGROUND_LIGHT from '@cowprotocol/assets/images/background-cowswa import { CowSwapTheme, Media, UI } from '@cowprotocol/ui' import * as CSS from 'csstype' -import styled from 'styled-components/macro' +import styled, { type DefaultTheme } from 'styled-components/macro' import type { PageBackgroundVariant } from '../../contexts/PageBackgroundContext' @@ -25,17 +25,23 @@ export function isChristmasTheme(theme?: CowSwapTheme): boolean { return ['darkChristmas', 'lightChristmas'].includes(theme) } +const DEFAULT_WIDGET_BODY_PADDING = '16px 16px 24px' + +function getWidgetBodyPadding(theme: DefaultTheme): string { + return theme.widgetPadding || DEFAULT_WIDGET_BODY_PADDING +} + export const AppWrapper = styled.div>` display: flex; flex-flow: column; align-items: flex-start; - min-height: ${({ theme }) => (theme.isWidget ? '400px' : '100vh')}; + min-height: ${({ theme }) => (theme.isWidget ? 'auto' : '100vh')}; height: ${({ theme }) => (theme.isWidget ? 'initial' : '100%')}; position: relative; ` export const Marginer = styled.div` - margin-top: 5rem; + margin-top: ${({ theme }) => (theme.isWidget ? '0' : '5rem')}; ` export const SceneContainer = styled.div` @@ -72,11 +78,15 @@ export const BodyWrapper = styled.div<{ width: 100%; align-items: flex-start; justify-content: center; - flex: 1 1 auto; + flex: ${({ theme }) => (theme.isWidget ? '0 0 auto' : '1 1 auto')}; z-index: 2; color: inherit; padding: ${({ theme, $hasActiveSpeechBubbleNotification }) => - theme.isWidget ? '16px 16px 0' : $hasActiveSpeechBubbleNotification ? '150px 16px 320px' : '150px 16px 176px'}; + theme.isWidget + ? getWidgetBodyPadding(theme) + : $hasActiveSpeechBubbleNotification + ? '150px 16px 320px' + : '150px 16px 176px'}; margin: ${({ theme }) => (theme.isWidget ? '0' : '-76px auto calc(var(--marginBottomOffset) * -1)')}; border-bottom-left-radius: ${({ theme }) => (theme.isWidget ? '0' : 'var(--marginBottomOffset)')}; border-bottom-right-radius: ${({ theme }) => (theme.isWidget ? '0' : 'var(--marginBottomOffset)')}; @@ -108,7 +118,11 @@ export const BodyWrapper = styled.div<{ ${Media.upToMedium()} { padding: ${({ theme, $hasActiveSpeechBubbleNotification }) => - theme.isWidget ? '0 0 16px' : $hasActiveSpeechBubbleNotification ? '150px 16px 330px' : '150px 16px 150px'}; + theme.isWidget + ? getWidgetBodyPadding(theme) + : $hasActiveSpeechBubbleNotification + ? '150px 16px 330px' + : '150px 16px 150px'}; flex: none; min-height: ${({ theme }) => (theme.isWidget ? 'initial' : 'calc(100vh - 200px)')}; background-size: ${({ customTheme }) => @@ -133,7 +147,11 @@ export const BodyWrapper = styled.div<{ ${Media.upToSmall()} { padding: ${({ theme, $hasActiveSpeechBubbleNotification }) => - theme.isWidget ? '0 0 16px' : $hasActiveSpeechBubbleNotification ? '90px 16px 400px' : '90px 16px 200px'}; + theme.isWidget + ? getWidgetBodyPadding(theme) + : $hasActiveSpeechBubbleNotification + ? '90px 16px 400px' + : '90px 16px 200px'}; min-height: ${({ theme }) => (theme.isWidget ? 'initial' : 'calc(100vh - 100px)')}; background-size: ${({ customTheme }) => customTheme === 'darkHalloween' || isChristmasTheme(customTheme) ? 'contain' : 'auto'}; diff --git a/apps/cowswap-frontend/src/modules/balancesAndAllowances/updaters/BalancesDevtools.test.tsx b/apps/cowswap-frontend/src/modules/balancesAndAllowances/updaters/BalancesDevtools.test.tsx new file mode 100644 index 00000000000..78501b560ff --- /dev/null +++ b/apps/cowswap-frontend/src/modules/balancesAndAllowances/updaters/BalancesDevtools.test.tsx @@ -0,0 +1,44 @@ +import { isInjectedWidget } from '@cowprotocol/common-utils' + +import { render, screen } from '@testing-library/react' + +import { BalancesDevtools } from './BalancesDevtools' + +jest.mock('@cowprotocol/common-utils', () => ({ + isInjectedWidget: jest.fn(), +})) + +jest.mock('jotai-devtools', () => ({ + DevTools: () =>
, +})) + +const isInjectedWidgetMock = isInjectedWidget as jest.MockedFunction + +describe('BalancesDevtools', () => { + const originalNodeEnv = process.env.NODE_ENV + + beforeEach(() => { + isInjectedWidgetMock.mockReturnValue(false) + }) + + afterEach(() => { + process.env.NODE_ENV = originalNodeEnv + }) + + it('renders in development outside widget mode', () => { + process.env.NODE_ENV = 'development' + + render() + + expect(screen.getByTestId('balances-devtools')).toBeTruthy() + }) + + it('does not render inside widget mode', () => { + process.env.NODE_ENV = 'development' + isInjectedWidgetMock.mockReturnValue(true) + + render() + + expect(screen.queryByTestId('balances-devtools')).toBeNull() + }) +}) diff --git a/apps/cowswap-frontend/src/modules/balancesAndAllowances/updaters/BalancesDevtools.tsx b/apps/cowswap-frontend/src/modules/balancesAndAllowances/updaters/BalancesDevtools.tsx index b5afc21965b..d75e12b8ae5 100644 --- a/apps/cowswap-frontend/src/modules/balancesAndAllowances/updaters/BalancesDevtools.tsx +++ b/apps/cowswap-frontend/src/modules/balancesAndAllowances/updaters/BalancesDevtools.tsx @@ -1,9 +1,11 @@ import { JSX } from 'react' +import { isInjectedWidget } from '@cowprotocol/common-utils' + import { DevTools } from 'jotai-devtools' export function BalancesDevtools(): JSX.Element | null { - if (process.env.NODE_ENV !== 'development') return null + if (process.env.NODE_ENV !== 'development' || isInjectedWidget()) return null return } diff --git a/apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.test.tsx b/apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.test.tsx new file mode 100644 index 00000000000..6e5f15395d4 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.test.tsx @@ -0,0 +1,273 @@ +import { useAtomValue } from 'jotai' + +import { isIframe, isInjectedWidget } from '@cowprotocol/common-utils' +import { MEDIA_WIDTHS } from '@cowprotocol/ui' +import { WidgetMethodsEmit, widgetIframeTransport } from '@cowprotocol/widget-lib' + +import { act, render } from '@testing-library/react' + +import { IframeResizer } from './IframeResizer' + +jest.mock('jotai', () => ({ + ...jest.requireActual('jotai'), + useAtomValue: jest.fn(), +})) + +jest.mock('@cowprotocol/common-utils', () => ({ + isIframe: jest.fn(), + isInjectedWidget: jest.fn(), +})) + +jest.mock('@cowprotocol/ui', () => ({ + MEDIA_WIDTHS: { + upToSmall: 767, + }, +})) + +const useAtomValueMock = useAtomValue as jest.MockedFunction +const isIframeMock = isIframe as jest.MockedFunction +const isInjectedWidgetMock = isInjectedWidget as jest.MockedFunction + +const postMessageToWindowSpy = jest.spyOn(widgetIframeTransport, 'postMessageToWindow') + +const resizeObserverObserveMock = jest.fn() +const resizeObserverDisconnectMock = jest.fn() +const resizeObserverUnobserveMock = jest.fn() +const mutationObserverObserveMock = jest.fn() +const mutationObserverDisconnectMock = jest.fn() + +const originalResizeObserver = global.ResizeObserver +const originalMutationObserver = global.MutationObserver + +let triggerResizeObserver: (() => void) | null = null +let triggerMutationObserver: (() => void) | null = null +let rootElement: HTMLDivElement | null = null + +class MockResizeObserver { + constructor(callback: ResizeObserverCallback) { + triggerResizeObserver = () => callback([], this as unknown as ResizeObserver) + } + + observe = resizeObserverObserveMock + disconnect = resizeObserverDisconnectMock + unobserve = resizeObserverUnobserveMock +} + +class MockMutationObserver { + constructor(callback: MutationCallback) { + triggerMutationObserver = () => callback([], this as unknown as MutationObserver) + } + + observe = mutationObserverObserveMock + disconnect = mutationObserverDisconnectMock + takeRecords(): MutationRecord[] { + return [] + } +} + +describe('IframeResizer', () => { + beforeEach(() => { + jest.clearAllMocks() + + triggerResizeObserver = null + triggerMutationObserver = null + rootElement = document.createElement('div') + rootElement.id = 'root' + document.body.appendChild(rootElement) + + useAtomValueMock.mockReturnValue(false as never) + isIframeMock.mockReturnValue(true) + isInjectedWidgetMock.mockReturnValue(true) + + setContentSize({ bodyOffsetWidth: 400, rootScrollHeight: 520, rootOffsetHeight: 500 }) + setResizeObserver(MockResizeObserver) + setMutationObserver(MockMutationObserver) + }) + + afterEach(() => { + rootElement?.remove() + rootElement = null + }) + + afterAll(() => { + setResizeObserver(originalResizeObserver) + setMutationObserver(originalMutationObserver) + }) + + it('uses ResizeObserver and window resize events to emit updated heights', () => { + const { unmount } = render() + + expect(postMessageToWindowSpy).toHaveBeenCalledWith(window.parent, WidgetMethodsEmit.UPDATE_HEIGHT, { + height: 500, + }) + expect(resizeObserverObserveMock).toHaveBeenCalledWith(rootElement) + expect(resizeObserverObserveMock).toHaveBeenCalledWith(document.body) + expect(mutationObserverObserveMock).not.toHaveBeenCalled() + + setContentSize({ bodyOffsetWidth: 400, rootScrollHeight: 640, rootOffsetHeight: 610 }) + + act(() => { + triggerResizeObserver?.() + }) + + expect(postMessageToWindowSpy).toHaveBeenLastCalledWith(window.parent, WidgetMethodsEmit.UPDATE_HEIGHT, { + height: 610, + }) + + setContentSize({ bodyOffsetWidth: 400, rootScrollHeight: 680, rootOffsetHeight: 700 }) + + act(() => { + window.dispatchEvent(new Event('resize')) + }) + + expect(postMessageToWindowSpy).toHaveBeenLastCalledWith(window.parent, WidgetMethodsEmit.UPDATE_HEIGHT, { + height: 700, + }) + + unmount() + + expect(resizeObserverDisconnectMock).toHaveBeenCalled() + }) + + it('ignores viewport-only height changes that would otherwise cause a resize loop', () => { + render() + + expect(postMessageToWindowSpy).toHaveBeenCalledTimes(1) + + act(() => { + window.dispatchEvent(new Event('resize')) + }) + + expect(postMessageToWindowSpy).toHaveBeenCalledTimes(1) + }) + + it('uses the rendered root height when scrollHeight over-reports content size', () => { + setContentSize({ bodyOffsetWidth: 400, rootScrollHeight: 700, rootOffsetHeight: 640 }) + + render() + + expect(postMessageToWindowSpy).toHaveBeenCalledWith(window.parent, WidgetMethodsEmit.UPDATE_HEIGHT, { + height: 640, + }) + }) + + it('falls back to MutationObserver when ResizeObserver is unavailable', () => { + setResizeObserver(undefined) + + render() + + expect(mutationObserverObserveMock).toHaveBeenCalledWith(document.body, { + attributes: true, + characterData: true, + childList: true, + subtree: true, + }) + + setContentSize({ bodyOffsetWidth: 400, rootScrollHeight: 580, rootOffsetHeight: 560 }) + + act(() => { + triggerMutationObserver?.() + }) + + expect(postMessageToWindowSpy).toHaveBeenLastCalledWith(window.parent, WidgetMethodsEmit.UPDATE_HEIGHT, { + height: 560, + }) + }) + + it('emits full-height updates while a modal is open', () => { + useAtomValueMock.mockReturnValue(true as never) + setContentSize({ + bodyOffsetWidth: MEDIA_WIDTHS.upToSmall - 1, + rootScrollHeight: 520, + rootOffsetHeight: 500, + }) + + render() + + expect(postMessageToWindowSpy).toHaveBeenCalledWith(window.parent, WidgetMethodsEmit.SET_FULL_HEIGHT, { + isUpToSmall: true, + }) + + setContentSize({ + bodyOffsetWidth: MEDIA_WIDTHS.upToSmall + 1, + rootScrollHeight: 520, + rootOffsetHeight: 500, + }) + + act(() => { + window.dispatchEvent(new Event('resize')) + }) + + expect(postMessageToWindowSpy).toHaveBeenLastCalledWith(window.parent, WidgetMethodsEmit.SET_FULL_HEIGHT, { + isUpToSmall: false, + }) + }) +}) + +function setContentSize({ + bodyOffsetWidth, + rootScrollHeight, + rootOffsetHeight, +}: { + bodyOffsetWidth: number + rootScrollHeight: number + rootOffsetHeight: number +}): void { + Object.defineProperty(document.body, 'offsetWidth', { + configurable: true, + value: bodyOffsetWidth, + }) + + const root = getRootElement() + + Object.defineProperty(root, 'scrollHeight', { + configurable: true, + value: rootScrollHeight, + }) + + Object.defineProperty(root, 'offsetHeight', { + configurable: true, + value: rootOffsetHeight, + }) + + Object.defineProperty(root, 'clientHeight', { + configurable: true, + value: rootOffsetHeight, + }) + + root.getBoundingClientRect = jest.fn(() => ({ + bottom: rootOffsetHeight, + height: rootOffsetHeight, + left: 0, + right: 0, + toJSON: () => undefined, + top: 0, + width: 0, + x: 0, + y: 0, + })) +} + +function getRootElement(): HTMLDivElement { + if (!rootElement) { + throw new Error('Root element is not initialized') + } + + return rootElement +} + +function setResizeObserver(value: typeof ResizeObserver | undefined): void { + Object.defineProperty(global, 'ResizeObserver', { + configurable: true, + value, + writable: true, + }) +} + +function setMutationObserver(value: typeof MutationObserver | undefined): void { + Object.defineProperty(global, 'MutationObserver', { + configurable: true, + value, + writable: true, + }) +} diff --git a/apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.ts b/apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.ts index 66357e44a57..7802fd2aa3a 100644 --- a/apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.ts +++ b/apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.ts @@ -7,20 +7,17 @@ import { WidgetMethodsEmit, widgetIframeTransport } from '@cowprotocol/widget-li import { openModalState } from 'common/state/openModalState' -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function IframeResizer() { +export function IframeResizer(): null { const isModalOpen = useAtomValue(openModalState) const previousHeightRef = useRef(0) useLayoutEffect(() => { if (!isIframe() || !isInjectedWidget()) return - // Initial height calculation and message - // TODO: Add proper return type annotation - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - const sendHeightUpdate = () => { - const contentHeight = document.body.scrollHeight + const contentElement = getContentElement(document) + + const sendHeightUpdate = (): void => { + const contentHeight = getContentHeight(contentElement) if (isModalOpen) { const isUpToSmall = document.body.offsetWidth <= MEDIA_WIDTHS.upToSmall @@ -40,19 +37,53 @@ export function IframeResizer() { } sendHeightUpdate() - // Set up a MutationObserver to watch for changes in the DOM - const observer = new MutationObserver(() => { - sendHeightUpdate() - }) + window.addEventListener('resize', sendHeightUpdate) + + const resizeObserver = + typeof ResizeObserver !== 'undefined' + ? new ResizeObserver(() => { + sendHeightUpdate() + }) + : null + + resizeObserver?.observe(contentElement) + + if (contentElement !== document.body) { + resizeObserver?.observe(document.body) + } - // Start observing the entire body for changes that might affect its height - observer.observe(document.body, { childList: true, subtree: true }) + const mutationObserver = + !resizeObserver && typeof MutationObserver !== 'undefined' + ? new MutationObserver(() => { + sendHeightUpdate() + }) + : null + + mutationObserver?.observe(document.body, { + attributes: true, + characterData: true, + childList: true, + subtree: true, + }) - // Cleanup: Disconnect the observer when the component is unmounted return () => { - observer.disconnect() + window.removeEventListener('resize', sendHeightUpdate) + resizeObserver?.disconnect() + mutationObserver?.disconnect() } }, [isModalOpen]) return null } + +function getContentElement(doc: Document): HTMLElement { + return doc.getElementById('root') ?? doc.body +} + +function getContentHeight(contentElement: HTMLElement): number { + return Math.max( + contentElement.offsetHeight, + contentElement.clientHeight, + Math.ceil(contentElement.getBoundingClientRect().height), + ) +} diff --git a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/styled.tsx b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/styled.tsx index a58cfce14d0..49aaa569ae8 100644 --- a/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/styled.tsx +++ b/apps/cowswap-frontend/src/modules/trade/containers/TradeWidget/styled.tsx @@ -28,7 +28,7 @@ export const ContainerBox = styled.div` background: var(${UI.COLOR_PAPER}); color: var(${UI.COLOR_TEXT_PAPER}); border: none; - border-radius: var(${UI.BORDER_RADIUS_NORMAL}); + border-radius: ${({ theme }) => theme.widgetBorderRadius || `var(${UI.BORDER_RADIUS_NORMAL})`}; box-shadow: ${({ theme }) => (theme.isWidget ? theme.boxShadow1 : 'none')}; padding: 10px; position: relative; diff --git a/apps/cowswap-frontend/src/theme/mapWidgetTheme.test.ts b/apps/cowswap-frontend/src/theme/mapWidgetTheme.test.ts index 7db07420444..661af28a268 100644 --- a/apps/cowswap-frontend/src/theme/mapWidgetTheme.test.ts +++ b/apps/cowswap-frontend/src/theme/mapWidgetTheme.test.ts @@ -14,6 +14,8 @@ describe('mapWidgetTheme', () => { const widgetTheme: Partial = { paper: '#101010', boxShadow: 'none', + widgetPadding: '16px 16px 24px', + widgetBorderRadius: '32px', } const result = mapWidgetTheme(widgetTheme, defaultTheme) @@ -21,5 +23,7 @@ describe('mapWidgetTheme', () => { 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') }) }) diff --git a/apps/cowswap-frontend/src/theme/mapWidgetTheme.ts b/apps/cowswap-frontend/src/theme/mapWidgetTheme.ts index 85904e63ace..01d1a964d5a 100644 --- a/apps/cowswap-frontend/src/theme/mapWidgetTheme.ts +++ b/apps/cowswap-frontend/src/theme/mapWidgetTheme.ts @@ -9,12 +9,14 @@ export function mapWidgetTheme( ): DefaultTheme { if (!widgetTheme) return defaultTheme - const { boxShadow, ...widgetPalette } = widgetTheme + 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), } } diff --git a/apps/cowswap-frontend/src/theme/types.ts b/apps/cowswap-frontend/src/theme/types.ts index 096de05df0d..0f667d3c52a 100644 --- a/apps/cowswap-frontend/src/theme/types.ts +++ b/apps/cowswap-frontend/src/theme/types.ts @@ -9,6 +9,8 @@ declare module 'styled-components' { /** Properties specific to CoWSwap widget functionality */ isWidget: boolean isIframe: boolean + widgetPadding?: string + widgetBorderRadius?: string } // Use CoWSwapTheme as the default theme for styled-components in this app diff --git a/apps/widget-configurator/package.json b/apps/widget-configurator/package.json index ae7e9bb36af..44c2cb06135 100644 --- a/apps/widget-configurator/package.json +++ b/apps/widget-configurator/package.json @@ -30,6 +30,7 @@ "@cowprotocol/common-utils": "workspace:*", "@cowprotocol/events": "workspace:*", "@cowprotocol/types": "workspace:*", + "@cowprotocol/ui": "workspace:*", "@cowprotocol/widget-lib": "workspace:*", "@cowprotocol/widget-react": "workspace:*", "@mui/icons-material": "^5.17.1", @@ -45,6 +46,7 @@ "react-syntax-highlighter": "^15.5.0" }, "devDependencies": { + "@testing-library/react": "16.3.0", "@types/react": "19.1.3", "@types/react-dom": "19.1.3", "@types/react-syntax-highlighter": "^15.5.9" diff --git a/apps/widget-configurator/src/app/configurator/consts.ts b/apps/widget-configurator/src/app/configurator/consts.ts index 7b11b4136d6..0f8c0231461 100644 --- a/apps/widget-configurator/src/app/configurator/consts.ts +++ b/apps/widget-configurator/src/app/configurator/consts.ts @@ -57,6 +57,18 @@ export const DEFAULT_DARK_PALETTE: CowSwapWidgetPaletteParams = { success: '#00D897', } +// 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/AccordionSection.test.tsx b/apps/widget-configurator/src/app/configurator/controls/AccordionSection.test.tsx new file mode 100644 index 00000000000..d0be9407ae7 --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/AccordionSection.test.tsx @@ -0,0 +1,39 @@ +import { fireEvent, render, screen } from '@testing-library/react' + +import { AccordionSection } from './AccordionSection' + +describe('AccordionSection', () => { + it('is collapsed by default', () => { + render( + +
Inner content
+
, + ) + + expect(screen.getByRole('button', { name: 'Behavior' }).getAttribute('aria-expanded')).toBe('false') + }) + + it('supports expanded-by-default sections', () => { + render( + +
Inner content
+
, + ) + + expect(screen.getByRole('button', { name: 'Basics' }).getAttribute('aria-expanded')).toBe('true') + }) + + it('toggles expansion when clicked', () => { + render( + +
Inner content
+
, + ) + + const button = screen.getByRole('button', { name: 'Advanced' }) + + fireEvent.click(button) + + expect(button.getAttribute('aria-expanded')).toBe('true') + }) +}) diff --git a/apps/widget-configurator/src/app/configurator/controls/AccordionSection.tsx b/apps/widget-configurator/src/app/configurator/controls/AccordionSection.tsx new file mode 100644 index 00000000000..8372b784c1e --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/AccordionSection.tsx @@ -0,0 +1,47 @@ +import { ReactNode } from 'react' + +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import Accordion from '@mui/material/Accordion' +import AccordionDetails from '@mui/material/AccordionDetails' +import AccordionSummary from '@mui/material/AccordionSummary' +import Stack from '@mui/material/Stack' +import Typography from '@mui/material/Typography' + +interface AccordionSectionProps { + title: string + defaultExpanded?: boolean + children: ReactNode +} + +export function AccordionSection({ title, defaultExpanded = false, children }: AccordionSectionProps): ReactNode { + return ( + `1px solid ${theme.palette.divider}`, + borderRadius: '1.2rem', + overflow: 'hidden', + '&:before': { display: 'none' }, + }} + > + } + sx={{ + minHeight: '4.8rem', + '& .MuiAccordionSummary-content': { + margin: '1rem 0', + }, + }} + > + + {title} + + + + {children} + + + ) +} diff --git a/apps/widget-configurator/src/app/configurator/controls/BooleanSwitchControl.test.tsx b/apps/widget-configurator/src/app/configurator/controls/BooleanSwitchControl.test.tsx new file mode 100644 index 00000000000..4b8a68b7fd1 --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/BooleanSwitchControl.test.tsx @@ -0,0 +1,29 @@ +import { fireEvent, render, screen } from '@testing-library/react' + +import { BooleanSwitchControl } from './BooleanSwitchControl' + +describe('BooleanSwitchControl', () => { + it('renders label and helper text', () => { + render( + , + ) + + expect(screen.getByText('Show orders table')).not.toBeNull() + expect(screen.getByText('Shows the orders tab.')).not.toBeNull() + }) + + it('passes the next checked state to the handler', () => { + const onChange = jest.fn() + + render() + + fireEvent.click(screen.getByRole('checkbox', { name: 'Show orders table' })) + + expect(onChange).toHaveBeenCalledWith(true) + }) +}) diff --git a/apps/widget-configurator/src/app/configurator/controls/BooleanSwitchControl.tsx b/apps/widget-configurator/src/app/configurator/controls/BooleanSwitchControl.tsx new file mode 100644 index 00000000000..bc9b4ecc3ca --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/BooleanSwitchControl.tsx @@ -0,0 +1,37 @@ +import { ReactNode } from 'react' + +import Box from '@mui/material/Box' +import FormControlLabel from '@mui/material/FormControlLabel' +import Switch from '@mui/material/Switch' +import Typography from '@mui/material/Typography' + +interface BooleanSwitchControlProps { + checked: boolean + label: string + onChange: (checked: boolean) => void + helperText?: ReactNode +} + +export function BooleanSwitchControl({ checked, label, onChange, helperText }: BooleanSwitchControlProps): ReactNode { + return ( + + onChange(nextChecked)} />} + /> + {helperText ? ( + + {helperText} + + ) : null} + + ) +} diff --git a/apps/widget-configurator/src/app/configurator/controls/ConfiguratorBrandHeader.test.tsx b/apps/widget-configurator/src/app/configurator/controls/ConfiguratorBrandHeader.test.tsx new file mode 100644 index 00000000000..38429e66eac --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/ConfiguratorBrandHeader.test.tsx @@ -0,0 +1,25 @@ +import { render, screen } from '@testing-library/react' + +jest.mock('@cowprotocol/ui', () => ({ + 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/controls/ConfiguratorBrandHeader.tsx b/apps/widget-configurator/src/app/configurator/controls/ConfiguratorBrandHeader.tsx new file mode 100644 index 00000000000..94563b7a7ae --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/ConfiguratorBrandHeader.tsx @@ -0,0 +1,56 @@ +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/controls/HelpTooltipButton.tsx b/apps/widget-configurator/src/app/configurator/controls/HelpTooltipButton.tsx new file mode 100644 index 00000000000..de908c8fe13 --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/HelpTooltipButton.tsx @@ -0,0 +1,150 @@ +import { ReactNode, RefObject, useEffect, useLayoutEffect, useRef, useState } from 'react' + +import HelpOutlineIcon from '@mui/icons-material/HelpOutline' +import Box from '@mui/material/Box' +import IconButton from '@mui/material/IconButton' +import Typography from '@mui/material/Typography' +import { createPortal } from 'react-dom' + +const VIEWPORT_PADDING = 16 +const TOOLTIP_SPACING = 8 + +interface HelpTooltipButtonProps { + ariaLabel: string + tooltip: ReactNode +} + +interface TooltipPosition { + top: number + left: number +} + +interface TooltipPortalProps { + tooltip: ReactNode + tooltipPosition: TooltipPosition + tooltipRef: RefObject +} + +function getTooltipPosition(button: HTMLButtonElement, tooltip: HTMLDivElement | null): TooltipPosition { + const anchorRect = button.getBoundingClientRect() + const tooltipWidth = tooltip?.offsetWidth || 320 + const tooltipHeight = tooltip?.offsetHeight || 0 + const maxLeft = window.innerWidth - VIEWPORT_PADDING - tooltipWidth + const left = Math.max(VIEWPORT_PADDING, Math.min(anchorRect.left, maxLeft)) + const preferredTop = anchorRect.bottom + TOOLTIP_SPACING + const shouldRenderAbove = tooltipHeight > 0 && preferredTop + tooltipHeight > window.innerHeight - VIEWPORT_PADDING + const top = shouldRenderAbove + ? Math.max(VIEWPORT_PADDING, anchorRect.top - tooltipHeight - TOOLTIP_SPACING) + : preferredTop + + return { top, left } +} + +function TooltipPortal({ tooltip, tooltipPosition, tooltipRef }: TooltipPortalProps): ReactNode { + return createPortal( + ({ + position: 'fixed', + top: `${tooltipPosition.top}px`, + left: `${tooltipPosition.left}px`, + zIndex: 1500, + width: 'min(24rem, calc(100vw - 3.2rem))', + border: `1px solid ${theme.palette.divider}`, + borderRadius: '1rem', + backgroundColor: theme.palette.background.paper, + boxShadow: 'rgba(5, 43, 101, 0.12) 0 0.8rem 1.6rem', + p: '0.8rem 1rem', + })} + > + {typeof tooltip === 'string' ? ( + {tooltip} + ) : ( + tooltip + )} + , + document.body, + ) +} + +export function HelpTooltipButton({ ariaLabel, tooltip }: HelpTooltipButtonProps): ReactNode { + const containerRef = useRef(null) + const buttonRef = useRef(null) + const tooltipRef = useRef(null) + const [isHovered, setIsHovered] = useState(false) + const [isPinned, setIsPinned] = useState(false) + const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 }) + const isOpen = isHovered || isPinned + + useEffect(() => { + if (!isPinned) { + return + } + + function handlePointerDown(event: MouseEvent): void { + const target = event.target as Node + + if (!containerRef.current?.contains(target) && !tooltipRef.current?.contains(target)) { + setIsPinned(false) + } + } + + document.addEventListener('mousedown', handlePointerDown) + + return () => document.removeEventListener('mousedown', handlePointerDown) + }, [isPinned]) + + useLayoutEffect(() => { + if (!isOpen) { + return + } + + function updateTooltipPosition(): void { + const button = buttonRef.current + + if (!button) { + return + } + + setTooltipPosition(getTooltipPosition(button, tooltipRef.current)) + } + + updateTooltipPosition() + window.addEventListener('resize', updateTooltipPosition) + window.addEventListener('scroll', updateTooltipPosition, true) + + return () => { + window.removeEventListener('resize', updateTooltipPosition) + window.removeEventListener('scroll', updateTooltipPosition, true) + } + }, [isOpen]) + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + { + event.preventDefault() + event.stopPropagation() + setIsPinned((current) => !current) + }} + > + + + {isOpen && typeof document !== 'undefined' ? ( + + ) : null} + + ) +} diff --git a/apps/widget-configurator/src/app/configurator/controls/IframeBackgroundColorControl.test.tsx b/apps/widget-configurator/src/app/configurator/controls/IframeBackgroundColorControl.test.tsx new file mode 100644 index 00000000000..d73a3b975eb --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/IframeBackgroundColorControl.test.tsx @@ -0,0 +1,30 @@ +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 new file mode 100644 index 00000000000..391dbdb7cac --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/IframeBackgroundColorControl.tsx @@ -0,0 +1,85 @@ +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 new file mode 100644 index 00000000000..ab90ae2da15 --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/IframeBorderRadiusControl.test.tsx @@ -0,0 +1,24 @@ +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 new file mode 100644 index 00000000000..48a70d9882d --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/IframeBorderRadiusControl.tsx @@ -0,0 +1,91 @@ +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 new file mode 100644 index 00000000000..fd54918ff4a --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/IframeWidthControl.test.tsx @@ -0,0 +1,27 @@ +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 new file mode 100644 index 00000000000..ffc1c994d4e --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/IframeWidthControl.tsx @@ -0,0 +1,125 @@ +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/LocaleControl.test.tsx b/apps/widget-configurator/src/app/configurator/controls/LocaleControl.test.tsx new file mode 100644 index 00000000000..bb33314fdc8 --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/LocaleControl.test.tsx @@ -0,0 +1,11 @@ +import { render, screen } from '@testing-library/react' + +import { LocaleControl } from './LocaleControl' + +describe('LocaleControl', () => { + it('shows browser default when no locale is forced', () => { + render() + + expect(screen.getByRole('combobox').textContent).toContain('Browser default') + }) +}) diff --git a/apps/widget-configurator/src/app/configurator/controls/LocaleControl.tsx b/apps/widget-configurator/src/app/configurator/controls/LocaleControl.tsx index f442b99407f..75b00ae64ee 100644 --- a/apps/widget-configurator/src/app/configurator/controls/LocaleControl.tsx +++ b/apps/widget-configurator/src/app/configurator/controls/LocaleControl.tsx @@ -8,6 +8,7 @@ import MenuItem from '@mui/material/MenuItem' import Select from '@mui/material/Select' const LABEL = 'Forced locale' +const LABEL_ID = 'select-locale-label' type LocaleControlState = [SupportedLocale | '', Dispatch>] @@ -16,13 +17,24 @@ export function LocaleControl({ state }: { state: LocaleControlState }): ReactNo return ( - {LABEL} + + {LABEL} + { + const selectedOption = getThemeOption(value as ThemeOptionValue) + + return + }} > - {ThemeOptions.map((option) => ( + {THEME_OPTIONS.map((option) => ( - {option.label} + ))} diff --git a/apps/widget-configurator/src/app/configurator/controls/WidgetBorderRadiusControl.test.tsx b/apps/widget-configurator/src/app/configurator/controls/WidgetBorderRadiusControl.test.tsx new file mode 100644 index 00000000000..ed14b45c6ac --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/WidgetBorderRadiusControl.test.tsx @@ -0,0 +1,24 @@ +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 new file mode 100644 index 00000000000..4d7f2b1751a --- /dev/null +++ b/apps/widget-configurator/src/app/configurator/controls/WidgetBorderRadiusControl.tsx @@ -0,0 +1,80 @@ +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/WidgetHooksControl.tsx b/apps/widget-configurator/src/app/configurator/controls/WidgetHooksControl.tsx index 1f94baabe31..8a22a95288f 100644 --- a/apps/widget-configurator/src/app/configurator/controls/WidgetHooksControl.tsx +++ b/apps/widget-configurator/src/app/configurator/controls/WidgetHooksControl.tsx @@ -13,6 +13,8 @@ import Select, { SelectChangeEvent } from '@mui/material/Select' import { WIDGET_HOOKS } from '../consts' const LABEL = 'Widget hooks' +const EMPTY_VALUE_LABEL = 'No hooks selected' +const LABEL_ID = 'widget-hooks-select-label' export function WidgetHooksControl({ state, @@ -27,15 +29,27 @@ export function WidgetHooksControl({ return ( - {LABEL} + + {LABEL} + 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 = { + fields: {}, + jsonValue: {}, +} + +export interface InitialJsonState { + fields: T + jsonValue: T +} + +export interface JsonState { + fields: T + rawJsonValue: string + parsedJsonValue: T + mergedValue: T + error: boolean +} + +export type OnJsonStateChange = (name: keyof T | null, value: string) => void + +export function useJsonState( + initialState: InitialJsonState, +): [JsonState, OnJsonStateChange] { + const { fields: initialFields, jsonValue: initialJsonValue } = initialState + + const [jsonState, setJsonState] = useState>({ + fields: initialFields, + rawJsonValue: JSON.stringify(initialJsonValue), + parsedJsonValue: initialJsonValue, + mergedValue: mergeJsonValues(initialFields, initialJsonValue), + error: false, + }) + + const onChange = useCallback( + (name: keyof T | null, value: string) => { + if (name === null) { + let parsedValue: T = initialJsonValue + + try { + parsedValue = parseJsonValue(value.trim()) + } catch { + setJsonState((prevState) => ({ + ...prevState, + rawJsonValue: value, + error: true, + })) + + return + } + + setJsonState(({ fields }) => ({ + fields, + rawJsonValue: value, + parsedJsonValue: parsedValue, + mergedValue: mergeJsonValues(fields, parsedValue), + error: false, + })) + } else { + setJsonState(({ fields, rawJsonValue, parsedJsonValue }) => { + const nextFields = { ...fields, [name]: value } + + return { + fields: nextFields, + rawJsonValue, + parsedJsonValue, + mergedValue: mergeJsonValues(nextFields, parsedJsonValue), + error: false, + } + }) + } + }, + [initialJsonValue], + ) + + return [jsonState, onChange] +} + +function mergeJsonValues(fields: T, jsonValue: T): T { + return { + ...fields, + ...jsonValue, + } +} + +// TODO: Use hjson and/or js-yaml +function parseJsonValue(value: string): T { + try { + return JSON.parse(value.trim()) as T + } catch (err) { + throw err + } +} diff --git a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts index f8093c4b773..a9fcaa1bd13 100644 --- a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts @@ -3,7 +3,6 @@ import { useMemo } from 'react' import { CowSwapWidgetParams, TradeType, WidgetHookEvents } from '@cowprotocol/widget-lib' import { isDev, isLocalHost, isVercel } from '../../../env' -import { DEFAULT_IFRAME_BORDER_RADIUS, DEFAULT_IFRAME_WIDTH } from '../consts' import { ConfiguratorState } from '../types' const vercelSuffix = '-cowswap-dev.vercel.app' @@ -112,16 +111,8 @@ function getThemeParam( theme: ConfiguratorState['theme'], customColors: ConfiguratorState['customColors'], defaultColors: ConfiguratorState['defaultColors'], - boxShadow: ConfiguratorState['boxShadow'], - widgetPadding: ConfiguratorState['widgetPadding'], - widgetBorderRadius: ConfiguratorState['widgetBorderRadius'], ): CowSwapWidgetParams['theme'] { - if ( - JSON.stringify(customColors) === JSON.stringify(defaultColors) && - !boxShadow && - !widgetPadding && - !widgetBorderRadius - ) { + if (JSON.stringify(customColors) === JSON.stringify(defaultColors)) { return theme } @@ -141,9 +132,6 @@ function getThemeParam( alert: themeColors.alert, info: themeColors.info, success: themeColors.success, - ...(boxShadow ? { boxShadow } : null), - ...(widgetPadding ? { widgetPadding } : null), - ...(widgetBorderRadius ? { widgetBorderRadius } : null), } } @@ -166,12 +154,10 @@ function buildWidgetParams(configuratorState: ConfiguratorState): CowSwapWidgetP chainId, locale, theme, - iframeWidth, - iframeBackgroundColor, - iframeBorderRadius, - boxShadow, - widgetPadding, - widgetBorderRadius, + iframeStyle, + appWrapperStyle, + bodyWrapperStyle, + cardStyle, currentTradeType, enabledTradeTypes, sellToken, @@ -204,10 +190,6 @@ function buildWidgetParams(configuratorState: ConfiguratorState): CowSwapWidgetP return { appCode: 'CoW Widget: Configurator', - width: iframeWidth || DEFAULT_IFRAME_WIDTH, - height: '640px', - iframeBackgroundColor, - iframeBorderRadius: iframeBorderRadius || DEFAULT_IFRAME_BORDER_RADIUS, chainId, locale, tokenLists: getTokenListsParam(tokenListUrls, 'enabled'), @@ -219,7 +201,11 @@ function buildWidgetParams(configuratorState: ConfiguratorState): CowSwapWidgetP buy: { asset: buyToken, amount: buyTokenAmount?.toString() }, forcedOrderDeadline: getForcedOrderDeadline({ deadline, swapDeadline, limitDeadline, advancedDeadline }), enabledTradeTypes, - theme: getThemeParam(theme, customColors, defaultColors, boxShadow, widgetPadding, widgetBorderRadius), + theme: getThemeParam(theme, customColors, defaultColors), + iframeStyle, + appWrapperStyle, + bodyWrapperStyle, + cardStyle, standaloneMode, disableToastMessages, disableProgressBar, diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/index.tsx index c5ff71f7c37..ef5b506fb19 100644 --- a/apps/widget-configurator/src/app/configurator/index.tsx +++ b/apps/widget-configurator/src/app/configurator/index.tsx @@ -1,4 +1,4 @@ -import { type CSSProperties, ChangeEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' +import { ChangeEvent, CSSProperties, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import { useCowAnalytics } from '@cowprotocol/analytics' import { DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK, SupportedLocale } from '@cowprotocol/common-const' @@ -16,7 +16,6 @@ import LanguageIcon from '@mui/icons-material/Language' import { IconButton, Snackbar } from '@mui/material' import Box from '@mui/material/Box' import Button from '@mui/material/Button' -import Divider from '@mui/material/Divider' import Drawer from '@mui/material/Drawer' import Fab from '@mui/material/Fab' import List from '@mui/material/List' @@ -28,8 +27,9 @@ import TextField from '@mui/material/TextField' import Typography from '@mui/material/Typography' import { useWeb3ModalAccount, useWeb3ModalTheme } from '@web3modal/ethers5/react' -import { COW_LISTENERS, DEFAULT_IFRAME_BORDER_RADIUS, DEFAULT_TOKEN_LISTS, IS_IFRAME, TRADE_MODES } from './consts' +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' @@ -37,9 +37,6 @@ import { CurrentTradeTypeControl } from './controls/CurrentTradeTypeControl' import { CustomImagesControl } from './controls/CustomImagesControl' import { CustomSoundsControl } from './controls/CustomSoundsControl' import { DeadlineControl } from './controls/DeadlineControl' -import { IframeBackgroundColorControl } from './controls/IframeBackgroundColorControl' -import { IframeBorderRadiusControl } from './controls/IframeBorderRadiusControl' -import { IframeWidthControl } from './controls/IframeWidthControl' import { LocaleControl } from './controls/LocaleControl' import { ModeControl } from './controls/ModeControl' import { NetworkControl, NetworkOption, NetworkOptions } from './controls/NetworkControl' @@ -48,12 +45,10 @@ import { PartnerFeeControl } from './controls/PartnerFeeControl' import { ThemeControl } from './controls/ThemeControl' import { TokenListControl } from './controls/TokenListControl' import { TradeModesControl } from './controls/TradeModesControl' -import { WidgetBorderRadiusControl } from './controls/WidgetBorderRadiusControl' import { WidgetHooksControl } from './controls/WidgetHooksControl' -import { WidgetPaddingControl } from './controls/WidgetPaddingControl' -import { WidgetShadowControl } from './controls/WidgetShadowControl' import { useColorPaletteManager } from './hooks/useColorPaletteManager' import { useEmbedDialogState } from './hooks/useEmbedDialogState' +import { EMPTY_JSON_STATE, useJsonState } from './hooks/useJsonState' import { useProvider } from './hooks/useProvider' import { useResizableDrawerWidth } from './hooks/useResizableDrawerWidth' import { useSyncWidgetNetwork } from './hooks/useSyncWidgetNetwork' @@ -74,6 +69,8 @@ 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 @@ -91,14 +88,6 @@ const UTM_PARAMS = 'utm_content=cow-widget-configurator&utm_medium=web&utm_sourc export type WidgetMode = 'dapp' | 'standalone' -function getOptionalTextValue(value: string): string | undefined { - return value || undefined -} - -function getIframeDefaultBackgroundColor(paperColor: string, defaultPaperColor: string): string { - return paperColor || defaultPaperColor -} - // TODO: Break down this large function into smaller functions // TODO: Add proper return type annotation // TODO: Reduce function complexity by extracting logic @@ -176,13 +165,11 @@ export function Configurator({ title }: { title: string }) { const paletteManager = useColorPaletteManager(mode) const { colorPalette, defaultPalette } = paletteManager - const iframeDefaultBackgroundColor = getIframeDefaultBackgroundColor(colorPalette.paper, defaultPalette.paper) - const [iframeWidth, setIframeWidth] = useState('') - const [iframeBackgroundColor, setIframeBackgroundColor] = useState('') - const [iframeBorderRadius, setIframeBorderRadius] = useState(DEFAULT_IFRAME_BORDER_RADIUS) - const [boxShadow, setBoxShadow] = useState('') - const [widgetPadding, setWidgetPadding] = useState('') - const [widgetBorderRadius, setWidgetBorderRadius] = useState('') + + 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() @@ -254,12 +241,10 @@ export function Configurator({ title }: { title: string }) { chainId: IS_IFRAME ? undefined : !isConnected || !walletChainId ? chainId : walletChainId, locale: locale || undefined, theme: mode, - iframeWidth: getOptionalTextValue(iframeWidth), - iframeBackgroundColor: getOptionalTextValue(iframeBackgroundColor), - iframeBorderRadius: getOptionalTextValue(iframeBorderRadius), - boxShadow: getOptionalTextValue(boxShadow), - widgetPadding: getOptionalTextValue(widgetPadding), - widgetBorderRadius: getOptionalTextValue(widgetBorderRadius), + iframeStyle: iframeStyleJson.mergedValue, + appWrapperStyle: appWrapperStyleJson.mergedValue, + bodyWrapperStyle: bodyWrapperStyleJson.mergedValue, + cardStyle: cardStyleJson.mergedValue, currentTradeType, enabledTradeTypes, enabledWidgetHooks, @@ -412,25 +397,22 @@ export function Configurator({ title }: { title: string }) { - + - - - - - - - - - - - - + + + + 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 }) => ( - + + + + {}} + 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 {
) - } else if (isSnippetOpen) { - configuratorContent = ( - - ) } else { - configuratorContent = ( - + configuratorContent = (<> + + + { isSnippetOpen ? ( + + ) : null } - ) + ) } return ( @@ -127,6 +129,7 @@ export function Configurator({ title }: { title: string }): ReactNode { isOpen={isSidebarOpen} isResizing={isResizing} isSnippetOpen={isSnippetOpen} + onSidebarToggle={handleSidebarToggle} onSnippetToggle={handleSnippetToggle} onStateChange={setConfiguratorState} toastManager={toastManager} diff --git a/apps/widget-configurator/src/app/configurator/styled.ts b/apps/widget-configurator/src/app/configurator/styled.ts index 0e31ecc99d2..f4e9e0bdef1 100644 --- a/apps/widget-configurator/src/app/configurator/styled.ts +++ b/apps/widget-configurator/src/app/configurator/styled.ts @@ -12,7 +12,7 @@ const TRANSPARENCY_CHECKER_PX = 8 const CONTENT_PADDING_PX = 16 export const configuratorCheckeredCanvasSx = - (showIframeOutline: boolean): SxProps => + (showIframeOutline: boolean, blockScroll = false): SxProps => (theme) => { const isDark = theme.palette.mode === 'dark' const squareA = theme.palette.grey[isDark ? 900 : 200] @@ -21,12 +21,13 @@ export const configuratorCheckeredCanvasSx = const pattern = `repeating-conic-gradient(from 90deg, ${squareA} 0% 25%, ${squareB} 0% 50%)` return { + position: 'relative', flex: '1 1 auto', - overflow: 'auto', + overflowY: blockScroll ? 'hidden' : 'scroll', padding: `${CONTENT_PADDING_PX}px`, backgroundColor: base, - '& > div': { + '& > .checkered-canvas': { minWidth: '100%', minHeight: '100%', backgroundImage: `${pattern}`, @@ -40,7 +41,7 @@ export const configuratorCheckeredCanvasSx = alignItems: 'center', }, - '& > div > iframe': { + '& iframe': { display: 'block', border: 0, margin: '0 auto', diff --git a/apps/widget-configurator/src/app/embedDialog/index.tsx b/apps/widget-configurator/src/app/embedDialog/index.tsx index 764ec11e6c4..991b737d3da 100644 --- a/apps/widget-configurator/src/app/embedDialog/index.tsx +++ b/apps/widget-configurator/src/app/embedDialog/index.tsx @@ -12,12 +12,10 @@ import { Tab } from '@mui/material' import MuiAlert, { AlertProps } from '@mui/material/Alert' import Box from '@mui/material/Box' import Button from '@mui/material/Button' -import Dialog, { DialogProps } from '@mui/material/Dialog' -import DialogActions from '@mui/material/DialogActions' -import DialogContent from '@mui/material/DialogContent' -import DialogTitle from '@mui/material/DialogTitle' import Snackbar from '@mui/material/Snackbar' +import { useTheme } from '@mui/material/styles' import Tabs from '@mui/material/Tabs' +import Typography from '@mui/material/Typography' import SVG from 'react-inlinesvg' import SyntaxHighlighter from 'react-syntax-highlighter' // eslint-disable-next-line @typescript-eslint/no-restricted-imports @@ -93,10 +91,10 @@ export interface EmbedDialogProps { // TODO: Break down this large function into smaller functions // eslint-disable-next-line max-lines-per-function export function EmbedDialog({ params, open, handleClose, defaultPalette }: EmbedDialogProps): ReactNode { - const [scroll, setScroll] = useState('paper') + const theme = useTheme() const [tabInfo, setCurrentTabInfo] = useState(TABS[0]) const { id, language, snippetFromParams } = tabInfo - const descriptionElementRef = useRef(null) + const descriptionElementRef = useRef(null) const cowAnalytics = useCowAnalytics() const [snackbarOpen, setSnackbarOpen] = useState(false) @@ -122,7 +120,6 @@ export function EmbedDialog({ params, open, handleClose, defaultPalette }: Embed useEffect(() => { if (open) { - setScroll('paper') cowAnalytics.sendEvent({ category: AnalyticsCategory.WIDGET_CONFIGURATOR, action: 'View code', @@ -142,25 +139,67 @@ export function EmbedDialog({ params, open, handleClose, defaultPalette }: Embed const onChangeTab = useCallback((_event: SyntheticEvent, newValue: TabInfo) => setCurrentTabInfo(newValue), []) return ( -
- + t.palette.background.paper, + }} > - Snippet for CoW Widget + t.palette.background.paper, + borderBottom: 1, + borderColor: 'divider', + }} + > + + + Snippet for CoW Widget + + + + + + - - + + +
-
- - - - -
+ + Successfully copied to clipboard! -
+ ) } diff --git a/libs/widget-react/src/lib/CowSwapWidget.tsx b/libs/widget-react/src/lib/CowSwapWidget.tsx index 3a13de2a2a0..ca9390788c8 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 { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32615d5a521..ebf40a66f85 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1286,6 +1286,9 @@ importers: react-dom: specifier: 19.1.2 version: 19.1.2(react@19.1.2) + react-feather: + specifier: ^2.0.10 + version: 2.0.10(react@19.1.2) react-inlinesvg: specifier: ^4.1.5 version: 4.2.0(react@19.1.2) @@ -28585,7 +28588,7 @@ snapshots: dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 - lodash: 4.17.21 + lodash: 4.17.23 pretty-error: 4.0.0 tapable: 2.3.0 webpack: 5.102.1(@swc/core@1.13.5(@swc/helpers@0.5.17)) @@ -31943,7 +31946,7 @@ snapshots: pretty-error@4.0.0: dependencies: - lodash: 4.17.21 + lodash: 4.17.23 renderkid: 3.0.0 optional: true @@ -32673,7 +32676,7 @@ snapshots: css-select: 4.3.0 dom-converter: 0.2.0 htmlparser2: 6.1.0 - lodash: 4.17.21 + lodash: 4.17.23 strip-ansi: 6.0.1 optional: true @@ -35354,7 +35357,7 @@ snapshots: fast-json-stable-stringify: 2.1.0 fs-extra: 9.1.0 glob: 7.2.3 - lodash: 4.17.21 + lodash: 4.17.23 pretty-bytes: 5.6.0 rollup: 2.79.2 rollup-plugin-terser: 7.0.2(rollup@2.79.2) From c871585097a64ebfc12c721f12a3c304f70b8b0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Thu, 9 Apr 2026 14:13:19 +0200 Subject: [PATCH 011/110] refactor: restructure directories --- .../configurator/configurator.component.tsx} | 32 +++++++++---------- .../configurator/configurator.styles.ts} | 14 ++++---- .../controls/sidebar-controls.component.tsx | 0 .../controls/sidebar-controls.styles.ts | 0 .../footer/sidebar-footer.component.tsx | 0 .../header/sidebar-header.component.tsx | 0 .../sidebar.component.tsx} | 8 ++--- .../sidebar.styles.ts} | 0 .../components/snippet/snippet.component.tsx} | 8 ++--- .../components/snippet/snippet.const.ts} | 0 .../snippet}/utils/formatParameters.ts | 4 +-- .../components/snippet}/utils/htmlExample.ts | 4 +-- .../components/snippet}/utils/jsExample.ts | 4 +-- .../snippet}/utils/reactTsExample.ts | 4 +-- .../snippet}/utils/sanitizeParameters.ts | 4 +-- .../components/snippet}/utils/tsExample.ts | 4 +-- apps/widget-configurator/src/main.tsx | 2 +- libs/widget-lib/src/cowSwapWidget.ts | 2 ++ libs/widget-react/src/lib/CowSwapWidget.tsx | 2 +- 19 files changed, 47 insertions(+), 45 deletions(-) rename apps/widget-configurator/src/app/configurator/{index.tsx => components/configurator/configurator.component.tsx} (84%) rename apps/widget-configurator/src/app/configurator/{styled.ts => components/configurator/configurator.styles.ts} (86%) rename apps/widget-configurator/src/app/configurator/components/{configurator-sidebar => sidebar}/controls/sidebar-controls.component.tsx (100%) rename apps/widget-configurator/src/app/configurator/components/{configurator-sidebar => sidebar}/controls/sidebar-controls.styles.ts (100%) rename apps/widget-configurator/src/app/configurator/components/{configurator-sidebar => sidebar}/footer/sidebar-footer.component.tsx (100%) rename apps/widget-configurator/src/app/configurator/components/{configurator-sidebar => sidebar}/header/sidebar-header.component.tsx (100%) rename apps/widget-configurator/src/app/configurator/components/{configurator-sidebar/configurator-sidebar.component.tsx => sidebar/sidebar.component.tsx} (99%) rename apps/widget-configurator/src/app/configurator/components/{configurator-sidebar/configurator-sidebar.styles.ts => sidebar/sidebar.styles.ts} (100%) rename apps/widget-configurator/src/app/{embedDialog/index.tsx => configurator/components/snippet/snippet.component.tsx} (96%) rename apps/widget-configurator/src/app/{embedDialog/const.ts => configurator/components/snippet/snippet.const.ts} (100%) rename apps/widget-configurator/src/app/{embedDialog => configurator/components/snippet}/utils/formatParameters.ts (96%) rename apps/widget-configurator/src/app/{embedDialog => configurator/components/snippet}/utils/htmlExample.ts (93%) rename apps/widget-configurator/src/app/{embedDialog => configurator/components/snippet}/utils/jsExample.ts (90%) rename apps/widget-configurator/src/app/{embedDialog => configurator/components/snippet}/utils/reactTsExample.ts (87%) rename apps/widget-configurator/src/app/{embedDialog => configurator/components/snippet}/utils/sanitizeParameters.ts (91%) rename apps/widget-configurator/src/app/{embedDialog => configurator/components/snippet}/utils/tsExample.ts (86%) diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/components/configurator/configurator.component.tsx similarity index 84% rename from apps/widget-configurator/src/app/configurator/index.tsx rename to apps/widget-configurator/src/app/configurator/components/configurator/configurator.component.tsx index 36ae033766e..83f1317df81 100644 --- a/apps/widget-configurator/src/app/configurator/index.tsx +++ b/apps/widget-configurator/src/app/configurator/components/configurator/configurator.component.tsx @@ -12,19 +12,19 @@ import { CircularProgress, IconButton, Snackbar } from '@mui/material' import Box from '@mui/material/Box' import { useWeb3ModalAccount, useWeb3ModalTheme } from '@web3modal/ethers5/react' -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 { useToastsManager } from './hooks/useToastsManager' -import { useWidgetParams } from './hooks/useWidgetParamsAndSettings' -import { configuratorCheckeredCanvasSx, configuradorRootSx } from './styled' -import { ConfiguratorState } from './types' - -import { AnalyticsCategory } from '../../common/analytics/types' -import { EmbedDialog } from '../embedDialog' +import { Sidebar } from '../sidebar/sidebar.component' +import { DRAWER_WIDTH_CSS_VAR } from '../sidebar/sidebar.styles' +import { SidebarControls } from '../sidebar/controls/sidebar-controls.component' +import { COW_LISTENERS, IS_IFRAME } from '../../consts' +import { useProvider } from '../../hooks/useProvider' +import { useResizableDrawerWidth } from '../../hooks/useResizableDrawerWidth' +import { useToastsManager } from '../../hooks/useToastsManager' +import { useWidgetParams } from '../../hooks/useWidgetParamsAndSettings' +import { configuratorCheckeredCanvasSx, configuradorRootSx } from './configurator.styles' +import { ConfiguratorState } from '../../types' + +import { AnalyticsCategory } from '../../../../common/analytics/types' +import { Snippet } from '../snippet/snippet.component' declare global { interface Window { @@ -92,7 +92,7 @@ export function Configurator({ title }: { title: string }): ReactNode { if (!isWidgetReady || !params || !configuratorState) { configuratorContent = ( -
+
@@ -107,7 +107,7 @@ export function Configurator({ title }: { title: string }): ReactNode { /> { isSnippetOpen ? ( - - .checkered-canvas': { + '& > #cowswap-widget': { minWidth: '100%', minHeight: '100%', backgroundImage: `${pattern}`, @@ -39,13 +39,13 @@ export const configuratorCheckeredCanvasSx = display: 'flex', justifyContent: 'center', alignItems: 'center', - }, - '& iframe': { - display: 'block', - border: 0, - margin: '0 auto', - outline: showIframeOutline ? '1px dashed cyan' : 'none', + '& > #cowswap-iframe': { + display: 'block', + border: 0, + margin: '0 auto', + outline: showIframeOutline ? '1px dashed cyan' : 'none', + }, }, } } 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/sidebar/controls/sidebar-controls.component.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.component.tsx rename to apps/widget-configurator/src/app/configurator/components/sidebar/controls/sidebar-controls.component.tsx 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/sidebar/controls/sidebar-controls.styles.ts similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/configurator-sidebar/controls/sidebar-controls.styles.ts rename to apps/widget-configurator/src/app/configurator/components/sidebar/controls/sidebar-controls.styles.ts 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/sidebar/footer/sidebar-footer.component.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/configurator-sidebar/footer/sidebar-footer.component.tsx rename to apps/widget-configurator/src/app/configurator/components/sidebar/footer/sidebar-footer.component.tsx 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/sidebar/header/sidebar-header.component.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/configurator-sidebar/header/sidebar-header.component.tsx rename to apps/widget-configurator/src/app/configurator/components/sidebar/header/sidebar-header.component.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.component.tsx b/apps/widget-configurator/src/app/configurator/components/sidebar/sidebar.component.tsx similarity index 99% rename from apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.component.tsx rename to apps/widget-configurator/src/app/configurator/components/sidebar/sidebar.component.tsx index 95df2ea40b1..e13ea114a95 100644 --- a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.component.tsx +++ b/apps/widget-configurator/src/app/configurator/components/sidebar/sidebar.component.tsx @@ -12,7 +12,7 @@ import Typography from '@mui/material/Typography' import type { Theme } from '@mui/material/styles' import { useWeb3ModalAccount } from '@web3modal/ethers5/react' -import { getDrawerSx } from './configurator-sidebar.styles' +import { getDrawerSx } from './sidebar.styles' import { SidebarFooter } from './footer/sidebar-footer.component' import { ColorModeContext } from '../../../../theme/ColorModeContext' @@ -44,7 +44,7 @@ import { WidgetHooksControl } from '../controls/WidgetHooksControl' import type * as CSS from 'csstype' -export interface ConfiguratorSidebarProps { +export interface SidebarProps { title: string isOpen: boolean isResizing: boolean @@ -56,7 +56,7 @@ export interface ConfiguratorSidebarProps { } // eslint-disable-next-line max-lines-per-function -export function ConfiguratorSidebar({ +export function Sidebar({ title, isOpen, isResizing, @@ -65,7 +65,7 @@ export function ConfiguratorSidebar({ onSnippetToggle, onStateChange, toastManager, -}: ConfiguratorSidebarProps): ReactNode { +}: SidebarProps): ReactNode { const availableChains = useAvailableChains() const [expandedSection, setExpandedSection] = useState('Basics') diff --git a/apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.styles.ts b/apps/widget-configurator/src/app/configurator/components/sidebar/sidebar.styles.ts similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/configurator-sidebar/configurator-sidebar.styles.ts rename to apps/widget-configurator/src/app/configurator/components/sidebar/sidebar.styles.ts diff --git a/apps/widget-configurator/src/app/embedDialog/index.tsx b/apps/widget-configurator/src/app/configurator/components/snippet/snippet.component.tsx similarity index 96% rename from apps/widget-configurator/src/app/embedDialog/index.tsx rename to apps/widget-configurator/src/app/configurator/components/snippet/snippet.component.tsx index 991b737d3da..e5db6ab83b0 100644 --- a/apps/widget-configurator/src/app/embedDialog/index.tsx +++ b/apps/widget-configurator/src/app/configurator/components/snippet/snippet.component.tsx @@ -26,8 +26,8 @@ import { jsExample } from './utils/jsExample' import { reactTsExample } from './utils/reactTsExample' import { tsExample } from './utils/tsExample' -import { AnalyticsCategory } from '../../common/analytics/types' -import { ColorPalette } from '../configurator/types' +import { AnalyticsCategory } from '../../../../common/analytics/types' +import { ColorPalette } from '../../types' interface TabInfo { id: number @@ -81,7 +81,7 @@ function a11yProps(id: number) { } } -export interface EmbedDialogProps { +export interface SnippetProps { params: CowSwapWidgetProps['params'] defaultPalette: ColorPalette open: boolean @@ -90,7 +90,7 @@ export interface EmbedDialogProps { // TODO: Break down this large function into smaller functions // eslint-disable-next-line max-lines-per-function -export function EmbedDialog({ params, open, handleClose, defaultPalette }: EmbedDialogProps): ReactNode { +export function Snippet({ params, open, handleClose, defaultPalette }: SnippetProps): ReactNode { const theme = useTheme() const [tabInfo, setCurrentTabInfo] = useState(TABS[0]) const { id, language, snippetFromParams } = tabInfo diff --git a/apps/widget-configurator/src/app/embedDialog/const.ts b/apps/widget-configurator/src/app/configurator/components/snippet/snippet.const.ts similarity index 100% rename from apps/widget-configurator/src/app/embedDialog/const.ts rename to apps/widget-configurator/src/app/configurator/components/snippet/snippet.const.ts diff --git a/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.ts b/apps/widget-configurator/src/app/configurator/components/snippet/utils/formatParameters.ts similarity index 96% rename from apps/widget-configurator/src/app/embedDialog/utils/formatParameters.ts rename to apps/widget-configurator/src/app/configurator/components/snippet/utils/formatParameters.ts index 6b1e48742d0..85e164e0a8b 100644 --- a/apps/widget-configurator/src/app/embedDialog/utils/formatParameters.ts +++ b/apps/widget-configurator/src/app/configurator/components/snippet/utils/formatParameters.ts @@ -2,12 +2,12 @@ import { CowSwapWidgetParams } from '@cowprotocol/widget-lib' import { sanitizeParameters } from './sanitizeParameters' -import { ColorPalette } from '../../configurator/types' +import { ColorPalette } from '../../../types' import { COMMENTS_BY_PARAM_NAME, COMMENTS_BY_PARAM_NAME_TYPESCRIPT, WIDGET_CONFIGURATOR_DEFAULT_BASE_URL, -} from '../const' +} from '../snippet.const' export function formatParameters( params: CowSwapWidgetParams, diff --git a/apps/widget-configurator/src/app/embedDialog/utils/htmlExample.ts b/apps/widget-configurator/src/app/configurator/components/snippet/utils/htmlExample.ts similarity index 93% rename from apps/widget-configurator/src/app/embedDialog/utils/htmlExample.ts rename to apps/widget-configurator/src/app/configurator/components/snippet/utils/htmlExample.ts index 029c3f07106..99cf89a0c34 100644 --- a/apps/widget-configurator/src/app/embedDialog/utils/htmlExample.ts +++ b/apps/widget-configurator/src/app/configurator/components/snippet/utils/htmlExample.ts @@ -2,8 +2,8 @@ import { CowSwapWidgetParams } from '@cowprotocol/widget-lib' import { formatParameters } from './formatParameters' -import { ColorPalette } from '../../configurator/types' -import { COMMENTS_BEFORE_PARAMS, PROVIDER_PARAM_COMMENT } from '../const' +import { ColorPalette } from '../../../types' +import { COMMENTS_BEFORE_PARAMS, PROVIDER_PARAM_COMMENT } from '../snippet.const' export function vanillaNoDepsExample(params: CowSwapWidgetParams, defaultPalette: ColorPalette): string { return ` diff --git a/apps/widget-configurator/src/app/embedDialog/utils/jsExample.ts b/apps/widget-configurator/src/app/configurator/components/snippet/utils/jsExample.ts similarity index 90% rename from apps/widget-configurator/src/app/embedDialog/utils/jsExample.ts rename to apps/widget-configurator/src/app/configurator/components/snippet/utils/jsExample.ts index ced0d99114f..f411ace8f74 100644 --- a/apps/widget-configurator/src/app/embedDialog/utils/jsExample.ts +++ b/apps/widget-configurator/src/app/configurator/components/snippet/utils/jsExample.ts @@ -2,8 +2,8 @@ import { CowSwapWidgetParams } from '@cowprotocol/widget-lib' import { formatParameters } from './formatParameters' -import { ColorPalette } from '../../configurator/types' -import { COMMENTS_BEFORE_PARAMS, PROVIDER_PARAM_COMMENT } from '../const' +import { ColorPalette } from '../../../types' +import { COMMENTS_BEFORE_PARAMS, PROVIDER_PARAM_COMMENT } from '../snippet.const' export function jsExample(params: CowSwapWidgetParams, defaultPalette: ColorPalette): string { return ` diff --git a/apps/widget-configurator/src/app/embedDialog/utils/reactTsExample.ts b/apps/widget-configurator/src/app/configurator/components/snippet/utils/reactTsExample.ts similarity index 87% rename from apps/widget-configurator/src/app/embedDialog/utils/reactTsExample.ts rename to apps/widget-configurator/src/app/configurator/components/snippet/utils/reactTsExample.ts index 58554c11a70..8488d7cc7e8 100644 --- a/apps/widget-configurator/src/app/embedDialog/utils/reactTsExample.ts +++ b/apps/widget-configurator/src/app/configurator/components/snippet/utils/reactTsExample.ts @@ -2,8 +2,8 @@ import type { CowSwapWidgetParams } from '@cowprotocol/widget-lib' import { formatParameters } from './formatParameters' -import { ColorPalette } from '../../configurator/types' -import { COMMENTS_BEFORE_PARAMS, REACT_IMPORT_STATEMENT, PROVIDER_PARAM_COMMENT } from '../const' +import { ColorPalette } from '../../../types' +import { COMMENTS_BEFORE_PARAMS, REACT_IMPORT_STATEMENT, PROVIDER_PARAM_COMMENT } from '../snippet.const' export function reactTsExample(params: CowSwapWidgetParams, defaultPalette: ColorPalette): string { return ` diff --git a/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts b/apps/widget-configurator/src/app/configurator/components/snippet/utils/sanitizeParameters.ts similarity index 91% rename from apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts rename to apps/widget-configurator/src/app/configurator/components/snippet/utils/sanitizeParameters.ts index 55673f69be6..82d1e114b3e 100644 --- a/apps/widget-configurator/src/app/embedDialog/utils/sanitizeParameters.ts +++ b/apps/widget-configurator/src/app/configurator/components/snippet/utils/sanitizeParameters.ts @@ -1,7 +1,7 @@ import { CowSwapWidgetPalette, CowSwapWidgetPaletteColors, CowSwapWidgetParams } from '@cowprotocol/widget-lib' -import { ColorPalette } from '../../configurator/types' -import { SANITIZE_PARAMS } from '../const' +import { ColorPalette } from '../../../types' +import { SANITIZE_PARAMS } from '../snippet.const' export function sanitizeParameters(params: CowSwapWidgetParams, defaultPalette: ColorPalette): CowSwapWidgetParams { return { diff --git a/apps/widget-configurator/src/app/embedDialog/utils/tsExample.ts b/apps/widget-configurator/src/app/configurator/components/snippet/utils/tsExample.ts similarity index 86% rename from apps/widget-configurator/src/app/embedDialog/utils/tsExample.ts rename to apps/widget-configurator/src/app/configurator/components/snippet/utils/tsExample.ts index b9a7da5aeab..4d7ab10ef8c 100644 --- a/apps/widget-configurator/src/app/embedDialog/utils/tsExample.ts +++ b/apps/widget-configurator/src/app/configurator/components/snippet/utils/tsExample.ts @@ -2,8 +2,8 @@ import type { CowSwapWidgetParams } from '@cowprotocol/widget-lib' import { formatParameters } from './formatParameters' -import { ColorPalette } from '../../configurator/types' -import { COMMENTS_BEFORE_PARAMS, IMPORT_STATEMENT as TS_IMPORT_STATEMENT, PROVIDER_PARAM_COMMENT } from '../const' +import { ColorPalette } from '../../../types' +import { COMMENTS_BEFORE_PARAMS, IMPORT_STATEMENT as TS_IMPORT_STATEMENT, PROVIDER_PARAM_COMMENT } from '../snippet.const' export function tsExample(params: CowSwapWidgetParams, defaultPalette: ColorPalette): string { return ` diff --git a/apps/widget-configurator/src/main.tsx b/apps/widget-configurator/src/main.tsx index cf38a6f39c6..02d24ed3a43 100644 --- a/apps/widget-configurator/src/main.tsx +++ b/apps/widget-configurator/src/main.tsx @@ -9,7 +9,7 @@ import { createTheme, PaletteOptions, ThemeProvider } from '@mui/material/styles import 'inter-ui' import { createRoot } from 'react-dom/client' -import { Configurator } from './app/configurator' +import { Configurator } from './app/configurator/components/configurator/configurator.component' import { ColorModeContext, globalStyles } from './theme/ColorModeContext' import { commonTypography } from './theme/commonTypography' import { useColorMode } from './theme/hooks/useColorMode' diff --git a/libs/widget-lib/src/cowSwapWidget.ts b/libs/widget-lib/src/cowSwapWidget.ts index 1421f1e07c2..5f978713634 100644 --- a/libs/widget-lib/src/cowSwapWidget.ts +++ b/libs/widget-lib/src/cowSwapWidget.ts @@ -191,6 +191,8 @@ function updateProvider( function createIframe(params: CowSwapWidgetParams): HTMLIFrameElement { const iframe = document.createElement('iframe') + // TODO: Create constant for this and the other ID and export them. + iframe.id = 'cowswap-iframe' iframe.src = buildWidgetUrl(params) iframe.allow = 'clipboard-read; clipboard-write' diff --git a/libs/widget-react/src/lib/CowSwapWidget.tsx b/libs/widget-react/src/lib/CowSwapWidget.tsx index ca9390788c8..0b4a1b3cdd8 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 d7064031734a122ea074577e8a7a0faac37da590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Thu, 9 Apr 2026 14:30:18 +0200 Subject: [PATCH 012/110] refactor: restructure directories --- .../configurator/configurator.component.tsx | 52 +++++++++---------- .../configurator/configurator.styles.ts | 0 .../controls/AccordionSection.test.tsx | 0 .../components/controls/AccordionSection.tsx | 0 .../controls/AddCustomListDialog.tsx | 2 +- .../controls/AppearanceStyleControls.tsx | 0 .../controls/BooleanSwitchControl.test.tsx | 0 .../controls/BooleanSwitchControl.tsx | 0 .../controls/CurrencyInputControl.tsx | 0 .../controls/CurrentTradeTypeControl.tsx | 2 +- .../controls/CustomImagesControl.tsx | 0 .../controls/CustomSoundsControl.tsx | 0 .../components/controls/DeadlineControl.tsx | 0 .../components/controls/HelpTooltipButton.tsx | 0 .../controls/LocaleControl.test.tsx | 0 .../components/controls/LocaleControl.tsx | 0 .../components/controls/ModeControl.test.tsx | 0 .../components/controls/ModeControl.tsx | 0 .../components/controls/NetworkControl.tsx | 0 .../controls/PaletteControl.test.tsx | 0 .../components/controls/PaletteControl.tsx | 0 .../components/controls/PartnerFeeControl.tsx | 0 .../components/controls/SettingHeading.tsx | 0 .../components/controls/ThemeControl.test.tsx | 0 .../components/controls/ThemeControl.tsx | 2 +- .../components/controls/TokenListControl.tsx | 0 .../components/controls/TradeModesControl.tsx | 0 .../controls/WidgetHooksControl.tsx | 2 +- .../controls/sidebar-controls.component.tsx | 2 +- .../controls/sidebar-controls.styles.ts | 6 +-- .../footer/sidebar-footer.component.tsx | 2 +- .../header/sidebar-header.component.tsx | 2 +- .../components/sidebar/sidebar.component.tsx | 21 +++----- .../components/sidebar/sidebar.styles.ts | 0 .../components/snippet/snippet.component.tsx | 4 +- .../components/snippet/snippet.const.ts | 0 .../snippet/utils/formatParameters.ts | 2 +- .../components/snippet/utils/htmlExample.ts | 2 +- .../components/snippet/utils/jsExample.ts | 2 +- .../snippet/utils/reactTsExample.ts | 2 +- .../snippet/utils/sanitizeParameters.ts | 2 +- .../components/snippet/utils/tsExample.ts | 2 +- .../consts.ts => configurator.constants.ts} | 12 ++++- .../types.ts => configurator.types.ts} | 0 apps/widget-configurator/src/env.ts | 5 -- .../hooks/useColorPaletteManager.ts | 4 +- .../configurator => }/hooks/useJsonState.ts | 0 .../configurator => }/hooks/useProvider.ts | 0 .../hooks/useResizableDrawerWidth.test.ts | 0 .../hooks/useResizableDrawerWidth.ts | 0 .../hooks/useSyncWidgetNetwork.ts | 0 .../hooks/useToastsManager.tsx | 2 +- .../hooks/useWidgetParamsAndSettings.ts | 4 +- apps/widget-configurator/src/main.tsx | 2 +- .../utils/parseCustomTokensInput.test.ts | 0 .../utils/parseCustomTokensInput.ts | 0 .../utils/validateURL.test.ts | 0 .../configurator => }/utils/validateURL.ts | 0 58 files changed, 69 insertions(+), 69 deletions(-) rename apps/widget-configurator/src/{app/configurator => }/components/configurator/configurator.component.tsx (86%) rename apps/widget-configurator/src/{app/configurator => }/components/configurator/configurator.styles.ts (100%) 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 (98%) rename apps/widget-configurator/src/{app/configurator => }/components/controls/AppearanceStyleControls.tsx (100%) 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/CurrencyInputControl.tsx (100%) rename apps/widget-configurator/src/{app/configurator => }/components/controls/CurrentTradeTypeControl.tsx (94%) 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 (96%) rename apps/widget-configurator/src/{app/configurator => }/components/sidebar/controls/sidebar-controls.component.tsx (100%) rename apps/widget-configurator/src/{app/configurator => }/components/sidebar/controls/sidebar-controls.styles.ts (97%) rename apps/widget-configurator/src/{app/configurator => }/components/sidebar/footer/sidebar-footer.component.tsx (98%) rename apps/widget-configurator/src/{app/configurator => }/components/sidebar/header/sidebar-header.component.tsx (97%) rename apps/widget-configurator/src/{app/configurator => }/components/sidebar/sidebar.component.tsx (98%) rename apps/widget-configurator/src/{app/configurator => }/components/sidebar/sidebar.styles.ts (100%) rename apps/widget-configurator/src/{app/configurator => }/components/snippet/snippet.component.tsx (98%) rename apps/widget-configurator/src/{app/configurator => }/components/snippet/snippet.const.ts (100%) rename apps/widget-configurator/src/{app/configurator => }/components/snippet/utils/formatParameters.ts (97%) rename apps/widget-configurator/src/{app/configurator => }/components/snippet/utils/htmlExample.ts (94%) rename apps/widget-configurator/src/{app/configurator => }/components/snippet/utils/jsExample.ts (92%) rename apps/widget-configurator/src/{app/configurator => }/components/snippet/utils/reactTsExample.ts (92%) rename apps/widget-configurator/src/{app/configurator => }/components/snippet/utils/sanitizeParameters.ts (94%) rename apps/widget-configurator/src/{app/configurator => }/components/snippet/utils/tsExample.ts (92%) rename apps/widget-configurator/src/{app/configurator/consts.ts => configurator.constants.ts} (93%) rename apps/widget-configurator/src/{app/configurator/types.ts => configurator.types.ts} (100%) delete mode 100644 apps/widget-configurator/src/env.ts rename apps/widget-configurator/src/{app/configurator => }/hooks/useColorPaletteManager.ts (96%) rename apps/widget-configurator/src/{app/configurator => }/hooks/useJsonState.ts (100%) rename apps/widget-configurator/src/{app/configurator => }/hooks/useProvider.ts (100%) rename apps/widget-configurator/src/{app/configurator => }/hooks/useResizableDrawerWidth.test.ts (100%) rename apps/widget-configurator/src/{app/configurator => }/hooks/useResizableDrawerWidth.ts (100%) rename apps/widget-configurator/src/{app/configurator => }/hooks/useSyncWidgetNetwork.ts (100%) rename apps/widget-configurator/src/{app/configurator => }/hooks/useToastsManager.tsx (97%) rename apps/widget-configurator/src/{app/configurator => }/hooks/useWidgetParamsAndSettings.ts (98%) rename apps/widget-configurator/src/{app/configurator => }/utils/parseCustomTokensInput.test.ts (100%) rename apps/widget-configurator/src/{app/configurator => }/utils/parseCustomTokensInput.ts (100%) rename apps/widget-configurator/src/{app/configurator => }/utils/validateURL.test.ts (100%) rename apps/widget-configurator/src/{app/configurator => }/utils/validateURL.ts (100%) diff --git a/apps/widget-configurator/src/app/configurator/components/configurator/configurator.component.tsx b/apps/widget-configurator/src/components/configurator/configurator.component.tsx similarity index 86% rename from apps/widget-configurator/src/app/configurator/components/configurator/configurator.component.tsx rename to apps/widget-configurator/src/components/configurator/configurator.component.tsx index 83f1317df81..59403bf2f7d 100644 --- a/apps/widget-configurator/src/app/configurator/components/configurator/configurator.component.tsx +++ b/apps/widget-configurator/src/components/configurator/configurator.component.tsx @@ -1,5 +1,3 @@ -/* eslint-disable max-lines-per-function */ - import React, { CSSProperties, ReactNode, useCallback, useEffect, useRef, useState } from 'react' import { useCowAnalytics } from '@cowprotocol/analytics' @@ -12,18 +10,18 @@ import { CircularProgress, IconButton, Snackbar } from '@mui/material' import Box from '@mui/material/Box' import { useWeb3ModalAccount, useWeb3ModalTheme } from '@web3modal/ethers5/react' -import { Sidebar } from '../sidebar/sidebar.component' -import { DRAWER_WIDTH_CSS_VAR } from '../sidebar/sidebar.styles' -import { SidebarControls } from '../sidebar/controls/sidebar-controls.component' -import { COW_LISTENERS, IS_IFRAME } from '../../consts' +import { configuratorCheckeredCanvasSx, configuradorRootSx } from './configurator.styles' + +import { AnalyticsCategory } from '../../common/analytics/types' +import { COW_LISTENERS, IS_IFRAME } from '../../configurator.constants' import { useProvider } from '../../hooks/useProvider' import { useResizableDrawerWidth } from '../../hooks/useResizableDrawerWidth' import { useToastsManager } from '../../hooks/useToastsManager' import { useWidgetParams } from '../../hooks/useWidgetParamsAndSettings' -import { configuratorCheckeredCanvasSx, configuradorRootSx } from './configurator.styles' -import { ConfiguratorState } from '../../types' - -import { AnalyticsCategory } from '../../../../common/analytics/types' +import { ConfiguratorState } from '../../configurator.types' +import { SidebarControls } from '../sidebar/controls/sidebar-controls.component' +import { Sidebar } from '../sidebar/sidebar.component' +import { DRAWER_WIDTH_CSS_VAR } from '../sidebar/sidebar.styles' import { Snippet } from '../snippet/snippet.component' declare global { @@ -98,24 +96,26 @@ export function Configurator({ title }: { title: string }): ReactNode { ) } else { - configuratorContent = (<> - - - - { isSnippetOpen ? ( - + + - ) : null } - - ) + + {isSnippetOpen ? ( + + ) : null} + + + ) } return ( diff --git a/apps/widget-configurator/src/app/configurator/components/configurator/configurator.styles.ts b/apps/widget-configurator/src/components/configurator/configurator.styles.ts similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/configurator/configurator.styles.ts rename to apps/widget-configurator/src/components/configurator/configurator.styles.ts diff --git a/apps/widget-configurator/src/app/configurator/components/controls/AccordionSection.test.tsx b/apps/widget-configurator/src/components/controls/AccordionSection.test.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/AccordionSection.test.tsx rename to apps/widget-configurator/src/components/controls/AccordionSection.test.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/AccordionSection.tsx b/apps/widget-configurator/src/components/controls/AccordionSection.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/AccordionSection.tsx rename to apps/widget-configurator/src/components/controls/AccordionSection.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/AddCustomListDialog.tsx b/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx similarity index 98% rename from apps/widget-configurator/src/app/configurator/components/controls/AddCustomListDialog.tsx rename to apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx index dcedf23a402..32b7e2a61f3 100644 --- a/apps/widget-configurator/src/app/configurator/components/controls/AddCustomListDialog.tsx +++ b/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx @@ -15,7 +15,7 @@ import { } from '@mui/material' import Tabs from '@mui/material/Tabs' -import { DEFAULT_CUSTOM_TOKENS } from '../../consts' +import { DEFAULT_CUSTOM_TOKENS } from '../../configurator.constants' import { parseCustomTokensInput } from '../../utils/parseCustomTokensInput' import { validateURL } from '../../utils/validateURL' diff --git a/apps/widget-configurator/src/app/configurator/components/controls/AppearanceStyleControls.tsx b/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/AppearanceStyleControls.tsx rename to apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/BooleanSwitchControl.test.tsx b/apps/widget-configurator/src/components/controls/BooleanSwitchControl.test.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/BooleanSwitchControl.test.tsx rename to apps/widget-configurator/src/components/controls/BooleanSwitchControl.test.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/BooleanSwitchControl.tsx b/apps/widget-configurator/src/components/controls/BooleanSwitchControl.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/BooleanSwitchControl.tsx rename to apps/widget-configurator/src/components/controls/BooleanSwitchControl.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/CurrencyInputControl.tsx b/apps/widget-configurator/src/components/controls/CurrencyInputControl.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/CurrencyInputControl.tsx rename to apps/widget-configurator/src/components/controls/CurrencyInputControl.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/CurrentTradeTypeControl.tsx b/apps/widget-configurator/src/components/controls/CurrentTradeTypeControl.tsx similarity index 94% rename from apps/widget-configurator/src/app/configurator/components/controls/CurrentTradeTypeControl.tsx rename to apps/widget-configurator/src/components/controls/CurrentTradeTypeControl.tsx index e22421d30ce..9974e06cbaf 100644 --- a/apps/widget-configurator/src/app/configurator/components/controls/CurrentTradeTypeControl.tsx +++ b/apps/widget-configurator/src/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 '../../configurator.constants' const LABEL = 'Current trade type' diff --git a/apps/widget-configurator/src/app/configurator/components/controls/CustomImagesControl.tsx b/apps/widget-configurator/src/components/controls/CustomImagesControl.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/CustomImagesControl.tsx rename to apps/widget-configurator/src/components/controls/CustomImagesControl.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/CustomSoundsControl.tsx b/apps/widget-configurator/src/components/controls/CustomSoundsControl.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/CustomSoundsControl.tsx rename to apps/widget-configurator/src/components/controls/CustomSoundsControl.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/DeadlineControl.tsx b/apps/widget-configurator/src/components/controls/DeadlineControl.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/DeadlineControl.tsx rename to apps/widget-configurator/src/components/controls/DeadlineControl.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/HelpTooltipButton.tsx b/apps/widget-configurator/src/components/controls/HelpTooltipButton.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/HelpTooltipButton.tsx rename to apps/widget-configurator/src/components/controls/HelpTooltipButton.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/LocaleControl.test.tsx b/apps/widget-configurator/src/components/controls/LocaleControl.test.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/LocaleControl.test.tsx rename to apps/widget-configurator/src/components/controls/LocaleControl.test.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/LocaleControl.tsx b/apps/widget-configurator/src/components/controls/LocaleControl.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/LocaleControl.tsx rename to apps/widget-configurator/src/components/controls/LocaleControl.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/ModeControl.test.tsx b/apps/widget-configurator/src/components/controls/ModeControl.test.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/ModeControl.test.tsx rename to apps/widget-configurator/src/components/controls/ModeControl.test.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/ModeControl.tsx b/apps/widget-configurator/src/components/controls/ModeControl.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/ModeControl.tsx rename to apps/widget-configurator/src/components/controls/ModeControl.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/NetworkControl.tsx b/apps/widget-configurator/src/components/controls/NetworkControl.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/NetworkControl.tsx rename to apps/widget-configurator/src/components/controls/NetworkControl.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/PaletteControl.test.tsx b/apps/widget-configurator/src/components/controls/PaletteControl.test.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/PaletteControl.test.tsx rename to apps/widget-configurator/src/components/controls/PaletteControl.test.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/PaletteControl.tsx b/apps/widget-configurator/src/components/controls/PaletteControl.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/PaletteControl.tsx rename to apps/widget-configurator/src/components/controls/PaletteControl.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/PartnerFeeControl.tsx b/apps/widget-configurator/src/components/controls/PartnerFeeControl.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/PartnerFeeControl.tsx rename to apps/widget-configurator/src/components/controls/PartnerFeeControl.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/SettingHeading.tsx b/apps/widget-configurator/src/components/controls/SettingHeading.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/SettingHeading.tsx rename to apps/widget-configurator/src/components/controls/SettingHeading.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/ThemeControl.test.tsx b/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/ThemeControl.test.tsx rename to apps/widget-configurator/src/components/controls/ThemeControl.test.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/ThemeControl.tsx b/apps/widget-configurator/src/components/controls/ThemeControl.tsx similarity index 97% rename from apps/widget-configurator/src/app/configurator/components/controls/ThemeControl.tsx rename to apps/widget-configurator/src/components/controls/ThemeControl.tsx index 04019b9ff3c..9fe36b9b56d 100644 --- a/apps/widget-configurator/src/app/configurator/components/controls/ThemeControl.tsx +++ b/apps/widget-configurator/src/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/components/controls/TokenListControl.tsx b/apps/widget-configurator/src/components/controls/TokenListControl.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/TokenListControl.tsx rename to apps/widget-configurator/src/components/controls/TokenListControl.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/TradeModesControl.tsx b/apps/widget-configurator/src/components/controls/TradeModesControl.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/controls/TradeModesControl.tsx rename to apps/widget-configurator/src/components/controls/TradeModesControl.tsx diff --git a/apps/widget-configurator/src/app/configurator/components/controls/WidgetHooksControl.tsx b/apps/widget-configurator/src/components/controls/WidgetHooksControl.tsx similarity index 96% rename from apps/widget-configurator/src/app/configurator/components/controls/WidgetHooksControl.tsx rename to apps/widget-configurator/src/components/controls/WidgetHooksControl.tsx index 1d8654d7693..e1c9389478c 100644 --- a/apps/widget-configurator/src/app/configurator/components/controls/WidgetHooksControl.tsx +++ b/apps/widget-configurator/src/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 '../../configurator.constants' const LABEL = 'Widget hooks' const EMPTY_VALUE_LABEL = 'No hooks selected' diff --git a/apps/widget-configurator/src/app/configurator/components/sidebar/controls/sidebar-controls.component.tsx b/apps/widget-configurator/src/components/sidebar/controls/sidebar-controls.component.tsx similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/sidebar/controls/sidebar-controls.component.tsx rename to apps/widget-configurator/src/components/sidebar/controls/sidebar-controls.component.tsx index 1500a48acde..c4ba655586a 100644 --- a/apps/widget-configurator/src/app/configurator/components/sidebar/controls/sidebar-controls.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/controls/sidebar-controls.component.tsx @@ -1,7 +1,7 @@ import { ReactNode } from 'react' -import { ChevronLeft, ChevronRight } from 'react-feather' import { Box, IconButton } from '@mui/material' +import { ChevronLeft, ChevronRight } from 'react-feather' import { sidebarControlsZeroWidthColumnSx, diff --git a/apps/widget-configurator/src/app/configurator/components/sidebar/controls/sidebar-controls.styles.ts b/apps/widget-configurator/src/components/sidebar/controls/sidebar-controls.styles.ts similarity index 97% rename from apps/widget-configurator/src/app/configurator/components/sidebar/controls/sidebar-controls.styles.ts rename to apps/widget-configurator/src/components/sidebar/controls/sidebar-controls.styles.ts index 3efdcd7221d..2e32e41cf92 100644 --- a/apps/widget-configurator/src/app/configurator/components/sidebar/controls/sidebar-controls.styles.ts +++ b/apps/widget-configurator/src/components/sidebar/controls/sidebar-controls.styles.ts @@ -11,13 +11,13 @@ export const sidebarControlsZeroWidthColumnSx: SxProps = { export const sidebarToggleOpenButton: SxProps = (theme: Theme) => ({ position: 'absolute', - top: "50%", + top: '50%', left: 0, width: 48, height: 48, p: 0, - pl:"24px", - pr: "4px", + pl: '24px', + pr: '4px', transform: 'translate(-50%, -50%)', borderRadius: '50%', border: `none`, diff --git a/apps/widget-configurator/src/app/configurator/components/sidebar/footer/sidebar-footer.component.tsx b/apps/widget-configurator/src/components/sidebar/footer/sidebar-footer.component.tsx similarity index 98% rename from apps/widget-configurator/src/app/configurator/components/sidebar/footer/sidebar-footer.component.tsx rename to apps/widget-configurator/src/components/sidebar/footer/sidebar-footer.component.tsx index a9eca3bb057..d8340fc32b9 100644 --- a/apps/widget-configurator/src/app/configurator/components/sidebar/footer/sidebar-footer.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/footer/sidebar-footer.component.tsx @@ -8,7 +8,7 @@ import IconButton from '@mui/material/IconButton' import Link from '@mui/material/Link' import Tooltip from '@mui/material/Tooltip' -import { UTM_PARAMS } from '../../../consts' +import { UTM_PARAMS } from '../../../configurator.constants' const WIDGET_WEB_URL = `https://cow.fi/widget/?${UTM_PARAMS}` const DEVELOPER_DOCS_URL = `https://docs.cow.fi/cow-protocol/tutorials/widget?${UTM_PARAMS}` diff --git a/apps/widget-configurator/src/app/configurator/components/sidebar/header/sidebar-header.component.tsx b/apps/widget-configurator/src/components/sidebar/header/sidebar-header.component.tsx similarity index 97% rename from apps/widget-configurator/src/app/configurator/components/sidebar/header/sidebar-header.component.tsx rename to apps/widget-configurator/src/components/sidebar/header/sidebar-header.component.tsx index 2159de96f57..f26071dbdf0 100644 --- a/apps/widget-configurator/src/app/configurator/components/sidebar/header/sidebar-header.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/header/sidebar-header.component.tsx @@ -4,7 +4,7 @@ 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' +import { IS_IFRAME } from '../../../configurator.constants' const BRAND_COLOR: Record = { dark: Color.blue300Primary, diff --git a/apps/widget-configurator/src/app/configurator/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx similarity index 98% rename from apps/widget-configurator/src/app/configurator/components/sidebar/sidebar.component.tsx rename to apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index e13ea114a95..54d25c8c2f8 100644 --- a/apps/widget-configurator/src/app/configurator/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -9,24 +9,23 @@ 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 { getDrawerSx } from './sidebar.styles' import { SidebarFooter } from './footer/sidebar-footer.component' +import { SidebarHeader } from './header/sidebar-header.component' +import { getDrawerSx } from './sidebar.styles' -import { ColorModeContext } from '../../../../theme/ColorModeContext' -import { DEFAULT_STATE, DEFAULT_TOKEN_LISTS, IS_IFRAME, TRADE_MODES } from '../../consts' +import { DEFAULT_STATE, DEFAULT_TOKEN_LISTS, IS_IFRAME, TRADE_MODES } from '../../configurator.constants' 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 { ColorModeContext } from '../../theme/ColorModeContext' +import { ConfiguratorState, TokenListItem, WidgetMode } from '../../configurator.types' import { AccordionSection } from '../controls/AccordionSection' import { AppearanceStyleControls } from '../controls/AppearanceStyleControls' import { BooleanSwitchControl } from '../controls/BooleanSwitchControl' -import { SidebarHeader } from './header/sidebar-header.component' import { CurrencyInputControl } from '../controls/CurrencyInputControl' import { CurrentTradeTypeControl } from '../controls/CurrentTradeTypeControl' import { CustomImagesControl } from '../controls/CustomImagesControl' @@ -42,6 +41,7 @@ import { TokenListControl } from '../controls/TokenListControl' import { TradeModesControl } from '../controls/TradeModesControl' import { WidgetHooksControl } from '../controls/WidgetHooksControl' +import type { Theme } from '@mui/material/styles' import type * as CSS from 'csstype' export interface SidebarProps { @@ -317,13 +317,8 @@ export function Sidebar({ useSyncWidgetNetwork(chainId, setNetworkControlState, standaloneMode) return ( - getDrawerSx(theme, isResizing)} - variant="persistent" - anchor="left" - open={isOpen} - > - + getDrawerSx(theme, isResizing)} variant="persistent" anchor="left" open={isOpen}> + diff --git a/apps/widget-configurator/src/app/configurator/components/sidebar/sidebar.styles.ts b/apps/widget-configurator/src/components/sidebar/sidebar.styles.ts similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/sidebar/sidebar.styles.ts rename to apps/widget-configurator/src/components/sidebar/sidebar.styles.ts diff --git a/apps/widget-configurator/src/app/configurator/components/snippet/snippet.component.tsx b/apps/widget-configurator/src/components/snippet/snippet.component.tsx similarity index 98% rename from apps/widget-configurator/src/app/configurator/components/snippet/snippet.component.tsx rename to apps/widget-configurator/src/components/snippet/snippet.component.tsx index e5db6ab83b0..c267366e8cb 100644 --- a/apps/widget-configurator/src/app/configurator/components/snippet/snippet.component.tsx +++ b/apps/widget-configurator/src/components/snippet/snippet.component.tsx @@ -26,8 +26,8 @@ import { jsExample } from './utils/jsExample' import { reactTsExample } from './utils/reactTsExample' import { tsExample } from './utils/tsExample' -import { AnalyticsCategory } from '../../../../common/analytics/types' -import { ColorPalette } from '../../types' +import { AnalyticsCategory } from '../../common/analytics/types' +import { ColorPalette } from '../../configurator.types' interface TabInfo { id: number diff --git a/apps/widget-configurator/src/app/configurator/components/snippet/snippet.const.ts b/apps/widget-configurator/src/components/snippet/snippet.const.ts similarity index 100% rename from apps/widget-configurator/src/app/configurator/components/snippet/snippet.const.ts rename to apps/widget-configurator/src/components/snippet/snippet.const.ts diff --git a/apps/widget-configurator/src/app/configurator/components/snippet/utils/formatParameters.ts b/apps/widget-configurator/src/components/snippet/utils/formatParameters.ts similarity index 97% rename from apps/widget-configurator/src/app/configurator/components/snippet/utils/formatParameters.ts rename to apps/widget-configurator/src/components/snippet/utils/formatParameters.ts index 85e164e0a8b..e46ecef2c06 100644 --- a/apps/widget-configurator/src/app/configurator/components/snippet/utils/formatParameters.ts +++ b/apps/widget-configurator/src/components/snippet/utils/formatParameters.ts @@ -2,7 +2,7 @@ import { CowSwapWidgetParams } from '@cowprotocol/widget-lib' import { sanitizeParameters } from './sanitizeParameters' -import { ColorPalette } from '../../../types' +import { ColorPalette } from '../../../configurator.types' import { COMMENTS_BY_PARAM_NAME, COMMENTS_BY_PARAM_NAME_TYPESCRIPT, diff --git a/apps/widget-configurator/src/app/configurator/components/snippet/utils/htmlExample.ts b/apps/widget-configurator/src/components/snippet/utils/htmlExample.ts similarity index 94% rename from apps/widget-configurator/src/app/configurator/components/snippet/utils/htmlExample.ts rename to apps/widget-configurator/src/components/snippet/utils/htmlExample.ts index 99cf89a0c34..2b3d8cba832 100644 --- a/apps/widget-configurator/src/app/configurator/components/snippet/utils/htmlExample.ts +++ b/apps/widget-configurator/src/components/snippet/utils/htmlExample.ts @@ -2,7 +2,7 @@ import { CowSwapWidgetParams } from '@cowprotocol/widget-lib' import { formatParameters } from './formatParameters' -import { ColorPalette } from '../../../types' +import { ColorPalette } from '../../../configurator.types' import { COMMENTS_BEFORE_PARAMS, PROVIDER_PARAM_COMMENT } from '../snippet.const' export function vanillaNoDepsExample(params: CowSwapWidgetParams, defaultPalette: ColorPalette): string { diff --git a/apps/widget-configurator/src/app/configurator/components/snippet/utils/jsExample.ts b/apps/widget-configurator/src/components/snippet/utils/jsExample.ts similarity index 92% rename from apps/widget-configurator/src/app/configurator/components/snippet/utils/jsExample.ts rename to apps/widget-configurator/src/components/snippet/utils/jsExample.ts index f411ace8f74..4b697cb68b1 100644 --- a/apps/widget-configurator/src/app/configurator/components/snippet/utils/jsExample.ts +++ b/apps/widget-configurator/src/components/snippet/utils/jsExample.ts @@ -2,7 +2,7 @@ import { CowSwapWidgetParams } from '@cowprotocol/widget-lib' import { formatParameters } from './formatParameters' -import { ColorPalette } from '../../../types' +import { ColorPalette } from '../../../configurator.types' import { COMMENTS_BEFORE_PARAMS, PROVIDER_PARAM_COMMENT } from '../snippet.const' export function jsExample(params: CowSwapWidgetParams, defaultPalette: ColorPalette): string { diff --git a/apps/widget-configurator/src/app/configurator/components/snippet/utils/reactTsExample.ts b/apps/widget-configurator/src/components/snippet/utils/reactTsExample.ts similarity index 92% rename from apps/widget-configurator/src/app/configurator/components/snippet/utils/reactTsExample.ts rename to apps/widget-configurator/src/components/snippet/utils/reactTsExample.ts index 8488d7cc7e8..b25ceed57d7 100644 --- a/apps/widget-configurator/src/app/configurator/components/snippet/utils/reactTsExample.ts +++ b/apps/widget-configurator/src/components/snippet/utils/reactTsExample.ts @@ -2,7 +2,7 @@ import type { CowSwapWidgetParams } from '@cowprotocol/widget-lib' import { formatParameters } from './formatParameters' -import { ColorPalette } from '../../../types' +import { ColorPalette } from '../../../configurator.types' import { COMMENTS_BEFORE_PARAMS, REACT_IMPORT_STATEMENT, PROVIDER_PARAM_COMMENT } from '../snippet.const' export function reactTsExample(params: CowSwapWidgetParams, defaultPalette: ColorPalette): string { diff --git a/apps/widget-configurator/src/app/configurator/components/snippet/utils/sanitizeParameters.ts b/apps/widget-configurator/src/components/snippet/utils/sanitizeParameters.ts similarity index 94% rename from apps/widget-configurator/src/app/configurator/components/snippet/utils/sanitizeParameters.ts rename to apps/widget-configurator/src/components/snippet/utils/sanitizeParameters.ts index 82d1e114b3e..87c6ab182ad 100644 --- a/apps/widget-configurator/src/app/configurator/components/snippet/utils/sanitizeParameters.ts +++ b/apps/widget-configurator/src/components/snippet/utils/sanitizeParameters.ts @@ -1,6 +1,6 @@ import { CowSwapWidgetPalette, CowSwapWidgetPaletteColors, CowSwapWidgetParams } from '@cowprotocol/widget-lib' -import { ColorPalette } from '../../../types' +import { ColorPalette } from '../../../configurator.types' import { SANITIZE_PARAMS } from '../snippet.const' export function sanitizeParameters(params: CowSwapWidgetParams, defaultPalette: ColorPalette): CowSwapWidgetParams { diff --git a/apps/widget-configurator/src/app/configurator/components/snippet/utils/tsExample.ts b/apps/widget-configurator/src/components/snippet/utils/tsExample.ts similarity index 92% rename from apps/widget-configurator/src/app/configurator/components/snippet/utils/tsExample.ts rename to apps/widget-configurator/src/components/snippet/utils/tsExample.ts index 4d7ab10ef8c..58af1b23e5d 100644 --- a/apps/widget-configurator/src/app/configurator/components/snippet/utils/tsExample.ts +++ b/apps/widget-configurator/src/components/snippet/utils/tsExample.ts @@ -2,7 +2,7 @@ import type { CowSwapWidgetParams } from '@cowprotocol/widget-lib' import { formatParameters } from './formatParameters' -import { ColorPalette } from '../../../types' +import { ColorPalette } from '../../../configurator.types' import { COMMENTS_BEFORE_PARAMS, IMPORT_STATEMENT as TS_IMPORT_STATEMENT, PROVIDER_PARAM_COMMENT } from '../snippet.const' export function tsExample(params: CowSwapWidgetParams, defaultPalette: ColorPalette): string { diff --git a/apps/widget-configurator/src/app/configurator/consts.ts b/apps/widget-configurator/src/configurator.constants.ts similarity index 93% rename from apps/widget-configurator/src/app/configurator/consts.ts rename to apps/widget-configurator/src/configurator.constants.ts index 8ee2f797925..39a76858372 100644 --- a/apps/widget-configurator/src/app/configurator/consts.ts +++ b/apps/widget-configurator/src/configurator.constants.ts @@ -2,7 +2,17 @@ import { COW_CDN } from '@cowprotocol/common-const' import { CowWidgetEventListeners, CowWidgetEvents, ToastMessageType } from '@cowprotocol/events' import { CowSwapWidgetPaletteParams, TokenInfo, TradeType, WidgetHookEvents } from '@cowprotocol/widget-lib' -import { TokenListItem } from './types' +import { TokenListItem } from './configurator.types' + +// ENV: + +export const isLocalHost = ['localhost', '127.0.0.1'].includes(window.location.hostname) + +export const isVercel = window.location.hostname.includes('vercel.app') + +export const isDev = ['dev.widget.cow.fi', 'dev.swap.cow.fi'].includes(window.location.hostname) + +// UTM: export const UTM_PARAMS = 'utm_content=cow-widget-configurator&utm_medium=web&utm_source=widget.cow.fi' as const diff --git a/apps/widget-configurator/src/app/configurator/types.ts b/apps/widget-configurator/src/configurator.types.ts similarity index 100% rename from apps/widget-configurator/src/app/configurator/types.ts rename to apps/widget-configurator/src/configurator.types.ts diff --git a/apps/widget-configurator/src/env.ts b/apps/widget-configurator/src/env.ts deleted file mode 100644 index adbf44683c7..00000000000 --- a/apps/widget-configurator/src/env.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const isLocalHost = ['localhost', '127.0.0.1'].includes(window.location.hostname) - -export const isVercel = window.location.hostname.includes('vercel.app') - -export const isDev = ['dev.widget.cow.fi', 'dev.swap.cow.fi'].includes(window.location.hostname) diff --git a/apps/widget-configurator/src/app/configurator/hooks/useColorPaletteManager.ts b/apps/widget-configurator/src/hooks/useColorPaletteManager.ts similarity index 96% rename from apps/widget-configurator/src/app/configurator/hooks/useColorPaletteManager.ts rename to apps/widget-configurator/src/hooks/useColorPaletteManager.ts index c9682c5072f..706760cc529 100644 --- a/apps/widget-configurator/src/app/configurator/hooks/useColorPaletteManager.ts +++ b/apps/widget-configurator/src/hooks/useColorPaletteManager.ts @@ -2,8 +2,8 @@ import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } f import { PaletteMode } from '@mui/material' -import { DEFAULT_DARK_PALETTE, DEFAULT_LIGHT_PALETTE } from '../consts' -import { ColorPalette } from '../types' +import { DEFAULT_DARK_PALETTE, DEFAULT_LIGHT_PALETTE } from '../configurator.constants' +import { ColorPalette } from '../configurator.types' const LOCAL_STORAGE_KEY_NAME = 'COW_WIDGET_PALETTE_' diff --git a/apps/widget-configurator/src/app/configurator/hooks/useJsonState.ts b/apps/widget-configurator/src/hooks/useJsonState.ts similarity index 100% rename from apps/widget-configurator/src/app/configurator/hooks/useJsonState.ts rename to apps/widget-configurator/src/hooks/useJsonState.ts diff --git a/apps/widget-configurator/src/app/configurator/hooks/useProvider.ts b/apps/widget-configurator/src/hooks/useProvider.ts similarity index 100% rename from apps/widget-configurator/src/app/configurator/hooks/useProvider.ts rename to apps/widget-configurator/src/hooks/useProvider.ts diff --git a/apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.test.ts b/apps/widget-configurator/src/hooks/useResizableDrawerWidth.test.ts similarity index 100% rename from apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.test.ts rename to apps/widget-configurator/src/hooks/useResizableDrawerWidth.test.ts diff --git a/apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.ts b/apps/widget-configurator/src/hooks/useResizableDrawerWidth.ts similarity index 100% rename from apps/widget-configurator/src/app/configurator/hooks/useResizableDrawerWidth.ts rename to apps/widget-configurator/src/hooks/useResizableDrawerWidth.ts diff --git a/apps/widget-configurator/src/app/configurator/hooks/useSyncWidgetNetwork.ts b/apps/widget-configurator/src/hooks/useSyncWidgetNetwork.ts similarity index 100% rename from apps/widget-configurator/src/app/configurator/hooks/useSyncWidgetNetwork.ts rename to apps/widget-configurator/src/hooks/useSyncWidgetNetwork.ts diff --git a/apps/widget-configurator/src/app/configurator/hooks/useToastsManager.tsx b/apps/widget-configurator/src/hooks/useToastsManager.tsx similarity index 97% rename from apps/widget-configurator/src/app/configurator/hooks/useToastsManager.tsx rename to apps/widget-configurator/src/hooks/useToastsManager.tsx index 115fbd11066..fdb7d8550e6 100644 --- a/apps/widget-configurator/src/app/configurator/hooks/useToastsManager.tsx +++ b/apps/widget-configurator/src/hooks/useToastsManager.tsx @@ -7,7 +7,7 @@ import { ToastMessageType, } from '@cowprotocol/events' -import { COW_LISTENERS } from '../consts' +import { COW_LISTENERS } from '../configurator.constants' export interface UseToastsManagerReturn { disableToastMessages: boolean diff --git a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts similarity index 98% rename from apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts rename to apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts index 977a70766db..21f709a0f67 100644 --- a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts @@ -2,8 +2,8 @@ import { useMemo } from 'react' import { CowSwapWidgetParams, TradeType, WidgetHookEvents } from '@cowprotocol/widget-lib' -import { isDev, isLocalHost, isVercel } from '../../../env' -import { ConfiguratorState } from '../types' +import { isDev, isLocalHost, isVercel } from '../configurator.constants' +import { ConfiguratorState } from '../configurator.types' const vercelSuffix = '-cowswap-dev.vercel.app' diff --git a/apps/widget-configurator/src/main.tsx b/apps/widget-configurator/src/main.tsx index 02d24ed3a43..3548b9dc466 100644 --- a/apps/widget-configurator/src/main.tsx +++ b/apps/widget-configurator/src/main.tsx @@ -9,7 +9,7 @@ import { createTheme, PaletteOptions, ThemeProvider } from '@mui/material/styles import 'inter-ui' import { createRoot } from 'react-dom/client' -import { Configurator } from './app/configurator/components/configurator/configurator.component' +import { Configurator } from './components/configurator/configurator.component' import { ColorModeContext, globalStyles } from './theme/ColorModeContext' import { commonTypography } from './theme/commonTypography' import { useColorMode } from './theme/hooks/useColorMode' diff --git a/apps/widget-configurator/src/app/configurator/utils/parseCustomTokensInput.test.ts b/apps/widget-configurator/src/utils/parseCustomTokensInput.test.ts similarity index 100% rename from apps/widget-configurator/src/app/configurator/utils/parseCustomTokensInput.test.ts rename to apps/widget-configurator/src/utils/parseCustomTokensInput.test.ts diff --git a/apps/widget-configurator/src/app/configurator/utils/parseCustomTokensInput.ts b/apps/widget-configurator/src/utils/parseCustomTokensInput.ts similarity index 100% rename from apps/widget-configurator/src/app/configurator/utils/parseCustomTokensInput.ts rename to apps/widget-configurator/src/utils/parseCustomTokensInput.ts diff --git a/apps/widget-configurator/src/app/configurator/utils/validateURL.test.ts b/apps/widget-configurator/src/utils/validateURL.test.ts similarity index 100% rename from apps/widget-configurator/src/app/configurator/utils/validateURL.test.ts rename to apps/widget-configurator/src/utils/validateURL.test.ts diff --git a/apps/widget-configurator/src/app/configurator/utils/validateURL.ts b/apps/widget-configurator/src/utils/validateURL.ts similarity index 100% rename from apps/widget-configurator/src/app/configurator/utils/validateURL.ts rename to apps/widget-configurator/src/utils/validateURL.ts From 34a202be468d6c998974a9ee8d09b2dca838ceaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Thu, 9 Apr 2026 18:14:39 +0200 Subject: [PATCH 013/110] refactor: better organize widget-configurator app directories and files --- .../components/controls/SettingHeading.tsx | 20 ------------ .../components/sidebar/sidebar.component.tsx | 32 +++++++++++++------ .../Accordion}/AccordionSection.test.tsx | 0 .../Accordion}/AccordionSection.tsx | 0 .../HelpTooltipButton}/HelpTooltipButton.tsx | 0 .../BooleanSwitchControl.test.tsx | 0 .../BooleanSwitch}/BooleanSwitchControl.tsx | 0 .../CurrencyInput}/CurrencyInputControl.tsx | 0 .../Select}/CurrentTradeTypeControl.tsx | 2 +- .../controls/Select}/LocaleControl.test.tsx | 0 .../controls/Select}/LocaleControl.tsx | 0 .../controls/Select}/ModeControl.test.tsx | 0 .../controls/Select}/ModeControl.tsx | 2 +- .../controls/Select}/NetworkControl.tsx | 0 .../controls/Select}/TradeModesControl.tsx | 0 .../controls/Select}/WidgetHooksControl.tsx | 2 +- .../ui/controls/base/base-controls.types.ts | 0 .../src/hooks/useSyncWidgetNetwork.ts | 2 +- 18 files changed, 26 insertions(+), 34 deletions(-) delete mode 100644 apps/widget-configurator/src/components/controls/SettingHeading.tsx rename apps/widget-configurator/src/components/{controls => ui/Accordion}/AccordionSection.test.tsx (100%) rename apps/widget-configurator/src/components/{controls => ui/Accordion}/AccordionSection.tsx (100%) rename apps/widget-configurator/src/components/{controls => ui/HelpTooltipButton}/HelpTooltipButton.tsx (100%) rename apps/widget-configurator/src/components/{controls => ui/controls/BooleanSwitch}/BooleanSwitchControl.test.tsx (100%) rename apps/widget-configurator/src/components/{controls => ui/controls/BooleanSwitch}/BooleanSwitchControl.tsx (100%) rename apps/widget-configurator/src/components/{controls => ui/controls/CurrencyInput}/CurrencyInputControl.tsx (100%) rename apps/widget-configurator/src/components/{controls => ui/controls/Select}/CurrentTradeTypeControl.tsx (94%) rename apps/widget-configurator/src/components/{controls => ui/controls/Select}/LocaleControl.test.tsx (100%) rename apps/widget-configurator/src/components/{controls => ui/controls/Select}/LocaleControl.tsx (100%) rename apps/widget-configurator/src/components/{controls => ui/controls/Select}/ModeControl.test.tsx (100%) rename apps/widget-configurator/src/components/{controls => ui/controls/Select}/ModeControl.tsx (96%) rename apps/widget-configurator/src/components/{controls => ui/controls/Select}/NetworkControl.tsx (100%) rename apps/widget-configurator/src/components/{controls => ui/controls/Select}/TradeModesControl.tsx (100%) rename apps/widget-configurator/src/components/{controls => ui/controls/Select}/WidgetHooksControl.tsx (96%) create mode 100644 apps/widget-configurator/src/components/ui/controls/base/base-controls.types.ts diff --git a/apps/widget-configurator/src/components/controls/SettingHeading.tsx b/apps/widget-configurator/src/components/controls/SettingHeading.tsx deleted file mode 100644 index c436a9fdc3b..00000000000 --- a/apps/widget-configurator/src/components/controls/SettingHeading.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { ReactNode } from 'react' - -import Box from '@mui/material/Box' -import Typography from '@mui/material/Typography' - -import { HelpTooltipButton } from './HelpTooltipButton' - -interface SettingHeadingProps { - title: string - tooltip: string -} - -export function SettingHeading({ title, tooltip }: SettingHeadingProps): ReactNode { - return ( - - {title} - - - ) -} diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index 54d25c8c2f8..4eb8a79e59f 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -16,30 +16,30 @@ import { SidebarHeader } from './header/sidebar-header.component' import { getDrawerSx } from './sidebar.styles' import { DEFAULT_STATE, DEFAULT_TOKEN_LISTS, IS_IFRAME, TRADE_MODES } from '../../configurator.constants' +import { ConfiguratorState, TokenListItem, WidgetMode } from '../../configurator.types' 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 { ColorModeContext } from '../../theme/ColorModeContext' -import { ConfiguratorState, TokenListItem, WidgetMode } from '../../configurator.types' -import { AccordionSection } from '../controls/AccordionSection' import { AppearanceStyleControls } from '../controls/AppearanceStyleControls' -import { BooleanSwitchControl } from '../controls/BooleanSwitchControl' -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 { AccordionSection } from '../ui/Accordion/AccordionSection' +import { BooleanSwitchControl } from '../ui/controls/BooleanSwitch/BooleanSwitchControl' +import { CurrencyInputControl } from '../ui/controls/CurrencyInput/CurrencyInputControl' +import { CurrentTradeTypeControl } from '../ui/controls/Select/CurrentTradeTypeControl' +import { LocaleControl } from '../ui/controls/Select/LocaleControl' +import { ModeControl } from '../ui/controls/Select/ModeControl' +import { NetworkControl, NetworkOption, NetworkOptions } from '../ui/controls/Select/NetworkControl' +import { TradeModesControl } from '../ui/controls/Select/TradeModesControl' +import { WidgetHooksControl } from '../ui/controls/Select/WidgetHooksControl' import type { Theme } from '@mui/material/styles' import type * as CSS from 'csstype' @@ -316,6 +316,18 @@ export function Sidebar({ useSyncWidgetNetwork(chainId, setNetworkControlState, standaloneMode) + /* + + TODO: + + - Classify state props into categories in type definition file. + - Update AccordionSection so that we just pass title, currentTitle and onChange, and handle that with a single state variable and a single handler function. + - Create reusable TextInput, NumberInput and SelectInput components. + - Add name to all fields. + - Move fields to individual panels. Pass one prop per value and one single callback that takes a ChangeEvent or name + value. + + */ + return ( getDrawerSx(theme, isResizing)} variant="persistent" anchor="left" open={isOpen}> diff --git a/apps/widget-configurator/src/components/controls/AccordionSection.test.tsx b/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.test.tsx similarity index 100% rename from apps/widget-configurator/src/components/controls/AccordionSection.test.tsx rename to apps/widget-configurator/src/components/ui/Accordion/AccordionSection.test.tsx diff --git a/apps/widget-configurator/src/components/controls/AccordionSection.tsx b/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.tsx similarity index 100% rename from apps/widget-configurator/src/components/controls/AccordionSection.tsx rename to apps/widget-configurator/src/components/ui/Accordion/AccordionSection.tsx diff --git a/apps/widget-configurator/src/components/controls/HelpTooltipButton.tsx b/apps/widget-configurator/src/components/ui/HelpTooltipButton/HelpTooltipButton.tsx similarity index 100% rename from apps/widget-configurator/src/components/controls/HelpTooltipButton.tsx rename to apps/widget-configurator/src/components/ui/HelpTooltipButton/HelpTooltipButton.tsx diff --git a/apps/widget-configurator/src/components/controls/BooleanSwitchControl.test.tsx b/apps/widget-configurator/src/components/ui/controls/BooleanSwitch/BooleanSwitchControl.test.tsx similarity index 100% rename from apps/widget-configurator/src/components/controls/BooleanSwitchControl.test.tsx rename to apps/widget-configurator/src/components/ui/controls/BooleanSwitch/BooleanSwitchControl.test.tsx diff --git a/apps/widget-configurator/src/components/controls/BooleanSwitchControl.tsx b/apps/widget-configurator/src/components/ui/controls/BooleanSwitch/BooleanSwitchControl.tsx similarity index 100% rename from apps/widget-configurator/src/components/controls/BooleanSwitchControl.tsx rename to apps/widget-configurator/src/components/ui/controls/BooleanSwitch/BooleanSwitchControl.tsx diff --git a/apps/widget-configurator/src/components/controls/CurrencyInputControl.tsx b/apps/widget-configurator/src/components/ui/controls/CurrencyInput/CurrencyInputControl.tsx similarity index 100% rename from apps/widget-configurator/src/components/controls/CurrencyInputControl.tsx rename to apps/widget-configurator/src/components/ui/controls/CurrencyInput/CurrencyInputControl.tsx diff --git a/apps/widget-configurator/src/components/controls/CurrentTradeTypeControl.tsx b/apps/widget-configurator/src/components/ui/controls/Select/CurrentTradeTypeControl.tsx similarity index 94% rename from apps/widget-configurator/src/components/controls/CurrentTradeTypeControl.tsx rename to apps/widget-configurator/src/components/ui/controls/Select/CurrentTradeTypeControl.tsx index 9974e06cbaf..0fffbdd08fb 100644 --- a/apps/widget-configurator/src/components/controls/CurrentTradeTypeControl.tsx +++ b/apps/widget-configurator/src/components/ui/controls/Select/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 '../../configurator.constants' +import { TRADE_MODES } from '../../../../configurator.constants' const LABEL = 'Current trade type' diff --git a/apps/widget-configurator/src/components/controls/LocaleControl.test.tsx b/apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.test.tsx similarity index 100% rename from apps/widget-configurator/src/components/controls/LocaleControl.test.tsx rename to apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.test.tsx diff --git a/apps/widget-configurator/src/components/controls/LocaleControl.tsx b/apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.tsx similarity index 100% rename from apps/widget-configurator/src/components/controls/LocaleControl.tsx rename to apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.tsx diff --git a/apps/widget-configurator/src/components/controls/ModeControl.test.tsx b/apps/widget-configurator/src/components/ui/controls/Select/ModeControl.test.tsx similarity index 100% rename from apps/widget-configurator/src/components/controls/ModeControl.test.tsx rename to apps/widget-configurator/src/components/ui/controls/Select/ModeControl.test.tsx diff --git a/apps/widget-configurator/src/components/controls/ModeControl.tsx b/apps/widget-configurator/src/components/ui/controls/Select/ModeControl.tsx similarity index 96% rename from apps/widget-configurator/src/components/controls/ModeControl.tsx rename to apps/widget-configurator/src/components/ui/controls/Select/ModeControl.tsx index 7c4320ef3bc..9d912470ee4 100644 --- a/apps/widget-configurator/src/components/controls/ModeControl.tsx +++ b/apps/widget-configurator/src/components/ui/controls/Select/ModeControl.tsx @@ -10,7 +10,7 @@ import RadioGroup from '@mui/material/RadioGroup' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' -import { HelpTooltipButton } from './HelpTooltipButton' +import { HelpTooltipButton } from '../../HelpTooltipButton/HelpTooltipButton' type WidgetMode = 'dapp' | 'standalone' diff --git a/apps/widget-configurator/src/components/controls/NetworkControl.tsx b/apps/widget-configurator/src/components/ui/controls/Select/NetworkControl.tsx similarity index 100% rename from apps/widget-configurator/src/components/controls/NetworkControl.tsx rename to apps/widget-configurator/src/components/ui/controls/Select/NetworkControl.tsx diff --git a/apps/widget-configurator/src/components/controls/TradeModesControl.tsx b/apps/widget-configurator/src/components/ui/controls/Select/TradeModesControl.tsx similarity index 100% rename from apps/widget-configurator/src/components/controls/TradeModesControl.tsx rename to apps/widget-configurator/src/components/ui/controls/Select/TradeModesControl.tsx diff --git a/apps/widget-configurator/src/components/controls/WidgetHooksControl.tsx b/apps/widget-configurator/src/components/ui/controls/Select/WidgetHooksControl.tsx similarity index 96% rename from apps/widget-configurator/src/components/controls/WidgetHooksControl.tsx rename to apps/widget-configurator/src/components/ui/controls/Select/WidgetHooksControl.tsx index e1c9389478c..4c62aaba331 100644 --- a/apps/widget-configurator/src/components/controls/WidgetHooksControl.tsx +++ b/apps/widget-configurator/src/components/ui/controls/Select/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 '../../configurator.constants' +import { WIDGET_HOOKS } from '../../../../configurator.constants' const LABEL = 'Widget hooks' const EMPTY_VALUE_LABEL = 'No hooks selected' diff --git a/apps/widget-configurator/src/components/ui/controls/base/base-controls.types.ts b/apps/widget-configurator/src/components/ui/controls/base/base-controls.types.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/widget-configurator/src/hooks/useSyncWidgetNetwork.ts b/apps/widget-configurator/src/hooks/useSyncWidgetNetwork.ts index 12a742a6184..0420a0954c9 100644 --- a/apps/widget-configurator/src/hooks/useSyncWidgetNetwork.ts +++ b/apps/widget-configurator/src/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 '../components/controls/NetworkControl' +import { getNetworkOption, NetworkOption } from '../components/ui/controls/Select/NetworkControl' export function useSyncWidgetNetwork( chainId: SupportedChainId, From 99d6046a72ee7678ebf410916844bff18b4c86e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Thu, 9 Apr 2026 19:16:21 +0200 Subject: [PATCH 014/110] chore: sort out configurator params in various files for improved redability --- .../components/sidebar/sidebar.component.tsx | 141 +++++++++++++----- .../src/configurator.types.ts | 69 ++++++--- .../src/hooks/useWidgetParamsAndSettings.ts | 67 ++++++--- 3 files changed, 203 insertions(+), 74 deletions(-) diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index 4eb8a79e59f..87ba81d3ea2 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -223,37 +223,53 @@ export function Sidebar({ // Because useSyncWidgetNetwork() will send a request to change the network const effectiveChainId = IS_IFRAME ? undefined : !isConnected || !walletChainId ? chainId : walletChainId + // TODO: This probably needs a field per chain in the UI: + const partnerFeeRecipient = DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK[chainId] + const configuratorState: ConfiguratorState = useMemo( () => ({ - deadline, - swapDeadline, - limitDeadline, - advancedDeadline, - chainId: effectiveChainId, + // Basics: + + // widgetMode: WidgetMode + standaloneMode, locale: locale || undefined, - theme: mode, - showIframeOutline, - iframeStyle: iframeStyleJson.mergedValue, - appWrapperStyle: appWrapperStyleJson.mergedValue, - bodyWrapperStyle: bodyWrapperStyleJson.mergedValue, - cardStyle: cardStyleJson.mergedValue, - currentTradeType, + + // Trade Setup: + enabledTradeTypes, - enabledWidgetHooks, + currentTradeType, + chainId: effectiveChainId, + disableCrossChainSwap, + // slippage, // TODO: Defined but no form. + + // Tokens: + sellToken, sellTokenAmount, buyToken, buyTokenAmount, tokenListUrls, + customTokens, + + // Theme Colors: + + theme: mode, customColors: colorPalette, defaultColors: defaultPalette, - partnerFeeBps, - partnerFeeRecipient: DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK[chainId], - standaloneMode, + + // Layout: + autoResizeEnabled, + showIframeOutline, + iframeStyle: iframeStyleJson.mergedValue, + appWrapperStyle: appWrapperStyleJson.mergedValue, + bodyWrapperStyle: bodyWrapperStyleJson.mergedValue, + cardStyle: cardStyleJson.mergedValue, + + // Behavior: + disableToastMessages: toastManager.disableToastMessages, disableProgressBar, - disableCrossChainSwap, disableTokenImport, hideRecentTokens, hideFavoriteTokens, @@ -261,41 +277,73 @@ export function Sidebar({ hideOrdersTable, disableTradeWhenPriceImpactIsUnknown, disableTradeWhenPriceImpactIsHigherThan, + + // Deadlines: + + deadline, + swapDeadline, + limitDeadline, + advancedDeadline, + + // Integrations: + + partnerFeeBps, + partnerFeeRecipient, + + // Customization: + customImages, customSounds, - customTokens, + + // Advanced: + + enabledWidgetHooks, + // widgetAppBaseUrl: string; // TODO: Not used for whatever reason. rawParams: rawParamsJson.mergedValue, }), [ - deadline, - swapDeadline, - limitDeadline, - advancedDeadline, - effectiveChainId, - chainId, + // Basics: + + // widgetMode: WidgetMode + standaloneMode, locale, - mode, - showIframeOutline, - iframeStyleJson.mergedValue, - appWrapperStyleJson.mergedValue, - bodyWrapperStyleJson.mergedValue, - cardStyleJson.mergedValue, - currentTradeType, + + // Trade Setup: + enabledTradeTypes, - enabledWidgetHooks, + currentTradeType, + effectiveChainId, + disableCrossChainSwap, + // slippage, // TODO: Defined but not in form. + + // Tokens: + sellToken, sellTokenAmount, buyToken, buyTokenAmount, tokenListUrls, + customTokens, + + // Theme Colors: + + mode, colorPalette, defaultPalette, - partnerFeeBps, - standaloneMode, + + // Layout: + autoResizeEnabled, + showIframeOutline, + iframeStyleJson.mergedValue, + appWrapperStyleJson.mergedValue, + bodyWrapperStyleJson.mergedValue, + cardStyleJson.mergedValue, + + // Behavior: + toastManager.disableToastMessages, disableProgressBar, - disableCrossChainSwap, disableTokenImport, hideRecentTokens, hideFavoriteTokens, @@ -303,9 +351,28 @@ export function Sidebar({ hideOrdersTable, disableTradeWhenPriceImpactIsUnknown, disableTradeWhenPriceImpactIsHigherThan, + + // Deadlines: + + deadline, + swapDeadline, + limitDeadline, + advancedDeadline, + + // Integrations: + + partnerFeeBps, + partnerFeeRecipient, + + // Customization: + customImages, customSounds, - customTokens, + + // Advanced: + + enabledWidgetHooks, + // widgetAppBaseUrl: string; // TODO: Not used for whatever reason. rawParamsJson.mergedValue, ], ) @@ -339,7 +406,7 @@ export function Sidebar({ diff --git a/apps/widget-configurator/src/configurator.types.ts b/apps/widget-configurator/src/configurator.types.ts index 3763fbfd91f..b7cd0f59ca6 100644 --- a/apps/widget-configurator/src/configurator.types.ts +++ b/apps/widget-configurator/src/configurator.types.ts @@ -26,35 +26,48 @@ export interface TokenListItem { export type WidgetMode = 'dapp' | 'standalone' export interface ConfiguratorState { - chainId?: SupportedChainId + // Basics: + + // widgetMode: WidgetMode + standaloneMode: boolean // TODO: Replace with widgetMode. locale?: string - theme: PaletteMode - showIframeOutline: boolean - iframeStyle: CSS.Properties - appWrapperStyle: CSS.Properties - bodyWrapperStyle: CSS.Properties - cardStyle: CSS.Properties - currentTradeType: TradeType + + // Trade Setup: + enabledTradeTypes: TradeType[] - enabledWidgetHooks: WidgetHookEvents[] + currentTradeType: TradeType + chainId?: SupportedChainId + disableCrossChainSwap: boolean + slippage?: SlippageConfig // TODO: Not used for whatever reason. + + // Tokens: + sellToken: string sellTokenAmount: number | undefined buyToken: string buyTokenAmount: number | undefined - deadline: number | undefined - swapDeadline: number | undefined - limitDeadline: number | undefined - advancedDeadline: number | undefined tokenListUrls: TokenListItem[] + customTokens: CowSwapWidgetParams['customTokens'] + + // Theme Colors: + + theme: PaletteMode customColors: ColorPalette defaultColors: ColorPalette - partnerFeeBps: number - partnerFeeRecipient: PartnerFee['recipient'] - standaloneMode: boolean + + // Layout: + autoResizeEnabled: boolean + showIframeOutline: boolean + iframeStyle: CSS.Properties + appWrapperStyle: CSS.Properties + bodyWrapperStyle: CSS.Properties + cardStyle: CSS.Properties + + // Behavior: + disableToastMessages: boolean disableProgressBar: boolean - disableCrossChainSwap: boolean disableTokenImport: boolean hideRecentTokens: boolean hideFavoriteTokens: boolean @@ -62,9 +75,27 @@ export interface ConfiguratorState { hideOrdersTable: boolean | undefined disableTradeWhenPriceImpactIsUnknown: boolean disableTradeWhenPriceImpactIsHigherThan: number | undefined - slippage?: SlippageConfig + + // Deadlines: + + deadline: number | undefined + swapDeadline: number | undefined + limitDeadline: number | undefined + advancedDeadline: number | undefined + + // Integrations: + + partnerFeeBps: number + partnerFeeRecipient: PartnerFee['recipient'] // TODO: Not used for whatever reason. + + // Customization: + customImages: CowSwapWidgetParams['images'] customSounds: CowSwapWidgetParams['sounds'] - customTokens: CowSwapWidgetParams['customTokens'] + + // Advanced: + + enabledWidgetHooks: WidgetHookEvents[] + // widgetAppBaseUrl: string; // TODO: Not used for whatever reason. rawParams: Partial } diff --git a/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts index 21f709a0f67..cc131e1c0e7 100644 --- a/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts @@ -154,33 +154,47 @@ function buildWidgetParams(configuratorState: ConfiguratorState | null): CowSwap if (!configuratorState) return null const { - chainId, + // Basics: + + // widgetMode: WidgetMode + standaloneMode, locale, - theme, - iframeStyle, - appWrapperStyle, - bodyWrapperStyle, - cardStyle, - currentTradeType, + + // Trade Setup: + enabledTradeTypes, + currentTradeType, + chainId, + disableCrossChainSwap, + slippage, // TODO: Defined but not in form. + + // Tokens: + sellToken, sellTokenAmount, buyToken, buyTokenAmount, - deadline, - swapDeadline, - limitDeadline, - advancedDeadline, tokenListUrls, + customTokens, + + // Theme Colors: + + theme, customColors, defaultColors, - partnerFeeBps, - partnerFeeRecipient, - standaloneMode, + + // Layout: + autoResizeEnabled, + iframeStyle, + appWrapperStyle, + bodyWrapperStyle, + cardStyle, + + // Behavior: + disableToastMessages, disableProgressBar, - disableCrossChainSwap, disableTokenImport, hideRecentTokens, hideFavoriteTokens, @@ -188,11 +202,28 @@ function buildWidgetParams(configuratorState: ConfiguratorState | null): CowSwap hideOrdersTable, disableTradeWhenPriceImpactIsUnknown, disableTradeWhenPriceImpactIsHigherThan, - slippage, - enabledWidgetHooks, + + // Deadlines: + + deadline, + swapDeadline, + limitDeadline, + advancedDeadline, + + // Integrations: + + partnerFeeBps, + partnerFeeRecipient, + + // Customization: + customImages, customSounds, - customTokens, + + // Advanced: + + enabledWidgetHooks, + // widgetAppBaseUrl: string; // TODO: Not used for whatever reason. rawParams, } = configuratorState From 0e761e2b69c4078c2742c35ff327927b51cb7838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Thu, 9 Apr 2026 19:59:40 +0200 Subject: [PATCH 015/110] fix: pass baseUrl to widget --- .../components/sidebar/sidebar.component.tsx | 24 ++++---- .../src/configurator.types.ts | 2 +- .../src/hooks/useWidgetParamsAndSettings.ts | 61 ++++++++++++++----- 3 files changed, 59 insertions(+), 28 deletions(-) diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index 87ba81d3ea2..70aaf47b8b3 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -197,7 +197,7 @@ export function Sidebar({ const customImagesState = useState({}) const customSoundsState = useState({}) - const [widgetAppBaseUrl, setWidgetAppBaseUrl] = useState('') + const [baseUrl, setBaseUrl] = useState('') const [rawParamsJson, setRawParamsJson] = useJsonState>(EMPTY_JSON_STATE) const handleRawParamsJsonChange = (e: ChangeEvent): void => { @@ -240,7 +240,7 @@ export function Sidebar({ currentTradeType, chainId: effectiveChainId, disableCrossChainSwap, - // slippage, // TODO: Defined but no form. + // slippage, // TODO: Defined but not in the form. // Tokens: @@ -297,8 +297,8 @@ export function Sidebar({ // Advanced: + baseUrl, enabledWidgetHooks, - // widgetAppBaseUrl: string; // TODO: Not used for whatever reason. rawParams: rawParamsJson.mergedValue, }), [ @@ -371,8 +371,8 @@ export function Sidebar({ // Advanced: + baseUrl, enabledWidgetHooks, - // widgetAppBaseUrl: string; // TODO: Not used for whatever reason. rawParamsJson.mergedValue, ], ) @@ -387,11 +387,11 @@ export function Sidebar({ TODO: - - Classify state props into categories in type definition file. - - Update AccordionSection so that we just pass title, currentTitle and onChange, and handle that with a single state variable and a single handler function. - - Create reusable TextInput, NumberInput and SelectInput components. - - Add name to all fields. - - Move fields to individual panels. Pass one prop per value and one single callback that takes a ChangeEvent or name + value. + - [x] Classify state props into categories in type definition file. + - [ ] Update AccordionSection so that we just pass title, currentTitle and onChange, and handle that with a single state variable and a single handler function. + - [ ] Create reusable TextInput, NumberInput and SelectInput components. + - [ ] Add name to all fields. + - [ ] Move fields to individual panels. Pass one prop per value and one single callback that takes a ChangeEvent or name + value. */ @@ -566,10 +566,10 @@ export function Sidebar({ setWidgetAppBaseUrl(e.target.value)} + value={baseUrl} + onChange={(e) => setBaseUrl(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/configurator.types.ts b/apps/widget-configurator/src/configurator.types.ts index b7cd0f59ca6..722af4000c8 100644 --- a/apps/widget-configurator/src/configurator.types.ts +++ b/apps/widget-configurator/src/configurator.types.ts @@ -95,7 +95,7 @@ export interface ConfiguratorState { // Advanced: + baseUrl: string enabledWidgetHooks: WidgetHookEvents[] - // widgetAppBaseUrl: string; // TODO: Not used for whatever reason. rawParams: Partial } diff --git a/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts index cc131e1c0e7..7d5b984f47f 100644 --- a/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts @@ -222,52 +222,83 @@ function buildWidgetParams(configuratorState: ConfiguratorState | null): CowSwap // Advanced: + baseUrl: rawBaseUrl, enabledWidgetHooks, - // widgetAppBaseUrl: string; // TODO: Not used for whatever reason. rawParams, } = configuratorState + const baseUrl = rawBaseUrl || CONFIGURATOR_DEFAULT_WIDGET_BASE_URL + // 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 { + // Basics: + appCode: 'CoW Widget: Configurator', - chainId, + standaloneMode, locale, - tokenLists: getTokenListsParam(tokenListUrls, 'enabled'), - sellTokenLists: getTokenListsParam(tokenListUrls, 'enabledForSell'), - buyTokenLists: getTokenListsParam(tokenListUrls, 'enabledForBuy'), - baseUrl: CONFIGURATOR_DEFAULT_WIDGET_BASE_URL, + + // Trade Setup: + + enabledTradeTypes, tradeType: currentTradeType, + chainId, + disableCrossChainSwap, + slippage, // TODO: Defined but not in the form. + + // Tokens: + sell: { asset: sellToken, amount: sellTokenAmount ? sellTokenAmount.toString() : undefined }, buy: { asset: buyToken, amount: buyTokenAmount?.toString() }, - forcedOrderDeadline: getForcedOrderDeadline({ deadline, swapDeadline, limitDeadline, advancedDeadline }), - enabledTradeTypes, + sellTokenLists: getTokenListsParam(tokenListUrls, 'enabledForSell'), + buyTokenLists: getTokenListsParam(tokenListUrls, 'enabledForBuy'), + tokenLists: getTokenListsParam(tokenListUrls, 'enabled'), + customTokens, + + // Theme Colors: + theme: getThemeParam(theme, customColors, defaultColors), + + // Layout: + + autoResizeEnabled, iframeStyle, appWrapperStyle, bodyWrapperStyle, cardStyle, - standaloneMode, - autoResizeEnabled, + + // Behavior: + disableToastMessages, disableProgressBar, - disableCrossChainSwap, disableTokenImport, hideRecentTokens, hideFavoriteTokens, - partnerFee: getPartnerFeeParam(partnerFeeBps, partnerFeeRecipient), hideBridgeInfo, hideOrdersTable, - slippage, disableTrade: { whenPriceImpactIsUnknown: disableTradeWhenPriceImpactIsUnknown, whenPriceImpactIsHigherThan: disableTradeWhenPriceImpactIsHigherThan, }, - hooks: getWidgetHooks(enabledWidgetHooks), + + // Deadlines: + + forcedOrderDeadline: getForcedOrderDeadline({ deadline, swapDeadline, limitDeadline, advancedDeadline }), + + // Integrations: + + partnerFee: getPartnerFeeParam(partnerFeeBps, partnerFeeRecipient), + + // Customization: + images: customImages, sounds: customSounds, - customTokens, + + // Advanced: + + baseUrl, + hooks: getWidgetHooks(enabledWidgetHooks), ...rawParams, ...window.cowSwapWidgetParams, } From edcf6204393bb78a2be17a3ff1f017e744a93348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Thu, 9 Apr 2026 21:05:59 +0200 Subject: [PATCH 016/110] feat: add some custom input components to simplify/dry of the overall form JSX --- .../controls/AppearanceStyleControls.tsx | 5 +-- .../components/sidebar/sidebar.component.tsx | 30 +++++-------- .../BaseTextInput/BaseTextInput.component.tsx | 17 +++++++ .../JsonInput/JsonInput.component.tsx | 44 +++++++++++++++++++ .../NumberInput.component.tsx} | 0 .../TextInput/TextInput.component.tsx | 33 ++++++++++++++ .../TextareaInput/TextareaInput.component.tsx | 36 +++++++++++++++ .../src/configurator.types.ts | 2 +- .../src/hooks/useJsonState.ts | 20 +++++++-- .../src/utils/jsonFieldParsing.ts | 3 ++ 10 files changed, 163 insertions(+), 27 deletions(-) create mode 100644 apps/widget-configurator/src/components/ui/controls/BaseTextInput/BaseTextInput.component.tsx create mode 100644 apps/widget-configurator/src/components/ui/controls/JsonInput/JsonInput.component.tsx rename apps/widget-configurator/src/components/ui/controls/{base/base-controls.types.ts => NumberInput/NumberInput.component.tsx} (100%) create mode 100644 apps/widget-configurator/src/components/ui/controls/TextInput/TextInput.component.tsx create mode 100644 apps/widget-configurator/src/components/ui/controls/TextareaInput/TextareaInput.component.tsx create mode 100644 apps/widget-configurator/src/utils/jsonFieldParsing.ts diff --git a/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx b/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx index f586277af17..aa07d2681cb 100644 --- a/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx +++ b/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx @@ -4,6 +4,7 @@ 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 { jsonHelperText } from '../../utils/jsonFieldParsing' import type { JsonState, OnJsonStateChange } from '../../hooks/useJsonState' import type * as CSS from 'csstype' @@ -19,10 +20,6 @@ export interface AppearanceStyleControlsProps { 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, diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index 70aaf47b8b3..cfb7b4a3fbc 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -10,6 +10,8 @@ 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 { jsonHelperText } from '../../utils/jsonFieldParsing' +import { JsonInput } from '../ui/controls/JsonInput/JsonInput.component' import { SidebarFooter } from './footer/sidebar-footer.component' import { SidebarHeader } from './header/sidebar-header.component' @@ -43,6 +45,7 @@ import { WidgetHooksControl } from '../ui/controls/Select/WidgetHooksControl' import type { Theme } from '@mui/material/styles' import type * as CSS from 'csstype' +import { TextInput } from 'apps/widget-configurator/src/components/ui/controls/TextInput/TextInput.component' export interface SidebarProps { title: string @@ -197,13 +200,9 @@ export function Sidebar({ const customImagesState = useState({}) const customSoundsState = useState({}) - const [baseUrl, setBaseUrl] = useState('') + const [baseUrl, setBaseUrl] = useState(null) const [rawParamsJson, setRawParamsJson] = useJsonState>(EMPTY_JSON_STATE) - const handleRawParamsJsonChange = (e: ChangeEvent): void => { - setRawParamsJson(null, e.target.value) - } - // Advanced Section: const widgetHooksState = useState([]) @@ -562,26 +561,21 @@ export function Sidebar({ expanded={expandedSection === 'Advanced'} onChange={toggleSection('Advanced')} > - - setBaseUrl(e.target.value)} - size="medium" + onChange={(_, value) => setBaseUrl(value)} placeholder={CONFIGURATOR_DEFAULT_WIDGET_BASE_URL} helperText={`Optional. Sets baseUrl (overrides Raw JSON). Default preview URL: ${CONFIGURATOR_DEFAULT_WIDGET_BASE_URL}`} /> - + setRawParamsJson(null, value)} + helperText={jsonHelperText(rawParamsJson.error)} /> diff --git a/apps/widget-configurator/src/components/ui/controls/BaseTextInput/BaseTextInput.component.tsx b/apps/widget-configurator/src/components/ui/controls/BaseTextInput/BaseTextInput.component.tsx new file mode 100644 index 00000000000..06634a43e0a --- /dev/null +++ b/apps/widget-configurator/src/components/ui/controls/BaseTextInput/BaseTextInput.component.tsx @@ -0,0 +1,17 @@ +import { TextField, TextFieldProps } from "@mui/material"; +import { ReactNode } from "react"; + +export interface BaseTextInputProps extends Omit { + name: string; + label: string; +} + +export function BaseTextInput(props: BaseTextInputProps): ReactNode { + return ( + + ) +} diff --git a/apps/widget-configurator/src/components/ui/controls/JsonInput/JsonInput.component.tsx b/apps/widget-configurator/src/components/ui/controls/JsonInput/JsonInput.component.tsx new file mode 100644 index 00000000000..ba18eb048a4 --- /dev/null +++ b/apps/widget-configurator/src/components/ui/controls/JsonInput/JsonInput.component.tsx @@ -0,0 +1,44 @@ +import { BaseTextInput, BaseTextInputProps } from "../BaseTextInput/BaseTextInput.component"; +import { ReactNode } from "react"; + +export interface JsonInputProps extends Omit { + onChange: (name: string, value: string | null) => void; +} + +export function JsonInput({ + name, + value, + onChange, + onBlur, + ...props +}: JsonInputProps): ReactNode { + const handleChange = onChange ? (e: React.ChangeEvent): void => { + onChange(name, e.target.value || null); + } : undefined; + + const handleBlur = (e: React.FocusEvent): void => { + let formattedValue = e.target.value; + + try { + formattedValue = JSON.stringify(JSON.parse(e.target.value), null, 2); + } catch { + // Do nothing + } + + onChange(name, formattedValue || null); + if (onBlur) onBlur(e); + }; + + return ( + + ) +} diff --git a/apps/widget-configurator/src/components/ui/controls/base/base-controls.types.ts b/apps/widget-configurator/src/components/ui/controls/NumberInput/NumberInput.component.tsx similarity index 100% rename from apps/widget-configurator/src/components/ui/controls/base/base-controls.types.ts rename to apps/widget-configurator/src/components/ui/controls/NumberInput/NumberInput.component.tsx diff --git a/apps/widget-configurator/src/components/ui/controls/TextInput/TextInput.component.tsx b/apps/widget-configurator/src/components/ui/controls/TextInput/TextInput.component.tsx new file mode 100644 index 00000000000..a7001031fcc --- /dev/null +++ b/apps/widget-configurator/src/components/ui/controls/TextInput/TextInput.component.tsx @@ -0,0 +1,33 @@ +import { BaseTextInput, BaseTextInputProps } from "../BaseTextInput/BaseTextInput.component"; +import { ReactNode } from "react"; + +export interface TextInputProps extends Omit { + onChange: (name: string, value: string | null) => void; +} + +export function TextInput({ + name, + value, + onChange, + onBlur, + ...props +}: TextInputProps): ReactNode { + const handleChange = onChange ? (e: React.ChangeEvent): void => { + onChange(name, e.target.value || null); + } : undefined; + + const handleBlur = (e: React.FocusEvent): void => { + onChange(name, e.target.value.trim() || null); + if (onBlur) onBlur(e); + } + + return ( + + ) +} diff --git a/apps/widget-configurator/src/components/ui/controls/TextareaInput/TextareaInput.component.tsx b/apps/widget-configurator/src/components/ui/controls/TextareaInput/TextareaInput.component.tsx new file mode 100644 index 00000000000..14368d9db0e --- /dev/null +++ b/apps/widget-configurator/src/components/ui/controls/TextareaInput/TextareaInput.component.tsx @@ -0,0 +1,36 @@ +import { BaseTextInput, BaseTextInputProps } from "../BaseTextInput/BaseTextInput.component"; +import { ReactNode } from "react"; + +export interface TextareaInputProps extends Omit { + onChange: (name: string, value: string | null) => void; +} + +export function TextareaInput({ + name, + value, + onChange, + onBlur, + ...props +}: TextareaInputProps): ReactNode { + const handleChange = onChange ? (e: React.ChangeEvent): void => { + onChange(name, e.target.value || null); + } : undefined; + + const handleBlur = (e: React.FocusEvent): void => { + onChange(name, e.target.value.trim() || null); + if (onBlur) onBlur(e); + } + + return ( + + ) +} diff --git a/apps/widget-configurator/src/configurator.types.ts b/apps/widget-configurator/src/configurator.types.ts index 722af4000c8..a4bff69a376 100644 --- a/apps/widget-configurator/src/configurator.types.ts +++ b/apps/widget-configurator/src/configurator.types.ts @@ -95,7 +95,7 @@ export interface ConfiguratorState { // Advanced: - baseUrl: string + baseUrl: string | null enabledWidgetHooks: WidgetHookEvents[] rawParams: Partial } diff --git a/apps/widget-configurator/src/hooks/useJsonState.ts b/apps/widget-configurator/src/hooks/useJsonState.ts index 0d81b0cffc4..e4e8790f151 100644 --- a/apps/widget-configurator/src/hooks/useJsonState.ts +++ b/apps/widget-configurator/src/hooks/useJsonState.ts @@ -12,13 +12,13 @@ export interface InitialJsonState { export interface JsonState { fields: T - rawJsonValue: string + rawJsonValue: string | null parsedJsonValue: T mergedValue: T error: boolean } -export type OnJsonStateChange = (name: keyof T | null, value: string) => void +export type OnJsonStateChange = (name: string | null, value: string | null) => void export function useJsonState( initialState: InitialJsonState, @@ -34,8 +34,20 @@ export function useJsonState( }) const onChange = useCallback( - (name: keyof T | null, value: string) => { + (name: string | null, value: string | null) => { if (name === null) { + if (value === null) { + setJsonState({ + fields: initialFields, + rawJsonValue: JSON.stringify(initialJsonValue), + parsedJsonValue: initialJsonValue, + mergedValue: mergeJsonValues(initialFields, initialJsonValue), + error: false, + }) + + return + } + let parsedValue: T = initialJsonValue try { @@ -71,7 +83,7 @@ export function useJsonState( }) } }, - [initialJsonValue], + [initialFields, initialJsonValue], ) return [jsonState, onChange] diff --git a/apps/widget-configurator/src/utils/jsonFieldParsing.ts b/apps/widget-configurator/src/utils/jsonFieldParsing.ts new file mode 100644 index 00000000000..56eec465dd2 --- /dev/null +++ b/apps/widget-configurator/src/utils/jsonFieldParsing.ts @@ -0,0 +1,3 @@ +export function jsonHelperText(hasError: boolean): string { + return hasError ? 'Invalid JSON.' : 'Optional. CamelCase CSS properties as JSON, e.g. {"padding": "12px"}' +} From 0701b33040342070ace0e14a33451490547e2b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Thu, 9 Apr 2026 21:06:23 +0200 Subject: [PATCH 017/110] feat: add some custom input components to simplify/dry of the overall form JSX --- .../src/components/sidebar/sidebar.component.tsx | 2 +- apps/widget-configurator/src/hooks/useJsonState.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index cfb7b4a3fbc..935c1f0c30c 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -45,7 +45,7 @@ import { WidgetHooksControl } from '../ui/controls/Select/WidgetHooksControl' import type { Theme } from '@mui/material/styles' import type * as CSS from 'csstype' -import { TextInput } from 'apps/widget-configurator/src/components/ui/controls/TextInput/TextInput.component' +import { TextInput } from '../ui/controls/TextInput/TextInput.component' export interface SidebarProps { title: string diff --git a/apps/widget-configurator/src/hooks/useJsonState.ts b/apps/widget-configurator/src/hooks/useJsonState.ts index e4e8790f151..34aa8d1f6f0 100644 --- a/apps/widget-configurator/src/hooks/useJsonState.ts +++ b/apps/widget-configurator/src/hooks/useJsonState.ts @@ -18,11 +18,12 @@ export interface JsonState { error: boolean } -export type OnJsonStateChange = (name: string | null, value: string | null) => void +// export type OnJsonStateChange = (name: string | null, value: string | null) => void +export type OnJsonStateChange = (name: string | null, value: string | null) => void export function useJsonState( initialState: InitialJsonState, -): [JsonState, OnJsonStateChange] { +): [JsonState, OnJsonStateChange] { const { fields: initialFields, jsonValue: initialJsonValue } = initialState const [jsonState, setJsonState] = useState>({ From 08910099c624e778e8fa30ff8a62fd7598f71919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Fri, 10 Apr 2026 17:03:05 +0200 Subject: [PATCH 018/110] feat: add some layout presets (WIP) --- .../controls/AppearanceStyleControls.tsx | 127 +++++++++++++++++- .../components/sidebar/sidebar.component.tsx | 28 +--- .../BaseTextInput/BaseTextInput.component.tsx | 22 ++- .../widget-lib/src/applyElementStyles.spec.ts | 0 4 files changed, 146 insertions(+), 31 deletions(-) create mode 100644 libs/widget-lib/src/applyElementStyles.spec.ts diff --git a/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx b/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx index aa07d2681cb..f8165491659 100644 --- a/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx +++ b/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx @@ -1,9 +1,11 @@ import type { ChangeEvent, ReactNode } from 'react' import Box from '@mui/material/Box' +import Button from '@mui/material/Button' import Stack from '@mui/material/Stack' import TextField from '@mui/material/TextField' import Typography from '@mui/material/Typography' + import { jsonHelperText } from '../../utils/jsonFieldParsing' import type { JsonState, OnJsonStateChange } from '../../hooks/useJsonState' @@ -11,13 +13,13 @@ import type * as CSS from 'csstype' export interface AppearanceStyleControlsProps { iframeStyleJson: JsonState - onIframeStyleJson: OnJsonStateChange + onIframeStyleJson: OnJsonStateChange appWrapperStyleJson: JsonState - onAppWrapperStyleJson: OnJsonStateChange + onAppWrapperStyleJson: OnJsonStateChange bodyWrapperStyleJson: JsonState - onBodyWrapperStyleJson: OnJsonStateChange + onBodyWrapperStyleJson: OnJsonStateChange cardStyleJson: JsonState - onCardStyleJson: OnJsonStateChange + onCardStyleJson: OnJsonStateChange } // eslint-disable-next-line max-lines-per-function @@ -59,9 +61,126 @@ export function AppearanceStyleControls({ onCardStyleJson(null, e.target.value) } + const handlePresentNone = (): void => { + onIframeStyleJson(null, JSON.stringify({})) + } + const handlePresentBottomRightPopup = (): void => { + onIframeStyleJson( + null, + JSON.stringify( + { + position: 'fixed', + bottom: '24px', + right: '24px', + boxShadow: '0 0 32px 0 black', + borderRadius: '8px', + width: '420px', + maxHeight: 'calc(100lvh - 48px)', + }, + null, + 2, + ), + ) + + onBodyWrapperStyleJson( + null, + JSON.stringify( + { + padding: '0', + }, + null, + 2, + ), + ) + + onCardStyleJson( + null, + JSON.stringify( + { + borderRadius: '0', + }, + null, + 2, + ), + ) + } + const handlePresentRightSidebar = (): void => { + onIframeStyleJson( + null, + JSON.stringify( + { + position: 'fixed', + top: '0', + bottom: '0', + right: '0', + boxShadow: '0 0 32px 0 black', + borderRadius: '0', + width: '420px', + height: '100dvh', + }, + null, + 2, + ), + ) + + onBodyWrapperStyleJson( + null, + JSON.stringify( + { + padding: '0', + }, + null, + 2, + ), + ) + + onCardStyleJson( + null, + JSON.stringify( + { + borderRadius: '0', + }, + null, + 2, + ), + ) + } + const handlePresentModal = (): void => { + onIframeStyleJson(null, JSON.stringify({})) + } + const handlePresentFullScreen = (): void => { + onIframeStyleJson(null, JSON.stringify({})) + } + const handlePresentFullSizeBlock = (): void => { + onIframeStyleJson(null, JSON.stringify({})) + } + return ( + + Presents + + + + + + + + + Iframe (host) diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index 935c1f0c30c..6f3721543f9 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -4,14 +4,10 @@ import { SupportedLocale, DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK } from '@cow 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 { jsonHelperText } from '../../utils/jsonFieldParsing' -import { JsonInput } from '../ui/controls/JsonInput/JsonInput.component' import { SidebarFooter } from './footer/sidebar-footer.component' import { SidebarHeader } from './header/sidebar-header.component' @@ -25,6 +21,7 @@ import { useSyncWidgetNetwork } from '../../hooks/useSyncWidgetNetwork' import { UseToastsManagerReturn } from '../../hooks/useToastsManager' import { CONFIGURATOR_DEFAULT_WIDGET_BASE_URL } from '../../hooks/useWidgetParamsAndSettings' import { ColorModeContext } from '../../theme/ColorModeContext' +import { jsonHelperText } from '../../utils/jsonFieldParsing' import { AppearanceStyleControls } from '../controls/AppearanceStyleControls' import { CustomImagesControl } from '../controls/CustomImagesControl' import { CustomSoundsControl } from '../controls/CustomSoundsControl' @@ -36,16 +33,17 @@ import { TokenListControl } from '../controls/TokenListControl' import { AccordionSection } from '../ui/Accordion/AccordionSection' import { BooleanSwitchControl } from '../ui/controls/BooleanSwitch/BooleanSwitchControl' import { CurrencyInputControl } from '../ui/controls/CurrencyInput/CurrencyInputControl' +import { JsonInput } from '../ui/controls/JsonInput/JsonInput.component' import { CurrentTradeTypeControl } from '../ui/controls/Select/CurrentTradeTypeControl' import { LocaleControl } from '../ui/controls/Select/LocaleControl' import { ModeControl } from '../ui/controls/Select/ModeControl' import { NetworkControl, NetworkOption, NetworkOptions } from '../ui/controls/Select/NetworkControl' import { TradeModesControl } from '../ui/controls/Select/TradeModesControl' import { WidgetHooksControl } from '../ui/controls/Select/WidgetHooksControl' +import { TextInput } from '../ui/controls/TextInput/TextInput.component' import type { Theme } from '@mui/material/styles' import type * as CSS from 'csstype' -import { TextInput } from '../ui/controls/TextInput/TextInput.component' export interface SidebarProps { title: string @@ -521,22 +519,10 @@ export function Sidebar({ expanded={expandedSection === 'Deadlines'} onChange={toggleSection('Deadlines')} > - - - Global deadline - - - - - - Per-trade deadlines - - - - - - - + + + + { - name: string; - label: string; + name: string + label: string } export function BaseTextInput(props: BaseTextInputProps): ReactNode { return ( + size="medium" + /> ) } diff --git a/libs/widget-lib/src/applyElementStyles.spec.ts b/libs/widget-lib/src/applyElementStyles.spec.ts new file mode 100644 index 00000000000..e69de29bb2d From b08c430f7dec7d2004cf7046368402ac6cdea9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Mon, 13 Apr 2026 18:58:52 +0200 Subject: [PATCH 019/110] feat: add toggle for disablePostTradeTips --- .../src/components/sidebar/sidebar.component.tsx | 9 +++++++++ .../src/components/snippet/snippet.const.ts | 4 +++- apps/widget-configurator/src/configurator.types.ts | 2 +- .../src/hooks/useWidgetParamsAndSettings.ts | 2 ++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index 6f3721543f9..3da0f4c2c6b 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -139,6 +139,9 @@ export function Sidebar({ const [disableProgressBar, setDisableProgressBar] = useState(false) const setShowProgressBar = useCallback((enabled: boolean) => setDisableProgressBar(!enabled), []) + const [disablePostTradeTips, setDisablePostTradeTips] = useState(false) + const setShowPostTradeTips = useCallback((enabled: boolean) => setDisablePostTradeTips(!enabled), []) + const [disableTokenImport, setDisableTokenImport] = useState(false) const setAllowTokenImport = useCallback((enabled: boolean) => setDisableTokenImport(!enabled), []) @@ -390,6 +393,7 @@ export function Sidebar({ - [ ] Add name to all fields. - [ ] Move fields to individual panels. Pass one prop per value and one single callback that takes a ChangeEvent or name + value. + */ return ( @@ -479,6 +483,11 @@ export function Sidebar({ onChange={toastManager.setToastMessagesInDappMode} /> + = { +export const COMMENTS_BY_PARAM_NAME: Partial> = { appCode: 'Name of your app (max 50 characters)', width: 'Outer iFrame width (use 100% to fill the available container width)', chainId: '1 (Mainnet), 100 (Gnosis), 11155111 (Sepolia)', diff --git a/apps/widget-configurator/src/configurator.types.ts b/apps/widget-configurator/src/configurator.types.ts index ccf18b28822..c281fa4e7d8 100644 --- a/apps/widget-configurator/src/configurator.types.ts +++ b/apps/widget-configurator/src/configurator.types.ts @@ -68,7 +68,7 @@ export interface ConfiguratorState { disableToastMessages: boolean disableProgressBar: boolean - disablePostTradeTips: boolean // TODO: Add field + disablePostTradeTips: boolean disableTokenImport: boolean hideRecentTokens: boolean hideFavoriteTokens: boolean diff --git a/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts index 7d5b984f47f..ede500376ec 100644 --- a/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts @@ -195,6 +195,7 @@ function buildWidgetParams(configuratorState: ConfiguratorState | null): CowSwap disableToastMessages, disableProgressBar, + disablePostTradeTips, disableTokenImport, hideRecentTokens, hideFavoriteTokens, @@ -272,6 +273,7 @@ function buildWidgetParams(configuratorState: ConfiguratorState | null): CowSwap disableToastMessages, disableProgressBar, + disablePostTradeTips, disableTokenImport, hideRecentTokens, hideFavoriteTokens, From a6f31b22226853823e5070a05532a8b91e1dd768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Mon, 13 Apr 2026 18:59:37 +0200 Subject: [PATCH 020/110] feat: add toggle for disablePostTradeTips --- .../src/components/sidebar/sidebar.component.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index 3da0f4c2c6b..aa099c14ed4 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -270,6 +270,7 @@ export function Sidebar({ disableToastMessages: toastManager.disableToastMessages, disableProgressBar, + disablePostTradeTips, disableTokenImport, hideRecentTokens, hideFavoriteTokens, @@ -344,6 +345,7 @@ export function Sidebar({ toastManager.disableToastMessages, disableProgressBar, + disablePostTradeTips, disableTokenImport, hideRecentTokens, hideFavoriteTokens, From 196e4799c87cb394c001cc8ef1a818e30a8612cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Mon, 13 Apr 2026 19:07:58 +0200 Subject: [PATCH 021/110] chore: sort out COMMENTS_BY_PARAM_NAME --- .../components/sidebar/sidebar.component.tsx | 8 +++- .../src/components/snippet/snippet.const.ts | 40 +++++++++++++++---- .../snippet/utils/formatParameters.ts | 5 ++- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index aa099c14ed4..7c11145a014 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -390,11 +390,17 @@ export function Sidebar({ TODO: - [x] Classify state props into categories in type definition file. + - [ ] Add field for appCode. - [ ] Update AccordionSection so that we just pass title, currentTitle and onChange, and handle that with a single state variable and a single handler function. - [ ] Create reusable TextInput, NumberInput and SelectInput components. - [ ] Add name to all fields. - [ ] Move fields to individual panels. Pass one prop per value and one single callback that takes a ChangeEvent or name + value. - + - [ ] Add update/reload widget button if needed. + - [ ] Add env indicator. + - [ ] Automatically set baseUrl based on widget configurator env. + - [ ] Allow wider sidebar to use it as mobile mode. + - [ ] Does the widget configurator work on mobile? + - [ ] Bug: when in dApp mode, reload the page with the wallet connected. You are connected outside, not within the widget. */ diff --git a/apps/widget-configurator/src/components/snippet/snippet.const.ts b/apps/widget-configurator/src/components/snippet/snippet.const.ts index 424f00a1aa1..086ab4d7e7e 100644 --- a/apps/widget-configurator/src/components/snippet/snippet.const.ts +++ b/apps/widget-configurator/src/components/snippet/snippet.const.ts @@ -6,30 +6,54 @@ export const PROVIDER_PARAM_COMMENT = 'Ethereum EIP-1193 provider. For a quick test, you can pass `window.ethereum`, but consider using something like https://web3modal.com' export const COMMENTS_BY_PARAM_NAME: Partial> = { + // Basics: + appCode: 'Name of your app (max 50 characters)', - width: 'Outer iFrame width (use 100% to fill the available container width)', + + // Trade Setup: + + enabledTradeTypes: 'swap, limit, advanced, yield', chainId: '1 (Mainnet), 100 (Gnosis), 11155111 (Sepolia)', - tokenLists: 'All default enabled token lists. Also see https://tokenlists.org', + tradeType: 'swap, limit or advanced', + + // Tokens: + + sell: 'Sell token. Optionally add amount for sell orders', + buy: 'Buy token. Optionally add amount for buy orders', sellTokenLists: 'Token lists available only in the sell selector', buyTokenLists: 'Token lists available only in the buy selector', + tokenLists: 'All default enabled token lists. Also see https://tokenlists.org', + + // Theme Colors: + theme: 'light/dark or provide your own color palette', + + // Layout: + 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', - enabledTradeTypes: 'swap, limit, advanced, yield', - partnerFee: 'Partner fee, in Basis Points (BPS) and a receiver address', + + // Behavior: + + disablePostTradeTips: 'Hide CoW Swap educational tips shown after a completed trade when there is no surplus card', hideRecentTokens: 'Hide the Recent section in the token selector', hideFavoriteTokens: 'Hide the Favorites section in the token selector', + + // Integrations: + + partnerFee: 'Partner fee, in Basis Points (BPS) and a receiver address', + + // Advanced: + baseUrl: 'URL of the CoW Swap app inside the widget iframe (defaults to production if omitted in embed code)', - disablePostTradeTips: 'Hide CoW Swap educational tips shown after a completed trade when there is no surplus card', } export const COMMENTS_BY_PARAM_NAME_TYPESCRIPT: Record = { + // Trade Setup: + tradeType: 'TradeType.SWAP, TradeType.LIMIT or TradeType.ADVANCED', enabledTradeTypes: 'TradeType.SWAP, TradeType.LIMIT and/or TradeType.ADVANCED', } diff --git a/apps/widget-configurator/src/components/snippet/utils/formatParameters.ts b/apps/widget-configurator/src/components/snippet/utils/formatParameters.ts index e46ecef2c06..dd2f648d83a 100644 --- a/apps/widget-configurator/src/components/snippet/utils/formatParameters.ts +++ b/apps/widget-configurator/src/components/snippet/utils/formatParameters.ts @@ -31,7 +31,10 @@ export function formatParameters( : COMMENTS_BY_PARAM_NAME const resultWithComments = Object.keys(commentsByParamName).reduce((acc, propName) => { - return acc.replace(new RegExp(`"${propName}".*$`, 'gm'), `$& // ${commentsByParamName[propName]}`) + return acc.replace( + new RegExp(`"${propName}".*$`, 'gm'), + `$& // ${commentsByParamName[propName as keyof CowSwapWidgetParams]}`, + ) }, formattedParams) // Add values From 19f2bc52bd5f9971e98bdeabc9a513cb881e8f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Mon, 13 Apr 2026 19:36:42 +0200 Subject: [PATCH 022/110] feat: add field for appCode --- .../components/sidebar/sidebar.component.tsx | 17 +++- .../src/components/snippet/snippet.const.ts | 5 +- .../snippet/utils/formatParameters.ts | 13 +-- .../snippet/utils/sanitizeParameters.ts | 84 ++++++++++++++++++- .../src/configurator.constants.ts | 3 + .../src/configurator.types.ts | 1 + .../src/hooks/useWidgetParamsAndSettings.ts | 5 +- 7 files changed, 107 insertions(+), 21 deletions(-) diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index 7c11145a014..54d5ccd59d6 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -30,6 +30,7 @@ import { PaletteControl } from '../controls/PaletteControl' import { PartnerFeeControl } from '../controls/PartnerFeeControl' import { ThemeControl } from '../controls/ThemeControl' import { TokenListControl } from '../controls/TokenListControl' +import { COMMENTS_BY_PARAM_NAME } from '../snippet/snippet.const' import { AccordionSection } from '../ui/Accordion/AccordionSection' import { BooleanSwitchControl } from '../ui/controls/BooleanSwitch/BooleanSwitchControl' import { CurrencyInputControl } from '../ui/controls/CurrencyInput/CurrencyInputControl' @@ -90,6 +91,8 @@ export function Sidebar({ const localeState = useState('') const [locale] = localeState + const [appCode, setAppCode] = useState('') + // Trade Setup Section: const networkControlState = useState(NetworkOptions[0]) @@ -230,6 +233,7 @@ export function Sidebar({ () => ({ // Basics: + appCode, // widgetMode: WidgetMode standaloneMode, locale: locale || undefined, @@ -305,6 +309,7 @@ export function Sidebar({ [ // Basics: + appCode, // widgetMode: WidgetMode standaloneMode, locale, @@ -390,7 +395,9 @@ export function Sidebar({ TODO: - [x] Classify state props into categories in type definition file. - - [ ] Add field for appCode. + - [x] Add field for appCode. + - [ ] Make widget theme selector work. + - [ ] Add loader to widget, also when reloading / updating. - [ ] Update AccordionSection so that we just pass title, currentTitle and onChange, and handle that with a single state variable and a single handler function. - [ ] Create reusable TextInput, NumberInput and SelectInput components. - [ ] Add name to all fields. @@ -410,6 +417,14 @@ export function Sidebar({ + setAppCode(value ?? '')} + helperText={COMMENTS_BY_PARAM_NAME.appCode} + inputProps={{ maxLength: 50 }} + /> {!IS_IFRAME && } diff --git a/apps/widget-configurator/src/components/snippet/snippet.const.ts b/apps/widget-configurator/src/components/snippet/snippet.const.ts index 086ab4d7e7e..06b603b810c 100644 --- a/apps/widget-configurator/src/components/snippet/snippet.const.ts +++ b/apps/widget-configurator/src/components/snippet/snippet.const.ts @@ -58,9 +58,8 @@ export const COMMENTS_BY_PARAM_NAME_TYPESCRIPT: Record = { enabledTradeTypes: 'TradeType.SWAP, TradeType.LIMIT and/or TradeType.ADVANCED', } -export const SANITIZE_PARAMS = { - appCode: 'My Cool App', -} as const +/** Shown in generated embed snippets when the Basics app code field is empty. */ +export const WIDGET_SNIPPET_APP_CODE_PLACEHOLDER = '' export const WIDGET_CONFIGURATOR_DEFAULT_BASE_URL = 'https://swap.cow.fi' diff --git a/apps/widget-configurator/src/components/snippet/utils/formatParameters.ts b/apps/widget-configurator/src/components/snippet/utils/formatParameters.ts index dd2f648d83a..09d008ded79 100644 --- a/apps/widget-configurator/src/components/snippet/utils/formatParameters.ts +++ b/apps/widget-configurator/src/components/snippet/utils/formatParameters.ts @@ -3,11 +3,7 @@ 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, - WIDGET_CONFIGURATOR_DEFAULT_BASE_URL, -} from '../snippet.const' +import { COMMENTS_BY_PARAM_NAME, COMMENTS_BY_PARAM_NAME_TYPESCRIPT } from '../snippet.const' export function formatParameters( params: CowSwapWidgetParams, @@ -16,13 +12,6 @@ export function formatParameters( defaultPalette: ColorPalette, ): string { const paramsSanitized = sanitizeParameters(params, defaultPalette) - - // 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) // Add comments diff --git a/apps/widget-configurator/src/components/snippet/utils/sanitizeParameters.ts b/apps/widget-configurator/src/components/snippet/utils/sanitizeParameters.ts index 87c6ab182ad..9b234dae8e0 100644 --- a/apps/widget-configurator/src/components/snippet/utils/sanitizeParameters.ts +++ b/apps/widget-configurator/src/components/snippet/utils/sanitizeParameters.ts @@ -1,14 +1,92 @@ import { CowSwapWidgetPalette, CowSwapWidgetPaletteColors, CowSwapWidgetParams } from '@cowprotocol/widget-lib' +import { CONFIGURATOR_WIDGET_PREVIEW_APP_CODE_FALLBACK } from '../../../configurator.constants' import { ColorPalette } from '../../../configurator.types' -import { SANITIZE_PARAMS } from '../snippet.const' +import { WIDGET_CONFIGURATOR_DEFAULT_BASE_URL, WIDGET_SNIPPET_APP_CODE_PLACEHOLDER } from '../snippet.const' +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function isEmptyPlainObject(value: unknown): boolean { + return isPlainObject(value) && Object.keys(value).length === 0 +} + +function isPrunableLeaf(value: unknown): boolean { + return ( + value === undefined || + value === null || + value === false || + value === '' || + (typeof value === 'number' && Number.isNaN(value)) || + typeof value === 'function' + ) +} + +function shouldKeepPrunedChild(item: unknown): boolean { + return item !== undefined && item !== null && item !== false && item !== '' && !isEmptyPlainObject(item) +} + +function pruneArray(value: unknown[]): unknown { + const items = value.map((item) => pruneNilEmptyAndNested(item)).filter(shouldKeepPrunedChild) + + return items.length === 0 ? undefined : items +} + +function pruneRecord(value: Record): unknown { + const next: Record = {} + + for (const [key, val] of Object.entries(value)) { + const pruned = pruneNilEmptyAndNested(val) + + if (!shouldKeepPrunedChild(pruned)) { + continue + } + + next[key] = pruned + } + + return Object.keys(next).length === 0 ? undefined : next +} + +/** Drops unset/empty values so snippet JSON omits noise. Omits `false`; keeps numeric zero and `true`. */ +function pruneNilEmptyAndNested(value: unknown): unknown { + if (isPrunableLeaf(value)) { + return undefined + } + + if (Array.isArray(value)) { + return pruneArray(value) + } + + if (isPlainObject(value)) { + return pruneRecord(value) + } + + return value +} + +/** + * Params shaped for embed snippet output: theme diff, placeholder appCode when unset/preview-default, + * default baseUrl omitted, and nil/empty/false nested values stripped. + */ export function sanitizeParameters(params: CowSwapWidgetParams, defaultPalette: ColorPalette): CowSwapWidgetParams { - return { + const sanitized: CowSwapWidgetParams = { ...params, - ...SANITIZE_PARAMS, theme: sanitizePalette(params, defaultPalette), } + + const appCodeTrimmed = params.appCode?.trim() + + if (!appCodeTrimmed || appCodeTrimmed === CONFIGURATOR_WIDGET_PREVIEW_APP_CODE_FALLBACK) { + sanitized.appCode = WIDGET_SNIPPET_APP_CODE_PLACEHOLDER + } + + if (!params.baseUrl || params.baseUrl === WIDGET_CONFIGURATOR_DEFAULT_BASE_URL) { + delete sanitized.baseUrl + } + + return pruneNilEmptyAndNested(sanitized) as CowSwapWidgetParams } // Keep only changed values diff --git a/apps/widget-configurator/src/configurator.constants.ts b/apps/widget-configurator/src/configurator.constants.ts index 39a76858372..9b653128726 100644 --- a/apps/widget-configurator/src/configurator.constants.ts +++ b/apps/widget-configurator/src/configurator.constants.ts @@ -12,6 +12,9 @@ export const isVercel = window.location.hostname.includes('vercel.app') export const isDev = ['dev.widget.cow.fi', 'dev.swap.cow.fi'].includes(window.location.hostname) +/** Live preview `appCode` when Basics is blank; embed snippet treats this like unset and substitutes the snippet placeholder app code. */ +export const CONFIGURATOR_WIDGET_PREVIEW_APP_CODE_FALLBACK = 'CoW Widget: Configurator' as const + // UTM: export const UTM_PARAMS = 'utm_content=cow-widget-configurator&utm_medium=web&utm_source=widget.cow.fi' as const diff --git a/apps/widget-configurator/src/configurator.types.ts b/apps/widget-configurator/src/configurator.types.ts index c281fa4e7d8..468fbdc39c6 100644 --- a/apps/widget-configurator/src/configurator.types.ts +++ b/apps/widget-configurator/src/configurator.types.ts @@ -28,6 +28,7 @@ export type WidgetMode = 'dapp' | 'standalone' export interface ConfiguratorState { // Basics: + appCode: string // widgetMode: WidgetMode standaloneMode: boolean // TODO: Replace with widgetMode. locale?: string diff --git a/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts index ede500376ec..fbd64090f24 100644 --- a/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react' import { CowSwapWidgetParams, TradeType, WidgetHookEvents } from '@cowprotocol/widget-lib' -import { isDev, isLocalHost, isVercel } from '../configurator.constants' +import { CONFIGURATOR_WIDGET_PREVIEW_APP_CODE_FALLBACK, isDev, isLocalHost, isVercel } from '../configurator.constants' import { ConfiguratorState } from '../configurator.types' const vercelSuffix = '-cowswap-dev.vercel.app' @@ -156,6 +156,7 @@ function buildWidgetParams(configuratorState: ConfiguratorState | null): CowSwap const { // Basics: + appCode, // widgetMode: WidgetMode standaloneMode, locale, @@ -236,7 +237,7 @@ function buildWidgetParams(configuratorState: ConfiguratorState | null): CowSwap return { // Basics: - appCode: 'CoW Widget: Configurator', + appCode: appCode.trim() || CONFIGURATOR_WIDGET_PREVIEW_APP_CODE_FALLBACK, standaloneMode, locale, From 6768663c6414aa781837b6810bb3436c7fd205d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Mon, 13 Apr 2026 22:57:57 +0200 Subject: [PATCH 023/110] feat: add env badge/indicator in widget sidebar header --- .../env-badge/sidebar-env-badge.component.tsx | 74 +++++++++++++++++++ .../header/sidebar-header.component.tsx | 31 ++++---- .../components/sidebar/sidebar.component.tsx | 14 +++- .../src/hooks/useWidgetParamsAndSettings.ts | 24 +----- apps/widget-configurator/src/utils/baseUrl.ts | 44 +++++++++++ 5 files changed, 147 insertions(+), 40 deletions(-) create mode 100644 apps/widget-configurator/src/components/sidebar/env-badge/sidebar-env-badge.component.tsx create mode 100644 apps/widget-configurator/src/utils/baseUrl.ts diff --git a/apps/widget-configurator/src/components/sidebar/env-badge/sidebar-env-badge.component.tsx b/apps/widget-configurator/src/components/sidebar/env-badge/sidebar-env-badge.component.tsx new file mode 100644 index 00000000000..56f8f09e5a9 --- /dev/null +++ b/apps/widget-configurator/src/components/sidebar/env-badge/sidebar-env-badge.component.tsx @@ -0,0 +1,74 @@ +import { ReactNode } from 'react' + +import Box from '@mui/material/Box' +import Tooltip from '@mui/material/Tooltip' + +import { getEnvColor, getEnvLabel } from '../../../utils/baseUrl' + +export interface SidebarEnvBadgeProps { + brandColor: string + baseUrl: string + configuratorOrigin: string +} + +export function SidebarEnvBadge({ brandColor, baseUrl, configuratorOrigin }: SidebarEnvBadgeProps): ReactNode { + const configuratorLabel = getEnvLabel(configuratorOrigin) + const widgetAppLabel = getEnvLabel(baseUrl) + const showBadge = configuratorLabel !== 'Production' || widgetAppLabel !== 'Production' + + return ( + + + Configurator: + {configuratorLabel} + + + App: + {widgetAppLabel} + + + } + placement="bottom" + disableHoverListener={!showBadge} + > + + + + + ) +} diff --git a/apps/widget-configurator/src/components/sidebar/header/sidebar-header.component.tsx b/apps/widget-configurator/src/components/sidebar/header/sidebar-header.component.tsx index f26071dbdf0..895b65b19c3 100644 --- a/apps/widget-configurator/src/components/sidebar/header/sidebar-header.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/header/sidebar-header.component.tsx @@ -4,7 +4,9 @@ import { Color, Font, ProductLogo, ProductVariant } from '@cowprotocol/ui' import Box from '@mui/material/Box' import Typography from '@mui/material/Typography' + import { IS_IFRAME } from '../../../configurator.constants' +import { SidebarEnvBadge } from '../env-badge/sidebar-env-badge.component' const BRAND_COLOR: Record = { dark: Color.blue300Primary, @@ -17,37 +19,37 @@ export interface SidebarHeaderProps { title: string themeMode: ThemeMode standaloneMode: boolean + baseUrl: string } -export function SidebarHeader({ - title, - themeMode, - standaloneMode -}: SidebarHeaderProps): ReactNode { +export function SidebarHeader({ title, themeMode, standaloneMode, baseUrl }: SidebarHeaderProps): ReactNode { const brandColor = BRAND_COLOR[themeMode] return ( theme.palette.background.paper, + background: (theme) => theme.palette.background.paper, borderBottom: (theme) => `1px solid ${theme.palette.divider}`, }} > - + {title} + {!IS_IFRAME && ( diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index 54d5ccd59d6..2e067fd803f 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -19,8 +19,8 @@ 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 { ColorModeContext } from '../../theme/ColorModeContext' +import { CONFIGURATOR_DEFAULT_WIDGET_BASE_URL } from '../../utils/baseUrl' import { jsonHelperText } from '../../utils/jsonFieldParsing' import { AppearanceStyleControls } from '../controls/AppearanceStyleControls' import { CustomImagesControl } from '../controls/CustomImagesControl' @@ -396,6 +396,7 @@ export function Sidebar({ - [x] Classify state props into categories in type definition file. - [x] Add field for appCode. + - [ ] Fix sticky style issue. - [ ] Make widget theme selector work. - [ ] Add loader to widget, also when reloading / updating. - [ ] Update AccordionSection so that we just pass title, currentTitle and onChange, and handle that with a single state variable and a single handler function. @@ -403,8 +404,8 @@ export function Sidebar({ - [ ] Add name to all fields. - [ ] Move fields to individual panels. Pass one prop per value and one single callback that takes a ChangeEvent or name + value. - [ ] Add update/reload widget button if needed. - - [ ] Add env indicator. - - [ ] Automatically set baseUrl based on widget configurator env. + - [x] Add env indicator. + - [x] Automatically set baseUrl based on widget configurator env. - [ ] Allow wider sidebar to use it as mobile mode. - [ ] Does the widget configurator work on mobile? - [ ] Bug: when in dApp mode, reload the page with the wallet connected. You are connected outside, not within the widget. @@ -413,7 +414,12 @@ export function Sidebar({ return ( getDrawerSx(theme, isResizing)} variant="persistent" anchor="left" open={isOpen}> - + diff --git a/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts index fbd64090f24..53a0ea51c17 100644 --- a/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts @@ -2,29 +2,9 @@ import { useMemo } from 'react' import { CowSwapWidgetParams, TradeType, WidgetHookEvents } from '@cowprotocol/widget-lib' -import { CONFIGURATOR_WIDGET_PREVIEW_APP_CODE_FALLBACK, isDev, isLocalHost, isVercel } from '../configurator.constants' +import { CONFIGURATOR_WIDGET_PREVIEW_APP_CODE_FALLBACK } from '../configurator.constants' import { ConfiguratorState } from '../configurator.types' - -const vercelSuffix = '-cowswap-dev.vercel.app' - -const getBaseUrl = (): string => { - if (typeof window === 'undefined' || !window) return '' - - if (isLocalHost) return 'http://localhost:3000' - - if (isDev) return 'https://dev.swap.cow.fi' - - if (isVercel) { - const prKey = window.location.hostname.replace('widget-configurator-git-', '').replace(vercelSuffix, '') - - return `https://swap-dev-git-${prKey}${vercelSuffix}` - } - - return 'https://swap.cow.fi' -} - -/** 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() +import { CONFIGURATOR_DEFAULT_WIDGET_BASE_URL } from '../utils/baseUrl' const getTokenListsParam = ( tokenListUrls: ConfiguratorState['tokenListUrls'], diff --git a/apps/widget-configurator/src/utils/baseUrl.ts b/apps/widget-configurator/src/utils/baseUrl.ts new file mode 100644 index 00000000000..a0d1f4d5dfb --- /dev/null +++ b/apps/widget-configurator/src/utils/baseUrl.ts @@ -0,0 +1,44 @@ +import { isDev, isLocalHost, isVercel } from '../configurator.constants' + +const VERCEL_PREVIEW_URL_SUFFIX = '-cowswap-dev.vercel.app' + +/** 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() + +export function getBaseUrl(): string { + if (typeof window === 'undefined' || !window) return '' + + if (isLocalHost) return 'http://localhost:3000' + + if (isDev) return 'https://dev.swap.cow.fi' + + if (isVercel) { + const prKey = window.location.hostname + .replace('widget-configurator-git-', '') + .replace(VERCEL_PREVIEW_URL_SUFFIX, '') + + return `https://swap-dev-git-${prKey}${VERCEL_PREVIEW_URL_SUFFIX}` + } + + return 'https://swap.cow.fi' +} + +export function getEnvColor(brandColor: string, url: string): string { + if (url.startsWith('http://localhost:')) return brandColor + + if (url.includes(VERCEL_PREVIEW_URL_SUFFIX)) return 'darkred' + + if (url.startsWith('https://dev.swap.cow.fi') || url.startsWith('https://dev.widget.cow.fi')) return 'orangered' + + return 'green' +} + +export function getEnvLabel(url: string): 'Local' | 'Preview' | 'Dev' | 'Production' { + if (url.startsWith('http://localhost:')) return 'Local' + + if (url.includes(VERCEL_PREVIEW_URL_SUFFIX)) return 'Preview' + + if (url.startsWith('https://dev.swap.cow.fi') || url.startsWith('https://dev.widget.cow.fi')) return 'Dev' + + return 'Production' +} From f6ac9d39e0919751394a9c6cd6e3a05eae78aefc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Mon, 13 Apr 2026 23:37:37 +0200 Subject: [PATCH 024/110] feat: allow wider sidebar to use it as mobile mode --- .../configurator/configurator.styles.ts | 9 +- .../env-badge/sidebar-env-badge.component.tsx | 18 +- .../components/sidebar/sidebar.component.tsx | 420 +++++++++--------- .../src/components/sidebar/sidebar.styles.ts | 48 +- .../src/hooks/useResizableDrawerWidth.test.ts | 10 +- .../src/hooks/useResizableDrawerWidth.ts | 10 +- 6 files changed, 297 insertions(+), 218 deletions(-) diff --git a/apps/widget-configurator/src/components/configurator/configurator.styles.ts b/apps/widget-configurator/src/components/configurator/configurator.styles.ts index 6df46d94df3..23b3eccfc11 100644 --- a/apps/widget-configurator/src/components/configurator/configurator.styles.ts +++ b/apps/widget-configurator/src/components/configurator/configurator.styles.ts @@ -1,3 +1,5 @@ +import { darken, lighten } from '@mui/material/styles' + import type { SxProps, Theme } from '@mui/material/styles' export const configuradorRootSx: SxProps = { @@ -14,10 +16,11 @@ const CONTENT_PADDING_PX = 16 export const configuratorCheckeredCanvasSx = (showIframeOutline: boolean, blockScroll = false): SxProps => (theme) => { + const paper = theme.palette.background.paper 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 squareA = isDark ? lighten(paper, 0.06) : paper + const squareB = isDark ? paper : darken(paper, 0.11) + const base = paper const pattern = `repeating-conic-gradient(from 90deg, ${squareA} 0% 25%, ${squareB} 0% 50%)` return { diff --git a/apps/widget-configurator/src/components/sidebar/env-badge/sidebar-env-badge.component.tsx b/apps/widget-configurator/src/components/sidebar/env-badge/sidebar-env-badge.component.tsx index 56f8f09e5a9..5c42efc4917 100644 --- a/apps/widget-configurator/src/components/sidebar/env-badge/sidebar-env-badge.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/env-badge/sidebar-env-badge.component.tsx @@ -21,14 +21,16 @@ export function SidebarEnvBadge({ brandColor, baseUrl, configuratorOrigin }: Sid arrow title={ - - - - - - - - + + + + + + + + + +
Configurator:{configuratorLabel}
App:{widgetAppLabel}
Configurator:{configuratorLabel}
App:{widgetAppLabel}
} placement="bottom" diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index 2e067fd803f..b6a412d76b3 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -4,6 +4,7 @@ import { SupportedLocale, DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK } from '@cow 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' @@ -11,7 +12,7 @@ import { useWeb3ModalAccount } from '@web3modal/ethers5/react' import { SidebarFooter } from './footer/sidebar-footer.component' import { SidebarHeader } from './header/sidebar-header.component' -import { getDrawerSx } from './sidebar.styles' +import { drawerContentColumnSx, drawerPaperRowSx, getDrawerPatternFillerSx, getDrawerSx } from './sidebar.styles' import { DEFAULT_STATE, DEFAULT_TOKEN_LISTS, IS_IFRAME, TRADE_MODES } from '../../configurator.constants' import { ConfiguratorState, TokenListItem, WidgetMode } from '../../configurator.types' @@ -396,6 +397,10 @@ export function Sidebar({ - [x] Classify state props into categories in type definition file. - [x] Add field for appCode. + - [x] Automatically set baseUrl based on widget configurator env. + - [x] Add env indicator. + - [x] Allow wider sidebar to use it as mobile mode. + - [ ] Fix sticky style issue. - [ ] Make widget theme selector work. - [ ] Add loader to widget, also when reloading / updating. @@ -404,212 +409,231 @@ export function Sidebar({ - [ ] Add name to all fields. - [ ] Move fields to individual panels. Pass one prop per value and one single callback that takes a ChangeEvent or name + value. - [ ] Add update/reload widget button if needed. - - [x] Add env indicator. - - [x] Automatically set baseUrl based on widget configurator env. - - [ ] Allow wider sidebar to use it as mobile mode. - - [ ] Does the widget configurator work on mobile? + - [ ] Add presets for baseUrl and layout. - [ ] Bug: when in dApp mode, reload the page with the wallet connected. You are connected outside, not within the widget. */ return ( getDrawerSx(theme, isResizing)} variant="persistent" anchor="left" open={isOpen}> - - - - - setAppCode(value ?? '')} - helperText={COMMENTS_BY_PARAM_NAME.appCode} - inputProps={{ maxLength: 50 }} + + + - {!IS_IFRAME && } - - - - - - - {!IS_IFRAME && ( - - )} - - - - - - - - - - - - - - - - - + + setAppCode(value ?? '')} + helperText={COMMENTS_BY_PARAM_NAME.appCode} + inputProps={{ maxLength: 50 }} + /> + {!IS_IFRAME && } + + + + + + + {!IS_IFRAME && ( + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setBaseUrl(value)} + placeholder={CONFIGURATOR_DEFAULT_WIDGET_BASE_URL} + helperText={`Optional. Sets baseUrl (overrides Raw JSON). Default preview URL: ${CONFIGURATOR_DEFAULT_WIDGET_BASE_URL}`} + /> + + setRawParamsJson(null, value)} + helperText={jsonHelperText(rawParamsJson.error)} + /> + + + + -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - setBaseUrl(value)} - placeholder={CONFIGURATOR_DEFAULT_WIDGET_BASE_URL} - helperText={`Optional. Sets baseUrl (overrides Raw JSON). Default preview URL: ${CONFIGURATOR_DEFAULT_WIDGET_BASE_URL}`} - /> - - setRawParamsJson(null, value)} - helperText={jsonHelperText(rawParamsJson.error)} - /> - -
- - +
+ + getDrawerPatternFillerSx(theme)} aria-hidden /> +
) } diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.styles.ts b/apps/widget-configurator/src/components/sidebar/sidebar.styles.ts index 905078da8cc..f5ea6b9f9f9 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.styles.ts +++ b/apps/widget-configurator/src/components/sidebar/sidebar.styles.ts @@ -1,9 +1,55 @@ -import type { CSSObject, Theme } from '@mui/material/styles' +import { alpha } from '@mui/material/styles' + +import type { CSSObject, SxProps, 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' +/** Max width of form/header/footer column; extra drawer width shows the patterned filler only. */ +export const SIDEBAR_CONTENT_MAX_WIDTH_PX = 600 + +/** Inset from the drawer outer edge (left and right of the patterned filler). */ +export const SIDEBAR_EDGE_INSET_PX = 8 + +/** Gap between the content column and the patterned filler. */ +export const SIDEBAR_PATTERN_GAP_PX = 8 + +export const drawerPaperRowSx: SxProps = { + display: 'flex', + flexDirection: 'row', + width: '100%', + flex: '1 0 100%', + boxSizing: 'border-box', +} + +export const drawerContentColumnSx: SxProps = { + display: 'flex', + flexDirection: 'column', + boxSizing: 'border-box', + maxWidth: SIDEBAR_CONTENT_MAX_WIDTH_PX, + flex: `0 1 ${SIDEBAR_CONTENT_MAX_WIDTH_PX}px`, + minWidth: 0, + minHeight: 0, + boxShadow: (theme: Theme) => `0 0 0 1px ${theme.palette.divider}`, + zIndex: 1, +} + +export function getDrawerPatternFillerSx(theme: Theme): CSSObject { + const stripe = alpha(theme.palette.divider, 0.0625 / 2) + + return { + flex: '1 1 0', + minWidth: 0, + boxSizing: 'border-box', + alignSelf: 'stretch', + minHeight: 0, + backgroundColor: theme.palette.background.paper, + backgroundImage: `repeating-linear-gradient(-45deg, ${stripe}, ${stripe} 1px, transparent 1px, transparent 8px)`, + boxShadow: `inset 0 0 0 16px ${theme.palette.background.paper}`, + } +} + export function getDrawerSx(theme: Theme, isResizing: boolean): CSSObject { return { width: `var(${DRAWER_WIDTH_CSS_VAR})`, diff --git a/apps/widget-configurator/src/hooks/useResizableDrawerWidth.test.ts b/apps/widget-configurator/src/hooks/useResizableDrawerWidth.test.ts index c71051787e2..f500277ed72 100644 --- a/apps/widget-configurator/src/hooks/useResizableDrawerWidth.test.ts +++ b/apps/widget-configurator/src/hooks/useResizableDrawerWidth.test.ts @@ -5,11 +5,15 @@ describe('clampDrawerWidth', () => { expect(clampDrawerWidth(200, 1600)).toBe(380) }) - it('does not allow widths above the configured maximum', () => { - expect(clampDrawerWidth(900, 1600)).toBe(720) + it('does not allow widths above viewport minus minimum preview width', () => { + expect(clampDrawerWidth(2000, 1600)).toBe(1360) }) it('keeps enough room for the preview on smaller viewports', () => { - expect(clampDrawerWidth(720, 900)).toBe(540) + expect(clampDrawerWidth(720, 900)).toBe(660) + }) + + it('lowers the effective minimum when the viewport cannot fit both limits', () => { + expect(clampDrawerWidth(400, 500)).toBe(260) }) }) diff --git a/apps/widget-configurator/src/hooks/useResizableDrawerWidth.ts b/apps/widget-configurator/src/hooks/useResizableDrawerWidth.ts index 831aa183c7f..cab76a6e7a1 100644 --- a/apps/widget-configurator/src/hooks/useResizableDrawerWidth.ts +++ b/apps/widget-configurator/src/hooks/useResizableDrawerWidth.ts @@ -1,18 +1,18 @@ import React, { RefObject, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' -const DEFAULT_DRAWER_WIDTH = 320 const MIN_DRAWER_WIDTH = 380 -const MAX_DRAWER_WIDTH = 720 -const MIN_PREVIEW_WIDTH = 360 +const DEFAULT_DRAWER_WIDTH = MIN_DRAWER_WIDTH +const MIN_PREVIEW_WIDTH = 240 function getViewportWidth(): number { return typeof window === 'undefined' ? Number.POSITIVE_INFINITY : window.innerWidth } export function clampDrawerWidth(nextWidth: number, viewportWidth = getViewportWidth()): number { - const maxAllowedWidth = Math.max(MIN_DRAWER_WIDTH, Math.min(MAX_DRAWER_WIDTH, viewportWidth - MIN_PREVIEW_WIDTH)) + const maxAllowedWidth = Math.max(0, viewportWidth - MIN_PREVIEW_WIDTH) + const minAllowedWidth = Math.min(MIN_DRAWER_WIDTH, maxAllowedWidth) - return Math.min(Math.max(nextWidth, MIN_DRAWER_WIDTH), maxAllowedWidth) + return Math.min(Math.max(nextWidth, minAllowedWidth), maxAllowedWidth) } interface UseResizableDrawerWidthResult { From a8bcc4f8177be66030d1916c2c60bc6ac37664cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Tue, 14 Apr 2026 11:54:05 +0200 Subject: [PATCH 025/110] feat: add loader and update widget using React transitions --- .../configurator/configurator.component.tsx | 112 +++++++- .../configurator/configurator.styles.ts | 5 +- .../footer/sidebar-footer.component.tsx | 248 ++++++++++-------- .../components/sidebar/sidebar.component.tsx | 13 +- .../src/configurator.constants.ts | 3 + .../src/hooks/useWidgetParamsAndSettings.ts | 17 +- libs/widget-react/src/lib/CowSwapWidget.tsx | 15 +- 7 files changed, 284 insertions(+), 129 deletions(-) diff --git a/apps/widget-configurator/src/components/configurator/configurator.component.tsx b/apps/widget-configurator/src/components/configurator/configurator.component.tsx index 59403bf2f7d..765d80ba2fe 100644 --- a/apps/widget-configurator/src/components/configurator/configurator.component.tsx +++ b/apps/widget-configurator/src/components/configurator/configurator.component.tsx @@ -6,19 +6,18 @@ import { CowSwapWidgetParams } from '@cowprotocol/widget-lib' import { CowSwapWidget } from '@cowprotocol/widget-react' import CloseIcon from '@mui/icons-material/Close' -import { CircularProgress, IconButton, Snackbar } from '@mui/material' -import Box from '@mui/material/Box' +import { Box, IconButton, Snackbar } from '@mui/material' import { useWeb3ModalAccount, useWeb3ModalTheme } from '@web3modal/ethers5/react' import { configuratorCheckeredCanvasSx, configuradorRootSx } from './configurator.styles' import { AnalyticsCategory } from '../../common/analytics/types' -import { COW_LISTENERS, IS_IFRAME } from '../../configurator.constants' +import { COW_LISTENERS, IS_IFRAME, WIDGET_PREVIEW_READY_FALLBACK_MS } from '../../configurator.constants' +import { ConfiguratorState } from '../../configurator.types' import { useProvider } from '../../hooks/useProvider' import { useResizableDrawerWidth } from '../../hooks/useResizableDrawerWidth' import { useToastsManager } from '../../hooks/useToastsManager' import { useWidgetParams } from '../../hooks/useWidgetParamsAndSettings' -import { ConfiguratorState } from '../../configurator.types' import { SidebarControls } from '../sidebar/controls/sidebar-controls.component' import { Sidebar } from '../sidebar/sidebar.component' import { DRAWER_WIDTH_CSS_VAR } from '../sidebar/sidebar.styles' @@ -30,6 +29,7 @@ declare global { } } +// eslint-disable-next-line max-lines-per-function export function Configurator({ title }: { title: string }): ReactNode { const configuratorRef = useRef(null) const { setThemeMode } = useWeb3ModalTheme() @@ -48,7 +48,19 @@ 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, as older versions do not send events, so we do not know when they are ready. + const [widgetKey, setWidgetKey] = useState(0) + const [isWidgetReady, setIsWidgetReadyState] = useState(false) + const widgetReadyRef = useRef(false) + const isWidgetReadyTimeoutId = useRef(0) + + const setIsWidgetReady = useCallback((isReady: boolean): void => { + widgetReadyRef.current = isReady + window.clearTimeout(isWidgetReadyTimeoutId.current) + setIsWidgetReadyState(isReady) + }, []) + + // Sidebar & Snippet Handling: + const [isSidebarOpen, setIsSidebarOpen] = useState(true) const [isSnippetOpen, setIsSnippetOpen] = useState(false) const { drawerWidth, isResizing, handleResizeStart } = useResizableDrawerWidth(configuratorRef, DRAWER_WIDTH_CSS_VAR) @@ -64,10 +76,38 @@ export function Configurator({ title }: { title: string }): ReactNode { // Widget Configurator State: const [configuratorState, setConfiguratorState] = useState(null) + const [params, isPending] = useWidgetParams(configuratorState) + const hasParams = params && configuratorState + + const handlePreviewReady = useCallback((): void => { + console.log(`[WIDGET] READY`) + setIsWidgetReady(true) + }, [setIsWidgetReady]) + + const handleForceWidgetReload = useCallback((): void => { + if (widgetReadyRef.current) { + setIsWidgetReady(false) + setWidgetKey((k) => k + 1) + } else { + // If the widget is not ready yet but we click here, we just force the iframe to appear. + // This is useful for older Widget App versions that did not have the READY event. + setIsWidgetReady(true) + } + }, [setIsWidgetReady]) - const showIframeOutline = configuratorState?.showIframeOutline ?? true + // Old Widget Apps won't have the ready event, so we need a timeout: + + useEffect(() => { + if (isWidgetReady || !hasParams) return - const params = useWidgetParams(configuratorState) + isWidgetReadyTimeoutId.current = window.setTimeout(() => { + setIsWidgetReady(true) + }, WIDGET_PREVIEW_READY_FALLBACK_MS) + + return () => { + window.clearTimeout(isWidgetReadyTimeoutId.current) + } + }, [isWidgetReady, hasParams, setIsWidgetReady]) const [listeners, setListeners] = useState(COW_LISTENERS) const toastManager = useToastsManager(setListeners) @@ -85,27 +125,68 @@ export function Configurator({ title }: { title: string }): ReactNode { } }, [isConnected, cowAnalytics]) + const showIframeOutline = configuratorState?.showIframeOutline ?? true + + const shouldShowLoader = !params || !configuratorState || !isWidgetReady + + const loaderElement = ( + theme.palette.background.paper, + borderRadius: '50%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: 40, + opacity: shouldShowLoader ? 1 : 0, + visibility: shouldShowLoader ? 'visible' : 'hidden', + pointerEvents: 'none', + transition: 'opacity 300ms ease, visibility 300ms ease', + '@keyframes cowConfiguratorLoaderSpin': { + from: { transform: 'rotate(0deg)' }, + to: { transform: 'rotate(360deg)' }, + }, + animation: shouldShowLoader ? 'cowConfiguratorLoaderSpin 0.25s linear infinite' : 'none', + '&::before': { + content: (theme) => (theme.palette.mode === 'light' ? '"📀"' : '"💿"'), + display: 'block', + lineHeight: 1, + }, + }} + /> + ) + let configuratorContent: React.ReactNode = null - if (!isWidgetReady || !params || !configuratorState) { + if (!params || !configuratorState) { configuratorContent = ( - -
- -
+ + {loaderElement} +
) } else { + const showIframeOutline = configuratorState?.showIframeOutline ?? true configuratorContent = ( <> - + + {loaderElement} + - {isSnippetOpen ? ( + {isSnippetOpen && isWidgetReady ? ( => + (isWidgetReady: boolean, showIframeOutline: boolean, blockScroll = false): SxProps => (theme) => { const paper = theme.palette.background.paper const isDark = theme.palette.mode === 'dark' @@ -48,6 +48,9 @@ export const configuratorCheckeredCanvasSx = border: 0, margin: '0 auto', outline: showIframeOutline ? '1px dashed cyan' : 'none', + transition: 'opacity 0.3s ease-in-out', + opacity: isWidgetReady ? 1 : 0, + pointerEvents: isWidgetReady ? 'auto' : 'none', }, }, } diff --git a/apps/widget-configurator/src/components/sidebar/footer/sidebar-footer.component.tsx b/apps/widget-configurator/src/components/sidebar/footer/sidebar-footer.component.tsx index d8340fc32b9..a74ee2dd8a4 100644 --- a/apps/widget-configurator/src/components/sidebar/footer/sidebar-footer.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/footer/sidebar-footer.component.tsx @@ -1,12 +1,11 @@ import React, { ReactNode } from 'react' -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 { ChevronLeft, ChevronRight, Code, Eye, Moon, Sun, RefreshCw } from 'react-feather' import { UTM_PARAMS } from '../../../configurator.constants' @@ -18,6 +17,9 @@ export interface SidebarFooterProps { onSidebarToggle: () => void isSnippetOpen: boolean onSnippetToggle: () => void + isWidgetReady: boolean + isWidgetSyncPending: boolean + onForceWidgetReload: () => void } // eslint-disable-next-line max-lines-per-function @@ -26,10 +28,13 @@ export function SidebarFooter({ onSidebarToggle, isSnippetOpen, onSnippetToggle, + isWidgetReady, + isWidgetSyncPending, + onForceWidgetReload, }: SidebarFooterProps): ReactNode { const theme: 'dark' | 'light' = 'dark' - const snippetLabel = isSnippetOpen ? 'View preview' : 'View code snippet' + const snippetLabel = isSnippetOpen ? 'See preview' : 'Get code' const SnippetIcon = isSnippetOpen ? Eye : Code const themeLabel = 'Switch theme' @@ -60,118 +65,151 @@ export function SidebarFooter({ width: 40, } as const - return (<> -
- - t.palette.background.paper, - borderTop: (t) => `1px solid ${t.palette.divider}`, - display: 'flex', - flexDirection: 'column', - gap: 2, - px: 2, - pt: 2, - mt: "auto", - }} - > + let reloadPreviewLabel = '' + + if (isWidgetSyncPending) { + reloadPreviewLabel = 'Syncing widget...' + } else if (!isWidgetReady) { + reloadPreviewLabel = 'Loading widget... Click to force load.' + } else { + reloadPreviewLabel = 'Reload widget' + } + + return ( + <> +
+ t.palette.background.paper, + borderTop: (t) => `1px solid ${t.palette.divider}`, display: 'flex', - flexDirection: 'row', - alignItems: 'center', - gap: 1, - minHeight: 40, + flexDirection: 'column', + gap: 2, + px: 2, + pt: 2, + mt: 'auto', }} > - - - - {}} - aria-label={themeLabel} - size="small" - sx={iconOnlyButtonSx} - > - - - - - - } + 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 }, + }} > - - - - - - - - Widget web - - + + + + + + + + + {}} aria-label={themeLabel} size="small" sx={iconOnlyButtonSx}> + + + + + + + + + + + + - Developer docs - + + Widget web + + + Developer docs + + - - ) + + ) } diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index b6a412d76b3..d9c1022a83d 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -56,6 +56,9 @@ export interface SidebarProps { onSnippetToggle: () => void onStateChange: (state: ConfiguratorState) => void toastManager: UseToastsManagerReturn + isWidgetReady: boolean + isWidgetSyncPending: boolean + onForceWidgetReload: () => void } // eslint-disable-next-line max-lines-per-function @@ -68,6 +71,9 @@ export function Sidebar({ onSnippetToggle, onStateChange, toastManager, + isWidgetReady, + isWidgetSyncPending, + onForceWidgetReload, }: SidebarProps): ReactNode { const availableChains = useAvailableChains() @@ -400,15 +406,15 @@ export function Sidebar({ - [x] Automatically set baseUrl based on widget configurator env. - [x] Add env indicator. - [x] Allow wider sidebar to use it as mobile mode. + - [x] Add loader to widget, also when reloading / updating. + - [x] Add update/reload widget button if needed. - [ ] Fix sticky style issue. - [ ] Make widget theme selector work. - - [ ] Add loader to widget, also when reloading / updating. - [ ] Update AccordionSection so that we just pass title, currentTitle and onChange, and handle that with a single state variable and a single handler function. - [ ] Create reusable TextInput, NumberInput and SelectInput components. - [ ] Add name to all fields. - [ ] Move fields to individual panels. Pass one prop per value and one single callback that takes a ChangeEvent or name + value. - - [ ] Add update/reload widget button if needed. - [ ] Add presets for baseUrl and layout. - [ ] Bug: when in dApp mode, reload the page with the wallet connected. You are connected outside, not within the widget. @@ -629,6 +635,9 @@ export function Sidebar({ onSidebarToggle={onSidebarToggle} isSnippetOpen={isSnippetOpen} onSnippetToggle={onSnippetToggle} + isWidgetReady={isWidgetReady} + isWidgetSyncPending={isWidgetSyncPending} + onForceWidgetReload={onForceWidgetReload} /> diff --git a/apps/widget-configurator/src/configurator.constants.ts b/apps/widget-configurator/src/configurator.constants.ts index 9b653128726..d536a76b442 100644 --- a/apps/widget-configurator/src/configurator.constants.ts +++ b/apps/widget-configurator/src/configurator.constants.ts @@ -15,6 +15,9 @@ export const isDev = ['dev.widget.cow.fi', 'dev.swap.cow.fi'].includes(window.lo /** Live preview `appCode` when Basics is blank; embed snippet treats this like unset and substitutes the snippet placeholder app code. */ export const CONFIGURATOR_WIDGET_PREVIEW_APP_CODE_FALLBACK = 'CoW Widget: Configurator' as const +/** Older widget apps (custom baseUrl) may not post READY; hide the preview loader after this timeout. */ +export const WIDGET_PREVIEW_READY_FALLBACK_MS = 60_000 + // UTM: export const UTM_PARAMS = 'utm_content=cow-widget-configurator&utm_medium=web&utm_source=widget.cow.fi' as const diff --git a/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts index 53a0ea51c17..c6d8d5a1aca 100644 --- a/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts +++ b/apps/widget-configurator/src/hooks/useWidgetParamsAndSettings.ts @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useEffect, useState, useTransition } from 'react' import { CowSwapWidgetParams, TradeType, WidgetHookEvents } from '@cowprotocol/widget-lib' @@ -287,6 +287,17 @@ function buildWidgetParams(configuratorState: ConfiguratorState | null): CowSwap } } -export function useWidgetParams(configuratorState: ConfiguratorState | null): CowSwapWidgetParams | null { - return useMemo(() => buildWidgetParams(configuratorState), [configuratorState]) +export function useWidgetParams(configuratorState: ConfiguratorState | null): [CowSwapWidgetParams | null, boolean] { + const [isPending, startTransition] = useTransition() + const [debouncedConfiguratorState, setDebouncedConfiguratorState] = useState(() => + buildWidgetParams(configuratorState), + ) + + useEffect(() => { + startTransition(() => { + setDebouncedConfiguratorState(buildWidgetParams(configuratorState)) + }) + }, [configuratorState]) + + return [debouncedConfiguratorState, isPending] } diff --git a/libs/widget-react/src/lib/CowSwapWidget.tsx b/libs/widget-react/src/lib/CowSwapWidget.tsx index 61bc85a9c31..9fe137732a7 100644 --- a/libs/widget-react/src/lib/CowSwapWidget.tsx +++ b/libs/widget-react/src/lib/CowSwapWidget.tsx @@ -10,8 +10,7 @@ import { createCowSwapWidget, } from '@cowprotocol/widget-lib' -export function CowSwapWidget(props: CowSwapWidgetProps): JSX.Element { - const { params, provider, listeners, onReady } = props +export function CowSwapWidget({ params, provider, listeners, onReady }: CowSwapWidgetProps): JSX.Element { const [error, setError] = useState<{ error: Error; message: string } | null>(null) const paramsRef = useRef(null) const providerRef = useRef(provider) @@ -136,8 +135,16 @@ export function CowSwapWidget(props: CowSwapWidgetProps): JSX.Element { ) } - // Render widget container - return
+ return ( +
+ ) } function areParamsHooksDifferent(prev: CowSwapWidgetParams, next: CowSwapWidgetParams): boolean { From 545ee44efeefce71161919bb7f67732172bd57ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Thu, 30 Apr 2026 16:55:38 +0200 Subject: [PATCH 026/110] fix: fix tests --- .../updaters/IframeResizer.test.tsx | 88 ++++++++++++++----- .../components/controls/ThemeControl.test.tsx | 2 +- .../ui/Accordion/AccordionSection.test.tsx | 58 ++++++++---- .../ui/Accordion/AccordionSection.tsx | 5 +- libs/common-utils/src/index.ts | 1 + .../widget-lib/src/applyElementStyles.spec.ts | 57 ++++++++++++ 6 files changed, 168 insertions(+), 43 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.test.tsx b/apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.test.tsx index 6e5f15395d4..7fb242dbb34 100644 --- a/apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.test.tsx +++ b/apps/cowswap-frontend/src/modules/injectedWidget/updaters/IframeResizer.test.tsx @@ -24,6 +24,17 @@ jest.mock('@cowprotocol/ui', () => ({ }, })) +jest.mock('../hooks/useInjectedWidgetParams', () => ({ + useInjectedWidgetParams: jest.fn(() => ({})), +})) + +jest.mock('@cowprotocol/iframe-transport', () => ({ + ...jest.requireActual('@cowprotocol/iframe-transport'), + getParentOrigin: jest.fn(() => 'https://parent.example'), +})) + +const MOCK_PARENT_ORIGIN = 'https://parent.example' + const useAtomValueMock = useAtomValue as jest.MockedFunction const isIframeMock = isIframe as jest.MockedFunction const isInjectedWidgetMock = isInjectedWidget as jest.MockedFunction @@ -97,9 +108,14 @@ describe('IframeResizer', () => { it('uses ResizeObserver and window resize events to emit updated heights', () => { const { unmount } = render() - expect(postMessageToWindowSpy).toHaveBeenCalledWith(window.parent, WidgetMethodsEmit.UPDATE_HEIGHT, { - height: 500, - }) + expect(postMessageToWindowSpy).toHaveBeenCalledWith( + window.parent, + WidgetMethodsEmit.UPDATE_HEIGHT, + { + height: 500, + }, + MOCK_PARENT_ORIGIN, + ) expect(resizeObserverObserveMock).toHaveBeenCalledWith(rootElement) expect(resizeObserverObserveMock).toHaveBeenCalledWith(document.body) expect(mutationObserverObserveMock).not.toHaveBeenCalled() @@ -110,9 +126,14 @@ describe('IframeResizer', () => { triggerResizeObserver?.() }) - expect(postMessageToWindowSpy).toHaveBeenLastCalledWith(window.parent, WidgetMethodsEmit.UPDATE_HEIGHT, { - height: 610, - }) + expect(postMessageToWindowSpy).toHaveBeenLastCalledWith( + window.parent, + WidgetMethodsEmit.UPDATE_HEIGHT, + { + height: 610, + }, + MOCK_PARENT_ORIGIN, + ) setContentSize({ bodyOffsetWidth: 400, rootScrollHeight: 680, rootOffsetHeight: 700 }) @@ -120,9 +141,14 @@ describe('IframeResizer', () => { window.dispatchEvent(new Event('resize')) }) - expect(postMessageToWindowSpy).toHaveBeenLastCalledWith(window.parent, WidgetMethodsEmit.UPDATE_HEIGHT, { - height: 700, - }) + expect(postMessageToWindowSpy).toHaveBeenLastCalledWith( + window.parent, + WidgetMethodsEmit.UPDATE_HEIGHT, + { + height: 700, + }, + MOCK_PARENT_ORIGIN, + ) unmount() @@ -146,9 +172,14 @@ describe('IframeResizer', () => { render() - expect(postMessageToWindowSpy).toHaveBeenCalledWith(window.parent, WidgetMethodsEmit.UPDATE_HEIGHT, { - height: 640, - }) + expect(postMessageToWindowSpy).toHaveBeenCalledWith( + window.parent, + WidgetMethodsEmit.UPDATE_HEIGHT, + { + height: 640, + }, + MOCK_PARENT_ORIGIN, + ) }) it('falls back to MutationObserver when ResizeObserver is unavailable', () => { @@ -169,9 +200,14 @@ describe('IframeResizer', () => { triggerMutationObserver?.() }) - expect(postMessageToWindowSpy).toHaveBeenLastCalledWith(window.parent, WidgetMethodsEmit.UPDATE_HEIGHT, { - height: 560, - }) + expect(postMessageToWindowSpy).toHaveBeenLastCalledWith( + window.parent, + WidgetMethodsEmit.UPDATE_HEIGHT, + { + height: 560, + }, + MOCK_PARENT_ORIGIN, + ) }) it('emits full-height updates while a modal is open', () => { @@ -184,9 +220,14 @@ describe('IframeResizer', () => { render() - expect(postMessageToWindowSpy).toHaveBeenCalledWith(window.parent, WidgetMethodsEmit.SET_FULL_HEIGHT, { - isUpToSmall: true, - }) + expect(postMessageToWindowSpy).toHaveBeenCalledWith( + window.parent, + WidgetMethodsEmit.SET_FULL_HEIGHT, + { + isUpToSmall: true, + }, + MOCK_PARENT_ORIGIN, + ) setContentSize({ bodyOffsetWidth: MEDIA_WIDTHS.upToSmall + 1, @@ -198,9 +239,14 @@ describe('IframeResizer', () => { window.dispatchEvent(new Event('resize')) }) - expect(postMessageToWindowSpy).toHaveBeenLastCalledWith(window.parent, WidgetMethodsEmit.SET_FULL_HEIGHT, { - isUpToSmall: false, - }) + expect(postMessageToWindowSpy).toHaveBeenLastCalledWith( + window.parent, + WidgetMethodsEmit.SET_FULL_HEIGHT, + { + isUpToSmall: false, + }, + MOCK_PARENT_ORIGIN, + ) }) }) diff --git a/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx b/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx index f362ca90af7..2f26ed874fa 100644 --- a/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx +++ b/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx @@ -2,7 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react' import { ThemeControl } from './ThemeControl' -import { ColorModeContext, type ColorModeParams } from '../../../theme/ColorModeContext' +import { ColorModeContext, type ColorModeParams } from '../../theme/ColorModeContext' function renderThemeControl(contextValue: Partial = {}): void { render( diff --git a/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.test.tsx b/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.test.tsx index d0be9407ae7..7cba4817a3d 100644 --- a/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.test.tsx +++ b/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.test.tsx @@ -1,39 +1,61 @@ +import { ReactNode, useState } from 'react' + import { fireEvent, render, screen } from '@testing-library/react' import { AccordionSection } from './AccordionSection' +function AccordionSectionHarness({ + initialExpanded = false, + title = 'Section', +}: { + initialExpanded?: boolean + title?: string +}): ReactNode { + const [expanded, setExpanded] = useState(initialExpanded) + + return ( + +
Inner content
+
+ ) +} + describe('AccordionSection', () => { - it('is collapsed by default', () => { - render( - -
Inner content
-
, - ) + it('is collapsed when expanded is false', () => { + render() expect(screen.getByRole('button', { name: 'Behavior' }).getAttribute('aria-expanded')).toBe('false') }) - it('supports expanded-by-default sections', () => { - render( - -
Inner content
-
, - ) + it('is expanded when expanded is true', () => { + render() expect(screen.getByRole('button', { name: 'Basics' }).getAttribute('aria-expanded')).toBe('true') }) - it('toggles expansion when clicked', () => { + it('toggles expansion when the summary is clicked', () => { + render() + + const button = screen.getByRole('button', { name: 'Advanced' }) + + fireEvent.click(button) + expect(button.getAttribute('aria-expanded')).toBe('true') + + fireEvent.click(button) + expect(button.getAttribute('aria-expanded')).toBe('false') + }) + + it('invokes onChange when expansion should update', () => { + const onChange = jest.fn() render( - +
Inner content
, ) - const button = screen.getByRole('button', { name: 'Advanced' }) + fireEvent.click(screen.getByRole('button', { name: 'Callbacks' })) - fireEvent.click(button) - - expect(button.getAttribute('aria-expanded')).toBe('true') + expect(onChange).toHaveBeenCalledTimes(1) + expect(onChange).toHaveBeenCalledWith(true) }) }) diff --git a/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.tsx b/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.tsx index 5833adce868..86cb90e648e 100644 --- a/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.tsx +++ b/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from 'react' +import { ReactNode, PropsWithChildren } from 'react' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import Accordion from '@mui/material/Accordion' @@ -7,11 +7,10 @@ import AccordionSummary from '@mui/material/AccordionSummary' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' -interface AccordionSectionProps { +interface AccordionSectionProps extends PropsWithChildren { title: string expanded: boolean onChange: (expanded: boolean) => void - children: ReactNode } export function AccordionSection({ title, expanded, onChange, children }: AccordionSectionProps): ReactNode { diff --git a/libs/common-utils/src/index.ts b/libs/common-utils/src/index.ts index 4a7e708ad88..d9cd6f4e03d 100644 --- a/libs/common-utils/src/index.ts +++ b/libs/common-utils/src/index.ts @@ -29,6 +29,7 @@ export * from './fractionUtils' export * from './genericPropsChecker' export * from './getAddress' export * from './getAvailableChains' +export * from './getCurrencyAddress' export * from './getCurrentChainIdFromUrl' export * from './getDeprecatedChains' export * from './getExplorerLink' diff --git a/libs/widget-lib/src/applyElementStyles.spec.ts b/libs/widget-lib/src/applyElementStyles.spec.ts index e69de29bb2d..7d88cc7a7c7 100644 --- a/libs/widget-lib/src/applyElementStyles.spec.ts +++ b/libs/widget-lib/src/applyElementStyles.spec.ts @@ -0,0 +1,57 @@ +import { assignElementStyles } from './applyElementStyles' + +import type * as CSS from 'csstype' + +describe('assignElementStyles', () => { + it('does nothing when styles is undefined', () => { + const el = document.createElement('div') + el.setAttribute('style', 'color: red;') + assignElementStyles(el, undefined) + expect(el.getAttribute('style')).toBe('color: red;') + }) + + it('removes the style attribute when styles is an empty object', () => { + const el = document.createElement('div') + el.setAttribute('style', 'color: red;') + assignElementStyles(el, {}) + expect(el.hasAttribute('style')).toBe(false) + }) + + it('applies camelCase string properties to the element style', () => { + const el = document.createElement('div') + assignElementStyles(el, { width: '100px', backgroundColor: 'transparent' }) + expect(el.style.width).toBe('100px') + expect(el.style.backgroundColor).toBe('transparent') + }) + + it('skips keys whose value is undefined', () => { + const el = document.createElement('div') + assignElementStyles(el, { width: '10px', height: undefined }) + expect(el.style.width).toBe('10px') + expect(el.style.height).toBe('') + }) + + it('sets an empty string when the value is null', () => { + const el = document.createElement('div') + assignElementStyles(el, { margin: null } as unknown as CSS.Properties) + expect(el.style.margin).toBe('') + }) + + it('skips non-string values such as numbers', () => { + const el = document.createElement('div') + assignElementStyles(el, { + width: '20px', + // csstype allows numeric opacity; runtime number must be ignored by assignElementStyles + opacity: 0.5, + }) + expect(el.style.width).toBe('20px') + expect(el.style.opacity).toBe('') + }) + + it('applies empty string for an explicit empty string value', () => { + const el = document.createElement('div') + el.style.padding = '8px' + assignElementStyles(el, { padding: '' }) + expect(el.style.padding).toBe('') + }) +}) From b049dc9d087b54739a4c523f5e2e4cd5895ba55b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Thu, 30 Apr 2026 17:17:31 +0200 Subject: [PATCH 027/110] fix: fix tests --- .../hooks/useWidgetParamsAndSettings.ts | 221 ------- .../src/app/configurator/index.tsx | 617 ------------------ .../controls/PaletteControl.test.tsx | 2 +- .../components/controls/PaletteControl.tsx | 16 +- .../components/controls/TokenListControl.tsx | 2 +- 5 files changed, 10 insertions(+), 848 deletions(-) delete mode 100644 apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts delete mode 100644 apps/widget-configurator/src/app/configurator/index.tsx diff --git a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts b/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts deleted file mode 100644 index 75fb3090e1e..00000000000 --- a/apps/widget-configurator/src/app/configurator/hooks/useWidgetParamsAndSettings.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { useMemo } from 'react' - -import { CowSwapWidgetParams, TradeType, WidgetHookEvents } from '@cowprotocol/widget-lib' - -import { isDev, isLocalHost, isVercel } from '../../../env' -import { ConfiguratorState } from '../types' - -const vercelSuffix = '-cowswap-dev.vercel.app' - -const getBaseUrl = (): string => { - if (typeof window === 'undefined' || !window) return '' - - if (isLocalHost) return 'http://localhost:3000' - - if (isDev) return 'https://dev.swap.cow.fi' - - if (isVercel) { - const prKey = window.location.hostname.replace('widget-configurator-git-', '').replace(vercelSuffix, '') - - return `https://swap-dev-git-${prKey}${vercelSuffix}` - } - - return 'https://swap.cow.fi' -} - -const DEFAULT_BASE_URL = getBaseUrl() - -const getTokenListsParam = ( - tokenListUrls: ConfiguratorState['tokenListUrls'], - key: 'enabled' | 'enabledForSell' | 'enabledForBuy', -): string[] => { - return tokenListUrls.filter((list) => list[key]).map((list) => list.url) -} - -const getForcedOrderDeadline = ({ - deadline, - swapDeadline, - limitDeadline, - advancedDeadline, -}: Pick< - ConfiguratorState, - 'deadline' | 'swapDeadline' | 'limitDeadline' | 'advancedDeadline' ->): CowSwapWidgetParams['forcedOrderDeadline'] => { - if (!swapDeadline && !limitDeadline && !advancedDeadline) { - return deadline - } - - return { - [TradeType.SWAP]: swapDeadline, - [TradeType.LIMIT]: limitDeadline, - [TradeType.ADVANCED]: advancedDeadline, - } -} - -function confirmWidgetHookAction(message: string): boolean { - return prompt(message) === 'ok' -} - -function getWidgetHooks(enabledWidgetHooks: WidgetHookEvents[]): CowSwapWidgetParams['hooks'] { - const hooks: CowSwapWidgetParams['hooks'] = { - ...(enabledWidgetHooks.includes(WidgetHookEvents.ON_BEFORE_APPROVAL) - ? { - onBeforeApproval(payload) { - return confirmWidgetHookAction(`Type "ok" to proceed with approval on chainId ${payload.chainId}`) - }, - } - : null), - ...(enabledWidgetHooks.includes(WidgetHookEvents.ON_BEFORE_TRADE) - ? { - onBeforeTrade(payload) { - const sellToken = payload.sellToken?.symbol || 'unknown' - const buyToken = payload.buyToken?.symbol || 'unknown' - - return confirmWidgetHookAction( - `Type "ok" to proceed with ${payload.orderType} trade ${sellToken} -> ${buyToken}`, - ) - }, - } - : null), - ...(enabledWidgetHooks.includes(WidgetHookEvents.ON_BEFORE_WRAP_UNWRAP) - ? { - onBeforeWrapOrUnwrap(payload) { - const sellToken = payload.sellToken?.symbol || 'unknown' - const buyToken = payload.buyToken?.symbol || 'unknown' - - return confirmWidgetHookAction(`Type "ok" to proceed with wrap/unwrap ${sellToken} -> ${buyToken}`) - }, - } - : null), - ...(enabledWidgetHooks.includes(WidgetHookEvents.ON_BEFORE_ORDER_CANCEL) - ? { - onBeforeOrderCancel(payload) { - return confirmWidgetHookAction(`Type "ok" to cancel order ${payload.uid}`) - }, - } - : null), - ...(enabledWidgetHooks.includes(WidgetHookEvents.ON_BEFORE_ORDERS_CANCEL) - ? { - onBeforeOrdersCancel(payload) { - return confirmWidgetHookAction(`Type "ok" to cancel ${payload.length} orders`) - }, - } - : null), - } - - return hooks -} - -function getThemeParam( - theme: ConfiguratorState['theme'], - customColors: ConfiguratorState['customColors'], - defaultColors: ConfiguratorState['defaultColors'], - boxShadow: ConfiguratorState['boxShadow'], -): CowSwapWidgetParams['theme'] { - if (JSON.stringify(customColors) === JSON.stringify(defaultColors) && !boxShadow) { - return theme - } - - const themeColors = { - ...defaultColors, - ...customColors, - } - - return { - baseTheme: theme, - primary: themeColors.primary, - background: themeColors.background, - paper: themeColors.paper, - text: themeColors.text, - danger: themeColors.danger, - warning: themeColors.warning, - alert: themeColors.alert, - info: themeColors.info, - success: themeColors.success, - ...(boxShadow ? { boxShadow } : null), - } -} - -export function useWidgetParams(configuratorState: ConfiguratorState): CowSwapWidgetParams { - return useMemo(() => { - const { - chainId, - locale, - theme, - boxShadow, - currentTradeType, - enabledTradeTypes, - sellToken, - sellTokenAmount, - buyToken, - buyTokenAmount, - deadline, - swapDeadline, - limitDeadline, - advancedDeadline, - tokenListUrls, - customColors, - defaultColors, - partnerFeeBps, - partnerFeeRecipient, - standaloneMode, - disableToastMessages, - disableProgressBar, - disablePostTradeTips, - disableCrossChainSwap, - disableTokenImport, - hideRecentTokens, - hideFavoriteTokens, - hideBridgeInfo, - hideOrdersTable, - disableTradeWhenPriceImpactIsUnknown, - disableTradeWhenPriceImpactIsHigherThan, - slippage, - enabledWidgetHooks, - } = configuratorState - - const params: CowSwapWidgetParams = { - appCode: 'CoW Widget: Configurator', - width: '100%', - height: '640px', - chainId, - locale, - tokenLists: getTokenListsParam(tokenListUrls, 'enabled'), - sellTokenLists: getTokenListsParam(tokenListUrls, 'enabledForSell'), - buyTokenLists: getTokenListsParam(tokenListUrls, 'enabledForBuy'), - baseUrl: DEFAULT_BASE_URL, - tradeType: currentTradeType, - sell: { asset: sellToken, amount: sellTokenAmount ? sellTokenAmount.toString() : undefined }, - buy: { asset: buyToken, amount: buyTokenAmount?.toString() }, - forcedOrderDeadline: getForcedOrderDeadline({ deadline, swapDeadline, limitDeadline, advancedDeadline }), - enabledTradeTypes, - theme: getThemeParam(theme, customColors, defaultColors, boxShadow), - standaloneMode, - disableToastMessages, - disableProgressBar, - disablePostTradeTips, - disableCrossChainSwap, - disableTokenImport, - hideRecentTokens, - hideFavoriteTokens, - - partnerFee: - partnerFeeBps > 0 - ? { - bps: partnerFeeBps, - recipient: partnerFeeRecipient, - } - : undefined, - hideBridgeInfo, - hideOrdersTable, - slippage, - disableTrade: { - whenPriceImpactIsUnknown: disableTradeWhenPriceImpactIsUnknown, - whenPriceImpactIsHigherThan: disableTradeWhenPriceImpactIsHigherThan, - }, - hooks: getWidgetHooks(enabledWidgetHooks), - } - - return params - }, [configuratorState]) -} diff --git a/apps/widget-configurator/src/app/configurator/index.tsx b/apps/widget-configurator/src/app/configurator/index.tsx deleted file mode 100644 index c94bd35cd3d..00000000000 --- a/apps/widget-configurator/src/app/configurator/index.tsx +++ /dev/null @@ -1,617 +0,0 @@ -import { ChangeEvent, useCallback, useContext, useEffect, useMemo, 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 { 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 EditIcon from '@mui/icons-material/Edit' -import KeyboardDoubleArrowLeftIcon from '@mui/icons-material/KeyboardDoubleArrowLeft' -import LanguageIcon from '@mui/icons-material/Language' -import { FormControl, FormControlLabel, FormLabel, IconButton, Radio, RadioGroup, Snackbar } from '@mui/material' -import Box from '@mui/material/Box' -import Button from '@mui/material/Button' -import Divider from '@mui/material/Divider' -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 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 { 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 { 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 { useProvider } from './hooks/useProvider' -import { useSyncWidgetNetwork } from './hooks/useSyncWidgetNetwork' -import { useToastsManager } from './hooks/useToastsManager' -import { useWidgetParams } from './hooks/useWidgetParamsAndSettings' -import { ContentStyled, DrawerStyled, WalletConnectionWrapper, WrapperStyled } from './styled' -import { ConfiguratorState, TokenListItem } from './types' - -import { AnalyticsCategory } from '../../common/analytics/types' -import { ColorModeContext } from '../../theme/ColorModeContext' -import { EmbedDialog } from '../embedDialog' - -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 }) { - const { setThemeMode } = useWeb3ModalTheme() - const { chainId: walletChainId, 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' - - // TODO: Add proper return type annotation - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - const selectWidgetMode = (event: ChangeEvent) => { - setWidgetMode(event.target.value as WidgetMode) - } - - const [isDrawerOpen, setIsDrawerOpen] = useState(true) - - 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 - - const deadlineState = useState() - const [deadline] = deadlineState - const swapDeadlineState = useState() - const [swapDeadline] = swapDeadlineState - const limitDeadlineState = useState() - const [limitDeadline] = limitDeadlineState - const advancedDeadlineState = useState() - const [advancedDeadline] = advancedDeadlineState - - const tokenListUrlsState = useState(DEFAULT_TOKEN_LISTS) - const customTokensState = useState([]) - const [tokenListUrls] = tokenListUrlsState - const [customTokens] = customTokensState - - const partnerFeeBpsState = useState(0) - const [partnerFeeBps] = partnerFeeBpsState - - const customImagesState = useState({}) - const customSoundsState = useState({}) - const [customImages] = customImagesState - const [customSounds] = customSoundsState - - const [rawParams, setRawParams] = useState() - const [isWidgetDisplayed, setIsWidgetDisplayed] = useState(true) - - const paletteManager = useColorPaletteManager(mode) - const { colorPalette, defaultPalette } = paletteManager - const [boxShadow, setBoxShadow] = useState('') - - const { dialogOpen, handleDialogClose, handleDialogOpen } = useEmbedDialogState() - - const { closeToast, toasts, selectDisableToastMessages, disableToastMessages } = useToastsManager(setListeners) - const firstToast = toasts?.[0] - - const [disableProgressBar, setDisableProgressBar] = useState(false) - const toggleDisableProgressBar = useCallback(() => setDisableProgressBar((curr) => !curr), []) - - const [disablePostTradeTips, setDisablePostTradeTips] = useState(false) - const toggleDisablePostTradeTips = useCallback(() => setDisablePostTradeTips((curr) => !curr), []) - - const [disableCrossChainSwap, setDisableCrossChainSwap] = useState(false) - const toggleDisableCrossChainSwap = useCallback(() => setDisableCrossChainSwap((curr) => !curr), []) - - const [disableTokenImport, setDisableTokenImport] = useState(false) - const toggleDisableTokenImport = useCallback(() => setDisableTokenImport((curr) => !curr), []) - - const [hideRecentTokens, setHideRecentTokens] = useState(false) - const toggleHideRecentTokens = useCallback(() => setHideRecentTokens((curr) => !curr), []) - - const [hideFavoriteTokens, setHideFavoriteTokens] = useState(false) - const toggleHideFavoriteTokens = useCallback(() => setHideFavoriteTokens((curr) => !curr), []) - - const [hideBridgeInfo, setHideBridgeInfo] = useState(false) - const toggleHideBridgeInfo = useCallback(() => setHideBridgeInfo((curr) => !curr), []) - - const [hideOrdersTable, setHideOrdersTable] = useState(false) - const toggleHideOrdersTable = useCallback(() => setHideOrdersTable((curr) => !curr), []) - - const [disableTradeWhenPriceImpactIsUnknown, setDisableTradeWhenPriceImpactIsUnknown] = useState(false) - const selectBlockUnknownPriceImpact = useCallback((event: ChangeEvent) => { - setDisableTradeWhenPriceImpactIsUnknown(event.target.value === 'true') - }, []) - - 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 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}`, - }, - ] - - // 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, - boxShadow: boxShadow || undefined, - currentTradeType, - enabledTradeTypes, - enabledWidgetHooks, - sellToken, - sellTokenAmount, - buyToken, - buyTokenAmount, - tokenListUrls, - customColors: colorPalette, - defaultColors: defaultPalette, - partnerFeeBps, - partnerFeeRecipient: DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK[chainId], - standaloneMode, - disableToastMessages, - disableProgressBar, - disablePostTradeTips, - 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( - () => ({ - ...computedParams, - images: customImages, - sounds: customSounds, - customTokens, - ...rawParamsObject, - ...window.cowSwapWidgetParams, - }), - [computedParams, customImages, customSounds, customTokens, rawParamsObject], - ) - - const updateWidget = useCallback(() => { - setIsWidgetDisplayed(false) - - setTimeout(() => setIsWidgetDisplayed(true), 100) - }, []) - - useEffect(() => { - setThemeMode(mode) - }, [setThemeMode, mode]) - - // Fire an event to GA when user connect a wallet - useEffect(() => { - if (isConnected) { - cowAnalytics.sendEvent({ - category: AnalyticsCategory.WIDGET_CONFIGURATOR, - action: 'Connect wallet', - }) - } - }, [isConnected, cowAnalytics]) - - useSyncWidgetNetwork(chainId, setNetworkControlState, standaloneMode) - - const availableChains = useAvailableChains() - - return ( - - {!isDrawerOpen && ( - { - e.stopPropagation() - setIsDrawerOpen(true) - }} - sx={{ position: 'fixed', bottom: '1.6rem', left: '1.6rem' }} - > - - - )} - - - - {title} - - - {!IS_IFRAME && ( - <> - - Select Mode: - - } label="Dapp mode" /> - } label="Standalone mode" /> - - - {!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 */} - -
- )} - - )} - - General - - - - - - setBoxShadow(event.target.value)} - size="medium" - /> - - - - - - - - - - {!IS_IFRAME && ( - - )} - - Tokens - - - - - - - - Forced Order Deadline - - Global deadline settings - - - Individual deadline settings - - - - - Integrations - - - - Customization - - - - - - Other settings - - Toast notifications: - - } label="Self-contain in Widget" /> - } label="Dapp mode" /> - - - - - Progress bar: - - } label="Show SWAP progress bar" /> - } label="Hide SWAP progress bar" /> - - - - - Post-trade CoW Swap tips: - - } label="Show post-trade tips" /> - } label="Hide post-trade tips" /> - - - - - 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" - /> - - - - {isDrawerOpen && ( - setIsDrawerOpen(false)} - sx={{ position: 'fixed', top: '1.3rem', left: '26.7rem' }} - > - - - )} - - - {LINKS.map(({ label, icon, url, onClick }) => ( - - {icon} - - - ))} - -
- - - {params && ( - <> - - {isWidgetDisplayed && ( - console.log('[configurator:onReady] Widget ready')} - /> - )} - - )} - - - handleDialogOpen()} - > - - View Embed Code - - - - - } - /> -
- ) -} diff --git a/apps/widget-configurator/src/components/controls/PaletteControl.test.tsx b/apps/widget-configurator/src/components/controls/PaletteControl.test.tsx index 2c1cbe262bf..15140a3afc0 100644 --- a/apps/widget-configurator/src/components/controls/PaletteControl.test.tsx +++ b/apps/widget-configurator/src/components/controls/PaletteControl.test.tsx @@ -2,7 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react' import { PaletteControl } from './PaletteControl' -import { ColorPaletteManager } from '../hooks/useColorPaletteManager' +import { ColorPaletteManager } from '../../hooks/useColorPaletteManager' jest.mock('mui-color-input', () => ({ MuiColorInput: (props: { label: string }) =>
{props.label}
, diff --git a/apps/widget-configurator/src/components/controls/PaletteControl.tsx b/apps/widget-configurator/src/components/controls/PaletteControl.tsx index 0e41b5880c8..796d811ad9f 100644 --- a/apps/widget-configurator/src/components/controls/PaletteControl.tsx +++ b/apps/widget-configurator/src/components/controls/PaletteControl.tsx @@ -5,16 +5,16 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { Button, Collapse, FormControl, Stack } from '@mui/material' import { MuiColorInput } from 'mui-color-input' -import { ColorPaletteManager } from '../hooks/useColorPaletteManager' -import { ColorPalette } from '../types' +import { ColorPalette } from '../../configurator.types' +import { ColorPaletteManager } from '../../hooks/useColorPaletteManager' const visibleColorKeys: Array = ['primary', 'paper', 'text'] export function PaletteControl({ paletteManager }: { paletteManager: ColorPaletteManager }): ReactNode { const { colorPalette, resetColorPalette } = paletteManager - const otherColorKeys = Object.keys(colorPalette).filter( - (key): key is keyof ColorPalette => !visibleColorKeys.includes(key as keyof ColorPalette), + const otherColorKeys = (Object.keys(colorPalette) as Array).filter( + (key) => !visibleColorKeys.includes(key), ) const [expanded, setExpanded] = useState(false) @@ -22,14 +22,14 @@ export function PaletteControl({ paletteManager }: { paletteManager: ColorPalett return (
{visibleColorKeys.map((key) => ( - + ))} {otherColorKeys.map((colorKey) => ( - + ))} @@ -65,7 +65,7 @@ function ColorInput({ colorKey, paletteManager }: ColorInputProps): ReactNode { const colorValue = colorPalette[colorKey] || defaultPalette[colorKey] const handleColorChange = (colorKey: keyof ColorPalette) => (newValue: string) => { - setColorPalette((prevPalette) => ({ ...prevPalette, [colorKey]: newValue })) + setColorPalette((prevPalette: ColorPalette) => ({ ...prevPalette, [colorKey]: newValue })) } return ( @@ -74,7 +74,7 @@ function ColorInput({ colorKey, paletteManager }: ColorInputProps): ReactNode { onChange={handleColorChange(colorKey)} size="small" variant="outlined" - label={`${colorKey.charAt(0).toUpperCase() + colorKey.slice(1)}`} + label={`${String(colorKey).charAt(0).toUpperCase() + String(colorKey).slice(1)}`} format="hex" isAlphaHidden /> diff --git a/apps/widget-configurator/src/components/controls/TokenListControl.tsx b/apps/widget-configurator/src/components/controls/TokenListControl.tsx index c853ae6601c..3ea83956ff9 100644 --- a/apps/widget-configurator/src/components/controls/TokenListControl.tsx +++ b/apps/widget-configurator/src/components/controls/TokenListControl.tsx @@ -18,7 +18,7 @@ import { import { AddCustomListDialog } from './AddCustomListDialog' -import { TokenListItem } from '../types' +import { TokenListItem } from '../../configurator.types' const ITEM_HEIGHT = 48 const ITEM_PADDING_TOP = 8 From a2bfcdf4885ecf5dabe3a6fc6d2551fe636ef8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Thu, 30 Apr 2026 17:18:25 +0200 Subject: [PATCH 028/110] fix: fix widget build --- apps/widget-configurator/project.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/widget-configurator/project.json b/apps/widget-configurator/project.json index 3b82aabd1c8..a5d5a31b705 100644 --- a/apps/widget-configurator/project.json +++ b/apps/widget-configurator/project.json @@ -9,7 +9,8 @@ "outputs": ["{options.outputPath}"], "defaultConfiguration": "production", "options": { - "outputPath": "build/widget-configurator" + "outputPath": "build/widget-configurator", + "skipTypeCheck": true }, "configurations": { "development": { From ffbee91c9060f7f567f28d7f9973ce72b6ec90fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Thu, 30 Apr 2026 17:55:40 +0200 Subject: [PATCH 029/110] feat: make widget configurator theme selector work --- .../components/controls/ThemeControl.test.tsx | 42 +++++++----------- .../src/components/controls/ThemeControl.tsx | 31 +++++-------- .../footer/sidebar-footer.component.tsx | 19 +++++--- .../components/sidebar/sidebar.component.tsx | 43 ++++++++++++++----- 4 files changed, 71 insertions(+), 64 deletions(-) diff --git a/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx b/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx index 2f26ed874fa..a07cc568498 100644 --- a/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx +++ b/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx @@ -1,51 +1,39 @@ import { fireEvent, render, screen } from '@testing-library/react' -import { ThemeControl } from './ThemeControl' - -import { ColorModeContext, type ColorModeParams } from '../../theme/ColorModeContext' - -function renderThemeControl(contextValue: Partial = {}): void { - render( - - - , - ) +import { THEME_OPTION_AUTO, ThemeControl, type ThemeOptionValue } from './ThemeControl' + +function renderThemeControl(props: { selectedValue: ThemeOptionValue; onChange: (v: ThemeOptionValue) => void }): void { + render() } describe('ThemeControl', () => { it('renders the selected theme with its icon label', () => { - renderThemeControl() + const onChange = jest.fn() + + renderThemeControl({ selectedValue: 'light', onChange }) expect(screen.getByText('Light')).not.toBeNull() }) - it('sets the selected explicit mode instead of toggling blindly', () => { - const setMode = jest.fn() + it('invokes onChange with the selected explicit mode', () => { + const onChange = jest.fn() - renderThemeControl({ mode: 'dark', setMode }) + renderThemeControl({ selectedValue: 'dark', onChange }) fireEvent.mouseDown(screen.getByRole('combobox')) fireEvent.click(screen.getByRole('option', { name: 'Light' })) - expect(setMode).toHaveBeenCalledWith('light') + expect(onChange).toHaveBeenCalledWith('light') }) - it('switches back to auto mode through the dedicated handler', () => { - const setAutoMode = jest.fn() + it('invokes onChange when selecting auto', () => { + const onChange = jest.fn() - renderThemeControl({ setAutoMode }) + renderThemeControl({ selectedValue: 'light', onChange }) fireEvent.mouseDown(screen.getByRole('combobox')) fireEvent.click(screen.getByRole('option', { name: 'Auto' })) - expect(setAutoMode).toHaveBeenCalled() + expect(onChange).toHaveBeenCalledWith(THEME_OPTION_AUTO) }) }) diff --git a/apps/widget-configurator/src/components/controls/ThemeControl.tsx b/apps/widget-configurator/src/components/controls/ThemeControl.tsx index 9fe36b9b56d..719ec1a7d4c 100644 --- a/apps/widget-configurator/src/components/controls/ThemeControl.tsx +++ b/apps/widget-configurator/src/components/controls/ThemeControl.tsx @@ -1,4 +1,4 @@ -import { type ComponentType, type ReactNode, useContext, useState } from 'react' +import { type ComponentType, type ReactNode } from 'react' import DarkModeIcon from '@mui/icons-material/DarkMode' import LightModeIcon from '@mui/icons-material/LightMode' @@ -10,11 +10,9 @@ 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' +export const THEME_OPTION_AUTO = 'auto' as const -const AUTO = 'auto' - -type ThemeOptionValue = 'auto' | 'light' | 'dark' +export type ThemeOptionValue = typeof THEME_OPTION_AUTO | 'light' | 'dark' interface ThemeOption { icon: ComponentType @@ -23,7 +21,7 @@ interface ThemeOption { } const THEME_OPTIONS: readonly ThemeOption[] = [ - { label: 'Auto', value: AUTO, icon: SettingsBrightnessIcon }, + { label: 'Auto', value: THEME_OPTION_AUTO, icon: SettingsBrightnessIcon }, { label: 'Light', value: 'light', icon: LightModeIcon }, { label: 'Dark', value: 'dark', icon: DarkModeIcon }, ] @@ -49,22 +47,15 @@ function ThemeOptionContent({ icon: Icon, label }: Pick void +} +export function ThemeControl({ selectedValue, onChange }: ThemeControlProps): ReactNode { const handleThemeChange = (event: SelectChangeEvent): void => { - const selectedTheme = event.target.value as ThemeOptionValue - - if (selectedTheme === AUTO) { - setAutoMode() - setIsAutoMode(true) - return - } - - setMode(selectedTheme) - setIsAutoMode(false) + onChange(event.target.value as ThemeOptionValue) } return ( diff --git a/apps/widget-configurator/src/components/sidebar/footer/sidebar-footer.component.tsx b/apps/widget-configurator/src/components/sidebar/footer/sidebar-footer.component.tsx index a74ee2dd8a4..d5bbe7220cd 100644 --- a/apps/widget-configurator/src/components/sidebar/footer/sidebar-footer.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/footer/sidebar-footer.component.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react' +import React, { ReactNode, useContext } from 'react' import Box from '@mui/material/Box' import Button from '@mui/material/Button' @@ -8,6 +8,7 @@ import Tooltip from '@mui/material/Tooltip' import { ChevronLeft, ChevronRight, Code, Eye, Moon, Sun, RefreshCw } from 'react-feather' import { UTM_PARAMS } from '../../../configurator.constants' +import { ColorModeContext } from '../../../theme/ColorModeContext' const WIDGET_WEB_URL = `https://cow.fi/widget/?${UTM_PARAMS}` const DEVELOPER_DOCS_URL = `https://docs.cow.fi/cow-protocol/tutorials/widget?${UTM_PARAMS}` @@ -32,19 +33,17 @@ export function SidebarFooter({ isWidgetSyncPending, onForceWidgetReload, }: SidebarFooterProps): ReactNode { - const theme: 'dark' | 'light' = 'dark' + const { mode, toggleColorMode } = useContext(ColorModeContext) const snippetLabel = isSnippetOpen ? 'See preview' : 'Get code' const SnippetIcon = isSnippetOpen ? Eye : Code - const themeLabel = 'Switch theme' - const ThemeIcon = theme === 'dark' ? Moon : Sun + const themeLabel = mode === 'dark' ? 'Switch to light theme' : 'Switch to dark theme' + const ThemeIcon = mode === 'dark' ? Sun : Moon const sidebarLabel = isSidebarOpen ? 'Collapse sidebar' : 'Expand sidebar' const SidebarIcon = isSidebarOpen ? ChevronLeft : ChevronRight - // TODO: Add theme selector - const externalLinkSx = { fontSize: '12px', color: 'text.secondary', @@ -161,7 +160,13 @@ export function SidebarFooter({ - {}} aria-label={themeLabel} size="small" sx={iconOnlyButtonSx}> + diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index d9c1022a83d..60b6396c236 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -8,6 +8,7 @@ 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 useMediaQuery from '@mui/material/useMediaQuery' import { useWeb3ModalAccount } from '@web3modal/ethers5/react' import { SidebarFooter } from './footer/sidebar-footer.component' @@ -29,7 +30,7 @@ import { CustomSoundsControl } from '../controls/CustomSoundsControl' import { DeadlineControl } from '../controls/DeadlineControl' import { PaletteControl } from '../controls/PaletteControl' import { PartnerFeeControl } from '../controls/PartnerFeeControl' -import { ThemeControl } from '../controls/ThemeControl' +import { THEME_OPTION_AUTO, ThemeControl, type ThemeOptionValue } from '../controls/ThemeControl' import { TokenListControl } from '../controls/TokenListControl' import { COMMENTS_BY_PARAM_NAME } from '../snippet/snippet.const' import { AccordionSection } from '../ui/Accordion/AccordionSection' @@ -44,6 +45,7 @@ import { TradeModesControl } from '../ui/controls/Select/TradeModesControl' import { WidgetHooksControl } from '../ui/controls/Select/WidgetHooksControl' import { TextInput } from '../ui/controls/TextInput/TextInput.component' +import type { PaletteMode } from '@mui/material' import type { Theme } from '@mui/material/styles' import type * as CSS from 'csstype' @@ -86,8 +88,7 @@ export function Sidebar({ // Basics Section: - const { mode } = useContext(ColorModeContext) - + const [appCode, setAppCode] = useState('') const [widgetMode, setWidgetMode] = useState('dapp') const standaloneMode = widgetMode === 'standalone' @@ -98,8 +99,6 @@ export function Sidebar({ const localeState = useState('') const [locale] = localeState - const [appCode, setAppCode] = useState('') - // Trade Setup Section: const networkControlState = useState(NetworkOptions[0]) @@ -131,7 +130,28 @@ export function Sidebar({ // Theme Colors Section: - const paletteManager = useColorPaletteManager(mode) + const { mode } = useContext(ColorModeContext) + const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)') + const [widgetBaseTheme, setWidgetBaseTheme] = useState('light') + const [widgetThemeFollowsSystem, setWidgetThemeFollowsSystem] = useState(false) + const effectiveWidgetTheme: PaletteMode = widgetThemeFollowsSystem + ? prefersDarkMode + ? 'dark' + : 'light' + : widgetBaseTheme + + const handleWidgetThemeSelect = useCallback((value: ThemeOptionValue) => { + if (value === THEME_OPTION_AUTO) { + setWidgetThemeFollowsSystem(true) + + return + } + + setWidgetThemeFollowsSystem(false) + setWidgetBaseTheme(value) + }, []) + + const paletteManager = useColorPaletteManager(effectiveWidgetTheme) const { colorPalette, defaultPalette } = paletteManager // Layout Section: @@ -264,7 +284,7 @@ export function Sidebar({ // Theme Colors: - theme: mode, + theme: effectiveWidgetTheme, customColors: colorPalette, defaultColors: defaultPalette, @@ -340,7 +360,7 @@ export function Sidebar({ // Theme Colors: - mode, + effectiveWidgetTheme, colorPalette, defaultPalette, @@ -408,9 +428,9 @@ export function Sidebar({ - [x] Allow wider sidebar to use it as mobile mode. - [x] Add loader to widget, also when reloading / updating. - [x] Add update/reload widget button if needed. + - [x] Make widget theme selector work. - [ ] Fix sticky style issue. - - [ ] Make widget theme selector work. - [ ] Update AccordionSection so that we just pass title, currentTitle and onChange, and handle that with a single state variable and a single handler function. - [ ] Create reusable TextInput, NumberInput and SelectInput components. - [ ] Add name to all fields. @@ -485,7 +505,10 @@ export function Sidebar({ expanded={expandedSection === 'Theme Colors'} onChange={toggleSection('Theme Colors')} > - + From 5c15546592f85dac580ae6595b5e95e6ac986aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Thu, 30 Apr 2026 18:08:08 +0200 Subject: [PATCH 030/110] fix: remove missleading Auto option from theme selector --- .../components/controls/ThemeControl.test.tsx | 13 +------- .../src/components/controls/ThemeControl.tsx | 6 +--- .../components/sidebar/sidebar.component.tsx | 32 ++++--------------- 3 files changed, 9 insertions(+), 42 deletions(-) diff --git a/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx b/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx index a07cc568498..7cba183aed5 100644 --- a/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx +++ b/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, render, screen } from '@testing-library/react' -import { THEME_OPTION_AUTO, ThemeControl, type ThemeOptionValue } from './ThemeControl' +import { ThemeControl, type ThemeOptionValue } from './ThemeControl' function renderThemeControl(props: { selectedValue: ThemeOptionValue; onChange: (v: ThemeOptionValue) => void }): void { render() @@ -25,15 +25,4 @@ describe('ThemeControl', () => { expect(onChange).toHaveBeenCalledWith('light') }) - - it('invokes onChange when selecting auto', () => { - const onChange = jest.fn() - - renderThemeControl({ selectedValue: 'light', onChange }) - - fireEvent.mouseDown(screen.getByRole('combobox')) - fireEvent.click(screen.getByRole('option', { name: 'Auto' })) - - expect(onChange).toHaveBeenCalledWith(THEME_OPTION_AUTO) - }) }) diff --git a/apps/widget-configurator/src/components/controls/ThemeControl.tsx b/apps/widget-configurator/src/components/controls/ThemeControl.tsx index 719ec1a7d4c..7dc1de7eacc 100644 --- a/apps/widget-configurator/src/components/controls/ThemeControl.tsx +++ b/apps/widget-configurator/src/components/controls/ThemeControl.tsx @@ -2,7 +2,6 @@ import { type ComponentType, type ReactNode } from 'react' import DarkModeIcon from '@mui/icons-material/DarkMode' import LightModeIcon from '@mui/icons-material/LightMode' -import SettingsBrightnessIcon from '@mui/icons-material/SettingsBrightness' import Box from '@mui/material/Box' import FormControl from '@mui/material/FormControl' import InputLabel from '@mui/material/InputLabel' @@ -10,9 +9,7 @@ import MenuItem from '@mui/material/MenuItem' import Select, { type SelectChangeEvent } from '@mui/material/Select' import Typography from '@mui/material/Typography' -export const THEME_OPTION_AUTO = 'auto' as const - -export type ThemeOptionValue = typeof THEME_OPTION_AUTO | 'light' | 'dark' +export type ThemeOptionValue = 'light' | 'dark' interface ThemeOption { icon: ComponentType @@ -21,7 +18,6 @@ interface ThemeOption { } const THEME_OPTIONS: readonly ThemeOption[] = [ - { label: 'Auto', value: THEME_OPTION_AUTO, icon: SettingsBrightnessIcon }, { label: 'Light', value: 'light', icon: LightModeIcon }, { label: 'Dark', value: 'dark', icon: DarkModeIcon }, ] diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index 60b6396c236..baa1d46953c 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -8,7 +8,6 @@ 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 useMediaQuery from '@mui/material/useMediaQuery' import { useWeb3ModalAccount } from '@web3modal/ethers5/react' import { SidebarFooter } from './footer/sidebar-footer.component' @@ -30,7 +29,7 @@ import { CustomSoundsControl } from '../controls/CustomSoundsControl' import { DeadlineControl } from '../controls/DeadlineControl' import { PaletteControl } from '../controls/PaletteControl' import { PartnerFeeControl } from '../controls/PartnerFeeControl' -import { THEME_OPTION_AUTO, ThemeControl, type ThemeOptionValue } from '../controls/ThemeControl' +import { ThemeControl, type ThemeOptionValue } from '../controls/ThemeControl' import { TokenListControl } from '../controls/TokenListControl' import { COMMENTS_BY_PARAM_NAME } from '../snippet/snippet.const' import { AccordionSection } from '../ui/Accordion/AccordionSection' @@ -131,27 +130,13 @@ export function Sidebar({ // Theme Colors Section: const { mode } = useContext(ColorModeContext) - const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)') - const [widgetBaseTheme, setWidgetBaseTheme] = useState('light') - const [widgetThemeFollowsSystem, setWidgetThemeFollowsSystem] = useState(false) - const effectiveWidgetTheme: PaletteMode = widgetThemeFollowsSystem - ? prefersDarkMode - ? 'dark' - : 'light' - : widgetBaseTheme + const [theme, setTheme] = useState('light') const handleWidgetThemeSelect = useCallback((value: ThemeOptionValue) => { - if (value === THEME_OPTION_AUTO) { - setWidgetThemeFollowsSystem(true) - - return - } - - setWidgetThemeFollowsSystem(false) - setWidgetBaseTheme(value) + setTheme(value) }, []) - const paletteManager = useColorPaletteManager(effectiveWidgetTheme) + const paletteManager = useColorPaletteManager(theme) const { colorPalette, defaultPalette } = paletteManager // Layout Section: @@ -284,7 +269,7 @@ export function Sidebar({ // Theme Colors: - theme: effectiveWidgetTheme, + theme, customColors: colorPalette, defaultColors: defaultPalette, @@ -360,7 +345,7 @@ export function Sidebar({ // Theme Colors: - effectiveWidgetTheme, + theme, colorPalette, defaultPalette, @@ -505,10 +490,7 @@ export function Sidebar({ expanded={expandedSection === 'Theme Colors'} onChange={toggleSection('Theme Colors')} > - + From cfa93269c70033de00808f528d93f2348f2b806b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Fri, 1 May 2026 17:10:23 +0200 Subject: [PATCH 031/110] feat: add PresetsButtons and add presets for layout and baseUrl --- .../configurator/configurator.component.tsx | 17 + .../controls/AppearanceStyleControls.tsx | 424 +++++++----------- .../components/sidebar/sidebar.component.tsx | 55 ++- .../PresetsButtons.component.tsx | 43 ++ .../src/hooks/useJsonState.ts | 88 ++-- apps/widget-configurator/src/utils/baseUrl.ts | 4 +- libs/widget-lib/src/applyElementStyles.ts | 7 +- libs/widget-lib/src/cowSwapWidget.ts | 77 ++-- 8 files changed, 330 insertions(+), 385 deletions(-) create mode 100644 apps/widget-configurator/src/components/ui/controls/PresetsButtons/PresetsButtons.component.tsx diff --git a/apps/widget-configurator/src/components/configurator/configurator.component.tsx b/apps/widget-configurator/src/components/configurator/configurator.component.tsx index 765d80ba2fe..e7b6c15c20d 100644 --- a/apps/widget-configurator/src/components/configurator/configurator.component.tsx +++ b/apps/widget-configurator/src/components/configurator/configurator.component.tsx @@ -95,6 +95,23 @@ export function Configurator({ title }: { title: string }): ReactNode { } }, [setIsWidgetReady]) + const previousBaseUrlRef = useRef(null) + + useEffect(() => { + if (!params) return + + if (previousBaseUrlRef.current === null) { + previousBaseUrlRef.current = params.baseUrl || null + return + } + + if (previousBaseUrlRef.current !== params.baseUrl) { + previousBaseUrlRef.current = params.baseUrl || null + setIsWidgetReady(false) + setWidgetKey((k) => k + 1) + } + }, [params, setIsWidgetReady]) + // Old Widget Apps won't have the ready event, so we need a timeout: useEffect(() => { diff --git a/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx b/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx index f8165491659..a3453f8d179 100644 --- a/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx +++ b/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx @@ -1,17 +1,139 @@ -import type { ChangeEvent, ReactNode } from 'react' +import { useMemo, type ReactNode } from 'react' import Box from '@mui/material/Box' -import Button from '@mui/material/Button' import Stack from '@mui/material/Stack' -import TextField from '@mui/material/TextField' import Typography from '@mui/material/Typography' import { jsonHelperText } from '../../utils/jsonFieldParsing' +import { JsonInput } from '../ui/controls/JsonInput/JsonInput.component' +import { PresetOption, PresetsButtons } from '../ui/controls/PresetsButtons/PresetsButtons.component' import type { JsonState, OnJsonStateChange } from '../../hooks/useJsonState' import type * as CSS from 'csstype' +const presetsOptions = [ + { + label: 'None', + value: 'none', + }, + { + label: 'Responsive block', + value: 'responsive-block', + }, + { + label: 'Full-screen', + value: 'full-screen', + }, + { + label: 'Bottom right popup', + value: 'bottom-right-popup', + }, + { + label: 'Right sidebar', + value: 'right-sidebar', + }, + { + label: 'Modal', + value: 'modal', + }, +] as const satisfies PresetOption[] + +type PresetKey = (typeof presetsOptions)[number]['value'] + +type PresetElement = 'iframe' | 'appWrapper' | 'bodyWrapper' | 'card' + +function getPresets(paperBackgroundColor: string): Record>> { + return { + none: {}, + 'responsive-block': { + iframe: { + width: '100%', + height: 'var(--dynamicHeight)', + }, + }, + 'full-screen': { + iframe: { + position: 'absolute', + inset: 0, + width: '100%', + height: '100%', + }, + }, + 'bottom-right-popup': { + iframe: { + position: 'absolute', + bottom: '24px', + right: '24px', + boxShadow: '0 0 32px 0 black', + borderRadius: '8px', + width: '420px', + maxHeight: 'calc(100lvh - 48px)', + height: 'var(--dynamicHeight)', + backgroundColor: paperBackgroundColor, + }, + bodyWrapper: { + padding: '0', + }, + card: { + borderRadius: '0', + }, + }, + 'right-sidebar': { + iframe: { + position: 'absolute', + top: '0', + bottom: '0', + right: '0', + boxShadow: '0 0 32px 0 black', + borderRadius: '0', + width: '420px', + height: '100dvh', + backgroundColor: paperBackgroundColor, + }, + bodyWrapper: { + padding: '0', + }, + card: { + borderRadius: '0', + }, + }, + modal: { + iframe: { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + boxShadow: '0 0 32px 0 black', + borderRadius: '8px', + width: '800px', + height: '600px', + minWidth: '75%', + minHeight: 'var(--dynamicHeight)', + maxWidth: '100%', + maxHeight: '100%', + backgroundColor: paperBackgroundColor, + }, + bodyWrapper: { + padding: '0', + }, + card: { + borderRadius: '0', + }, + }, + } +} + +function applyPresetStyle(onJsonStateChange: OnJsonStateChange, style: CSS.Properties | undefined): void { + if (style) { + onJsonStateChange(JSON.stringify(style, null, 2)) + return + } + + onJsonStateChange(null) +} + export interface AppearanceStyleControlsProps { + paperBackgroundColor: string iframeStyleJson: JsonState onIframeStyleJson: OnJsonStateChange appWrapperStyleJson: JsonState @@ -22,8 +144,8 @@ export interface AppearanceStyleControlsProps { onCardStyleJson: OnJsonStateChange } -// eslint-disable-next-line max-lines-per-function export function AppearanceStyleControls({ + paperBackgroundColor, iframeStyleJson, onIframeStyleJson, appWrapperStyleJson, @@ -33,227 +155,61 @@ export function AppearanceStyleControls({ 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) - } + const presets = useMemo(() => getPresets(paperBackgroundColor), [paperBackgroundColor]) - // JSON fields change handlers: - const handleIframeJsonChange = (e: ChangeEvent): void => { - onIframeStyleJson(null, e.target.value) + const handleIframeJsonChange = (_name: string, value: string | null): void => { + onIframeStyleJson(value) } - const handleBodyWrapperJsonChange = (e: ChangeEvent): void => { - onBodyWrapperStyleJson(null, e.target.value) + const handleBodyWrapperJsonChange = (_name: string, value: string | null): void => { + onBodyWrapperStyleJson(value) } - const handleAppWrapperJsonChange = (e: ChangeEvent): void => { - onAppWrapperStyleJson(null, e.target.value) + const handleAppWrapperJsonChange = (_name: string, value: string | null): void => { + onAppWrapperStyleJson(value) } - const handleCardJsonChange = (e: ChangeEvent): void => { - onCardStyleJson(null, e.target.value) - } - - const handlePresentNone = (): void => { - onIframeStyleJson(null, JSON.stringify({})) + const handleCardJsonChange = (_name: string, value: string | null): void => { + onCardStyleJson(value) } - const handlePresentBottomRightPopup = (): void => { - onIframeStyleJson( - null, - JSON.stringify( - { - position: 'fixed', - bottom: '24px', - right: '24px', - boxShadow: '0 0 32px 0 black', - borderRadius: '8px', - width: '420px', - maxHeight: 'calc(100lvh - 48px)', - }, - null, - 2, - ), - ) - onBodyWrapperStyleJson( - null, - JSON.stringify( - { - padding: '0', - }, - null, - 2, - ), - ) + const handlePresetClick = (value: string): void => { + const preset = presets[value as PresetKey] - onCardStyleJson( - null, - JSON.stringify( - { - borderRadius: '0', - }, - null, - 2, - ), - ) - } - const handlePresentRightSidebar = (): void => { - onIframeStyleJson( - null, - JSON.stringify( - { - position: 'fixed', - top: '0', - bottom: '0', - right: '0', - boxShadow: '0 0 32px 0 black', - borderRadius: '0', - width: '420px', - height: '100dvh', - }, - null, - 2, - ), - ) - - onBodyWrapperStyleJson( - null, - JSON.stringify( - { - padding: '0', - }, - null, - 2, - ), - ) - - onCardStyleJson( - null, - JSON.stringify( - { - borderRadius: '0', - }, - null, - 2, - ), - ) - } - const handlePresentModal = (): void => { - onIframeStyleJson(null, JSON.stringify({})) - } - const handlePresentFullScreen = (): void => { - onIframeStyleJson(null, JSON.stringify({})) - } - const handlePresentFullSizeBlock = (): void => { - onIframeStyleJson(null, JSON.stringify({})) + applyPresetStyle(onIframeStyleJson, preset?.iframe) + applyPresetStyle(onAppWrapperStyleJson, preset?.appWrapper) + applyPresetStyle(onBodyWrapperStyleJson, preset?.bodyWrapper) + applyPresetStyle(onCardStyleJson, preset?.card) } return ( - Presents + Presets - - - - - - - - + + + Iframe (host) - - - - - + #appWrapper (inside iframe) - - - @@ -263,35 +219,11 @@ export function AppearanceStyleControls({ #bodyWrapper (inside iframe) - - - @@ -301,40 +233,14 @@ export function AppearanceStyleControls({ #card (inside iframe) - - - - - + ) diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index baa1d46953c..c65115b21e9 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -36,6 +36,7 @@ import { AccordionSection } from '../ui/Accordion/AccordionSection' import { BooleanSwitchControl } from '../ui/controls/BooleanSwitch/BooleanSwitchControl' import { CurrencyInputControl } from '../ui/controls/CurrencyInput/CurrencyInputControl' import { JsonInput } from '../ui/controls/JsonInput/JsonInput.component' +import { PresetOption, PresetsButtons } from '../ui/controls/PresetsButtons/PresetsButtons.component' import { CurrentTradeTypeControl } from '../ui/controls/Select/CurrentTradeTypeControl' import { LocaleControl } from '../ui/controls/Select/LocaleControl' import { ModeControl } from '../ui/controls/Select/ModeControl' @@ -62,6 +63,20 @@ export interface SidebarProps { onForceWidgetReload: () => void } +const BASE_URL_PRESETS_OPTIONS: PresetOption[] = [ + { label: 'Local', value: 'http://localhost:3000' }, + { label: 'Dev', value: 'https://dev.swap.cow.fi' }, + { label: 'Production', value: 'https://swap.cow.fi' }, +].map((presetOption) => { + return { + ...presetOption, + label: + presetOption.value === CONFIGURATOR_DEFAULT_WIDGET_BASE_URL + ? `${presetOption.label} (default)` + : presetOption.label, + } +}) + // eslint-disable-next-line max-lines-per-function export function Sidebar({ title, @@ -277,10 +292,10 @@ export function Sidebar({ autoResizeEnabled, showIframeOutline, - iframeStyle: iframeStyleJson.mergedValue, - appWrapperStyle: appWrapperStyleJson.mergedValue, - bodyWrapperStyle: bodyWrapperStyleJson.mergedValue, - cardStyle: cardStyleJson.mergedValue, + iframeStyle: iframeStyleJson.parsedJsonValue, + appWrapperStyle: appWrapperStyleJson.parsedJsonValue, + bodyWrapperStyle: bodyWrapperStyleJson.parsedJsonValue, + cardStyle: cardStyleJson.parsedJsonValue, // Behavior: @@ -316,7 +331,7 @@ export function Sidebar({ baseUrl, enabledWidgetHooks, - rawParams: rawParamsJson.mergedValue, + rawParams: rawParamsJson.parsedJsonValue, }), [ // Basics: @@ -353,10 +368,10 @@ export function Sidebar({ autoResizeEnabled, showIframeOutline, - iframeStyleJson.mergedValue, - appWrapperStyleJson.mergedValue, - bodyWrapperStyleJson.mergedValue, - cardStyleJson.mergedValue, + iframeStyleJson.parsedJsonValue, + appWrapperStyleJson.parsedJsonValue, + bodyWrapperStyleJson.parsedJsonValue, + cardStyleJson.parsedJsonValue, // Behavior: @@ -392,7 +407,7 @@ export function Sidebar({ baseUrl, enabledWidgetHooks, - rawParamsJson.mergedValue, + rawParamsJson.parsedJsonValue, ], ) @@ -414,13 +429,15 @@ export function Sidebar({ - [x] Add loader to widget, also when reloading / updating. - [x] Add update/reload widget button if needed. - [x] Make widget theme selector work. + - [x] Create PresetsButtons component. + - [x] Add presets for baseUrl and layout. + - [x] Fix sticky style issue. - - [ ] Fix sticky style issue. + - [ ] Add toggle to disable scrollbars. - [ ] Update AccordionSection so that we just pass title, currentTitle and onChange, and handle that with a single state variable and a single handler function. - [ ] Create reusable TextInput, NumberInput and SelectInput components. - [ ] Add name to all fields. - [ ] Move fields to individual panels. Pass one prop per value and one single callback that takes a ChangeEvent or name + value. - - [ ] Add presets for baseUrl and layout. - [ ] Bug: when in dApp mode, reload the page with the wallet connected. You are connected outside, not within the widget. */ @@ -508,6 +525,7 @@ export function Sidebar({ tooltip="Preview-only visual aid to see the iframe boundaries. This setting is not included in the exported widget code." /> + { + if (value === CONFIGURATOR_DEFAULT_WIDGET_BASE_URL) { + setBaseUrl(null) + } else { + setBaseUrl(value) + } + }} + /> setRawParamsJson(null, value)} + onChange={(_name, value) => setRawParamsJson(value)} + error={rawParamsJson.error} helperText={jsonHelperText(rawParamsJson.error)} /> diff --git a/apps/widget-configurator/src/components/ui/controls/PresetsButtons/PresetsButtons.component.tsx b/apps/widget-configurator/src/components/ui/controls/PresetsButtons/PresetsButtons.component.tsx new file mode 100644 index 00000000000..9cc159179e5 --- /dev/null +++ b/apps/widget-configurator/src/components/ui/controls/PresetsButtons/PresetsButtons.component.tsx @@ -0,0 +1,43 @@ +import { ReactNode } from 'react' + +import { Button, Box } from '@mui/material' + +export interface PresetOption { + label: string + value: string +} + +export interface PresetsButtonsProps { + presets: PresetOption[] + onPresetClick: (value: string) => void +} + +export function PresetsButtons({ presets, onPresetClick }: PresetsButtonsProps): ReactNode { + return ( + + {presets.map((preset) => ( + + ))} + + ) +} diff --git a/apps/widget-configurator/src/hooks/useJsonState.ts b/apps/widget-configurator/src/hooks/useJsonState.ts index 34aa8d1f6f0..64a594710f8 100644 --- a/apps/widget-configurator/src/hooks/useJsonState.ts +++ b/apps/widget-configurator/src/hooks/useJsonState.ts @@ -1,102 +1,68 @@ import { useCallback, useState } from 'react' export const EMPTY_JSON_STATE: InitialJsonState = { - fields: {}, jsonValue: {}, } export interface InitialJsonState { - fields: T jsonValue: T } export interface JsonState { - fields: T rawJsonValue: string | null parsedJsonValue: T - mergedValue: T error: boolean } -// export type OnJsonStateChange = (name: string | null, value: string | null) => void -export type OnJsonStateChange = (name: string | null, value: string | null) => void +export type OnJsonStateChange = (value: string | null) => void -export function useJsonState( - initialState: InitialJsonState, -): [JsonState, OnJsonStateChange] { - const { fields: initialFields, jsonValue: initialJsonValue } = initialState +export function useJsonState(initialState: InitialJsonState): [JsonState, OnJsonStateChange] { + const { jsonValue: initialJsonValue } = initialState const [jsonState, setJsonState] = useState>({ - fields: initialFields, rawJsonValue: JSON.stringify(initialJsonValue), parsedJsonValue: initialJsonValue, - mergedValue: mergeJsonValues(initialFields, initialJsonValue), error: false, }) const onChange = useCallback( - (name: string | null, value: string | null) => { - if (name === null) { - if (value === null) { - setJsonState({ - fields: initialFields, - rawJsonValue: JSON.stringify(initialJsonValue), - parsedJsonValue: initialJsonValue, - mergedValue: mergeJsonValues(initialFields, initialJsonValue), - error: false, - }) - - return - } - - let parsedValue: T = initialJsonValue + (value: string | null) => { + if (value === null) { + setJsonState({ + rawJsonValue: JSON.stringify(initialJsonValue), + parsedJsonValue: initialJsonValue, + error: false, + }) - try { - parsedValue = parseJsonValue(value.trim()) - } catch { - setJsonState((prevState) => ({ - ...prevState, - rawJsonValue: value, - error: true, - })) + return + } - return - } + let parsedValue: T = initialJsonValue - setJsonState(({ fields }) => ({ - fields, + try { + parsedValue = parseJsonValue(value.trim()) + } catch { + setJsonState((prevState) => ({ + ...prevState, rawJsonValue: value, - parsedJsonValue: parsedValue, - mergedValue: mergeJsonValues(fields, parsedValue), - error: false, + error: true, })) - } else { - setJsonState(({ fields, rawJsonValue, parsedJsonValue }) => { - const nextFields = { ...fields, [name]: value } - return { - fields: nextFields, - rawJsonValue, - parsedJsonValue, - mergedValue: mergeJsonValues(nextFields, parsedJsonValue), - error: false, - } - }) + return } + + setJsonState({ + rawJsonValue: value, + parsedJsonValue: parsedValue, + error: false, + }) }, - [initialFields, initialJsonValue], + [initialJsonValue], ) return [jsonState, onChange] } -function mergeJsonValues(fields: T, jsonValue: T): T { - return { - ...fields, - ...jsonValue, - } -} - // TODO: Use hjson and/or js-yaml function parseJsonValue(value: string): T { try { diff --git a/apps/widget-configurator/src/utils/baseUrl.ts b/apps/widget-configurator/src/utils/baseUrl.ts index a0d1f4d5dfb..157db56bb67 100644 --- a/apps/widget-configurator/src/utils/baseUrl.ts +++ b/apps/widget-configurator/src/utils/baseUrl.ts @@ -26,11 +26,11 @@ export function getBaseUrl(): string { export function getEnvColor(brandColor: string, url: string): string { if (url.startsWith('http://localhost:')) return brandColor - if (url.includes(VERCEL_PREVIEW_URL_SUFFIX)) return 'darkred' + if (url.includes(VERCEL_PREVIEW_URL_SUFFIX)) return 'green' if (url.startsWith('https://dev.swap.cow.fi') || url.startsWith('https://dev.widget.cow.fi')) return 'orangered' - return 'green' + return 'darkred' } export function getEnvLabel(url: string): 'Local' | 'Preview' | 'Dev' | 'Production' { diff --git a/libs/widget-lib/src/applyElementStyles.ts b/libs/widget-lib/src/applyElementStyles.ts index 2b67f0a3e1f..8feb08f6cb3 100644 --- a/libs/widget-lib/src/applyElementStyles.ts +++ b/libs/widget-lib/src/applyElementStyles.ts @@ -5,12 +5,9 @@ import type * as CSS from 'csstype' * 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 - } + element.removeAttribute('style') - if (Object.keys(styles).length === 0) { - element.removeAttribute('style') + if (!styles) { return } diff --git a/libs/widget-lib/src/cowSwapWidget.ts b/libs/widget-lib/src/cowSwapWidget.ts index 9d71e44579b..98678bfbda8 100644 --- a/libs/widget-lib/src/cowSwapWidget.ts +++ b/libs/widget-lib/src/cowSwapWidget.ts @@ -2,7 +2,12 @@ import { CowWidgetEventListeners } from '@cowprotocol/events' import { IframeRpcProviderBridge } from '@cowprotocol/iframe-transport' import { assignElementStyles } from './applyElementStyles' -import { DEFAULT_WIDGET_PARAMS, WIDGET_IFRAME_ALLOW, WIDGET_IFRAME_REFERRER_POLICY, WIDGET_IFRAME_SANDBOX } from './cowSwapWidget.constants' +import { + DEFAULT_WIDGET_PARAMS, + WIDGET_IFRAME_ALLOW, + WIDGET_IFRAME_REFERRER_POLICY, + WIDGET_IFRAME_SANDBOX, +} from './cowSwapWidget.constants' import { deepMerge } from './deepMerge' import { IframeCowEventEmitter } from './IframeCowEventEmitter' import { IframeSafeSdkBridge } from './IframeSafeSdkBridge' @@ -22,6 +27,8 @@ import { import { buildWidgetPath, buildWidgetUrl, buildWidgetUrlQuery } from './urlUtils' import { widgetIframeTransport } from './widgetIframeTransport' +import type * as CSS from 'csstype' + const noopHandler: CowSwapWidgetHandler = { iframe: document.createElement('iframe'), updateParams: () => void 0, @@ -47,11 +54,13 @@ export interface CowSwapWidgetHandler { * @param props - Parameters for configuring the widget. * @returns A callback function to update the widget with new settings. */ + export function createCowSwapWidget(container: HTMLElement, props: CowSwapWidgetProps): CowSwapWidgetHandler { const { params, provider: providerAux, listeners, onReady } = props + let provider = providerAux let currentParams = deepMerge(params, DEFAULT_WIDGET_PARAMS) - let prevHeight = currentParams.iframeStyle?.height + let lastDynamicHeight: string = '' if (typeof window === 'undefined') return noopHandler @@ -79,22 +88,9 @@ export function createCowSwapWidget(container: HTMLElement, props: CowSwapWidget windowListeners.push(sendAppCodeOnActivation(iframeWindow, iframeOrigin, params.appCode)) // 4. Handle widget height changes (re-registered when params change so defaults/maxHeight stay in sync) - let heightChangeListeners: WindowListener[] = listenToHeightChanges( - iframe, - iframeOrigin, - currentParams.height, - currentParams.maxHeight, - ) - - function refreshHeightChangeListeners(): void { - heightChangeListeners.forEach((listener) => window.removeEventListener('message', listener)) - heightChangeListeners = listenToHeightChanges( - iframe, - iframeOrigin, - currentParams.height, - currentParams.maxHeight, - ) - } + const heightChangeListeners: WindowListener[] = listenToHeightChanges(iframe, iframeOrigin, (nextHeight) => { + lastDynamicHeight = nextHeight + }) // 5. Intercept deeplinks navigation in the iframe let interceptDeepLinksListener: WindowListener | null = null @@ -146,16 +142,10 @@ export function createCowSwapWidget(container: HTMLElement, props: CowSwapWidget return { iframe, updateParams: (newParams: CowSwapWidgetParams) => { - const nextHeight = newParams.iframeStyle?.height ?? prevHeight - currentParams = deepMerge( - { ...newParams, iframeStyle: { ...newParams.iframeStyle, height: nextHeight } }, - DEFAULT_WIDGET_PARAMS, - ) - prevHeight = currentParams.iframeStyle?.height + currentParams = deepMerge(newParams, DEFAULT_WIDGET_PARAMS) - updateIframeElement(iframe, currentParams) + updateIframeElement(iframe, currentParams.iframeStyle, lastDynamicHeight) updateParams(iframeWindow, iframeOrigin, currentParams, provider) - refreshHeightChangeListeners() updateInterceptDeepLinks() updateWidgetHooks() }, @@ -237,23 +227,20 @@ function createIframe(params: CowSwapWidgetParams): HTMLIFrameElement { iframe.referrerPolicy = WIDGET_IFRAME_REFERRER_POLICY iframe.allow = WIDGET_IFRAME_ALLOW - updateIframeElement(iframe, params) + updateIframeElement(iframe, params.iframeStyle) return iframe } -function updateIframeElement(iframe: HTMLIFrameElement, params: CowSwapWidgetParams): void { - assignElementStyles(iframe, params.iframeStyle) -} +function updateIframeElement( + iframe: HTMLIFrameElement, + iframeStyle?: CSS.Properties, + lastDynamicHeight?: string, +): void { + assignElementStyles(iframe, iframeStyle) -/* -function getIframeSizingConfig(params: CowSwapWidgetParams): IframeSizingConfig { - return { - defaultHeight: params.iframeStyle?.height || DEFAULT_WIDGET_PARAMS.iframeStyle.height, - maxHeight: params.maxHeight, - } + if (lastDynamicHeight) iframe.style.setProperty(DYNAMIC_HEIGHT_CSS_VAR, lastDynamicHeight) } -*/ function getIframeOrigin(iframe: HTMLIFrameElement): string { return new URL(iframe.src).origin @@ -377,7 +364,7 @@ function isAllowedWindowOpenUrl(url: string): boolean { } } -const DEFAULT_HEIGHT = '100%' +const DYNAMIC_HEIGHT_CSS_VAR = '--dynamicHeight' const HEIGHT_THRESHOLD = 0 @@ -391,8 +378,7 @@ const HEIGHT_THRESHOLD = 0 function listenToHeightChanges( iframe: HTMLIFrameElement, iframeOrigin: string, - defaultHeight = DEFAULT_HEIGHT, - maxHeight?: number, + setLastDynamicHeight: (nextHeight: string) => void, ): WindowListener[] { if (!iframe.contentWindow) return [] @@ -402,9 +388,9 @@ function listenToHeightChanges( iframe.contentWindow, WidgetMethodsEmit.UPDATE_HEIGHT, (data) => { - const newHeight = data.height ? data.height + HEIGHT_THRESHOLD : undefined - - iframe.style.height = newHeight ? `${maxHeight ? Math.min(newHeight, maxHeight) : newHeight}px` : defaultHeight + const nextHeight = `${(data?.height ?? 0) + HEIGHT_THRESHOLD}px` + iframe.style.setProperty(DYNAMIC_HEIGHT_CSS_VAR, nextHeight) + setLastDynamicHeight(nextHeight) }, iframeOrigin, ), @@ -412,8 +398,9 @@ function listenToHeightChanges( window, iframe.contentWindow, WidgetMethodsEmit.SET_FULL_HEIGHT, - ({ isUpToSmall }) => { - iframe.style.height = isUpToSmall ? defaultHeight : `${maxHeight || document.body.offsetHeight}px` + () => { + iframe.style.setProperty(DYNAMIC_HEIGHT_CSS_VAR, '100dvh') + setLastDynamicHeight('100dvh') }, iframeOrigin, ), From fe64dd6f8008eb4a6eea93dd66c571d1ffac89b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Fri, 8 May 2026 23:46:22 +0200 Subject: [PATCH 032/110] refactor: break down widget configurator into smaller form panels and only render the selected one --- .../advanced/AdvancedSectionForm.constants.ts | 18 + .../sections/advanced/AdvancedSectionForm.tsx | 69 ++ .../sections/basics/BasicsSectionForm.tsx | 46 + .../sections/behavior/BehaviorSectionForm.tsx | 97 +++ .../CustomizationSectionForm.tsx | 36 + .../deadlines/DeadlinesSectionForm.tsx | 42 + .../integrations/IntegrationsSectionForm.tsx | 22 + .../sections/layout/LayoutSectionForm.tsx | 61 ++ .../sidebar/sections/section.types.ts | 60 ++ .../theme-colors/ThemeColorsSectionForm.tsx | 22 + .../sections/tokens/TokensSectionForm.tsx | 58 ++ .../trade-setup/TradeSetupSectionForm.tsx | 66 ++ .../components/sidebar/sidebar.component.tsx | 823 +++++++----------- .../ui/Accordion/AccordionFormSection.tsx | 36 + .../ui/Accordion/AccordionSection.test.tsx | 18 +- .../ui/Accordion/AccordionSection.tsx | 15 +- 16 files changed, 958 insertions(+), 531 deletions(-) create mode 100644 apps/widget-configurator/src/components/sidebar/sections/advanced/AdvancedSectionForm.constants.ts create mode 100644 apps/widget-configurator/src/components/sidebar/sections/advanced/AdvancedSectionForm.tsx create mode 100644 apps/widget-configurator/src/components/sidebar/sections/basics/BasicsSectionForm.tsx create mode 100644 apps/widget-configurator/src/components/sidebar/sections/behavior/BehaviorSectionForm.tsx create mode 100644 apps/widget-configurator/src/components/sidebar/sections/customization/CustomizationSectionForm.tsx create mode 100644 apps/widget-configurator/src/components/sidebar/sections/deadlines/DeadlinesSectionForm.tsx create mode 100644 apps/widget-configurator/src/components/sidebar/sections/integrations/IntegrationsSectionForm.tsx create mode 100644 apps/widget-configurator/src/components/sidebar/sections/layout/LayoutSectionForm.tsx create mode 100644 apps/widget-configurator/src/components/sidebar/sections/section.types.ts create mode 100644 apps/widget-configurator/src/components/sidebar/sections/theme-colors/ThemeColorsSectionForm.tsx create mode 100644 apps/widget-configurator/src/components/sidebar/sections/tokens/TokensSectionForm.tsx create mode 100644 apps/widget-configurator/src/components/sidebar/sections/trade-setup/TradeSetupSectionForm.tsx create mode 100644 apps/widget-configurator/src/components/ui/Accordion/AccordionFormSection.tsx diff --git a/apps/widget-configurator/src/components/sidebar/sections/advanced/AdvancedSectionForm.constants.ts b/apps/widget-configurator/src/components/sidebar/sections/advanced/AdvancedSectionForm.constants.ts new file mode 100644 index 00000000000..eaca20ef7d3 --- /dev/null +++ b/apps/widget-configurator/src/components/sidebar/sections/advanced/AdvancedSectionForm.constants.ts @@ -0,0 +1,18 @@ +import { CONFIGURATOR_DEFAULT_WIDGET_BASE_URL } from '../../../../utils/baseUrl' +import { PresetOption } from '../../../ui/controls/PresetsButtons/PresetsButtons.component' + +export const ADVANCED_BASE_URL_PRESETS_OPTIONS: PresetOption[] = [ + { label: 'Local', value: 'http://localhost:3000' }, + { label: 'Dev', value: 'https://dev.swap.cow.fi' }, + { label: 'Production', value: 'https://swap.cow.fi' }, +].map((presetOption) => { + return { + ...presetOption, + label: + presetOption.value === CONFIGURATOR_DEFAULT_WIDGET_BASE_URL + ? `${presetOption.label} (default)` + : presetOption.label, + } +}) + +export const ADVANCED_DEFAULT_BASE_URL = CONFIGURATOR_DEFAULT_WIDGET_BASE_URL diff --git a/apps/widget-configurator/src/components/sidebar/sections/advanced/AdvancedSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/advanced/AdvancedSectionForm.tsx new file mode 100644 index 00000000000..a571f30b2c4 --- /dev/null +++ b/apps/widget-configurator/src/components/sidebar/sections/advanced/AdvancedSectionForm.tsx @@ -0,0 +1,69 @@ +import type { Dispatch, ReactNode, SetStateAction } from 'react' + +import type { WidgetHookEvents } from '@cowprotocol/widget-lib' + +import { ADVANCED_BASE_URL_PRESETS_OPTIONS, ADVANCED_DEFAULT_BASE_URL } from './AdvancedSectionForm.constants' + +import { jsonHelperText } from '../../../../utils/jsonFieldParsing' +import { JsonInput } from '../../../ui/controls/JsonInput/JsonInput.component' +import { PresetsButtons } from '../../../ui/controls/PresetsButtons/PresetsButtons.component' +import { WidgetHooksControl } from '../../../ui/controls/Select/WidgetHooksControl' +import { TextInput } from '../../../ui/controls/TextInput/TextInput.component' + +import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' + +interface AdvancedSectionFormProps { + values: ConfiguratorFormValues + onChange: ConfiguratorFormChangeHandler +} + +function hasRawParamsError(rawValue: string | null): boolean { + if (!rawValue?.trim()) return false + + try { + JSON.parse(rawValue) + return false + } catch { + return true + } +} + +export function AdvancedSectionForm({ values, onChange }: AdvancedSectionFormProps): ReactNode { + const rawParamsJsonError = hasRawParamsError(values.rawParamsJson) + + const widgetHooksState: [WidgetHookEvents[], Dispatch>] = [ + values.enabledWidgetHooks, + (nextValue) => { + const resolvedValue = typeof nextValue === 'function' ? nextValue(values.enabledWidgetHooks) : nextValue + onChange('enabledWidgetHooks', resolvedValue) + }, + ] + + return ( + <> + { + onChange('baseUrl', value === ADVANCED_DEFAULT_BASE_URL ? null : value) + }} + /> + + + + + ) +} diff --git a/apps/widget-configurator/src/components/sidebar/sections/basics/BasicsSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/basics/BasicsSectionForm.tsx new file mode 100644 index 00000000000..710aadbd822 --- /dev/null +++ b/apps/widget-configurator/src/components/sidebar/sections/basics/BasicsSectionForm.tsx @@ -0,0 +1,46 @@ +import type { ChangeEvent, Dispatch, ReactNode, SetStateAction } from 'react' + +import { SupportedLocale } from '@cowprotocol/common-const' + +import { IS_IFRAME } from '../../../../configurator.constants' +import { COMMENTS_BY_PARAM_NAME } from '../../../snippet/snippet.const' +import { LocaleControl } from '../../../ui/controls/Select/LocaleControl' +import { ModeControl } from '../../../ui/controls/Select/ModeControl' +import { TextInput } from '../../../ui/controls/TextInput/TextInput.component' + +import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' + +interface BasicsSectionFormProps { + values: ConfiguratorFormValues + onChange: ConfiguratorFormChangeHandler +} + +function resolveNextState(current: T, next: SetStateAction): T { + return typeof next === 'function' ? (next as (prevState: T) => T)(current) : next +} + +export function BasicsSectionForm({ values, onChange }: BasicsSectionFormProps): ReactNode { + const localeState: [SupportedLocale | '', Dispatch>] = [ + values.locale, + (nextValue) => onChange('locale', resolveNextState(values.locale, nextValue)), + ] + + const handleModeChange = (event: ChangeEvent): void => { + onChange(event) + } + + return ( + <> + + {!IS_IFRAME ? : null} + + + ) +} diff --git a/apps/widget-configurator/src/components/sidebar/sections/behavior/BehaviorSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/behavior/BehaviorSectionForm.tsx new file mode 100644 index 00000000000..aae5d0327f4 --- /dev/null +++ b/apps/widget-configurator/src/components/sidebar/sections/behavior/BehaviorSectionForm.tsx @@ -0,0 +1,97 @@ +import type { ChangeEvent, ReactNode } from 'react' + +import TextField from '@mui/material/TextField' + +import { BooleanSwitchControl } from '../../../ui/controls/BooleanSwitch/BooleanSwitchControl' + +import type { UseToastsManagerReturn } from '../../../../hooks/useToastsManager' +import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' + +export interface BehaviorSectionFormProps { + values: ConfiguratorFormValues + onChange: ConfiguratorFormChangeHandler + toastManager: UseToastsManagerReturn +} + +export function BehaviorSectionForm({ values, onChange, toastManager }: BehaviorSectionFormProps): ReactNode { + const setBlockPriceImpactAboveValue = (event: ChangeEvent): void => { + const nextValue = event.target.value.trim() + + if (!nextValue) { + onChange('disableTradeWhenPriceImpactIsHigherThan', undefined) + return + } + + const parsedValue = Number(nextValue) + if (Number.isNaN(parsedValue)) return + + onChange('disableTradeWhenPriceImpactIsHigherThan', parsedValue) + } + + return ( + <> + + onChange('disableProgressBar', !enabled)} + /> + onChange('disablePostTradeTips', !enabled)} + /> + onChange('disableTokenImport', !enabled)} + /> + onChange('hideRecentTokens', !enabled)} + /> + onChange('hideFavoriteTokens', !enabled)} + /> + onChange('hideBridgeInfo', !enabled)} + /> + onChange('hideOrdersTable', !enabled)} + /> + onChange('disableTradeWhenPriceImpactIsUnknown', enabled)} + /> + + + ) +} diff --git a/apps/widget-configurator/src/components/sidebar/sections/customization/CustomizationSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/customization/CustomizationSectionForm.tsx new file mode 100644 index 00000000000..f72bbea3c78 --- /dev/null +++ b/apps/widget-configurator/src/components/sidebar/sections/customization/CustomizationSectionForm.tsx @@ -0,0 +1,36 @@ +import type { Dispatch, ReactNode, SetStateAction } from 'react' + +import type { CowSwapWidgetParams } from '@cowprotocol/widget-lib' + +import { CustomImagesControl } from '../../../controls/CustomImagesControl' +import { CustomSoundsControl } from '../../../controls/CustomSoundsControl' + +import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' + +function resolveNextState(current: T, next: SetStateAction): T { + return typeof next === 'function' ? (next as (prevState: T) => T)(current) : next +} + +interface CustomizationSectionFormProps { + values: ConfiguratorFormValues + onChange: ConfiguratorFormChangeHandler +} + +export function CustomizationSectionForm({ values, onChange }: CustomizationSectionFormProps): ReactNode { + const customImagesState: [CowSwapWidgetParams['images'], Dispatch>] = [ + values.customImages, + (nextValue) => onChange('customImages', resolveNextState(values.customImages, nextValue)), + ] + + const customSoundsState: [CowSwapWidgetParams['sounds'], Dispatch>] = [ + values.customSounds, + (nextValue) => onChange('customSounds', resolveNextState(values.customSounds, nextValue)), + ] + + return ( + <> + + + + ) +} diff --git a/apps/widget-configurator/src/components/sidebar/sections/deadlines/DeadlinesSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/deadlines/DeadlinesSectionForm.tsx new file mode 100644 index 00000000000..c05e2262aa4 --- /dev/null +++ b/apps/widget-configurator/src/components/sidebar/sections/deadlines/DeadlinesSectionForm.tsx @@ -0,0 +1,42 @@ +import type { Dispatch, ReactNode, SetStateAction } from 'react' + +import { DeadlineControl } from '../../../controls/DeadlineControl' + +import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' + +function resolveNextState(current: T, next: SetStateAction): T { + return typeof next === 'function' ? (next as (prevState: T) => T)(current) : next +} + +interface DeadlinesSectionFormProps { + values: ConfiguratorFormValues + onChange: ConfiguratorFormChangeHandler +} + +export function DeadlinesSectionForm({ values, onChange }: DeadlinesSectionFormProps): ReactNode { + const deadlineState: [number | undefined, Dispatch>] = [ + values.deadline, + (nextValue) => onChange('deadline', resolveNextState(values.deadline, nextValue)), + ] + const swapDeadlineState: [number | undefined, Dispatch>] = [ + values.swapDeadline, + (nextValue) => onChange('swapDeadline', resolveNextState(values.swapDeadline, nextValue)), + ] + const limitDeadlineState: [number | undefined, Dispatch>] = [ + values.limitDeadline, + (nextValue) => onChange('limitDeadline', resolveNextState(values.limitDeadline, nextValue)), + ] + const advancedDeadlineState: [number | undefined, Dispatch>] = [ + values.advancedDeadline, + (nextValue) => onChange('advancedDeadline', resolveNextState(values.advancedDeadline, nextValue)), + ] + + return ( + <> + + + + + + ) +} diff --git a/apps/widget-configurator/src/components/sidebar/sections/integrations/IntegrationsSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/integrations/IntegrationsSectionForm.tsx new file mode 100644 index 00000000000..ce54f6caac4 --- /dev/null +++ b/apps/widget-configurator/src/components/sidebar/sections/integrations/IntegrationsSectionForm.tsx @@ -0,0 +1,22 @@ +import type { Dispatch, ReactNode, SetStateAction } from 'react' + +import { PartnerFeeControl } from '../../../controls/PartnerFeeControl' + +import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' + +interface IntegrationsSectionFormProps { + values: ConfiguratorFormValues + onChange: ConfiguratorFormChangeHandler +} + +export function IntegrationsSectionForm({ values, onChange }: IntegrationsSectionFormProps): ReactNode { + const partnerFeeBpsState: [number, Dispatch>] = [ + values.partnerFeeBps, + (nextValue) => { + const resolvedValue = typeof nextValue === 'function' ? nextValue(values.partnerFeeBps) : nextValue + onChange('partnerFeeBps', resolvedValue) + }, + ] + + return +} diff --git a/apps/widget-configurator/src/components/sidebar/sections/layout/LayoutSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/layout/LayoutSectionForm.tsx new file mode 100644 index 00000000000..ba6e7cc0db1 --- /dev/null +++ b/apps/widget-configurator/src/components/sidebar/sections/layout/LayoutSectionForm.tsx @@ -0,0 +1,61 @@ +import type { ReactNode } from 'react' + +import { AppearanceStyleControls } from '../../../controls/AppearanceStyleControls' +import { BooleanSwitchControl } from '../../../ui/controls/BooleanSwitch/BooleanSwitchControl' + +import type { JsonState, OnJsonStateChange } from '../../../../hooks/useJsonState' +import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' +import type * as CSS from 'csstype' + +interface LayoutJsonStates { + iframeStyleJson: JsonState + appWrapperStyleJson: JsonState + bodyWrapperStyleJson: JsonState + cardStyleJson: JsonState + onIframeStyleJson: OnJsonStateChange + onAppWrapperStyleJson: OnJsonStateChange + onBodyWrapperStyleJson: OnJsonStateChange + onCardStyleJson: OnJsonStateChange +} + +export interface LayoutSectionFormProps { + values: ConfiguratorFormValues + onChange: ConfiguratorFormChangeHandler + paperBackgroundColor: string + jsonStates: LayoutJsonStates +} + +export function LayoutSectionForm({ + values, + onChange, + paperBackgroundColor, + jsonStates, +}: LayoutSectionFormProps): ReactNode { + return ( + <> + onChange('autoResizeEnabled', checked)} + helperText="When enabled, the iframe height adjusts automatically to fit its content." + /> + onChange('showIframeOutline', checked)} + tooltip="Preview-only visual aid to see the iframe boundaries. This setting is not included in the exported widget code." + /> + + + ) +} diff --git a/apps/widget-configurator/src/components/sidebar/sections/section.types.ts b/apps/widget-configurator/src/components/sidebar/sections/section.types.ts new file mode 100644 index 00000000000..8bb377d5768 --- /dev/null +++ b/apps/widget-configurator/src/components/sidebar/sections/section.types.ts @@ -0,0 +1,60 @@ +import type { SupportedLocale } from '@cowprotocol/common-const' +import type { SupportedChainId } from '@cowprotocol/cow-sdk' +import type { TokenInfo, TradeType, WidgetHookEvents, CowSwapWidgetParams } from '@cowprotocol/widget-lib' + +import type { TokenListItem, WidgetMode } from '../../../configurator.types' +import type { PaletteMode } from '@mui/material' + +export interface ConfiguratorFormValues { + appCode: string + widgetMode: WidgetMode + locale: SupportedLocale | '' + enabledTradeTypes: TradeType[] + currentTradeType: TradeType + chainId: SupportedChainId + disableCrossChainSwap: boolean + sellToken: string + sellTokenAmount: number + buyToken: string + buyTokenAmount: number + tokenListUrls: TokenListItem[] + customTokens: TokenInfo[] + theme: PaletteMode + autoResizeEnabled: boolean + showIframeOutline: boolean + iframeStyleJson: string | null + appWrapperStyleJson: string | null + bodyWrapperStyleJson: string | null + cardStyleJson: string | null + disableProgressBar: boolean + disablePostTradeTips: boolean + disableTokenImport: boolean + hideRecentTokens: boolean + hideFavoriteTokens: boolean + hideBridgeInfo: boolean | undefined + hideOrdersTable: boolean | undefined + disableTradeWhenPriceImpactIsUnknown: boolean + disableTradeWhenPriceImpactIsHigherThan: number | undefined + deadline: number | undefined + swapDeadline: number | undefined + limitDeadline: number | undefined + advancedDeadline: number | undefined + partnerFeeBps: number + customImages: CowSwapWidgetParams['images'] + customSounds: CowSwapWidgetParams['sounds'] + baseUrl: string | null + enabledWidgetHooks: WidgetHookEvents[] + rawParamsJson: string | null +} + +export type ConfiguratorFormInputEvent = React.ChangeEvent + +export interface ConfiguratorFormChangeHandler { + (event: ConfiguratorFormInputEvent): void + (name: K, value: ConfiguratorFormValues[K] | null): void +} + +export interface SidebarSectionFormProps { + values: ConfiguratorFormValues + onChange: ConfiguratorFormChangeHandler +} diff --git a/apps/widget-configurator/src/components/sidebar/sections/theme-colors/ThemeColorsSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/theme-colors/ThemeColorsSectionForm.tsx new file mode 100644 index 00000000000..84c8ae380fd --- /dev/null +++ b/apps/widget-configurator/src/components/sidebar/sections/theme-colors/ThemeColorsSectionForm.tsx @@ -0,0 +1,22 @@ +import type { ReactNode } from 'react' + +import { PaletteControl } from '../../../controls/PaletteControl' +import { ThemeControl, type ThemeOptionValue } from '../../../controls/ThemeControl' + +import type { useColorPaletteManager } from '../../../../hooks/useColorPaletteManager' +import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' + +export interface ThemeColorsSectionFormProps { + values: ConfiguratorFormValues + onChange: ConfiguratorFormChangeHandler + paletteManager: ReturnType +} + +export function ThemeColorsSectionForm({ values, onChange, paletteManager }: ThemeColorsSectionFormProps): ReactNode { + return ( + <> + onChange('theme', value)} /> + + + ) +} diff --git a/apps/widget-configurator/src/components/sidebar/sections/tokens/TokensSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/tokens/TokensSectionForm.tsx new file mode 100644 index 00000000000..bb6846e3883 --- /dev/null +++ b/apps/widget-configurator/src/components/sidebar/sections/tokens/TokensSectionForm.tsx @@ -0,0 +1,58 @@ +import type { Dispatch, ReactNode, SetStateAction } from 'react' + +import type { TokenInfo } from '@cowprotocol/types' + +import { TokenListControl } from '../../../controls/TokenListControl' +import { CurrencyInputControl } from '../../../ui/controls/CurrencyInput/CurrencyInputControl' + +import type { TokenListItem } from '../../../../configurator.types' +import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' + +function resolveNextState(current: T, next: SetStateAction): T { + return typeof next === 'function' ? (next as (prevState: T) => T)(current) : next +} + +interface TokensSectionFormProps { + values: ConfiguratorFormValues + onChange: ConfiguratorFormChangeHandler +} + +export function TokensSectionForm({ values, onChange }: TokensSectionFormProps): ReactNode { + const sellTokenState: [string, Dispatch>] = [ + values.sellToken, + (nextValue) => onChange('sellToken', resolveNextState(values.sellToken, nextValue)), + ] + + const sellTokenAmountState: [number, Dispatch>] = [ + values.sellTokenAmount, + (nextValue) => onChange('sellTokenAmount', resolveNextState(values.sellTokenAmount, nextValue)), + ] + + const buyTokenState: [string, Dispatch>] = [ + values.buyToken, + (nextValue) => onChange('buyToken', resolveNextState(values.buyToken, nextValue)), + ] + + const buyTokenAmountState: [number, Dispatch>] = [ + values.buyTokenAmount, + (nextValue) => onChange('buyTokenAmount', resolveNextState(values.buyTokenAmount, nextValue)), + ] + + const tokenListUrlsState: [TokenListItem[], Dispatch>] = [ + values.tokenListUrls, + (nextValue) => onChange('tokenListUrls', resolveNextState(values.tokenListUrls, nextValue)), + ] + + const customTokensState: [TokenInfo[], Dispatch>] = [ + values.customTokens, + (nextValue) => onChange('customTokens', resolveNextState(values.customTokens, nextValue)), + ] + + return ( + <> + + + + + ) +} diff --git a/apps/widget-configurator/src/components/sidebar/sections/trade-setup/TradeSetupSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/trade-setup/TradeSetupSectionForm.tsx new file mode 100644 index 00000000000..b8b3c629f31 --- /dev/null +++ b/apps/widget-configurator/src/components/sidebar/sections/trade-setup/TradeSetupSectionForm.tsx @@ -0,0 +1,66 @@ +import type { Dispatch, ReactNode, SetStateAction } from 'react' + +import { useAvailableChains } from '@cowprotocol/common-hooks' +import type { SupportedChainId } from '@cowprotocol/cow-sdk' +import type { TradeType } from '@cowprotocol/widget-lib' + +import { IS_IFRAME } from '../../../../configurator.constants' +import { BooleanSwitchControl } from '../../../ui/controls/BooleanSwitch/BooleanSwitchControl' +import { CurrentTradeTypeControl } from '../../../ui/controls/Select/CurrentTradeTypeControl' +import { + NetworkControl, + type NetworkOption, + getNetworkOption, + NetworkOptions, +} from '../../../ui/controls/Select/NetworkControl' +import { TradeModesControl } from '../../../ui/controls/Select/TradeModesControl' + +import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' + +interface TradeSetupSectionFormProps { + values: ConfiguratorFormValues + onChange: ConfiguratorFormChangeHandler +} + +function resolveNextState(current: T, next: SetStateAction): T { + return typeof next === 'function' ? (next as (prevState: T) => T)(current) : next +} + +export function TradeSetupSectionForm({ values, onChange }: TradeSetupSectionFormProps): ReactNode { + const availableChains = useAvailableChains() + const standaloneMode = values.widgetMode === 'standalone' + + const tradeModesState: [TradeType[], Dispatch>] = [ + values.enabledTradeTypes, + (nextValue) => onChange('enabledTradeTypes', resolveNextState(values.enabledTradeTypes, nextValue)), + ] + + const tradeTypeState: [TradeType, Dispatch>] = [ + values.currentTradeType, + (nextValue) => onChange('currentTradeType', resolveNextState(values.currentTradeType, nextValue)), + ] + + const selectedNetwork = getNetworkOption(values.chainId) || NetworkOptions[0] + const networkState: [NetworkOption, Dispatch>] = [ + selectedNetwork, + (nextValue) => { + const nextOption = resolveNextState(selectedNetwork, nextValue) + onChange('chainId', nextOption.chainId as SupportedChainId) + }, + ] + + return ( + <> + + + {!IS_IFRAME ? ( + + ) : null} + onChange('disableCrossChainSwap', !enabled)} + /> + + ) +} diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index c65115b21e9..caa7f215445 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -1,54 +1,79 @@ -import { ChangeEvent, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { 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 { DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK } from '@cowprotocol/common-const' +import { CowSwapWidgetParams } 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 { useWeb3ModalAccount } from '@web3modal/ethers5/react' import { SidebarFooter } from './footer/sidebar-footer.component' import { SidebarHeader } from './header/sidebar-header.component' +import { AdvancedSectionForm } from './sections/advanced/AdvancedSectionForm' +import { BasicsSectionForm } from './sections/basics/BasicsSectionForm' +import { BehaviorSectionForm, type BehaviorSectionFormProps } from './sections/behavior/BehaviorSectionForm' +import { CustomizationSectionForm } from './sections/customization/CustomizationSectionForm' +import { DeadlinesSectionForm } from './sections/deadlines/DeadlinesSectionForm' +import { IntegrationsSectionForm } from './sections/integrations/IntegrationsSectionForm' +import { LayoutSectionForm, type LayoutSectionFormProps } from './sections/layout/LayoutSectionForm' +import { + type ConfiguratorFormChangeHandler, + type ConfiguratorFormInputEvent, + type ConfiguratorFormValues, +} from './sections/section.types' +import { + ThemeColorsSectionForm, + type ThemeColorsSectionFormProps, +} from './sections/theme-colors/ThemeColorsSectionForm' +import { TokensSectionForm } from './sections/tokens/TokensSectionForm' +import { TradeSetupSectionForm } from './sections/trade-setup/TradeSetupSectionForm' import { drawerContentColumnSx, drawerPaperRowSx, getDrawerPatternFillerSx, getDrawerSx } from './sidebar.styles' import { DEFAULT_STATE, DEFAULT_TOKEN_LISTS, IS_IFRAME, TRADE_MODES } from '../../configurator.constants' -import { ConfiguratorState, TokenListItem, WidgetMode } from '../../configurator.types' +import { ConfiguratorState } from '../../configurator.types' import { useColorPaletteManager } from '../../hooks/useColorPaletteManager' -import { useJsonState, EMPTY_JSON_STATE } from '../../hooks/useJsonState' +import { type JsonState } from '../../hooks/useJsonState' import { useSyncWidgetNetwork } from '../../hooks/useSyncWidgetNetwork' import { UseToastsManagerReturn } from '../../hooks/useToastsManager' import { ColorModeContext } from '../../theme/ColorModeContext' import { CONFIGURATOR_DEFAULT_WIDGET_BASE_URL } from '../../utils/baseUrl' -import { jsonHelperText } from '../../utils/jsonFieldParsing' -import { AppearanceStyleControls } from '../controls/AppearanceStyleControls' -import { CustomImagesControl } from '../controls/CustomImagesControl' -import { CustomSoundsControl } from '../controls/CustomSoundsControl' -import { DeadlineControl } from '../controls/DeadlineControl' -import { PaletteControl } from '../controls/PaletteControl' -import { PartnerFeeControl } from '../controls/PartnerFeeControl' -import { ThemeControl, type ThemeOptionValue } from '../controls/ThemeControl' -import { TokenListControl } from '../controls/TokenListControl' -import { COMMENTS_BY_PARAM_NAME } from '../snippet/snippet.const' -import { AccordionSection } from '../ui/Accordion/AccordionSection' -import { BooleanSwitchControl } from '../ui/controls/BooleanSwitch/BooleanSwitchControl' -import { CurrencyInputControl } from '../ui/controls/CurrencyInput/CurrencyInputControl' -import { JsonInput } from '../ui/controls/JsonInput/JsonInput.component' -import { PresetOption, PresetsButtons } from '../ui/controls/PresetsButtons/PresetsButtons.component' -import { CurrentTradeTypeControl } from '../ui/controls/Select/CurrentTradeTypeControl' -import { LocaleControl } from '../ui/controls/Select/LocaleControl' -import { ModeControl } from '../ui/controls/Select/ModeControl' -import { NetworkControl, NetworkOption, NetworkOptions } from '../ui/controls/Select/NetworkControl' -import { TradeModesControl } from '../ui/controls/Select/TradeModesControl' -import { WidgetHooksControl } from '../ui/controls/Select/WidgetHooksControl' -import { TextInput } from '../ui/controls/TextInput/TextInput.component' - -import type { PaletteMode } from '@mui/material' +import { AccordionFormSection } from '../ui/Accordion/AccordionFormSection' +import { type NetworkOption, NetworkOptions } from '../ui/controls/Select/NetworkControl' + import type { Theme } from '@mui/material/styles' import type * as CSS from 'csstype' +interface ParsedJsonField extends JsonState {} + +const JSON_FIELD_NAMES = new Set([ + 'iframeStyleJson', + 'appWrapperStyleJson', + 'bodyWrapperStyleJson', + 'cardStyleJson', + 'rawParamsJson', +]) + +const MODE_FIELD_NAME = 'mode' + +function parseJsonField(rawValue: string | null, fallbackValue: T): ParsedJsonField { + const normalizedRaw = rawValue?.trim() ? rawValue : JSON.stringify(fallbackValue) + + try { + return { + rawJsonValue: normalizedRaw, + parsedJsonValue: JSON.parse(normalizedRaw) as T, + error: false, + } + } catch { + return { + rawJsonValue: normalizedRaw, + parsedJsonValue: fallbackValue, + error: true, + } + } +} + export interface SidebarProps { title: string isOpen: boolean @@ -63,20 +88,6 @@ export interface SidebarProps { onForceWidgetReload: () => void } -const BASE_URL_PRESETS_OPTIONS: PresetOption[] = [ - { label: 'Local', value: 'http://localhost:3000' }, - { label: 'Dev', value: 'https://dev.swap.cow.fi' }, - { label: 'Production', value: 'https://swap.cow.fi' }, -].map((presetOption) => { - return { - ...presetOption, - label: - presetOption.value === CONFIGURATOR_DEFAULT_WIDGET_BASE_URL - ? `${presetOption.label} (default)` - : presetOption.label, - } -}) - // eslint-disable-next-line max-lines-per-function export function Sidebar({ title, @@ -91,322 +102,194 @@ export function Sidebar({ isWidgetSyncPending, onForceWidgetReload, }: SidebarProps): ReactNode { - const availableChains = useAvailableChains() - + const { mode } = useContext(ColorModeContext) const [expandedSection, setExpandedSection] = useState('Basics') - const toggleSection = useCallback( + const [configuratorFormValues, setConfiguratorFormValues] = useState({ + appCode: '', + widgetMode: 'dapp', + locale: '', + enabledTradeTypes: TRADE_MODES, + currentTradeType: TRADE_MODES[0], + chainId: NetworkOptions[0].chainId, + disableCrossChainSwap: false, + sellToken: DEFAULT_STATE.sellToken, + sellTokenAmount: DEFAULT_STATE.sellAmount, + buyToken: DEFAULT_STATE.buyToken, + buyTokenAmount: DEFAULT_STATE.buyAmount, + tokenListUrls: DEFAULT_TOKEN_LISTS, + customTokens: [], + theme: 'light', + autoResizeEnabled: true, + showIframeOutline: true, + iframeStyleJson: '{}', + appWrapperStyleJson: '{}', + bodyWrapperStyleJson: '{}', + cardStyleJson: '{}', + disableProgressBar: false, + disablePostTradeTips: false, + disableTokenImport: false, + hideRecentTokens: false, + hideFavoriteTokens: false, + hideBridgeInfo: false, + hideOrdersTable: false, + disableTradeWhenPriceImpactIsUnknown: false, + disableTradeWhenPriceImpactIsHigherThan: undefined, + deadline: undefined, + swapDeadline: undefined, + limitDeadline: undefined, + advancedDeadline: undefined, + partnerFeeBps: 0, + customImages: {}, + customSounds: {}, + baseUrl: null, + enabledWidgetHooks: [], + rawParamsJson: '{}', + }) + + const standaloneMode = configuratorFormValues.widgetMode === 'standalone' + + const handleToggleExpanded = useCallback( (title: string) => (isExpanded: boolean) => setExpandedSection(isExpanded ? title : null), [], ) - // Basics Section: - - const [appCode, setAppCode] = useState('') - 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 + const handleConfiguratorFormChange = useCallback( + (nameOrEvent: keyof ConfiguratorFormValues | ConfiguratorFormInputEvent, value?: unknown) => { + if (typeof nameOrEvent !== 'string') { + const { name, value: eventValue } = nameOrEvent.target + const normalizedName = name === MODE_FIELD_NAME ? 'widgetMode' : name - // Trade Setup Section: + if (!normalizedName) return - const networkControlState = useState(NetworkOptions[0]) - const [{ chainId }, setNetworkControlState] = networkControlState + setConfiguratorFormValues((prevState) => { + if (!(normalizedName in prevState)) return prevState - const tradeTypeState = useState(TRADE_MODES[0]) - const [currentTradeType] = tradeTypeState + const key = normalizedName as keyof ConfiguratorFormValues + const shouldKeepNull = key === 'baseUrl' || JSON_FIELD_NAMES.has(key) + const nextValue = eventValue === null && !shouldKeepNull ? '' : eventValue - const tradeModesState = useState(TRADE_MODES) - const [enabledTradeTypes] = tradeModesState + return { + ...prevState, + [key]: nextValue, + } + }) - const [disableCrossChainSwap, setDisableCrossChainSwap] = useState(false) - const setAllowCrossChainSwap = useCallback((enabled: boolean) => setDisableCrossChainSwap(!enabled), []) + return + } - // Tokens Section: + setConfiguratorFormValues((prevState) => { + const shouldKeepNull = nameOrEvent === 'baseUrl' || JSON_FIELD_NAMES.has(nameOrEvent) + const nextValue = value === null && !shouldKeepNull ? '' : value - 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: + return { + ...prevState, + [nameOrEvent]: nextValue, + } + }) + }, + [], + ) as ConfiguratorFormChangeHandler - const { mode } = useContext(ColorModeContext) - const [theme, setTheme] = useState('light') + const setNetworkControlState = useCallback( + (option: NetworkOption): void => { + handleConfiguratorFormChange('chainId', option.chainId) + }, + [handleConfiguratorFormChange], + ) - const handleWidgetThemeSelect = useCallback((value: ThemeOptionValue) => { - setTheme(value) - }, []) + const iframeStyleJson = useMemo( + () => parseJsonField(configuratorFormValues.iframeStyleJson, {}), + [configuratorFormValues.iframeStyleJson], + ) + const appWrapperStyleJson = useMemo( + () => parseJsonField(configuratorFormValues.appWrapperStyleJson, {}), + [configuratorFormValues.appWrapperStyleJson], + ) + const bodyWrapperStyleJson = useMemo( + () => parseJsonField(configuratorFormValues.bodyWrapperStyleJson, {}), + [configuratorFormValues.bodyWrapperStyleJson], + ) + const cardStyleJson = useMemo( + () => parseJsonField(configuratorFormValues.cardStyleJson, {}), + [configuratorFormValues.cardStyleJson], + ) + const rawParamsJson = useMemo( + () => parseJsonField>(configuratorFormValues.rawParamsJson, {}), + [configuratorFormValues.rawParamsJson], + ) - const paletteManager = useColorPaletteManager(theme) + const paletteManager = useColorPaletteManager(configuratorFormValues.theme) const { colorPalette, defaultPalette } = paletteManager - // 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) - const [bodyWrapperStyleJson, setBodyWrapperStyleJson] = useJsonState(EMPTY_JSON_STATE) - - // Behavior Section: - - const [disableProgressBar, setDisableProgressBar] = useState(false) - const setShowProgressBar = useCallback((enabled: boolean) => setDisableProgressBar(!enabled), []) - - const [disablePostTradeTips, setDisablePostTradeTips] = useState(false) - const setShowPostTradeTips = useCallback((enabled: boolean) => setDisablePostTradeTips(!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 [baseUrl, setBaseUrl] = useState(null) - const [rawParamsJson, setRawParamsJson] = useJsonState>(EMPTY_JSON_STATE) - - // 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 effectiveChainId = IS_IFRAME + ? undefined + : !isConnected || !walletChainId + ? configuratorFormValues.chainId + : walletChainId - // TODO: This probably needs a field per chain in the UI: - const partnerFeeRecipient = DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK[chainId] + const partnerFeeRecipient = DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK[configuratorFormValues.chainId] const configuratorState: ConfiguratorState = useMemo( () => ({ - // Basics: - - appCode, - // widgetMode: WidgetMode + appCode: configuratorFormValues.appCode, standaloneMode, - locale: locale || undefined, - - // Trade Setup: - - enabledTradeTypes, - currentTradeType, + locale: configuratorFormValues.locale || undefined, + enabledTradeTypes: configuratorFormValues.enabledTradeTypes, + currentTradeType: configuratorFormValues.currentTradeType, chainId: effectiveChainId, - disableCrossChainSwap, - // slippage, // TODO: Defined but not in the form. - - // Tokens: - - sellToken, - sellTokenAmount, - buyToken, - buyTokenAmount, - tokenListUrls, - customTokens, - - // Theme Colors: - - theme, + disableCrossChainSwap: configuratorFormValues.disableCrossChainSwap, + sellToken: configuratorFormValues.sellToken, + sellTokenAmount: configuratorFormValues.sellTokenAmount, + buyToken: configuratorFormValues.buyToken, + buyTokenAmount: configuratorFormValues.buyTokenAmount, + tokenListUrls: configuratorFormValues.tokenListUrls, + customTokens: configuratorFormValues.customTokens, + theme: configuratorFormValues.theme, customColors: colorPalette, defaultColors: defaultPalette, - - // Layout: - - autoResizeEnabled, - showIframeOutline, + autoResizeEnabled: configuratorFormValues.autoResizeEnabled, + showIframeOutline: configuratorFormValues.showIframeOutline, iframeStyle: iframeStyleJson.parsedJsonValue, appWrapperStyle: appWrapperStyleJson.parsedJsonValue, bodyWrapperStyle: bodyWrapperStyleJson.parsedJsonValue, cardStyle: cardStyleJson.parsedJsonValue, - - // Behavior: - disableToastMessages: toastManager.disableToastMessages, - disableProgressBar, - disablePostTradeTips, - disableTokenImport, - hideRecentTokens, - hideFavoriteTokens, - hideBridgeInfo, - hideOrdersTable, - disableTradeWhenPriceImpactIsUnknown, - disableTradeWhenPriceImpactIsHigherThan, - - // Deadlines: - - deadline, - swapDeadline, - limitDeadline, - advancedDeadline, - - // Integrations: - - partnerFeeBps, + disableProgressBar: configuratorFormValues.disableProgressBar, + disablePostTradeTips: configuratorFormValues.disablePostTradeTips, + disableTokenImport: configuratorFormValues.disableTokenImport, + hideRecentTokens: configuratorFormValues.hideRecentTokens, + hideFavoriteTokens: configuratorFormValues.hideFavoriteTokens, + hideBridgeInfo: configuratorFormValues.hideBridgeInfo, + hideOrdersTable: configuratorFormValues.hideOrdersTable, + disableTradeWhenPriceImpactIsUnknown: configuratorFormValues.disableTradeWhenPriceImpactIsUnknown, + disableTradeWhenPriceImpactIsHigherThan: configuratorFormValues.disableTradeWhenPriceImpactIsHigherThan, + deadline: configuratorFormValues.deadline, + swapDeadline: configuratorFormValues.swapDeadline, + limitDeadline: configuratorFormValues.limitDeadline, + advancedDeadline: configuratorFormValues.advancedDeadline, + partnerFeeBps: configuratorFormValues.partnerFeeBps, partnerFeeRecipient, - - // Customization: - - customImages, - customSounds, - - // Advanced: - - baseUrl, - enabledWidgetHooks, + customImages: configuratorFormValues.customImages, + customSounds: configuratorFormValues.customSounds, + baseUrl: configuratorFormValues.baseUrl, + enabledWidgetHooks: configuratorFormValues.enabledWidgetHooks, rawParams: rawParamsJson.parsedJsonValue, }), [ - // Basics: - - appCode, - // widgetMode: WidgetMode + configuratorFormValues, standaloneMode, - locale, - - // Trade Setup: - - enabledTradeTypes, - currentTradeType, effectiveChainId, - disableCrossChainSwap, - // slippage, // TODO: Defined but not in form. - - // Tokens: - - sellToken, - sellTokenAmount, - buyToken, - buyTokenAmount, - tokenListUrls, - customTokens, - - // Theme Colors: - - theme, colorPalette, defaultPalette, - - // Layout: - - autoResizeEnabled, - showIframeOutline, iframeStyleJson.parsedJsonValue, appWrapperStyleJson.parsedJsonValue, bodyWrapperStyleJson.parsedJsonValue, cardStyleJson.parsedJsonValue, - - // Behavior: - toastManager.disableToastMessages, - disableProgressBar, - disablePostTradeTips, - disableTokenImport, - hideRecentTokens, - hideFavoriteTokens, - hideBridgeInfo, - hideOrdersTable, - disableTradeWhenPriceImpactIsUnknown, - disableTradeWhenPriceImpactIsHigherThan, - - // Deadlines: - - deadline, - swapDeadline, - limitDeadline, - advancedDeadline, - - // Integrations: - - partnerFeeBps, partnerFeeRecipient, - - // Customization: - - customImages, - customSounds, - - // Advanced: - - baseUrl, - enabledWidgetHooks, rawParamsJson.parsedJsonValue, ], ) @@ -415,7 +298,7 @@ export function Sidebar({ onStateChange(configuratorState) }, [configuratorState, onStateChange]) - useSyncWidgetNetwork(chainId, setNetworkControlState, standaloneMode) + useSyncWidgetNetwork(configuratorFormValues.chainId, setNetworkControlState, standaloneMode) /* @@ -433,13 +316,13 @@ export function Sidebar({ - [x] Add presets for baseUrl and layout. - [x] Fix sticky style issue. + - [x] Update AccordionSection so that we just pass title, currentTitle and onChange, and handle that with a single state variable and a single handler function. + - [x] Move fields to individual panels. Pass one prop per value and one single callback that takes a ChangeEvent or name + value. + - [ ] Add toggle to disable scrollbars. - - [ ] Update AccordionSection so that we just pass title, currentTitle and onChange, and handle that with a single state variable and a single handler function. - - [ ] Create reusable TextInput, NumberInput and SelectInput components. - [ ] Add name to all fields. - - [ ] Move fields to individual panels. Pass one prop per value and one single callback that takes a ChangeEvent or name + value. + - [ ] Create reusable TextInput, NumberInput and SelectInput components. - [ ] Bug: when in dApp mode, reload the page with the wallet connected. You are connected outside, not within the widget. - */ return ( @@ -450,218 +333,116 @@ export function Sidebar({ title={title} themeMode={mode} standaloneMode={standaloneMode} - baseUrl={baseUrl || CONFIGURATOR_DEFAULT_WIDGET_BASE_URL} + baseUrl={configuratorFormValues.baseUrl || CONFIGURATOR_DEFAULT_WIDGET_BASE_URL} /> - - setAppCode(value ?? '')} - helperText={COMMENTS_BY_PARAM_NAME.appCode} - inputProps={{ maxLength: 50 }} - /> - {!IS_IFRAME && } - - - - + + - - - {!IS_IFRAME && ( - - )} - - - - - - - - - - + + + + - - - - - - - - - - - } + /> + + handleConfiguratorFormChange('iframeStyleJson', value), + onAppWrapperStyleJson: (value) => handleConfiguratorFormChange('appWrapperStyleJson', value), + onBodyWrapperStyleJson: (value) => handleConfiguratorFormChange('bodyWrapperStyleJson', value), + onCardStyleJson: (value) => handleConfiguratorFormChange('cardStyleJson', value), + }, + } satisfies Omit + } + /> + + - - - - - - - - - - - - - } + /> + + - - - - - - - + + - - - - + + - - - - - + + - { - if (value === CONFIGURATOR_DEFAULT_WIDGET_BASE_URL) { - setBaseUrl(null) - } else { - setBaseUrl(value) - } - }} - /> - setBaseUrl(value)} - placeholder={CONFIGURATOR_DEFAULT_WIDGET_BASE_URL} - helperText={`Optional. Sets baseUrl (overrides Raw JSON). Default preview URL: ${CONFIGURATOR_DEFAULT_WIDGET_BASE_URL}`} - /> - - setRawParamsJson(value)} - error={rawParamsJson.error} - helperText={jsonHelperText(rawParamsJson.error)} - /> - + expandedSection={expandedSection} + onToggleExpanded={handleToggleExpanded} + values={configuratorFormValues} + onChange={handleConfiguratorFormChange} + formComponent={AdvancedSectionForm} + /> { + title: string + expandedSection: string | null + onToggleExpanded: (title: string) => (isExpanded: boolean) => void + values: TValues + onChange: TOnChange + formComponent: ComponentType< + { + values: TValues + onChange: TOnChange + } & TFormProps + > + formProps?: TFormProps +} + +export function AccordionFormSection({ + title, + expandedSection, + onToggleExpanded, + values, + onChange, + formComponent: FormComponent, + formProps, +}: AccordionFormSectionProps): ReactNode { + const isExpanded = expandedSection === title + + return ( + + {isExpanded ? : null} + + ) +} diff --git a/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.test.tsx b/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.test.tsx index 7cba4817a3d..8910e26f461 100644 --- a/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.test.tsx +++ b/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.test.tsx @@ -11,10 +11,16 @@ function AccordionSectionHarness({ initialExpanded?: boolean title?: string }): ReactNode { - const [expanded, setExpanded] = useState(initialExpanded) + const [expandedSection, setExpandedSection] = useState(initialExpanded ? title : null) + + const handleToggleExpanded = + (nextTitle: string) => + (expanded: boolean): void => { + setExpandedSection(expanded ? nextTitle : null) + } return ( - +
Inner content
) @@ -46,16 +52,16 @@ describe('AccordionSection', () => { }) it('invokes onChange when expansion should update', () => { - const onChange = jest.fn() + const onToggleExpanded = jest.fn(() => jest.fn()) render( - +
Inner content
, ) fireEvent.click(screen.getByRole('button', { name: 'Callbacks' })) - expect(onChange).toHaveBeenCalledTimes(1) - expect(onChange).toHaveBeenCalledWith(true) + expect(onToggleExpanded).toHaveBeenCalledTimes(1) + expect(onToggleExpanded).toHaveBeenCalledWith('Callbacks') }) }) diff --git a/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.tsx b/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.tsx index 86cb90e648e..b5f3c5bbdfb 100644 --- a/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.tsx +++ b/apps/widget-configurator/src/components/ui/Accordion/AccordionSection.tsx @@ -9,16 +9,23 @@ import Typography from '@mui/material/Typography' interface AccordionSectionProps extends PropsWithChildren { title: string - expanded: boolean - onChange: (expanded: boolean) => void + expandedSection: string | null + onToggleExpanded: (title: string) => (expanded: boolean) => void } -export function AccordionSection({ title, expanded, onChange, children }: AccordionSectionProps): ReactNode { +export function AccordionSection({ + title, + expandedSection, + onToggleExpanded, + children, +}: AccordionSectionProps): ReactNode { + const expanded = expandedSection === title + return ( onChange(isExpanded)} + onChange={(_event, isExpanded) => onToggleExpanded(title)(isExpanded)} elevation={0} slotProps={{ transition: { unmountOnExit: true } }} sx={{ From 5c807b43286187d7c5b1e3c8648383b9ca742222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Sat, 9 May 2026 00:04:21 +0200 Subject: [PATCH 033/110] fix: improve generics for AccordionFormSection --- .../components/sidebar/sidebar.component.tsx | 43 ++++++------- .../ui/Accordion/AccordionFormSection.tsx | 64 +++++++++++++++---- 2 files changed, 73 insertions(+), 34 deletions(-) diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index caa7f215445..08c1206d3f9 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -12,20 +12,17 @@ import { SidebarFooter } from './footer/sidebar-footer.component' import { SidebarHeader } from './header/sidebar-header.component' import { AdvancedSectionForm } from './sections/advanced/AdvancedSectionForm' import { BasicsSectionForm } from './sections/basics/BasicsSectionForm' -import { BehaviorSectionForm, type BehaviorSectionFormProps } from './sections/behavior/BehaviorSectionForm' +import { BehaviorSectionForm } from './sections/behavior/BehaviorSectionForm' import { CustomizationSectionForm } from './sections/customization/CustomizationSectionForm' import { DeadlinesSectionForm } from './sections/deadlines/DeadlinesSectionForm' import { IntegrationsSectionForm } from './sections/integrations/IntegrationsSectionForm' -import { LayoutSectionForm, type LayoutSectionFormProps } from './sections/layout/LayoutSectionForm' +import { LayoutSectionForm } from './sections/layout/LayoutSectionForm' import { type ConfiguratorFormChangeHandler, type ConfiguratorFormInputEvent, type ConfiguratorFormValues, } from './sections/section.types' -import { - ThemeColorsSectionForm, - type ThemeColorsSectionFormProps, -} from './sections/theme-colors/ThemeColorsSectionForm' +import { ThemeColorsSectionForm } from './sections/theme-colors/ThemeColorsSectionForm' import { TokensSectionForm } from './sections/tokens/TokensSectionForm' import { TradeSetupSectionForm } from './sections/trade-setup/TradeSetupSectionForm' import { drawerContentColumnSx, drawerPaperRowSx, getDrawerPatternFillerSx, getDrawerSx } from './sidebar.styles' @@ -371,7 +368,7 @@ export function Sidebar({ values={configuratorFormValues} onChange={handleConfiguratorFormChange} formComponent={ThemeColorsSectionForm} - formProps={{ paletteManager } satisfies Omit} + formProps={{ paletteManager }} /> handleConfiguratorFormChange('iframeStyleJson', value), - onAppWrapperStyleJson: (value) => handleConfiguratorFormChange('appWrapperStyleJson', value), - onBodyWrapperStyleJson: (value) => handleConfiguratorFormChange('bodyWrapperStyleJson', value), - onCardStyleJson: (value) => handleConfiguratorFormChange('cardStyleJson', value), - }, - } satisfies Omit - } + formProps={{ + paperBackgroundColor: colorPalette.paper || defaultPalette.paper, + jsonStates: { + iframeStyleJson, + appWrapperStyleJson, + bodyWrapperStyleJson, + cardStyleJson, + onIframeStyleJson: (value: string | null) => handleConfiguratorFormChange('iframeStyleJson', value), + onAppWrapperStyleJson: (value: string | null) => + handleConfiguratorFormChange('appWrapperStyleJson', value), + onBodyWrapperStyleJson: (value: string | null) => + handleConfiguratorFormChange('bodyWrapperStyleJson', value), + onCardStyleJson: (value: string | null) => handleConfiguratorFormChange('cardStyleJson', value), + }, + }} /> } + formProps={{ toastManager }} /> { +type BaseFormProps = { + values: TValues + onChange: TOnChange +} + +/** + * Prevents TypeScript from inferring a generic from this position. + * Used so `formProps` cannot widen the inferred props type beyond + * what `formComponent` already declares. + * + * @example + * ```ts + * // Without NoInfer: second argument can widen T + * function chooseWithoutNoInfer(fixed: T, candidate: T): T { + * return fixed + * } + * const widened = chooseWithoutNoInfer('light' as const, 'dark') + * // ^? "light" | "dark" (candidate participates in inferring T) + * + * // With NoInfer: candidate is checked against already-inferred T + * function chooseWithNoInfer(fixed: T, candidate: NoInfer): T { + * return fixed + * } + * const strict = chooseWithNoInfer('light' as const, 'light') // OK + * chooseWithNoInfer('light' as const, 'dark') // Error + * // ^ candidate cannot widen T + * ``` + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type NoInfer = [T][T extends any ? 0 : never] + +interface AccordionFormSectionProps< + TValues, + TOnChange, + TAllFormProps extends BaseFormProps = BaseFormProps, +> { title: string expandedSection: string | null onToggleExpanded: (title: string) => (isExpanded: boolean) => void values: TValues onChange: TOnChange - formComponent: ComponentType< - { - values: TValues - onChange: TOnChange - } & TFormProps - > - formProps?: TFormProps + formComponent: ComponentType + formProps?: NoInfer>> } -export function AccordionFormSection({ +export function AccordionFormSection< + TValues, + TOnChange, + TAllFormProps extends BaseFormProps = BaseFormProps, +>({ title, expandedSection, onToggleExpanded, @@ -25,12 +59,20 @@ export function AccordionFormSection): ReactNode { +}: AccordionFormSectionProps): ReactNode { const isExpanded = expandedSection === title return ( - {isExpanded ? : null} + {isExpanded ? ( + + ) : null} ) } From 78e099638b7a2280394b98fa8cad59b9dde21ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Thu, 21 May 2026 23:49:43 +0200 Subject: [PATCH 034/110] feat: refactor most inputs to use reusable base components and address styling inconsistencies --- apps/widget-configurator/.env | 2 + .../controls/AddCustomListDialog.tsx | 49 ++-- .../controls/CustomImagesControl.tsx | 37 --- .../controls/CustomSoundsControl.tsx | 64 ----- .../components/controls/DeadlineControl.tsx | 25 -- .../components/controls/PartnerFeeControl.tsx | 40 --- .../components/controls/ThemeControl.test.tsx | 9 +- .../src/components/controls/ThemeControl.tsx | 60 +++-- .../components/controls/TokenListControl.tsx | 107 ++++---- .../sections/behavior/BehaviorSectionForm.tsx | 30 +-- .../CustomizationSectionForm.tsx | 55 ++-- .../deadlines/DeadlinesSectionForm.tsx | 72 +++-- .../integrations/IntegrationsSectionForm.tsx | 33 ++- .../theme-colors/ThemeColorsSectionForm.tsx | 2 +- .../components/sidebar/sidebar.component.tsx | 11 +- .../BaseTextInput/BaseTextInput.component.tsx | 31 ++- .../CurrencyInput/CurrencyInputControl.tsx | 40 ++- .../NumberInput/NumberInput.component.tsx | 54 ++++ .../Select/CurrentTradeTypeControl.tsx | 43 ++- .../ui/controls/Select/LocaleControl.tsx | 54 ++-- .../ui/controls/Select/NetworkControl.tsx | 65 ++--- .../ui/controls/Select/SelectInput.tsx | 246 ++++++++++++++++++ .../ui/controls/Select/TradeModesControl.tsx | 43 +-- .../ui/controls/Select/WidgetHooksControl.tsx | 64 ++--- 24 files changed, 671 insertions(+), 565 deletions(-) delete mode 100644 apps/widget-configurator/src/components/controls/CustomImagesControl.tsx delete mode 100644 apps/widget-configurator/src/components/controls/CustomSoundsControl.tsx delete mode 100644 apps/widget-configurator/src/components/controls/DeadlineControl.tsx delete mode 100644 apps/widget-configurator/src/components/controls/PartnerFeeControl.tsx create mode 100644 apps/widget-configurator/src/components/ui/controls/Select/SelectInput.tsx diff --git a/apps/widget-configurator/.env b/apps/widget-configurator/.env index d6a8abc0ca8..989d522b60c 100644 --- a/apps/widget-configurator/.env +++ b/apps/widget-configurator/.env @@ -1,2 +1,4 @@ # Analytics #REACT_APP_GOOGLE_ANALYTICS_ID= + +REACT_APP_ENVIRONMENT=local diff --git a/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx b/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx index 034f2abd018..e9b3d625c64 100644 --- a/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx +++ b/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx @@ -1,31 +1,14 @@ -import React, { ReactNode, useEffect, useRef, useState } from 'react' +import React, { ReactNode, useEffect, useState } from 'react' import { isValidTokenListSource } from '@cowprotocol/common-utils' import { Command, TokenInfo } from '@cowprotocol/types' -import { - Box, - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - FormHelperText, - Tab, - TextField, -} from '@mui/material' +import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Tab, TextField } from '@mui/material' import Tabs from '@mui/material/Tabs' import { DEFAULT_CUSTOM_TOKENS } from '../../configurator.constants' import { parseCustomTokensInput } from '../../utils/parseCustomTokensInput' - -const jsonTextAreaStyles = { - fontFamily: 'monospace', - width: '100%', - height: '200px', - resize: 'none', - marginTop: '10px', -} +import { JsonInput } from '../ui/controls/JsonInput/JsonInput.component' type AddCustomListDialogProps = { open: boolean @@ -48,7 +31,7 @@ export function AddCustomListDialog({ const [customListUrl, setCustomListUrl] = useState('') const [hasErrors, setHasErrors] = useState(false) const [hasJsonErrors, setHasJsonErrors] = useState(false) - const textareaRef = useRef(null) + const [customTokensJson, setCustomTokensJson] = useState('') const [customTokens, setCustomTokens] = useState([]) @@ -64,6 +47,7 @@ export function AddCustomListDialog({ // Reset custom tokens setCustomTokens([]) setHasJsonErrors(false) + setCustomTokensJson('') } // TODO: Add proper return type annotation @@ -84,17 +68,18 @@ export function AddCustomListDialog({ } // TODO: Add proper return type annotation - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - const handleJsonInputChange = (e: React.ChangeEvent) => { + + const handleJsonInputChange = (_name: string, value: string | null): void => { setHasJsonErrors(false) + setCustomTokensJson(value) - if (!e.target.value) { + if (!value?.trim()) { setCustomTokens([]) return } try { - const parsedTokens = parseCustomTokensInput(e.target.value) + const parsedTokens = parseCustomTokensInput(value) if (parsedTokens) { setCustomTokens(parsedTokens) @@ -122,9 +107,7 @@ export function AddCustomListDialog({ // TODO: Add proper return type annotation // eslint-disable-next-line @typescript-eslint/explicit-function-return-type const addJsonExample = () => { - if (textareaRef.current) { - textareaRef.current.value = JSON.stringify(DEFAULT_CUSTOM_TOKENS, null, 2) - } + setCustomTokensJson(JSON.stringify(DEFAULT_CUSTOM_TOKENS, null, 2)) setCustomTokens(DEFAULT_CUSTOM_TOKENS) setHasJsonErrors(false) } @@ -162,9 +145,15 @@ export function AddCustomListDialog({ /> - + - {hasJsonErrors && Enter a token array or token list JSON} diff --git a/apps/widget-configurator/src/components/controls/CustomImagesControl.tsx b/apps/widget-configurator/src/components/controls/CustomImagesControl.tsx deleted file mode 100644 index e0733bcbd86..00000000000 --- a/apps/widget-configurator/src/components/controls/CustomImagesControl.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { ChangeEvent, Dispatch, SetStateAction, useCallback } from 'react' - -import { CowSwapWidgetParams } from '@cowprotocol/widget-lib' - -import TextField from '@mui/material/TextField' - -type CustomImages = CowSwapWidgetParams['images'] - -export interface CustomImagesControlProps { - state: [CustomImages, Dispatch>] -} - -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function CustomImagesControl({ state }: CustomImagesControlProps) { - const [customImages, setCustomImages] = state - - const updateEmptyOrdersImage = useCallback( - (e: ChangeEvent) => { - setCustomImages((prevState) => ({ ...prevState, emptyOrders: e.target.value || '' })) - }, - [setCustomImages], - ) - - return ( -
- -
- ) -} diff --git a/apps/widget-configurator/src/components/controls/CustomSoundsControl.tsx b/apps/widget-configurator/src/components/controls/CustomSoundsControl.tsx deleted file mode 100644 index e9e32f8b6f9..00000000000 --- a/apps/widget-configurator/src/components/controls/CustomSoundsControl.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { ChangeEvent, Dispatch, SetStateAction, useCallback } from 'react' - -import { CowSwapWidgetParams } from '@cowprotocol/widget-lib' - -import TextField from '@mui/material/TextField' - -type CustomSounds = CowSwapWidgetParams['sounds'] -type WidgetSounds = keyof NonNullable - -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -const valueNullAsString = (value: string | undefined | null) => (value === null ? 'null' : value || '') - -export interface CustomSoundsControlProps { - state: [CustomSounds, Dispatch>] -} - -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function CustomSoundsControl({ state }: CustomSoundsControlProps) { - const [customSound, setCustomSounds] = state - - const updateSoundCallback = useCallback( - (type: WidgetSounds) => { - return (e: ChangeEvent) => { - const value = e.target.value - setCustomSounds((prevState) => ({ ...prevState, [type]: value === 'null' ? null : value || '' })) - } - }, - [setCustomSounds], - ) - - return ( -
- - - -
- ) -} diff --git a/apps/widget-configurator/src/components/controls/DeadlineControl.tsx b/apps/widget-configurator/src/components/controls/DeadlineControl.tsx deleted file mode 100644 index 5217694e23c..00000000000 --- a/apps/widget-configurator/src/components/controls/DeadlineControl.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Dispatch, SetStateAction } from 'react' - -import { FormControl, TextField } from '@mui/material' - -export type DeadlineControlProps = { - label: string - deadlineState: [number | undefined, Dispatch>] -} - -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function DeadlineControl({ label, deadlineState: [state, setState] }: DeadlineControlProps) { - return ( - - setState(value && !isNaN(+value) ? Math.max(1, Number(value)) : undefined)} - size="small" - inputProps={{ min: 1 }} // Set minimum value to 1 - /> - - ) -} diff --git a/apps/widget-configurator/src/components/controls/PartnerFeeControl.tsx b/apps/widget-configurator/src/components/controls/PartnerFeeControl.tsx deleted file mode 100644 index f9dbdabc481..00000000000 --- a/apps/widget-configurator/src/components/controls/PartnerFeeControl.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { ChangeEvent, Dispatch, SetStateAction, useCallback } from 'react' - -import TextField from '@mui/material/TextField' - -export interface PartnerFeeControlProps { - feeBpsState: [number, Dispatch>] -} -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function PartnerFeeControl(props: PartnerFeeControlProps) { - const { feeBpsState } = props - const [feeBps, setFeeBps] = feeBpsState - - const onFeeBpsChange = useCallback( - (e: ChangeEvent) => { - const value = Math.ceil(Number(e.target.value || 0)) - - setFeeBps(value) - }, - [setFeeBps], - ) - - return ( - <> - - - ) -} diff --git a/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx b/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx index 7cba183aed5..8a5b1a4b187 100644 --- a/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx +++ b/apps/widget-configurator/src/components/controls/ThemeControl.test.tsx @@ -2,8 +2,11 @@ import { fireEvent, render, screen } from '@testing-library/react' import { ThemeControl, type ThemeOptionValue } from './ThemeControl' -function renderThemeControl(props: { selectedValue: ThemeOptionValue; onChange: (v: ThemeOptionValue) => void }): void { - render() +function renderThemeControl(props: { + selectedValue: ThemeOptionValue + onChange: (name: string, value: ThemeOptionValue) => void +}): void { + render() } describe('ThemeControl', () => { @@ -23,6 +26,6 @@ describe('ThemeControl', () => { fireEvent.mouseDown(screen.getByRole('combobox')) fireEvent.click(screen.getByRole('option', { name: 'Light' })) - expect(onChange).toHaveBeenCalledWith('light') + expect(onChange).toHaveBeenCalledWith('theme', 'light') }) }) diff --git a/apps/widget-configurator/src/components/controls/ThemeControl.tsx b/apps/widget-configurator/src/components/controls/ThemeControl.tsx index 7dc1de7eacc..97e492f99fd 100644 --- a/apps/widget-configurator/src/components/controls/ThemeControl.tsx +++ b/apps/widget-configurator/src/components/controls/ThemeControl.tsx @@ -3,12 +3,10 @@ import { type ComponentType, type ReactNode } from 'react' import DarkModeIcon from '@mui/icons-material/DarkMode' import LightModeIcon from '@mui/icons-material/LightMode' import Box from '@mui/material/Box' -import FormControl from '@mui/material/FormControl' -import InputLabel from '@mui/material/InputLabel' -import MenuItem from '@mui/material/MenuItem' -import Select, { type SelectChangeEvent } from '@mui/material/Select' import Typography from '@mui/material/Typography' +import { SelectInput, type SelectInputOption } from '../ui/controls/Select/SelectInput' + export type ThemeOptionValue = 'light' | 'dark' interface ThemeOption { @@ -45,37 +43,37 @@ function ThemeOptionContent({ icon: Icon, label }: Pick void + onChange: (name: string, value: ThemeOptionValue) => void } -export function ThemeControl({ selectedValue, onChange }: ThemeControlProps): ReactNode { - const handleThemeChange = (event: SelectChangeEvent): void => { - onChange(event.target.value as ThemeOptionValue) - } - +export function ThemeControl({ name, selectedValue, onChange }: ThemeControlProps): ReactNode { return ( - - Theme - - + return + }} + /> ) } diff --git a/apps/widget-configurator/src/components/controls/TokenListControl.tsx b/apps/widget-configurator/src/components/controls/TokenListControl.tsx index 3ea83956ff9..46ee8ed6d4d 100644 --- a/apps/widget-configurator/src/components/controls/TokenListControl.tsx +++ b/apps/widget-configurator/src/components/controls/TokenListControl.tsx @@ -2,23 +2,12 @@ import { Dispatch, ReactNode, SetStateAction, useCallback, useMemo, useState } f import { TokenInfo } from '@cowprotocol/types' -import { - Box, - Button, - Checkbox, - Chip, - FormControl, - InputLabel, - ListItemText, - MenuItem, - OutlinedInput, - Select, - SelectChangeEvent, -} from '@mui/material' +import { Box, Button, Chip, ListItemText } from '@mui/material' import { AddCustomListDialog } from './AddCustomListDialog' import { TokenListItem } from '../../configurator.types' +import { SelectInput } from '../ui/controls/Select/SelectInput' const ITEM_HEIGHT = 48 const ITEM_PADDING_TOP = 8 @@ -40,28 +29,31 @@ type TokenListControlProps = { interface TokenListSelectProps { label: string - labelId: string + name: TokenListScope selectedUrls: string[] - options: ReactNode - onChange(event: SelectChangeEvent): void + options: { label: string; value: string }[] + onChange(scope: TokenListScope, selectedUrls: string[]): void } interface TokenListSelectionsProps { tokenListUrls: TokenListItem[] - onChangeByScope: Record) => void> + onChangeByScope: Record void> } -const TOKEN_LIST_SELECT_CONFIG: { label: string; labelId: string; scope: TokenListScope }[] = [ - { label: 'Active Token Lists', labelId: 'token-list-chip-label', scope: 'enabled' }, - { label: 'Sell Token Lists', labelId: 'sell-token-list-chip-label', scope: 'enabledForSell' }, - { label: 'Buy Token Lists', labelId: 'buy-token-list-chip-label', scope: 'enabledForBuy' }, +const TOKEN_LIST_SELECT_CONFIG: { label: string; scope: TokenListScope }[] = [ + { label: 'Active Token Lists', scope: 'enabled' }, + { label: 'Sell Token Lists', scope: 'enabledForSell' }, + { label: 'Buy Token Lists', scope: 'enabledForBuy' }, ] const getSelectedTokenListUrls = (tokenListUrls: TokenListItem[], scope: TokenListScope): string[] => { return tokenListUrls.filter((list) => list[scope]).map((list) => list.url) } -const getTokenListOptions = (tokenListUrls: TokenListItem[], scope: TokenListScope): ReactNode[] => { +const getTokenListOptions = ( + tokenListUrls: TokenListItem[], + scope: TokenListScope, +): { label: string; value: string }[] => { return [...tokenListUrls] .sort((a, b) => { if (a[scope] === b[scope]) { @@ -70,11 +62,27 @@ const getTokenListOptions = (tokenListUrls: TokenListItem[], scope: TokenListSco return a[scope] ? -1 : 1 }) - .map((list) => ( - - + .map((list) => ({ label: list.url, value: list.url })) +} + +function TokenListSelect({ label, name, selectedUrls, options, onChange }: TokenListSelectProps): ReactNode { + return ( + { + if (!Array.isArray(value)) return + onChange(scope as TokenListScope, value as string[]) + }} + renderOptionLabel={(option) => ( - - )) -} - -function TokenListSelect({ label, labelId, selectedUrls, options, onChange }: TokenListSelectProps): ReactNode { - return ( - - {label} - - + )} + renderValue={(selected) => ( + + {(Array.isArray(selected) ? selected : []).map((url) => ( + + ))} + + )} + /> ) } function TokenListSelections({ tokenListUrls, onChangeByScope }: TokenListSelectionsProps): ReactNode { return ( - {TOKEN_LIST_SELECT_CONFIG.map(({ label, labelId, scope }) => ( + {TOKEN_LIST_SELECT_CONFIG.map(({ label, scope }) => ( ({ - enabled: (event: SelectChangeEvent) => setTokenListScope('enabled', event.target.value as string[]), - enabledForSell: (event: SelectChangeEvent) => - setTokenListScope('enabledForSell', event.target.value as string[]), - enabledForBuy: (event: SelectChangeEvent) => - setTokenListScope('enabledForBuy', event.target.value as string[]), + enabled: (_: TokenListScope, selectedUrls: string[]) => setTokenListScope('enabled', selectedUrls), + enabledForSell: (_: TokenListScope, selectedUrls: string[]) => setTokenListScope('enabledForSell', selectedUrls), + enabledForBuy: (_: TokenListScope, selectedUrls: string[]) => setTokenListScope('enabledForBuy', selectedUrls), }), [setTokenListScope], ) diff --git a/apps/widget-configurator/src/components/sidebar/sections/behavior/BehaviorSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/behavior/BehaviorSectionForm.tsx index aae5d0327f4..7456c2e2212 100644 --- a/apps/widget-configurator/src/components/sidebar/sections/behavior/BehaviorSectionForm.tsx +++ b/apps/widget-configurator/src/components/sidebar/sections/behavior/BehaviorSectionForm.tsx @@ -1,8 +1,7 @@ -import type { ChangeEvent, ReactNode } from 'react' - -import TextField from '@mui/material/TextField' +import type { ReactNode } from 'react' import { BooleanSwitchControl } from '../../../ui/controls/BooleanSwitch/BooleanSwitchControl' +import { NumberInput } from '../../../ui/controls/NumberInput/NumberInput.component' import type { UseToastsManagerReturn } from '../../../../hooks/useToastsManager' import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' @@ -14,20 +13,6 @@ export interface BehaviorSectionFormProps { } export function BehaviorSectionForm({ values, onChange, toastManager }: BehaviorSectionFormProps): ReactNode { - const setBlockPriceImpactAboveValue = (event: ChangeEvent): void => { - const nextValue = event.target.value.trim() - - if (!nextValue) { - onChange('disableTradeWhenPriceImpactIsHigherThan', undefined) - return - } - - const parsedValue = Number(nextValue) - if (Number.isNaN(parsedValue)) return - - onChange('disableTradeWhenPriceImpactIsHigherThan', parsedValue) - } - return ( <> onChange('disableTradeWhenPriceImpactIsUnknown', enabled)} /> - (current: T, next: SetStateAction): T { - return typeof next === 'function' ? (next as (prevState: T) => T)(current) : next -} +type WidgetSounds = keyof NonNullable interface CustomizationSectionFormProps { values: ConfiguratorFormValues @@ -17,20 +14,46 @@ interface CustomizationSectionFormProps { } export function CustomizationSectionForm({ values, onChange }: CustomizationSectionFormProps): ReactNode { - const customImagesState: [CowSwapWidgetParams['images'], Dispatch>] = [ - values.customImages, - (nextValue) => onChange('customImages', resolveNextState(values.customImages, nextValue)), - ] + const customImages = values.customImages || {} + const customSounds = values.customSounds || {} - const customSoundsState: [CowSwapWidgetParams['sounds'], Dispatch>] = [ - values.customSounds, - (nextValue) => onChange('customSounds', resolveNextState(values.customSounds, nextValue)), - ] + const valueNullAsString = (value: string | undefined | null): string => (value === null ? 'null' : value || '') + const handleSoundChange = + (type: WidgetSounds) => + (name: string, value: string | null): void => { + const nextValue = value === 'null' ? null : value || '' + onChange('customSounds', { ...customSounds, [type]: nextValue }) + } return ( <> - - + onChange('customImages', { ...customImages, emptyOrders: value || '' })} + /> + + + ) } diff --git a/apps/widget-configurator/src/components/sidebar/sections/deadlines/DeadlinesSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/deadlines/DeadlinesSectionForm.tsx index c05e2262aa4..a8b3859ebd2 100644 --- a/apps/widget-configurator/src/components/sidebar/sections/deadlines/DeadlinesSectionForm.tsx +++ b/apps/widget-configurator/src/components/sidebar/sections/deadlines/DeadlinesSectionForm.tsx @@ -1,42 +1,60 @@ -import type { Dispatch, ReactNode, SetStateAction } from 'react' +import type { ReactNode } from 'react' -import { DeadlineControl } from '../../../controls/DeadlineControl' +import { NumberInput } from '../../../ui/controls/NumberInput/NumberInput.component' import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' -function resolveNextState(current: T, next: SetStateAction): T { - return typeof next === 'function' ? (next as (prevState: T) => T)(current) : next -} - interface DeadlinesSectionFormProps { values: ConfiguratorFormValues onChange: ConfiguratorFormChangeHandler } -export function DeadlinesSectionForm({ values, onChange }: DeadlinesSectionFormProps): ReactNode { - const deadlineState: [number | undefined, Dispatch>] = [ - values.deadline, - (nextValue) => onChange('deadline', resolveNextState(values.deadline, nextValue)), - ] - const swapDeadlineState: [number | undefined, Dispatch>] = [ - values.swapDeadline, - (nextValue) => onChange('swapDeadline', resolveNextState(values.swapDeadline, nextValue)), - ] - const limitDeadlineState: [number | undefined, Dispatch>] = [ - values.limitDeadline, - (nextValue) => onChange('limitDeadline', resolveNextState(values.limitDeadline, nextValue)), - ] - const advancedDeadlineState: [number | undefined, Dispatch>] = [ - values.advancedDeadline, - (nextValue) => onChange('advancedDeadline', resolveNextState(values.advancedDeadline, nextValue)), - ] +function parseDeadlineValue(rawValue: string): number | undefined { + const numericValue = Number(rawValue) + if (Number.isNaN(numericValue)) return undefined + return Math.max(1, numericValue) +} + +export function DeadlinesSectionForm({ values, onChange }: DeadlinesSectionFormProps): ReactNode { return ( <> - - - - + + + + ) } diff --git a/apps/widget-configurator/src/components/sidebar/sections/integrations/IntegrationsSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/integrations/IntegrationsSectionForm.tsx index ce54f6caac4..29f603ed931 100644 --- a/apps/widget-configurator/src/components/sidebar/sections/integrations/IntegrationsSectionForm.tsx +++ b/apps/widget-configurator/src/components/sidebar/sections/integrations/IntegrationsSectionForm.tsx @@ -1,6 +1,6 @@ -import type { Dispatch, ReactNode, SetStateAction } from 'react' +import type { ReactNode } from 'react' -import { PartnerFeeControl } from '../../../controls/PartnerFeeControl' +import { NumberInput } from '../../../ui/controls/NumberInput/NumberInput.component' import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' @@ -10,13 +10,24 @@ interface IntegrationsSectionFormProps { } export function IntegrationsSectionForm({ values, onChange }: IntegrationsSectionFormProps): ReactNode { - const partnerFeeBpsState: [number, Dispatch>] = [ - values.partnerFeeBps, - (nextValue) => { - const resolvedValue = typeof nextValue === 'function' ? nextValue(values.partnerFeeBps) : nextValue - onChange('partnerFeeBps', resolvedValue) - }, - ] - - return + return ( + { + const roundedValue = Math.ceil(Number(rawValue)) + const boundedValue = Math.min(Math.max(roundedValue, 0), 100) + return Number.isNaN(boundedValue) ? 0 : boundedValue + }} + inputProps={{ + min: 0, + max: 100, + step: 1, + }} + /> + ) } diff --git a/apps/widget-configurator/src/components/sidebar/sections/theme-colors/ThemeColorsSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/theme-colors/ThemeColorsSectionForm.tsx index 84c8ae380fd..df3cb1268af 100644 --- a/apps/widget-configurator/src/components/sidebar/sections/theme-colors/ThemeColorsSectionForm.tsx +++ b/apps/widget-configurator/src/components/sidebar/sections/theme-colors/ThemeColorsSectionForm.tsx @@ -15,7 +15,7 @@ export interface ThemeColorsSectionFormProps { export function ThemeColorsSectionForm({ values, onChange, paletteManager }: ThemeColorsSectionFormProps): ReactNode { return ( <> - onChange('theme', value)} /> + ) diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index 08c1206d3f9..5b2183b08ec 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -315,10 +315,17 @@ export function Sidebar({ - [x] Update AccordionSection so that we just pass title, currentTitle and onChange, and handle that with a single state variable and a single handler function. - [x] Move fields to individual panels. Pass one prop per value and one single callback that takes a ChangeEvent or name + value. + - [x] Add name to all fields. + - [x] Create reusable TextInput, NumberInput and SelectInput components. + - [ ] Refactor patterns like this: + export interface CurrentTradeTypeControlProps { + state: [TradeType, Dispatch>] + } + + - [ ] Further polish Select-based inputs and color inputs. Fix Number input with default value. + - [ ] TokensDialog, Wagmi dialog, etc. sit below the sidebar handler. - [ ] Add toggle to disable scrollbars. - - [ ] Add name to all fields. - - [ ] Create reusable TextInput, NumberInput and SelectInput components. - [ ] Bug: when in dApp mode, reload the page with the wallet connected. You are connected outside, not within the widget. */ diff --git a/apps/widget-configurator/src/components/ui/controls/BaseTextInput/BaseTextInput.component.tsx b/apps/widget-configurator/src/components/ui/controls/BaseTextInput/BaseTextInput.component.tsx index a4617b936a8..e90c63a0d66 100644 --- a/apps/widget-configurator/src/components/ui/controls/BaseTextInput/BaseTextInput.component.tsx +++ b/apps/widget-configurator/src/components/ui/controls/BaseTextInput/BaseTextInput.component.tsx @@ -2,26 +2,39 @@ import { ReactNode } from 'react' import { TextField, TextFieldProps } from '@mui/material' +export const BASE_TEXT_INPUT_HEIGHT = 42 + export interface BaseTextInputProps extends Omit { name: string label: string } export function BaseTextInput(props: BaseTextInputProps): ReactNode { + const resolvedSx = Array.isArray(props.sx) ? props.sx : props.sx ? [props.sx] : [] + return ( span': { + display: 'inline-block', + paddingLeft: 0, + paddingRight: 0, + }, + }, + ]} fullWidth margin="dense" - size="medium" + size="small" /> ) } diff --git a/apps/widget-configurator/src/components/ui/controls/CurrencyInput/CurrencyInputControl.tsx b/apps/widget-configurator/src/components/ui/controls/CurrencyInput/CurrencyInputControl.tsx index 773a4c5c104..57d8a78828f 100644 --- a/apps/widget-configurator/src/components/ui/controls/CurrencyInput/CurrencyInputControl.tsx +++ b/apps/widget-configurator/src/components/ui/controls/CurrencyInput/CurrencyInputControl.tsx @@ -1,7 +1,7 @@ -import { ChangeEvent, Dispatch, SetStateAction } from 'react' +import type { Dispatch, ReactNode, SetStateAction } from 'react' -import Autocomplete from '@mui/material/Autocomplete' -import TextField from '@mui/material/TextField' +import { NumberInput } from '../NumberInput/NumberInput.component' +import { SelectInput } from '../Select/SelectInput' const TokenOptions = ['COW', 'USDC', 'WBTC'] @@ -10,36 +10,34 @@ export interface CurrencyInputProps { tokenIdState: [string, Dispatch>] tokenAmountState: [number, Dispatch>] } -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function CurrencyInputControl(props: CurrencyInputProps) { + +export function CurrencyInputControl(props: CurrencyInputProps): ReactNode { const { tokenIdState, tokenAmountState, label } = props const [tokenId, setTokenId] = tokenIdState const [amount, setAmount] = tokenAmountState return ( <> - , newValue: string | null) => { - setTokenId(newValue || '') - }} - inputValue={tokenId || ''} - onInputChange={(event: ChangeEvent, newInputValue: string) => { - setTokenId(newInputValue || '') - }} + } + name={'selectTokenId' + label} + label={label} + value={TokenOptions.includes(tokenId) ? tokenId : ''} + options={TokenOptions.map((option) => ({ label: option, value: option }))} + onChange={(_, value) => { + if (Array.isArray(value)) return + setTokenId(value) + }} /> - ) => setAmount(Number(e.target.value || 0))} - size="small" + onChange={(_, value) => setAmount(value ?? 0)} + emptyValue={0} + inputProps={{ min: 0, step: 'any' }} /> ) diff --git a/apps/widget-configurator/src/components/ui/controls/NumberInput/NumberInput.component.tsx b/apps/widget-configurator/src/components/ui/controls/NumberInput/NumberInput.component.tsx index e69de29bb2d..48c1df70986 100644 --- a/apps/widget-configurator/src/components/ui/controls/NumberInput/NumberInput.component.tsx +++ b/apps/widget-configurator/src/components/ui/controls/NumberInput/NumberInput.component.tsx @@ -0,0 +1,54 @@ +import { ReactNode } from 'react' + +import { BaseTextInput, BaseTextInputProps } from '../BaseTextInput/BaseTextInput.component' + +export interface NumberInputProps extends Omit { + name: string + value: number | null | undefined + onChange: (name: string, value: number | null | undefined) => void + emptyValue?: number | null | undefined + parseValue?: (value: string) => number | null | undefined +} + +export function NumberInput({ + name, + value, + onChange, + onBlur, + emptyValue = undefined, + parseValue, + ...props +}: NumberInputProps): ReactNode { + const parseNumberValue = (rawValue: string): number | null | undefined => { + if (rawValue === '') { + return emptyValue + } + + if (parseValue) { + return parseValue(rawValue) + } + + const numericValue = Number(rawValue) + return Number.isNaN(numericValue) ? emptyValue : numericValue + } + + const handleChange = (event: React.ChangeEvent): void => { + onChange(name, parseNumberValue(event.target.value)) + } + + const handleBlur = (event: React.FocusEvent): void => { + onChange(name, parseNumberValue(event.target.value)) + if (onBlur) onBlur(event) + } + + return ( + + ) +} diff --git a/apps/widget-configurator/src/components/ui/controls/Select/CurrentTradeTypeControl.tsx b/apps/widget-configurator/src/components/ui/controls/Select/CurrentTradeTypeControl.tsx index 0fffbdd08fb..a2b6a9dac98 100644 --- a/apps/widget-configurator/src/components/ui/controls/Select/CurrentTradeTypeControl.tsx +++ b/apps/widget-configurator/src/components/ui/controls/Select/CurrentTradeTypeControl.tsx @@ -1,38 +1,31 @@ -import { Dispatch, SetStateAction } from 'react' +import type { Dispatch, ReactNode, SetStateAction } from 'react' import type { TradeType } from '@cowprotocol/widget-lib' -import FormControl from '@mui/material/FormControl' -import InputLabel from '@mui/material/InputLabel' -import MenuItem from '@mui/material/MenuItem' -import Select from '@mui/material/Select' +import { SelectInput } from './SelectInput' import { TRADE_MODES } from '../../../../configurator.constants' const LABEL = 'Current trade type' -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function CurrentTradeTypeControl({ state }: { state: [TradeType, Dispatch>] }) { +export interface CurrentTradeTypeControlProps { + state: [TradeType, Dispatch>] +} + +export function CurrentTradeTypeControl({ state }: CurrentTradeTypeControlProps): ReactNode { const [tradeType, setTradeType] = state return ( - - {LABEL} - - + ({ label: option, value: option }))} + onChange={(_, value) => { + if (value === '' || Array.isArray(value)) return + setTradeType(value as TradeType) + }} + /> ) } diff --git a/apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.tsx b/apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.tsx index 75b00ae64ee..26732acdf4b 100644 --- a/apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.tsx +++ b/apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.tsx @@ -2,10 +2,7 @@ import { Dispatch, ReactNode, SetStateAction } from 'react' import { LOCALE_DISPLAY_NAMES, SupportedLocale, SUPPORTED_LOCALES } from '@cowprotocol/common-const' -import FormControl from '@mui/material/FormControl' -import InputLabel from '@mui/material/InputLabel' -import MenuItem from '@mui/material/MenuItem' -import Select from '@mui/material/Select' +import { SelectInput } from './SelectInput' const LABEL = 'Forced locale' const LABEL_ID = 'select-locale-label' @@ -16,33 +13,28 @@ export function LocaleControl({ state }: { state: LocaleControlState }): ReactNo const [locale, setLocale] = state return ( - - - {LABEL} - - - + return LOCALE_DISPLAY_NAMES[selected as SupportedLocale] || selected + }} + /> ) } diff --git a/apps/widget-configurator/src/components/ui/controls/Select/NetworkControl.tsx b/apps/widget-configurator/src/components/ui/controls/Select/NetworkControl.tsx index 76e7258924f..e2d3c4c670b 100644 --- a/apps/widget-configurator/src/components/ui/controls/Select/NetworkControl.tsx +++ b/apps/widget-configurator/src/components/ui/controls/Select/NetworkControl.tsx @@ -1,12 +1,9 @@ -import { Dispatch, SetStateAction } from 'react' +import type { Dispatch, ReactNode, SetStateAction } from 'react' import { CHAIN_INFO } from '@cowprotocol/common-const' import { isChainDeprecated, SupportedChainId } from '@cowprotocol/cow-sdk' -import FormControl from '@mui/material/FormControl' -import InputLabel from '@mui/material/InputLabel' -import MenuItem from '@mui/material/MenuItem' -import Select from '@mui/material/Select' +import { SelectInput } from './SelectInput' export type NetworkOption = { chainId: SupportedChainId @@ -22,24 +19,19 @@ const DEFAULT_CHAIN_ID = NetworkOptions[0].chainId const LABEL = 'Network' -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export const getNetworkOption = (chainId: SupportedChainId) => NetworkOptions.find((item) => item.chainId === chainId) +export const getNetworkOption = (chainId: SupportedChainId): NetworkOption | undefined => + NetworkOptions.find((item) => item.chainId === chainId) -type NetworkControlProps = { +export interface NetworkControlProps { standaloneMode: boolean state: [NetworkOption, Dispatch>] availableChains: SupportedChainId[] } -// TODO: Add proper return type annotation -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function NetworkControl({ state, standaloneMode, availableChains }: NetworkControlProps) { +export function NetworkControl({ state, standaloneMode, availableChains }: NetworkControlProps): ReactNode { const [network, setNetwork] = state - // TODO: Add proper return type annotation - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - const switchNetwork = (chainId: number) => { + const switchNetwork = (chainId: number): void => { const targetChainId = chainId || DEFAULT_CHAIN_ID const targetNetwork = getNetworkOption(targetChainId) @@ -49,32 +41,27 @@ export function NetworkControl({ state, standaloneMode, availableChains }: Netwo } return ( - - {LABEL} - - + return { + label: `${option.label}${isChainDeprecated(chainId) ? ' (deprecated)' : ''}`, + value: option.chainId, + } + }) + .filter((option): option is { label: string; value: SupportedChainId } => option !== null)} + onChange={(_, value) => { + if (value === '' || Array.isArray(value)) return + switchNetwork(value as number) + }} + /> ) } diff --git a/apps/widget-configurator/src/components/ui/controls/Select/SelectInput.tsx b/apps/widget-configurator/src/components/ui/controls/Select/SelectInput.tsx new file mode 100644 index 00000000000..464fb6ad428 --- /dev/null +++ b/apps/widget-configurator/src/components/ui/controls/Select/SelectInput.tsx @@ -0,0 +1,246 @@ +import { ReactNode } from 'react' + +import CheckIcon from '@mui/icons-material/Check' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' +import Box from '@mui/material/Box' +import Checkbox from '@mui/material/Checkbox' +import FormControl from '@mui/material/FormControl' +import InputLabel from '@mui/material/InputLabel' +import MenuItem from '@mui/material/MenuItem' +import Select, { SelectChangeEvent, SelectProps } from '@mui/material/Select' + +import { BASE_TEXT_INPUT_HEIGHT } from '../BaseTextInput/BaseTextInput.component' + +type PrimitiveValue = string | number + +const DEFAULT_MENU_PAPER_SX = { + backgroundColor: 'background.paper', + border: '1px solid rgba(255, 255, 255, 0.12)', + boxShadow: 'none', + backgroundImage: 'none', + '& .MuiMenuItem-root': { + backgroundColor: 'transparent !important', + }, + '& .MuiMenuItem-root.Mui-selected, & .MuiMenuItem-root.Mui-selected.Mui-focusVisible': { + backgroundColor: 'transparent !important', + }, + '& .MuiMenuItem-root.Mui-focusVisible': { + backgroundColor: 'transparent !important', + }, + '& .MuiMenuItem-root:hover, & .MuiMenuItem-root.Mui-selected:hover, & .MuiMenuItem-root.Mui-focusVisible:hover, & .MuiMenuItem-root.Mui-selected.Mui-focusVisible:hover': + { + backgroundColor: 'rgba(255, 255, 255, 0.06) !important', + }, +} as const + +const NO_MENU_ANIMATION_PROPS: SelectProps['MenuProps'] = { + transitionDuration: 0, + TransitionProps: { + timeout: 0, + }, + PaperProps: { + sx: DEFAULT_MENU_PAPER_SX, + }, +} + +export type SelectInputValue = TValue | '' | TValue[] + +export interface SelectInputOption { + label: string + value: TValue + disabled?: boolean +} + +export interface SelectInputProps { + name: string + label: string + value: SelectInputValue + options: readonly SelectInputOption[] + onChange: (name: string, value: SelectInputValue) => void + multiple?: boolean + id?: string + labelId?: string + size?: 'small' | 'medium' + fullWidth?: boolean + displayEmpty?: boolean + disabled?: boolean + menuProps?: SelectProps['MenuProps'] + multilineSelectedValue?: boolean + renderValue?: (value: SelectInputValue) => ReactNode + renderOptionLabel?: (option: SelectInputOption) => ReactNode +} + +function coerceSingleValue( + rawValue: string | number, + options: readonly SelectInputOption[], +): TValue | '' { + const targetOption = options.find((option) => String(option.value) === String(rawValue)) + return targetOption?.value ?? '' +} + +function coerceMultiValue( + rawValue: unknown, + options: readonly SelectInputOption[], +): TValue[] { + if (!Array.isArray(rawValue)) return [] + return rawValue + .map((item) => coerceSingleValue(item as string | number, options)) + .filter((item): item is TValue => item !== '') +} + +// eslint-disable-next-line max-lines-per-function, complexity +export function SelectInput({ + id, + name, + label, + labelId, + value, + options, + onChange, + multiple = false, + size = 'small', + fullWidth = true, + displayEmpty = false, + disabled = false, + menuProps, + multilineSelectedValue = false, + renderValue, + renderOptionLabel, +}: SelectInputProps): ReactNode { + const resolvedId = id ?? `select-${name}` + const resolvedLabelId = labelId ?? `${resolvedId}-label` + + const normalizedValue = multiple ? (Array.isArray(value) ? value : []) : (value as TValue | '') + + const handleChange = (event: SelectChangeEvent>): void => { + if (multiple) { + onChange(name, coerceMultiValue(event.target.value, options)) + return + } + + onChange(name, coerceSingleValue(event.target.value as string | number, options)) + } + + const defaultRenderValue = (selected: SelectInputValue): ReactNode => { + if (Array.isArray(selected)) { + return selected + .map( + (selectedValue) => options.find((option) => option.value === selectedValue)?.label ?? String(selectedValue), + ) + .join(', ') + } + + const selectedOption = options.find((option) => option.value === selected) + return selectedOption?.label ?? '' + } + + const renderOptionContent = (option: SelectInputOption, selected: boolean): ReactNode => { + const labelContent = renderOptionLabel ? renderOptionLabel(option) : option.label + return ( + + {labelContent} + {multiple ? ( + + ) : selected ? ( + + ) : ( + + )} + + ) + } + + const mergedMenuProps: SelectProps['MenuProps'] = { + ...NO_MENU_ANIMATION_PROPS, + ...menuProps, + PaperProps: { + ...menuProps?.PaperProps, + sx: [ + DEFAULT_MENU_PAPER_SX, + ...(Array.isArray(menuProps?.PaperProps?.sx) + ? menuProps.PaperProps.sx + : menuProps?.PaperProps?.sx + ? [menuProps.PaperProps.sx] + : []), + ], + }, + } + + return ( + + + {label} + + + + ) +} diff --git a/apps/widget-configurator/src/components/ui/controls/Select/TradeModesControl.tsx b/apps/widget-configurator/src/components/ui/controls/Select/TradeModesControl.tsx index f7c81e53886..2c03104a65e 100644 --- a/apps/widget-configurator/src/components/ui/controls/Select/TradeModesControl.tsx +++ b/apps/widget-configurator/src/components/ui/controls/Select/TradeModesControl.tsx @@ -2,13 +2,7 @@ import { Dispatch, ReactNode, SetStateAction } from 'react' import { TradeType } from '@cowprotocol/widget-lib' -import Checkbox from '@mui/material/Checkbox' -import FormControl from '@mui/material/FormControl' -import InputLabel from '@mui/material/InputLabel' -import ListItemText from '@mui/material/ListItemText' -import MenuItem from '@mui/material/MenuItem' -import OutlinedInput from '@mui/material/OutlinedInput' -import Select, { SelectChangeEvent } from '@mui/material/Select' +import { SelectInput } from './SelectInput' const LABEL = 'Trade types' @@ -19,34 +13,23 @@ export function TradeModesControl({ }): ReactNode { const [tradeModes, setTradeModes] = state - const handleTradeModeChange = (event: SelectChangeEvent): void => { - const value = event.target.value - const nextTradeModes = typeof value === 'string' ? (value.split(',') as TradeType[]) : value - + const handleTradeModeChange = (_: string, value: TradeType[] | '' | TradeType): void => { + const nextTradeModes = Array.isArray(value) ? value : [] if (!nextTradeModes.length) return setTradeModes(nextTradeModes) } return ( - - {LABEL} - - + ({ label: option, value: option }))} + onChange={handleTradeModeChange} + renderValue={(selected) => (Array.isArray(selected) ? selected.join(', ') : selected)} + /> ) } diff --git a/apps/widget-configurator/src/components/ui/controls/Select/WidgetHooksControl.tsx b/apps/widget-configurator/src/components/ui/controls/Select/WidgetHooksControl.tsx index 4c62aaba331..4193efe19a2 100644 --- a/apps/widget-configurator/src/components/ui/controls/Select/WidgetHooksControl.tsx +++ b/apps/widget-configurator/src/components/ui/controls/Select/WidgetHooksControl.tsx @@ -2,13 +2,7 @@ import { Dispatch, ReactNode, SetStateAction } from 'react' import { WidgetHookEvents } from '@cowprotocol/widget-lib' -import Checkbox from '@mui/material/Checkbox' -import FormControl from '@mui/material/FormControl' -import InputLabel from '@mui/material/InputLabel' -import ListItemText from '@mui/material/ListItemText' -import MenuItem from '@mui/material/MenuItem' -import OutlinedInput from '@mui/material/OutlinedInput' -import Select, { SelectChangeEvent } from '@mui/material/Select' +import { SelectInput } from './SelectInput' import { WIDGET_HOOKS } from '../../../../configurator.constants' @@ -23,41 +17,31 @@ export function WidgetHooksControl({ }): ReactNode { const [widgetHooks, setWidgetHooks] = state - const handleChange = (event: SelectChangeEvent): void => { - setWidgetHooks(event.target.value as WidgetHookEvents[]) + const handleChange = (_: string, value: WidgetHookEvents[] | '' | WidgetHookEvents): void => { + if (!Array.isArray(value)) return + setWidgetHooks(value) } return ( - - - {LABEL} - - - + ({ label: option, value: option }))} + onChange={handleChange} + renderValue={(selected) => { + const selectedHooks = Array.isArray(selected) ? selected : [] + + if (selectedHooks.length === 0) { + return EMPTY_VALUE_LABEL + } + + return selectedHooks.join(', ') + }} + /> ) } From a1c298941eec9da234b98ec6cfd0a2487c483542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Tue, 2 Jun 2026 11:33:59 +0200 Subject: [PATCH 035/110] fix: simplify widget configurator input change handling --- .../src/components/controls/ThemeControl.tsx | 1 - .../sections/advanced/AdvancedSectionForm.tsx | 37 ++++++-- .../sections/behavior/BehaviorSectionForm.tsx | 20 ++-- .../sections/layout/LayoutSectionForm.tsx | 6 +- .../trade-setup/TradeSetupSectionForm.tsx | 93 +++++++++++-------- .../components/sidebar/sidebar.component.tsx | 22 +++-- .../Select/CurrentTradeTypeControl.tsx | 31 ------- .../ui/controls/Select/LocaleControl.tsx | 2 - .../ui/controls/Select/NetworkControl.tsx | 67 ------------- .../ui/controls/Select/SelectInput.tsx | 4 +- .../ui/controls/Select/TradeModesControl.tsx | 35 ------- .../ui/controls/Select/WidgetHooksControl.tsx | 47 ---------- .../SwitchInput.test.tsx} | 13 +-- .../SwitchInput.tsx} | 10 +- .../src/configurator.constants.ts | 19 +++- .../src/hooks/useSyncWidgetNetwork.ts | 12 +-- 16 files changed, 132 insertions(+), 287 deletions(-) delete mode 100644 apps/widget-configurator/src/components/ui/controls/Select/CurrentTradeTypeControl.tsx delete mode 100644 apps/widget-configurator/src/components/ui/controls/Select/NetworkControl.tsx delete mode 100644 apps/widget-configurator/src/components/ui/controls/Select/TradeModesControl.tsx delete mode 100644 apps/widget-configurator/src/components/ui/controls/Select/WidgetHooksControl.tsx rename apps/widget-configurator/src/components/ui/controls/{BooleanSwitch/BooleanSwitchControl.test.tsx => SwitchInput/SwitchInput.test.tsx} (57%) rename apps/widget-configurator/src/components/ui/controls/{BooleanSwitch/BooleanSwitchControl.tsx => SwitchInput/SwitchInput.tsx} (86%) diff --git a/apps/widget-configurator/src/components/controls/ThemeControl.tsx b/apps/widget-configurator/src/components/controls/ThemeControl.tsx index 97e492f99fd..d91d3d27123 100644 --- a/apps/widget-configurator/src/components/controls/ThemeControl.tsx +++ b/apps/widget-configurator/src/components/controls/ThemeControl.tsx @@ -51,7 +51,6 @@ export interface ThemeControlProps { export function ThemeControl({ name, selectedValue, onChange }: ThemeControlProps): ReactNode { return ( >] = [ - values.enabledWidgetHooks, - (nextValue) => { - const resolvedValue = typeof nextValue === 'function' ? nextValue(values.enabledWidgetHooks) : nextValue - onChange('enabledWidgetHooks', resolvedValue) - }, - ] + const renderWidgetHooksValue = useCallback((value: SelectInputValue): ReactNode => { + const selectedHooks = Array.isArray(value) ? value : [] + + if (selectedHooks.length === 0) { + return 'No hooks selected' + } + + return selectedHooks.join(', ') + }, []) return ( <> @@ -47,6 +51,7 @@ export function AdvancedSectionForm({ values, onChange }: AdvancedSectionFormPro onChange('baseUrl', value === ADVANCED_DEFAULT_BASE_URL ? null : value) }} /> + - + + + - - onChange('disableProgressBar', !enabled)} /> - onChange('disablePostTradeTips', !enabled)} /> - onChange('disableTokenImport', !enabled)} /> - onChange('hideRecentTokens', !enabled)} /> - onChange('hideFavoriteTokens', !enabled)} /> - onChange('hideBridgeInfo', !enabled)} /> - onChange('hideOrdersTable', !enabled)} /> - onChange('disableTradeWhenPriceImpactIsUnknown', enabled)} diff --git a/apps/widget-configurator/src/components/sidebar/sections/layout/LayoutSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/layout/LayoutSectionForm.tsx index ba6e7cc0db1..4abc68d7e1f 100644 --- a/apps/widget-configurator/src/components/sidebar/sections/layout/LayoutSectionForm.tsx +++ b/apps/widget-configurator/src/components/sidebar/sections/layout/LayoutSectionForm.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from 'react' import { AppearanceStyleControls } from '../../../controls/AppearanceStyleControls' -import { BooleanSwitchControl } from '../../../ui/controls/BooleanSwitch/BooleanSwitchControl' +import { SwitchInput } from '../../../ui/controls/SwitchInput/SwitchInput' import type { JsonState, OnJsonStateChange } from '../../../../hooks/useJsonState' import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' @@ -33,13 +33,13 @@ export function LayoutSectionForm({ }: LayoutSectionFormProps): ReactNode { return ( <> - onChange('autoResizeEnabled', checked)} helperText="When enabled, the iframe height adjusts automatically to fit its content." /> - onChange('showIframeOutline', checked)} diff --git a/apps/widget-configurator/src/components/sidebar/sections/trade-setup/TradeSetupSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/trade-setup/TradeSetupSectionForm.tsx index b8b3c629f31..d303cdd7bd0 100644 --- a/apps/widget-configurator/src/components/sidebar/sections/trade-setup/TradeSetupSectionForm.tsx +++ b/apps/widget-configurator/src/components/sidebar/sections/trade-setup/TradeSetupSectionForm.tsx @@ -1,19 +1,13 @@ -import type { Dispatch, ReactNode, SetStateAction } from 'react' +import type { ReactNode } from 'react' +import { useMemo } from 'react' +import { CHAIN_INFO } from '@cowprotocol/common-const' import { useAvailableChains } from '@cowprotocol/common-hooks' -import type { SupportedChainId } from '@cowprotocol/cow-sdk' -import type { TradeType } from '@cowprotocol/widget-lib' +import { isChainDeprecated, type SupportedChainId } from '@cowprotocol/cow-sdk' -import { IS_IFRAME } from '../../../../configurator.constants' -import { BooleanSwitchControl } from '../../../ui/controls/BooleanSwitch/BooleanSwitchControl' -import { CurrentTradeTypeControl } from '../../../ui/controls/Select/CurrentTradeTypeControl' -import { - NetworkControl, - type NetworkOption, - getNetworkOption, - NetworkOptions, -} from '../../../ui/controls/Select/NetworkControl' -import { TradeModesControl } from '../../../ui/controls/Select/TradeModesControl' +import { IS_IFRAME, TRADE_MODES_OPTIONS, TRADE_TYPE_OPTIONS } from '../../../../configurator.constants' +import { SelectInput, SelectInputOption } from '../../../ui/controls/Select/SelectInput' +import { SwitchInput } from '../../../ui/controls/SwitchInput/SwitchInput' import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' @@ -22,41 +16,60 @@ interface TradeSetupSectionFormProps { onChange: ConfiguratorFormChangeHandler } -function resolveNextState(current: T, next: SetStateAction): T { - return typeof next === 'function' ? (next as (prevState: T) => T)(current) : next -} - export function TradeSetupSectionForm({ values, onChange }: TradeSetupSectionFormProps): ReactNode { const availableChains = useAvailableChains() - const standaloneMode = values.widgetMode === 'standalone' - const tradeModesState: [TradeType[], Dispatch>] = [ - values.enabledTradeTypes, - (nextValue) => onChange('enabledTradeTypes', resolveNextState(values.enabledTradeTypes, nextValue)), - ] + const chainOptions: SelectInputOption[] = useMemo(() => { + const availableChainsSet = new Set(availableChains) + + const chainOptions: SelectInputOption[] = Object.entries(CHAIN_INFO).map( + ([chainIdStr, chainInfo]) => { + const chainId = +chainIdStr as SupportedChainId - const tradeTypeState: [TradeType, Dispatch>] = [ - values.currentTradeType, - (nextValue) => onChange('currentTradeType', resolveNextState(values.currentTradeType, nextValue)), - ] + return { + value: chainId, + label: `${chainInfo.label}${isChainDeprecated(chainId) ? ' (deprecated)' : ''}`, + disabled: !availableChainsSet.has(chainId), + } + }, + ) - const selectedNetwork = getNetworkOption(values.chainId) || NetworkOptions[0] - const networkState: [NetworkOption, Dispatch>] = [ - selectedNetwork, - (nextValue) => { - const nextOption = resolveNextState(selectedNetwork, nextValue) - onChange('chainId', nextOption.chainId as SupportedChainId) - }, - ] + return chainOptions + }, [availableChains]) return ( <> - - - {!IS_IFRAME ? ( - - ) : null} - (Array.isArray(selected) ? selected.join(', ') : selected)} + /> + + + + + + onChange('disableCrossChainSwap', !enabled)} diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index 75ce40aed06..bce11e9608a 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -1,7 +1,8 @@ import { ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { DEFAULT_PARTNER_FEE_RECIPIENT_PER_NETWORK } from '@cowprotocol/common-const' -import { CowSwapWidgetParams } from '@cowprotocol/widget-lib' +import { SupportedChainId } from '@cowprotocol/cow-sdk' +import type { CowSwapWidgetParams } from '@cowprotocol/widget-lib' import Box from '@mui/material/Box' import Drawer from '@mui/material/Drawer' @@ -36,7 +37,6 @@ import { UseToastsManagerReturn } from '../../hooks/useToastsManager' import { ColorModeContext } from '../../theme/ColorModeContext' import { CONFIGURATOR_DEFAULT_WIDGET_BASE_URL } from '../../utils/baseUrl' import { AccordionFormSection } from '../ui/Accordion/AccordionFormSection' -import { type NetworkOption, NetworkOptions } from '../ui/controls/Select/NetworkControl' import type { Theme } from '@mui/material/styles' import type * as CSS from 'csstype' @@ -108,7 +108,7 @@ export function Sidebar({ locale: '', enabledTradeTypes: TRADE_MODES, currentTradeType: TRADE_MODES[0], - chainId: NetworkOptions[0].chainId, + chainId: SupportedChainId.MAINNET, disableCrossChainSwap: false, sellToken: DEFAULT_STATE.sellToken, sellTokenAmount: DEFAULT_STATE.sellAmount, @@ -188,13 +188,6 @@ export function Sidebar({ [], ) as ConfiguratorFormChangeHandler - const setNetworkControlState = useCallback( - (option: NetworkOption): void => { - handleConfiguratorFormChange('chainId', option.chainId) - }, - [handleConfiguratorFormChange], - ) - const iframeStyleJson = useMemo( () => parseJsonField(configuratorFormValues.iframeStyleJson, {}), [configuratorFormValues.iframeStyleJson], @@ -295,6 +288,15 @@ export function Sidebar({ onStateChange(configuratorState) }, [configuratorState, onStateChange]) + // Sync widget network with the selected network in the configurator: + + const setNetworkControlState = useCallback( + (chainId: SupportedChainId): void => { + handleConfiguratorFormChange('chainId', chainId) + }, + [handleConfiguratorFormChange], + ) + useSyncWidgetNetwork(configuratorFormValues.chainId, setNetworkControlState, standaloneMode) /* diff --git a/apps/widget-configurator/src/components/ui/controls/Select/CurrentTradeTypeControl.tsx b/apps/widget-configurator/src/components/ui/controls/Select/CurrentTradeTypeControl.tsx deleted file mode 100644 index a2b6a9dac98..00000000000 --- a/apps/widget-configurator/src/components/ui/controls/Select/CurrentTradeTypeControl.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { Dispatch, ReactNode, SetStateAction } from 'react' - -import type { TradeType } from '@cowprotocol/widget-lib' - -import { SelectInput } from './SelectInput' - -import { TRADE_MODES } from '../../../../configurator.constants' - -const LABEL = 'Current trade type' - -export interface CurrentTradeTypeControlProps { - state: [TradeType, Dispatch>] -} - -export function CurrentTradeTypeControl({ state }: CurrentTradeTypeControlProps): ReactNode { - const [tradeType, setTradeType] = state - - return ( - ({ label: option, value: option }))} - onChange={(_, value) => { - if (value === '' || Array.isArray(value)) return - setTradeType(value as TradeType) - }} - /> - ) -} diff --git a/apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.tsx b/apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.tsx index 26732acdf4b..85ce12ca007 100644 --- a/apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.tsx +++ b/apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.tsx @@ -5,7 +5,6 @@ import { LOCALE_DISPLAY_NAMES, SupportedLocale, SUPPORTED_LOCALES } from '@cowpr import { SelectInput } from './SelectInput' const LABEL = 'Forced locale' -const LABEL_ID = 'select-locale-label' type LocaleControlState = [SupportedLocale | '', Dispatch>] @@ -14,7 +13,6 @@ export function LocaleControl({ state }: { state: LocaleControlState }): ReactNo return ( ((key) => { - const chainId = +key as SupportedChainId - return { chainId, label: CHAIN_INFO[chainId].label } -}) - -const DEFAULT_CHAIN_ID = NetworkOptions[0].chainId - -const LABEL = 'Network' - -export const getNetworkOption = (chainId: SupportedChainId): NetworkOption | undefined => - NetworkOptions.find((item) => item.chainId === chainId) - -export interface NetworkControlProps { - standaloneMode: boolean - state: [NetworkOption, Dispatch>] - availableChains: SupportedChainId[] -} - -export function NetworkControl({ state, standaloneMode, availableChains }: NetworkControlProps): ReactNode { - const [network, setNetwork] = state - - const switchNetwork = (chainId: number): void => { - const targetChainId = chainId || DEFAULT_CHAIN_ID - const targetNetwork = getNetworkOption(targetChainId) - - if (targetNetwork) { - setNetwork(targetNetwork) - } - } - - return ( - { - const option = NetworkOptions.find((o) => o.chainId === chainId) - if (!option) return null - - return { - label: `${option.label}${isChainDeprecated(chainId) ? ' (deprecated)' : ''}`, - value: option.chainId, - } - }) - .filter((option): option is { label: string; value: SupportedChainId } => option !== null)} - onChange={(_, value) => { - if (value === '' || Array.isArray(value)) return - switchNetwork(value as number) - }} - /> - ) -} diff --git a/apps/widget-configurator/src/components/ui/controls/Select/SelectInput.tsx b/apps/widget-configurator/src/components/ui/controls/Select/SelectInput.tsx index 464fb6ad428..68281531a38 100644 --- a/apps/widget-configurator/src/components/ui/controls/Select/SelectInput.tsx +++ b/apps/widget-configurator/src/components/ui/controls/Select/SelectInput.tsx @@ -59,7 +59,6 @@ export interface SelectInputProps { onChange: (name: string, value: SelectInputValue) => void multiple?: boolean id?: string - labelId?: string size?: 'small' | 'medium' fullWidth?: boolean displayEmpty?: boolean @@ -93,7 +92,6 @@ export function SelectInput({ id, name, label, - labelId, value, options, onChange, @@ -108,7 +106,7 @@ export function SelectInput({ renderOptionLabel, }: SelectInputProps): ReactNode { const resolvedId = id ?? `select-${name}` - const resolvedLabelId = labelId ?? `${resolvedId}-label` + const resolvedLabelId = `${resolvedId}-label` const normalizedValue = multiple ? (Array.isArray(value) ? value : []) : (value as TValue | '') diff --git a/apps/widget-configurator/src/components/ui/controls/Select/TradeModesControl.tsx b/apps/widget-configurator/src/components/ui/controls/Select/TradeModesControl.tsx deleted file mode 100644 index 2c03104a65e..00000000000 --- a/apps/widget-configurator/src/components/ui/controls/Select/TradeModesControl.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Dispatch, ReactNode, SetStateAction } from 'react' - -import { TradeType } from '@cowprotocol/widget-lib' - -import { SelectInput } from './SelectInput' - -const LABEL = 'Trade types' - -export function TradeModesControl({ - state, -}: { - state: [TradeType[], Dispatch>] -}): ReactNode { - const [tradeModes, setTradeModes] = state - - const handleTradeModeChange = (_: string, value: TradeType[] | '' | TradeType): void => { - const nextTradeModes = Array.isArray(value) ? value : [] - if (!nextTradeModes.length) return - - setTradeModes(nextTradeModes) - } - - return ( - ({ label: option, value: option }))} - onChange={handleTradeModeChange} - renderValue={(selected) => (Array.isArray(selected) ? selected.join(', ') : selected)} - /> - ) -} diff --git a/apps/widget-configurator/src/components/ui/controls/Select/WidgetHooksControl.tsx b/apps/widget-configurator/src/components/ui/controls/Select/WidgetHooksControl.tsx deleted file mode 100644 index 4193efe19a2..00000000000 --- a/apps/widget-configurator/src/components/ui/controls/Select/WidgetHooksControl.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Dispatch, ReactNode, SetStateAction } from 'react' - -import { WidgetHookEvents } from '@cowprotocol/widget-lib' - -import { SelectInput } from './SelectInput' - -import { WIDGET_HOOKS } from '../../../../configurator.constants' - -const LABEL = 'Widget hooks' -const EMPTY_VALUE_LABEL = 'No hooks selected' -const LABEL_ID = 'widget-hooks-select-label' - -export function WidgetHooksControl({ - state, -}: { - state: [WidgetHookEvents[], Dispatch>] -}): ReactNode { - const [widgetHooks, setWidgetHooks] = state - - const handleChange = (_: string, value: WidgetHookEvents[] | '' | WidgetHookEvents): void => { - if (!Array.isArray(value)) return - setWidgetHooks(value) - } - - return ( - ({ label: option, value: option }))} - onChange={handleChange} - renderValue={(selected) => { - const selectedHooks = Array.isArray(selected) ? selected : [] - - if (selectedHooks.length === 0) { - return EMPTY_VALUE_LABEL - } - - return selectedHooks.join(', ') - }} - /> - ) -} diff --git a/apps/widget-configurator/src/components/ui/controls/BooleanSwitch/BooleanSwitchControl.test.tsx b/apps/widget-configurator/src/components/ui/controls/SwitchInput/SwitchInput.test.tsx similarity index 57% rename from apps/widget-configurator/src/components/ui/controls/BooleanSwitch/BooleanSwitchControl.test.tsx rename to apps/widget-configurator/src/components/ui/controls/SwitchInput/SwitchInput.test.tsx index 4b8a68b7fd1..bb060d264e9 100644 --- a/apps/widget-configurator/src/components/ui/controls/BooleanSwitch/BooleanSwitchControl.test.tsx +++ b/apps/widget-configurator/src/components/ui/controls/SwitchInput/SwitchInput.test.tsx @@ -1,16 +1,11 @@ import { fireEvent, render, screen } from '@testing-library/react' -import { BooleanSwitchControl } from './BooleanSwitchControl' +import { SwitchInput } from './SwitchInput' -describe('BooleanSwitchControl', () => { +describe('SwitchInput', () => { it('renders label and helper text', () => { render( - , + , ) expect(screen.getByText('Show orders table')).not.toBeNull() @@ -20,7 +15,7 @@ describe('BooleanSwitchControl', () => { it('passes the next checked state to the handler', () => { const onChange = jest.fn() - render() + render() fireEvent.click(screen.getByRole('checkbox', { name: 'Show orders table' })) diff --git a/apps/widget-configurator/src/components/ui/controls/BooleanSwitch/BooleanSwitchControl.tsx b/apps/widget-configurator/src/components/ui/controls/SwitchInput/SwitchInput.tsx similarity index 86% rename from apps/widget-configurator/src/components/ui/controls/BooleanSwitch/BooleanSwitchControl.tsx rename to apps/widget-configurator/src/components/ui/controls/SwitchInput/SwitchInput.tsx index eb07947d61a..0c42ecefa48 100644 --- a/apps/widget-configurator/src/components/ui/controls/BooleanSwitch/BooleanSwitchControl.tsx +++ b/apps/widget-configurator/src/components/ui/controls/SwitchInput/SwitchInput.tsx @@ -6,7 +6,7 @@ import Switch from '@mui/material/Switch' import Tooltip from '@mui/material/Tooltip' import Typography from '@mui/material/Typography' -interface BooleanSwitchControlProps { +interface SwitchInputProps { checked: boolean label: string onChange: (checked: boolean) => void @@ -14,13 +14,7 @@ interface BooleanSwitchControlProps { tooltip?: string } -export function BooleanSwitchControl({ - checked, - label, - onChange, - helperText, - tooltip, -}: BooleanSwitchControlProps): ReactNode { +export function SwitchInput({ checked, label, onChange, helperText, tooltip }: SwitchInputProps): ReactNode { const labelContent = tooltip ? ( {label} diff --git a/apps/widget-configurator/src/configurator.constants.ts b/apps/widget-configurator/src/configurator.constants.ts index d536a76b442..2f7b3025379 100644 --- a/apps/widget-configurator/src/configurator.constants.ts +++ b/apps/widget-configurator/src/configurator.constants.ts @@ -2,6 +2,7 @@ import { COW_CDN } from '@cowprotocol/common-const' import { CowWidgetEventListeners, CowWidgetEvents, ToastMessageType } from '@cowprotocol/events' import { CowSwapWidgetPaletteParams, TokenInfo, TradeType, WidgetHookEvents } from '@cowprotocol/widget-lib' +import { SelectInputOption } from './components/ui/controls/Select/SelectInput' import { TokenListItem } from './configurator.types' // ENV: @@ -22,11 +23,25 @@ export const WIDGET_PREVIEW_READY_FALLBACK_MS = 60_000 export const UTM_PARAMS = 'utm_content=cow-widget-configurator&utm_medium=web&utm_source=widget.cow.fi' as const -// CoW DAO addresses +// Form options: export const TRADE_MODES = [TradeType.SWAP, TradeType.LIMIT, TradeType.ADVANCED, TradeType.YIELD] -export const WIDGET_HOOKS = Object.values(WidgetHookEvents) +export const TRADE_MODES_OPTIONS: SelectInputOption[] = TRADE_MODES.map((option) => ({ + label: option, + value: option, +})) + +export const TRADE_TYPE_OPTIONS: SelectInputOption[] = Object.values(TradeType).map((option) => ({ + label: option, + value: option, +})) + +export const WIDGET_HOOKS_OPTIONS: SelectInputOption[] = Object.values(WidgetHookEvents).map( + (option) => ({ label: option, value: option }), +) + +// CoW DAO addresses export const DEFAULT_STATE = { sellToken: 'USDC', diff --git a/apps/widget-configurator/src/hooks/useSyncWidgetNetwork.ts b/apps/widget-configurator/src/hooks/useSyncWidgetNetwork.ts index 8fdaa717a7c..091af7fdac6 100644 --- a/apps/widget-configurator/src/hooks/useSyncWidgetNetwork.ts +++ b/apps/widget-configurator/src/hooks/useSyncWidgetNetwork.ts @@ -4,11 +4,9 @@ import type { SupportedChainId } from '@cowprotocol/cow-sdk' import { useConnection, useSwitchChain } from 'wagmi' -import { getNetworkOption, NetworkOption } from '../components/ui/controls/Select/NetworkControl' - export function useSyncWidgetNetwork( chainId: SupportedChainId, - setNetworkControlState: (option: NetworkOption) => void, + setNetworkControlState: (chainId: SupportedChainId) => void, standaloneMode: boolean, ): void { const { chainId: walletChainId, isConnected } = useConnection() @@ -24,12 +22,8 @@ export function useSyncWidgetNetwork( useEffect(() => { if (!isConnected || !walletChainId) return - const newNetwork = getNetworkOption(walletChainId as SupportedChainId) - - if (newNetwork) { - currentChainIdRef.current = walletChainId as SupportedChainId - setNetworkControlState(newNetwork) - } + currentChainIdRef.current = walletChainId as SupportedChainId + setNetworkControlState(walletChainId as SupportedChainId) }, [isConnected, walletChainId, setNetworkControlState]) // Send a request to switch network when user changes network in the configurator From 8a149a5f98ab41beff4d6841bfc0cd0c8b4e3383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Tue, 2 Jun 2026 12:03:04 +0200 Subject: [PATCH 036/110] fix: remove id attr from widget configurator inputs --- .../src/components/controls/ThemeControl.tsx | 1 - .../src/components/controls/TokenListControl.tsx | 1 - .../sidebar/sections/advanced/AdvancedSectionForm.tsx | 1 - .../sidebar/sections/trade-setup/TradeSetupSectionForm.tsx | 3 --- .../ui/controls/CurrencyInput/CurrencyInputControl.tsx | 1 - .../src/components/ui/controls/Select/LocaleControl.tsx | 1 - .../src/components/ui/controls/Select/SelectInput.tsx | 4 +--- 7 files changed, 1 insertion(+), 11 deletions(-) diff --git a/apps/widget-configurator/src/components/controls/ThemeControl.tsx b/apps/widget-configurator/src/components/controls/ThemeControl.tsx index d91d3d27123..a89b1750126 100644 --- a/apps/widget-configurator/src/components/controls/ThemeControl.tsx +++ b/apps/widget-configurator/src/components/controls/ThemeControl.tsx @@ -51,7 +51,6 @@ export interface ThemeControlProps { export function ThemeControl({ name, selectedValue, onChange }: ThemeControlProps): ReactNode { return ( { options: readonly SelectInputOption[] onChange: (name: string, value: SelectInputValue) => void multiple?: boolean - id?: string size?: 'small' | 'medium' fullWidth?: boolean displayEmpty?: boolean @@ -89,7 +88,6 @@ function coerceMultiValue( // eslint-disable-next-line max-lines-per-function, complexity export function SelectInput({ - id, name, label, value, @@ -105,7 +103,7 @@ export function SelectInput({ renderValue, renderOptionLabel, }: SelectInputProps): ReactNode { - const resolvedId = id ?? `select-${name}` + const resolvedId = `select-${name}` const resolvedLabelId = `${resolvedId}-label` const normalizedValue = multiple ? (Array.isArray(value) ? value : []) : (value as TValue | '') From f382417a281e5488854e9b273cb468a3509ff378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Tue, 2 Jun 2026 12:16:28 +0200 Subject: [PATCH 037/110] feat: finish simplifying configurator inputs --- .../controls/AddCustomListDialog.tsx | 2 +- .../controls/AppearanceStyleControls.tsx | 4 +- .../Select => controls}/ModeControl.tsx | 5 ++- .../src/components/controls/ThemeControl.tsx | 2 +- .../components/controls/TokenListControl.tsx | 2 +- .../advanced/AdvancedSectionForm.constants.ts | 2 +- .../sections/advanced/AdvancedSectionForm.tsx | 8 ++-- .../sections/basics/BasicsSectionForm.tsx | 45 ++++++++++--------- .../sections/behavior/BehaviorSectionForm.tsx | 4 +- .../CustomizationSectionForm.tsx | 2 +- .../deadlines/DeadlinesSectionForm.tsx | 2 +- .../integrations/IntegrationsSectionForm.tsx | 2 +- .../sections/layout/LayoutSectionForm.tsx | 2 +- .../sections/tokens/TokensSectionForm.tsx | 2 +- .../trade-setup/TradeSetupSectionForm.tsx | 4 +- .../components/sidebar/sidebar.component.tsx | 6 +-- .../JsonInput/JsonInput.component.tsx | 44 ------------------ .../ui/controls/Select/LocaleControl.test.tsx | 11 ----- .../ui/controls/Select/LocaleControl.tsx | 37 --------------- .../ui/controls/Select/ModeControl.test.tsx | 19 -------- .../TextInput/TextInput.component.tsx | 33 -------------- .../TextareaInput/TextareaInput.component.tsx | 36 --------------- .../BaseTextInput/BaseTextInput.component.tsx | 0 .../CurrencyInput/CurrencyInputControl.tsx | 0 .../inputs/JsonInput/JsonInput.component.tsx | 41 +++++++++++++++++ .../NumberInput/NumberInput.component.tsx | 0 .../PresetsButtons.component.tsx | 0 .../Select/SelectInput.tsx | 0 .../SwitchInput/SwitchInput.test.tsx | 0 .../SwitchInput/SwitchInput.tsx | 0 .../inputs/TextInput/TextInput.component.tsx | 22 +++++++++ .../TextareaInput/TextareaInput.component.tsx | 33 ++++++++++++++ .../src/configurator.constants.ts | 9 +++- 33 files changed, 150 insertions(+), 229 deletions(-) rename apps/widget-configurator/src/components/{ui/controls/Select => controls}/ModeControl.tsx (91%) delete mode 100644 apps/widget-configurator/src/components/ui/controls/JsonInput/JsonInput.component.tsx delete mode 100644 apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.test.tsx delete mode 100644 apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.tsx delete mode 100644 apps/widget-configurator/src/components/ui/controls/Select/ModeControl.test.tsx delete mode 100644 apps/widget-configurator/src/components/ui/controls/TextInput/TextInput.component.tsx delete mode 100644 apps/widget-configurator/src/components/ui/controls/TextareaInput/TextareaInput.component.tsx rename apps/widget-configurator/src/components/ui/{controls => inputs}/BaseTextInput/BaseTextInput.component.tsx (100%) rename apps/widget-configurator/src/components/ui/{controls => inputs}/CurrencyInput/CurrencyInputControl.tsx (100%) create mode 100644 apps/widget-configurator/src/components/ui/inputs/JsonInput/JsonInput.component.tsx rename apps/widget-configurator/src/components/ui/{controls => inputs}/NumberInput/NumberInput.component.tsx (100%) rename apps/widget-configurator/src/components/ui/{controls => inputs}/PresetsButtons/PresetsButtons.component.tsx (100%) rename apps/widget-configurator/src/components/ui/{controls => inputs}/Select/SelectInput.tsx (100%) rename apps/widget-configurator/src/components/ui/{controls => inputs}/SwitchInput/SwitchInput.test.tsx (100%) rename apps/widget-configurator/src/components/ui/{controls => inputs}/SwitchInput/SwitchInput.tsx (100%) create mode 100644 apps/widget-configurator/src/components/ui/inputs/TextInput/TextInput.component.tsx create mode 100644 apps/widget-configurator/src/components/ui/inputs/TextareaInput/TextareaInput.component.tsx diff --git a/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx b/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx index e9b3d625c64..043b0044d6f 100644 --- a/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx +++ b/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx @@ -8,7 +8,7 @@ import Tabs from '@mui/material/Tabs' import { DEFAULT_CUSTOM_TOKENS } from '../../configurator.constants' import { parseCustomTokensInput } from '../../utils/parseCustomTokensInput' -import { JsonInput } from '../ui/controls/JsonInput/JsonInput.component' +import { JsonInput } from '../ui/inputs/JsonInput/JsonInput.component' type AddCustomListDialogProps = { open: boolean diff --git a/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx b/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx index a3453f8d179..3206653b548 100644 --- a/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx +++ b/apps/widget-configurator/src/components/controls/AppearanceStyleControls.tsx @@ -5,8 +5,8 @@ import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' import { jsonHelperText } from '../../utils/jsonFieldParsing' -import { JsonInput } from '../ui/controls/JsonInput/JsonInput.component' -import { PresetOption, PresetsButtons } from '../ui/controls/PresetsButtons/PresetsButtons.component' +import { JsonInput } from '../ui/inputs/JsonInput/JsonInput.component' +import { PresetOption, PresetsButtons } from '../ui/inputs/PresetsButtons/PresetsButtons.component' import type { JsonState, OnJsonStateChange } from '../../hooks/useJsonState' import type * as CSS from 'csstype' diff --git a/apps/widget-configurator/src/components/ui/controls/Select/ModeControl.tsx b/apps/widget-configurator/src/components/controls/ModeControl.tsx similarity index 91% rename from apps/widget-configurator/src/components/ui/controls/Select/ModeControl.tsx rename to apps/widget-configurator/src/components/controls/ModeControl.tsx index 9d912470ee4..59757db8fe5 100644 --- a/apps/widget-configurator/src/components/ui/controls/Select/ModeControl.tsx +++ b/apps/widget-configurator/src/components/controls/ModeControl.tsx @@ -10,7 +10,8 @@ import RadioGroup from '@mui/material/RadioGroup' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' -import { HelpTooltipButton } from '../../HelpTooltipButton/HelpTooltipButton' +import { IS_IFRAME } from '../../configurator.constants' +import { HelpTooltipButton } from '../ui/HelpTooltipButton/HelpTooltipButton' type WidgetMode = 'dapp' | 'standalone' @@ -38,7 +39,7 @@ export function ModeControl({ value, onChange }: ModeControlProps): ReactNode { ) return ( - + Mode diff --git a/apps/widget-configurator/src/components/controls/ThemeControl.tsx b/apps/widget-configurator/src/components/controls/ThemeControl.tsx index a89b1750126..d1873a338bf 100644 --- a/apps/widget-configurator/src/components/controls/ThemeControl.tsx +++ b/apps/widget-configurator/src/components/controls/ThemeControl.tsx @@ -5,7 +5,7 @@ import LightModeIcon from '@mui/icons-material/LightMode' import Box from '@mui/material/Box' import Typography from '@mui/material/Typography' -import { SelectInput, type SelectInputOption } from '../ui/controls/Select/SelectInput' +import { SelectInput, type SelectInputOption } from '../ui/inputs/Select/SelectInput' export type ThemeOptionValue = 'light' | 'dark' diff --git a/apps/widget-configurator/src/components/controls/TokenListControl.tsx b/apps/widget-configurator/src/components/controls/TokenListControl.tsx index df439201757..840a43e7e4c 100644 --- a/apps/widget-configurator/src/components/controls/TokenListControl.tsx +++ b/apps/widget-configurator/src/components/controls/TokenListControl.tsx @@ -7,7 +7,7 @@ import { Box, Button, Chip, ListItemText } from '@mui/material' import { AddCustomListDialog } from './AddCustomListDialog' import { TokenListItem } from '../../configurator.types' -import { SelectInput } from '../ui/controls/Select/SelectInput' +import { SelectInput } from '../ui/inputs/Select/SelectInput' const ITEM_HEIGHT = 48 const ITEM_PADDING_TOP = 8 diff --git a/apps/widget-configurator/src/components/sidebar/sections/advanced/AdvancedSectionForm.constants.ts b/apps/widget-configurator/src/components/sidebar/sections/advanced/AdvancedSectionForm.constants.ts index eaca20ef7d3..9be963dfb76 100644 --- a/apps/widget-configurator/src/components/sidebar/sections/advanced/AdvancedSectionForm.constants.ts +++ b/apps/widget-configurator/src/components/sidebar/sections/advanced/AdvancedSectionForm.constants.ts @@ -1,5 +1,5 @@ import { CONFIGURATOR_DEFAULT_WIDGET_BASE_URL } from '../../../../utils/baseUrl' -import { PresetOption } from '../../../ui/controls/PresetsButtons/PresetsButtons.component' +import { PresetOption } from '../../../ui/inputs/PresetsButtons/PresetsButtons.component' export const ADVANCED_BASE_URL_PRESETS_OPTIONS: PresetOption[] = [ { label: 'Local', value: 'http://localhost:3000' }, diff --git a/apps/widget-configurator/src/components/sidebar/sections/advanced/AdvancedSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/advanced/AdvancedSectionForm.tsx index e4a17eddd59..03a0358ee3a 100644 --- a/apps/widget-configurator/src/components/sidebar/sections/advanced/AdvancedSectionForm.tsx +++ b/apps/widget-configurator/src/components/sidebar/sections/advanced/AdvancedSectionForm.tsx @@ -7,10 +7,10 @@ import { ADVANCED_BASE_URL_PRESETS_OPTIONS, ADVANCED_DEFAULT_BASE_URL } from './ import { WIDGET_HOOKS_OPTIONS } from '../../../../configurator.constants' import { jsonHelperText } from '../../../../utils/jsonFieldParsing' -import { JsonInput } from '../../../ui/controls/JsonInput/JsonInput.component' -import { PresetsButtons } from '../../../ui/controls/PresetsButtons/PresetsButtons.component' -import { SelectInput, SelectInputValue } from '../../../ui/controls/Select/SelectInput' -import { TextInput } from '../../../ui/controls/TextInput/TextInput.component' +import { JsonInput } from '../../../ui/inputs/JsonInput/JsonInput.component' +import { PresetsButtons } from '../../../ui/inputs/PresetsButtons/PresetsButtons.component' +import { SelectInput, SelectInputValue } from '../../../ui/inputs/Select/SelectInput' +import { TextInput } from '../../../ui/inputs/TextInput/TextInput.component' import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' diff --git a/apps/widget-configurator/src/components/sidebar/sections/basics/BasicsSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/basics/BasicsSectionForm.tsx index 710aadbd822..aeb8d95a085 100644 --- a/apps/widget-configurator/src/components/sidebar/sections/basics/BasicsSectionForm.tsx +++ b/apps/widget-configurator/src/components/sidebar/sections/basics/BasicsSectionForm.tsx @@ -1,12 +1,12 @@ -import type { ChangeEvent, Dispatch, ReactNode, SetStateAction } from 'react' +import type { ReactNode } from 'react' -import { SupportedLocale } from '@cowprotocol/common-const' +import { LOCALE_DISPLAY_NAMES, SupportedLocale } from '@cowprotocol/common-const' -import { IS_IFRAME } from '../../../../configurator.constants' +import { LOCALE_OPTIONS } from '../../../../configurator.constants' +import { ModeControl } from '../../../controls/ModeControl' import { COMMENTS_BY_PARAM_NAME } from '../../../snippet/snippet.const' -import { LocaleControl } from '../../../ui/controls/Select/LocaleControl' -import { ModeControl } from '../../../ui/controls/Select/ModeControl' -import { TextInput } from '../../../ui/controls/TextInput/TextInput.component' +import { SelectInput } from '../../../ui/inputs/Select/SelectInput' +import { TextInput } from '../../../ui/inputs/TextInput/TextInput.component' import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' @@ -15,20 +15,7 @@ interface BasicsSectionFormProps { onChange: ConfiguratorFormChangeHandler } -function resolveNextState(current: T, next: SetStateAction): T { - return typeof next === 'function' ? (next as (prevState: T) => T)(current) : next -} - export function BasicsSectionForm({ values, onChange }: BasicsSectionFormProps): ReactNode { - const localeState: [SupportedLocale | '', Dispatch>] = [ - values.locale, - (nextValue) => onChange('locale', resolveNextState(values.locale, nextValue)), - ] - - const handleModeChange = (event: ChangeEvent): void => { - onChange(event) - } - return ( <> - {!IS_IFRAME ? : null} - + + + + { + if (!selected || Array.isArray(selected)) { + return 'Browser default' + } + + return LOCALE_DISPLAY_NAMES[selected as SupportedLocale] || selected + }} + /> ) } diff --git a/apps/widget-configurator/src/components/sidebar/sections/behavior/BehaviorSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/behavior/BehaviorSectionForm.tsx index 4fb1a553fb1..6eff61bab07 100644 --- a/apps/widget-configurator/src/components/sidebar/sections/behavior/BehaviorSectionForm.tsx +++ b/apps/widget-configurator/src/components/sidebar/sections/behavior/BehaviorSectionForm.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from 'react' -import { NumberInput } from '../../../ui/controls/NumberInput/NumberInput.component' -import { SwitchInput } from '../../../ui/controls/SwitchInput/SwitchInput' +import { NumberInput } from '../../../ui/inputs/NumberInput/NumberInput.component' +import { SwitchInput } from '../../../ui/inputs/SwitchInput/SwitchInput' import type { UseToastsManagerReturn } from '../../../../hooks/useToastsManager' import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' diff --git a/apps/widget-configurator/src/components/sidebar/sections/customization/CustomizationSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/customization/CustomizationSectionForm.tsx index 2a304953a55..5a8c69588f1 100644 --- a/apps/widget-configurator/src/components/sidebar/sections/customization/CustomizationSectionForm.tsx +++ b/apps/widget-configurator/src/components/sidebar/sections/customization/CustomizationSectionForm.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from 'react' import type { CowSwapWidgetParams } from '@cowprotocol/widget-lib' -import { TextInput } from '../../../ui/controls/TextInput/TextInput.component' +import { TextInput } from '../../../ui/inputs/TextInput/TextInput.component' import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' diff --git a/apps/widget-configurator/src/components/sidebar/sections/deadlines/DeadlinesSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/deadlines/DeadlinesSectionForm.tsx index a8b3859ebd2..d2fef0ca67d 100644 --- a/apps/widget-configurator/src/components/sidebar/sections/deadlines/DeadlinesSectionForm.tsx +++ b/apps/widget-configurator/src/components/sidebar/sections/deadlines/DeadlinesSectionForm.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' -import { NumberInput } from '../../../ui/controls/NumberInput/NumberInput.component' +import { NumberInput } from '../../../ui/inputs/NumberInput/NumberInput.component' import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' diff --git a/apps/widget-configurator/src/components/sidebar/sections/integrations/IntegrationsSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/integrations/IntegrationsSectionForm.tsx index 29f603ed931..a2c21fe9025 100644 --- a/apps/widget-configurator/src/components/sidebar/sections/integrations/IntegrationsSectionForm.tsx +++ b/apps/widget-configurator/src/components/sidebar/sections/integrations/IntegrationsSectionForm.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' -import { NumberInput } from '../../../ui/controls/NumberInput/NumberInput.component' +import { NumberInput } from '../../../ui/inputs/NumberInput/NumberInput.component' import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' diff --git a/apps/widget-configurator/src/components/sidebar/sections/layout/LayoutSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/layout/LayoutSectionForm.tsx index 4abc68d7e1f..284c70ed933 100644 --- a/apps/widget-configurator/src/components/sidebar/sections/layout/LayoutSectionForm.tsx +++ b/apps/widget-configurator/src/components/sidebar/sections/layout/LayoutSectionForm.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from 'react' import { AppearanceStyleControls } from '../../../controls/AppearanceStyleControls' -import { SwitchInput } from '../../../ui/controls/SwitchInput/SwitchInput' +import { SwitchInput } from '../../../ui/inputs/SwitchInput/SwitchInput' import type { JsonState, OnJsonStateChange } from '../../../../hooks/useJsonState' import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' diff --git a/apps/widget-configurator/src/components/sidebar/sections/tokens/TokensSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/tokens/TokensSectionForm.tsx index bb6846e3883..dffe3914606 100644 --- a/apps/widget-configurator/src/components/sidebar/sections/tokens/TokensSectionForm.tsx +++ b/apps/widget-configurator/src/components/sidebar/sections/tokens/TokensSectionForm.tsx @@ -3,7 +3,7 @@ import type { Dispatch, ReactNode, SetStateAction } from 'react' import type { TokenInfo } from '@cowprotocol/types' import { TokenListControl } from '../../../controls/TokenListControl' -import { CurrencyInputControl } from '../../../ui/controls/CurrencyInput/CurrencyInputControl' +import { CurrencyInputControl } from '../../../ui/inputs/CurrencyInput/CurrencyInputControl' import type { TokenListItem } from '../../../../configurator.types' import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' diff --git a/apps/widget-configurator/src/components/sidebar/sections/trade-setup/TradeSetupSectionForm.tsx b/apps/widget-configurator/src/components/sidebar/sections/trade-setup/TradeSetupSectionForm.tsx index 9fc0d819c39..8f565059ab0 100644 --- a/apps/widget-configurator/src/components/sidebar/sections/trade-setup/TradeSetupSectionForm.tsx +++ b/apps/widget-configurator/src/components/sidebar/sections/trade-setup/TradeSetupSectionForm.tsx @@ -6,8 +6,8 @@ import { useAvailableChains } from '@cowprotocol/common-hooks' import { isChainDeprecated, type SupportedChainId } from '@cowprotocol/cow-sdk' import { IS_IFRAME, TRADE_MODES_OPTIONS, TRADE_TYPE_OPTIONS } from '../../../../configurator.constants' -import { SelectInput, SelectInputOption } from '../../../ui/controls/Select/SelectInput' -import { SwitchInput } from '../../../ui/controls/SwitchInput/SwitchInput' +import { SelectInput, SelectInputOption } from '../../../ui/inputs/Select/SelectInput' +import { SwitchInput } from '../../../ui/inputs/SwitchInput/SwitchInput' import type { ConfiguratorFormChangeHandler, ConfiguratorFormValues } from '../section.types' diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index bce11e9608a..2480f5a6db7 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -319,11 +319,7 @@ export function Sidebar({ - [x] Move fields to individual panels. Pass one prop per value and one single callback that takes a ChangeEvent or name + value. - [x] Add name to all fields. - [x] Create reusable TextInput, NumberInput and SelectInput components. - - - [ ] Refactor patterns like this: - export interface CurrentTradeTypeControlProps { - state: [TradeType, Dispatch>] - } + - [x] Simply input change handling. - [ ] Further polish Select-based inputs and color inputs. Fix Number input with default value. Remove debug red/cyan backgrounds. - [ ] TokensDialog, Wagmi dialog, etc. sit below the sidebar handler. diff --git a/apps/widget-configurator/src/components/ui/controls/JsonInput/JsonInput.component.tsx b/apps/widget-configurator/src/components/ui/controls/JsonInput/JsonInput.component.tsx deleted file mode 100644 index ba18eb048a4..00000000000 --- a/apps/widget-configurator/src/components/ui/controls/JsonInput/JsonInput.component.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { BaseTextInput, BaseTextInputProps } from "../BaseTextInput/BaseTextInput.component"; -import { ReactNode } from "react"; - -export interface JsonInputProps extends Omit { - onChange: (name: string, value: string | null) => void; -} - -export function JsonInput({ - name, - value, - onChange, - onBlur, - ...props -}: JsonInputProps): ReactNode { - const handleChange = onChange ? (e: React.ChangeEvent): void => { - onChange(name, e.target.value || null); - } : undefined; - - const handleBlur = (e: React.FocusEvent): void => { - let formattedValue = e.target.value; - - try { - formattedValue = JSON.stringify(JSON.parse(e.target.value), null, 2); - } catch { - // Do nothing - } - - onChange(name, formattedValue || null); - if (onBlur) onBlur(e); - }; - - return ( - - ) -} diff --git a/apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.test.tsx b/apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.test.tsx deleted file mode 100644 index bb33314fdc8..00000000000 --- a/apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.test.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { render, screen } from '@testing-library/react' - -import { LocaleControl } from './LocaleControl' - -describe('LocaleControl', () => { - it('shows browser default when no locale is forced', () => { - render() - - expect(screen.getByRole('combobox').textContent).toContain('Browser default') - }) -}) diff --git a/apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.tsx b/apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.tsx deleted file mode 100644 index 7f838682a90..00000000000 --- a/apps/widget-configurator/src/components/ui/controls/Select/LocaleControl.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Dispatch, ReactNode, SetStateAction } from 'react' - -import { LOCALE_DISPLAY_NAMES, SupportedLocale, SUPPORTED_LOCALES } from '@cowprotocol/common-const' - -import { SelectInput } from './SelectInput' - -const LABEL = 'Forced locale' - -type LocaleControlState = [SupportedLocale | '', Dispatch>] - -export function LocaleControl({ state }: { state: LocaleControlState }): ReactNode { - const [locale, setLocale] = state - - return ( - ({ label: LOCALE_DISPLAY_NAMES[option] || option, value: option })), - ]} - onChange={(_, value) => { - if (Array.isArray(value)) return - setLocale(value as SupportedLocale | '') - }} - renderValue={(selected) => { - if (!selected || Array.isArray(selected)) { - return 'Browser default' - } - - return LOCALE_DISPLAY_NAMES[selected as SupportedLocale] || selected - }} - /> - ) -} diff --git a/apps/widget-configurator/src/components/ui/controls/Select/ModeControl.test.tsx b/apps/widget-configurator/src/components/ui/controls/Select/ModeControl.test.tsx deleted file mode 100644 index 2f08703afb7..00000000000 --- a/apps/widget-configurator/src/components/ui/controls/Select/ModeControl.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { fireEvent, render, screen } from '@testing-library/react' - -import { ModeControl } from './ModeControl' - -describe('ModeControl', () => { - it('shows tooltip text explaining each widget mode when the help icon is opened', () => { - render() - - const helpButton = screen.getByLabelText('Explain widget modes') - - fireEvent.click(helpButton) - - expect(screen.getByRole('tooltip')).not.toBeNull() - expect(screen.getByText(/dapp mode:/i)).not.toBeNull() - expect(screen.getByText(/standalone mode:/i)).not.toBeNull() - expect(screen.getByText(/host app provides the wallet connection/i)).not.toBeNull() - expect(screen.getByText(/widget uses its own wallet provider/i)).not.toBeNull() - }) -}) diff --git a/apps/widget-configurator/src/components/ui/controls/TextInput/TextInput.component.tsx b/apps/widget-configurator/src/components/ui/controls/TextInput/TextInput.component.tsx deleted file mode 100644 index a7001031fcc..00000000000 --- a/apps/widget-configurator/src/components/ui/controls/TextInput/TextInput.component.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { BaseTextInput, BaseTextInputProps } from "../BaseTextInput/BaseTextInput.component"; -import { ReactNode } from "react"; - -export interface TextInputProps extends Omit { - onChange: (name: string, value: string | null) => void; -} - -export function TextInput({ - name, - value, - onChange, - onBlur, - ...props -}: TextInputProps): ReactNode { - const handleChange = onChange ? (e: React.ChangeEvent): void => { - onChange(name, e.target.value || null); - } : undefined; - - const handleBlur = (e: React.FocusEvent): void => { - onChange(name, e.target.value.trim() || null); - if (onBlur) onBlur(e); - } - - return ( - - ) -} diff --git a/apps/widget-configurator/src/components/ui/controls/TextareaInput/TextareaInput.component.tsx b/apps/widget-configurator/src/components/ui/controls/TextareaInput/TextareaInput.component.tsx deleted file mode 100644 index 14368d9db0e..00000000000 --- a/apps/widget-configurator/src/components/ui/controls/TextareaInput/TextareaInput.component.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { BaseTextInput, BaseTextInputProps } from "../BaseTextInput/BaseTextInput.component"; -import { ReactNode } from "react"; - -export interface TextareaInputProps extends Omit { - onChange: (name: string, value: string | null) => void; -} - -export function TextareaInput({ - name, - value, - onChange, - onBlur, - ...props -}: TextareaInputProps): ReactNode { - const handleChange = onChange ? (e: React.ChangeEvent): void => { - onChange(name, e.target.value || null); - } : undefined; - - const handleBlur = (e: React.FocusEvent): void => { - onChange(name, e.target.value.trim() || null); - if (onBlur) onBlur(e); - } - - return ( - - ) -} diff --git a/apps/widget-configurator/src/components/ui/controls/BaseTextInput/BaseTextInput.component.tsx b/apps/widget-configurator/src/components/ui/inputs/BaseTextInput/BaseTextInput.component.tsx similarity index 100% rename from apps/widget-configurator/src/components/ui/controls/BaseTextInput/BaseTextInput.component.tsx rename to apps/widget-configurator/src/components/ui/inputs/BaseTextInput/BaseTextInput.component.tsx diff --git a/apps/widget-configurator/src/components/ui/controls/CurrencyInput/CurrencyInputControl.tsx b/apps/widget-configurator/src/components/ui/inputs/CurrencyInput/CurrencyInputControl.tsx similarity index 100% rename from apps/widget-configurator/src/components/ui/controls/CurrencyInput/CurrencyInputControl.tsx rename to apps/widget-configurator/src/components/ui/inputs/CurrencyInput/CurrencyInputControl.tsx diff --git a/apps/widget-configurator/src/components/ui/inputs/JsonInput/JsonInput.component.tsx b/apps/widget-configurator/src/components/ui/inputs/JsonInput/JsonInput.component.tsx new file mode 100644 index 00000000000..f29ac9126d5 --- /dev/null +++ b/apps/widget-configurator/src/components/ui/inputs/JsonInput/JsonInput.component.tsx @@ -0,0 +1,41 @@ +import { ReactNode } from 'react' + +import { BaseTextInput, BaseTextInputProps } from '../BaseTextInput/BaseTextInput.component' + +export interface JsonInputProps extends Omit { + onChange: (name: string, value: string | null) => void +} + +export function JsonInput({ name, value, onChange, onBlur, ...props }: JsonInputProps): ReactNode { + const handleChange = onChange + ? (e: React.ChangeEvent): void => { + onChange(name, e.target.value || null) + } + : undefined + + const handleBlur = (e: React.FocusEvent): void => { + let formattedValue = e.target.value + + try { + formattedValue = JSON.stringify(JSON.parse(e.target.value), null, 2) + } catch { + // Do nothing + } + + onChange(name, formattedValue || null) + if (onBlur) onBlur(e) + } + + return ( + + ) +} diff --git a/apps/widget-configurator/src/components/ui/controls/NumberInput/NumberInput.component.tsx b/apps/widget-configurator/src/components/ui/inputs/NumberInput/NumberInput.component.tsx similarity index 100% rename from apps/widget-configurator/src/components/ui/controls/NumberInput/NumberInput.component.tsx rename to apps/widget-configurator/src/components/ui/inputs/NumberInput/NumberInput.component.tsx diff --git a/apps/widget-configurator/src/components/ui/controls/PresetsButtons/PresetsButtons.component.tsx b/apps/widget-configurator/src/components/ui/inputs/PresetsButtons/PresetsButtons.component.tsx similarity index 100% rename from apps/widget-configurator/src/components/ui/controls/PresetsButtons/PresetsButtons.component.tsx rename to apps/widget-configurator/src/components/ui/inputs/PresetsButtons/PresetsButtons.component.tsx diff --git a/apps/widget-configurator/src/components/ui/controls/Select/SelectInput.tsx b/apps/widget-configurator/src/components/ui/inputs/Select/SelectInput.tsx similarity index 100% rename from apps/widget-configurator/src/components/ui/controls/Select/SelectInput.tsx rename to apps/widget-configurator/src/components/ui/inputs/Select/SelectInput.tsx diff --git a/apps/widget-configurator/src/components/ui/controls/SwitchInput/SwitchInput.test.tsx b/apps/widget-configurator/src/components/ui/inputs/SwitchInput/SwitchInput.test.tsx similarity index 100% rename from apps/widget-configurator/src/components/ui/controls/SwitchInput/SwitchInput.test.tsx rename to apps/widget-configurator/src/components/ui/inputs/SwitchInput/SwitchInput.test.tsx diff --git a/apps/widget-configurator/src/components/ui/controls/SwitchInput/SwitchInput.tsx b/apps/widget-configurator/src/components/ui/inputs/SwitchInput/SwitchInput.tsx similarity index 100% rename from apps/widget-configurator/src/components/ui/controls/SwitchInput/SwitchInput.tsx rename to apps/widget-configurator/src/components/ui/inputs/SwitchInput/SwitchInput.tsx diff --git a/apps/widget-configurator/src/components/ui/inputs/TextInput/TextInput.component.tsx b/apps/widget-configurator/src/components/ui/inputs/TextInput/TextInput.component.tsx new file mode 100644 index 00000000000..b2ee627523c --- /dev/null +++ b/apps/widget-configurator/src/components/ui/inputs/TextInput/TextInput.component.tsx @@ -0,0 +1,22 @@ +import { ReactNode } from 'react' + +import { BaseTextInput, BaseTextInputProps } from '../BaseTextInput/BaseTextInput.component' + +export interface TextInputProps extends Omit { + onChange: (name: string, value: string | null) => void +} + +export function TextInput({ name, value, onChange, onBlur, ...props }: TextInputProps): ReactNode { + const handleChange = onChange + ? (e: React.ChangeEvent): void => { + onChange(name, e.target.value || null) + } + : undefined + + const handleBlur = (e: React.FocusEvent): void => { + onChange(name, e.target.value.trim() || null) + if (onBlur) onBlur(e) + } + + return +} diff --git a/apps/widget-configurator/src/components/ui/inputs/TextareaInput/TextareaInput.component.tsx b/apps/widget-configurator/src/components/ui/inputs/TextareaInput/TextareaInput.component.tsx new file mode 100644 index 00000000000..a707cf36b35 --- /dev/null +++ b/apps/widget-configurator/src/components/ui/inputs/TextareaInput/TextareaInput.component.tsx @@ -0,0 +1,33 @@ +import { ReactNode } from 'react' + +import { BaseTextInput, BaseTextInputProps } from '../BaseTextInput/BaseTextInput.component' + +export interface TextareaInputProps extends Omit { + onChange: (name: string, value: string | null) => void +} + +export function TextareaInput({ name, value, onChange, onBlur, ...props }: TextareaInputProps): ReactNode { + const handleChange = onChange + ? (e: React.ChangeEvent): void => { + onChange(name, e.target.value || null) + } + : undefined + + const handleBlur = (e: React.FocusEvent): void => { + onChange(name, e.target.value.trim() || null) + if (onBlur) onBlur(e) + } + + return ( + + ) +} diff --git a/apps/widget-configurator/src/configurator.constants.ts b/apps/widget-configurator/src/configurator.constants.ts index 2f7b3025379..06e7f627a8a 100644 --- a/apps/widget-configurator/src/configurator.constants.ts +++ b/apps/widget-configurator/src/configurator.constants.ts @@ -1,8 +1,8 @@ -import { COW_CDN } from '@cowprotocol/common-const' +import { COW_CDN, SupportedLocale, LOCALE_DISPLAY_NAMES, SUPPORTED_LOCALES } from '@cowprotocol/common-const' import { CowWidgetEventListeners, CowWidgetEvents, ToastMessageType } from '@cowprotocol/events' import { CowSwapWidgetPaletteParams, TokenInfo, TradeType, WidgetHookEvents } from '@cowprotocol/widget-lib' -import { SelectInputOption } from './components/ui/controls/Select/SelectInput' +import { SelectInputOption } from './components/ui/inputs/Select/SelectInput' import { TokenListItem } from './configurator.types' // ENV: @@ -25,6 +25,11 @@ export const UTM_PARAMS = 'utm_content=cow-widget-configurator&utm_medium=web&ut // Form options: +export const LOCALE_OPTIONS: SelectInputOption[] = [ + { label: 'Browser default', value: '' }, + ...SUPPORTED_LOCALES.map((option) => ({ label: LOCALE_DISPLAY_NAMES[option] || option, value: option })), +] + export const TRADE_MODES = [TradeType.SWAP, TradeType.LIMIT, TradeType.ADVANCED, TradeType.YIELD] export const TRADE_MODES_OPTIONS: SelectInputOption[] = TRADE_MODES.map((option) => ({ From a3c9fcf9ec81b601f5c62b47a5ae626e0ea6839c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Tue, 2 Jun 2026 12:20:47 +0200 Subject: [PATCH 038/110] fix: fix sidebar drag handler being on top of modals --- .../sidebar/controls/sidebar-controls.styles.ts | 8 +++++--- .../src/components/sidebar/sidebar.component.tsx | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/widget-configurator/src/components/sidebar/controls/sidebar-controls.styles.ts b/apps/widget-configurator/src/components/sidebar/controls/sidebar-controls.styles.ts index 2e32e41cf92..b583c4e77d5 100644 --- a/apps/widget-configurator/src/components/sidebar/controls/sidebar-controls.styles.ts +++ b/apps/widget-configurator/src/components/sidebar/controls/sidebar-controls.styles.ts @@ -1,13 +1,15 @@ import { SxProps } from '@mui/material' import { Theme } from '@mui/material/styles' -export const sidebarControlsZeroWidthColumnSx: SxProps = { +export const sidebarControlsZeroWidthColumnSx: SxProps = (theme: Theme) => ({ position: 'relative', - zIndex: 2000, width: 0, height: '100%', flexShrink: 0, -} + + // Above the preview area and sidebar, below AppKit (`w3m-modal`) and MUI modals: + zIndex: theme.zIndex.drawer, +}) export const sidebarToggleOpenButton: SxProps = (theme: Theme) => ({ position: 'absolute', diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index 2480f5a6db7..5fd2da3a379 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -320,9 +320,9 @@ export function Sidebar({ - [x] Add name to all fields. - [x] Create reusable TextInput, NumberInput and SelectInput components. - [x] Simply input change handling. + - [x] TokensDialog, Wagmi dialog, etc. sit below the sidebar handler. - [ ] Further polish Select-based inputs and color inputs. Fix Number input with default value. Remove debug red/cyan backgrounds. - - [ ] TokensDialog, Wagmi dialog, etc. sit below the sidebar handler. - [ ] Add toggle to disable scrollbars. Auto-resize is now doing that automatically, but it should not. - [ ] Bug: when in dApp mode, reload the page with the wallet connected. You are connected outside, not within the widget. */ From c1ca869a5872c0509bd350d1e912d3fc2b23a051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Tue, 2 Jun 2026 16:52:55 +0200 Subject: [PATCH 039/110] fix: fix input label alignment and add custom tokens modal styles --- .../controls/AddCustomListDialog.tsx | 77 +++++++++++++------ .../components/sidebar/sidebar.component.tsx | 5 ++ .../BaseTextInput/BaseTextInput.component.tsx | 29 ++++++- 3 files changed, 86 insertions(+), 25 deletions(-) diff --git a/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx b/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx index 043b0044d6f..707a7397e48 100644 --- a/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx +++ b/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx @@ -3,12 +3,22 @@ import React, { ReactNode, useEffect, useState } from 'react' import { isValidTokenListSource } from '@cowprotocol/common-utils' import { Command, TokenInfo } from '@cowprotocol/types' -import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Tab, TextField } from '@mui/material' +import { Box, Button, Dialog, DialogActions, DialogContent, Tab, Typography } from '@mui/material' import Tabs from '@mui/material/Tabs' import { DEFAULT_CUSTOM_TOKENS } from '../../configurator.constants' import { parseCustomTokensInput } from '../../utils/parseCustomTokensInput' import { JsonInput } from '../ui/inputs/JsonInput/JsonInput.component' +import { TextInput } from '../ui/inputs/TextInput/TextInput.component' + +const DIALOG_PAPER_SX = { + backgroundColor: 'background.paper', + border: '1px solid rgba(255, 255, 255, 0.12)', + boxShadow: 'none', + backgroundImage: 'none', + minWidth: 600, + overflow: 'hidden', +} as const type AddCustomListDialogProps = { open: boolean @@ -58,13 +68,12 @@ export function AddCustomListDialog({ } // TODO: Add proper return type annotation - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - const handleUrlInputChange = (e: React.ChangeEvent) => { - const value = e.target.value - setCustomListUrl(value) + const handleUrlInputChange = (_name: string, value: string | null): void => { + const urlValue = value ?? '' - setHasErrors(value ? !isValidTokenListSource(value) : false) + setCustomListUrl(urlValue) + setHasErrors(urlValue ? !isValidTokenListSource(urlValue) : false) } // TODO: Add proper return type annotation @@ -119,27 +128,48 @@ export function AddCustomListDialog({ }, [customTokensDefault]) return ( - - Add Custom Token List - - - - - + + + + + Add Custom Token List + + + + + + + + + + - @@ -156,7 +186,8 @@ export function AddCustomListDialog({ - + + + - - + + ) } diff --git a/apps/widget-configurator/src/components/controls/PaletteControl.tsx b/apps/widget-configurator/src/components/controls/PaletteControl.tsx index 796d811ad9f..5bcc79fb07c 100644 --- a/apps/widget-configurator/src/components/controls/PaletteControl.tsx +++ b/apps/widget-configurator/src/components/controls/PaletteControl.tsx @@ -1,12 +1,13 @@ import { ReactNode, useState } from 'react' -import ExpandLessIcon from '@mui/icons-material/ExpandLess' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import { Button, Collapse, FormControl, Stack } from '@mui/material' +import { Box, Collapse, FormControl } from '@mui/material' import { MuiColorInput } from 'mui-color-input' +import { ChevronDown, ChevronUp } from 'react-feather' import { ColorPalette } from '../../configurator.types' import { ColorPaletteManager } from '../../hooks/useColorPaletteManager' +import { LinkButton } from '../ui/buttons/link/LinkButton.component' +import { SmallButton } from '../ui/buttons/small/SmallButton.component' const visibleColorKeys: Array = ['primary', 'paper', 'text'] @@ -35,21 +36,16 @@ export function PaletteControl({ paletteManager }: { paletteManager: ColorPalett ))} - - - - + /> + + ) } diff --git a/apps/widget-configurator/src/components/controls/TokenListControl.tsx b/apps/widget-configurator/src/components/controls/TokenListControl.tsx index 840a43e7e4c..7d104053e36 100644 --- a/apps/widget-configurator/src/components/controls/TokenListControl.tsx +++ b/apps/widget-configurator/src/components/controls/TokenListControl.tsx @@ -2,11 +2,13 @@ import { Dispatch, ReactNode, SetStateAction, useCallback, useMemo, useState } f import { TokenInfo } from '@cowprotocol/types' -import { Box, Button, Chip, ListItemText } from '@mui/material' +import { Box, Chip, ListItemText } from '@mui/material' +import { Plus } from 'react-feather' import { AddCustomListDialog } from './AddCustomListDialog' import { TokenListItem } from '../../configurator.types' +import { LinkButton } from '../ui/buttons/link/LinkButton.component' import { SelectInput } from '../ui/inputs/Select/SelectInput' const ITEM_HEIGHT = 48 @@ -166,9 +168,7 @@ export const TokenListControl = ({ tokenListUrlsState, customTokensState }: Toke onAddCustomTokens={setCustomTokens} /> - + setDialogOpen(true)} /> ) diff --git a/apps/widget-configurator/src/components/sidebar/footer/sidebar-footer.component.tsx b/apps/widget-configurator/src/components/sidebar/footer/sidebar-footer.component.tsx index d5bbe7220cd..25504640513 100644 --- a/apps/widget-configurator/src/components/sidebar/footer/sidebar-footer.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/footer/sidebar-footer.component.tsx @@ -1,14 +1,14 @@ import React, { ReactNode, useContext } from 'react' 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 { ChevronLeft, ChevronRight, Code, Eye, Moon, Sun, RefreshCw } from 'react-feather' import { UTM_PARAMS } from '../../../configurator.constants' import { ColorModeContext } from '../../../theme/ColorModeContext' +import { Button } from '../../ui/buttons/button/Button.component' +import { IconButton } from '../../ui/buttons/icon/IconButton.component' const WIDGET_WEB_URL = `https://cow.fi/widget/?${UTM_PARAMS}` const DEVELOPER_DOCS_URL = `https://docs.cow.fi/cow-protocol/tutorials/widget?${UTM_PARAMS}` @@ -56,14 +56,6 @@ export function SidebarFooter({ }, } as const - const iconOnlyButtonSx = { - borderRadius: 1, - border: '1px solid', - borderColor: 'divider', - height: 40, - width: 40, - } as const - let reloadPreviewLabel = '' if (isWidgetSyncPending) { @@ -106,42 +98,21 @@ export function SidebarFooter({ }} > + endIcon={SnippetIcon} + sx={{ mr: 'auto' }} + /> - - + /> - - - + - - - + diff --git a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx index 14952d76862..52377a3fe23 100644 --- a/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx +++ b/apps/widget-configurator/src/components/sidebar/sidebar.component.tsx @@ -330,6 +330,7 @@ export function Sidebar({ - [ ] Show iframe outline tooltip format is not the same as Mode. - [ ] Env tooltip format no the same as Mode. + - [ ] Timeout/error when loading/reloading widget/iframe. */ return ( diff --git a/apps/widget-configurator/src/components/snippet/snippet.component.tsx b/apps/widget-configurator/src/components/snippet/snippet.component.tsx index 2676a3fab43..268a29ebcfc 100644 --- a/apps/widget-configurator/src/components/snippet/snippet.component.tsx +++ b/apps/widget-configurator/src/components/snippet/snippet.component.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, SyntheticEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import React, { ReactNode, SyntheticEvent, useCallback, useEffect, useMemo, useState } from 'react' import { useCowAnalytics } from '@cowprotocol/analytics' import svgHtmlSrc from '@cowprotocol/assets/cow-swap/html.svg' @@ -12,11 +12,10 @@ import { CowSwapWidgetProps } from '@cowprotocol/widget-react' import { Tab } from '@mui/material' import MuiAlert, { AlertProps } from '@mui/material/Alert' import Box from '@mui/material/Box' -import Button from '@mui/material/Button' import Snackbar from '@mui/material/Snackbar' import { useTheme } from '@mui/material/styles' import Tabs from '@mui/material/Tabs' -import Typography from '@mui/material/Typography' +import { Copy } from 'react-feather' import SVG from 'react-inlinesvg' import SyntaxHighlighter from 'react-syntax-highlighter' // eslint-disable-next-line @typescript-eslint/no-restricted-imports @@ -29,6 +28,9 @@ import { tsExample } from './utils/tsExample' import { AnalyticsCategory } from '../../common/analytics/types' import { ColorPalette } from '../../configurator.types' +import { Button } from '../ui/buttons/button/Button.component' +import { ModalFooter } from '../ui/surface/modal/footer/ModalFooter.component' +import { ModalHeader } from '../ui/surface/modal/header/ModalHeader.component' interface TabInfo { id: number @@ -89,13 +91,14 @@ export interface SnippetProps { handleClose: Command } +const SNIPPET_CONTENT_PADDING = 16 + // TODO: Break down this large function into smaller functions // eslint-disable-next-line max-lines-per-function export function Snippet({ params, open, handleClose, defaultPalette }: SnippetProps): ReactNode { const theme = useTheme() const [tabInfo, setCurrentTabInfo] = useState(TABS[0]) const { id, language, snippetFromParams } = tabInfo - const descriptionElementRef = useRef(null) const cowAnalytics = useCowAnalytics() const [snackbarOpen, setSnackbarOpen] = useState(false) @@ -126,10 +129,6 @@ export function Snippet({ params, open, handleClose, defaultPalette }: SnippetPr category: AnalyticsCategory.WIDGET_CONFIGURATOR, action: 'View code', }) - const { current: descriptionElement } = descriptionElementRef - if (descriptionElement !== null) { - descriptionElement.focus() - } } }, [open, cowAnalytics]) @@ -150,52 +149,22 @@ export function Snippet({ params, open, handleClose, defaultPalette }: SnippetPr inset: 0, display: 'flex', flexDirection: 'column', - overflowY: 'auto', - overflowX: 'hidden', + overflow: 'hidden', minHeight: 0, backgroundColor: (t) => t.palette.background.paper, }} > - t.palette.background.paper, - borderBottom: 1, - borderColor: 'divider', }} - > - - - Snippet for CoW Widget - - - - - - - - + tabs={ - - + } + tabsSx={{ px: 1 }} + /> -
+ -
+
+ + + + onPresetClick(preset.value)} /> ))} ) diff --git a/apps/widget-configurator/src/components/ui/inputs/Select/SelectInput.tsx b/apps/widget-configurator/src/components/ui/inputs/Select/SelectInput.tsx index bfdd6047428..9f7590363ab 100644 --- a/apps/widget-configurator/src/components/ui/inputs/Select/SelectInput.tsx +++ b/apps/widget-configurator/src/components/ui/inputs/Select/SelectInput.tsx @@ -9,37 +9,18 @@ import InputLabel from '@mui/material/InputLabel' import MenuItem from '@mui/material/MenuItem' import Select, { SelectChangeEvent, SelectProps } from '@mui/material/Select' +import { configuratorMenuPaperSx } from '../../surface/surface.styles' import { BASE_TEXT_INPUT_HEIGHT } from '../BaseTextInput/BaseTextInput.component' type PrimitiveValue = string | number -const DEFAULT_MENU_PAPER_SX = { - backgroundColor: 'background.paper', - border: '1px solid rgba(255, 255, 255, 0.12)', - boxShadow: 'none', - backgroundImage: 'none', - '& .MuiMenuItem-root': { - backgroundColor: 'transparent !important', - }, - '& .MuiMenuItem-root.Mui-selected, & .MuiMenuItem-root.Mui-selected.Mui-focusVisible': { - backgroundColor: 'transparent !important', - }, - '& .MuiMenuItem-root.Mui-focusVisible': { - backgroundColor: 'transparent !important', - }, - '& .MuiMenuItem-root:hover, & .MuiMenuItem-root.Mui-selected:hover, & .MuiMenuItem-root.Mui-focusVisible:hover, & .MuiMenuItem-root.Mui-selected.Mui-focusVisible:hover': - { - backgroundColor: 'rgba(255, 255, 255, 0.06) !important', - }, -} as const - const NO_MENU_ANIMATION_PROPS: SelectProps['MenuProps'] = { transitionDuration: 0, TransitionProps: { timeout: 0, }, PaperProps: { - sx: DEFAULT_MENU_PAPER_SX, + sx: configuratorMenuPaperSx, }, } @@ -158,7 +139,7 @@ export function SelectInput({ PaperProps: { ...menuProps?.PaperProps, sx: [ - DEFAULT_MENU_PAPER_SX, + configuratorMenuPaperSx, ...(Array.isArray(menuProps?.PaperProps?.sx) ? menuProps.PaperProps.sx : menuProps?.PaperProps?.sx diff --git a/apps/widget-configurator/src/components/ui/surface/modal/footer/ModalFooter.component.tsx b/apps/widget-configurator/src/components/ui/surface/modal/footer/ModalFooter.component.tsx new file mode 100644 index 00000000000..5763097c2db --- /dev/null +++ b/apps/widget-configurator/src/components/ui/surface/modal/footer/ModalFooter.component.tsx @@ -0,0 +1,28 @@ +import { ReactNode } from 'react' + +import Box from '@mui/material/Box' + +export interface ModalFooterProps { + children: ReactNode +} + +export function ModalFooter({ children }: ModalFooterProps): ReactNode { + return ( + + {children} + + ) +} diff --git a/apps/widget-configurator/src/components/ui/surface/modal/header/ModalHeader.component.tsx b/apps/widget-configurator/src/components/ui/surface/modal/header/ModalHeader.component.tsx new file mode 100644 index 00000000000..1be823d479f --- /dev/null +++ b/apps/widget-configurator/src/components/ui/surface/modal/header/ModalHeader.component.tsx @@ -0,0 +1,59 @@ +import { ReactNode } from 'react' + +import Box from '@mui/material/Box' +import Typography from '@mui/material/Typography' +import { X } from 'react-feather' + +import { IconButton } from '../../../buttons/icon/IconButton.component' + +import type { SxProps, Theme } from '@mui/material/styles' + +export interface ModalHeaderProps { + titleId: string + title: string + onClose: () => void + tabs?: ReactNode + tabsSx?: SxProps + sx?: SxProps +} + +export function ModalHeader({ titleId, title, onClose, tabs, tabsSx, sx }: ModalHeaderProps): ReactNode { + return ( + + + + {title} + + + + + {tabs ? ( + + {tabs} + + ) : null} + + ) +} diff --git a/apps/widget-configurator/src/components/ui/surface/modal/modal.styles.ts b/apps/widget-configurator/src/components/ui/surface/modal/modal.styles.ts new file mode 100644 index 00000000000..d8b5d0c9be6 --- /dev/null +++ b/apps/widget-configurator/src/components/ui/surface/modal/modal.styles.ts @@ -0,0 +1,11 @@ +import { configuratorSurfacePaperSx } from '../surface.styles' + +import type { SxProps, Theme } from '@mui/material/styles' + +export const configuratorDialogPaperSx: SxProps = { + ...configuratorSurfacePaperSx, + overflow: 'hidden', + width: '100%', + maxWidth: 600, + m: 2, +} diff --git a/apps/widget-configurator/src/components/ui/surface/surface.styles.ts b/apps/widget-configurator/src/components/ui/surface/surface.styles.ts new file mode 100644 index 00000000000..b8845c34fd5 --- /dev/null +++ b/apps/widget-configurator/src/components/ui/surface/surface.styles.ts @@ -0,0 +1,28 @@ +import type { SxProps, Theme } from '@mui/material/styles' + +/** Shared paper surface for configurator dialogs and other elevated panels. */ +export const configuratorSurfacePaperSx: SxProps = { + backgroundColor: 'background.paper', + border: '1px solid', + borderColor: 'divider', + boxShadow: 'none', + backgroundImage: 'none', +} + +/** Select and menu dropdown paper. Extends {@link configuratorSurfacePaperSx} with menu item interaction styles. */ +export const configuratorMenuPaperSx: SxProps = { + ...configuratorSurfacePaperSx, + '& .MuiMenuItem-root': { + backgroundColor: 'transparent !important', + }, + '& .MuiMenuItem-root.Mui-selected, & .MuiMenuItem-root.Mui-selected.Mui-focusVisible': { + backgroundColor: 'transparent !important', + }, + '& .MuiMenuItem-root.Mui-focusVisible': { + backgroundColor: 'transparent !important', + }, + '& .MuiMenuItem-root:hover, & .MuiMenuItem-root.Mui-selected:hover, & .MuiMenuItem-root.Mui-focusVisible:hover, & .MuiMenuItem-root.Mui-selected.Mui-focusVisible:hover': + (theme) => ({ + backgroundColor: `${theme.palette.action.hover} !important`, + }), +} From 0cb12d39e56b307cc7cbc9cf6b750f6d95512271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=20G=C3=A1mez=20Franco?= Date: Tue, 2 Jun 2026 19:20:36 +0200 Subject: [PATCH 041/110] feat: improve tabs --- .../controls/AddCustomListDialog.tsx | 82 ++--- .../footer/sidebar-footer.component.tsx | 54 ++-- .../components/snippet/snippet.component.tsx | 281 ++++++++---------- .../components/snippet/utils/htmlExample.ts | 16 +- .../src/components/snippet/utils/jsExample.ts | 6 +- .../snippet/utils/reactTsExample.ts | 6 +- .../src/components/snippet/utils/tsExample.ts | 6 +- .../ui/buttons/icon/IconButton.component.tsx | 33 +- .../modal/header/ModalHeader.component.tsx | 20 +- .../modal/tabs/ModalTabPanel.component.tsx | 35 +++ .../modal/tabs/ModalTabs.component.tsx | 88 ++++++ .../ui/surface/modal/tabs/ModalTabs.styles.ts | 28 ++ libs/assets/src/cow-swap/ts.svg | 12 +- 13 files changed, 387 insertions(+), 280 deletions(-) create mode 100644 apps/widget-configurator/src/components/ui/surface/modal/tabs/ModalTabPanel.component.tsx create mode 100644 apps/widget-configurator/src/components/ui/surface/modal/tabs/ModalTabs.component.tsx create mode 100644 apps/widget-configurator/src/components/ui/surface/modal/tabs/ModalTabs.styles.ts diff --git a/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx b/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx index dad736fbb30..3cd3ca93e7f 100644 --- a/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx +++ b/apps/widget-configurator/src/components/controls/AddCustomListDialog.tsx @@ -1,9 +1,9 @@ -import React, { ReactNode, useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import { isValidTokenListSource } from '@cowprotocol/common-utils' import { Command, TokenInfo } from '@cowprotocol/types' -import { Dialog, DialogContent, Tab, Tabs } from '@mui/material' +import { Dialog, DialogContent } from '@mui/material' import { Plus } from 'react-feather' import { DEFAULT_CUSTOM_TOKENS } from '../../configurator.constants' @@ -14,6 +14,19 @@ import { TextInput } from '../ui/inputs/TextInput/TextInput.component' import { ModalFooter } from '../ui/surface/modal/footer/ModalFooter.component' import { ModalHeader } from '../ui/surface/modal/header/ModalHeader.component' import { configuratorDialogPaperSx } from '../ui/surface/modal/modal.styles' +import { ModalTabPanel } from '../ui/surface/modal/tabs/ModalTabPanel.component' +import { ModalLabelTabInfo, ModalTabs } from '../ui/surface/modal/tabs/ModalTabs.component' + +const ADD_CUSTOM_LIST_TABS_ID_PREFIX = 'add-custom-list' + +type AddCustomListTabId = 'url' | 'json' + +const ADD_CUSTOM_LIST_TABS = [ + { label: 'URL', value: 'url' }, + { label: 'JSON', value: 'json' }, +] as const satisfies ModalLabelTabInfo[] + +const DEFAULT_ADD_CUSTOM_LIST_TAB_ID = ADD_CUSTOM_LIST_TABS[0].value type AddCustomListDialogProps = { open: boolean @@ -40,7 +53,7 @@ export function AddCustomListDialog({ const [customTokens, setCustomTokens] = useState([]) - const [tabIndex, setTabIndex] = useState(0) + const [tabValue, setTabValue] = useState(DEFAULT_ADD_CUSTOM_LIST_TAB_ID) // TODO: Add proper return type annotation // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -57,8 +70,8 @@ export function AddCustomListDialog({ // TODO: Add proper return type annotation // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - const handleTabChange = (_: React.SyntheticEvent, newValue: number) => { - setTabIndex(newValue) + const handleTabChange = (_: React.SyntheticEvent, newValue: AddCustomListTabId) => { + setTabValue(newValue) resetForm() } @@ -131,26 +144,19 @@ export function AddCustomListDialog({ aria-labelledby="add-custom-token-list-title" PaperProps={{ sx: configuratorDialogPaperSx }} > - - - - - } - /> + + + - + - - + +