diff --git a/CHANGELOG.md b/CHANGELOG.md index 829cefe5..917f1784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- [Refactored side panel components](https://github.com/multiversx/mx-sdk-dapp-ui/pull/271) - [Fixed trimmed address not showing in address-table](https://github.com/multiversx/mx-sdk-dapp-ui/pull/273) - [Added minimize/maximize action for toasts](https://github.com/multiversx/mx-sdk-dapp-ui/pull/272) - [Eslint and prettierrc fixes](https://github.com/multiversx/mx-sdk-dapp-ui/pull/270) diff --git a/src/common/ProviderIdleScreen/ProviderIdleScreen.tsx b/src/common/ProviderIdleScreen/ProviderIdleScreen.tsx index 8c266ab1..93cbab78 100644 --- a/src/common/ProviderIdleScreen/ProviderIdleScreen.tsx +++ b/src/common/ProviderIdleScreen/ProviderIdleScreen.tsx @@ -4,6 +4,7 @@ import type { IProviderBase } from 'types/provider.types'; import { ProviderTypeEnum } from 'types/provider.types'; import { getProviderIntroText } from './helpers/getProviderIntroText'; +import { SidePanelHeader } from 'components/visual/SidePanel/components/SidePanelHeader/SidePanelHeader'; const styles = { container: 'mvx:flex mvx:flex-col mvx:flex-1 mvx:overflow-hidden', @@ -50,7 +51,7 @@ export function ProviderIdleScreen({ if (provider.type === ProviderTypeEnum.ledger) { return ( - + @@ -59,7 +60,7 @@ export function ProviderIdleScreen({ return (
- +
{providerIntroIcon}
diff --git a/src/components.d.ts b/src/components.d.ts index a9d86ead..390aed8c 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -16,7 +16,6 @@ import { ITransactionListItem as ITransactionListItem1 } from "./components/visu import { IToastDataState, ITransactionProgressState } from "./components/functional/toasts-list/components/transaction-toast/transaction-toast.type"; import { TransactionStatusEnum } from "./constants/transactionStatus.enum"; import { TransactionRowType } from "./components/controlled/transactions-table/transactions-table.type"; -import { IProviderBase } from "./types/provider.types"; import { IEventBus as IEventBus1, unknown as IWalletConnectPanelData } from "./components.d"; export { IAddressTableData } from "./types/address-table.types"; export { ButtonSizeEnum, ButtonVariantEnum } from "./common/Button/button.types"; @@ -29,7 +28,6 @@ export { ITransactionListItem as ITransactionListItem1 } from "./components/visu export { IToastDataState, ITransactionProgressState } from "./components/functional/toasts-list/components/transaction-toast/transaction-toast.type"; export { TransactionStatusEnum } from "./constants/transactionStatus.enum"; export { TransactionRowType } from "./components/controlled/transactions-table/transactions-table.type"; -export { IProviderBase } from "./types/provider.types"; export { IEventBus as IEventBus1, unknown as IWalletConnectPanelData } from "./components.d"; export namespace Components { interface MvxAddressTable { @@ -218,43 +216,6 @@ export namespace Components { interface MvxPreloader { "class"?: string; } - interface MvxSidePanel { - "hasBackButton"?: boolean; - /** - * @default false - */ - "isOpen": boolean; - "panelClassName"?: string; - "panelTitle": string; - /** - * @default true - */ - "showHeader"?: boolean; - } - interface MvxSidePanelHeader { - /** - * @default true - */ - "hasLeftButton"?: boolean; - /** - * @default true - */ - "hasRightButton"?: boolean; - "panelClassName"?: string; - "panelTitle": string; - } - interface MvxSidePanelSwiper { - "close": () => Promise; - /** - * @default false - */ - "open": boolean; - "openToSnapPoint": (snapIndex?: number) => Promise; - /** - * @default '' - */ - "sidePanelIdentifier": string; - } interface MvxSignTransactionsPanel { "closeWithAnimation": () => Promise; "getEventBus": () => Promise; @@ -344,14 +305,6 @@ export namespace Components { "dataTestId"?: string; "text": string; } - interface MvxUnlockButton { - "class"?: string; - "dataTestId"?: string; - "icon"?: HTMLElement; - "iconUrl": string; - "label": string; - "type"?: IProviderBase['type']; - } interface MvxUnlockPanel { "closeWithAnimation": () => Promise; "getEventBus": () => Promise; @@ -431,18 +384,6 @@ export interface MvxPaginationEllipsisFormCustomEvent extends CustomEvent detail: T; target: HTMLMvxPaginationEllipsisFormElement; } -export interface MvxSidePanelCustomEvent extends CustomEvent { - detail: T; - target: HTMLMvxSidePanelElement; -} -export interface MvxSidePanelHeaderCustomEvent extends CustomEvent { - detail: T; - target: HTMLMvxSidePanelHeaderElement; -} -export interface MvxSidePanelSwiperCustomEvent extends CustomEvent { - detail: T; - target: HTMLMvxSidePanelSwiperElement; -} export interface MvxSimpleToastCustomEvent extends CustomEvent { detail: T; target: HTMLMvxSimpleToastElement; @@ -741,60 +682,6 @@ declare global { prototype: HTMLMvxPreloaderElement; new (): HTMLMvxPreloaderElement; }; - interface HTMLMvxSidePanelElementEventMap { - "close": void; - "back": void; - } - interface HTMLMvxSidePanelElement extends Components.MvxSidePanel, HTMLStencilElement { - addEventListener(type: K, listener: (this: HTMLMvxSidePanelElement, ev: MvxSidePanelCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLMvxSidePanelElement, ev: MvxSidePanelCustomEvent) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; - } - var HTMLMvxSidePanelElement: { - prototype: HTMLMvxSidePanelElement; - new (): HTMLMvxSidePanelElement; - }; - interface HTMLMvxSidePanelHeaderElementEventMap { - "rightButtonClick": any; - "leftButtonClick": any; - } - interface HTMLMvxSidePanelHeaderElement extends Components.MvxSidePanelHeader, HTMLStencilElement { - addEventListener(type: K, listener: (this: HTMLMvxSidePanelHeaderElement, ev: MvxSidePanelHeaderCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLMvxSidePanelHeaderElement, ev: MvxSidePanelHeaderCustomEvent) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; - } - var HTMLMvxSidePanelHeaderElement: { - prototype: HTMLMvxSidePanelHeaderElement; - new (): HTMLMvxSidePanelHeaderElement; - }; - interface HTMLMvxSidePanelSwiperElementEventMap { - "sheetDismiss": void; - "sheetSnapChange": { index: number; snapPoint: string }; - } - interface HTMLMvxSidePanelSwiperElement extends Components.MvxSidePanelSwiper, HTMLStencilElement { - addEventListener(type: K, listener: (this: HTMLMvxSidePanelSwiperElement, ev: MvxSidePanelSwiperCustomEvent) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void; - addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLMvxSidePanelSwiperElement, ev: MvxSidePanelSwiperCustomEvent) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void; - removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void; - } - var HTMLMvxSidePanelSwiperElement: { - prototype: HTMLMvxSidePanelSwiperElement; - new (): HTMLMvxSidePanelSwiperElement; - }; interface HTMLMvxSignTransactionsPanelElement extends Components.MvxSignTransactionsPanel, HTMLStencilElement { } var HTMLMvxSignTransactionsPanelElement: { @@ -917,12 +804,6 @@ declare global { prototype: HTMLMvxTrimElement; new (): HTMLMvxTrimElement; }; - interface HTMLMvxUnlockButtonElement extends Components.MvxUnlockButton, HTMLStencilElement { - } - var HTMLMvxUnlockButtonElement: { - prototype: HTMLMvxUnlockButtonElement; - new (): HTMLMvxUnlockButtonElement; - }; interface HTMLMvxUnlockPanelElement extends Components.MvxUnlockPanel, HTMLStencilElement { } var HTMLMvxUnlockPanelElement: { @@ -1028,9 +909,6 @@ declare global { "mvx-passkey-provider-icon": HTMLMvxPasskeyProviderIconElement; "mvx-pending-transactions-panel": HTMLMvxPendingTransactionsPanelElement; "mvx-preloader": HTMLMvxPreloaderElement; - "mvx-side-panel": HTMLMvxSidePanelElement; - "mvx-side-panel-header": HTMLMvxSidePanelHeaderElement; - "mvx-side-panel-swiper": HTMLMvxSidePanelSwiperElement; "mvx-sign-transactions-panel": HTMLMvxSignTransactionsPanelElement; "mvx-simple-toast": HTMLMvxSimpleToastElement; "mvx-spinner-icon": HTMLMvxSpinnerIconElement; @@ -1044,7 +922,6 @@ declare global { "mvx-transaction-toast-progress": HTMLMvxTransactionToastProgressElement; "mvx-transactions-table": HTMLMvxTransactionsTableElement; "mvx-trim": HTMLMvxTrimElement; - "mvx-unlock-button": HTMLMvxUnlockButtonElement; "mvx-unlock-panel": HTMLMvxUnlockPanelElement; "mvx-wallet-connect": HTMLMvxWalletConnectElement; "mvx-wallet-connect-app-gallery-icon": HTMLMvxWalletConnectAppGalleryIconElement; @@ -1248,47 +1125,6 @@ declare namespace LocalJSX { interface MvxPreloader { "class"?: string; } - interface MvxSidePanel { - "hasBackButton"?: boolean; - /** - * @default false - */ - "isOpen"?: boolean; - "onBack"?: (event: MvxSidePanelCustomEvent) => void; - "onClose"?: (event: MvxSidePanelCustomEvent) => void; - "panelClassName"?: string; - "panelTitle"?: string; - /** - * @default true - */ - "showHeader"?: boolean; - } - interface MvxSidePanelHeader { - /** - * @default true - */ - "hasLeftButton"?: boolean; - /** - * @default true - */ - "hasRightButton"?: boolean; - "onLeftButtonClick"?: (event: MvxSidePanelHeaderCustomEvent) => void; - "onRightButtonClick"?: (event: MvxSidePanelHeaderCustomEvent) => void; - "panelClassName"?: string; - "panelTitle"?: string; - } - interface MvxSidePanelSwiper { - "onSheetDismiss"?: (event: MvxSidePanelSwiperCustomEvent) => void; - "onSheetSnapChange"?: (event: MvxSidePanelSwiperCustomEvent<{ index: number; snapPoint: string }>) => void; - /** - * @default false - */ - "open"?: boolean; - /** - * @default '' - */ - "sidePanelIdentifier"?: string; - } interface MvxSignTransactionsPanel { } interface MvxSimpleToast { @@ -1379,14 +1215,6 @@ declare namespace LocalJSX { "dataTestId"?: string; "text"?: string; } - interface MvxUnlockButton { - "class"?: string; - "dataTestId"?: string; - "icon"?: HTMLElement; - "iconUrl"?: string; - "label"?: string; - "type"?: IProviderBase['type']; - } interface MvxUnlockPanel { } interface MvxWalletConnect { @@ -1469,9 +1297,6 @@ declare namespace LocalJSX { "mvx-passkey-provider-icon": MvxPasskeyProviderIcon; "mvx-pending-transactions-panel": MvxPendingTransactionsPanel; "mvx-preloader": MvxPreloader; - "mvx-side-panel": MvxSidePanel; - "mvx-side-panel-header": MvxSidePanelHeader; - "mvx-side-panel-swiper": MvxSidePanelSwiper; "mvx-sign-transactions-panel": MvxSignTransactionsPanel; "mvx-simple-toast": MvxSimpleToast; "mvx-spinner-icon": MvxSpinnerIcon; @@ -1485,7 +1310,6 @@ declare namespace LocalJSX { "mvx-transaction-toast-progress": MvxTransactionToastProgress; "mvx-transactions-table": MvxTransactionsTable; "mvx-trim": MvxTrim; - "mvx-unlock-button": MvxUnlockButton; "mvx-unlock-panel": MvxUnlockPanel; "mvx-wallet-connect": MvxWalletConnect; "mvx-wallet-connect-app-gallery-icon": MvxWalletConnectAppGalleryIcon; @@ -1535,9 +1359,6 @@ declare module "@stencil/core" { "mvx-passkey-provider-icon": LocalJSX.MvxPasskeyProviderIcon & JSXBase.HTMLAttributes; "mvx-pending-transactions-panel": LocalJSX.MvxPendingTransactionsPanel & JSXBase.HTMLAttributes; "mvx-preloader": LocalJSX.MvxPreloader & JSXBase.HTMLAttributes; - "mvx-side-panel": LocalJSX.MvxSidePanel & JSXBase.HTMLAttributes; - "mvx-side-panel-header": LocalJSX.MvxSidePanelHeader & JSXBase.HTMLAttributes; - "mvx-side-panel-swiper": LocalJSX.MvxSidePanelSwiper & JSXBase.HTMLAttributes; "mvx-sign-transactions-panel": LocalJSX.MvxSignTransactionsPanel & JSXBase.HTMLAttributes; "mvx-simple-toast": LocalJSX.MvxSimpleToast & JSXBase.HTMLAttributes; "mvx-spinner-icon": LocalJSX.MvxSpinnerIcon & JSXBase.HTMLAttributes; @@ -1551,7 +1372,6 @@ declare module "@stencil/core" { "mvx-transaction-toast-progress": LocalJSX.MvxTransactionToastProgress & JSXBase.HTMLAttributes; "mvx-transactions-table": LocalJSX.MvxTransactionsTable & JSXBase.HTMLAttributes; "mvx-trim": LocalJSX.MvxTrim & JSXBase.HTMLAttributes; - "mvx-unlock-button": LocalJSX.MvxUnlockButton & JSXBase.HTMLAttributes; "mvx-unlock-panel": LocalJSX.MvxUnlockPanel & JSXBase.HTMLAttributes; "mvx-wallet-connect": LocalJSX.MvxWalletConnect & JSXBase.HTMLAttributes; "mvx-wallet-connect-app-gallery-icon": LocalJSX.MvxWalletConnectAppGalleryIcon & JSXBase.HTMLAttributes; diff --git a/src/components/common/unlock-button/unlock-button.tsx b/src/components/common/unlock-button/unlock-button.tsx deleted file mode 100644 index 8fb85306..00000000 --- a/src/components/common/unlock-button/unlock-button.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Component, h, Prop } from '@stencil/core'; -import { UnlockButton as UnlockButtonComponent } from 'common/UnlockButton/UnlockButton'; -import type { IProviderBase } from 'types/provider.types'; - -@Component({ - tag: 'mvx-unlock-button', -}) -export class UnlockButton { - @Prop() label: string; - @Prop() iconUrl: string; - @Prop() icon?: HTMLElement; - @Prop() dataTestId?: string; - @Prop() type?: IProviderBase['type']; - @Prop() class?: string; - - render() { - return ( - - ); - } -} diff --git a/src/components/common/format-amount/format-amount.scss b/src/components/controlled/format-amount/format-amount.scss similarity index 100% rename from src/components/common/format-amount/format-amount.scss rename to src/components/controlled/format-amount/format-amount.scss diff --git a/src/components/common/format-amount/format-amount.tsx b/src/components/controlled/format-amount/format-amount.tsx similarity index 100% rename from src/components/common/format-amount/format-amount.tsx rename to src/components/controlled/format-amount/format-amount.tsx diff --git a/src/components/common/format-amount/tests/format-amount.spec.ts b/src/components/controlled/format-amount/tests/format-amount.spec.ts similarity index 100% rename from src/components/common/format-amount/tests/format-amount.spec.ts rename to src/components/controlled/format-amount/tests/format-amount.spec.ts diff --git a/src/components/functional/ledger-connect/ledger-connect.tsx b/src/components/functional/ledger-connect/ledger-connect.tsx index bb7e44f4..a35c67fd 100644 --- a/src/components/functional/ledger-connect/ledger-connect.tsx +++ b/src/components/functional/ledger-connect/ledger-connect.tsx @@ -6,6 +6,7 @@ import { EventBus, type IEventBus } from 'utils/EventBus'; import { getLedgerAddressByIndex } from './helpers/getLedgerAddressByIndex'; import type { ILedgerConnectPanelData } from './ledger-connect.types'; import { LedgerConnectEventsEnum } from './ledger-connect.types'; +import { SidePanelHeader } from 'components/visual/SidePanel/components/SidePanelHeader/SidePanelHeader'; // prettier-ignore const styles = { @@ -89,7 +90,7 @@ export class LedgerConnect { if (this.ledgerDataState.accountScreenData) { return ( - - - * { + >* { @apply mvx:w-full mvx:max-w-full mvx:box-border; } -} +} \ No newline at end of file diff --git a/src/components/functional/notifications-feed/notifications-feed.tsx b/src/components/functional/notifications-feed/notifications-feed.tsx index a72a6453..064900b6 100644 --- a/src/components/functional/notifications-feed/notifications-feed.tsx +++ b/src/components/functional/notifications-feed/notifications-feed.tsx @@ -1,5 +1,6 @@ import { Component, h, Method, State } from '@stencil/core'; import { Icon } from 'common/Icon'; +import { SidePanel } from 'components/visual/SidePanel/SidePanel'; import { ConnectionMonitor } from 'utils/ConnectionMonitor'; import type { IEventBus } from 'utils/EventBus'; import { EventBus } from 'utils/EventBus'; @@ -7,6 +8,7 @@ import { EventBus } from 'utils/EventBus'; import type { ITransactionListItem } from '../../visual/transaction-list-item/transaction-list-item.types'; import type { ITransactionToast } from '../toasts-list/components/transaction-toast/transaction-toast.type'; import { NotificationsFeedEventsEnum } from './notifications-feed.types'; +import { ANIMATION_DELAY_PROMISE } from 'components/visual/SidePanel/sidePanel.constants'; @Component({ tag: 'mvx-notifications-feed', @@ -26,7 +28,7 @@ export class NotificationsFeed { @Method() async closeWithAnimation() { this.isOpen = false; - const animationDelay = await new Promise(resolve => setTimeout(resolve, 300)); + const animationDelay = await ANIMATION_DELAY_PROMISE; return animationDelay; } @@ -91,7 +93,7 @@ export class NotificationsFeed { const hasPending = this.pendingTransactions?.length > 0; return ( -
- + ); } } diff --git a/src/components/functional/pending-transactions-panel/pending-transactions-panel.scss b/src/components/functional/pending-transactions-panel/pending-transactions-panel.scss index 207cefb9..ff1343ee 100644 --- a/src/components/functional/pending-transactions-panel/pending-transactions-panel.scss +++ b/src/components/functional/pending-transactions-panel/pending-transactions-panel.scss @@ -28,4 +28,4 @@ align-items: center; flex: 1; padding: 16px; -} +} \ No newline at end of file diff --git a/src/components/functional/pending-transactions-panel/pending-transactions-panel.tsx b/src/components/functional/pending-transactions-panel/pending-transactions-panel.tsx index d438e3b9..5b34d5ce 100644 --- a/src/components/functional/pending-transactions-panel/pending-transactions-panel.tsx +++ b/src/components/functional/pending-transactions-panel/pending-transactions-panel.tsx @@ -1,6 +1,7 @@ import { Component, h, Method, State } from '@stencil/core'; import { ProviderIdleScreen } from 'common/ProviderIdleScreen/ProviderIdleScreen'; -import { ANIMATION_DELAY_PROMISE } from 'components/visual/side-panel/side-panel.constants'; +import { ANIMATION_DELAY_PROMISE } from 'components/visual/SidePanel/sidePanel.constants'; +import { SidePanel } from 'components/visual/SidePanel/SidePanel'; import type { IProviderBase } from 'types/provider.types'; import { ProviderTypeEnum } from 'types/provider.types'; import { ConnectionMonitor } from 'utils/ConnectionMonitor'; @@ -76,7 +77,11 @@ export class PendingTransactionsPanel { render() { return ( - + - + ); } } diff --git a/src/components/functional/pending-transactions-panel/tests/pending-transactions-panel.e2e.ts b/src/components/functional/pending-transactions-panel/tests/pending-transactions-panel.e2e.ts index c1bbafed..181ff800 100644 --- a/src/components/functional/pending-transactions-panel/tests/pending-transactions-panel.e2e.ts +++ b/src/components/functional/pending-transactions-panel/tests/pending-transactions-panel.e2e.ts @@ -21,12 +21,13 @@ describe('pending-transactions-panel', () => { }); page.rootInstance.provider = { name: title }; + page.rootInstance.isOpen = true; await page.waitForChanges(); - const panel = page.root.shadowRoot.querySelector('mvx-side-panel'); + const panel = page.root.shadowRoot.querySelector('#side-panel'); expect(panel).toBeTruthy(); - const panelTitle = panel.getAttribute('panelTitle'); - expect(panelTitle).toBe(title); + expect(page.rootInstance.provider?.name).toBe(title); + expect(page.rootInstance.isOpen).toBe(true); }); }); diff --git a/src/components/functional/sign-transactions-panel/sign-transactions-panel.scss b/src/components/functional/sign-transactions-panel/sign-transactions-panel.scss index 874140ed..265ab534 100644 --- a/src/components/functional/sign-transactions-panel/sign-transactions-panel.scss +++ b/src/components/functional/sign-transactions-panel/sign-transactions-panel.scss @@ -33,4 +33,4 @@ .trim-right { @apply mvx:select-none mvx:pointer-events-none mvx:inline mvx:text-base mvx:leading-5 mvx:text-clip; -} +} \ No newline at end of file diff --git a/src/components/functional/sign-transactions-panel/sign-transactions-panel.tsx b/src/components/functional/sign-transactions-panel/sign-transactions-panel.tsx index 1e9a1706..9730aa45 100644 --- a/src/components/functional/sign-transactions-panel/sign-transactions-panel.tsx +++ b/src/components/functional/sign-transactions-panel/sign-transactions-panel.tsx @@ -1,6 +1,7 @@ import { Component, h, Method, State } from '@stencil/core'; import { getCopyClickAction } from 'common/CopyButton/getCopyClickAction'; -import { ANIMATION_DELAY_PROMISE } from 'components/visual/side-panel/side-panel.constants'; +import { ANIMATION_DELAY_PROMISE } from 'components/visual/SidePanel/sidePanel.constants'; +import { SidePanel } from 'components/visual/SidePanel/SidePanel'; import { DataTestIdsEnum } from 'constants/dataTestIds.enum'; import { ConnectionMonitor } from 'utils/ConnectionMonitor'; import type { IEventBus } from 'utils/EventBus'; @@ -148,7 +149,7 @@ export class SignTransactionsPanel { const { currentIndex, transactionsCount, origin } = commonData; return ( - - + ); } } diff --git a/src/components/functional/unlock-panel/unlock-panel.scss b/src/components/functional/unlock-panel/unlock-panel.scss index 21b0cbc1..ff1190ee 100644 --- a/src/components/functional/unlock-panel/unlock-panel.scss +++ b/src/components/functional/unlock-panel/unlock-panel.scss @@ -1 +1 @@ -// This is needed to trigger the Stecil Tailwind compilation for inline Tailwind classes. +// This is needed to trigger the Stecil Tailwind compilation for inline Tailwind classes. \ No newline at end of file diff --git a/src/components/functional/unlock-panel/unlock-panel.tsx b/src/components/functional/unlock-panel/unlock-panel.tsx index 2e9ab875..e3402fd2 100644 --- a/src/components/functional/unlock-panel/unlock-panel.tsx +++ b/src/components/functional/unlock-panel/unlock-panel.tsx @@ -1,6 +1,7 @@ import { Component, Element, h, Method, State } from '@stencil/core'; import { ProviderIdleScreen } from 'common/ProviderIdleScreen/ProviderIdleScreen'; -import { ANIMATION_DELAY_PROMISE } from 'components/visual/side-panel/side-panel.constants'; +import { ANIMATION_DELAY_PROMISE } from 'components/visual/SidePanel/sidePanel.constants'; +import { SidePanel } from 'components/visual/SidePanel/SidePanel'; import type { IProviderBase } from 'types/provider.types'; import { ProviderTypeEnum } from 'types/provider.types'; import { ConnectionMonitor } from 'utils/ConnectionMonitor'; @@ -159,7 +160,7 @@ export class UnlockPanel { const isCustomProviderActive = this.selectedMethod && this.isCustomProvider(this.selectedMethod.type); return ( - )} - + ); } } diff --git a/src/components/functional/wallet-connect/wallet-connect.tsx b/src/components/functional/wallet-connect/wallet-connect.tsx index 8095cde9..ca715258 100644 --- a/src/components/functional/wallet-connect/wallet-connect.tsx +++ b/src/components/functional/wallet-connect/wallet-connect.tsx @@ -1,13 +1,13 @@ import { Component, Element, h, Host, Method, Prop, State, Watch } from '@stencil/core'; import { Icon } from 'common/Icon'; import type { IEventBus, IWalletConnectPanelData } from 'components'; -import { SidePanelHeaderSlotEnum } from 'components/visual/side-panel/components/side-panel-header/side-panel-header'; import { providerLabels } from 'constants/providerFactory.constants'; import QRCode from 'qrcode'; import { ConnectionMonitor } from 'utils/ConnectionMonitor'; import { EventBus } from 'utils/EventBus'; import { WalletConnectEventsEnum } from './wallet-connect.types'; +import { SidePanelHeader } from 'components/visual/SidePanel/components/SidePanelHeader/SidePanelHeader'; // prettier-ignore const styles = { @@ -86,16 +86,15 @@ export class WalletConnect { render() { return ( - this.eventBus.publish(WalletConnectEventsEnum.CLOSE)} - > - {!this.showScanPage && } - - + leftIcon={!this.showScanPage ? : undefined} + rightIcon={} + />
{this.showScanPage ? ( diff --git a/src/components/visual/SidePanel/SidePanel.tsx b/src/components/visual/SidePanel/SidePanel.tsx new file mode 100644 index 00000000..f3ee8acd --- /dev/null +++ b/src/components/visual/SidePanel/SidePanel.tsx @@ -0,0 +1,87 @@ +import { h } from '@stencil/core'; +import classNames from 'classnames'; +import { SidePanelHeader } from './components/SidePanelHeader/SidePanelHeader'; +import { SidePanelSwiper } from './components/SidePanelSwiper/SidePanelSwiper'; +import { handleSidePanelOpenChange } from './helpers/handleSidePanelOpenChange'; +import { state } from './sidePanelStore'; + +import styles from './sidePanel.styles'; + +interface SidePanelPropsType { + isOpen?: boolean; + panelClassName?: string; + panelTitle: string; + hasBackButton?: boolean; + showHeader?: boolean; + onClose?: () => void; + onBack?: () => void; +} + +export function SidePanel({ + isOpen = false, + panelClassName, + panelTitle, + hasBackButton, + showHeader = true, + onClose, + onBack +}: SidePanelPropsType, children: JSX.Element) { + if (isOpen !== undefined) { + handleSidePanelOpenChange(isOpen, (shouldAnimate) => { + state.shouldAnimate = shouldAnimate; + }); + } + + const sidePanelIdentifier = 'side-panel'; + const shouldAnimate = state.shouldAnimate; + + const handleOverlayClick = (event: MouseEvent) => { + if (event.target === event.currentTarget) { + onClose?.(); + } + }; + + const handleCloseClick = (event: MouseEvent) => { + event.preventDefault(); + onClose?.(); + }; + + const handleBackClick = (event: MouseEvent) => { + event.preventDefault(); + onBack?.(); + }; + + return ( +
+ +
+ {showHeader && ( + + )} + +
+ {children} +
+
+
+
+ ); +} + diff --git a/src/components/visual/SidePanel/components/SidePanelHeader/SidePanelHeader.tsx b/src/components/visual/SidePanel/components/SidePanelHeader/SidePanelHeader.tsx new file mode 100644 index 00000000..02d55ce4 --- /dev/null +++ b/src/components/visual/SidePanel/components/SidePanelHeader/SidePanelHeader.tsx @@ -0,0 +1,59 @@ +import { h } from '@stencil/core'; +import classNames from 'classnames'; +import { Icon } from 'common/Icon'; + +// prettier-ignore +const styles = { + sidePanelHeading: 'side-panel-heading mvx:flex mvx:items-center mvx:leading-none mvx:gap-3 mvx:z-1 mvx:relative mvx:px-0 mvx:py-2 mvx:text-2xl mvx:shadow-lg mvx:shadow-surface', + sidePanelHeadingLeft: 'side-panel-heading-left mvx:mr-auto mvx:hidden mvx:pointer-events-none mvx:cursor-pointer mvx:opacity-0 mvx:text-secondary-text mvx:xs:flex mvx:hover:opacity-75', + sidePanelHeadingRight: 'side-panel-heading-right mvx:ml-auto mvx:hidden mvx:pointer-events-none mvx:cursor-pointer mvx:opacity-0 mvx:text-secondary-text mvx:xs:flex mvx:hover:opacity-75', + sidePanelHeadingLeftVisible: 'side-panel-heading-left-visible mvx:transition-all mvx:opacity-100 mvx:duration-200 mvx:ease-in-out mvx:!pointer-events-auto mvx:flex mvx:hover:opacity-75', + sidePanelHeadingRightVisible: 'side-panel-heading-right-visible mvx:transition-all mvx:opacity-100 mvx:duration-200 mvx:ease-in-out mvx:!pointer-events-auto mvx:!flex mvx:hover:opacity-75', + sidePanelHeadingTitle: 'side-panel-heading-title mvx:font-medium mvx:text-primary' +} satisfies Record; + +interface SidePanelHeaderPropsType { + panelClassName?: string; + panelTitle: string; + hasLeftButton?: boolean; + hasRightButton?: boolean; + onRightButtonClick?: (event: MouseEvent) => void; + onLeftButtonClick?: (event: MouseEvent) => void; + leftIcon?: any; + rightIcon?: any; +} + +export function SidePanelHeader({ panelClassName, panelTitle, hasLeftButton = true, hasRightButton = true, onRightButtonClick, onLeftButtonClick, leftIcon, rightIcon }: SidePanelHeaderPropsType) { + const handleRightIconClick = (event: MouseEvent) => { + event.preventDefault(); + onRightButtonClick?.(event); + } + + const handleLeftIconClick = (event: MouseEvent) => { + event.preventDefault(); + onLeftButtonClick?.(event); + } + + return ( +
+
+ {hasLeftButton && ( + leftIcon || + )} +
+ +
{panelTitle}
+ +
+ {rightIcon || } +
+
+ ); +} + diff --git a/src/components/visual/SidePanel/components/SidePanelSwiper/SidePanelSwiper.tsx b/src/components/visual/SidePanel/components/SidePanelSwiper/SidePanelSwiper.tsx new file mode 100644 index 00000000..6589e8a0 --- /dev/null +++ b/src/components/visual/SidePanel/components/SidePanelSwiper/SidePanelSwiper.tsx @@ -0,0 +1,257 @@ +import { h } from '@stencil/core'; +import { state } from '../../sidePanelStore'; +import styles from './sidePanelSwiper.styles'; + +interface SidePanelSwiperPropsType { + open: boolean; + onSheetDismiss?: () => void; + onSheetSnapChange?: (index: number, snapPoint: string) => void; +} + +let hasInitialized = false; +let previousOpen: boolean | null = null; + +const snapPointsArray: string[] = ['100%']; +const SNAP_PERCENT_DEFAULT = '50'; +const OPEN_TIMEOUT_VALUE = 50; +const CLOSE_TTMEOUT_VALUE = 300; +const TRANSLATE_Y_VALUE = 100; +let sheetElement: HTMLElement | null = null; + +let dragState = { + startY: 0, + currentY: 0, + startTransform: 100, + isAnimating: false, +}; + +let isDragging = false; + +export function SidePanelSwiper({ open = false, onSheetDismiss, onSheetSnapChange }: SidePanelSwiperPropsType, children: JSX.Element) { + const handleSheetDismiss = () => { + onSheetDismiss?.(); + } + + const animateToPosition = (snapIndex: number, emitEvent: boolean = true) => { + if (!sheetElement || dragState.isAnimating) { + return; + } + + const snapPercent = parseFloat(snapPointsArray[snapIndex] || SNAP_PERCENT_DEFAULT); + const targetY = 100 - snapPercent; + + dragState.isAnimating = true; + dragState.startTransform = targetY; + + sheetElement.style.transition = 'transform 350ms cubic-bezier(0.25, 0.46, 0.45, 0.94)'; + sheetElement.style.transform = `translateY(${targetY}%)`; + + setTimeout(() => { + dragState.isAnimating = false; + if (emitEvent && state.isVisible) { + onSheetSnapChange?.(snapIndex, snapPointsArray[snapIndex]); + } + if (sheetElement) { + sheetElement.style.transition = ''; + } + }, 350); + } + + const openToSnapPoint = (snapIndex: number = 1) => { + if (dragState.isAnimating) { + return; + } + + state.currentSnapIndex = Math.max(0, Math.min(snapIndex, snapPointsArray.length - 1)); + state.isVisible = true; + + setTimeout(() => { + if (sheetElement && state.isVisible) { + animateToPosition(state.currentSnapIndex, false); + } + }, OPEN_TIMEOUT_VALUE); + } + + const animateToClose = () => { + if (!sheetElement || dragState.isAnimating) { + return; + } + + dragState.isAnimating = true; + sheetElement.style.transition = 'transform 300ms cubic-bezier(0.25, 0.46, 0.45, 0.94)'; + sheetElement.style.transform = 'translateY(100%)'; + + setTimeout(() => { + dragState.isAnimating = false; + state.isVisible = false; + handleSheetDismiss(); + if (sheetElement) { + sheetElement.style.transition = ''; + } + }, CLOSE_TTMEOUT_VALUE); + } + + const closeSwiper = () => { + if (dragState.isAnimating || !state.isVisible) { + return; + } + + animateToClose(); + } + + if (previousOpen !== null && previousOpen !== open) { + if (open && !state.isVisible) { + openToSnapPoint(state.currentSnapIndex); + } else if (!open && state.isVisible) { + closeSwiper(); + } + } + + previousOpen = open; + + const setSheetRef = (el: HTMLElement | null) => { + sheetElement = el; + + if (el && !hasInitialized) { + hasInitialized = true; + state.isVisible = open; + + if (window.innerWidth <= 480) { + el.style.transform = 'translateY(100%)'; + } + + if (open) { + openToSnapPoint(state.currentSnapIndex); + } + } + } + + + const handleDragStart = (e: MouseEvent | TouchEvent) => { + if (dragState.isAnimating) { + return; + } + + e.preventDefault(); + e.stopPropagation(); + + isDragging = true; + const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY; + dragState.startY = clientY; + dragState.currentY = clientY; + + // Get current transform + const transform = getCurrentTransform(); + dragState.startTransform = transform; + + // Add global event listeners + document.addEventListener('mousemove', handleDragMove, { + passive: false, + }); + document.addEventListener('touchmove', handleDragMove, { + passive: false, + }); + document.addEventListener('mouseup', handleDragEnd); + document.addEventListener('touchend', handleDragEnd); + }; + + const handleDragMove = (e: MouseEvent | TouchEvent) => { + if (!isDragging || !sheetElement || dragState.isAnimating) { + return; + } + + e.preventDefault(); + e.stopPropagation(); + + const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY; + dragState.currentY = clientY; + + const deltaY = dragState.currentY - dragState.startY; + const viewportHeight = window.innerHeight; + const deltaPercent = (deltaY / viewportHeight) * 100; + + const newTransform = Math.min(100, Math.max(0, dragState.startTransform + deltaPercent)); + + sheetElement.style.transform = `translateY(${newTransform}%)`; + }; + + const handleDragEnd = () => { + if (!isDragging || dragState.isAnimating) { + return; + } + + isDragging = false; + + // Remove global event listeners + document.removeEventListener('mousemove', handleDragMove); + document.removeEventListener('touchmove', handleDragMove); + document.removeEventListener('mouseup', handleDragEnd); + document.removeEventListener('touchend', handleDragEnd); + + const currentTransform = getCurrentTransform(); + const velocity = dragState.currentY - dragState.startY; + + // Close if dragged down significantly or fast downward velocity + if (currentTransform > 70 || velocity > 150) { + closeSwiper(); + return; + } + + // Find closest snap point + const snapPercentages = snapPointsArray.map(point => parseFloat(point)); + let closestIndex = 0; + let closestDistance = Math.abs(100 - currentTransform - snapPercentages[0]); + + for (let i = 1; i < snapPercentages.length; i++) { + const distance = Math.abs(100 - currentTransform - snapPercentages[i]); + if (distance < closestDistance) { + closestDistance = distance; + closestIndex = i; + } + } + + state.currentSnapIndex = closestIndex; + animateToPosition(closestIndex, true); + }; + + const getCurrentTransform = (): number => { + if (!sheetElement) { + return TRANSLATE_Y_VALUE; + } + + const transform = sheetElement.style.transform; + if (transform && transform.includes('translateY')) { + const match = transform.match(/translateY\(([^)]+)%?\)/); + if (match) { + return parseFloat(match[1].replace('%', '')); + } + } + return TRANSLATE_Y_VALUE; + } + return ( +
+
+
event.stopPropagation()} + > +
+
+
+
+
+ +
+ {children} +
+
+
+
+ ); +} + diff --git a/src/components/visual/SidePanel/components/SidePanelSwiper/sidePanelSwiper.styles.ts b/src/components/visual/SidePanel/components/SidePanelSwiper/sidePanelSwiper.styles.ts new file mode 100644 index 00000000..4affe250 --- /dev/null +++ b/src/components/visual/SidePanel/components/SidePanelSwiper/sidePanelSwiper.styles.ts @@ -0,0 +1,13 @@ +// prettier-ignore +export default { + sidePanelSwiperContainer: 'side-panel-swipper-container mvx:flex mvx:xs:flex-col mvx:xs:h-full', + sidePanelSwiperWrapper: 'side-panel-swipper-wrapper mvx:fixed mvx:left-0 mvx:top-0 mvx:bottom-0 mvx:right-0 mvx:z-50 mvx:xs:static mvx:xs:h-full mvx:before:opacity-90 mvx:before:left-0 mvx:before:top-0 mvx:before:right-0 mvx:before:bottom-0 mvx:before:transition-all mvx:before:duration-200 mvx:before:pointer-events-none mvx:before:absolute mvx:before:ease-in-out mvx:before:bg-neutral-900 mvx:before:content-[""] mvx:before:supports-[backdrop-filter]:opacity-50 mvx:before:supports-[backdrop-filter]:backdrop-blur-sm mvx:before:supports-[backdrop-filter]:bg-neutral-900 mvx:xs:before:content-none', + sidePanelSwiperWrapperVisible: 'side-panel-swiper-visible mvx:!flex', + sidePanelSwiperWrapperHidden: 'side-panel-swiper-wrapper-hidden mvx:hidden mvx:xs:block', + sidePanelSwiperHidden: 'side-panel-swiper-hidden mvx:translate-y-full', + sidePanelSwiper: 'side-panel-swiper mvx:bottom-0 mvx:absolute mvx:left-0 mvx:right-0 mvx:flex mvx:flex-col mvx:justify-end mvx:touch-pan-y mvx:h-auto mvx:min-h-dvh mvx:rounded-t-3xl mvx:transition-none mvx:backface-hidden mvx:will-change-transform mvx:xs:h-full mvx:xs:static mvx:xs:rounded-none mvx:xs:transform-none mvx:xs:[justify-content:unset] mvx:xs:min-h-auto', + sidePanelSwiperHandleWrapper: 'side-panel-swiper-handle-wrapper mvx:top-8 mvx:relative mvx:h-8 mvx:w-full mvx:z-12 mvx:xs:hidden', + sidePanelSwiperHandleContainer: 'side-panel-swiper-handle-container mvx:flex mvx:top-0 mvx:bottom-0 mvx:absolute mvx:right-0 mvx:left-0 mvx:justify-center mvx:touch-none mvx:select-none mvx:cursor-grab mvx:active:cursor-grabbing', + sidePanelSwiperHandle: 'side-panel-swiper-handle mvx:w-32 mvx:mt-3 mvx:h-1 mvx:rounded mvx:bg-primary', + sidePanelSwiperContent: 'side-panel-swiper-content mvx:overflow-y-auto mvx:max-h-[calc(100dvh-4rem)] mvx:xs:max-h-none mvx:xs:h-full' +} satisfies Record; \ No newline at end of file diff --git a/src/components/visual/SidePanel/helpers/handleSidePanelOpenChange.ts b/src/components/visual/SidePanel/helpers/handleSidePanelOpenChange.ts new file mode 100644 index 00000000..4945590b --- /dev/null +++ b/src/components/visual/SidePanel/helpers/handleSidePanelOpenChange.ts @@ -0,0 +1,10 @@ +export function handleSidePanelOpenChange( + isOpen: boolean, + setShouldAnimate: (value: boolean) => void +) { + if (isOpen) { + requestAnimationFrame(() => setShouldAnimate(true)); + } else { + setShouldAnimate(false); + } +} \ No newline at end of file diff --git a/src/components/visual/side-panel/side-panel.constants.ts b/src/components/visual/SidePanel/sidePanel.constants.ts similarity index 100% rename from src/components/visual/side-panel/side-panel.constants.ts rename to src/components/visual/SidePanel/sidePanel.constants.ts diff --git a/src/components/visual/SidePanel/sidePanel.styles.ts b/src/components/visual/SidePanel/sidePanel.styles.ts new file mode 100644 index 00000000..05b9e8c9 --- /dev/null +++ b/src/components/visual/SidePanel/sidePanel.styles.ts @@ -0,0 +1,8 @@ +// prettier-ignore +export default { + sidePanelWrapper: 'side-panel-wrapper mvx:flex mvx:justify-end mvx:items-start mvx:z-50 mvx:pointer-events-none mvx:invisible mvx:xs:fixed mvx:xs:top-0 mvx:xs:left-0 mvx:xs:right-0 mvx:xs:bottom-0 mvx:xs:p-4 mvx:xs:pr-0 mvx:xs:items-center mvx:before:opacity-0 mvx:before:left-0 mvx:before:top-0 mvx:before:right-0 mvx:before:bottom-0 mvx:before:transition-all mvx:before:absolute mvx:before:duration-200 mvx:before:pointer-events-none mvx:before:ease-in-out mvx:before:bg-neutral-900 mvx:before:content-[""] mvx:before:supports-[backdrop-filter]:opacity-50 mvx:before:supports-[backdrop-filter]:backdrop-blur-sm mvx:before:supports-[backdrop-filter]:bg-neutral-900', + sidePanelWrapperVisible: 'side-panel-wrapper-visible mvx:!pointer-events-auto mvx:!visible mvx:before:!opacity-90 mvx:before:supports-[backdrop-filter]:!opacity-50', + sidePanel: 'side-panel mvx:p-6 mvx:w-full mvx:flex mvx:overflow-hidden mvx:flex-col mvx:transition-all mvx:ease-in-out mvx:duration-200 mvx:rounded-tl-3xl mvx:rounded-tr-3xl mvx:backdrop-blur mvx:pb-0 mvx:border mvx:border-outline mvx:bg-surface mvx:xs:w-110 mvx:xs:h-full mvx:xs:mr-4 mvx:xs:rounded-[20px] mvx:xs:translate-x-[calc(100%+48px)] mvx:after:left-0 mvx:after:right-0 mvx:after:h-0 mvx:after:absolute mvx:after:shadow-lg mvx:after:shadow-surface mvx:after:-bottom-1 mvx:after:content-[""]', + sidePanelVisible: 'side-panel-visible mvx:!transform mvx:!translate-y-0 mvx:xs:!translate-x-0', + sidePanelContent: 'side-panel-content mvx:flex-1 mvx:flex mvx:flex-col mvx:overflow-auto mvx:scrollbar-hide' +} satisfies Record; \ No newline at end of file diff --git a/src/components/visual/SidePanel/sidePanelStore.ts b/src/components/visual/SidePanel/sidePanelStore.ts new file mode 100644 index 00000000..8c03d4f3 --- /dev/null +++ b/src/components/visual/SidePanel/sidePanelStore.ts @@ -0,0 +1,23 @@ +import { createStore } from '@stencil/store'; + +interface ISidePanelState { + isVisible: boolean; + currentSnapIndex: number; + shouldAnimate: boolean; +} + +const initialState: ISidePanelState = { + isVisible: false, + currentSnapIndex: 1, + shouldAnimate: false +} + +const store = createStore({ + ...initialState, +}); + +export const state = store.state; + +export const resetState = () => { + Object.assign(state, initialState); +}; diff --git a/src/components/common/button/button.scss b/src/components/visual/button/button.scss similarity index 100% rename from src/components/common/button/button.scss rename to src/components/visual/button/button.scss diff --git a/src/components/common/button/button.stories.tsx b/src/components/visual/button/button.stories.tsx similarity index 100% rename from src/components/common/button/button.stories.tsx rename to src/components/visual/button/button.stories.tsx diff --git a/src/components/common/button/button.tsx b/src/components/visual/button/button.tsx similarity index 100% rename from src/components/common/button/button.tsx rename to src/components/visual/button/button.tsx diff --git a/src/components/common/copy-button/copy-button.stories.tsx b/src/components/visual/copy-button/copy-button.stories.tsx similarity index 100% rename from src/components/common/copy-button/copy-button.stories.tsx rename to src/components/visual/copy-button/copy-button.stories.tsx diff --git a/src/components/common/copy-button/copy-button.tsx b/src/components/visual/copy-button/copy-button.tsx similarity index 100% rename from src/components/common/copy-button/copy-button.tsx rename to src/components/visual/copy-button/copy-button.tsx diff --git a/src/components/common/copy-button/tests/copy-button.spec.ts b/src/components/visual/copy-button/tests/copy-button.spec.ts similarity index 100% rename from src/components/common/copy-button/tests/copy-button.spec.ts rename to src/components/visual/copy-button/tests/copy-button.spec.ts diff --git a/src/components/visual/data-with-explorer-link/tests/data-with-explorer-link.spec.ts b/src/components/visual/data-with-explorer-link/tests/data-with-explorer-link.spec.ts index f59c8fb4..2c794d05 100644 --- a/src/components/visual/data-with-explorer-link/tests/data-with-explorer-link.spec.ts +++ b/src/components/visual/data-with-explorer-link/tests/data-with-explorer-link.spec.ts @@ -1,9 +1,9 @@ import { newSpecPage } from '@stencil/core/testing'; import { Trim } from 'common/Trim/Trim'; -import { CopyButton } from '../../../common/copy-button/copy-button'; -import { ExplorerLink } from '../../../common/explorer-link/explorer-link'; -import { Tooltip } from '../../../common/tooltip/tooltip'; +import { ExplorerLink } from '../../explorer-link/explorer-link'; +import { CopyButton } from '../../copy-button/copy-button'; +import { Tooltip } from '../../tooltip/tooltip'; import { DataWithExplorerLink } from '../data-with-explorer-link'; describe('DataWithExplorerLink', () => { diff --git a/src/components/common/explorer-link/explorer-link.scss b/src/components/visual/explorer-link/explorer-link.scss similarity index 100% rename from src/components/common/explorer-link/explorer-link.scss rename to src/components/visual/explorer-link/explorer-link.scss diff --git a/src/components/common/explorer-link/explorer-link.stories.tsx b/src/components/visual/explorer-link/explorer-link.stories.tsx similarity index 100% rename from src/components/common/explorer-link/explorer-link.stories.tsx rename to src/components/visual/explorer-link/explorer-link.stories.tsx diff --git a/src/components/common/explorer-link/explorer-link.tsx b/src/components/visual/explorer-link/explorer-link.tsx similarity index 100% rename from src/components/common/explorer-link/explorer-link.tsx rename to src/components/visual/explorer-link/explorer-link.tsx diff --git a/src/components/common/explorer-link/tests/explorer-link.spec.ts b/src/components/visual/explorer-link/tests/explorer-link.spec.ts similarity index 100% rename from src/components/common/explorer-link/tests/explorer-link.spec.ts rename to src/components/visual/explorer-link/tests/explorer-link.spec.ts diff --git a/src/components/visual/side-panel/components/side-panel-header/side-panel-header.scss b/src/components/visual/side-panel/components/side-panel-header/side-panel-header.scss deleted file mode 100644 index 2650a4f1..00000000 --- a/src/components/visual/side-panel/components/side-panel-header/side-panel-header.scss +++ /dev/null @@ -1,39 +0,0 @@ -.side-panel-heading { - @apply mvx:flex mvx:items-center mvx:leading-none mvx:gap-3 mvx:z-1 mvx:relative mvx:px-0 mvx:py-2 mvx:text-2xl; - box-shadow: 0px 8px 12px 12px var(--mvx-bg-color-primary); - - .side-panel-heading-left { - @apply mvx:mr-auto; - } - - .side-panel-heading-right { - @apply mvx:ml-auto; - } - - .side-panel-heading-left, - .side-panel-heading-right { - @apply mvx:hidden mvx:pointer-events-none mvx:cursor-pointer mvx:opacity-0; - color: var(--mvx-text-color-secondary); - - @media (min-width: 480px) { - @apply mvx:flex; - } - - &:hover { - @apply mvx:opacity-75; - } - - &.visible { - @apply mvx:transition-all mvx:opacity-100 mvx:duration-200 mvx:ease-in-out mvx:pointer-events-auto mvx:flex; - - &:hover { - @apply mvx:opacity-75; - } - } - } - - .side-panel-heading-title { - @apply mvx:font-medium; - color: var(--mvx-text-color-primary); - } -} diff --git a/src/components/visual/side-panel/components/side-panel-header/side-panel-header.tsx b/src/components/visual/side-panel/components/side-panel-header/side-panel-header.tsx deleted file mode 100644 index d82059e2..00000000 --- a/src/components/visual/side-panel/components/side-panel-header/side-panel-header.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import type { EventEmitter } from '@stencil/core'; -import { Component, Event, h, Prop } from '@stencil/core'; -import { Icon } from 'common/Icon'; - -export enum SidePanelHeaderSlotEnum { - leftIcon = 'left-icon', - rightIcon = 'right-icon', -} - -@Component({ - tag: 'mvx-side-panel-header', - styleUrl: 'side-panel-header.scss', - shadow: true, -}) -export class SidePanelHeader { - @Prop() panelClassName?: string; - @Prop() panelTitle: string; - @Prop() hasLeftButton?: boolean = true; - @Prop() hasRightButton?: boolean = true; - - @Event({ composed: false, bubbles: false }) rightButtonClick: EventEmitter; - @Event({ composed: false, bubbles: false }) leftButtonClick: EventEmitter; - - handleRightIconClick(event: MouseEvent) { - event.preventDefault(); - this.rightButtonClick.emit(); - } - - handleLeftIconClick(event: MouseEvent) { - event.preventDefault(); - this.leftButtonClick.emit(); - } - - render() { - return ( -
-
- {this.hasLeftButton && ( - - - - )} -
- -
{this.panelTitle}
- -
- - - -
-
- ); - } -} diff --git a/src/components/visual/side-panel/components/side-panel-swiper/side-panel-swiper.scss b/src/components/visual/side-panel/components/side-panel-swiper/side-panel-swiper.scss deleted file mode 100644 index 46b31a27..00000000 --- a/src/components/visual/side-panel/components/side-panel-swiper/side-panel-swiper.scss +++ /dev/null @@ -1,93 +0,0 @@ -:host { - @apply mvx:flex; - - @media (min-width: 480px) { - @apply mvx:flex-col mvx:h-full; - } - - .side-panel-swiper-wrapper { - @apply mvx:fixed mvx:left-0 mvx:top-0 mvx:bottom-0 mvx:right-0 mvx:z-50; - - @media (min-width: 480px) { - @apply mvx:static mvx:h-full; - } - - &:before { - @apply mvx:opacity-90 mvx:left-0 mvx:top-0 mvx:right-0 mvx:bottom-0 mvx:transition-all mvx:duration-200; - @apply mvx:pointer-events-none mvx:absolute mvx:ease-in-out; - background: var(--mvx-neutral-900); - content: ''; - - @supports ((-webkit-backdrop-filter: none) or (backdrop-filter: none)) { - @apply mvx:opacity-50; - background: var(--mvx-neutral-900); - -webkit-backdrop-filter: blur(0.375rem); - backdrop-filter: blur(0.375rem); - } - - @media (min-width: 480px) { - content: none; - } - } - - &.visible { - @apply mvx:block; - } - - &.hidden { - @apply mvx:hidden; - - @media (min-width: 480px) { - @apply mvx:block; - } - - .side-panel-swiper { - @apply mvx:translate-y-full; - } - } - - .side-panel-swiper { - @apply mvx:bottom-0 mvx:absolute mvx:left-0 mvx:right-0 mvx:flex mvx:flex-col mvx:justify-end mvx:touch-pan-y; - @apply mvx:h-auto mvx:min-h-dvh mvx:rounded-t-3xl mvx:transition-none; - backface-visibility: hidden; - will-change: transform; - - @media (min-width: 480px) { - @apply mvx:h-full mvx:static mvx:rounded-none mvx:transform-none; - justify-content: unset; - min-height: auto; - } - - .side-panel-swiper-handle-wrapper { - @apply mvx:top-8 mvx:relative mvx:h-8 mvx:w-full mvx:z-12; - - @media (min-width: 480px) { - @apply mvx:hidden; - } - - .side-panel-swiper-handle-container { - @apply mvx:flex mvx:top-0 mvx:bottom-0 mvx:absolute mvx:right-0 mvx:left-0 mvx:justify-center; - @apply mvx:touch-none mvx:select-none mvx:cursor-grab; - - &:active { - @apply mvx:cursor-grabbing; - } - - .side-panel-swiper-handle { - @apply mvx:w-32 mvx:mt-3 mvx:h-1 mvx:rounded; - background-color: var(--mvx-text-color-primary); - } - } - } - - .side-panel-swiper-content { - @apply mvx:overflow-y-auto; - max-height: calc(100dvh - 4rem); - - @media (min-width: 480px) { - @apply mvx:max-h-none mvx:h-full; - } - } - } - } -} diff --git a/src/components/visual/side-panel/components/side-panel-swiper/side-panel-swiper.tsx b/src/components/visual/side-panel/components/side-panel-swiper/side-panel-swiper.tsx deleted file mode 100644 index 26a2810e..00000000 --- a/src/components/visual/side-panel/components/side-panel-swiper/side-panel-swiper.tsx +++ /dev/null @@ -1,248 +0,0 @@ -import type { EventEmitter } from '@stencil/core'; -import { Component, Element, Event, h, Method, Prop, State, Watch } from '@stencil/core'; - -@Component({ - tag: 'mvx-side-panel-swiper', - styleUrl: 'side-panel-swiper.scss', - shadow: true, -}) -export class SidePanelSwiper { - @Element() sidePanelSwipeElement!: HTMLElement; - - @Prop() open: boolean = false; - @Prop() sidePanelIdentifier: string = ''; - - @Event() sheetDismiss!: EventEmitter; - @Event() sheetSnapChange!: EventEmitter<{ index: number; snapPoint: string }>; - - @State() isVisible: boolean = false; - @State() isDragging: boolean = false; - @State() currentSnapIndex: number = 1; - - private snapPointsArray: string[] = ['100%']; - private sheetElement!: HTMLElement; - - private dragState = { - startY: 0, - currentY: 0, - startTransform: 100, - isAnimating: false, - }; - - componentDidLoad() { - this.isVisible = this.open; - - if (this.sheetElement && window.innerWidth <= 480) { - this.sheetElement.style.transform = 'translateY(100%)'; - } - - if (this.open) { - this.openToSnapPoint(this.currentSnapIndex); - } - } - - @Watch('open') - openChanged(newValue: boolean) { - if (newValue && !this.isVisible) { - this.openToSnapPoint(this.currentSnapIndex); - } else if (!newValue && this.isVisible) { - this.close(); - } - } - - @Method() - async openToSnapPoint(snapIndex: number = 1) { - if (this.dragState.isAnimating) { - return; - } - - this.currentSnapIndex = Math.max(0, Math.min(snapIndex, this.snapPointsArray.length - 1)); - this.isVisible = true; - - await new Promise(resolve => setTimeout(resolve, 50)); - - if (this.sheetElement && this.isVisible) { - this.animateToPosition(this.currentSnapIndex, false); - } - } - - @Method() - async close() { - if (this.dragState.isAnimating || !this.isVisible) { - return; - } - - this.animateToClose(); - } - - private animateToPosition(snapIndex: number, emitEvent: boolean = true) { - if (!this.sheetElement || this.dragState.isAnimating) { - return; - } - - const snapPercent = parseFloat(this.snapPointsArray[snapIndex] || '50'); - const targetY = 100 - snapPercent; - - this.dragState.isAnimating = true; - this.dragState.startTransform = targetY; - - this.sheetElement.style.transition = 'transform 350ms cubic-bezier(0.25, 0.46, 0.45, 0.94)'; - this.sheetElement.style.transform = `translateY(${targetY}%)`; - - setTimeout(() => { - this.dragState.isAnimating = false; - if (emitEvent && this.isVisible) { - this.sheetSnapChange.emit({ - index: snapIndex, - snapPoint: this.snapPointsArray[snapIndex], - }); - } - this.sheetElement.style.transition = ''; - }, 350); - } - - private animateToClose() { - if (!this.sheetElement || this.dragState.isAnimating) { - return; - } - - this.dragState.isAnimating = true; - this.sheetElement.style.transition = 'transform 300ms cubic-bezier(0.25, 0.46, 0.45, 0.94)'; - this.sheetElement.style.transform = 'translateY(100%)'; - - setTimeout(() => { - this.dragState.isAnimating = false; - this.isVisible = false; - this.sheetDismiss.emit(); - this.sheetElement.style.transition = ''; - }, 300); - } - - private handleDragStart = (e: MouseEvent | TouchEvent) => { - if (this.dragState.isAnimating) { - return; - } - - e.preventDefault(); - e.stopPropagation(); - - this.isDragging = true; - const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY; - this.dragState.startY = clientY; - this.dragState.currentY = clientY; - - // Get current transform - const transform = this.getCurrentTransform(); - this.dragState.startTransform = transform; - - // Add global event listeners - document.addEventListener('mousemove', this.handleDragMove, { - passive: false, - }); - document.addEventListener('touchmove', this.handleDragMove, { - passive: false, - }); - document.addEventListener('mouseup', this.handleDragEnd); - document.addEventListener('touchend', this.handleDragEnd); - }; - - private handleDragMove = (e: MouseEvent | TouchEvent) => { - if (!this.isDragging || !this.sheetElement || this.dragState.isAnimating) { - return; - } - - e.preventDefault(); - e.stopPropagation(); - - const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY; - this.dragState.currentY = clientY; - - const deltaY = this.dragState.currentY - this.dragState.startY; - const viewportHeight = window.innerHeight; - const deltaPercent = (deltaY / viewportHeight) * 100; - - const newTransform = Math.min(100, Math.max(0, this.dragState.startTransform + deltaPercent)); - - this.sheetElement.style.transform = `translateY(${newTransform}%)`; - }; - - private handleDragEnd = () => { - if (!this.isDragging || this.dragState.isAnimating) { - return; - } - - this.isDragging = false; - - // Remove global event listeners - document.removeEventListener('mousemove', this.handleDragMove); - document.removeEventListener('touchmove', this.handleDragMove); - document.removeEventListener('mouseup', this.handleDragEnd); - document.removeEventListener('touchend', this.handleDragEnd); - - const currentTransform = this.getCurrentTransform(); - const velocity = this.dragState.currentY - this.dragState.startY; - - // Close if dragged down significantly or fast downward velocity - if (currentTransform > 70 || velocity > 150) { - this.close(); - return; - } - - // Find closest snap point - const snapPercentages = this.snapPointsArray.map(point => parseFloat(point)); - let closestIndex = 0; - let closestDistance = Math.abs(100 - currentTransform - snapPercentages[0]); - - for (let i = 1; i < snapPercentages.length; i++) { - const distance = Math.abs(100 - currentTransform - snapPercentages[i]); - if (distance < closestDistance) { - closestDistance = distance; - closestIndex = i; - } - } - - this.currentSnapIndex = closestIndex; - this.animateToPosition(closestIndex, true); - }; - - private getCurrentTransform(): number { - if (!this.sheetElement) { - return 100; - } - - const transform = this.sheetElement.style.transform; - if (transform && transform.includes('translateY')) { - const match = transform.match(/translateY\(([^)]+)%?\)/); - if (match) { - return parseFloat(match[1].replace('%', '')); - } - } - return 100; - } - - render() { - return ( -
-
(this.sheetElement = el!)} - onClick={(event: MouseEvent) => event.stopPropagation()} - > -
-
-
-
-
- -
- -
-
-
- ); - } -} diff --git a/src/components/visual/side-panel/side-panel.scss b/src/components/visual/side-panel/side-panel.scss deleted file mode 100644 index dc995c7f..00000000 --- a/src/components/visual/side-panel/side-panel.scss +++ /dev/null @@ -1,70 +0,0 @@ -.side-panel-wrapper { - @apply mvx:flex mvx:justify-end mvx:items-start mvx:z-50; - @apply mvx:pointer-events-none mvx:invisible; - - @media (min-width: 480px) { - @apply mvx:fixed mvx:top-0 mvx:left-0 mvx:right-0 mvx:bottom-0 mvx:p-4 mvx:pr-0 mvx:items-center; - } - - &:before { - @apply mvx:opacity-0 mvx:left-0 mvx:top-0 mvx:right-0 mvx:bottom-0 mvx:transition-all mvx:absolute mvx:duration-200; - @apply mvx:pointer-events-none mvx:ease-in-out; - background: var(--mvx-neutral-900); - content: ''; - - @supports ((-webkit-backdrop-filter: none) or (backdrop-filter: none)) { - @apply mvx:opacity-50; - background: var(--mvx-neutral-900); - -webkit-backdrop-filter: blur(0.375rem); - backdrop-filter: blur(0.375rem); - } - } - - &.visible { - @apply mvx:pointer-events-auto mvx:visible; - - &:before { - @apply mvx:opacity-90; - - @supports ((-webkit-backdrop-filter: none) or (backdrop-filter: none)) { - @apply mvx:opacity-50; - } - } - } - - .side-panel { - @apply mvx:p-6 mvx:w-full mvx:flex mvx:overflow-hidden mvx:flex-col mvx:transition-all mvx:ease-in-out mvx:duration-200; - @apply mvx:rounded-tl-3xl mvx:rounded-tr-3xl mvx:backdrop-blur mvx:pb-0; - border: 1px solid var(--mvx-border-color-primary); - background-color: var(--mvx-bg-color-primary); - - @media (min-width: 480px) { - @apply mvx:w-110 mvx:h-full mvx:mr-4; - transform: translateX(calc(100% + 48px)); - border-radius: 20px; - } - - &:after { - @apply mvx:left-0 mvx:right-0 mvx:h-0 mvx:absolute; - box-shadow: 0px -8px 12px 16px var(--mvx-bg-color-primary); - bottom: calc(4px * -1); - content: ''; - } - - &.visible { - @apply mvx:transform mvx:translate-y-0; - - @media (min-width: 480px) { - @apply mvx:translate-x-0; - } - } - } - - .side-panel-content { - @apply mvx:flex-1 mvx:flex mvx:flex-col mvx:overflow-auto; - - &::-webkit-scrollbar { - @apply mvx:hidden; - } - } -} diff --git a/src/components/visual/side-panel/side-panel.tsx b/src/components/visual/side-panel/side-panel.tsx deleted file mode 100644 index 55eee387..00000000 --- a/src/components/visual/side-panel/side-panel.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import type { EventEmitter } from '@stencil/core'; -import { Component, Element, Event, h, Prop, State, Watch } from '@stencil/core'; -import classNames from 'classnames'; - -@Component({ - tag: 'mvx-side-panel', - styleUrl: 'side-panel.scss', - shadow: true, -}) -export class SidePanel { - @Element() el: HTMLElement; - - @Prop() isOpen: boolean = false; - @Prop() panelClassName?: string; - @Prop() panelTitle: string; - @Prop() hasBackButton?: boolean; - @Prop() showHeader?: boolean = true; - - @Event() close: EventEmitter; - @Event() back: EventEmitter; - - @State() isVisible: boolean = false; - @State() shouldAnimate: boolean = false; - - private closeTimeout: NodeJS.Timeout | null = null; - private readonly ANIMATION_DURATION_MS = 300; - private sidePanelIdentifier = 'side-panel'; - - @Watch('isOpen') - handleOpenChange(newValue: boolean) { - if (newValue) { - clearTimeout(this.closeTimeout); - this.isVisible = true; - requestAnimationFrame(() => { - this.shouldAnimate = true; - }); - - return; - } - - this.shouldAnimate = false; - this.closeTimeout = setTimeout(() => { - this.isVisible = false; - }, this.ANIMATION_DURATION_MS); - } - - disconnectedCallback() { - clearTimeout(this.closeTimeout); - this.isVisible = false; - this.shouldAnimate = false; - this.closeTimeout = null; - } - - handleOverlayClick(event: MouseEvent) { - if (event.target === event.currentTarget) { - this.close.emit(); - } - } - - handleCloseClick(event: MouseEvent) { - event.preventDefault(); - this.close.emit(); - } - - handleBackClick(event: MouseEvent) { - event.preventDefault(); - this.back.emit(); - } - - render() { - return ( -
- this.close.emit()} - sidePanelIdentifier={this.sidePanelIdentifier} - > -
- {this.showHeader && ( - - )} - -
- -
-
-
-
- ); - } -} diff --git a/src/components/common/tooltip/tooltip.scss b/src/components/visual/tooltip/tooltip.scss similarity index 100% rename from src/components/common/tooltip/tooltip.scss rename to src/components/visual/tooltip/tooltip.scss diff --git a/src/components/common/tooltip/tooltip.tsx b/src/components/visual/tooltip/tooltip.tsx similarity index 100% rename from src/components/common/tooltip/tooltip.tsx rename to src/components/visual/tooltip/tooltip.tsx diff --git a/src/components/common/trim/tests/trim.e2e.tsx b/src/components/visual/trim/tests/trim.e2e.tsx similarity index 100% rename from src/components/common/trim/tests/trim.e2e.tsx rename to src/components/visual/trim/tests/trim.e2e.tsx diff --git a/src/components/common/trim/trim.scss b/src/components/visual/trim/trim.scss similarity index 100% rename from src/components/common/trim/trim.scss rename to src/components/visual/trim/trim.scss diff --git a/src/components/common/trim/trim.stories.tsx b/src/components/visual/trim/trim.stories.tsx similarity index 100% rename from src/components/common/trim/trim.stories.tsx rename to src/components/visual/trim/trim.stories.tsx diff --git a/src/components/common/trim/trim.tsx b/src/components/visual/trim/trim.tsx similarity index 100% rename from src/components/common/trim/trim.tsx rename to src/components/visual/trim/trim.tsx