diff --git a/package-lock.json b/package-lock.json index 7117c9a6..8310dc45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-toast": "^1.1.4", + "@radix-ui/react-toggle-group": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.6", "@skip-router/core": "^0.1.0-rc20", "@tanstack/react-query": "^4.29.5", @@ -5356,6 +5357,37 @@ } } }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", + "integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", @@ -5408,6 +5440,60 @@ } } }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz", + "integrity": "sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.0.4.tgz", + "integrity": "sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-toggle": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tooltip": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.6.tgz", @@ -24459,6 +24545,23 @@ "@radix-ui/react-slot": "1.0.2" } }, + "@radix-ui/react-roving-focus": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", + "integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1" + } + }, "@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", @@ -24488,6 +24591,32 @@ "@radix-ui/react-visually-hidden": "1.0.3" } }, + "@radix-ui/react-toggle": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz", + "integrity": "sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + } + }, + "@radix-ui/react-toggle-group": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.0.4.tgz", + "integrity": "sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-toggle": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + } + }, "@radix-ui/react-tooltip": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.6.tgz", diff --git a/package.json b/package.json index 37c993af..db83f755 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-toast": "^1.1.4", + "@radix-ui/react-toggle-group": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.6", "@skip-router/core": "^0.1.0-rc20", "@tanstack/react-query": "^4.29.5", diff --git a/src/components/HistoryButton.tsx b/src/components/HistoryButton.tsx index 824e9804..10854aa4 100644 --- a/src/components/HistoryButton.tsx +++ b/src/components/HistoryButton.tsx @@ -1,3 +1,4 @@ +import * as Tooltip from "@radix-ui/react-tooltip"; import { clsx } from "clsx"; import { ComponentProps } from "react"; @@ -10,17 +11,33 @@ export const HistoryButton = ({ ...props }: ComponentProps<"button">) => { return ( - + + + + + + + Transaction History + + + + ); }; diff --git a/src/components/HistoryClearButton.tsx b/src/components/HistoryDialog/HistoryClearButton.tsx similarity index 95% rename from src/components/HistoryClearButton.tsx rename to src/components/HistoryDialog/HistoryClearButton.tsx index 19632616..faac39d5 100644 --- a/src/components/HistoryClearButton.tsx +++ b/src/components/HistoryDialog/HistoryClearButton.tsx @@ -13,7 +13,7 @@ export const HistoryClearButton = ({ className, ...props }: Props) => { if (!hasHistory) return null; return ( - + <> + + + + Swap Settings + + + + + ); +}; diff --git a/src/components/SettingsDialog/SaveIndicator.tsx b/src/components/SettingsDialog/SaveIndicator.tsx new file mode 100644 index 00000000..9ee550a6 --- /dev/null +++ b/src/components/SettingsDialog/SaveIndicator.tsx @@ -0,0 +1,29 @@ +import { CheckIcon } from "@heroicons/react/20/solid"; +import { clsx } from "clsx"; +import { useEffect, useRef, useState } from "react"; + +import { useSettingsStore } from "@/context/settings"; + +export const SaveIndicator = () => { + const timeoutRef = useRef(null); + const [show, setShow] = useState(() => false); + + useEffect(() => { + return useSettingsStore.subscribe(() => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + setShow(true); + timeoutRef.current = window.setTimeout(() => setShow(false), 2000); + }); + }, []); + + return ( +
+ Settings saved! +
+ ); +}; diff --git a/src/components/SettingsDialog/SlippageSetting.tsx b/src/components/SettingsDialog/SlippageSetting.tsx new file mode 100644 index 00000000..e1018a8f --- /dev/null +++ b/src/components/SettingsDialog/SlippageSetting.tsx @@ -0,0 +1,50 @@ +import { clsx } from "clsx"; + +import { useSettingsStore } from "@/context/settings"; + +const OPTION_VALUES = ["1", "3", "5"]; + +export const SlippageSetting = () => { + const currentValue = useSettingsStore((state) => state.slippage); + + return ( +
+

Slippage

+
+
+
+ { + useSettingsStore.setState({ slippage: event.target.value }); + }} + /> +
+ % +
+
+
+ {OPTION_VALUES.map((value, i) => ( + + ))} +
+
+
+ ); +}; diff --git a/src/components/SettingsDialog/index.tsx b/src/components/SettingsDialog/index.tsx new file mode 100644 index 00000000..66f04b1a --- /dev/null +++ b/src/components/SettingsDialog/index.tsx @@ -0,0 +1,35 @@ +import { ArrowLeftIcon } from "@heroicons/react/20/solid"; + +import { useDisclosureKey } from "@/context/disclosures"; + +import { SaveIndicator } from "./SaveIndicator"; +import { SlippageSetting } from "./SlippageSetting"; + +export const SettingsDialog = () => { + const [isOpen, { close }] = useDisclosureKey("settingsDialog"); + + if (!isOpen) return null; + + return ( +
+
+
+ +

Swap Settings

+
+ +
+ +

+ Slippage is how much price movement you can tolerate between the time + you send out a transaction and the time it's executed. +

+
+
+ ); +}; diff --git a/src/components/SwapWidget/SwapWidget.tsx b/src/components/SwapWidget/SwapWidget.tsx index 1c6702e4..3eb2fa29 100644 --- a/src/components/SwapWidget/SwapWidget.tsx +++ b/src/components/SwapWidget/SwapWidget.tsx @@ -1,4 +1,5 @@ import { ArrowsUpDownIcon } from "@heroicons/react/20/solid"; +import * as Tooltip from "@radix-ui/react-tooltip"; import { FC, Fragment } from "react"; import { useChains as useSkipChains } from "@/api/queries"; @@ -12,6 +13,8 @@ import { HistoryDialog } from "../HistoryDialog"; import { JsonDialog } from "../JsonDialog"; import RouteLoadingBanner from "../RouteLoadingBanner"; import RouteTransactionCountBanner from "../RouteTransactionCountBanner"; +import { SettingsButton } from "../SettingsButton"; +import { SettingsDialog } from "../SettingsDialog"; import TransactionDialog from "../TransactionDialog"; import { useWalletModal, WalletModal } from "../WalletModal"; import { useSwapWidget } from "./useSwapWidget"; @@ -62,12 +65,14 @@ export const SwapWidget: FC = () => { return ( -
+

From

+ +
{address && wallet && isSourceWalletConnected ? ( { )}
+ -
+ ); diff --git a/src/components/TransactionDialog/TransactionDialogContent.tsx b/src/components/TransactionDialog/TransactionDialogContent.tsx index 4c91f17a..0cc4f1fa 100644 --- a/src/components/TransactionDialog/TransactionDialogContent.tsx +++ b/src/components/TransactionDialog/TransactionDialogContent.tsx @@ -6,6 +6,7 @@ import { FC, Fragment, useMemo, useState } from "react"; import { useAccount } from "wagmi"; import { Chain, useChains } from "@/api/queries"; +import { useSettingsStore } from "@/context/settings"; import { useToast } from "@/context/toast"; import { addTxHistory, @@ -139,6 +140,7 @@ const TransactionDialogContent: FC = ({ route, userAddresses, validateGasBalance: true, + slippageTolerancePercent: useSettingsStore.getState().slippage, getCosmosSigner: async (chainID) => { const chain = chains.find((c) => c.chainID === chainID); if (!chain) { diff --git a/src/context/disclosures.ts b/src/context/disclosures.ts index 3ba73704..0fccef2a 100644 --- a/src/context/disclosures.ts +++ b/src/context/disclosures.ts @@ -2,6 +2,7 @@ import { create } from "zustand"; const defaultValues = { historyDialog: false, + settingsDialog: false, // TODO: port dialogs to new system // assetSelect: false, diff --git a/src/context/settings.ts b/src/context/settings.ts new file mode 100644 index 00000000..8550f259 --- /dev/null +++ b/src/context/settings.ts @@ -0,0 +1,16 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +interface SettingsStore { + slippage: string; +} + +export const defaultValues: SettingsStore = { + slippage: "3", +}; + +export const useSettingsStore = create()( + persist(() => defaultValues, { + name: "SettingsStore", + }), +); diff --git a/src/styles/globals.css b/src/styles/globals.css index 79949617..880d3f90 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -2,19 +2,26 @@ @tailwind components; @tailwind utilities; +.number-input-arrows-hide::-webkit-outer-spin-button, +.number-input-arrows-hide::-webkit-inner-spin-button, +.number-input-arrows-hide { + -webkit-appearance: none; + margin: 0; + -moz-appearance: textfield !important; +} + .scrollbar-hide::-webkit-scrollbar { display: none; } /* For IE, Edge and Firefox */ .scrollbar-hide { - -ms-overflow-style: none; /* IE and Edge */ - scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ } -.DialogContent[data-state='open'] { +.DialogContent[data-state="open"] { animation: fadeIn 300ms cubic-bezier(0.16, 1, 0.3, 1); - } .TooltipContent { @@ -22,25 +29,25 @@ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); } -.TooltipContent[data-side='top'] { +.TooltipContent[data-side="top"] { animation-name: slideUp; } -.TooltipContent[data-side='bottom'] { +.TooltipContent[data-side="bottom"] { animation-name: slideDown; } -.ToastRoot[data-state='open'] { +.ToastRoot[data-state="open"] { animation: slideUp 300ms cubic-bezier(0.16, 1, 0.3, 1); } -.ToastRoot[data-state='closed'] { +.ToastRoot[data-state="closed"] { animation: swipeOut 300ms cubic-bezier(0.16, 1, 0.3, 1); } @keyframes fadeIn { from { opacity: 0; - scale: 0.90; + scale: 0.9; } to { scale: 1; @@ -48,7 +55,6 @@ } } - @keyframes slideDown { from { /* opacity: 0; */ @@ -89,4 +95,4 @@ to { transform: translateX(-142px); } -} \ No newline at end of file +} diff --git a/tailwind.config.js b/tailwind.config.js index bb48c9d9..963b0e69 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,4 +1,4 @@ -// eslint-disable-next-line @typescript-eslint/no-var-requires +/* eslint-disable @typescript-eslint/no-var-requires */ const plugin = require("tailwindcss/plugin"); /** @type {import('tailwindcss').Config} */